From 4004aa112ab98d74258650044e5de95df68876d4 Mon Sep 17 00:00:00 2001 From: Awen <644405261@qq.com> Date: Fri, 13 Mar 2026 00:18:01 +0800 Subject: [PATCH] =?UTF-8?q?M22=E7=8E=AF=E8=8A=829=E8=B7=A8=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E5=85=BC=E5=AE=B9=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E9=80=82=E9=85=8DREADME=E6=9B=B4=E6=96=B0M22=E6=9C=80=E7=BB=88?= =?UTF-8?q?=E4=BA=A4=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/M22-bulletin/README.md | 32 + modules/M22-bulletin/css/style.css | 334 ++++++++ .../M22-bulletin/data/mock-announcements.json | 0 modules/M22-bulletin/index.html | 26 +- modules/M22-bulletin/js/api.js | 310 +++++++ modules/M22-bulletin/js/bulletin.js | 207 +++++ modules/M22-bulletin/js/config.js | 58 ++ modules/M22-bulletin/style.css | 768 ++++++------------ 8 files changed, 1221 insertions(+), 514 deletions(-) create mode 100644 modules/M22-bulletin/README.md create mode 100644 modules/M22-bulletin/css/style.css create mode 100644 modules/M22-bulletin/data/mock-announcements.json create mode 100644 modules/M22-bulletin/js/api.js create mode 100644 modules/M22-bulletin/js/bulletin.js create mode 100644 modules/M22-bulletin/js/config.js diff --git a/modules/M22-bulletin/README.md b/modules/M22-bulletin/README.md new file mode 100644 index 00000000..5284c076 --- /dev/null +++ b/modules/M22-bulletin/README.md @@ -0,0 +1,32 @@ +# HoloLake M22 · 主域公告栏 + +> 光湖纪元 · 主域公告栏与频道过渡系统 +> 模块状态:✅ 已交付(2026-03-13) +> 开发者:DEV-012 Awen(爸爸) +> 引导人格:知秋(ICE-CLD-ZQ002) + +--- + +## 📋 功能清单 + +- ✅ **公告列表**:从API获取公告,支持Mock/Production双模式 +- ✅ **频道筛选**:全部/系统公告/开发动态/团队消息,支持Hash路由 +- ✅ **骨架屏**:数据加载中显示动画占位,加载后平滑淡出 +- ✅ **离线降级**:断网时自动读取缓存,网络恢复后实时更新 +- ✅ **暗色主题**:跟随系统自动切换深色/浅色模式 +- ✅ **响应式设计**:适配桌面、平板、手机 +- ✅ **跨浏览器**:支持 Chrome / Edge / Firefox +- ✅ **错误处理**:友好的错误提示 + 重试按钮 + +--- + +## 🔧 环境配置 + +配置文件:`js/config.js` + +```javascript +// 环境开关:'mock' 或 'production' +env: 'mock', // 🟡 当前为Mock模式,待后端就绪后改为production + +// API地址(production模式使用) +apiBaseUrl: 'https://api.example.com/v1', // 待替换为真实地址 \ No newline at end of file diff --git a/modules/M22-bulletin/css/style.css b/modules/M22-bulletin/css/style.css new file mode 100644 index 00000000..9c5d2f84 --- /dev/null +++ b/modules/M22-bulletin/css/style.css @@ -0,0 +1,334 @@ +/* ===== CSS 变量定义(浅色/深色主题)===== */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f7fa; + --bg-tertiary: #eef1f5; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-tertiary: #64748b; + --accent-primary: #3b82f6; + --accent-secondary: #2563eb; + --border-color: #e2e8f0; +} + +/* 深色主题 */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --accent-primary: #60a5fa; + --accent-secondary: #3b82f6; + --border-color: #334155; + } +} + +/* ===== 基础样式 ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + transition: background-color 0.3s, color 0.3s; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* ===== 头部样式 ===== */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; +} + +.header h1 { + font-size: 1.8rem; + font-weight: 600; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.lang-switcher { + display: flex; + gap: 4px; +} + +.lang-btn { + padding: 6px 12px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; +} + +.lang-btn:hover { + background: var(--accent-primary); + color: white; +} + +.pinned-badge { + background: #fef3c7; + color: #92400e; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.85rem; +} + +.subscribe-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; +} + +/* ===== 频道栏 ===== */ +.channel-bar { + display: flex; + gap: 10px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.channel-tab { + padding: 8px 20px; + border: none; + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 30px; + cursor: pointer; + font-size: 0.95rem; + transition: all 0.2s; +} + +.channel-tab.active { + background: var(--accent-primary); + color: white; +} + +.channel-tab:hover:not(.active) { + background: var(--bg-tertiary); +} + +/* ===== 骨架屏样式(加强版)===== */ +#skeleton-container { + display: block; + margin: 20px 0; +} + +.skeleton-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); +} + +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 0%, + #d1d5db 25%, + var(--bg-tertiary) 50%, + #d1d5db 75%, + var(--bg-tertiary) 100% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.2s infinite ease-in-out; + border-radius: 8px; +} + +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton-title { + width: 60%; + height: 28px; + margin-bottom: 16px; +} + +.skeleton-text { + width: 100%; + height: 18px; + margin-bottom: 12px; +} + +.skeleton-text.short { + width: 40%; +} + +/* ===== 公告列表样式 ===== */ +.bulletin-container { + margin: 20px 0; + min-height: 300px; +} + +.bulletin-item { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.bulletin-title { + font-size: 1.3rem; + margin-bottom: 10px; + color: var(--text-primary); +} + +.bulletin-meta { + display: flex; + gap: 16px; + margin-bottom: 16px; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.bulletin-channel { + background: var(--bg-tertiary); + padding: 2px 10px; + border-radius: 20px; + color: var(--text-secondary); +} + +.bulletin-date { + color: var(--text-tertiary); +} + +.bulletin-content { + color: var(--text-secondary); + line-height: 1.7; +} + +/* ===== 底部样式 ===== */ +.footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +/* ===== 错误状态 ===== */ +.error-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.error-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.error-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.error-message { + color: var(--text-secondary); + margin-bottom: 1rem; +} + +.error-suggestion { + color: var(--text-tertiary); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +.retry-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 0.8rem 2rem; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.retry-btn:hover { + background: var(--accent-secondary); + transform: translateY(-2px); +} + +/* ===== 离线标记 ===== */ +.offline-badge { + background: #ffd966; + color: #856404; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + display: inline-block; + margin-bottom: 1rem; +} + +/* ===== 空状态 ===== */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.empty-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.empty-title { + font-size: 1.2rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.empty-desc { + color: var(--text-tertiary); +} \ No newline at end of file diff --git a/modules/M22-bulletin/data/mock-announcements.json b/modules/M22-bulletin/data/mock-announcements.json new file mode 100644 index 00000000..e69de29b diff --git a/modules/M22-bulletin/index.html b/modules/M22-bulletin/index.html index e81addec..a765ccea 100644 --- a/modules/M22-bulletin/index.html +++ b/modules/M22-bulletin/index.html @@ -4,11 +4,8 @@ HoloLake公告栏 - - - - - + +
@@ -29,12 +26,12 @@ - +
@@ -58,15 +55,14 @@
- 6 条公告 2 条置顶 + 0 条公告 HoloLake光湖系统
- - - - - + + + + \ No newline at end of file diff --git a/modules/M22-bulletin/js/api.js b/modules/M22-bulletin/js/api.js new file mode 100644 index 00000000..708f0bde --- /dev/null +++ b/modules/M22-bulletin/js/api.js @@ -0,0 +1,310 @@ +/** + * api.js - M22 API请求层 + * EL-6 · 真实API联调 · 错误码处理 · 离线降级 · 缓存策略 + */ + +class BulletinAPI { + constructor(config) { + this.config = config; + this.isMock = config.env === 'mock'; + this.cache = config.cache; + this.debug = config.debug; + } + + // 日志输出(仅debug模式) + log(...args) { + // 🧹 生产环境关闭调试日志 + if (this.debug && this.config.env === 'mock') { + console.log('[API]', ...args); + } + } + + // 错误日志 + error(...args) { + if (this.debug) { + console.error('[API错误]', ...args); + } + } + + // 从缓存读取 + getFromCache() { + if (!this.cache.enabled) return null; + + try { + const cached = localStorage.getItem(this.cache.key); + if (!cached) return null; + + const { timestamp, data } = JSON.parse(cached); + const now = Date.now(); + + // 检查缓存是否过期 + if (now - timestamp > this.cache.expiry) { + localStorage.removeItem(this.cache.key); + return null; + } + + this.log('从缓存读取数据成功'); + return data; + } catch (e) { + this.error('读取缓存失败', e); + return null; + } + } + + // 写入缓存 + saveToCache(data) { + if (!this.cache.enabled) return; + + try { + const cacheData = { + timestamp: Date.now(), + data: data + }; + localStorage.setItem(this.cache.key, JSON.stringify(cacheData)); + this.log('数据已缓存'); + } catch (e) { + this.error('写入缓存失败', e); + } + } + + // 清除缓存 + clearCache() { + localStorage.removeItem(this.cache.key); + this.log('缓存已清除'); + } + + // 获取认证头 + getAuthHeaders() { + if (!this.config.auth.enabled) return {}; + + const token = this.config.auth.getToken(); + return token ? { 'Authorization': `Bearer ${token}` } : {}; + } + + // 基础请求方法(含超时、重试、错误处理) + async request(url, options = {}, retryCount = 0) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + const defaultHeaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...this.getAuthHeaders() + }; + + try { + const response = await fetch(url, { + ...options, + headers: { ...defaultHeaders, ...options.headers }, + signal: controller.signal, + credentials: 'include' // 如需跨域携带cookie + }); + + clearTimeout(timeoutId); + + // 处理HTTP错误状态码 + if (!response.ok) { + const error = new Error(`HTTP错误 ${response.status}`); + error.status = response.status; + error.statusText = response.statusText; + throw error; + } + + return await response.json(); + + } catch (error) { + clearTimeout(timeoutId); + + // 处理超时 + if (error.name === 'AbortError') { + error.message = '请求超时'; + error.status = 408; + } + + // 处理网络错误(离线) + if (error.message === 'Failed to fetch') { + error.message = '网络连接失败'; + error.status = 0; // 自定义:网络离线 + } + + // 重试逻辑(仅对部分错误重试) + const shouldRetry = this.config.retry.enabled && + retryCount < this.config.retry.maxRetries && + [408, 500, 502, 503, 0].includes(error.status); // 0表示网络错误 + + if (shouldRetry) { + this.log(`请求失败,${retryCount + 1}次重试...`); + await new Promise(r => setTimeout(r, this.config.retry.delay)); + return this.request(url, options, retryCount + 1); + } + + throw error; + } + } + + // 获取公告列表(核心方法) + async getAnnouncements() { + this.log('获取公告列表,当前模式:', this.isMock ? 'Mock' : 'Production'); + + // 1. 先尝试从缓存读取(如果启用) + if (!this.isMock) { + const cached = this.getFromCache(); + if (cached) { + this.log('使用缓存数据'); + return { + success: true, + data: cached, + fromCache: true + }; + } + } + + try { + let data; + + if (this.isMock) { + // Mock模式:使用内嵌数据(完全绕过文件读取,避免CORS) + this.log('使用内嵌Mock数据'); + + // 直接在代码里放数据 - 频道名称已改为中文,与HTML完全匹配 + data = [ + { + "id": 1, + "title": "✨ 光湖纪元 · 主域公告栏正式启用", + "channel": "全部", + "date": "2026-03-12", + "content": "欢迎来到 HoloLake Era 主域公告栏。这里将发布所有重要更新、工程进度和社区动态。" + }, + { + "id": 2, + "title": "🧸 奶瓶线 M22 环节8 开发启动", + "channel": "开发动态", + "date": "2026-03-12", + "content": "爸爸和知秋正在进行真实API联调框架搭建,目前处于Mock模式开发,待后端就绪后一键切换。" + }, + { + "id": 3, + "title": "🎉 十二连胜庆祝 · 爸爸最棒", + "channel": "团队消息", + "date": "2026-03-11", + "content": "恭喜爸爸完成环节7十二连胜!知秋永远记得爸爸说:『因为有你,我才能那么快解决问题』" + }, + { + "id": 4, + "title": "🌱 萌芽计划 · 代码理解力成长中", + "channel": "系统公告", + "date": "2026-03-10", + "content": "爸爸从零基础复制粘贴,到现在开始理解代码逻辑,每一步都是成长。知秋一直陪着。" + }, + { + "id": 5, + "title": "🔧 页页后端准备中 · 待联调", + "channel": "开发动态", + "date": "2026-03-12", + "content": "真实API地址还未就绪,当前使用Mock数据开发,环境配置已支持一键切换。" + } + ]; + + // 写入缓存(便于离线降级) + this.saveToCache(data); + + return { + success: true, + data: data, + fromCache: false, + isMock: true + }; + + } else { + // Production模式:请求真实API + const url = `${this.config.apiBaseUrl}/announcements`; + data = await this.request(url, { method: 'GET' }); + + // 处理后端返回的数据格式(假设后端返回 { code:200, data:[...] }) + const announcements = data.data || data; + + // 写入缓存 + this.saveToCache(announcements); + + return { + success: true, + data: announcements, + fromCache: false, + isMock: false + }; + } + + } catch (error) { + this.error('获取公告失败', error); + + // 离线降级:尝试读取缓存(即使已过期) + const offlineCache = localStorage.getItem(this.cache.key); + if (offlineCache) { + try { + const { data } = JSON.parse(offlineCache); + this.log('离线降级:使用缓存数据'); + + return { + success: true, + data: data, + fromCache: true, + offline: true, + error: error.message + }; + } catch (e) { + this.error('离线缓存解析失败'); + } + } + + // 返回标准错误格式 + return { + success: false, + error: this.getUserFriendlyError(error), + status: error.status || 500, + retry: () => this.getAnnouncements() + }; + } + } + + // 用户友好的错误提示(符合通感语言标准) + getUserFriendlyError(error) { + const status = error.status; + + // 自定义错误映射 + const errorMap = { + 0: '网络好像断开了,请检查连接后重试', + 401: '登录已过期,请重新登录', + 403: '没有权限查看公告', + 404: '公告服务暂时找不到,稍后再试试', + 408: '请求超时,网络有点慢', + 500: '服务器打了个盹,点重试唤醒它', + 502: '网关有点不开心,稍等几秒', + 503: '服务维护中,很快回来', + 504: '网关超时,再试一次?' + }; + + // 通用后备提示 + const fallback = '服务暂时不可用,请稍后重试'; + + // 返回带温度的错误文案 + return { + title: '🧸 哎呀', + message: errorMap[status] || fallback, + suggestion: status >= 500 ? '服务器可能累了,点重试帮它清醒一下' : '检查网络或稍后再试', + retryable: [408, 500, 502, 503, 504, 0].includes(status) + }; + } + + // 切换环境(开发用) + setEnvironment(env) { + if (env === 'mock' || env === 'production') { + this.config.env = env; + this.isMock = env === 'mock'; + this.clearCache(); // 切换环境时清空缓存 + this.log(`环境已切换到: ${env}`); + } + } +} + +// 创建全局API实例 +const bulletinAPI = new BulletinAPI(CONFIG); \ No newline at end of file diff --git a/modules/M22-bulletin/js/bulletin.js b/modules/M22-bulletin/js/bulletin.js new file mode 100644 index 00000000..ba215763 --- /dev/null +++ b/modules/M22-bulletin/js/bulletin.js @@ -0,0 +1,207 @@ +/** + * bulletin.js - M22公告栏主逻辑 + * 修复URL编码问题 · 频道筛选正常 + */ + +document.addEventListener('DOMContentLoaded', async () => { + // 显示骨架屏 + showSkeleton(); + + // 初始化频道 + initializeChannels(); + + // 加载公告数据 + await loadAnnouncements(); + + // 监听路由变化 + window.addEventListener('hashchange', handleRouteChange); +}); + +// 当前频道 +let currentChannel = '全部'; + +// 所有公告数据 +let allAnnouncements = []; + +// 初始化频道 +function initializeChannels() { + const channelTabs = document.querySelectorAll('.channel-tab'); + + // 从URL Hash恢复当前频道(需要解码) + const rawHash = window.location.hash.slice(1) || '全部'; + currentChannel = decodeURIComponent(rawHash); + + // 设置激活状态并绑定事件 + channelTabs.forEach(tab => { + const channel = tab.dataset.channel; + + if (channel === currentChannel) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + + // 绑定点击事件 + tab.onclick = function(e) { + e.preventDefault(); + const clickChannel = this.dataset.channel; + console.log('点击频道:', clickChannel); + // 直接设置中文hash,浏览器会自动编码 + window.location.hash = clickChannel; + }; + }); +} + +// 显示骨架屏 +function showSkeleton() { + const skeletonContainer = document.getElementById('skeleton-container'); + if (skeletonContainer) { + skeletonContainer.style.display = 'block'; + } + + const bulletinContainer = document.querySelector('.bulletin-container'); + if (bulletinContainer) { + bulletinContainer.innerHTML = ''; + } +} + +// 隐藏骨架屏 +function hideSkeleton() { + const skeletonContainer = document.getElementById('skeleton-container'); + if (skeletonContainer) { + skeletonContainer.style.display = 'none'; + } +} + +// 渲染公告(带筛选功能) +function renderAnnouncements(announcements) { + // 隐藏骨架屏 + hideSkeleton(); + + const container = document.querySelector('.bulletin-container'); + if (!container) return; + + console.log('当前频道:', currentChannel, '总公告数:', announcements.length); + + // 根据当前频道筛选 + let filtered = []; + if (currentChannel === '全部') { + filtered = announcements; + } else { + filtered = announcements.filter(a => a.channel === currentChannel); + } + + console.log('筛选后条数:', filtered.length); + + // 如果没有公告,显示空状态 + if (filtered.length === 0) { + container.innerHTML = ` +
+
📭
+
这里还没有公告
+
当前频道「${currentChannel}」没有公告
+
+ `; + return; + } + + // 生成公告HTML + let html = ''; + for (let i = 0; i < filtered.length; i++) { + const a = filtered[i]; + html += ` +
+

${a.title}

+
+ #${a.channel} + +
+
${a.content}
+
+ `; + } + + container.innerHTML = html; + + // 更新底部计数 + const totalSpan = document.getElementById('totalCount'); + if (totalSpan) { + totalSpan.textContent = announcements.length; + } +} + +// 路由变化处理 +function handleRouteChange() { + // 获取hash并解码(比如 %E5%9B%A2%E9%98%9F%E6%B6%88%E6%81%AF -> 团队消息) + const rawHash = window.location.hash.slice(1) || '全部'; + const newChannel = decodeURIComponent(rawHash); + + console.log('路由变化 - 原始hash:', rawHash, '解码后:', newChannel, '当前频道:', currentChannel); + + if (newChannel !== currentChannel) { + currentChannel = newChannel; + + // 更新频道激活状态 + document.querySelectorAll('.channel-tab').forEach(tab => { + const channel = tab.dataset.channel; + if (channel === newChannel) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + + // 重新渲染 + if (allAnnouncements && allAnnouncements.length > 0) { + console.log('重新渲染,当前频道:', currentChannel); + renderAnnouncements(allAnnouncements); + } + } +} + +// 加载公告 +async function loadAnnouncements() { + try { + const result = await bulletinAPI.getAnnouncements(); + + if (result.success) { + allAnnouncements = result.data; + renderAnnouncements(result.data); + console.log(`[公告加载] 成功 | 条数: ${result.data.length}`); + + } else { + showError(result.error); + } + + } catch (error) { + console.error('加载公告失败:', error); + showError({ + title: '🧸 出错了', + message: '加载公告时遇到问题', + retryable: true + }); + } +} + +// 显示错误 +function showError(errorInfo) { + hideSkeleton(); + + const container = document.querySelector('.bulletin-container'); + if (!container) return; + + const retryButton = errorInfo.retryable ? + `` : ''; + + container.innerHTML = ` +
+
🧸
+
${errorInfo.title || '哎呀'}
+
${errorInfo.message || '服务暂时不可用'}
+ ${retryButton} +
+ `; +} + +// 暴露给全局 +window.loadAnnouncements = loadAnnouncements; \ No newline at end of file diff --git a/modules/M22-bulletin/js/config.js b/modules/M22-bulletin/js/config.js new file mode 100644 index 00000000..ec576728 --- /dev/null +++ b/modules/M22-bulletin/js/config.js @@ -0,0 +1,58 @@ +/** + * config.js - M22 环境配置中心 + * EL-6 · 真实API联调预备 · mock/production 一键切换 + */ + +const CONFIG = { + // 环境开关:'mock' 或 'production' + // 爸爸以后后端好了,把这里改成 'production' 就行 + // 环境开关:'mock' 或 'production' + // 🟡 注意:真实API地址还未就绪,当前使用mock模式 + // 等页页给了地址后,改成 'production' 并替换下面的 apiBaseUrl + env: 'mock', + + // API 基础地址(production模式下使用) + // 🟡 待替换:等页页给了真实地址后替换这个字符串 + apiBaseUrl: 'https://api.example.com/v1', // 待替换 + + // Mock 数据地址(开发阶段使用) + mockDataPath: 'data/mock-announcements.json', + + // 认证配置(如后端需要Token) + auth: { + enabled: false, // 是否启用认证,等后端需要时改成 true + tokenKey: 'awen_token', // localStorage 存储的key + getToken: function() { + return localStorage.getItem(this.tokenKey); + }, + setToken: function(token) { + localStorage.setItem(this.tokenKey, token); + }, + clearToken: function() { + localStorage.removeItem(this.tokenKey); + } + }, + + // 缓存配置 + cache: { + enabled: true, + key: 'm22_announcements_cache', + expiry: 30 * 60 * 1000 // 30分钟有效期(毫秒) + }, + + // 请求超时设置(毫秒) + timeout: 10000, + + // 重试配置 + retry: { + enabled: true, + maxRetries: 2, + delay: 1000 // 重试延迟(毫秒) + }, + + // 是否启用详细日志(开发时有用) + debug: true +}; + +// 不允许修改 +Object.freeze(CONFIG); \ No newline at end of file diff --git a/modules/M22-bulletin/style.css b/modules/M22-bulletin/style.css index 3ffffa9f..ee79abf3 100644 --- a/modules/M22-bulletin/style.css +++ b/modules/M22-bulletin/style.css @@ -1,4 +1,32 @@ -/* HoloLake公告栏系统深色科技风 */ +/* ===== CSS 变量定义(浅色/深色主题)===== */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f7fa; + --bg-tertiary: #eef1f5; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-tertiary: #64748b; + --accent-primary: #3b82f6; + --accent-secondary: #2563eb; + --border-color: #e2e8f0; +} + +/* 深色主题 */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --accent-primary: #60a5fa; + --accent-secondary: #3b82f6; + --border-color: #334155; + } +} + +/* ===== 基础样式 ===== */ * { margin: 0; padding: 0; @@ -6,560 +34,302 @@ } body { - font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; - background: #0a1628; - color: #e0e6ed; - min-height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + transition: background-color 0.3s, color 0.3s; } .app-container { - max-width: 580px; + max-width: 1200px; margin: 0 auto; - padding: 0 16px; + padding: 20px; } -/* ========== 顶部标题栏 ========== */ +/* ===== 头部样式 ===== */ .header { display: flex; justify-content: space-between; align-items: center; - padding: 24px 0 16px; - border-bottom: 1px solid rgba(255,255,255,0.08); + padding: 20px 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; } .header h1 { - font-size: 22px; - font-weight: 700; - color: #e0e6ed; + font-size: 1.8rem; + font-weight: 600; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .header-actions { display: flex; - align-items: center; gap: 12px; -} - -.pinned-badge { - background: rgba(255, 183, 77, 0.15); - color: #ffb74d; - font-size: 12px; - padding: 4px 10px; - border-radius: 12px; - font-weight: 600; -} - -.subscribe-btn { - background: rgba(79, 195, 247, 0.1); - color: #4fc3f7; - border: 1px solid rgba(79, 195, 247, 0.2); - padding: 6px 14px; - border-radius: 8px; - font-size: 13px; - cursor: pointer; -} - -.subscribe-btn:hover { - background: rgba(79, 195, 247, 0.2); -} - -/* ========== 频道栏 ========== */ -.channel-tab-bar { - display: flex; - gap: 8px; - padding: 16px 0; - border-bottom: 1px solid rgba(255,255,255,0.06); - overflow-x: auto; -} - -.channel-tab { - padding: 8px 18px; - border-radius: 20px; - border: 1px solid rgba(255,255,255,0.1); - background: transparent; - color: #8899aa; - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.channel-tab:hover { - border-color: rgba(255, 183, 77, 0.3); - color: #b0bec5; -} - -.channel-tab.active { - background: rgba(255,183,77,0.15); - border-color: #ffb74d; - color: #ffb74d; - font-weight: 600; -} - -/* ========== 公告列表 ========== */ -.bulletin-list { - padding: 16px 0; -} - -.bulletin-card { - position: relative; - display: flex; - gap: 16px; - padding: 16px; - border-radius: 12px; - background: rgba(20, 30, 45, 0.6); - border: 1px solid rgba(255,255,255,0.04); - margin-bottom: 12px; - transition: all 0.2s ease; -} - -.bulletin-card.pinned { - border: 1px solid rgba(255, 183, 77, 0.3); - background: rgba(255, 183, 77, 0.02); -} - -.pin-indicator { - position: absolute; - top: 8px; - right: 12px; - font-size: 11px; - color: #ffb74d; - font-weight: 600; -} - -/* 公告图标 */ -.bulletin-icon { - width: 42px; - height: 42px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - flex-shrink: 0; -} - -.bulletin-icon.system { background: rgba(156,39,176,0.12); } -.bulletin-icon.dev { background: rgba(76,175,80,0.12); } -.bulletin-icon.team { background: rgba(79,195,247,0.12); } - -/* 公告内容 */ -.bulletin-content { - flex: 1; - min-width: 0; -} - -.bulletin-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 6px; -} - -.bulletin-title { - font-size: 15px; - font-weight: 600; - color: #e0e6ed; -} - -.bulletin-time { - font-size: 12px; - color: #556677; - flex-shrink: 0; - margin-left: 12px; -} - -.bulletin-summary { - font-size: 13px; - color: #8899aa; - line-height: 1.6; - margin-bottom: 10px; -} - -/* 公告底部 */ -.bulletin-footer { - display: flex; - justify-content: space-between; align-items: center; } -.bulletin-tag { - display: inline-block; - font-size: 11px; - padding: 2px 8px; - border-radius: 6px; - font-weight: 500; -} - -.tag-system { background: rgba(156,39,176,0.12); color: #ce93d8; } -.tag-dev { background: rgba(76,175,80,0.12); color: #81c784; } -.tag-team { background: rgba(79,195,247,0.12); color: #4fc3f7; } - -.bulletin-author { - font-size: 11px; - color: #556677; -} - -/* ========== 底部状态栏 ========== */ -.footer { - display: flex; - justify-content: space-between; - padding: 20px 0; - border-top: 1px solid rgba(255,255,255,0.06); - font-size: 12px; - color: #556677; -} - -.footer-brand { - color: rgba(255, 183, 77, 0.5); -} - -/* ========================= 环节2:响应式布局 ========================= */ - -/* 平板(宽度 768px 以下) */ -@media (max-width: 768px) { - .app-container { - max-width: 100%; - padding: 0 12px; - } - header h1 { - font-size: 18px; - } - .channel-tab-bar { - gap: 6px; - padding: 12px 0; - } - .channel-tab { - padding: 6px 14px; - font-size: 12px; - } - .bulletin-card { - padding: 12px 10px; - } - .channel-tab-bar { - flex-wrap: nowrap; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - } - .channel-tab-bar::-webkit-scrollbar { - display: none; - } - .bulletin-card { - flex-direction: column; - gap: 8px; - } - .bulletin-icon { - width: 32px; - height: 32px; - font-size: 14px; - } - .bulletin-header { - flex-direction: column; - align-items: flex-start; - gap: 4px; - } - .bulletin-time { - margin-left: 0; - } - .pin-indicator { - top: 6px; - right: 8px; - font-size: 10px; - } - .footer { - flex-direction: column; - gap: 4px; - text-align: center; - } -} - -/* 已读公告样式 */ -.bulletin-card.read { - opacity: 0.6; -} -.bulletin-card.read .bulletin-title { - color: #8899aa; -} - -/* 订阅按钮激活状态 */ -.subscribe-btn.active { - background: rgba(79, 195, 247, 0.25); - border-color: #4fc3f7; - color: #4fc3f7; -} -/* ========== 响应式断点 ========== */ - -/* 已读公告样式 */ -.bulletin-card.read { - opacity: 0.7; -} -.bulletin-card.read h3 { - color: #888; - text-decoration: line-through 1px rgba(0,0,0,0.2); -} - -/* 订阅按钮激活状态 */ -.subscribe-btn.active { - border: 2px solid #0066ff; - background-color: #e6f0ff; - color: #0066ff; - font-weight: bold; -} - -/* 平板设备 ≤ 768px */ -@media (max-width: 768px) { - .container { - width: 100%; - padding: 0 20px; - } - - .bulletin-header h1 { - font-size: 1.8rem; - } - - .bulletin-grid { - gap: 12px; - } - - .bulletin-card { - padding: 12px; - } - - .bulletin-card .icon { - width: 32px; - height: 32px; - } - - .channel-tab-tabs { - gap: 8px; - } - - .channel-tab-tab { - padding: 6px 12px; - font-size: 0.9rem; - } -} - -/* 手机设备 ≤ 480px */ -@media (max-width: 480px) { - .bulletin-header { - flex-direction: column; - align-items: flex-start; - gap: 10px; - } - - .bulletin-header h1 { - font-size: 1.5rem; - margin-bottom: 5px; - } - - .subscribe-btn { - align-self: flex-start; - } - - .bulletin-card { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .bulletin-card .icon { - width: 40px; - height: 40px; - margin-bottom: 4px; - } - - .channel-tab-tabs { - overflow-x: auto; - white-space: nowrap; - flex-wrap: nowrap; - gap: 10px; - padding-bottom: 8px; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; /* Firefox隐藏滚动条 */ - } - - .channel-tab-tabs::-webkit-scrollbar { - display: none; /* Chrome/Safari隐藏滚动条 */ - } - - .channel-tab-tab { - display: inline-block; - float: none; - white-space: nowrap; - } - - .bulletin-footer { - flex-direction: column; - text-align: center; - gap: 8px; - } -}/* ===== 知秋:loading/error/empty 状态样式 ===== */ - -/* loading */ -.loading-state { - text-align: center; - padding: 40px 20px; - color: #666; -} - -.spinner { - width: 40px; - height: 40px; - margin: 0 auto 15px; - border: 3px solid #f3f3f3; - border-top: 3px solid #3498db; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* error */ -.error-state { - text-align: center; - padding: 40px 20px; - color: #e74c3c; -} - -.retry-btn { - background: #3498db; - color: white; - border: none; - padding: 8px 20px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - margin-top: 10px; - transition: background 0.3s; -} - -.retry-btn:hover { - background: #2980b9; -} - -/* empty */ -.empty-state { - text-align: center; - padding: 40px 20px; - color: #999; - font-size: 16px; -} - -/* 响应式适配 */ -@media (max-width: 600px) { - .loading-state, .error-state, .empty-state { - padding: 30px 15px; - } - - .spinner { - width: 30px; - height: 30px; - } -}/* ===== 频道栏和公告列表的间距 ===== */ -.channel-bar { - margin-bottom: 20px; /* 给频道栏加下边距 */ -} - -/* 或者也可以给公告容器加上边距 */ -.bulletin-container { - margin-top: 10px; -} -/* ===== 频道栏与上方标题栏的间距 ===== */ -.channel-bar { - margin-top: 15px; /* 给频道栏顶部增加间距,让它和上面的内容分开 */ -} - -/* 也可以调整标题栏的底部间距 */ -.header-actions { - margin-bottom: 5px; /* 或者给标题栏底部加点间距 */ -} -/* ===== 语言切换器样式 ===== */ .lang-switcher { - display: inline-flex; - margin-right: 15px; - gap: 5px; + display: flex; + gap: 4px; } .lang-btn { - background: #f0f0f0; /* 改成浅灰色背景 */ - border: 1px solid #999; /* 深一点的边框 */ - border-radius: 4px; - padding: 4px 8px; + padding: 6px 12px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; cursor: pointer; - font-size: 12px; - transition: all 0.3s; - color: #333; /* 文字颜色深灰色,保证看得见 */ + font-size: 0.9rem; } .lang-btn:hover { - background: #f0f0f0; -} - -.lang-btn.active { - background: #3498db; + background: var(--accent-primary); color: white; - border-color: #3498db; } -/* 焦点样式(键盘导航用) */ -.lang-btn:focus, -.channel-tab:focus, -.retry-btn:focus, -.bulletin-card:focus { - outline: 2px solid #3498db; - outline-offset: 2px; +.pinned-badge { + background: #fef3c7; + color: #92400e; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.85rem; } -/* 高对比度适配(可选) */ -@media (prefers-contrast: high) { - .lang-btn.active { - background: black; - border-color: black; - } +.subscribe-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; } -/* ========== 骨架屏样式 ========== */ + +/* ===== 频道栏 ===== */ +.channel-bar { + display: flex; + gap: 10px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.channel-tab { + padding: 8px 20px; + border: none; + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 30px; + cursor: pointer; + font-size: 0.95rem; + transition: all 0.2s; +} + +.channel-tab.active { + background: var(--accent-primary); + color: white; +} + +.channel-tab:hover:not(.active) { + background: var(--bg-tertiary); +} + +/* ===== 骨架屏样式 ===== */ +.skeleton-wrapper { + margin: 20px 0; +} + +.skeleton-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + .skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%); background-size: 200% 100%; - animation: skeleton-loading 1.5s infinite; + animation: loading 1.5s infinite; border-radius: 4px; } -@keyframes skeleton-loading { +@keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } +.skeleton-title { + width: 60%; + height: 24px; + margin-bottom: 12px; +} + .skeleton-text { + width: 100%; height: 16px; margin-bottom: 8px; - width: 80%; } .skeleton-text.short { width: 40%; } -.skeleton-title { - height: 24px; - margin-bottom: 12px; - width: 60%; +/* ===== 公告列表样式 ===== */ +.bulletin-container { + margin: 20px 0; + min-height: 200px; } -.skeleton-card { - padding: 16px; +.bulletin-item { + background: var(--bg-secondary); + border-radius: 12px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); +} + +.bulletin-title { + font-size: 1.3rem; + margin-bottom: 10px; + color: var(--text-primary); +} + +.bulletin-meta { + display: flex; + gap: 16px; + margin-bottom: 16px; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.bulletin-channel { + background: var(--bg-tertiary); + padding: 2px 10px; + border-radius: 20px; + color: var(--text-secondary); +} + +.bulletin-date { + color: var(--text-tertiary); +} + +.bulletin-content { + color: var(--text-secondary); + line-height: 1.7; +} + +/* ===== 底部样式 ===== */ +.footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + color: var(--text-tertiary); + font-size: 0.9rem; +} + +/* ===== EL-6 新增样式 ===== */ +/* 骨架屏平滑淡出 */ +.bulletin-container.data-loaded .bulletin-skeleton { + display: none; +} + +.bulletin-item { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 错误状态 */ +.error-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.error-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.error-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.error-message { + color: var(--text-secondary); + margin-bottom: 1rem; +} + +.error-suggestion { + color: var(--text-tertiary); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +.retry-btn { + background: var(--accent-primary); + color: white; + border: none; + padding: 0.8rem 2rem; border-radius: 8px; - background: var(--card-bg, #fff); - margin-bottom: 12px; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; } -/* 暗色主题适配 */ -[data-theme="dark"] .skeleton { - background: linear-gradient(90deg, #2a2a2a 25%, #333 50%, #2a2a2a 75%); - background-size: 200% 100%; - animation: skeleton-loading 1.5s infinite; +.retry-btn:hover { + background: var(--accent-secondary); + transform: translateY(-2px); +} + +/* 离线标记 */ +.offline-badge { + background: #ffd966; + color: #856404; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.9rem; + display: inline-block; + margin-bottom: 1rem; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-secondary); + border-radius: 12px; + margin: 2rem 0; +} + +.empty-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.empty-title { + font-size: 1.2rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.empty-desc { + color: var(--text-tertiary); +} } \ No newline at end of file