397 lines
17 KiB
HTML
397 lines
17 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>光湖频道 · 一体化版</title>
|
|||
|
|
<style>
|
|||
|
|
.channel-container {
|
|||
|
|
max-width: 1200px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 20px;
|
|||
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|||
|
|
}
|
|||
|
|
.channel-nav {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
border-bottom: 2px solid #e5e7eb;
|
|||
|
|
padding-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
.channel-btn {
|
|||
|
|
padding: 10px 20px;
|
|||
|
|
border: none;
|
|||
|
|
background: #f3f4f6;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 16px;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
.channel-btn:hover {
|
|||
|
|
background: #e5e7eb;
|
|||
|
|
}
|
|||
|
|
.channel-btn.active {
|
|||
|
|
background: #3b82f6;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
.channel-btn.visited {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
.channel-btn.visited::after {
|
|||
|
|
content: "✓";
|
|||
|
|
position: absolute;
|
|||
|
|
top: -5px;
|
|||
|
|
right: -5px;
|
|||
|
|
background: #10b981;
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
.channel-content {
|
|||
|
|
min-height: 400px;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|||
|
|
transition: opacity 0.3s ease-in-out;
|
|||
|
|
}
|
|||
|
|
.channel-content.fade-out {
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
.channel-content.fade-in {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="channel-container">
|
|||
|
|
<nav class="channel-nav">
|
|||
|
|
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
|||
|
|
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
|||
|
|
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
|||
|
|
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
|||
|
|
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
|||
|
|
</nav>
|
|||
|
|
<main id="channel-content" class="channel-content">
|
|||
|
|
<!-- 动态内容会加载到这里 -->
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// ==================== 所有代码整合在一个文件里 ====================
|
|||
|
|
|
|||
|
|
// ---------- 模块注册表 ----------
|
|||
|
|
window.ModuleRegistry = {
|
|||
|
|
modules: {},
|
|||
|
|
register: function(name, module) {
|
|||
|
|
this.modules[name] = module;
|
|||
|
|
console.log(`[registry] 模块 ${name} 已注册`);
|
|||
|
|
},
|
|||
|
|
get: function(name) {
|
|||
|
|
return this.modules[name];
|
|||
|
|
},
|
|||
|
|
list: function() {
|
|||
|
|
return Object.keys(this.modules);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 事件总线 ----------
|
|||
|
|
window.EventBus = {
|
|||
|
|
listeners: {},
|
|||
|
|
on: function(event, callback) {
|
|||
|
|
if (!this.listeners[event]) this.listeners[event] = [];
|
|||
|
|
this.listeners[event].push(callback);
|
|||
|
|
console.log(`[事件总线] 订阅事件: ${event}`);
|
|||
|
|
},
|
|||
|
|
off: function(event, callback) {
|
|||
|
|
if (!this.listeners[event]) return;
|
|||
|
|
if (!callback) delete this.listeners[event];
|
|||
|
|
else this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
|||
|
|
console.log(`[事件总线] 取消订阅: ${event}`);
|
|||
|
|
},
|
|||
|
|
emit: function(event, data) {
|
|||
|
|
console.log(`[事件总线] 发送事件: ${event}`, data);
|
|||
|
|
if (!this.listeners[event]) return;
|
|||
|
|
this.listeners[event].forEach(callback => {
|
|||
|
|
try { callback(data); } catch (e) { console.error(`[事件总线] 执行回调出错: ${e}`); }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 模块生命周期 ----------
|
|||
|
|
window.ModuleLifecycle = {
|
|||
|
|
activeModules: new Set(),
|
|||
|
|
onLoad: function(moduleName) {
|
|||
|
|
this.activeModules.add(moduleName);
|
|||
|
|
console.log(`[生命周期] 模块加载: ${moduleName}`);
|
|||
|
|
EventBus.emit('module:loaded', { module: moduleName, time: Date.now() });
|
|||
|
|
},
|
|||
|
|
onUnload: function(moduleName) {
|
|||
|
|
this.activeModules.delete(moduleName);
|
|||
|
|
console.log(`[生命周期] 模块卸载: ${moduleName}`);
|
|||
|
|
EventBus.emit('module:unloaded', { module: moduleName, time: Date.now() });
|
|||
|
|
},
|
|||
|
|
getActiveModules: function() {
|
|||
|
|
return Array.from(this.activeModules);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 频道状态 ----------
|
|||
|
|
window.ChannelState = {
|
|||
|
|
STORAGE_KEY: 'hololake_channel_state',
|
|||
|
|
saveState: function(state) {
|
|||
|
|
const data = { ...state, timestamp: Date.now(), visited: state.visited || [] };
|
|||
|
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
|||
|
|
console.log('[state] 保存状态:', data);
|
|||
|
|
},
|
|||
|
|
restoreState: function() {
|
|||
|
|
const saved = localStorage.getItem(this.STORAGE_KEY);
|
|||
|
|
if (!saved) return null;
|
|||
|
|
try {
|
|||
|
|
const state = JSON.parse(saved);
|
|||
|
|
console.log('[state] 恢复状态:', state);
|
|||
|
|
return state;
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[state] 解析失败:', e);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
markVisited: function(channel) {
|
|||
|
|
const state = this.restoreState() || { visited: [] };
|
|||
|
|
if (!state.visited.includes(channel)) {
|
|||
|
|
state.visited.push(channel);
|
|||
|
|
this.saveState(state);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
clearState: function() {
|
|||
|
|
localStorage.removeItem(this.STORAGE_KEY);
|
|||
|
|
console.log('[state] 已清除');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 模块加载器 ----------
|
|||
|
|
window.ModuleLoader = {
|
|||
|
|
loadModule: function(moduleName, containerId) {
|
|||
|
|
console.log(`[loader] 加载模块: ${moduleName}`);
|
|||
|
|
const module = ModuleRegistry.get(moduleName);
|
|||
|
|
if (!module) {
|
|||
|
|
console.error(`[loader] 模块 ${moduleName} 未注册`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const container = document.getElementById(containerId);
|
|||
|
|
if (!container) {
|
|||
|
|
console.error(`[loader] 容器 ${containerId} 不存在`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (module.init && typeof module.init === 'function') {
|
|||
|
|
module.init(container);
|
|||
|
|
} else {
|
|||
|
|
container.innerHTML = `<div>模块 ${moduleName} 内容</div>`;
|
|||
|
|
}
|
|||
|
|
ModuleLifecycle.onLoad(moduleName);
|
|||
|
|
},
|
|||
|
|
unloadModule: function(moduleName) {
|
|||
|
|
console.log(`[loader] 卸载模块: ${moduleName}`);
|
|||
|
|
const module = ModuleRegistry.get(moduleName);
|
|||
|
|
if (module && module.destroy && typeof module.destroy === 'function') {
|
|||
|
|
module.destroy();
|
|||
|
|
}
|
|||
|
|
ModuleLifecycle.onUnload(moduleName);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 路由 ----------
|
|||
|
|
window.ChannelRouter = {
|
|||
|
|
currentChannel: 'home',
|
|||
|
|
contentEl: null,
|
|||
|
|
init: function(contentElement) {
|
|||
|
|
this.contentEl = contentElement;
|
|||
|
|
console.log('[router] 初始化');
|
|||
|
|
const savedState = ChannelState.restoreState();
|
|||
|
|
if (savedState && savedState.currentChannel) {
|
|||
|
|
this.currentChannel = savedState.currentChannel;
|
|||
|
|
}
|
|||
|
|
this.loadChannel(this.currentChannel);
|
|||
|
|
if (savedState && savedState.visited) {
|
|||
|
|
savedState.visited.forEach(channel => {
|
|||
|
|
document.querySelectorAll('.channel-btn').forEach(btn => {
|
|||
|
|
if (btn.dataset.channel === channel) btn.classList.add('visited');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
navigateTo: function(channel) {
|
|||
|
|
if (channel === this.currentChannel) return;
|
|||
|
|
console.log(`[router] 导航到: ${channel}`);
|
|||
|
|
this.contentEl.classList.add('fade-out');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.loadChannel(channel);
|
|||
|
|
this.contentEl.classList.remove('fade-out');
|
|||
|
|
this.contentEl.classList.add('fade-in');
|
|||
|
|
setTimeout(() => { this.contentEl.classList.remove('fade-in'); }, 300);
|
|||
|
|
}, 300);
|
|||
|
|
this.currentChannel = channel;
|
|||
|
|
document.querySelectorAll('.channel-btn').forEach(btn => {
|
|||
|
|
btn.classList.remove('active');
|
|||
|
|
if (btn.dataset.channel === channel) btn.classList.add('active');
|
|||
|
|
});
|
|||
|
|
ChannelState.saveState({ currentChannel: channel, visited: this.getVisitedChannels() });
|
|||
|
|
},
|
|||
|
|
loadChannel: function(channel) {
|
|||
|
|
if (!this.contentEl) return;
|
|||
|
|
let content = '';
|
|||
|
|
switch(channel) {
|
|||
|
|
case 'home':
|
|||
|
|
content = '<h2>🏠 首页</h2><p>欢迎来到光湖频道</p>';
|
|||
|
|
break;
|
|||
|
|
case 'module-a':
|
|||
|
|
content = '<h2>📦 模块A</h2><button id="sendMsgBtn">发送消息给模块B</button><div id="msgDisplay"></div>';
|
|||
|
|
break;
|
|||
|
|
case 'module-b':
|
|||
|
|
content = '<h2>📦 模块B</h2><div id="receivedMsg">等待消息...</div>';
|
|||
|
|
break;
|
|||
|
|
case 'module-c':
|
|||
|
|
content = '<h2>📦 模块C</h2><p>这是模块C的内容</p>';
|
|||
|
|
break;
|
|||
|
|
case 'debug':
|
|||
|
|
this.loadDebugPanel();
|
|||
|
|
return;
|
|||
|
|
default:
|
|||
|
|
content = '<h2>未知频道</h2>';
|
|||
|
|
}
|
|||
|
|
this.contentEl.innerHTML = content;
|
|||
|
|
ModuleLifecycle.onLoad(channel);
|
|||
|
|
if (channel === 'module-a') {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const btn = document.getElementById('sendMsgBtn');
|
|||
|
|
if (btn) {
|
|||
|
|
btn.addEventListener('click', () => {
|
|||
|
|
EventBus.emit('module-a:message', { text: '你好,模块B!', from: '模块A' });
|
|||
|
|
document.getElementById('msgDisplay').innerHTML = '✅ 消息已发送';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
if (channel === 'module-b') {
|
|||
|
|
this.subscribeModuleB();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
loadDebugPanel: function() {
|
|||
|
|
// 调试面板直接用内嵌 HTML,避免 fetch 问题
|
|||
|
|
this.contentEl.innerHTML = `
|
|||
|
|
<div class="debug-panel">
|
|||
|
|
<h2>🐞 调试面板</h2>
|
|||
|
|
<div class="module-list"><strong>当前活跃模块:</strong> <span id="active-modules">加载中...</span></div>
|
|||
|
|
<h3>事件总线聊天记录</h3>
|
|||
|
|
<ul class="message-list" id="debug-messages" style="border:1px solid #ccc; padding:10px; height:200px; overflow-y:auto; background:#f9f9f9;"></ul>
|
|||
|
|
<div style="display:flex; gap:10px; margin:10px 0;">
|
|||
|
|
<input type="text" id="debug-input" placeholder="输入要发送的消息..." style="flex:1; padding:8px;">
|
|||
|
|
<button id="debug-send" style="padding:8px 16px; background:#3b82f6; color:white; border:none; border-radius:4px;">发送测试消息</button>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex; gap:10px;">
|
|||
|
|
<button id="debug-clear" style="padding:8px 16px; background:#ef4444; color:white; border:none; border-radius:4px;">清除状态</button>
|
|||
|
|
<button id="debug-refresh" onclick="location.reload()" style="padding:8px 16px; background:#6b7280; color:white; border:none; border-radius:4px;">刷新页面</button>
|
|||
|
|
</div>
|
|||
|
|
<p><small>消息会广播给所有订阅的模块</small></p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
ModuleLifecycle.onLoad('debug');
|
|||
|
|
this.initDebugPanel();
|
|||
|
|
},
|
|||
|
|
initDebugPanel: function() {
|
|||
|
|
const msgList = document.getElementById('debug-messages');
|
|||
|
|
if (msgList && window.debugMessages) {
|
|||
|
|
window.debugMessages.forEach(msg => {
|
|||
|
|
const li = document.createElement('li');
|
|||
|
|
li.textContent = `[${msg.time}] ${msg.event}: ${JSON.stringify(msg.data)}`;
|
|||
|
|
msgList.appendChild(li);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
const sendBtn = document.getElementById('debug-send');
|
|||
|
|
const input = document.getElementById('debug-input');
|
|||
|
|
if (sendBtn && input) {
|
|||
|
|
sendBtn.addEventListener('click', () => {
|
|||
|
|
const text = input.value.trim();
|
|||
|
|
if (text) {
|
|||
|
|
EventBus.emit('debug:message', { text, from: '调试面板' });
|
|||
|
|
input.value = '';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
const clearBtn = document.getElementById('debug-clear');
|
|||
|
|
if (clearBtn) {
|
|||
|
|
clearBtn.addEventListener('click', () => {
|
|||
|
|
ChannelState.clearState();
|
|||
|
|
alert('状态已清除,刷新页面生效');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
// 更新活跃模块
|
|||
|
|
const updateActive = () => {
|
|||
|
|
const span = document.getElementById('active-modules');
|
|||
|
|
if (span) {
|
|||
|
|
const modules = ModuleLifecycle.getActiveModules();
|
|||
|
|
span.textContent = modules.length ? modules.join('、') : '无';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
EventBus.on('module:loaded', updateActive);
|
|||
|
|
EventBus.on('module:unloaded', updateActive);
|
|||
|
|
updateActive();
|
|||
|
|
},
|
|||
|
|
subscribeModuleB: function() {
|
|||
|
|
const handleMessage = (data) => {
|
|||
|
|
const display = document.getElementById('receivedMsg');
|
|||
|
|
if (display) {
|
|||
|
|
display.innerHTML = `📩 收到消息: ${data.text} (来自 ${data.from})`;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
EventBus.off('module-a:message');
|
|||
|
|
EventBus.on('module-a:message', handleMessage);
|
|||
|
|
},
|
|||
|
|
getVisitedChannels: function() {
|
|||
|
|
const visited = [];
|
|||
|
|
document.querySelectorAll('.channel-btn.visited').forEach(btn => visited.push(btn.dataset.channel));
|
|||
|
|
return visited;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- 应用启动 ----------
|
|||
|
|
console.log('[app] 启动中...');
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
console.log('[app] DOM 已加载');
|
|||
|
|
const contentEl = document.getElementById('channel-content');
|
|||
|
|
if (!contentEl) {
|
|||
|
|
console.error('[app] 找不到 #channel-content');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (window.ChannelRouter) {
|
|||
|
|
ChannelRouter.init(contentEl);
|
|||
|
|
}
|
|||
|
|
document.querySelectorAll('.channel-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', function() {
|
|||
|
|
const channel = this.dataset.channel;
|
|||
|
|
if (channel && window.ChannelRouter) {
|
|||
|
|
ChannelRouter.navigateTo(channel);
|
|||
|
|
this.classList.add('visited');
|
|||
|
|
ChannelState.markVisited(channel);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 拦截事件总线消息用于调试
|
|||
|
|
const originalEmit = EventBus.emit;
|
|||
|
|
EventBus.emit = function(event, data) {
|
|||
|
|
if (!window.debugMessages) window.debugMessages = [];
|
|||
|
|
window.debugMessages.push({ event, data, time: new Date().toLocaleTimeString() });
|
|||
|
|
originalEmit.call(this, event, data);
|
|||
|
|
};
|
|||
|
|
console.log('[app] 事件总线拦截器已安装');
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|