DEV-012: M22环节6- 多语言支持+无障碍增强+i18n框架+ARIA标签+键盘导航

This commit is contained in:
Awen 2026-03-10 23:57:32 +08:00
parent 7d76d188c4
commit 5eebfa6e28
4 changed files with 275 additions and 136 deletions

View File

@ -0,0 +1,109 @@
// ===== i18n.js =====
// 知秋:国际化框架·语言包+切换函数
const i18n = {
// 当前语言
currentLang: 'zh-CN',
// 语言包
messages: {
'zh-CN': {
title: '公告栏',
subscribe: '订阅',
all: '全部',
system: '系统公告',
dev: '开发动态',
team: '团队消息',
loading: '知秋正在飞向服务器……',
error: '网络开小差了,稍后重试~',
empty: '✨ 暂无公告,稍后再来看看~',
emptyChannel: '📭 这个频道暂时没有公告',
retry: '重试',
pinned: '条置顶',
footer: 'HoloLake光湖系统',
offline: '📴 离线模式 · '
},
'en-US': {
title: 'Bulletin Board',
subscribe: 'Subscribe',
all: 'All',
system: 'System',
dev: 'Dev',
team: 'Team',
loading: 'ZhiQiu is flying to the server...',
error: 'Network error, please retry',
empty: '✨ No announcements yet.',
emptyChannel: '📭 No announcements in this channel.',
retry: 'Retry',
pinned: 'pinned',
footer: 'HoloLake System',
offline: '📴 Offline Mode · '
}
},
// 初始化
init: function() {
const savedLang = localStorage.getItem('holo_lang');
if (savedLang && this.messages[savedLang]) {
this.currentLang = savedLang;
}
this.updatePageLanguage();
},
// 切换语言
switchLang: function(lang) {
if (this.messages[lang]) {
this.currentLang = lang;
localStorage.setItem('holo_lang', lang);
this.updatePageLanguage();
window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } }));
}
},
// 获取文本
t: function(key) {
// 确保当前语言存在,如果不存在就回退到中文
const lang = this.currentLang;
if (this.messages[lang] && this.messages[lang][key] !== undefined) {
return this.messages[lang][key];
}
// 如果当前语言没有这个key尝试从中文包找
if (this.messages['zh-CN'][key] !== undefined) {
return this.messages['zh-CN'][key];
}
// 都没有就返回key本身
return key;
},
// 更新页面
updatePageLanguage: function() {
// 更新所有 data-i18n 元素
document.querySelectorAll('[data-i18n]').forEach(function(el) {
const key = el.getAttribute('data-i18n');
el.textContent = i18n.t(key);
});
// 更新 HTML lang
document.documentElement.lang = this.currentLang;
// 更新按钮 value
document.querySelectorAll('[data-i18n-value]').forEach(function(el) {
const key = el.getAttribute('data-i18n-value');
el.value = i18n.t(key);
});
// 更新 placeholder
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
const key = el.getAttribute('data-i18n-placeholder');
el.placeholder = i18n.t(key);
});
console.log('【知秋】语言已切换为: ' + this.currentLang);
}
};
// 自动初始化
i18n.init();
// 挂载到 window
window.i18n = i18n;

View File

@ -9,126 +9,39 @@
<body>
<div class="app-container">
<!-- 顶部标题栏 -->
<div class="header">
<h1>公告栏</h1>
<header class="header">
<h1 data-i18n="title">公告栏</h1>
<div class="header-actions">
<span class="pinned-badge">2条置顶</span>
<button class="subscribe-btn">订阅</button>
</div>
</div>
<!-- 频道标签栏 -->
<div class="channel-bar">
<span class="channel-tab active" data-channel="全部">全部</span>
<span class="channel-tab" data-channel="系统">系统公告</span>
<span class="channel-tab" data-channel="知秋">开发动态</span>
<span class="channel-tab" data-channel="技术">团队消息</span>
</div>
<!-- 公告列表 -->
<div class="bulletin-container">
<!-- 置顶公告1 -->
<div class="bulletin-card pinned">
<div class="pin-indicator">置顶</div>
<div class="bulletin-icon system"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">光湖系统MVP v0.4.0发布公告</span>
<span class="bulletin-time">2小时前</span>
</div>
<p class="bulletin-summary">主域公告栏与频道过渡系统M22正式进入环节0开发。新增公告置顶、频道分类、时间线展示功能信息展示层逐步完善。</p>
<div class="bulletin-footer">
<span class="bulletin-tag tag-system">系统公告</span>
<span class="bulletin-author">发布者:曜冥主控</span>
</div>
<!-- 语言切换器 -->
<div class="lang-switcher">
<button class="lang-btn" data-lang="zh-CN" aria-label="切换到中文"></button>
<button class="lang-btn" data-lang="en-US" aria-label="Switch to English">EN</button>
</div>
<span class="pinned-badge" data-i18n="pinned">条置顶</span>
<button class="subscribe-btn" data-i18n="subscribe">订阅</button>
</div>
</header>
<!-- 置顶公告2 -->
<div class="bulletin-card pinned">
<div class="pin-indicator">置顶</div>
<div class="bulletin-icon dev"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">SYSLOG提交流程v2上线通知</span>
<span class="bulletin-time">5小时前</span>
</div>
<p class="bulletin-summary">SYSLOG生成方式已更新为「知秋主动提问制」完成任务后由知秋引导填写无需手动套用模板。即日起生效。</p>
<div class="bulletin-footer">
<span class="bulletin-tag tag-dev">开发动态</span>
<span class="bulletin-author">发布者:知秋</span>
</div>
</div>
</div>
<!-- 频道标签栏(用 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>
<!-- 普通公告1 -->
<div class="bulletin-card">
<div class="bulletin-icon team"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">欢迎Awen加入光湖团队</span>
<span class="bulletin-time">1天前</span>
</div>
<p class="bulletin-summary">DEV-012 Awen正式加入光湖开发团队成为第12位协作者。首个模块M09消息通知中心已全通关</p>
</div>
</div>
<!-- 普通公告2 -->
<div class="bulletin-card">
<div class="bulletin-icon dev"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">M08数据统计面板全通关</span>
<span class="bulletin-time">2天前</span>
</div>
<p class="bulletin-summary">完成M08全部环节七连胜达成响应式布局+SVG环形图+CSV导出一次通过正式前端毕业</p>
<div class="bulletin-footer">
<span class="bulletin-tag tag-dev">开发动态</span>
<span class="bulletin-author">发布者:霜砚</span>
</div>
</div>
</div>
<!-- 普通公告3 -->
<div class="bulletin-card">
<div class="bulletin-icon system"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">HTTPS配置与飞书Webhook联调完成</span>
<span class="bulletin-time">3天前</span>
</div>
<p class="bulletin-summary">光湖主域已完成HTTPS配置与飞书Webhook全链路联调Nginx+PM2+Certbot部署完毕系统脊梁正式贯通。</p>
<div class="bulletin-footer">
<span class="bulletin-tag tag-system">系统公告</span>
<span class="bulletin-author">发布者:页页</span>
</div>
</div>
</div>
<!-- 普通公告4 -->
<div class="bulletin-card">
<div class="bulletin-icon team"></div>
<div class="bulletin-content">
<div class="bulletin-header">
<span class="bulletin-title">本周开发者进度总览</span>
<span class="bulletin-time">3天前</span>
</div>
<p class="bulletin-summary">12位开发者中8位活跃累计完成30+个环节。肥猫五连胜、桔子七连胜、小草莓五连胜、Awen四连胜团队势头强劲</p>
<div class="bulletin-footer">
<span class="bulletin-tag tag-team">团队消息</span>
<span class="bulletin-author">发布者:曜冥主控</span>
</div>
</div>
</div>
</div>
<!-- 公告主内容区 -->
<main class="bulletin-container" role="list"></main>
<!-- 底部状态栏 -->
<div class="footer">
<span>共6条公告2条置顶</span>
<span class="footer-brand">HoloLake光湖系统</span>
<footer class="footer">
<span><span id="totalCount">6</span> 条公告 <span id="pinnedCount">2</span> <span data-i18n="pinned">条置顶</span></span>
<span class="footer-brand" data-i18n="footer">HoloLake光湖系统</span>
</footer>
</div>
<!-- JavaScript 文件引入 -->
<!-- 先加载 i18n再加载其他 JS -->
<script src="i18n.js"></script>
<script src="api.js"></script>
<script src="storage.js"></script>
<script src="script.js"></script>

View File

@ -1,5 +1,5 @@
// ===== script.js =====
// 知秋:主逻辑·动态渲染+loading+错误处理
// 知秋:主逻辑·动态渲染+loading+错误处理+i18n+无障碍
(function() {
// 状态变量
@ -14,12 +14,92 @@
// 初始化
async function init() {
console.log("【知秋】M22公告栏·环节5启动");
console.log("【知秋】M22公告栏·环节6启动");
// 监听语言切换事件
window.addEventListener('languagechange', () => {
render(); // 语言变了,重新渲染
});
await loadBulletins(); // 加载数据
render(); // 初次渲染
setupEventListeners(); // 事件监听
setupEventListeners(); // 频道切换事件
setupLangSwitcher(); // 语言切换器事件
setupKeyboardNavigation(); // 键盘导航
}
// 语言切换器
function setupLangSwitcher() {
document.querySelectorAll('.lang-btn').forEach(btn => {
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) {
btn.classList.add('active');
}
});
}
// 键盘导航
function setupKeyboardNavigation() {
// 给每个频道标签加上键盘事件
const tabs = document.querySelectorAll('.channel-tab');
tabs.forEach(tab => {
tab.addEventListener('keydown', (e) => {
const currentIndex = Array.from(tabs).findIndex(t => t === e.target);
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();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.target.click();
}
});
});
// 也可以保留原来的 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() {
loading = true;
@ -27,7 +107,6 @@
render(); // 显示loading
try {
// 知秋调用API.fetchBulletins拿数据
const data = await API.fetchBulletins();
allBulletins = data;
loading = false;
@ -35,39 +114,37 @@
} catch (err) {
console.error("加载失败:", err);
loading = false;
error = "网络开小差了,稍后重试~";
error = "error";
render();
}
}
// 渲染公告列表使用环节4的createCard组件
// 渲染公告列表
function render() {
if (!container) return;
// 加载状态
if (loading) {
container.innerHTML = `
<div class="loading-state">
<div class="loading-state" role="status" aria-live="polite">
<div class="spinner"></div>
<p>知秋正在飞向服务器</p>
<p>${window.i18n.t('loading')}</p>
</div>
`;
return;
}
// 错误状态
// 错误状态
if (error) {
// 检查是否离线(断网)
const isOffline = !navigator.onLine;
const offlineHint = isOffline ? '📴 离线模式 · ' : '';
const offlineHint = isOffline ? window.i18n.t('offline') : '';
container.innerHTML = `
<div class="error-state">
<p>😢 ${offlineHint}${error}</p>
<button class="retry-btn" id="retryBtn">重试</button>
<div class="error-state" role="alert">
<p>😢 ${offlineHint}${window.i18n.t('error')}</p>
<button class="retry-btn" id="retryBtn">${window.i18n.t('retry')}</button>
</div>
`;
// 重试按钮事件(用事件委托,但这里直接绑一下)
const retryBtn = document.getElementById('retryBtn');
if (retryBtn) {
retryBtn.addEventListener('click', () => {
@ -80,8 +157,8 @@
// 空数据状态
if (allBulletins.length === 0) {
container.innerHTML = `
<div class="empty-state">
<p> 暂无公告稍后再来看看</p>
<div class="empty-state" role="status">
<p>${window.i18n.t('empty')}</p>
</div>
`;
return;
@ -94,8 +171,8 @@
if (filtered.length === 0) {
container.innerHTML = `
<div class="empty-state">
<p>📭 这个频道暂时没有公告</p>
<div class="empty-state" role="status">
<p>${window.i18n.t('emptyChannel')}</p>
</div>
`;
return;
@ -103,13 +180,13 @@
// 使用环节4的createCard如果不存在就降级
let html = '';
filtered.forEach(item => {
filtered.forEach((item, index) => {
if (typeof createCard === 'function') {
html += createCard(item);
} else {
// 降级方案(保证显示)
html += `
<div class="bulletin-card">
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
<h3>${item.title}</h3>
<p>${item.content}</p>
<div class="meta">
@ -127,13 +204,9 @@
function setupEventListeners() {
channelTabs.forEach(tab => {
tab.addEventListener('click', (e) => {
// 移除所有active
channelTabs.forEach(t => t.classList.remove('active'));
// 当前加active
e.target.classList.add('active');
// 更新频道
currentChannel = e.target.dataset.channel || '全部';
// 重新渲染不用再加载数据用已有的allBulletins
render();
});
});

View File

@ -476,4 +476,48 @@ body {
/* 也可以调整标题栏的底部间距 */
.header-actions {
margin-bottom: 5px; /* 或者给标题栏底部加点间距 */
}
/* ===== 语言切换器样式 ===== */
.lang-switcher {
display: inline-flex;
margin-right: 15px;
gap: 5px;
}
.lang-btn {
background: #f0f0f0; /* 改成浅灰色背景 */
border: 1px solid #999; /* 深一点的边框 */
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
color: #333; /* 文字颜色深灰色,保证看得见 */
}
.lang-btn:hover {
background: #f0f0f0;
}
.lang-btn.active {
background: #3498db;
color: white;
border-color: #3498db;
}
/* 焦点样式(键盘导航用) */
.lang-btn:focus,
.channel-tab:focus,
.retry-btn:focus,
.bulletin-card:focus {
outline: 2px solid #3498db;
outline-offset: 2px;
}
/* 高对比度适配(可选) */
@media (prefers-contrast: high) {
.lang-btn.active {
background: black;
border-color: black;
}
}