diff --git a/modules/M22-bulletin/i18n.js b/modules/M22-bulletin/i18n.js new file mode 100644 index 00000000..68f95c3a --- /dev/null +++ b/modules/M22-bulletin/i18n.js @@ -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; \ No newline at end of file diff --git a/modules/M22-bulletin/index.html b/modules/M22-bulletin/index.html index f8026fd3..82e85611 100644 --- a/modules/M22-bulletin/index.html +++ b/modules/M22-bulletin/index.html @@ -9,126 +9,39 @@
-
-

公告栏

+
+

公告栏

- 2条置顶 - -
-
- - -
- 全部 - 系统公告 - 开发动态 - 团队消息 -
- - -
- -
-
置顶
-
-
-
- 光湖系统MVP v0.4.0发布公告 - 2小时前 -
-

主域公告栏与频道过渡系统(M22)正式进入环节0开发。新增公告置顶、频道分类、时间线展示功能,信息展示层逐步完善。

- + +
+ +
+ 条置顶 +
+ - -
-
置顶
-
-
-
- SYSLOG提交流程v2上线通知 - 5小时前 -
-

SYSLOG生成方式已更新为「知秋主动提问制」完成任务后由知秋引导填写,无需手动套用模板。即日起生效。

- -
-
+ + - -
-
-
-
- 欢迎Awen加入光湖团队! - 1天前 -
-

DEV-012 Awen正式加入光湖开发团队,成为第12位协作者。首个模块M09消息通知中心已全通关!

-
-
- - -
-
-
-
- M08数据统计面板全通关 - 2天前 -
-

完成M08全部环节,七连胜达成!响应式布局+SVG环形图+CSV导出一次通过,正式前端毕业

- -
-
- - -
-
-
-
- HTTPS配置与飞书Webhook联调完成 - 3天前 -
-

光湖主域已完成HTTPS配置与飞书Webhook全链路联调,Nginx+PM2+Certbot部署完毕,系统脊梁正式贯通。

- -
-
- - -
-
-
-
- 本周开发者进度总览 - 3天前 -
-

12位开发者中8位活跃,累计完成30+个环节。肥猫五连胜、桔子七连胜、小草莓五连胜、Awen四连胜团队势头强劲!

- -
-
-
+ +
- - + + diff --git a/modules/M22-bulletin/script.js b/modules/M22-bulletin/script.js index f227bc6b..9c1950b9 100644 --- a/modules/M22-bulletin/script.js +++ b/modules/M22-bulletin/script.js @@ -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 = ` -
+
-

知秋正在飞向服务器……

+

${window.i18n.t('loading')}

`; return; } - // 错误状态 + // 错误状态 if (error) { - // 检查是否离线(断网) const isOffline = !navigator.onLine; - const offlineHint = isOffline ? '📴 离线模式 · ' : ''; + const offlineHint = isOffline ? window.i18n.t('offline') : ''; container.innerHTML = ` -
-

😢 ${offlineHint}${error}

- + `; - // 重试按钮事件(用事件委托,但这里直接绑一下) const retryBtn = document.getElementById('retryBtn'); if (retryBtn) { retryBtn.addEventListener('click', () => { @@ -80,8 +157,8 @@ // 空数据状态 if (allBulletins.length === 0) { container.innerHTML = ` -
-

✨ 暂无公告,稍后再来看看~

+
+

${window.i18n.t('empty')}

`; return; @@ -94,8 +171,8 @@ if (filtered.length === 0) { container.innerHTML = ` -
-

📭 这个频道暂时没有公告

+
+

${window.i18n.t('emptyChannel')}

`; 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 += ` -
+

${item.title}

${item.content}

@@ -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(); }); }); diff --git a/modules/M22-bulletin/style.css b/modules/M22-bulletin/style.css index 41576001..351c2abb 100644 --- a/modules/M22-bulletin/style.css +++ b/modules/M22-bulletin/style.css @@ -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; + } } \ No newline at end of file