DEV-012: M22环节6- 多语言支持+无障碍增强+i18n框架+ARIA标签+键盘导航
This commit is contained in:
parent
7d76d188c4
commit
5eebfa6e28
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue