DEV-012: M22环节7- 性能优化+骨架屏+首屏加速+预加载
This commit is contained in:
parent
57478c0721
commit
c969a7ac56
|
|
@ -4,6 +4,10 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>HoloLake公告栏</title>
|
<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">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -22,13 +26,32 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 频道标签栏(用 nav 包裹) -->
|
<!-- 频道标签栏 -->
|
||||||
<nav class="channel-bar" role="tablist">
|
<nav class="channel-bar" role="tablist">
|
||||||
<button class="channel-tab active" data-channel="全部" role="tab" data-i18n="all">全部</button>
|
<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="system">系统公告</button>
|
||||||
<button class="channel-tab" data-channel="知秋" role="tab" data-i18n="dev">开发动态</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>
|
<button class="channel-tab" data-channel="技术" role="tab" data-i18n="team">团队消息</button>
|
||||||
</nav>
|
</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>
|
<main class="bulletin-container" role="list"></main>
|
||||||
|
|
@ -40,7 +63,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 先加载 i18n,再加载其他 JS -->
|
<!-- JS 文件 -->
|
||||||
<script src="i18n.js"></script>
|
<script src="i18n.js"></script>
|
||||||
<script src="api.js"></script>
|
<script src="api.js"></script>
|
||||||
<script src="storage.js"></script>
|
<script src="storage.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// ===== script.js =====
|
// ===== script.js =====
|
||||||
// 知秋:主逻辑·动态渲染+loading+错误处理+i18n+无障碍
|
// 知秋:主逻辑·动态渲染+错误处理+i18n+无障碍+骨架屏
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
// 状态变量
|
// 状态变量
|
||||||
|
|
@ -11,21 +11,39 @@
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const container = document.querySelector('.bulletin-container');
|
const container = document.querySelector('.bulletin-container');
|
||||||
const channelTabs = document.querySelectorAll('.channel-tab');
|
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() {
|
async function init() {
|
||||||
console.log("【知秋】M22公告栏·环节6启动");
|
console.log("【知秋】M22公告栏·环节7启动");
|
||||||
|
|
||||||
// 监听语言切换事件
|
// 监听语言切换事件
|
||||||
window.addEventListener('languagechange', () => {
|
window.addEventListener('languagechange', () => {
|
||||||
render(); // 语言变了,重新渲染
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadBulletins(); // 加载数据
|
await loadBulletins();
|
||||||
render(); // 初次渲染
|
render();
|
||||||
setupEventListeners(); // 频道切换事件
|
setupEventListeners();
|
||||||
setupLangSwitcher(); // 语言切换器事件
|
setupLangSwitcher();
|
||||||
setupKeyboardNavigation(); // 键盘导航
|
setupKeyboardNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语言切换器
|
// 语言切换器
|
||||||
|
|
@ -34,12 +52,10 @@
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const lang = btn.dataset.lang;
|
const lang = btn.dataset.lang;
|
||||||
window.i18n.switchLang(lang);
|
window.i18n.switchLang(lang);
|
||||||
// 更新按钮激活状态
|
|
||||||
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// 设置当前语言按钮激活状态
|
|
||||||
const currentLang = window.i18n.currentLang;
|
const currentLang = window.i18n.currentLang;
|
||||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||||
if (btn.dataset.lang === currentLang) {
|
if (btn.dataset.lang === currentLang) {
|
||||||
|
|
@ -48,9 +64,8 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 键盘导航
|
// 键盘导航
|
||||||
function setupKeyboardNavigation() {
|
function setupKeyboardNavigation() {
|
||||||
// 给每个频道标签加上键盘事件
|
|
||||||
const tabs = document.querySelectorAll('.channel-tab');
|
const tabs = document.querySelectorAll('.channel-tab');
|
||||||
|
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
|
|
@ -61,13 +76,11 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nextIndex = (currentIndex + 1) % tabs.length;
|
const nextIndex = (currentIndex + 1) % tabs.length;
|
||||||
tabs[nextIndex].focus();
|
tabs[nextIndex].focus();
|
||||||
// 自动激活聚焦的标签
|
|
||||||
tabs[nextIndex].click();
|
tabs[nextIndex].click();
|
||||||
} else if (e.key === 'ArrowLeft') {
|
} else if (e.key === 'ArrowLeft') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||||
tabs[prevIndex].focus();
|
tabs[prevIndex].focus();
|
||||||
// 自动激活聚焦的标签
|
|
||||||
tabs[prevIndex].click();
|
tabs[prevIndex].click();
|
||||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
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() {
|
async function loadBulletins() {
|
||||||
|
showSkeleton(); // 显示骨架屏
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
render(); // 显示loading
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await API.fetchBulletins();
|
const data = await API.fetchBulletins();
|
||||||
allBulletins = data;
|
allBulletins = data;
|
||||||
|
hideSkeleton(); // 数据到手,隐藏骨架屏
|
||||||
loading = false;
|
loading = false;
|
||||||
render();
|
render(); // 渲染真实内容
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("加载失败:", err);
|
console.error("加载失败:", err);
|
||||||
|
hideSkeleton(); // 出错也要隐藏骨架屏
|
||||||
loading = false;
|
loading = false;
|
||||||
error = "error";
|
error = "error";
|
||||||
render();
|
render(); // 显示错误状态
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,17 +115,6 @@
|
||||||
function render() {
|
function render() {
|
||||||
if (!container) return;
|
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) {
|
if (error) {
|
||||||
const isOffline = !navigator.onLine;
|
const isOffline = !navigator.onLine;
|
||||||
|
|
@ -178,13 +159,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用环节4的createCard(如果不存在就降级)
|
// 渲染公告卡片
|
||||||
let html = '';
|
let html = '';
|
||||||
filtered.forEach((item, index) => {
|
filtered.forEach((item, index) => {
|
||||||
if (typeof createCard === 'function') {
|
if (typeof createCard === 'function') {
|
||||||
html += createCard(item);
|
html += createCard(item);
|
||||||
} else {
|
} else {
|
||||||
// 降级方案(保证显示)
|
|
||||||
html += `
|
html += `
|
||||||
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
|
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
|
||||||
<h3>${item.title}</h3>
|
<h3>${item.title}</h3>
|
||||||
|
|
@ -200,7 +180,7 @@
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件监听(频道切换)
|
// 频道切换事件
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
channelTabs.forEach(tab => {
|
channelTabs.forEach(tab => {
|
||||||
tab.addEventListener('click', (e) => {
|
tab.addEventListener('click', (e) => {
|
||||||
|
|
|
||||||
|
|
@ -520,4 +520,46 @@ body {
|
||||||
background: black;
|
background: black;
|
||||||
border-color: 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