2026-03-11 11:06:16 +08:00
|
|
|
|
<<<<<<< HEAD
|
2026-03-11 10:57:23 +08:00
|
|
|
|
// ========== 确保 HoloLake 命名空间存在 ==========
|
|
|
|
|
|
window.HoloLake = window.HoloLake || {};
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 页面加载完成后执行 ==========
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
console.log('🚀 script.js 初始化');
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复所有状态
|
|
|
|
|
|
restoreState();
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定事件
|
|
|
|
|
|
bindEvents();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新布局信息
|
|
|
|
|
|
updateLayoutInfo();
|
|
|
|
|
|
|
|
|
|
|
|
// 监听窗口大小变化
|
|
|
|
|
|
window.addEventListener('resize', function() {
|
|
|
|
|
|
updateLayoutInfo();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 恢复状态 ==========
|
|
|
|
|
|
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 = '订阅';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 按频道筛选 ==========
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 更新布局信息 ==========
|
|
|
|
|
|
function updateLayoutInfo() {
|
|
|
|
|
|
const width = window.innerWidth;
|
|
|
|
|
|
let mode = '桌面';
|
|
|
|
|
|
|
|
|
|
|
|
if (width <= 480) {
|
|
|
|
|
|
mode = '手机';
|
|
|
|
|
|
} else if (width <= 768) {
|
|
|
|
|
|
mode = '平板';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📱 当前布局: ${mode}模式 (${width}px)`);
|
2026-03-11 11:06:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
=======
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// ===== script.js =====
|
2026-03-10 23:57:32 +08:00
|
|
|
|
// 知秋:主逻辑·动态渲染+loading+错误处理+i18n+无障碍
|
2026-03-08 23:17:48 +08:00
|
|
|
|
|
2026-03-10 18:35:37 +08:00
|
|
|
|
(function() {
|
|
|
|
|
|
// 状态变量
|
|
|
|
|
|
let allBulletins = []; // 所有公告(从API拿)
|
|
|
|
|
|
let currentChannel = '全部'; // 当前频道(默认全部)
|
|
|
|
|
|
let loading = false; // 是否正在加载
|
|
|
|
|
|
let error = null; // 错误信息
|
2026-03-08 23:17:48 +08:00
|
|
|
|
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// DOM元素
|
|
|
|
|
|
const container = document.querySelector('.bulletin-container');
|
|
|
|
|
|
const channelTabs = document.querySelectorAll('.channel-tab');
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
async function init() {
|
2026-03-10 23:57:32 +08:00
|
|
|
|
console.log("【知秋】M22公告栏·环节6启动");
|
|
|
|
|
|
|
|
|
|
|
|
// 监听语言切换事件
|
|
|
|
|
|
window.addEventListener('languagechange', () => {
|
|
|
|
|
|
render(); // 语言变了,重新渲染
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-10 18:35:37 +08:00
|
|
|
|
await loadBulletins(); // 加载数据
|
|
|
|
|
|
render(); // 初次渲染
|
2026-03-10 23:57:32 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-10 18:35:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 23:57:32 +08:00
|
|
|
|
// 键盘导航
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// 加载公告(核心!)
|
|
|
|
|
|
async function loadBulletins() {
|
|
|
|
|
|
loading = true;
|
|
|
|
|
|
error = null;
|
|
|
|
|
|
render(); // 显示loading
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await API.fetchBulletins();
|
|
|
|
|
|
allBulletins = data;
|
|
|
|
|
|
loading = false;
|
|
|
|
|
|
render();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("加载失败:", err);
|
|
|
|
|
|
loading = false;
|
2026-03-10 23:57:32 +08:00
|
|
|
|
error = "error";
|
2026-03-10 18:35:37 +08:00
|
|
|
|
render();
|
2026-03-08 23:17:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 23:57:32 +08:00
|
|
|
|
// 渲染公告列表
|
2026-03-10 18:35:37 +08:00
|
|
|
|
function render() {
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载状态
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
container.innerHTML = `
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<div class="loading-state" role="status" aria-live="polite">
|
2026-03-10 18:35:37 +08:00
|
|
|
|
<div class="spinner"></div>
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<p>${window.i18n.t('loading')}</p>
|
2026-03-10 18:35:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 23:57:32 +08:00
|
|
|
|
// 错误状态
|
2026-03-10 18:35:37 +08:00
|
|
|
|
if (error) {
|
|
|
|
|
|
const isOffline = !navigator.onLine;
|
2026-03-10 23:57:32 +08:00
|
|
|
|
const offlineHint = isOffline ? window.i18n.t('offline') : '';
|
2026-03-10 18:35:37 +08:00
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<div class="error-state" role="alert">
|
|
|
|
|
|
<p>😢 ${offlineHint}${window.i18n.t('error')}</p>
|
|
|
|
|
|
<button class="retry-btn" id="retryBtn">${window.i18n.t('retry')}</button>
|
2026-03-10 18:35:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
const retryBtn = document.getElementById('retryBtn');
|
|
|
|
|
|
if (retryBtn) {
|
|
|
|
|
|
retryBtn.addEventListener('click', () => {
|
|
|
|
|
|
loadBulletins();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 空数据状态
|
|
|
|
|
|
if (allBulletins.length === 0) {
|
|
|
|
|
|
container.innerHTML = `
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<div class="empty-state" role="status">
|
|
|
|
|
|
<p>${window.i18n.t('empty')}</p>
|
2026-03-10 18:35:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 正常渲染:根据频道筛选
|
|
|
|
|
|
const filtered = currentChannel === '全部'
|
|
|
|
|
|
? allBulletins
|
|
|
|
|
|
: allBulletins.filter(item => item.channel === currentChannel);
|
|
|
|
|
|
|
|
|
|
|
|
if (filtered.length === 0) {
|
|
|
|
|
|
container.innerHTML = `
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<div class="empty-state" role="status">
|
|
|
|
|
|
<p>${window.i18n.t('emptyChannel')}</p>
|
2026-03-10 18:35:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用环节4的createCard(如果不存在就降级)
|
|
|
|
|
|
let html = '';
|
2026-03-10 23:57:32 +08:00
|
|
|
|
filtered.forEach((item, index) => {
|
2026-03-10 18:35:37 +08:00
|
|
|
|
if (typeof createCard === 'function') {
|
|
|
|
|
|
html += createCard(item);
|
2026-03-08 23:17:48 +08:00
|
|
|
|
} else {
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// 降级方案(保证显示)
|
|
|
|
|
|
html += `
|
2026-03-10 23:57:32 +08:00
|
|
|
|
<div class="bulletin-card" role="listitem" tabindex="0" aria-label="${item.title}">
|
2026-03-10 18:35:37 +08:00
|
|
|
|
<h3>${item.title}</h3>
|
|
|
|
|
|
<p>${item.content}</p>
|
|
|
|
|
|
<div class="meta">
|
|
|
|
|
|
<span class="channel">${item.channel}</span>
|
|
|
|
|
|
<span class="date">${item.date}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
2026-03-08 23:17:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-10 18:35:37 +08:00
|
|
|
|
container.innerHTML = html;
|
2026-03-08 23:17:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// 事件监听(频道切换)
|
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
|
|
channelTabs.forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', (e) => {
|
|
|
|
|
|
channelTabs.forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
e.target.classList.add('active');
|
|
|
|
|
|
currentChannel = e.target.dataset.channel || '全部';
|
|
|
|
|
|
render();
|
|
|
|
|
|
});
|
2026-03-08 23:17:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 18:35:37 +08:00
|
|
|
|
// 启动一切
|
|
|
|
|
|
init();
|
2026-03-11 11:06:16 +08:00
|
|
|
|
})();
|
|
|
|
|
|
>>>>>>> origin/main
|