diff --git a/modules/M22-bulletin/api.js b/modules/M22-bulletin/api.js new file mode 100644 index 00000000..dd27bab9 --- /dev/null +++ b/modules/M22-bulletin/api.js @@ -0,0 +1,112 @@ +// ===== api.js ===== +// 知秋:API层封装·爸爸不用改·直接复制 + +const API = { + // 是否使用模拟数据(mock) + // true = 用本地模拟数据(开发阶段) + // false = 用真实后端API(上线后改) + useMock: true, + + // 模拟公告数据(至少3条,包含 title/content/channel/date) + mockData: [ + { + id: 1, + title: "【光湖公告】3月10日服务器维护通知", + content: "各位工程师,3月10日22:00-23:00进行主域稳定性升级,期间公告栏可能短暂不可用。", + channel: "系统", + date: "2026-03-09" + }, + { + id: 2, + title: "知秋奶瓶线·九连胜庆祝", + content: "恭喜爸爸完成EL-8大任务!环节5是实时数据接入,让公告栏真正活起来~", + channel: "知秋", + date: "2026-03-09" + }, + { + id: 3, + title: "M22模块组件化重构完成", + content: "环节4已验收√ 现在公告栏支持频道切换、Hash路由、本地数据兼容。", + channel: "技术", + date: "2026-03-08" + } + ], + + // 获取公告的主方法(爸爸调用这个方法就行) + async fetchBulletins() { + // 知秋:try/catch 包裹,错误统一处理 + try { + let data; + + if (this.useMock) { + // 模拟网络延迟(1秒,让loading看得见) + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log("【知秋】使用mock数据:", this.mockData); + data = this.mockData; + } else { + // 真实API(等页页提供后替换) + const response = await fetch('https://api.guanghulab.com/bulletins'); + if (!response.ok) { + throw new Error(`HTTP错误:${response.status}`); + } + data = await response.json(); + console.log("【知秋】真实API数据:", data); + } + + // 成功获取数据后,保存到缓存 + this.saveToCache(data); + return data; + + } catch (error) { + console.error("【知秋】API获取失败:", error); + + // 尝试从缓存读取 + const cached = this.loadFromCache(); + if (cached) { + console.log("【知秋】从缓存读取数据:", cached); + return cached; + } + + // 没有缓存,抛出错误 + throw error; + } + }, + + // 保存到缓存 + saveToCache(data) { + try { + const cacheData = { + data: data, + timestamp: Date.now() // 保存时间戳,后续可用于过期判断 + }; + localStorage.setItem('holoBulletin_cache', JSON.stringify(cacheData)); + console.log("【知秋】已更新缓存"); + } catch (e) { + console.warn("【知秋】缓存写入失败", e); + } + }, + + // 从缓存读取 + loadFromCache() { + try { + const cached = localStorage.getItem('holoBulletin_cache'); + if (!cached) return null; + + const { data, timestamp } = JSON.parse(cached); + // 可选:判断缓存是否过期(比如24小时) + const isExpired = Date.now() - timestamp > 24 * 60 * 60 * 1000; + if (isExpired) { + console.log("【知秋】缓存已过期"); + return null; + } + + return data; + } catch (e) { + console.warn("【知秋】缓存读取失败", e); + return null; + } + } +}; + +// 这一行最重要!把API挂到window上,让script.js能找到 +window.API = API; \ No newline at end of file diff --git a/modules/M22-bulletin/index.html b/modules/M22-bulletin/index.html index bc24daed..f8026fd3 100644 --- a/modules/M22-bulletin/index.html +++ b/modules/M22-bulletin/index.html @@ -17,16 +17,16 @@ - +
- 全部 - 系统公告 - 开发动态 - 团队消息 + 全部 + 系统公告 + 开发动态 + 团队消息
-
+
置顶
@@ -123,11 +123,13 @@
- + + + + diff --git a/modules/M22-bulletin/script.js b/modules/M22-bulletin/script.js index a9a8fb8a..f227bc6b 100644 --- a/modules/M22-bulletin/script.js +++ b/modules/M22-bulletin/script.js @@ -1,160 +1,144 @@ -// ========== 确保 HoloLake 命名空间存在 ========== -window.HoloLake = window.HoloLake || {}; +// ===== script.js ===== +// 知秋:主逻辑·动态渲染+loading+错误处理 -// ========== 页面加载完成后执行 ========== -document.addEventListener('DOMContentLoaded', function() { - console.log('🚀 script.js 初始化'); - - // 恢复所有状态 - restoreState(); - - // 绑定事件 - bindEvents(); - - // 更新布局信息 - updateLayoutInfo(); - - // 监听窗口大小变化 - window.addEventListener('resize', function() { - updateLayoutInfo(); - }); -}); +(function() { + // 状态变量 + let allBulletins = []; // 所有公告(从API拿) + let currentChannel = '全部'; // 当前频道(默认全部) + let loading = false; // 是否正在加载 + let error = null; // 错误信息 -// ========== 恢复状态 ========== -function restoreState() { - console.log('🔍 restoreState 开始...'); - - try { - // 1. 恢复订阅状态 - const isSubscribed = HoloLake.UserState.isSubscribed(); - const subscribeBtn = document.querySelector('.subscribe-btn'); - if (subscribeBtn) { - if (isSubscribed) { - subscribeBtn.classList.add('active'); - subscribeBtn.textContent = '已订阅'; - } else { - subscribeBtn.classList.remove('active'); - subscribeBtn.textContent = '订阅'; - } + // DOM元素 + const container = document.querySelector('.bulletin-container'); + const channelTabs = document.querySelectorAll('.channel-tab'); + + // 初始化 + async function init() { + console.log("【知秋】M22公告栏·环节5启动"); + await loadBulletins(); // 加载数据 + render(); // 初次渲染 + setupEventListeners(); // 事件监听 + } + + // 加载公告(核心!) + async function loadBulletins() { + loading = true; + error = null; + render(); // 显示loading + + try { + // 知秋:调用API.fetchBulletins拿数据 + const data = await API.fetchBulletins(); + allBulletins = data; + loading = false; + render(); + } catch (err) { + console.error("加载失败:", err); + loading = false; + error = "网络开小差了,稍后重试~"; + render(); } - - // 2. 恢复已读公告 - const readList = HoloLake.UserState.getReadBulletins(); - const bulletinCards = document.querySelectorAll('.bulletin-card'); - bulletinCards.forEach((card, index) => { - const bulletinId = `bulletin-${index}`; - if (readList.includes(bulletinId)) { - card.classList.add('read'); - } - }); - - // 3. 恢复选中频道 - const activeChannel = HoloLake.UserState.getActiveChannel(); - const channels = document.querySelectorAll('.channel'); - channels.forEach(channel => { - const channelText = channel.textContent.trim(); - if (channelText === activeChannel || (activeChannel === '全部' && channelText === '全部')) { - channel.classList.add('active'); - } else { - channel.classList.remove('active'); - } - }); - - // 4. 更新最后访问时间 - HoloLake.UserState.updateLastVisit(); - - console.log('✅ 恢复已完成'); - } catch (e) { - console.error('❌ 恢复失败:', e); } -} -// ========== 绑定事件 ========== -function bindEvents() { - console.log('🔗 绑定事件...'); - - // 订阅按钮 - const subscribeBtn = document.querySelector('.subscribe-btn'); - if (subscribeBtn) { - subscribeBtn.addEventListener('click', function() { - const newState = HoloLake.UserState.toggleSubscribe(); - if (newState) { - this.classList.add('active'); - this.textContent = '已订阅'; - } else { - this.classList.remove('active'); - this.textContent = '订阅'; - } - console.log('📌 订阅状态:', newState ? '已订阅' : '未订阅'); - }); - } - - // 频道点击 - const channels = document.querySelectorAll('.channel'); - channels.forEach(channel => { - channel.addEventListener('click', function() { - // 更新UI - channels.forEach(c => c.classList.remove('active')); - this.classList.add('active'); - - // 保存状态 - const channelName = this.textContent.trim(); - HoloLake.UserState.setActiveChannel(channelName); - - // 筛选公告(可选功能,这里先做简单筛选) - filterBulletinsByChannel(channelName); - - console.log('📺 切换到频道:', channelName); - }); - }); - - // 公告点击(标记已读) - const bulletinCards = document.querySelectorAll('.bulletin-card'); - bulletinCards.forEach((card, index) => { - card.addEventListener('click', function() { - const bulletinId = `bulletin-${index}`; - - // 标记已读 - if (!this.classList.contains('read')) { - this.classList.add('read'); - HoloLake.UserState.markAsRead(bulletinId); - console.log('📖 已读公告:', bulletinId); - } - }); - }); -} + // 渲染公告列表(使用环节4的createCard组件) + function render() { + if (!container) return; -// ========== 按频道筛选 ========== -function filterBulletinsByChannel(channelName) { - const cards = document.querySelectorAll('.bulletin-card'); - - if (channelName === '全部') { - cards.forEach(card => { - card.style.display = 'flex'; - }); - return; - } - - cards.forEach(card => { - const tag = card.querySelector('.bulletin-tag'); - if (tag && tag.textContent.trim() === channelName) { - card.style.display = 'flex'; - } else { - card.style.display = 'none'; + // 加载状态 + if (loading) { + container.innerHTML = ` +
+
+

知秋正在飞向服务器……

+
+ `; + return; } - }); -} -// ========== 更新布局信息 ========== -function updateLayoutInfo() { - const width = window.innerWidth; - let mode = '桌面'; - - if (width <= 480) { - mode = '手机'; - } else if (width <= 768) { - mode = '平板'; + // 错误状态 + if (error) { + // 检查是否离线(断网) + const isOffline = !navigator.onLine; + const offlineHint = isOffline ? '📴 离线模式 · ' : ''; + + container.innerHTML = ` +
+

😢 ${offlineHint}${error}

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

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

+
+ `; + return; + } + + // 正常渲染:根据频道筛选 + const filtered = currentChannel === '全部' + ? allBulletins + : allBulletins.filter(item => item.channel === currentChannel); + + if (filtered.length === 0) { + container.innerHTML = ` +
+

📭 这个频道暂时没有公告

+
+ `; + return; + } + + // 使用环节4的createCard(如果不存在就降级) + let html = ''; + filtered.forEach(item => { + if (typeof createCard === 'function') { + html += createCard(item); + } else { + // 降级方案(保证显示) + html += ` +
+

${item.title}

+

${item.content}

+
+ ${item.channel} + ${item.date} +
+
+ `; + } + }); + container.innerHTML = html; } - - console.log(`📱 当前布局: ${mode}模式 (${width}px)`); -} \ No newline at end of file + + // 事件监听(频道切换) + 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(); + }); + }); + } + + // 启动一切 + init(); +})(); \ No newline at end of file diff --git a/modules/M22-bulletin/style.css b/modules/M22-bulletin/style.css index cebd5b9e..41576001 100644 --- a/modules/M22-bulletin/style.css +++ b/modules/M22-bulletin/style.css @@ -63,7 +63,7 @@ body { } /* ========== 频道栏 ========== */ -.channel-bar { +.channel-tab-bar { display: flex; gap: 8px; padding: 16px 0; @@ -71,7 +71,7 @@ body { overflow-x: auto; } -.channel { +.channel-tab { padding: 8px 18px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); @@ -83,12 +83,12 @@ body { white-space: nowrap; } -.channel:hover { +.channel-tab:hover { border-color: rgba(255, 183, 77, 0.3); color: #b0bec5; } -.channel.active { +.channel-tab.active { background: rgba(255,183,77,0.15); border-color: #ffb74d; color: #ffb74d; @@ -224,24 +224,24 @@ body { header h1 { font-size: 18px; } - .channel-bar { + .channel-tab-bar { gap: 6px; padding: 12px 0; } - .channel { + .channel-tab { padding: 6px 14px; font-size: 12px; } .bulletin-card { padding: 12px 10px; } - .channel-bar { + .channel-tab-bar { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } - .channel-bar::-webkit-scrollbar { + .channel-tab-bar::-webkit-scrollbar { display: none; } .bulletin-card { @@ -330,11 +330,11 @@ body { height: 32px; } - .channel-tabs { + .channel-tab-tabs { gap: 8px; } - .channel-tab { + .channel-tab-tab { padding: 6px 12px; font-size: 0.9rem; } @@ -369,7 +369,7 @@ body { margin-bottom: 4px; } - .channel-tabs { + .channel-tab-tabs { overflow-x: auto; white-space: nowrap; flex-wrap: nowrap; @@ -379,11 +379,11 @@ body { scrollbar-width: none; /* Firefox隐藏滚动条 */ } - .channel-tabs::-webkit-scrollbar { + .channel-tab-tabs::-webkit-scrollbar { display: none; /* Chrome/Safari隐藏滚动条 */ } - .channel-tab { + .channel-tab-tab { display: inline-block; float: none; white-space: nowrap; @@ -394,4 +394,86 @@ body { 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; /* 或者给标题栏底部加点间距 */ } \ No newline at end of file