DEV-012: M22环节7- 性能优化+骨架屏+首屏加速+预加载
This commit is contained in:
parent
57478c0721
commit
c969a7ac56
|
|
@ -4,6 +4,10 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake公告栏</title>
|
||||
<!-- 关键资源预加载 -->
|
||||
<link rel="preload" href="style.css" as="style">
|
||||
<link rel="preload" href="i18n.js" as="script">
|
||||
<link rel="preload" href="script.js" as="script">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -22,13 +26,32 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 频道标签栏(用 nav 包裹) -->
|
||||
<!-- 频道标签栏 -->
|
||||
<nav class="channel-bar" role="tablist">
|
||||
<button class="channel-tab active" data-channel="全部" role="tab" data-i18n="all">全部</button>
|
||||
<button class="channel-tab" data-channel="系统" role="tab" data-i18n="system">系统公告</button>
|
||||
<button class="channel-tab" data-channel="知秋" role="tab" data-i18n="dev">开发动态</button>
|
||||
<button class="channel-tab" data-channel="技术" role="tab" data-i18n="team">团队消息</button>
|
||||
</nav>
|
||||
<button class="channel-tab active" data-channel="全部" role="tab" data-i18n="all">全部</button>
|
||||
<button class="channel-tab" data-channel="系统" role="tab" data-i18n="system">系统公告</button>
|
||||
<button class="channel-tab" data-channel="知秋" role="tab" data-i18n="dev">开发动态</button>
|
||||
<button class="channel-tab" data-channel="技术" role="tab" data-i18n="team">团队消息</button>
|
||||
</nav>
|
||||
|
||||
<!-- 骨架屏容器(独立于公告容器) -->
|
||||
<div id="skeleton-container" class="skeleton-wrapper">
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text short"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告主内容区 -->
|
||||
<main class="bulletin-container" role="list"></main>
|
||||
|
|
@ -40,7 +63,7 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 先加载 i18n,再加载其他 JS -->
|
||||
<!-- JS 文件 -->
|
||||
<script src="i18n.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// ===== script.js =====
|
||||
// 知秋:主逻辑·动态渲染+loading+错误处理+i18n+无障碍
|
||||
// 知秋:主逻辑·动态渲染+错误处理+i18n+无障碍+骨架屏
|
||||
|
||||
(function() {
|
||||
// 状态变量
|
||||
|
|
@ -11,21 +11,39 @@
|
|||
// DOM元素
|
||||
const container = document.querySelector('.bulletin-container');
|
||||
const channelTabs = document.querySelectorAll('.channel-tab');
|
||||
const skeletonContainer = document.getElementById('skeleton-container');
|
||||
|
||||
// ========== 骨架屏控制 ==========
|
||||
function showSkeleton() {
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function hideSkeleton() {
|
||||
if (skeletonContainer) {
|
||||
skeletonContainer.style.opacity = '0';
|
||||
skeletonContainer.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(() => {
|
||||
skeletonContainer.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
console.log("【知秋】M22公告栏·环节6启动");
|
||||
console.log("【知秋】M22公告栏·环节7启动");
|
||||
|
||||
// 监听语言切换事件
|
||||
window.addEventListener('languagechange', () => {
|
||||
render(); // 语言变了,重新渲染
|
||||
render();
|
||||
});
|
||||
|
||||
await loadBulletins(); // 加载数据
|
||||
render(); // 初次渲染
|
||||
setupEventListeners(); // 频道切换事件
|
||||
setupLangSwitcher(); // 语言切换器事件
|
||||
setupKeyboardNavigation(); // 键盘导航
|
||||
await loadBulletins();
|
||||
render();
|
||||
setupEventListeners();
|
||||
setupLangSwitcher();
|
||||
setupKeyboardNavigation();
|
||||
}
|
||||
|
||||
// 语言切换器
|
||||
|
|
@ -34,12 +52,10 @@
|
|||
btn.addEventListener('click', () => {
|
||||
const lang = btn.dataset.lang;
|
||||
window.i18n.switchLang(lang);
|
||||
// 更新按钮激活状态
|
||||
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
// 设置当前语言按钮激活状态
|
||||
const currentLang = window.i18n.currentLang;
|
||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||
if (btn.dataset.lang === currentLang) {
|
||||
|
|
@ -48,9 +64,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
// 键盘导航
|
||||
// 键盘导航
|
||||
function setupKeyboardNavigation() {
|
||||
// 给每个频道标签加上键盘事件
|
||||
const tabs = document.querySelectorAll('.channel-tab');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
|
|
@ -61,13 +76,11 @@
|
|||
e.preventDefault();
|
||||
const nextIndex = (currentIndex + 1) % tabs.length;
|
||||
tabs[nextIndex].focus();
|
||||
// 自动激活聚焦的标签
|
||||
tabs[nextIndex].click();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||
tabs[prevIndex].focus();
|
||||
// 自动激活聚焦的标签
|
||||
tabs[prevIndex].click();
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
|
|
@ -75,47 +88,26 @@
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 也可以保留原来的 channel-bar 监听作为备用
|
||||
const channelBar = document.querySelector('.channel-bar');
|
||||
if (channelBar) {
|
||||
channelBar.addEventListener('keydown', (e) => {
|
||||
// 防止重复处理
|
||||
if (e.target.classList.contains('channel-tab')) return;
|
||||
|
||||
const tabs = Array.from(document.querySelectorAll('.channel-tab'));
|
||||
const currentIndex = tabs.findIndex(tab => tab === document.activeElement);
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
const nextIndex = (currentIndex + 1) % tabs.length;
|
||||
tabs[nextIndex].focus();
|
||||
tabs[nextIndex].click();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||
tabs[prevIndex].focus();
|
||||
tabs[prevIndex].click();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// 加载公告(核心!)
|
||||
|
||||
// 加载公告
|
||||
async function loadBulletins() {
|
||||
showSkeleton(); // 显示骨架屏
|
||||
loading = true;
|
||||
error = null;
|
||||
render(); // 显示loading
|
||||
|
||||
try {
|
||||
const data = await API.fetchBulletins();
|
||||
allBulletins = data;
|
||||
hideSkeleton(); // 数据到手,隐藏骨架屏
|
||||
loading = false;
|
||||
render();
|
||||
render(); // 渲染真实内容
|
||||
} catch (err) {
|
||||
console.error("加载失败:", err);
|
||||
hideSkeleton(); // 出错也要隐藏骨架屏
|
||||
loading = false;
|
||||
error = "error";
|
||||
render();
|
||||
render(); // 显示错误状态
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,17 +115,6 @@
|
|||
function render() {
|
||||
if (!container) return;
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
container.innerHTML = `
|
||||
<div class="loading-state" role="status" aria-live="polite">
|
||||
<div class="spinner"></div>
|
||||
<p>${window.i18n.t('loading')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (error) {
|
||||
const isOffline = !navigator.onLine;
|
||||
|
|
@ -178,13 +159,12 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// 使用环节4的createCard(如果不存在就降级)
|
||||
// 渲染公告卡片
|
||||
let html = '';
|
||||
filtered.forEach((item, index) => {
|
||||
if (typeof createCard === 'function') {
|
||||
html += createCard(item);
|
||||
} else {
|
||||
// 降级方案(保证显示)
|
||||
html += `
|
||||
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
|
||||
<h3>${item.title}</h3>
|
||||
|
|
@ -200,7 +180,7 @@
|
|||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 事件监听(频道切换)
|
||||
// 频道切换事件
|
||||
function setupEventListeners() {
|
||||
channelTabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
|
|
|
|||
|
|
@ -520,4 +520,46 @@ body {
|
|||
background: black;
|
||||
border-color: black;
|
||||
}
|
||||
}
|
||||
/* ========== 骨架屏样式 ========== */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 16px;
|
||||
margin-bottom: 8px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.skeleton-text.short {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 24px;
|
||||
margin-bottom: 12px;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg, #fff);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
[data-theme="dark"] .skeleton {
|
||||
background: linear-gradient(90deg, #2a2a2a 25%, #333 50%, #2a2a2a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
Loading…
Reference in New Issue