From 0534c4e1fe4c9be57d7e54703af64f63da663771 Mon Sep 17 00:00:00 2001
From: juzi0412 <1824680224@qq.com>
Date: Sat, 14 Mar 2026 01:56:47 +0800
Subject: [PATCH] =?UTF-8?q?[BC-M-CHANNEL-006-JZ][DEV-010]=20=E7=8E=AF?=
=?UTF-8?q?=E8=8A=828=20=E9=A2=91=E9=81=93=E6=95=B0=E6=8D=AE=E9=9D=A2?=
=?UTF-8?q?=E6=9D=BF=E4=B8=8E=E6=80=A7=E8=83=BD=E7=9B=91=E6=8E=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
channel-enhancements.js | 318 ++++
channel-router.js | 153 ++
event-bus.js | 55 +
index.html | 0
module-lifecycle.js | 90 ++
modules/m-channel/adapters/m06-adapter.js | 61 +
modules/m-channel/adapters/m08-adapter.js | 47 +
modules/m-channel/adapters/m11-adapter.js | 47 +
modules/m-channel/adapters/module-adapter.js | 140 ++
modules/m-channel/app.js | 85 +-
modules/m-channel/backup-混乱版/app.js | 42 +
.../backup-混乱版/channel-router-backup.js | 98 ++
.../m-channel/backup-混乱版/channel-router.js | 267 ++++
.../m-channel/backup-混乱版/channel-state.js | 149 ++
.../m-channel/backup-混乱版/channel-style.css | 132 ++
.../backup-混乱版/channel-transition.css | 83 +
modules/m-channel/backup-混乱版/event-bus.js | 111 ++
modules/m-channel/backup-混乱版/index.html | 33 +
.../backup-混乱版/module-lifecycle.js | 90 ++
.../m-channel/backup-混乱版/module-loader.js | 116 ++
.../backup-混乱版/module-registry.js | 7 +
modules/m-channel/channel-analytics.js | 163 ++
modules/m-channel/channel-complete.html | 267 ++++
modules/m-channel/channel-dashboard.css | 199 +++
modules/m-channel/channel-dashboard.js | 201 +++
modules/m-channel/channel-enhancements.js | 338 ++++
modules/m-channel/channel-favorites.js | 271 ++++
modules/m-channel/channel-layout.css | 145 ++
modules/m-channel/channel-notifications.css | 200 +++
modules/m-channel/channel-notifications.js | 365 +++++
modules/m-channel/channel-preferences.js | 194 +++
modules/m-channel/channel-router.js | 302 ++--
modules/m-channel/channel-router.js.bak | 190 +++
modules/m-channel/channel-router.js.bakcat | 0
modules/m-channel/channel-state.js | 44 +
modules/m-channel/channel-stats.js | 298 ++++
modules/m-channel/channel-style.css | 188 +--
modules/m-channel/channel-theme.js | 157 ++
modules/m-channel/channel-transition.css | 48 +
modules/m-channel/channel-ultimate.html | 1401 +++++++++++++++++
modules/m-channel/error-boundary.js | 134 ++
modules/m-channel/event-bus.js | 37 +
modules/m-channel/index.html | 47 +-
modules/m-channel/index.html.bak | 33 +
modules/m-channel/mock-modules/mock-a.html | 21 +-
modules/m-channel/mock-modules/mock-b.html | 22 +-
modules/m-channel/mock-modules/mock-c.html | 5 -
modules/m-channel/mock-modules/mock-d.html | 5 -
modules/m-channel/module-lifecycle.js | 23 +
modules/m-channel/module-loader.js | 93 +-
modules/m-channel/module-registry.js | 22 +-
modules/m-channel/views/404.html | 5 -
modules/m-channel/views/about.html | 8 -
.../m-channel/views/channel-dashboard.html | 57 +
modules/m-channel/views/channel-debug.html | 93 ++
modules/m-channel/views/channel-settings.html | 272 ++++
modules/m-channel/views/channel.html | 29 -
modules/m-channel/views/home.html | 12 -
58 files changed, 7586 insertions(+), 427 deletions(-)
create mode 100644 channel-enhancements.js
create mode 100644 channel-router.js
create mode 100644 event-bus.js
create mode 100644 index.html
create mode 100644 module-lifecycle.js
create mode 100644 modules/m-channel/adapters/m06-adapter.js
create mode 100644 modules/m-channel/adapters/m08-adapter.js
create mode 100644 modules/m-channel/adapters/m11-adapter.js
create mode 100644 modules/m-channel/adapters/module-adapter.js
create mode 100644 modules/m-channel/backup-混乱版/app.js
create mode 100644 modules/m-channel/backup-混乱版/channel-router-backup.js
create mode 100644 modules/m-channel/backup-混乱版/channel-router.js
create mode 100644 modules/m-channel/backup-混乱版/channel-state.js
create mode 100644 modules/m-channel/backup-混乱版/channel-style.css
create mode 100644 modules/m-channel/backup-混乱版/channel-transition.css
create mode 100644 modules/m-channel/backup-混乱版/event-bus.js
create mode 100644 modules/m-channel/backup-混乱版/index.html
create mode 100644 modules/m-channel/backup-混乱版/module-lifecycle.js
create mode 100644 modules/m-channel/backup-混乱版/module-loader.js
create mode 100644 modules/m-channel/backup-混乱版/module-registry.js
create mode 100644 modules/m-channel/channel-analytics.js
create mode 100644 modules/m-channel/channel-complete.html
create mode 100644 modules/m-channel/channel-dashboard.css
create mode 100644 modules/m-channel/channel-dashboard.js
create mode 100644 modules/m-channel/channel-enhancements.js
create mode 100644 modules/m-channel/channel-favorites.js
create mode 100644 modules/m-channel/channel-layout.css
create mode 100644 modules/m-channel/channel-notifications.css
create mode 100644 modules/m-channel/channel-notifications.js
create mode 100644 modules/m-channel/channel-preferences.js
create mode 100644 modules/m-channel/channel-router.js.bak
create mode 100644 modules/m-channel/channel-router.js.bakcat
create mode 100644 modules/m-channel/channel-state.js
create mode 100644 modules/m-channel/channel-stats.js
create mode 100644 modules/m-channel/channel-theme.js
create mode 100644 modules/m-channel/channel-transition.css
create mode 100644 modules/m-channel/channel-ultimate.html
create mode 100644 modules/m-channel/error-boundary.js
create mode 100644 modules/m-channel/event-bus.js
create mode 100644 modules/m-channel/index.html.bak
delete mode 100644 modules/m-channel/mock-modules/mock-c.html
delete mode 100644 modules/m-channel/mock-modules/mock-d.html
create mode 100644 modules/m-channel/module-lifecycle.js
delete mode 100644 modules/m-channel/views/404.html
delete mode 100644 modules/m-channel/views/about.html
create mode 100644 modules/m-channel/views/channel-dashboard.html
create mode 100644 modules/m-channel/views/channel-debug.html
create mode 100644 modules/m-channel/views/channel-settings.html
delete mode 100644 modules/m-channel/views/channel.html
delete mode 100644 modules/m-channel/views/home.html
diff --git a/channel-enhancements.js b/channel-enhancements.js
new file mode 100644
index 00000000..8451f9f0
--- /dev/null
+++ b/channel-enhancements.js
@@ -0,0 +1,318 @@
+// channel-enhancements.js - 频道搜索、面包屑、快捷键增强功能(修复版)
+(function() {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+ function init() {
+ addStyles();
+ createSearchBox();
+ createBreadcrumb();
+ createShortcutHint();
+ initShortcuts();
+ initSearch();
+ window.addEventListener('hashchange', updateBreadcrumb);
+ updateBreadcrumb();
+ }
+
+ function addStyles() {
+ const style = document.createElement('style');
+ style.textContent = `
+ .channel-search-container {
+ padding: 16px 20px;
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .channel-search-input {
+ flex: 1;
+ padding: 10px 16px;
+ border: 1px solid #ced4da;
+ border-radius: 24px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+ }
+ .channel-search-input:focus {
+ border-color: #4d6bfe;
+ box-shadow: 0 0 0 3px rgba(77,107,254,0.1);
+ }
+ .channel-search-clear {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ color: #6c757d;
+ padding: 0 8px;
+ display: none;
+ }
+ .channel-search-clear:hover {
+ color: #212529;
+ }
+ .channel-search-input:not(:placeholder-shown) + .channel-search-clear {
+ display: inline-block;
+ }
+ .highlight {
+ background-color: #ffeb3b;
+ padding: 2px 0;
+ border-radius: 2px;
+ }
+ .no-results {
+ text-align: center;
+ padding: 40px;
+ color: #6c757d;
+ font-style: italic;
+ }
+ .channel-breadcrumb {
+ padding: 12px 20px;
+ background: white;
+ border-bottom: 1px solid #e9ecef;
+ font-size: 14px;
+ }
+ .channel-breadcrumb a {
+ color: #4d6bfe;
+ text-decoration: none;
+ }
+ .channel-breadcrumb a:hover {
+ text-decoration: underline;
+ }
+ .channel-breadcrumb span {
+ color: #6c757d;
+ }
+ .channel-breadcrumb .current {
+ color: #212529;
+ font-weight: 500;
+ }
+ .shortcut-hint {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: rgba(0,0,0,0.7);
+ color: white;
+ padding: 8px 16px;
+ border-radius: 30px;
+ font-size: 12px;
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+ }
+ .shortcut-hint kbd {
+ background: rgba(255,255,255,0.2);
+ padding: 2px 6px;
+ border-radius: 4px;
+ margin: 0 2px;
+ }
+ .module-card.selected {
+ outline: 2px solid #4d6bfe;
+ outline-offset: 2px;
+ transform: scale(1.02);
+ transition: all 0.2s;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ function createSearchBox() {
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.querySelector('.channel-content');
+ if (!container) return;
+
+ const searchContainer = document.createElement('div');
+ searchContainer.className = 'channel-search-container';
+ searchContainer.innerHTML = `
+
+
+ `;
+ container.parentNode.insertBefore(searchContainer, container);
+
+ const searchInput = searchContainer.querySelector('.channel-search-input');
+ const clearBtn = searchContainer.querySelector('.channel-search-clear');
+
+ searchInput.addEventListener('input', function() {
+ filterModules(this.value);
+ });
+
+ clearBtn.addEventListener('click', function() {
+ searchInput.value = '';
+ filterModules('');
+ searchInput.focus();
+ });
+
+ window.__channelSearchInput = searchInput;
+ }
+
+ function filterModules(keyword) {
+ const cards = document.querySelectorAll('.module-card');
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
+ let hasResults = false;
+
+ // 先移除所有高亮
+ removeAllHighlights();
+
+ cards.forEach(card => {
+ const text = card.innerText || card.textContent;
+ if (keyword === '') {
+ card.style.display = '';
+ hasResults = true;
+ } else {
+ const lowerText = text.toLowerCase();
+ const lowerKeyword = keyword.toLowerCase();
+ if (lowerText.includes(lowerKeyword)) {
+ card.style.display = '';
+ highlightText(card, keyword);
+ hasResults = true;
+ } else {
+ card.style.display = 'none';
+ }
+ }
+ });
+
+ let noResultsEl = document.querySelector('.no-results');
+ if (!hasResults && keyword !== '') {
+ if (!noResultsEl) {
+ noResultsEl = document.createElement('div');
+ noResultsEl.className = 'no-results';
+ noResultsEl.textContent = '没有找到匹配的模块';
+ container.parentNode.insertBefore(noResultsEl, container.nextSibling);
+ }
+ } else {
+ if (noResultsEl) noResultsEl.remove();
+ }
+ }
+
+ // 移除所有高亮span,恢复原文本
+ function removeAllHighlights() {
+ document.querySelectorAll('.highlight').forEach(span => {
+ const parent = span.parentNode;
+ parent.replaceChild(document.createTextNode(span.textContent), span);
+ parent.normalize(); // 合并相邻文本节点
+ });
+ }
+
+ // 高亮匹配文本(不破坏事件监听)
+ function highlightText(card, keyword) {
+ const regex = new RegExp(`(${keyword})`, 'gi');
+ const walk = document.createTreeWalker(card, NodeFilter.SHOW_TEXT, {
+ acceptNode: function(node) {
+ // 跳过已经高亮过的span内部
+ if (node.parentNode.classList && node.parentNode.classList.contains('highlight')) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }, false);
+
+ const textNodes = [];
+ while (walk.nextNode()) textNodes.push(walk.currentNode);
+
+ textNodes.forEach(node => {
+ const text = node.nodeValue;
+ if (regex.test(text)) {
+ const span = document.createElement('span');
+ span.className = 'highlight';
+ span.innerHTML = text.replace(regex, '$1');
+ node.parentNode.replaceChild(span, node);
+ }
+ });
+ }
+
+ function createBreadcrumb() {
+ const searchContainer = document.querySelector('.channel-search-container');
+ const breadcrumb = document.createElement('div');
+ breadcrumb.className = 'channel-breadcrumb';
+ breadcrumb.id = 'channelBreadcrumb';
+ if (searchContainer) {
+ searchContainer.parentNode.insertBefore(breadcrumb, searchContainer.nextSibling);
+ } else {
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
+ if (container) {
+ container.parentNode.insertBefore(breadcrumb, container);
+ }
+ }
+ }
+
+ function updateBreadcrumb() {
+ const breadcrumb = document.getElementById('channelBreadcrumb');
+ if (!breadcrumb) return;
+ const hash = window.location.hash.slice(1) || '';
+ let moduleName = '频道';
+ if (hash.startsWith('module-')) {
+ const moduleId = hash.replace('module-', '');
+ const moduleNames = {
+ 'M06': '工单管理',
+ 'M08': '数据看板',
+ 'M11': '用户反馈'
+ };
+ moduleName = moduleNames[moduleId] || moduleId;
+ }
+ breadcrumb.innerHTML = `
+ 首页 >
+ 频道 >
+ ${moduleName}
+ `;
+ }
+
+ function createShortcutHint() {
+ const hint = document.createElement('div');
+ hint.className = 'shortcut-hint';
+ hint.innerHTML = `
+ ⌘K 搜索 ·
+ ↑↓ 选择 ·
+ Enter 打开 ·
+ Esc 关闭
+ `;
+ document.body.appendChild(hint);
+ }
+
+ function initShortcuts() {
+ document.addEventListener('keydown', function(e) {
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ const searchInput = window.__channelSearchInput;
+ if (searchInput) searchInput.focus();
+ }
+ if (e.key === 'Escape') {
+ const searchInput = window.__channelSearchInput;
+ if (searchInput && document.activeElement === searchInput) {
+ searchInput.value = '';
+ filterModules('');
+ searchInput.blur();
+ } else if (searchInput && searchInput.value) {
+ searchInput.value = '';
+ filterModules('');
+ }
+ }
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+ const cards = Array.from(document.querySelectorAll('.module-card:not([style*="display: none"])'));
+ if (cards.length === 0) return;
+ e.preventDefault();
+ let selectedIndex = cards.findIndex(card => card.classList.contains('selected'));
+ if (selectedIndex === -1) {
+ selectedIndex = e.key === 'ArrowDown' ? 0 : cards.length - 1;
+ } else {
+ cards[selectedIndex].classList.remove('selected');
+ if (e.key === 'ArrowDown') {
+ selectedIndex = (selectedIndex + 1) % cards.length;
+ } else {
+ selectedIndex = (selectedIndex - 1 + cards.length) % cards.length;
+ }
+ }
+ cards[selectedIndex].classList.add('selected');
+ cards[selectedIndex].scrollIntoView({ block: 'nearest' });
+ }
+ if (e.key === 'Enter') {
+ const selected = document.querySelector('.module-card.selected');
+ if (selected) {
+ // 触发点击事件,模拟鼠标点击
+ selected.click();
+ }
+ }
+ });
+ }
+
+ function initSearch() {
+ // 不需要预先保存innerHTML了
+ }
+})();
diff --git a/channel-router.js b/channel-router.js
new file mode 100644
index 00000000..14dbf562
--- /dev/null
+++ b/channel-router.js
@@ -0,0 +1,153 @@
+// ================== 路由配置 ==================
+const routes = {
+ 'home': 'views/home.html',
+ 'channel': 'views/channel.html',
+ 'about': 'views/about.html'
+};
+
+// 动画时长
+const ANIMATION_DURATION = 300;
+
+// 获取当前 hash 中的路径(去掉 #/)
+function getHashPath() {
+ const hash = window.location.hash.slice(1) || '/';
+ const path = hash.startsWith('/') ? hash.slice(1) : hash;
+ return path || 'home';
+}
+
+// ================== 状态管理(localStorage) ==================
+const STORAGE_KEY = 'm-channel-state';
+
+// 保存状态
+function saveState(path) {
+ // 获取已访问模块列表
+ let visited = JSON.parse(localStorage.getItem(STORAGE_KEY)) || { visitedModules: [] };
+ if (!visited.visitedModules.includes(path)) {
+ visited.visitedModules.push(path);
+ }
+ visited.lastRoute = path;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(visited));
+ console.log(`[state] save ${path}`, visited);
+}
+
+// 恢复状态(返回上次访问的路由,如果没有则返回 'home')
+function restoreState() {
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
+ if (saved && saved.lastRoute) {
+ console.log(`[state] restore ${saved.lastRoute}`, saved);
+ return saved.lastRoute;
+ }
+ return 'home';
+}
+
+// ================== 加载视图(带动画+状态保存) ==================
+async function loadView(path) {
+ const routerView = document.getElementById('router-view');
+ if (!routerView) return;
+
+ // 1. 开始离开动画
+ routerView.classList.add('fade-leave-active', 'fade-leave-to');
+
+ // 2. 稍等片刻让离开动画跑起来,再加载新内容
+ setTimeout(async () => {
+ routerView.innerHTML = '
';
+
+ try {
+ const viewFile = routes[path];
+ if (!viewFile) {
+ await load404(routerView);
+ return;
+ }
+
+ const response = await fetch(viewFile);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const html = await response.text();
+ routerView.innerHTML = html;
+ } catch (error) {
+ console.error('加载视图失败:', error);
+ routerView.innerHTML = `
+
+ ❌ 加载失败:${error.message}
+ 请检查文件是否存在,或刷新重试
+
+ `;
+ }
+
+ // 3. 移除离开动画类,添加入场动画类
+ routerView.classList.remove('fade-leave-active', 'fade-leave-to');
+ routerView.classList.add('fade-enter-active', 'fade-enter-from');
+
+ requestAnimationFrame(() => {
+ routerView.classList.remove('fade-enter-from');
+ });
+
+ setTimeout(() => {
+ routerView.classList.remove('fade-enter-active');
+ }, ANIMATION_DURATION);
+
+ // 4. 保存状态到 localStorage(并打印日志)
+ saveState(path);
+
+ // 5. 更新导航高亮和状态栏
+ updateActiveNav(path);
+ updateStatusBar(path);
+ }, 50);
+}
+
+// 加载 404 页面
+async function load404(container) {
+ try {
+ const resp = await fetch('views/404.html');
+ if (resp.ok) {
+ container.innerHTML = await resp.text();
+ } else {
+ container.innerHTML = '⚠️ 404 - 页面未找到
';
+ }
+ } catch {
+ container.innerHTML = '⚠️ 404 - 页面未找到
';
+ }
+}
+
+// 更新导航高亮
+function updateActiveNav(path) {
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.classList.remove('active');
+ const linkPath = link.getAttribute('href').slice(2);
+ if (linkPath === path) {
+ link.classList.add('active');
+ }
+ });
+}
+
+// 更新状态栏
+function updateStatusBar(path) {
+ const statusEl = document.getElementById('current-route');
+ if (statusEl) {
+ statusEl.textContent = `当前路由:/${path}`;
+ }
+}
+
+// ================== 初始化:恢复上次访问的路由 ==================
+function initRouter() {
+ // 先尝试从 localStorage 恢复上次路由
+ const lastRoute = restoreState();
+ // 如果当前 hash 为空或为默认,就跳转到上次路由
+ if (!window.location.hash || window.location.hash === '#/home') {
+ window.location.hash = `#/${lastRoute}`;
+ } else {
+ // 否则加载当前 hash 对应的路由
+ const path = getHashPath();
+ loadView(path);
+ }
+}
+
+// 监听 hash 变化
+window.addEventListener('hashchange', () => {
+ const path = getHashPath();
+ loadView(path);
+});
+
+// 首次加载
+window.addEventListener('DOMContentLoaded', initRouter);
diff --git a/event-bus.js b/event-bus.js
new file mode 100644
index 00000000..c7d436fa
--- /dev/null
+++ b/event-bus.js
@@ -0,0 +1,55 @@
+// event-bus.js
+// 事件总线(发布/订阅模式)
+
+const EventBus = {
+ events: {},
+
+ // 订阅事件
+ on(eventName, callback) {
+ if (!this.events[eventName]) {
+ this.events[eventName] = [];
+ }
+ this.events[eventName].push(callback);
+ console.log(`[bus] 订阅事件: ${eventName}`);
+ return this; // 支持链式调用
+ },
+
+ // 发布事件
+ emit(eventName, data) {
+ console.log(`[bus] 发布事件: ${eventName}`, data);
+ if (this.events[eventName]) {
+ this.events[eventName].forEach(callback => {
+ try {
+ callback(data);
+ } catch (err) {
+ console.error(`[bus] 事件 ${eventName} 回调执行错误:`, err);
+ }
+ });
+ }
+ return this;
+ },
+
+ // 取消订阅
+ off(eventName, callback) {
+ if (!this.events[eventName]) return this;
+ if (!callback) {
+ // 如果没有提供回调,取消该事件的所有订阅
+ delete this.events[eventName];
+ console.log(`[bus] 取消所有订阅: ${eventName}`);
+ } else {
+ // 移除特定的回调
+ this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
+ console.log(`[bus] 取消一个订阅: ${eventName}`);
+ }
+ return this;
+ },
+
+ // 清空所有事件
+ clear() {
+ this.events = {};
+ console.log('[bus] 清空所有事件');
+ }
+};
+
+// 导出到全局,方便其他模块使用
+window.EventBus = EventBus;
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..e69de29b
diff --git a/module-lifecycle.js b/module-lifecycle.js
new file mode 100644
index 00000000..6d351209
--- /dev/null
+++ b/module-lifecycle.js
@@ -0,0 +1,90 @@
+// module-lifecycle.js
+// 模块生命周期管理
+
+const ModuleLifecycle = {
+ // 当前激活的模块
+ currentModule: null,
+
+ // 加载模块
+ async load(moduleName, containerId) {
+ console.log(`[lifecycle] 开始加载模块: ${moduleName}`);
+
+ // 如果有当前模块,先卸载
+ if (this.currentModule) {
+ await this.unload(this.currentModule.name);
+ }
+
+ const container = document.getElementById(containerId);
+ if (!container) {
+ console.error(`[lifecycle] 容器不存在: ${containerId}`);
+ return;
+ }
+
+ try {
+ // 假设模块的 HTML 放在 mock-modules/ 下
+ const resp = await fetch(`mock-modules/${moduleName}.html`);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ const html = await resp.text();
+ container.innerHTML = html;
+
+ // 记录当前模块
+ this.currentModule = {
+ name: moduleName,
+ containerId: containerId,
+ loadedAt: new Date()
+ };
+
+ // 触发模块的 onLoad 钩子(如果定义了)
+ if (window[`onModuleLoad_${moduleName}`]) {
+ window[`onModuleLoad_${moduleName}`]();
+ }
+
+ console.log(`[lifecycle] 模块加载完成: ${moduleName}`);
+ return this.currentModule;
+ } catch (err) {
+ console.error(`[lifecycle] 加载模块失败 ${moduleName}:`, err);
+ container.innerHTML = `加载模块 ${moduleName} 失败
`;
+ }
+ },
+
+ // 卸载当前模块
+ async unload(moduleName) {
+ if (!this.currentModule || this.currentModule.name !== moduleName) {
+ console.log(`[lifecycle] 模块 ${moduleName} 未激活,无需卸载`);
+ return;
+ }
+
+ console.log(`[lifecycle] 开始卸载模块: ${moduleName}`);
+
+ // 触发模块的 onUnload 钩子(如果定义了)
+ if (window[`onModuleUnload_${moduleName}`]) {
+ window[`onModuleUnload_${moduleName}`]();
+ }
+
+ // 取消该模块订阅的所有事件(约定:模块的事件名前缀为模块名)
+ // 这里简单演示,实际可能需要更精细的管理
+ Object.keys(EventBus.events).forEach(eventName => {
+ if (eventName.startsWith(moduleName)) {
+ EventBus.off(eventName);
+ }
+ });
+
+ // 清空容器
+ const container = document.getElementById(this.currentModule.containerId);
+ if (container) {
+ container.innerHTML = '';
+ }
+
+ this.currentModule = null;
+ console.log(`[lifecycle] 模块卸载完成: ${moduleName}`);
+ },
+
+ // 发送消息给指定模块(通过事件总线)
+ sendMessage(targetModule, eventName, data) {
+ const fullEventName = `${targetModule}:${eventName}`;
+ EventBus.emit(fullEventName, data);
+ console.log(`[lifecycle] 发送消息给 ${targetModule}: ${eventName}`, data);
+ }
+};
+
+window.ModuleLifecycle = ModuleLifecycle;
diff --git a/modules/m-channel/adapters/m06-adapter.js b/modules/m-channel/adapters/m06-adapter.js
new file mode 100644
index 00000000..4651cb12
--- /dev/null
+++ b/modules/m-channel/adapters/m06-adapter.js
@@ -0,0 +1,61 @@
+// M06 工单管理模块适配器
+window.M06Adapter = {
+ moduleId: 'm06',
+ moduleName: '工单管理',
+
+ // 初始化模块(由 ModuleAdapter 调用)
+ init: function(containerId) {
+ console.log('[M06Adapter] 初始化');
+
+ // 通过核心适配器加载
+ if (window.ModuleAdapter) {
+ return ModuleAdapter.loadModule(this.moduleId, containerId);
+ } else {
+ console.error('[M06Adapter] ModuleAdapter 未加载');
+ return null;
+ }
+ },
+
+ // 发送消息到工单模块
+ sendMessage: function(type, payload) {
+ return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
+ },
+
+ // 监听工单模块事件
+ onTicketCreate: function(callback) {
+ EventBus.on('module:m06:message', function(data) {
+ if (data.type === 'ticket:create') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ onTicketUpdate: function(callback) {
+ EventBus.on('module:m06:message', function(data) {
+ if (data.type === 'ticket:update') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ onTicketDelete: function(callback) {
+ EventBus.on('module:m06:message', function(data) {
+ if (data.type === 'ticket:delete') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ // 销毁模块
+ destroy: function() {
+ ModuleAdapter.unloadModule(this.moduleId);
+ }
+};
+
+// 注册到模块注册表
+if (window.ModuleRegistry) {
+ ModuleRegistry.register('m06', window.M06Adapter);
+ console.log('[M06Adapter] 已注册到模块注册表');
+}
+
+console.log('[M06Adapter] 已加载');
diff --git a/modules/m-channel/adapters/m08-adapter.js b/modules/m-channel/adapters/m08-adapter.js
new file mode 100644
index 00000000..b549438a
--- /dev/null
+++ b/modules/m-channel/adapters/m08-adapter.js
@@ -0,0 +1,47 @@
+// M08 数据统计模块适配器
+window.M08Adapter = {
+ moduleId: 'm08',
+ moduleName: '数据统计',
+
+ init: function(containerId) {
+ console.log('[M08Adapter] 初始化');
+ if (window.ModuleAdapter) {
+ return ModuleAdapter.loadModule(this.moduleId, containerId);
+ } else {
+ console.error('[M08Adapter] ModuleAdapter 未加载');
+ return null;
+ }
+ },
+
+ sendMessage: function(type, payload) {
+ return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
+ },
+
+ // 监听统计模块事件
+ onStatsRefresh: function(callback) {
+ EventBus.on('module:m08:message', function(data) {
+ if (data.type === 'stats:refresh') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ onStatsExport: function(callback) {
+ EventBus.on('module:m08:message', function(data) {
+ if (data.type === 'stats:export') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ destroy: function() {
+ ModuleAdapter.unloadModule(this.moduleId);
+ }
+};
+
+if (window.ModuleRegistry) {
+ ModuleRegistry.register('m08', window.M08Adapter);
+ console.log('[M08Adapter] 已注册到模块注册表');
+}
+
+console.log('[M08Adapter] 已加载');
diff --git a/modules/m-channel/adapters/m11-adapter.js b/modules/m-channel/adapters/m11-adapter.js
new file mode 100644
index 00000000..4e3a7b25
--- /dev/null
+++ b/modules/m-channel/adapters/m11-adapter.js
@@ -0,0 +1,47 @@
+// M11 系统组件库适配器
+window.M11Adapter = {
+ moduleId: 'm11',
+ moduleName: '组件库',
+
+ init: function(containerId) {
+ console.log('[M11Adapter] 初始化');
+ if (window.ModuleAdapter) {
+ return ModuleAdapter.loadModule(this.moduleId, containerId);
+ } else {
+ console.error('[M11Adapter] ModuleAdapter 未加载');
+ return null;
+ }
+ },
+
+ sendMessage: function(type, payload) {
+ return ModuleAdapter.sendMessage(this.moduleId, { type, payload });
+ },
+
+ // 监听组件库事件
+ onThemeChange: function(callback) {
+ EventBus.on('module:m11:message', function(data) {
+ if (data.type === 'theme:change') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ onComponentSelect: function(callback) {
+ EventBus.on('module:m11:message', function(data) {
+ if (data.type === 'component:select') {
+ callback(data.payload);
+ }
+ });
+ },
+
+ destroy: function() {
+ ModuleAdapter.unloadModule(this.moduleId);
+ }
+};
+
+if (window.ModuleRegistry) {
+ ModuleRegistry.register('m11', window.M11Adapter);
+ console.log('[M11Adapter] 已注册到模块注册表');
+}
+
+console.log('[M11Adapter] 已加载');
diff --git a/modules/m-channel/adapters/module-adapter.js b/modules/m-channel/adapters/module-adapter.js
new file mode 100644
index 00000000..26841ce9
--- /dev/null
+++ b/modules/m-channel/adapters/module-adapter.js
@@ -0,0 +1,140 @@
+// 模块适配器核心 - 万能转接头
+window.ModuleAdapter = {
+ // 已加载的真实模块缓存
+ loadedModules: {},
+
+ // 适配器配置
+ config: {
+ m06: {
+ name: '工单管理',
+ path: '/m06-ticket/index.html', // 真实路径,但文件可能不全
+ width: '100%',
+ height: '500px',
+ events: ['ticket:create', 'ticket:update', 'ticket:delete']
+ },
+ m08: {
+ name: '数据统计',
+ path: '/modules/m08/index.html', // 暂时未知,先保留错误路径触发边界
+ width: '100%',
+ height: '500px',
+ events: ['stats:refresh', 'stats:export']
+ },
+ m11: {
+ name: '组件库',
+ path: '/m11-module/index.html', // 正确路径!
+ width: '100%',
+ height: '600px',
+ events: ['theme:change', 'component:select']
+ }
+ },
+
+ // 加载真实模块(通过 iframe 沙箱)
+ loadModule: function(moduleId, containerId) {
+ console.log(`[adapter] 加载真实模块: ${moduleId}`);
+
+ const container = document.getElementById(containerId);
+ if (!container) {
+ console.error(`[adapter] 容器 ${containerId} 不存在`);
+ return null;
+ }
+
+ // 清理容器
+ container.innerHTML = '';
+
+ const config = this.config[moduleId];
+ if (!config) {
+ console.error(`[adapter] 未知模块: ${moduleId}`);
+ container.innerHTML = `模块配置不存在
`;
+ return null;
+ }
+
+ // 创建 iframe 沙箱(玻璃展柜)
+ const iframe = document.createElement('iframe');
+ iframe.src = config.path;
+ iframe.style.width = config.width;
+ iframe.style.height = config.height;
+ iframe.style.border = 'none';
+ iframe.style.borderRadius = '8px';
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
+ iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
+
+ // 添加加载指示器
+ const loadingDiv = document.createElement('div');
+ loadingDiv.className = 'module-loading';
+ loadingDiv.textContent = `加载 ${config.name}...`;
+ container.appendChild(loadingDiv);
+
+ // iframe 加载完成后移除加载指示器
+ iframe.onload = () => {
+ loadingDiv.remove();
+ console.log(`[adapter] ${moduleId} 加载完成`);
+
+ // 建立跨框架通信桥梁
+ this.setupMessageBridge(iframe, moduleId);
+ };
+
+ iframe.onerror = () => {
+ loadingDiv.remove();
+ container.innerHTML = `模块加载失败,请重试
`;
+ };
+
+ container.appendChild(iframe);
+ this.loadedModules[moduleId] = { iframe, containerId };
+
+ return iframe;
+ },
+
+ // 重新加载模块(错误重试)
+ reloadModule: function(moduleId, containerId) {
+ console.log(`[adapter] 重新加载模块: ${moduleId}`);
+ this.loadModule(moduleId, containerId);
+ },
+
+ // 建立消息桥梁(iframe 和主页通信)
+ setupMessageBridge: function(iframe, moduleId) {
+ // 监听来自 iframe 的消息
+ window.addEventListener('message', function(event) {
+ // 安全检查:确保消息来自正确的 iframe
+ if (event.source !== iframe.contentWindow) return;
+
+ const data = event.data;
+ if (!data || !data.type) return;
+
+ console.log(`[adapter] 收到来自 ${moduleId} 的消息:`, data);
+
+ // 转发为事件总线消息
+ if (window.EventBus) {
+ EventBus.emit(`module:${moduleId}:message`, {
+ from: moduleId,
+ type: data.type,
+ payload: data.payload
+ });
+ }
+ });
+ },
+
+ // 向模块发送消息
+ sendMessage: function(moduleId, message) {
+ const module = this.loadedModules[moduleId];
+ if (!module || !module.iframe) {
+ console.error(`[adapter] 模块 ${moduleId} 未加载`);
+ return false;
+ }
+
+ module.iframe.contentWindow.postMessage(message, '*');
+ return true;
+ },
+
+ // 卸载模块
+ unloadModule: function(moduleId) {
+ const module = this.loadedModules[moduleId];
+ if (module) {
+ const container = document.getElementById(module.containerId);
+ if (container) container.innerHTML = '';
+ delete this.loadedModules[moduleId];
+ console.log(`[adapter] 卸载模块: ${moduleId}`);
+ }
+ }
+};
+
+console.log('[adapter] 模块适配器已加载');
diff --git a/modules/m-channel/app.js b/modules/m-channel/app.js
index 486b8f3d..152d4aa9 100644
--- a/modules/m-channel/app.js
+++ b/modules/m-channel/app.js
@@ -1,45 +1,50 @@
-// 应用入口:初始化路由和模块加载器的协作
+// 应用入口
+console.log('[app] 启动中...');
-// 当路由变化时,如果离开频道页,自动卸载模块
-window.addEventListener('hashchange', () => {
- const path = window.location.hash.slice(2) || 'home';
- if (path !== 'channel') {
- // 离开频道页时卸载模块(避免残留)
- if (window.unloadModule) window.unloadModule();
+// 等待 DOM 加载完成
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('[app] DOM 已加载');
+
+ const contentEl = document.getElementById('channel-content');
+ if (!contentEl) {
+ console.error('[app] 找不到 #channel-content');
+ return;
}
-});
-
-// 页面加载完成后,如果当前是频道页,绑定卡片事件
-document.addEventListener('DOMContentLoaded', () => {
- // 延迟一点等待视图渲染
- setTimeout(initChannelPage, 100);
-});
-
-// 每次视图加载完成后也可能触发(路由引擎加载视图后)
-// 所以我们用 MutationObserver 监听 router-view 的变化,当内容变成频道页时初始化
-const observer = new MutationObserver(() => {
- const routerView = document.getElementById('router-view');
- if (!routerView) return;
- // 检查是否包含 .channel-view
- if (routerView.querySelector('.channel-view')) {
- initChannelPage();
+
+ // 初始化路由器
+ if (window.ChannelRouter) {
+ ChannelRouter.init(contentEl);
}
-});
-observer.observe(document.getElementById('router-view'), { childList: true, subtree: true });
-
-function initChannelPage() {
- const cards = document.querySelectorAll('.module-card');
- cards.forEach(card => {
- card.removeEventListener('click', cardClickHandler); // 防止重复绑定
- card.addEventListener('click', cardClickHandler);
+
+ // 绑定导航按钮
+ 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);
+ }
+ });
});
-}
+
+ console.log('[app] 初始化完成');
+});
-function cardClickHandler(e) {
- const card = e.currentTarget;
- const moduleId = card.dataset.module;
- if (moduleId && window.loadModule) {
- // 可选:先卸载再加载(但 loadModule 会覆盖,所以不需要显式 unload)
- window.loadModule(moduleId);
- }
-}
+// 拦截所有事件总线消息,用于调试面板
+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] 事件总线拦截器已安装');
diff --git a/modules/m-channel/backup-混乱版/app.js b/modules/m-channel/backup-混乱版/app.js
new file mode 100644
index 00000000..25e29f0a
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/app.js
@@ -0,0 +1,42 @@
+// app.js - 光湖频道动态渲染引擎入口
+console.log('[app] 启动中...');
+
+// 等待 DOM 完全加载
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('[app] DOM 已加载,初始化组件...');
+
+ // 获取内容容器
+ const contentEl = document.getElementById('channel-content');
+ if (!contentEl) {
+ console.error('[app] 找不到 #channel-content 元素');
+ return;
+ }
+
+ // 恢复状态(如果 ChannelState 存在)
+ if (window.ChannelState) {
+ const savedState = ChannelState.restoreState();
+ console.log('[app] 恢复的状态:', savedState);
+ } else {
+ console.warn('[app] ChannelState 未加载');
+ }
+
+ // 初始化路由器
+ if (window.ChannelRouter) {
+ ChannelRouter.init(contentEl);
+ } else {
+ console.error('[app] ChannelRouter 未加载');
+ return;
+ }
+
+ // 绑定导航按钮点击事件
+ document.querySelectorAll('.channel-btn').forEach(btn => {
+ btn.addEventListener('click', function(e) {
+ const channel = this.dataset.channel;
+ if (channel) {
+ ChannelRouter.navigateTo(channel);
+ }
+ });
+ });
+
+ console.log('[app] 初始化完成');
+});
diff --git a/modules/m-channel/backup-混乱版/channel-router-backup.js b/modules/m-channel/backup-混乱版/channel-router-backup.js
new file mode 100644
index 00000000..614fc3ed
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/channel-router-backup.js
@@ -0,0 +1,98 @@
+// ================== 路由配置 ==================
+const routes = {
+ 'home': 'views/home.html',
+ 'channel': 'views/channel.html',
+ 'about': 'views/about.html'
+};
+
+// 获取当前 hash 中的路径(去掉 #/)
+function getHashPath() {
+ const hash = window.location.hash.slice(1) || '/';
+ const path = hash.startsWith('/') ? hash.slice(1) : hash;
+ return path || 'home';
+}
+
+// ================== 加载视图 ==================
+async function loadView(path) {
+ const routerView = document.getElementById('router-view');
+ if (!routerView) return;
+
+ // 显示加载动画
+ routerView.innerHTML = '';
+
+ try {
+ const viewFile = routes[path];
+ if (!viewFile) {
+ await load404(routerView);
+ return;
+ }
+
+ const response = await fetch(viewFile);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const html = await response.text();
+ routerView.innerHTML = html;
+ } catch (error) {
+ console.error('加载视图失败:', error);
+ routerView.innerHTML = `
+
+ ❌ 加载失败:${error.message}
+ 请检查文件是否存在,或刷新重试
+
+ `;
+ }
+
+ // 更新导航高亮和状态栏
+ updateActiveNav(path);
+ updateStatusBar(path);
+}
+
+// 加载 404 页面
+async function load404(container) {
+ try {
+ const resp = await fetch('views/404.html');
+ if (resp.ok) {
+ container.innerHTML = await resp.text();
+ } else {
+ container.innerHTML = '⚠️ 404 - 页面未找到
';
+ }
+ } catch {
+ container.innerHTML = '⚠️ 404 - 页面未找到
';
+ }
+}
+
+// 更新导航高亮
+function updateActiveNav(path) {
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.classList.remove('active');
+ const linkPath = link.getAttribute('href').slice(2);
+ if (linkPath === path) {
+ link.classList.add('active');
+ }
+ });
+}
+
+// 更新状态栏
+function updateStatusBar(path) {
+ const statusEl = document.getElementById('current-route');
+ if (statusEl) {
+ statusEl.textContent = `当前路由:/${path}`;
+ }
+}
+
+// 监听 hash 变化
+window.addEventListener('hashchange', () => {
+ const path = getHashPath();
+ loadView(path);
+});
+
+// 首次加载
+window.addEventListener('DOMContentLoaded', () => {
+ if (!window.location.hash) {
+ window.location.hash = '#/home';
+ } else {
+ const path = getHashPath();
+ loadView(path);
+ }
+});
diff --git a/modules/m-channel/backup-混乱版/channel-router.js b/modules/m-channel/backup-混乱版/channel-router.js
new file mode 100644
index 00000000..b1930c6d
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/channel-router.js
@@ -0,0 +1,267 @@
+/**
+ * channel-router.js
+ * 用户频道路由引擎 - 带过渡动画和状态持久化
+ * 集成ChannelState和动画控制
+ */
+
+// 依赖全局变量:ChannelState 需要先加载
+(function(global) {
+ 'use strict';
+
+ // 默认配置
+ const defaultConfig = {
+ containerSelector: '#channel-content',
+ routes: {},
+ defaultRoute: '/home',
+ mode: 'fade', // 'fade' 或 'slide'
+ transitionDuration: 300
+ };
+
+ class ChannelRouter {
+ constructor(config) {
+ this.config = Object.assign({}, defaultConfig, config);
+ this.container = document.querySelector(this.config.containerSelector);
+ if (!this.container) {
+ throw new Error(`Container ${this.config.containerSelector} not found`);
+ }
+
+ // 确保容器有相对定位
+ this.container.style.position = 'relative';
+ this.container.classList.add('route-container');
+
+ // 当前活动路由
+ this.currentRoute = null;
+ this.currentPageElement = null;
+
+ // 防抖定时器
+ this.navTimer = null;
+
+ // 绑定方法
+ this.navigate = this.navigate.bind(this);
+ this.goBack = this.goBack.bind(this);
+ this.goForward = this.goForward.bind(this);
+ this.setMode = this.setMode.bind(this);
+ this.handlePopState = this.handlePopState.bind(this);
+
+ // 初始化模式
+ this.container.classList.add(`${this.config.mode}-mode`);
+
+ // 从ChannelState恢复上次的路由
+ this.initFromState();
+
+ // 监听浏览器前进后退
+ window.addEventListener('popstate', this.handlePopState);
+
+ console.log('[router] 初始化完成,模式:', this.config.mode);
+ }
+
+ // 从ChannelState恢复
+ initFromState() {
+ if (global.ChannelState) {
+ const state = global.ChannelState.getState();
+ const targetRoute = state.currentRoute || this.config.defaultRoute;
+ // 不触发pushState,直接渲染
+ this.renderRoute(targetRoute, { replace: true, fromState: true });
+ console.log('[router] 从状态恢复路由:', targetRoute);
+ } else {
+ // 没有状态管理器,走默认
+ this.renderRoute(this.config.defaultRoute, { replace: true });
+ }
+ }
+
+ // 渲染路由(内部方法)
+ renderRoute(path, options = {}) {
+ const { replace = false, fromState = false } = options;
+ const route = this.config.routes[path];
+ if (!route) {
+ console.warn(`[router] 路由 ${path} 未定义,使用404`);
+ // 可以跳转到404页面,这里简单返回
+ return;
+ }
+
+ // 如果是相同路由且不是强制刷新,不重复渲染
+ if (this.currentRoute === path && !options.force) {
+ return;
+ }
+
+ // 创建新页面元素
+ const newPage = document.createElement('div');
+ newPage.className = `route-page ${this.config.mode === 'slide' ? 'slide-enter' : ''}`;
+ newPage.innerHTML = route.template || route.content || '';
+
+ // 如果有模块加载器,执行模块加载
+ if (global.ModuleLoader && route.module) {
+ // 这里简化,实际可能需要加载模块
+ console.log('[router] 加载模块:', route.module);
+ }
+
+ // 旧页面元素
+ const oldPage = this.currentPageElement;
+
+ // 设置新页面为激活状态
+ newPage.classList.add('active');
+
+ // 如果是滑入模式,根据方向添加额外类
+ if (this.config.mode === 'slide') {
+ // 通过history判断方向:前进/后退
+ const direction = this.getDirection(path);
+ if (direction === 'back') {
+ this.container.classList.add('backward');
+ this.container.classList.remove('forward');
+ } else {
+ this.container.classList.add('forward');
+ this.container.classList.remove('backward');
+ }
+ }
+
+ // 添加新页面到容器
+ this.container.appendChild(newPage);
+
+ // 触发重绘以确保动画
+ newPage.offsetHeight;
+
+ // 如果有旧页面,移除它的active类并添加退出动画类
+ if (oldPage) {
+ oldPage.classList.remove('active');
+ if (this.config.mode === 'slide') {
+ oldPage.classList.add('slide-exit');
+ }
+ }
+
+ // 动画结束后清理旧页面
+ const onTransitionEnd = (e) => {
+ if (e.target === newPage || e.target === oldPage) {
+ if (oldPage && oldPage.parentNode) {
+ oldPage.parentNode.removeChild(oldPage);
+ }
+ newPage.removeEventListener('transitionend', onTransitionEnd);
+ }
+ };
+ newPage.addEventListener('transitionend', onTransitionEnd);
+
+ // 更新当前路由
+ this.currentRoute = path;
+ this.currentPageElement = newPage;
+
+ // 更新导航菜单激活状态
+ this.updateActiveNav(path);
+
+ // 保存状态(如果不是从状态恢复来的)
+ if (!fromState && global.ChannelState) {
+ // 判断是push还是replace
+ if (replace) {
+ // 替换当前历史记录(不增加新记录)
+ // 状态管理需要相应处理:替换当前记录而不是push
+ global.ChannelState.setCurrentRoute(path);
+ // 同时替换浏览器历史
+ if (!options.skipHistory) {
+ history.replaceState({ route: path }, '', `#${path}`);
+ }
+ } else {
+ // 正常跳转,push到历史
+ global.ChannelState.pushHistory(path);
+ if (!options.skipHistory) {
+ history.pushState({ route: path }, '', `#${path}`);
+ }
+ }
+ } else if (!global.ChannelState) {
+ // 没有状态管理器,只处理浏览器历史
+ if (!replace && !options.skipHistory) {
+ history.pushState({ route: path }, '', `#${path}`);
+ } else if (replace && !options.skipHistory) {
+ history.replaceState({ route: path }, '', `#${path}`);
+ }
+ }
+
+ console.log(`[router] 导航到 ${path}${replace ? ' (replace)' : ''}`);
+ }
+
+ // 判断前进后退方向(简单实现:看是否在历史栈中)
+ getDirection(path) {
+ if (!global.ChannelState) return 'forward';
+ const state = global.ChannelState.getState();
+ const currentIndex = state.historyIndex;
+ const stack = state.historyStack;
+ // 如果path在历史栈中且在当前位置之后,是后退?需要更精确
+ // 这里简化:根据当前路由和目标的索引比较
+ if (this.currentRoute) {
+ const currentIdx = stack.indexOf(this.currentRoute);
+ const targetIdx = stack.indexOf(path);
+ if (targetIdx < currentIdx) return 'back';
+ }
+ return 'forward';
+ }
+
+ // 更新导航菜单激活样式
+ updateActiveNav(path) {
+ document.querySelectorAll('.channel-nav a').forEach(link => {
+ const href = link.getAttribute('href').replace('#', '');
+ if (href === path) {
+ link.classList.add('active');
+ } else {
+ link.classList.remove('active');
+ }
+ });
+ }
+
+ // 公开导航方法
+ navigate(path, options = {}) {
+ // 防抖处理
+ if (this.navTimer) clearTimeout(this.navTimer);
+ this.navTimer = setTimeout(() => {
+ this.renderRoute(path, options);
+ this.navTimer = null;
+ }, 10); // 微小延迟确保快速点击不重叠
+ }
+
+ // 后退
+ goBack() {
+ if (global.ChannelState) {
+ const prev = global.ChannelState.goBack();
+ if (prev) {
+ this.renderRoute(prev, { fromState: true, skipHistory: true });
+ } else {
+ console.log('[router] 已在最前');
+ }
+ } else {
+ history.back();
+ }
+ }
+
+ // 前进
+ goForward() {
+ if (global.ChannelState) {
+ const next = global.ChannelState.goForward();
+ if (next) {
+ this.renderRoute(next, { fromState: true, skipHistory: true });
+ } else {
+ console.log('[router] 已在最后');
+ }
+ } else {
+ history.forward();
+ }
+ }
+
+ // 处理popstate事件(浏览器前进后退)
+ handlePopState(event) {
+ const route = event.state?.route || this.config.defaultRoute;
+ if (global.ChannelState) {
+ // 从状态中恢复索引,但不需要重复push
+ this.renderRoute(route, { fromState: true, skipHistory: true });
+ } else {
+ this.renderRoute(route, { skipHistory: true });
+ }
+ }
+
+ // 切换动画模式
+ setMode(mode) {
+ if (mode !== 'fade' && mode !== 'slide') return;
+ this.container.classList.remove('fade-mode', 'slide-mode');
+ this.container.classList.add(`${mode}-mode`);
+ this.config.mode = mode;
+ console.log('[router] 切换动画模式为:', mode);
+ }
+ }
+
+ global.ChannelRouter = ChannelRouter;
+})(window);
diff --git a/modules/m-channel/backup-混乱版/channel-state.js b/modules/m-channel/backup-混乱版/channel-state.js
new file mode 100644
index 00000000..35ea1734
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/channel-state.js
@@ -0,0 +1,149 @@
+/**
+ * channel-state.js
+ * 频道状态管理器 - 记忆妈妈的浏览足迹
+ * 使用localStorage持久化,刷新页面自动恢复
+ * 功能:保存/恢复当前路由、已访问模块列表、历史栈
+ */
+
+const ChannelState = (function() {
+ const STORAGE_KEY = 'm-channel-state';
+
+ // 默认状态
+ const defaultState = {
+ currentRoute: '/home',
+ visitedModules: [], // 已访问过的模块ID列表
+ historyStack: ['/home'], // 历史记录栈
+ historyIndex: 0 // 当前在历史栈中的位置
+ };
+
+ let state = { ...defaultState };
+
+ // 加载本地存储的状态
+ function load() {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved) {
+ state = JSON.parse(saved);
+ // 确保必要字段存在
+ if (!state.visitedModules) state.visitedModules = [];
+ if (!state.historyStack || !Array.isArray(state.historyStack)) {
+ state.historyStack = [state.currentRoute || '/home'];
+ }
+ if (typeof state.historyIndex !== 'number') {
+ state.historyIndex = 0;
+ }
+ console.log('[state] restore', state);
+ } else {
+ reset();
+ }
+ } catch (e) {
+ console.warn('[state] load failed, use default', e);
+ reset();
+ }
+ return state;
+ }
+
+ // 保存当前状态到localStorage
+ function save() {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ console.log('[state] save', state);
+ } catch (e) {
+ console.warn('[state] save failed', e);
+ }
+ }
+
+ // 重置为默认状态
+ function reset() {
+ state = { ...defaultState };
+ save();
+ }
+
+ // 更新当前路由
+ function setCurrentRoute(route) {
+ if (state.currentRoute !== route) {
+ state.currentRoute = route;
+ // 添加到已访问模块(如果是模块路由)
+ if (route.startsWith('/module/')) {
+ const moduleId = route.replace('/module/', '');
+ if (!state.visitedModules.includes(moduleId)) {
+ state.visitedModules.push(moduleId);
+ }
+ }
+ save();
+ }
+ }
+
+ // 添加到历史栈(用于前进后退)
+ function pushHistory(route) {
+ // 如果当前不在栈顶,先截断后面的记录
+ if (state.historyIndex < state.historyStack.length - 1) {
+ state.historyStack = state.historyStack.slice(0, state.historyIndex + 1);
+ }
+ state.historyStack.push(route);
+ state.historyIndex = state.historyStack.length - 1;
+ setCurrentRoute(route); // 会触发保存
+ }
+
+ // 后退
+ function goBack() {
+ if (state.historyIndex > 0) {
+ state.historyIndex--;
+ state.currentRoute = state.historyStack[state.historyIndex];
+ save();
+ return state.currentRoute;
+ }
+ return null;
+ }
+
+ // 前进
+ function goForward() {
+ if (state.historyIndex < state.historyStack.length - 1) {
+ state.historyIndex++;
+ state.currentRoute = state.historyStack[state.historyIndex];
+ save();
+ return state.currentRoute;
+ }
+ return null;
+ }
+
+ // 获取当前状态
+ function getState() {
+ return { ...state };
+ }
+
+ // 标记模块为已访问(外部调用)
+ function markModuleVisited(moduleId) {
+ if (!state.visitedModules.includes(moduleId)) {
+ state.visitedModules.push(moduleId);
+ save();
+ }
+ }
+
+ // 清除状态(用于测试)
+ function clear() {
+ localStorage.removeItem(STORAGE_KEY);
+ reset();
+ }
+
+ // 初始化:加载状态
+ load();
+
+ return {
+ load,
+ save,
+ reset,
+ setCurrentRoute,
+ pushHistory,
+ goBack,
+ goForward,
+ getState,
+ markModuleVisited,
+ clear
+ };
+})();
+
+// 导出(如果是模块环境)
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ChannelState;
+}
diff --git a/modules/m-channel/backup-混乱版/channel-style.css b/modules/m-channel/backup-混乱版/channel-style.css
new file mode 100644
index 00000000..f2a8cd20
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/channel-style.css
@@ -0,0 +1,132 @@
+/**
+ * channel-style.css
+ * 频道基础样式 + 过渡动画变量
+ */
+
+/* 引入过渡动画 */
+@import url('channel-transition.css');
+
+:root {
+ --primary-color: #4a90e2;
+ --secondary-color: #f5f5f5;
+ --text-color: #333;
+ --border-radius: 8px;
+ --transition-duration: 0.3s;
+ --transition-timing: ease;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ margin: 0;
+ padding: 20px;
+ background: #f0f2f5;
+ color: var(--text-color);
+}
+
+/* 频道头部 */
+.channel-header {
+ background: white;
+ padding: 15px 20px;
+ border-radius: var(--border-radius);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ margin-bottom: 20px;
+}
+
+.channel-header h1 {
+ margin: 0;
+ font-size: 1.8rem;
+ color: var(--primary-color);
+}
+
+/* 导航菜单 */
+.channel-nav {
+ background: white;
+ padding: 10px 20px;
+ border-radius: var(--border-radius);
+ margin-bottom: 20px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.channel-nav ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ gap: 20px;
+}
+
+.channel-nav a {
+ text-decoration: none;
+ color: var(--text-color);
+ padding: 8px 16px;
+ border-radius: 20px;
+ transition: background 0.2s;
+}
+
+.channel-nav a:hover {
+ background: var(--secondary-color);
+}
+
+.channel-nav a.active {
+ background: var(--primary-color);
+ color: white;
+}
+
+/* 内容区域 */
+.channel-content {
+ background: white;
+ padding: 20px;
+ border-radius: var(--border-radius);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ min-height: 300px;
+}
+
+/* 已访问模块标记 */
+.module-link.visited {
+ position: relative;
+}
+
+.module-link.visited::after {
+ content: "✓";
+ position: absolute;
+ top: -5px;
+ right: -5px;
+ background: var(--primary-color);
+ color: white;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 动画模式切换按钮 */
+.animation-toggle {
+ margin-left: auto;
+ display: flex;
+ gap: 10px;
+}
+
+.animation-toggle button {
+ padding: 5px 15px;
+ border: 1px solid #ddd;
+ background: white;
+ border-radius: 20px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.animation-toggle button.active {
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+/* 调试面板样式(后续使用) */
+.debug-panel {
+ margin-top: 30px;
+ border-top: 2px dashed #ccc;
+ padding-top: 20px;
+}
diff --git a/modules/m-channel/backup-混乱版/channel-transition.css b/modules/m-channel/backup-混乱版/channel-transition.css
new file mode 100644
index 00000000..b13bd025
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/channel-transition.css
@@ -0,0 +1,83 @@
+/**
+ * channel-transition.css
+ * 路由过渡动画 - 让切换像呼吸一样自然
+ * 提供两种模式:淡入淡出(fade) / 滑入滑出(slide)
+ */
+
+/* 基础容器样式 */
+.route-container {
+ position: relative;
+ width: 100%;
+ min-height: 200px;
+}
+
+/* 所有路由页面默认绝对定位,便于重叠动画 */
+.route-page {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+/* 淡入淡出模式 */
+.fade-mode .route-page {
+ opacity: 0;
+ transform: scale(0.98);
+ pointer-events: none;
+}
+
+.fade-mode .route-page.active {
+ opacity: 1;
+ transform: scale(1);
+ pointer-events: auto;
+ position: relative; /* 激活页变为相对定位,占据文档流高度 */
+}
+
+/* 滑入滑出模式 */
+.slide-mode .route-page {
+ opacity: 1;
+ transform: translateX(0);
+ pointer-events: none;
+}
+
+.slide-mode .route-page.active {
+ position: relative;
+ pointer-events: auto;
+}
+
+/* 进入动画:从右侧滑入 */
+.slide-mode .route-page.slide-enter {
+ transform: translateX(100%);
+}
+
+.slide-mode .route-page.active.slide-enter {
+ transform: translateX(0);
+}
+
+/* 离开动画:向左侧滑出(用于后退时的反向) */
+.slide-mode .route-page.slide-exit {
+ transform: translateX(-100%);
+}
+
+/* 前进/后退方向控制 */
+.slide-mode.forward .route-page.slide-enter {
+ transform: translateX(100%);
+}
+
+.slide-mode.forward .route-page.active.slide-enter {
+ transform: translateX(0);
+}
+
+.slide-mode.backward .route-page.slide-enter {
+ transform: translateX(-100%);
+}
+
+.slide-mode.backward .route-page.active.slide-enter {
+ transform: translateX(0);
+}
+
+/* 防止快速点击时动画重叠 */
+.route-page {
+ will-change: transform, opacity;
+}
diff --git a/modules/m-channel/backup-混乱版/event-bus.js b/modules/m-channel/backup-混乱版/event-bus.js
new file mode 100644
index 00000000..ab14007c
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/event-bus.js
@@ -0,0 +1,111 @@
+/**
+ * event-bus.js
+ * 事件总线 - 模块间的群聊频道
+ * 发布/订阅模式,支持命名空间和调试日志
+ */
+
+const EventBus = (function() {
+ // 存储订阅者:{ eventName: [handler1, handler2, ...] }
+ const listeners = {};
+ // 调试模式开关
+ let debugMode = true;
+
+ // 订阅事件
+ function on(eventName, handler) {
+ if (typeof handler !== 'function') {
+ console.error('[bus] 订阅必须提供函数');
+ return;
+ }
+
+ if (!listeners[eventName]) {
+ listeners[eventName] = [];
+ }
+ listeners[eventName].push(handler);
+
+ if (debugMode) {
+ console.log(`[bus] 订阅事件: ${eventName},当前订阅数: ${listeners[eventName].length}`);
+ }
+
+ // 返回取消订阅函数
+ return function off() {
+ off(eventName, handler);
+ };
+ }
+
+ // 取消订阅
+ function off(eventName, handler) {
+ if (!listeners[eventName]) return;
+
+ if (handler) {
+ // 移除特定handler
+ const index = listeners[eventName].indexOf(handler);
+ if (index !== -1) {
+ listeners[eventName].splice(index, 1);
+ if (debugMode) {
+ console.log(`[bus] 取消订阅: ${eventName},剩余: ${listeners[eventName].length}`);
+ }
+ }
+ } else {
+ // 移除该事件所有订阅
+ delete listeners[eventName];
+ if (debugMode) {
+ console.log(`[bus] 移除所有订阅: ${eventName}`);
+ }
+ }
+ }
+
+ // 触发事件
+ function emit(eventName, data) {
+ if (!listeners[eventName]) {
+ if (debugMode) {
+ console.log(`[bus] 触发事件 ${eventName} 但无订阅者`);
+ }
+ return;
+ }
+
+ if (debugMode) {
+ console.log(`[bus] 触发事件: ${eventName},数据:`, data);
+ }
+
+ // 复制一份以防在遍历过程中修改
+ const handlers = listeners[eventName].slice();
+ handlers.forEach(handler => {
+ try {
+ handler(data, eventName);
+ } catch (e) {
+ console.error(`[bus] 事件 ${eventName} 处理出错:`, e);
+ }
+ });
+ }
+
+ // 清空所有订阅
+ function clear() {
+ for (let key in listeners) {
+ delete listeners[key];
+ }
+ if (debugMode) {
+ console.log('[bus] 清空所有订阅');
+ }
+ }
+
+ // 开启/关闭调试
+ function setDebug(enable) {
+ debugMode = enable;
+ }
+
+ return {
+ on,
+ off,
+ emit,
+ clear,
+ setDebug
+ };
+})();
+
+// 挂载到全局
+window.EventBus = EventBus;
+
+// 如果是模块环境
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = EventBus;
+}
diff --git a/modules/m-channel/backup-混乱版/index.html b/modules/m-channel/backup-混乱版/index.html
new file mode 100644
index 00000000..e86e9904
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+ 光湖频道 · 动态渲染引擎
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/m-channel/backup-混乱版/module-lifecycle.js b/modules/m-channel/backup-混乱版/module-lifecycle.js
new file mode 100644
index 00000000..37daed60
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/module-lifecycle.js
@@ -0,0 +1,90 @@
+/**
+ * module-lifecycle.js
+ * 模块生命周期管理 - 让模块知道自己何时加载/卸载/收到消息
+ * 配合事件总线使用
+ */
+
+const ModuleLifecycle = (function() {
+ // 存储每个模块的钩子函数
+ const hooks = {};
+
+ // 注册模块的生命周期钩子
+ function register(moduleId, lifecycles) {
+ if (!moduleId) return;
+
+ hooks[moduleId] = {
+ onLoad: lifecycles.onLoad || null,
+ onUnload: lifecycles.onUnload || null,
+ onMessage: lifecycles.onMessage || null
+ };
+
+ console.log(`[lifecycle] 注册模块: ${moduleId}`, lifecycles);
+
+ // 如果已经加载(比如页面初始化时),自动触发onLoad?
+ // 这里留给loader去调用
+ }
+
+ // 触发模块加载
+ function triggerLoad(moduleId, params) {
+ const moduleHooks = hooks[moduleId];
+ if (moduleHooks && moduleHooks.onLoad) {
+ try {
+ moduleHooks.onLoad(params);
+ console.log(`[lifecycle] onLoad 模块: ${moduleId}`);
+ } catch (e) {
+ console.error(`[lifecycle] onLoad 模块 ${moduleId} 出错:`, e);
+ }
+ }
+ }
+
+ // 触发模块卸载
+ function triggerUnload(moduleId) {
+ const moduleHooks = hooks[moduleId];
+ if (moduleHooks && moduleHooks.onUnload) {
+ try {
+ moduleHooks.onUnload();
+ console.log(`[lifecycle] onUnload 模块: ${moduleId}`);
+ } catch (e) {
+ console.error(`[lifecycle] onUnload 模块 ${moduleId} 出错:`, e);
+ }
+ }
+
+ // 卸载时自动取消该模块的所有事件订阅
+ // 这里简单使用事件总线的off,但需要知道该模块订阅了哪些事件
+ // 我们约定模块在订阅时使用带命名空间的事件名,比如 moduleA:click
+ // 或者在onUnload里手动取消。为了简化,我们不清除订阅,但可以在onUnload里做。
+ // 更完善的做法是记录每个模块的订阅列表,但这里先不实现。
+ }
+
+ // 触发模块收到消息(由事件总线转发时调用)
+ function triggerMessage(moduleId, message, data) {
+ const moduleHooks = hooks[moduleId];
+ if (moduleHooks && moduleHooks.onMessage) {
+ try {
+ moduleHooks.onMessage(message, data);
+ console.log(`[lifecycle] onMessage 模块: ${moduleId} 消息: ${message}`);
+ } catch (e) {
+ console.error(`[lifecycle] onMessage 模块 ${moduleId} 出错:`, e);
+ }
+ }
+ }
+
+ // 获取模块的钩子(用于调试)
+ function getHooks(moduleId) {
+ return hooks[moduleId] || null;
+ }
+
+ return {
+ register,
+ triggerLoad,
+ triggerUnload,
+ triggerMessage,
+ getHooks
+ };
+})();
+
+window.ModuleLifecycle = ModuleLifecycle;
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ModuleLifecycle;
+}
diff --git a/modules/m-channel/backup-混乱版/module-loader.js b/modules/m-channel/backup-混乱版/module-loader.js
new file mode 100644
index 00000000..27474424
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/module-loader.js
@@ -0,0 +1,116 @@
+/**
+ * module-loader.js
+ * 模块加载器 - 负责动态加载模块HTML/JS,并管理生命周期
+ * 集成事件总线和生命周期钩子
+ */
+
+const ModuleLoader = (function() {
+ // 已加载的模块缓存
+ const loadedModules = {};
+
+ // 加载模块
+ async function loadModule(moduleId, container, params = {}) {
+ if (loadedModules[moduleId]) {
+ console.log(`[loader] 模块 ${moduleId} 已加载,直接显示`);
+ showModule(moduleId, container, params);
+ return;
+ }
+
+ try {
+ console.log(`[loader] 正在加载模块: ${moduleId}`);
+
+ // 获取模块URL(这里使用mock-modules下的html文件)
+ const url = `mock-modules/mock-${moduleId}.html`;
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const html = await response.text();
+
+ // 解析HTML,提取body内容作为模块内容
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ let moduleContent = doc.body.innerHTML;
+
+ // 提取script标签并执行
+ const scripts = doc.querySelectorAll('script');
+ scripts.forEach(script => {
+ const newScript = document.createElement('script');
+ if (script.src) {
+ newScript.src = script.src;
+ } else {
+ newScript.textContent = script.textContent;
+ }
+ document.body.appendChild(newScript);
+ // 注意:动态添加的脚本会立即执行
+ });
+
+ // 存储模块内容
+ loadedModules[moduleId] = {
+ content: moduleContent,
+ scripts: scripts.length
+ };
+
+ // 显示模块
+ showModule(moduleId, container, params);
+
+ // 触发生命周期 onLoad
+ ModuleLifecycle.triggerLoad(moduleId, params);
+
+ // 标记已访问(状态管理)
+ if (window.ChannelState) {
+ ChannelState.markModuleVisited(moduleId);
+ }
+
+ console.log(`[loader] 模块 ${moduleId} 加载完成`);
+ } catch (error) {
+ console.error(`[loader] 加载模块 ${moduleId} 失败:`, error);
+ container.innerHTML = `加载失败:${error.message}
`;
+ }
+ }
+
+ // 显示已加载的模块
+ function showModule(moduleId, container, params) {
+ const mod = loadedModules[moduleId];
+ if (!mod) return;
+
+ container.innerHTML = mod.content;
+
+ // 如果有参数,可以通过自定义事件传递
+ if (params) {
+ const event = new CustomEvent('module:params', { detail: params });
+ container.dispatchEvent(event);
+ }
+
+ // 通知模块已显示(通过生命周期?)
+ // 可以用triggerMessage
+ ModuleLifecycle.triggerMessage(moduleId, 'show', params);
+ }
+
+ // 卸载模块
+ function unloadModule(moduleId) {
+ if (loadedModules[moduleId]) {
+ // 触发生命周期 onUnload
+ ModuleLifecycle.triggerUnload(moduleId);
+
+ // 清理缓存(可选)
+ delete loadedModules[moduleId];
+ console.log(`[loader] 模块 ${moduleId} 已卸载`);
+ }
+ }
+
+ // 预加载模块
+ function preloadModule(moduleId) {
+ // 简单fetch但不显示
+ fetch(`mock-modules/mock-${moduleId}.html`).catch(() => {});
+ }
+
+ return {
+ loadModule,
+ unloadModule,
+ preloadModule
+ };
+})();
+
+window.ModuleLoader = ModuleLoader;
diff --git a/modules/m-channel/backup-混乱版/module-registry.js b/modules/m-channel/backup-混乱版/module-registry.js
new file mode 100644
index 00000000..a0a87ff3
--- /dev/null
+++ b/modules/m-channel/backup-混乱版/module-registry.js
@@ -0,0 +1,7 @@
+const moduleRegistry = {
+ 'mock-a': 'mock-modules/mock-a.html',
+ 'mock-b': 'mock-modules/mock-b.html',
+ 'mock-c': 'mock-modules/mock-c.html',
+ 'mock-d': 'mock-modules/mock-d.html'
+};
+window.moduleRegistry = moduleRegistry;
diff --git a/modules/m-channel/channel-analytics.js b/modules/m-channel/channel-analytics.js
new file mode 100644
index 00000000..f1b43148
--- /dev/null
+++ b/modules/m-channel/channel-analytics.js
@@ -0,0 +1,163 @@
+
+/**
+ * channel-analytics.js
+ * 频道数据采集核心·环节8
+ * 记录模块访问次数、停留时间、页面加载速度
+ */
+
+const ChannelAnalytics = (function() {
+ const STORAGE_KEY = 'channel-analytics-data';
+
+ // 数据结构初始化
+ function getDefaultData() {
+ return {
+ modules: {}, // {moduleId: {visits: 0, totalDuration: 0, loadTimes: [], dailyVisits: {} }}
+ globalDaily: {}, // {'YYYY-MM-DD': totalVisits}
+ lastUpdated: null
+ };
+ }
+
+ // localStorage 读写
+ function loadData() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return JSON.parse(raw);
+ } catch(e) {
+ console.log('⚠️ 读取分析数据失败,重新初始化');
+ }
+ return getDefaultData();
+ }
+
+ function saveData(data) {
+ data.lastUpdated = new Date().toISOString();
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ }
+
+ // 日期工具
+ function today() {
+ return new Date().toISOString().split('T')[0];
+ }
+
+ // 确保模块数据结构存在
+ function ensureModule(data, moduleId) {
+ if (!data.modules[moduleId]) {
+ data.modules[moduleId] = {
+ visits: 0,
+ totalDuration: 0,
+ loadTimes: [],
+ dailyVisits: {}
+ };
+ }
+ return data.modules[moduleId];
+ }
+
+ // 当前会话状态
+ let currentModule = null;
+ let enterTime = null;
+ let loadStartTime = null;
+
+ // 公开方法
+ return {
+ // 记录模块访问
+ recordVisit: function(moduleId) {
+ if (!moduleId) return;
+ const data = loadData();
+ const mod = ensureModule(data, moduleId);
+
+ // 访问计数 +1
+ mod.visits++;
+
+ // 每日访问计数
+ const d = today();
+ mod.dailyVisits[d] = (mod.dailyVisits[d] || 0) + 1;
+
+ // 全局每日访问
+ data.globalDaily[d] = (data.globalDaily[d] || 0) + 1;
+
+ saveData(data);
+ console.log('📊 已记录:模块 ' + moduleId + ' 被访问,累计 ' + mod.visits + ' 次');
+
+ // 记录进入时间
+ this.startSession(moduleId);
+ },
+
+ // 开始计时
+ startSession: function(moduleId) {
+ // 先结束上一个模块的计时
+ if (currentModule && enterTime) {
+ this.endSession();
+ }
+ currentModule = moduleId;
+ enterTime = performance.now();
+ loadStartTime = performance.now();
+ },
+
+ // 结束计时(切换模块或离开时调用)
+ endSession: function() {
+ if (!currentModule || !enterTime) return;
+ const duration = Math.round((performance.now() - enterTime) / 1000); // 秒
+ const data = loadData();
+ const mod = ensureModule(data, currentModule);
+ mod.totalDuration += duration;
+ saveData(data);
+ console.log('⏱️ 停留时间:模块 ' + currentModule + ' 停留约 ' + duration + ' 秒');
+ currentModule = null;
+ enterTime = null;
+ },
+
+ // 记录加载性能
+ recordLoadTime: function(moduleId, loadTimeMs) {
+ if (!moduleId) return;
+ const data = loadData();
+ const mod = ensureModule(data, moduleId);
+ mod.loadTimes.push(loadTimeMs);
+ // 只保留最近50次
+ if (mod.loadTimes.length > 50) {
+ mod.loadTimes = mod.loadTimes.slice(-50);
+ }
+ saveData(data);
+ console.log('⚡ 加载耗时:模块 ' + moduleId + ' 加载 ' + Math.round(loadTimeMs) + ' 毫秒');
+ },
+
+ // 标记加载开始
+ markLoadStart: function() {
+ loadStartTime = performance.now();
+ },
+
+ // 标记加载完成并记录
+ markLoadEnd: function(moduleId) {
+ if (loadStartTime && moduleId) {
+ const loadTime = performance.now() - loadStartTime;
+ this.recordLoadTime(moduleId, loadTime);
+ loadStartTime = null;
+ }
+ },
+
+ // 获取所有数据(面板用)
+ getAllData: function() {
+ return loadData();
+ },
+
+ // 获取最近7天趋势
+ getWeeklyTrend: function() {
+ const data = loadData();
+ const trend = [];
+ for (let i = 6; i >= 0; i--) {
+ const d = new Date();
+ d.setDate(d.getDate() - i);
+ const dateStr = d.toISOString().split('T')[0];
+ trend.push({
+ date: dateStr,
+ visits: data.globalDaily[dateStr] || 0
+ });
+ }
+ return trend;
+ },
+
+ // 清除所有数据(调试用)
+ clearAll: function() {
+ localStorage.removeItem(STORAGE_KEY);
+ console.log('🗑️ 所有分析数据已清除');
+ }
+ };
+})();
diff --git a/modules/m-channel/channel-complete.html b/modules/m-channel/channel-complete.html
new file mode 100644
index 00000000..91629573
--- /dev/null
+++ b/modules/m-channel/channel-complete.html
@@ -0,0 +1,267 @@
+
+
+
+
+
+ 光湖频道 · 完全版
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
主题色
+
+
+
+
+
+
+
+
+
+
统计数据
+
+
+
+
其他
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/m-channel/channel-dashboard.css b/modules/m-channel/channel-dashboard.css
new file mode 100644
index 00000000..1269c86f
--- /dev/null
+++ b/modules/m-channel/channel-dashboard.css
@@ -0,0 +1,199 @@
+/**
+ * channel-dashboard.css
+ * 频道数据面板样式·深色主题
+ */
+
+.dashboard-container {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+ color: #e0e0e0;
+}
+
+.dashboard-header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.dashboard-header h2 {
+ font-size: 24px;
+ color: #4fc3f7;
+ margin-bottom: 8px;
+}
+
+.dashboard-header p {
+ font-size: 14px;
+ color: #888;
+}
+
+/* 图表网格 */
+.charts-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.chart-card {
+ background: #1a1a2e;
+ border-radius: 12px;
+ padding: 20px;
+ border: 1px solid #2a2a4a;
+}
+
+.chart-card h3 {
+ font-size: 16px;
+ color: #4fc3f7;
+ margin-bottom: 15px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #2a2a4a;
+}
+
+.chart-card canvas {
+ width: 100% !important;
+ height: 200px !important;
+ display: block;
+}
+
+/* 柱状图 */
+.bar-chart {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-around;
+ height: 200px;
+ padding: 10px 0;
+ border-bottom: 2px solid #333;
+}
+
+.bar-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ max-width: 80px;
+}
+
+.bar-fill {
+ width: 40px;
+ border-radius: 4px 4px 0 0;
+ transition: height 0.5s ease;
+ min-height: 4px;
+}
+
+.bar-label {
+ margin-top: 8px;
+ font-size: 11px;
+ color: #aaa;
+ text-align: center;
+ word-break: break-all;
+}
+
+.bar-value {
+ font-size: 12px;
+ color: #fff;
+ margin-bottom: 4px;
+}
+
+/* 饼图 */
+.pie-container {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.pie-canvas-wrap {
+ flex-shrink: 0;
+}
+
+.pie-legend {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.pie-legend li {
+ display: flex;
+ align-items: center;
+ margin-bottom: 6px;
+ font-size: 13px;
+}
+
+.pie-legend .dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-right: 8px;
+ flex-shrink: 0;
+}
+
+/* 折线图 */
+.line-chart-wrap {
+ position: relative;
+ height: 200px;
+}
+
+/* 性能表格 */
+.perf-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.perf-table th {
+ text-align: left;
+ padding: 8px;
+ background: #2a2a4a;
+ color: #4fc3f7;
+ font-weight: normal;
+}
+
+.perf-table td {
+ padding: 8px;
+ border-bottom: 1px solid #2a2a4a;
+}
+
+.perf-table tr:last-child td {
+ border-bottom: none;
+}
+
+.perf-table .slow {
+ color: #ff5252;
+ font-weight: bold;
+}
+
+/* 按钮 */
+.dashboard-actions {
+ margin-top: 20px;
+ text-align: right;
+}
+
+.btn-clear {
+ background: #ff5252;
+ color: #fff;
+ border: none;
+ padding: 10px 24px;
+ border-radius: 8px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.btn-clear:hover {
+ background: #ff1744;
+}
+
+/* 响应式 */
+@media (max-width: 768px) {
+ .charts-grid {
+ grid-template-columns: 1fr;
+ }
+ .pie-container {
+ flex-direction: column;
+ }
+}
+
+/* 饼图 canvas 保持正方形 */
+.pie-canvas-wrap canvas {
+ width: 160px !important;
+ height: 160px !important;
+}
diff --git a/modules/m-channel/channel-dashboard.js b/modules/m-channel/channel-dashboard.js
new file mode 100644
index 00000000..13f3555b
--- /dev/null
+++ b/modules/m-channel/channel-dashboard.js
@@ -0,0 +1,201 @@
+
+/**
+ * channel-dashboard.js
+ * 频道数据面板逻辑·图表渲染
+ */
+
+const ChannelDashboard = (function() {
+ // 配色方案
+ const COLORS = ['#4fc3f7', '#ffb74d', '#ff8a80', '#aed581', '#ba68c8', '#4dd0e1', '#ffd54f'];
+
+ // 柱状图渲染
+ function renderBarChart() {
+ const container = document.getElementById('visitBarChart');
+ if (!container) return;
+ const data = ChannelAnalytics.getAllData();
+ const modules = data.modules;
+ const keys = Object.keys(modules);
+ if (keys.length === 0) {
+ container.innerHTML = '暂无数据,多点几个模块再来看
';
+ return;
+ }
+ const maxVisits = Math.max.apply(null, keys.map(function(k) { return modules[k].visits; })) || 1;
+ let html = '';
+ keys.forEach(function(id, i) {
+ const mod = modules[id];
+ const height = Math.max(4, (mod.visits / maxVisits) * 180);
+ const color = COLORS[i % COLORS.length];
+ html += '' +
+ '
' + mod.visits + '' +
+ '
' +
+ '
' + id.replace('m-', '') + '' +
+ '
';
+ });
+ container.innerHTML = html;
+ }
+
+ // 饼图渲染
+ function renderPieChart() {
+ const canvas = document.getElementById('pieCanvas');
+ const legendEl = document.getElementById('pieLegend');
+ if (!canvas || !legendEl) return;
+ const ctx = canvas.getContext('2d');
+ const data = ChannelAnalytics.getAllData();
+ const modules = data.modules;
+ const keys = Object.keys(modules);
+ const total = keys.reduce(function(sum, k) {
+ return sum + (modules[k].totalDuration || 0);
+ }, 0);
+
+ // 清空画布
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ if (total === 0 || keys.length === 0) {
+ ctx.fillStyle = '#333';
+ ctx.beginPath();
+ ctx.arc(80, 80, 70, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillStyle = '#666';
+ ctx.font = '12px sans-serif';
+ ctx.textAlign = 'center';
+ ctx.fillText('暂无数据', 80, 84);
+ legendEl.innerHTML = '';
+ return;
+ }
+
+ let startAngle = -Math.PI / 2;
+ let legendHtml = '';
+ keys.forEach(function(id, i) {
+ const mod = modules[id];
+ const pct = mod.totalDuration / total;
+ const sweep = pct * Math.PI * 2;
+ const color = COLORS[i % COLORS.length];
+
+ ctx.beginPath();
+ ctx.moveTo(80, 80);
+ ctx.arc(80, 80, 70, startAngle, startAngle + sweep);
+ ctx.closePath();
+ ctx.fillStyle = color;
+ ctx.fill();
+
+ startAngle += sweep;
+
+ const minutes = Math.round(mod.totalDuration / 60);
+ const pctStr = Math.round(pct * 100);
+ legendHtml += '' +
+ id.replace('m-', '') + ' ' + pctStr + '% (' + minutes + '分钟)';
+ });
+ legendEl.innerHTML = legendHtml;
+ }
+
+ // 折线图渲染
+ function renderLineChart() {
+ const canvas = document.getElementById('lineCanvas');
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const trend = ChannelAnalytics.getWeeklyTrend();
+ const w = canvas.width;
+ const h = canvas.height;
+
+ ctx.clearRect(0, 0, w, h);
+
+ const maxVal = Math.max.apply(null, trend.map(function(t) { return t.visits; })) || 1;
+ const padLeft = 40;
+ const padBottom = 30;
+ const padTop = 10;
+ const chartW = w - padLeft - 20;
+ const chartH = h - padBottom - padTop;
+ const stepX = chartW / (trend.length - 1 || 1);
+
+ // 网格线
+ ctx.strokeStyle = '#2a2a4a';
+ ctx.lineWidth = 1;
+ for (let g = 0; g <= 4; g++) {
+ let gy = padTop + (chartH / 4) * g;
+ ctx.beginPath();
+ ctx.moveTo(padLeft, gy);
+ ctx.lineTo(w - 20, gy);
+ ctx.stroke();
+ }
+
+ // 折线
+ ctx.strokeStyle = '#4fc3f7';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ trend.forEach(function(t, i) {
+ let x = padLeft + stepX * i;
+ let y = padTop + chartH - (t.visits / maxVal) * chartH;
+ if (i === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ });
+ ctx.stroke();
+
+ // 数据点
+ trend.forEach(function(t, i) {
+ let x = padLeft + stepX * i;
+ let y = padTop + chartH - (t.visits / maxVal) * chartH;
+ ctx.beginPath();
+ ctx.arc(x, y, 4, 0, Math.PI * 2);
+ ctx.fillStyle = '#4fc3f7';
+ ctx.fill();
+ });
+
+ // 标签
+ ctx.fillStyle = '#aaa';
+ ctx.font = '11px sans-serif';
+ ctx.textAlign = 'center';
+ trend.forEach(function(t, i) {
+ let x = padLeft + stepX * i;
+ let y = padTop + chartH + 15;
+ ctx.fillText(t.date.slice(5), x, y);
+ });
+ }
+
+ // 性能表格渲染
+ function renderPerfTable() {
+ const tbody = document.getElementById('perfTableBody');
+ if (!tbody) return;
+ const data = ChannelAnalytics.getAllData();
+ const modules = data.modules;
+ const keys = Object.keys(modules);
+ if (keys.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ return;
+ }
+
+ let html = '';
+ keys.forEach(function(id) {
+ const mod = modules[id];
+ const avgLoad = mod.loadTimes.length ?
+ Math.round(mod.loadTimes.reduce((a, b) => a + b, 0) / mod.loadTimes.length) : 0;
+ const minutes = Math.round(mod.totalDuration / 60);
+ const statusClass = avgLoad > 300 ? 'slow' : '';
+ const statusText = avgLoad > 300 ? '⚠️ 慢' : '✅ 正常';
+
+ html += '' +
+ '| ' + id.replace('m-', '') + ' | ' +
+ '' + mod.visits + ' | ' +
+ '' + minutes + '分钟 | ' +
+ '' + avgLoad + 'ms | ' +
+ '' + statusText + ' | ' +
+ '
';
+ });
+ tbody.innerHTML = html;
+ }
+
+ // 公开方法
+ return {
+ render: function() {
+ renderBarChart();
+ renderPieChart();
+ renderLineChart();
+ renderPerfTable();
+ },
+ clearData: function() {
+ if (confirm('确定清除所有统计数据吗?')) {
+ ChannelAnalytics.clearAll();
+ this.render();
+ console.log('🗑️ 数据已清除,图表已重置');
+ }
+ }
+ };
+})();
diff --git a/modules/m-channel/channel-enhancements.js b/modules/m-channel/channel-enhancements.js
new file mode 100644
index 00000000..36896616
--- /dev/null
+++ b/modules/m-channel/channel-enhancements.js
@@ -0,0 +1,338 @@
+// channel-enhancements.js - 频道搜索、面包屑、快捷键增强功能(终极修复版)
+(function() {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+ function init() {
+ addStyles();
+ createSearchBox();
+ createBreadcrumb();
+ createShortcutHint();
+ initShortcuts();
+ bindCardClicks(); // ★ 新增:为卡片绑定点击切换模块
+ window.addEventListener('hashchange', updateBreadcrumb);
+ updateBreadcrumb();
+ }
+
+ function addStyles() {
+ const style = document.createElement('style');
+ style.textContent = `
+ .channel-search-container {
+ padding: 16px 20px;
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .channel-search-input {
+ flex: 1;
+ padding: 10px 16px;
+ border: 1px solid #ced4da;
+ border-radius: 24px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+ }
+ .channel-search-input:focus {
+ border-color: #4d6bfe;
+ box-shadow: 0 0 0 3px rgba(77,107,254,0.1);
+ }
+ .channel-search-clear {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ color: #6c757d;
+ padding: 0 8px;
+ display: none;
+ }
+ .channel-search-clear:hover {
+ color: #212529;
+ }
+ .channel-search-input:not(:placeholder-shown) + .channel-search-clear {
+ display: inline-block;
+ }
+ .highlight {
+ background-color: #ffeb3b;
+ padding: 2px 0;
+ border-radius: 2px;
+ }
+ .no-results {
+ text-align: center;
+ padding: 40px;
+ color: #6c757d;
+ font-style: italic;
+ }
+ .channel-breadcrumb {
+ padding: 12px 20px;
+ background: white;
+ border-bottom: 1px solid #e9ecef;
+ font-size: 14px;
+ }
+ .channel-breadcrumb a {
+ color: #4d6bfe;
+ text-decoration: none;
+ }
+ .channel-breadcrumb a:hover {
+ text-decoration: underline;
+ }
+ .channel-breadcrumb span {
+ color: #6c757d;
+ }
+ .channel-breadcrumb .current {
+ color: #212529;
+ font-weight: 500;
+ }
+ .shortcut-hint {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: rgba(0,0,0,0.7);
+ color: white;
+ padding: 8px 16px;
+ border-radius: 30px;
+ font-size: 12px;
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+ }
+ .shortcut-hint kbd {
+ background: rgba(255,255,255,0.2);
+ padding: 2px 6px;
+ border-radius: 4px;
+ margin: 0 2px;
+ }
+ .module-card.selected {
+ outline: 2px solid #4d6bfe;
+ outline-offset: 2px;
+ transform: scale(1.02);
+ transition: all 0.2s;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ function createSearchBox() {
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.querySelector('.channel-content');
+ if (!container) return;
+
+ const searchContainer = document.createElement('div');
+ searchContainer.className = 'channel-search-container';
+ searchContainer.innerHTML = `
+
+
+ `;
+ container.parentNode.insertBefore(searchContainer, container);
+
+ const searchInput = searchContainer.querySelector('.channel-search-input');
+ const clearBtn = searchContainer.querySelector('.channel-search-clear');
+
+ searchInput.addEventListener('input', function() {
+ filterModules(this.value);
+ });
+
+ clearBtn.addEventListener('click', function() {
+ searchInput.value = '';
+ filterModules('');
+ searchInput.focus();
+ });
+
+ window.__channelSearchInput = searchInput;
+ }
+
+ function filterModules(keyword) {
+ const cards = document.querySelectorAll('.module-card');
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
+ let hasResults = false;
+
+ // 移除所有高亮
+ removeAllHighlights();
+
+ cards.forEach(card => {
+ const text = card.innerText || card.textContent;
+ if (keyword === '') {
+ card.style.display = '';
+ hasResults = true;
+ } else {
+ const lowerText = text.toLowerCase();
+ const lowerKeyword = keyword.toLowerCase();
+ if (lowerText.includes(lowerKeyword)) {
+ card.style.display = '';
+ highlightText(card, keyword);
+ hasResults = true;
+ } else {
+ card.style.display = 'none';
+ }
+ }
+ });
+
+ let noResultsEl = document.querySelector('.no-results');
+ if (!hasResults && keyword !== '') {
+ if (!noResultsEl) {
+ noResultsEl = document.createElement('div');
+ noResultsEl.className = 'no-results';
+ noResultsEl.textContent = '没有找到匹配的模块';
+ container.parentNode.insertBefore(noResultsEl, container.nextSibling);
+ }
+ } else {
+ if (noResultsEl) noResultsEl.remove();
+ }
+ }
+
+ function removeAllHighlights() {
+ document.querySelectorAll('.highlight').forEach(span => {
+ const parent = span.parentNode;
+ parent.replaceChild(document.createTextNode(span.textContent), span);
+ parent.normalize();
+ });
+ }
+
+ function highlightText(card, keyword) {
+ const regex = new RegExp(`(${keyword})`, 'gi');
+ const walk = document.createTreeWalker(card, NodeFilter.SHOW_TEXT, {
+ acceptNode: function(node) {
+ if (node.parentNode.classList && node.parentNode.classList.contains('highlight')) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }, false);
+
+ const textNodes = [];
+ while (walk.nextNode()) textNodes.push(walk.currentNode);
+
+ textNodes.forEach(node => {
+ const text = node.nodeValue;
+ if (regex.test(text)) {
+ const span = document.createElement('span');
+ span.className = 'highlight';
+ span.innerHTML = text.replace(regex, '$1');
+ node.parentNode.replaceChild(span, node);
+ }
+ });
+ }
+
+ function createBreadcrumb() {
+ const searchContainer = document.querySelector('.channel-search-container');
+ const breadcrumb = document.createElement('div');
+ breadcrumb.className = 'channel-breadcrumb';
+ breadcrumb.id = 'channelBreadcrumb';
+ if (searchContainer) {
+ searchContainer.parentNode.insertBefore(breadcrumb, searchContainer.nextSibling);
+ } else {
+ const container = document.querySelector('.module-grid') || document.querySelector('#module-list');
+ if (container) {
+ container.parentNode.insertBefore(breadcrumb, container);
+ }
+ }
+ }
+
+ function updateBreadcrumb() {
+ const breadcrumb = document.getElementById('channelBreadcrumb');
+ if (!breadcrumb) return;
+ const hash = window.location.hash.slice(1) || '';
+ let moduleName = '频道';
+ if (hash.startsWith('module-')) {
+ const moduleId = hash.replace('module-', '');
+ const moduleNames = {
+ 'M06': '工单管理',
+ 'M08': '数据看板',
+ 'M11': '组件库',
+ 'debug': '调试面板'
+ };
+ moduleName = moduleNames[moduleId] || moduleId;
+ }
+ breadcrumb.innerHTML = `
+ 首页 >
+ 频道 >
+ ${moduleName}
+ `;
+ }
+
+ function createShortcutHint() {
+ const hint = document.createElement('div');
+ hint.className = 'shortcut-hint';
+ hint.innerHTML = `
+ ⌘K 搜索 ·
+ ↑↓ 选择 ·
+ Enter 打开 ·
+ Esc 关闭
+ `;
+ document.body.appendChild(hint);
+ }
+
+ function initShortcuts() {
+ document.addEventListener('keydown', function(e) {
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ const searchInput = window.__channelSearchInput;
+ if (searchInput) searchInput.focus();
+ }
+ if (e.key === 'Escape') {
+ const searchInput = window.__channelSearchInput;
+ if (searchInput && document.activeElement === searchInput) {
+ searchInput.value = '';
+ filterModules('');
+ searchInput.blur();
+ } else if (searchInput && searchInput.value) {
+ searchInput.value = '';
+ filterModules('');
+ }
+ }
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+ const cards = Array.from(document.querySelectorAll('.module-card:not([style*="display: none"])'));
+ if (cards.length === 0) return;
+ e.preventDefault();
+ let selectedIndex = cards.findIndex(card => card.classList.contains('selected'));
+ if (selectedIndex === -1) {
+ selectedIndex = e.key === 'ArrowDown' ? 0 : cards.length - 1;
+ } else {
+ cards[selectedIndex].classList.remove('selected');
+ if (e.key === 'ArrowDown') {
+ selectedIndex = (selectedIndex + 1) % cards.length;
+ } else {
+ selectedIndex = (selectedIndex - 1 + cards.length) % cards.length;
+ }
+ }
+ cards[selectedIndex].classList.add('selected');
+ cards[selectedIndex].scrollIntoView({ block: 'nearest' });
+ }
+ if (e.key === 'Enter') {
+ const selected = document.querySelector('.module-card.selected');
+ if (selected) {
+ selected.click(); // 触发我们绑定的点击事件
+ }
+ }
+ });
+ }
+
+ // ★★★ 核心修复:为所有卡片绑定点击切换模块 ★★★
+ function bindCardClicks() {
+ const grid = document.querySelector('.module-grid') || document.querySelector('#module-list') || document.body;
+ grid.addEventListener('click', function(e) {
+ const card = e.target.closest('.module-card');
+ if (!card) return;
+
+ // 阻止可能冲突的其他事件(可选)
+ e.preventDefault();
+
+ // 根据卡片文字判断是哪个模块
+ const text = card.innerText || card.textContent;
+ let moduleId = null;
+ if (text.includes('工单管理')) moduleId = 'M06';
+ else if (text.includes('数据统计')) moduleId = 'M08';
+ else if (text.includes('组件库')) moduleId = 'M11';
+ else if (text.includes('调试面板')) moduleId = 'debug';
+ // 如果识别不到,尝试从其他属性获取(比如 data-module-id)
+ else if (card.dataset.moduleId) moduleId = card.dataset.moduleId;
+
+ if (moduleId) {
+ // 改变 hash,触发路由更新
+ window.location.hash = `module-${moduleId}`;
+ }
+ });
+ }
+})();
diff --git a/modules/m-channel/channel-favorites.js b/modules/m-channel/channel-favorites.js
new file mode 100644
index 00000000..4208f653
--- /dev/null
+++ b/modules/m-channel/channel-favorites.js
@@ -0,0 +1,271 @@
+// 频道收藏与拖拽排序管理
+window.ChannelFavorites = {
+ // 当前正在拖拽的元素
+ draggingElement: null,
+
+ // 初始化
+ init: function() {
+ console.log('[favorites] 初始化');
+ this.bindEvents();
+ this.renderFavorites();
+ },
+
+ // 绑定事件
+ bindEvents: function() {
+ // 使用事件委托监听星星点击
+ document.addEventListener('click', (e) => {
+ const star = e.target.closest('.favorite-star');
+ if (star) {
+ e.preventDefault();
+ const card = star.closest('.module-card');
+ if (card) {
+ const moduleId = card.dataset.module;
+ if (moduleId) {
+ this.toggleFavorite(moduleId, star);
+ }
+ }
+ }
+ });
+
+ // 监听偏好变化,重新渲染收藏状态
+ if (window.EventBus) {
+ EventBus.on('preferences:changed', (data) => {
+ if (data.key === 'favorites' || data.full) {
+ this.updateAllStars();
+ }
+ });
+ }
+ },
+
+ // 切换收藏状态
+ toggleFavorite: function(moduleId, starElement) {
+ if (!window.ChannelPreferences) return;
+
+ const isNowFavorite = ChannelPreferences.toggleFavorite(moduleId);
+
+ // 更新星星样式
+ if (starElement) {
+ if (isNowFavorite) {
+ starElement.classList.add('active');
+ starElement.textContent = '★';
+ } else {
+ starElement.classList.remove('active');
+ starElement.textContent = '☆';
+ }
+ }
+
+ // 触发布局更新(收藏置顶)
+ this.updateFavoritesOrder();
+
+ // 发送事件
+ if (window.EventBus) {
+ EventBus.emit('favorite:toggled', {
+ module: moduleId,
+ favorite: isNowFavorite
+ });
+ }
+ },
+
+ // 更新所有星星的显示状态
+ updateAllStars: function() {
+ const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : [];
+ document.querySelectorAll('.favorite-star').forEach(star => {
+ const card = star.closest('.module-card');
+ if (card) {
+ const moduleId = card.dataset.module;
+ if (moduleId) {
+ if (favorites.includes(moduleId)) {
+ star.classList.add('active');
+ star.textContent = '★';
+ } else {
+ star.classList.remove('active');
+ star.textContent = '☆';
+ }
+ }
+ }
+ });
+ },
+
+ // 根据收藏状态重新排序(收藏置顶)
+ updateFavoritesOrder: function() {
+ const container = document.querySelector('.channel-content');
+ if (!container) return;
+
+ const cards = Array.from(container.querySelectorAll('.module-card'));
+ const favorites = window.ChannelPreferences ? ChannelPreferences.getFavorites() : [];
+
+ // 按收藏状态和原有顺序排序
+ cards.sort((a, b) => {
+ const aId = a.dataset.module;
+ const bId = b.dataset.module;
+ const aFav = favorites.includes(aId);
+ const bFav = favorites.includes(bId);
+
+ if (aFav && !bFav) return -1;
+ if (!aFav && bFav) return 1;
+
+ // 如果都是收藏或都不是收藏,保持原有顺序
+ const aOrder = cards.indexOf(a);
+ const bOrder = cards.indexOf(b);
+ return aOrder - bOrder;
+ });
+
+ // 重新插入到容器中
+ cards.forEach(card => container.appendChild(card));
+
+ // 保存排序到偏好设置
+ const moduleOrder = cards.map(card => card.dataset.module);
+ if (window.ChannelPreferences) {
+ ChannelPreferences.setModuleOrder(moduleOrder);
+ }
+ },
+
+ // 渲染收藏状态(初始化时调用)
+ renderFavorites: function() {
+ this.updateAllStars();
+ },
+
+ // ===== 拖拽排序相关 =====
+
+ // 初始化拖拽
+ initDragAndDrop: function() {
+ console.log('[favorites] 初始化拖拽排序');
+
+ const container = document.querySelector('.channel-content');
+ if (!container) return;
+
+ // 为每个卡片添加拖拽手柄
+ container.querySelectorAll('.module-card').forEach(card => {
+ // 检查是否已有拖拽手柄
+ if (!card.querySelector('.drag-handle')) {
+ const header = card.querySelector('.module-card-header');
+ if (header) {
+ const handle = document.createElement('span');
+ handle.className = 'drag-handle';
+ handle.innerHTML = '⋮⋮';
+ handle.setAttribute('draggable', 'false');
+ header.insertBefore(handle, header.firstChild);
+ }
+ }
+
+ // 设置 draggable
+ card.setAttribute('draggable', 'true');
+
+ // 移除旧监听器,添加新监听器
+ card.removeEventListener('dragstart', this.handleDragStart);
+ card.removeEventListener('dragend', this.handleDragEnd);
+ card.removeEventListener('dragover', this.handleDragOver);
+ card.removeEventListener('dragenter', this.handleDragEnter);
+ card.removeEventListener('dragleave', this.handleDragLeave);
+ card.removeEventListener('drop', this.handleDrop);
+
+ card.addEventListener('dragstart', this.handleDragStart.bind(this));
+ card.addEventListener('dragend', this.handleDragEnd.bind(this));
+ card.addEventListener('dragover', this.handleDragOver);
+ card.addEventListener('dragenter', this.handleDragEnter);
+ card.addEventListener('dragleave', this.handleDragLeave);
+ card.addEventListener('drop', this.handleDrop.bind(this));
+ });
+ },
+
+ // 拖拽开始
+ handleDragStart: function(e) {
+ this.draggingElement = e.target.closest('.module-card');
+ if (!this.draggingElement) return;
+
+ e.dataTransfer.setData('text/plain', this.draggingElement.dataset.module);
+ e.dataTransfer.effectAllowed = 'move';
+
+ // 添加拖拽中的样式
+ setTimeout(() => {
+ this.draggingElement.classList.add('dragging');
+ }, 0);
+ },
+
+ // 拖拽结束
+ handleDragEnd: function(e) {
+ const card = e.target.closest('.module-card');
+ if (card) {
+ card.classList.remove('dragging');
+ }
+
+ // 移除所有高亮
+ document.querySelectorAll('.module-card.drop-target').forEach(el => {
+ el.classList.remove('drop-target');
+ });
+
+ this.draggingElement = null;
+ },
+
+ // 拖拽经过(必须阻止默认事件才能成为放置目标)
+ handleDragOver: function(e) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ },
+
+ // 拖拽进入
+ handleDragEnter: function(e) {
+ const card = e.target.closest('.module-card');
+ if (card && card !== this.draggingElement) {
+ card.classList.add('drop-target');
+ }
+ },
+
+ // 拖拽离开
+ handleDragLeave: function(e) {
+ const card = e.target.closest('.module-card');
+ if (card) {
+ card.classList.remove('drop-target');
+ }
+ },
+
+ // 放置
+ handleDrop: function(e) {
+ e.preventDefault();
+
+ const targetCard = e.target.closest('.module-card');
+ if (!targetCard || !this.draggingElement || targetCard === this.draggingElement) {
+ return;
+ }
+
+ // 移除高亮
+ targetCard.classList.remove('drop-target');
+
+ // 获取所有卡片
+ const container = document.querySelector('.channel-content');
+ const cards = Array.from(container.querySelectorAll('.module-card'));
+
+ const fromIndex = cards.indexOf(this.draggingElement);
+ const toIndex = cards.indexOf(targetCard);
+
+ if (fromIndex === -1 || toIndex === -1) return;
+
+ // 重新排序
+ if (window.ChannelPreferences) {
+ ChannelPreferences.reorderModules(fromIndex, toIndex);
+ }
+
+ // 移动 DOM 元素
+ if (fromIndex < toIndex) {
+ targetCard.insertAdjacentElement('afterend', this.draggingElement);
+ } else {
+ targetCard.insertAdjacentElement('beforebegin', this.draggingElement);
+ }
+
+ // 触发事件
+ if (window.EventBus) {
+ EventBus.emit('favorites:reordered', {
+ from: fromIndex,
+ to: toIndex,
+ module: this.draggingElement.dataset.module
+ });
+ }
+ },
+
+ // 刷新拖拽功能(在布局变化后调用)
+ refreshDragAndDrop: function() {
+ this.initDragAndDrop();
+ }
+};
+
+console.log('[favorites] 已加载');
diff --git a/modules/m-channel/channel-layout.css b/modules/m-channel/channel-layout.css
new file mode 100644
index 00000000..31c39c3f
--- /dev/null
+++ b/modules/m-channel/channel-layout.css
@@ -0,0 +1,145 @@
+/* 频道布局样式 - 三种布局模式 */
+
+/* 基础卡片样式(所有布局共用) */
+.module-card {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ transition: all 0.3s ease;
+ overflow: hidden;
+ position: relative;
+}
+
+.module-card:hover {
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ transform: translateY(-2px);
+}
+
+/* 卡片头部 */
+.module-card-header {
+ padding: 16px;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.module-card-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* 收藏星星 */
+.favorite-star {
+ font-size: 20px;
+ color: #ccc;
+ cursor: pointer;
+ transition: color 0.2s;
+ user-select: none;
+}
+
+.favorite-star.active {
+ color: #fbbf24;
+}
+
+.favorite-star:hover {
+ transform: scale(1.1);
+}
+
+/* 卡片内容区 */
+.module-card-content {
+ padding: 16px;
+ min-height: 100px;
+}
+
+/* 拖拽把手 */
+.drag-handle {
+ font-size: 20px;
+ color: #999;
+ cursor: grab;
+ user-select: none;
+ margin-right: 8px;
+}
+
+.drag-handle:active {
+ cursor: grabbing;
+}
+
+/* ===== 布局1:网格模式(默认) ===== */
+.layout-grid .channel-content {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 24px;
+ padding: 24px;
+}
+
+.layout-grid .module-card {
+ height: fit-content;
+}
+
+/* ===== 布局2:列表模式(一行一个) ===== */
+.layout-list .channel-content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 24px;
+}
+
+.layout-list .module-card {
+ width: 100%;
+}
+
+.layout-list .module-card-header {
+ padding: 12px 16px;
+}
+
+.layout-list .module-card-content {
+ padding: 12px 16px;
+}
+
+/* ===== 布局3:紧凑模式(小卡片密排) ===== */
+.layout-compact .channel-content {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 12px;
+ padding: 16px;
+}
+
+.layout-compact .module-card {
+ font-size: 14px;
+}
+
+.layout-compact .module-card-header {
+ padding: 10px 12px;
+}
+
+.layout-compact .module-card-title {
+ font-size: 16px;
+}
+
+.layout-compact .module-card-content {
+ padding: 10px 12px;
+ min-height: 80px;
+}
+
+/* 布局切换动画 */
+.channel-content {
+ transition: all 0.3s ease-in-out;
+}
+
+/* 拖拽中的样式 */
+.module-card.dragging {
+ opacity: 0.6;
+ transform: scale(0.98);
+ box-shadow: 0 8px 16px rgba(0,0,0,0.2);
+}
+
+/* 拖拽放置区高亮 */
+.module-card.drop-target {
+ border: 2px dashed #3b82f6;
+ background: #f0f9ff;
+}
diff --git a/modules/m-channel/channel-notifications.css b/modules/m-channel/channel-notifications.css
new file mode 100644
index 00000000..5905e8df
--- /dev/null
+++ b/modules/m-channel/channel-notifications.css
@@ -0,0 +1,200 @@
+/* 频道通知系统样式 - 环节7 */
+:root {
+ --update-badge-bg: #2196f3;
+ --unread-dot-bg: #f44336;
+ --notification-panel-bg: #ffffff;
+ --notification-panel-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ --notification-hover-bg: #f5f5f5;
+}
+
+/* 更新标签(蓝色) */
+.update-badge {
+ display: inline-block;
+ background-color: var(--update-badge-bg);
+ color: white;
+ font-size: 12px;
+ font-weight: 500;
+ padding: 2px 8px;
+ border-radius: 12px;
+ margin-top: 8px;
+ align-self: flex-start;
+}
+
+/* 未读小红点(带数字) */
+.unread-dot {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ min-width: 18px;
+ height: 18px;
+ background-color: var(--unread-dot-bg);
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+ z-index: 10;
+}
+
+/* 铃铛图标容器 */
+.bell-container {
+ position: relative;
+ display: inline-block;
+ margin-right: 16px;
+ cursor: pointer;
+}
+
+.bell-icon {
+ font-size: 24px;
+ color: #555;
+ transition: color 0.2s;
+}
+
+.bell-icon:hover {
+ color: #000;
+}
+
+/* 铃铛上的数字气泡 */
+.bell-badge {
+ position: absolute;
+ top: -4px;
+ right: -6px;
+ min-width: 18px;
+ height: 18px;
+ background-color: var(--unread-dot-bg);
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 4px;
+ z-index: 20;
+}
+
+/* 侧滑通知面板 */
+.notification-panel {
+ position: fixed;
+ top: 0;
+ right: -400px;
+ width: 360px;
+ height: 100vh;
+ background: var(--notification-panel-bg);
+ box-shadow: var(--notification-panel-shadow);
+ transition: right 0.3s ease;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid #e0e0e0;
+}
+
+.notification-panel.open {
+ right: 0;
+}
+
+.panel-header {
+ padding: 20px;
+ border-bottom: 1px solid #e0e0e0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+}
+
+.panel-header button {
+ background: none;
+ border: none;
+ color: #2196f3;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.panel-header button:hover {
+ text-decoration: underline;
+}
+
+.notification-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.notification-item {
+ padding: 16px 20px;
+ border-bottom: 1px solid #f0f0f0;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.notification-item:hover {
+ background: var(--notification-hover-bg);
+}
+
+.notification-item.read {
+ opacity: 0.6;
+}
+
+.notification-time {
+ font-size: 12px;
+ color: #999;
+ margin-bottom: 4px;
+}
+
+.notification-module {
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.notification-summary {
+ font-size: 14px;
+ color: #333;
+}
+
+.empty-state {
+ padding: 40px 20px;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
+
+/* 遮罩层 */
+.panel-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.3);
+ z-index: 999;
+ display: none;
+}
+
+.panel-overlay.show {
+ display: block;
+}
+
+/* 让模块卡片成为相对定位容器(用于小红点定位) */
+.module-card {
+ position: relative !important;
+}
+
+/* 强制修复高亮点击问题 */
+.search-highlight,
+.highlight,
+[class*="highlight"] {
+ pointer-events: none !important;
+}
+
+/* 确保卡片本身可点击 */
+.module-card,
+[class*="module-card"] {
+ cursor: pointer;
+ pointer-events: auto !important;
+}
diff --git a/modules/m-channel/channel-notifications.js b/modules/m-channel/channel-notifications.js
new file mode 100644
index 00000000..a14c7818
--- /dev/null
+++ b/modules/m-channel/channel-notifications.js
@@ -0,0 +1,365 @@
+/**
+ * 频道通知系统 - 环节7 (晨星陪伴版)
+ * 功能:模块更新提醒、未读小红点、通知面板、高亮点击修复
+ * 修改:通过卡片文字内容匹配模块ID(解决moduleId undefined问题)
+ */
+
+(function() {
+ // ========== 初始化模拟数据 ==========
+ const STORAGE_KEY = 'channel_notifications';
+
+ // 默认模拟数据(基于已完成模块 M06, M08, M11, 以及当前频道模块)
+ const DEFAULT_UPDATES = {
+ 'M06': {
+ hasUpdate: true,
+ count: 2,
+ updates: [
+ { time: '10:30', summary: '修复了拖拽排序的bug' },
+ { time: '昨天', summary: '新增统计面板' }
+ ]
+ },
+ 'M08': {
+ hasUpdate: true,
+ count: 1,
+ updates: [
+ { time: '昨天', summary: '优化了模块加载性能' }
+ ]
+ },
+ 'M11': {
+ hasUpdate: true,
+ count: 3,
+ updates: [
+ { time: '15:20', summary: '新增键盘快捷键' },
+ { time: '昨天', summary: '修复焦点管理' },
+ { time: '3月10日', summary: '模块生命周期完善' }
+ ]
+ },
+ 'channel': {
+ hasUpdate: true,
+ count: 1,
+ updates: [
+ { time: '现在', summary: '通知系统上线啦!' }
+ ]
+ }
+ };
+
+ // 加载或初始化数据
+ let notificationData = JSON.parse(localStorage.getItem(STORAGE_KEY));
+ if (!notificationData) {
+ notificationData = DEFAULT_UPDATES;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
+ }
+
+ // ========== 工具函数:从卡片文字猜测模块ID ==========
+ function guessModuleIdFromCard(card) {
+ // 尝试找卡片里的标题文字
+ const titleElem = card.querySelector('h3, .module-title, .card-title, .module-name');
+ if (titleElem) {
+ const text = titleElem.textContent.trim();
+ // 简单映射:如果文字包含“工单” -> M06
+ if (text.includes('工单')) return 'M06';
+ if (text.includes('数据统计')) return 'M08';
+ if (text.includes('组件库')) return 'M11';
+ if (text.includes('调试面板')) return 'M11'; // 暂时用 M11
+ }
+
+ // 后备:用卡片内所有文字尝试匹配
+ const fullText = card.textContent;
+ if (fullText.includes('工单')) return 'M06';
+ if (fullText.includes('数据统计')) return 'M08';
+ if (fullText.includes('组件库')) return 'M11';
+
+ return null; // 实在猜不到就返回 null
+ }
+
+ function getModuleCards() {
+ return document.querySelectorAll('.module-card');
+ }
+
+ // ========== 更新标签渲染(带日志) ==========
+ function renderUpdateBadges() {
+ console.log('🟦 renderUpdateBadges 开始执行');
+ const cards = document.querySelectorAll('.module-card');
+ console.log('找到卡片数量:', cards.length);
+
+ cards.forEach(card => {
+ // 先尝试原有方法,失败则用猜测
+ let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
+ if (!moduleId) {
+ moduleId = guessModuleIdFromCard(card);
+ }
+ console.log('卡片最终 moduleId:', moduleId);
+
+ // 移除旧的标签
+ const oldBadge = card.querySelector('.update-badge');
+ if (oldBadge) oldBadge.remove();
+
+ const modData = moduleId ? notificationData[moduleId] : null;
+ console.log('模块数据:', moduleId, modData);
+
+ if (modData && modData.hasUpdate) {
+ console.log('✅ 应该添加标签的模块:', moduleId);
+ const badge = document.createElement('span');
+ badge.className = 'update-badge';
+ badge.textContent = '有更新';
+ // 内联样式保证可见
+ badge.style.backgroundColor = '#2196f3';
+ badge.style.color = 'white';
+ badge.style.padding = '2px 8px';
+ badge.style.borderRadius = '12px';
+ badge.style.fontSize = '12px';
+ badge.style.display = 'inline-block';
+ badge.style.marginTop = '8px';
+ // 直接追加到卡片末尾
+ card.appendChild(badge);
+ console.log('✅ 标签已追加到卡片', moduleId);
+ } else {
+ console.log('❌ 不需要添加标签的模块:', moduleId);
+ }
+ });
+ console.log('🟦 renderUpdateBadges 执行完毕');
+ }
+
+ // ========== 小红点渲染 ==========
+ function renderUnreadDots() {
+ const cards = getModuleCards();
+ cards.forEach(card => {
+ let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
+ if (!moduleId) {
+ moduleId = guessModuleIdFromCard(card);
+ }
+ if (!moduleId) return;
+
+ const oldDot = card.querySelector('.unread-dot');
+ if (oldDot) oldDot.remove();
+
+ const modData = notificationData[moduleId];
+ if (modData && modData.hasUpdate && modData.count > 0) {
+ const dot = document.createElement('span');
+ dot.className = 'unread-dot';
+ dot.textContent = modData.count > 9 ? '9+' : modData.count;
+ card.appendChild(dot);
+ }
+ });
+ }
+
+ // ========== 标记模块已读 ==========
+ function markModuleAsRead(moduleId) {
+ if (notificationData[moduleId]) {
+ notificationData[moduleId].hasUpdate = false;
+ notificationData[moduleId].count = 0;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
+ renderUpdateBadges();
+ renderUnreadDots();
+ updateBellBadge();
+ renderNotificationPanel();
+ }
+ }
+
+ // ========== 铃铛相关 ==========
+ let bellContainer, panel, overlay;
+ let isPanelOpen = false;
+
+ function createBell() {
+ const header = document.querySelector('.channel-header') || document.querySelector('header') || document.body;
+ bellContainer = document.createElement('div');
+ bellContainer.className = 'bell-container';
+ bellContainer.innerHTML = `
+ 🔔
+ 0
+ `;
+ const title = header.querySelector('h1, h2');
+ if (title) {
+ title.insertAdjacentElement('afterend', bellContainer);
+ } else {
+ header.appendChild(bellContainer);
+ }
+
+ panel = document.createElement('div');
+ panel.className = 'notification-panel';
+ panel.innerHTML = `
+
+
+ `;
+ overlay = document.createElement('div');
+ overlay.className = 'panel-overlay';
+ document.body.appendChild(panel);
+ document.body.appendChild(overlay);
+
+ bellContainer.addEventListener('click', togglePanel);
+ overlay.addEventListener('click', closePanel);
+ panel.querySelector('.mark-all-read').addEventListener('click', markAllRead);
+ }
+
+ function updateBellBadge() {
+ const totalUnread = Object.values(notificationData).reduce((acc, mod) => acc + (mod.count || 0), 0);
+ const badge = bellContainer?.querySelector('.bell-badge');
+ if (badge) {
+ if (totalUnread > 0) {
+ badge.style.display = 'flex';
+ badge.textContent = totalUnread > 9 ? '9+' : totalUnread;
+ } else {
+ badge.style.display = 'none';
+ }
+ }
+ }
+
+ function togglePanel(e) {
+ e.stopPropagation();
+ isPanelOpen ? closePanel() : openPanel();
+ }
+
+ function openPanel() {
+ isPanelOpen = true;
+ panel.classList.add('open');
+ overlay.classList.add('show');
+ renderNotificationPanel();
+ }
+
+ function closePanel() {
+ isPanelOpen = false;
+ panel.classList.remove('open');
+ overlay.classList.remove('show');
+ }
+
+ function renderNotificationPanel() {
+ const list = panel.querySelector('.notification-list');
+ if (!list) return;
+
+ let items = [];
+ for (const [moduleId, modData] of Object.entries(notificationData)) {
+ if (modData.updates && modData.updates.length > 0) {
+ modData.updates.forEach((update) => {
+ items.push({
+ moduleId,
+ time: update.time,
+ summary: update.summary,
+ read: !modData.hasUpdate
+ });
+ });
+ }
+ }
+
+ if (items.length === 0) {
+ list.innerHTML = '暂无新通知';
+ return;
+ }
+
+ items.sort((a, b) => (a.time > b.time ? -1 : 1));
+
+ list.innerHTML = items.map(item => `
+
+ ${item.time}
+ ${item.moduleId}
+ ${item.summary}
+
+ `).join('');
+
+ list.querySelectorAll('.notification-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ const moduleId = item.dataset.module;
+ const card = findCardByModuleId(moduleId);
+ if (card) {
+ card.click();
+ }
+ markModuleAsRead(moduleId);
+ closePanel();
+ });
+ });
+ }
+
+ function findCardByModuleId(moduleId) {
+ const cards = getModuleCards();
+ for (let card of cards) {
+ let id = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
+ if (!id) id = guessModuleIdFromCard(card);
+ if (id === moduleId) return card;
+ }
+ return null;
+ }
+
+ function markAllRead() {
+ for (let moduleId in notificationData) {
+ notificationData[moduleId].hasUpdate = false;
+ notificationData[moduleId].count = 0;
+ }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(notificationData));
+ renderUpdateBadges();
+ renderUnreadDots();
+ updateBellBadge();
+ renderNotificationPanel();
+ }
+
+ function fixHighlightClick() {
+ const container = document.querySelector('.modules-grid') || document.body;
+ container.addEventListener('click', (e) => {
+ const card = e.target.closest('.module-card');
+ if (card) {
+ // 不做额外处理,只是确保事件冒泡
+ }
+ }, true);
+
+ const style = document.createElement('style');
+ style.textContent = `
+ .search-highlight {
+ pointer-events: none !important;
+ }
+ .module-card {
+ cursor: pointer;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ function init() {
+ if (!document.querySelector('link[href*="channel-notifications.css"]')) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = 'channel-notifications.css';
+ document.head.appendChild(link);
+ }
+
+ renderUpdateBadges();
+ renderUnreadDots();
+
+ createBell();
+ updateBellBadge();
+
+ fixHighlightClick();
+
+ document.addEventListener('click', (e) => {
+ const card = e.target.closest('.module-card');
+ if (card) {
+ let moduleId = card.dataset.moduleId || card.id || card.querySelector('.module-name')?.textContent.trim();
+ if (!moduleId) moduleId = guessModuleIdFromCard(card);
+ if (moduleId) {
+ setTimeout(() => markModuleAsRead(moduleId), 100);
+ }
+ }
+ });
+
+ window.addEventListener('modulesRendered', () => {
+ renderUpdateBadges();
+ renderUnreadDots();
+ });
+
+ let lastCardCount = 0;
+ setInterval(() => {
+ const cards = getModuleCards();
+ if (cards.length !== lastCardCount) {
+ lastCardCount = cards.length;
+ renderUpdateBadges();
+ renderUnreadDots();
+ }
+ }, 2000);
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/modules/m-channel/channel-preferences.js b/modules/m-channel/channel-preferences.js
new file mode 100644
index 00000000..a4ee89c9
--- /dev/null
+++ b/modules/m-channel/channel-preferences.js
@@ -0,0 +1,194 @@
+// 频道偏好管理 - 记住用户的装修设置
+window.ChannelPreferences = {
+ STORAGE_KEY: 'hololake_channel_preferences',
+
+ // 默认配置
+ defaults: {
+ layout: 'grid', // grid, list, compact
+ theme: 'default', // default, ocean, forest, sunset, lavender
+ favorites: [], // 收藏的模块ID列表
+ moduleOrder: [], // 模块排序顺序(默认按ID)
+ stats: {} // 使用统计
+ },
+
+ // 当前配置
+ config: null,
+
+ // 初始化(加载配置)
+ init: function() {
+ console.log('[preferences] 初始化');
+ this.load();
+ return this.config;
+ },
+
+ // 加载配置
+ load: function() {
+ const saved = localStorage.getItem(this.STORAGE_KEY);
+ if (saved) {
+ try {
+ this.config = JSON.parse(saved);
+ console.log('[preferences] 加载配置:', this.config);
+ } catch (e) {
+ console.error('[preferences] 解析失败,使用默认配置', e);
+ this.config = { ...this.defaults };
+ }
+ } else {
+ console.log('[preferences] 无保存配置,使用默认');
+ this.config = { ...this.defaults };
+ }
+ return this.config;
+ },
+
+ // 保存配置
+ save: function() {
+ if (!this.config) return;
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.config));
+ console.log('[preferences] 保存配置:', this.config);
+ },
+
+ // 获取指定键的值
+ get: function(key) {
+ if (!this.config) this.load();
+ return this.config[key];
+ },
+
+ // 设置指定键的值(自动保存)
+ set: function(key, value) {
+ if (!this.config) this.load();
+ this.config[key] = value;
+ this.save();
+
+ // 触发事件总线通知
+ if (window.EventBus) {
+ EventBus.emit('preferences:changed', { key, value });
+ }
+ },
+
+ // 更新整个配置(合并)
+ update: function(newConfig) {
+ if (!this.config) this.load();
+ this.config = { ...this.config, ...newConfig };
+ this.save();
+ if (window.EventBus) {
+ EventBus.emit('preferences:changed', { full: this.config });
+ }
+ },
+
+ // 恢复默认设置
+ reset: function() {
+ this.config = { ...this.defaults };
+ this.save();
+ if (window.EventBus) {
+ EventBus.emit('preferences:reset', this.config);
+ }
+ console.log('[preferences] 恢复默认');
+ },
+
+ // 记录模块使用(用于统计)
+ recordUsage: function(moduleId) {
+ if (!this.config) this.load();
+ if (!this.config.stats) this.config.stats = {};
+ if (!this.config.stats[moduleId]) {
+ this.config.stats[moduleId] = {
+ count: 0,
+ lastUsed: null,
+ totalTime: 0
+ };
+ }
+ this.config.stats[moduleId].count++;
+ this.config.stats[moduleId].lastUsed = Date.now();
+ this.save();
+
+ // 开始计时(将在模块卸载时记录时长)
+ this.startTiming(moduleId);
+ },
+
+ // 计时相关
+ timing: {},
+ startTiming: function(moduleId) {
+ this.timing[moduleId] = Date.now();
+ },
+ stopTiming: function(moduleId) {
+ if (this.timing[moduleId]) {
+ const duration = (Date.now() - this.timing[moduleId]) / 1000; // 秒
+ if (this.config.stats[moduleId]) {
+ this.config.stats[moduleId].totalTime += duration;
+ this.save();
+ }
+ delete this.timing[moduleId];
+ }
+ },
+
+ // 获取统计报告
+ getStats: function() {
+ if (!this.config) this.load();
+ return this.config.stats || {};
+ },
+
+ // 获取收藏列表
+ getFavorites: function() {
+ return this.get('favorites') || [];
+ },
+
+ // 切换收藏状态
+ toggleFavorite: function(moduleId) {
+ let favorites = this.get('favorites') || [];
+ if (favorites.includes(moduleId)) {
+ favorites = favorites.filter(id => id !== moduleId);
+ } else {
+ favorites.push(moduleId);
+ }
+ this.set('favorites', favorites);
+ return favorites.includes(moduleId);
+ },
+
+ // 获取布局
+ getLayout: function() {
+ return this.get('layout') || 'grid';
+ },
+
+ // 设置布局
+ setLayout: function(layout) {
+ this.set('layout', layout);
+ },
+
+ // 获取主题
+ getTheme: function() {
+ return this.get('theme') || 'default';
+ },
+
+ // 设置主题
+ setTheme: function(theme) {
+ this.set('theme', theme);
+ },
+
+ // 获取模块排序
+ getModuleOrder: function() {
+ return this.get('moduleOrder') || [];
+ },
+
+ // 设置模块排序
+ setModuleOrder: function(order) {
+ this.set('moduleOrder', order);
+ },
+
+ // 重新排序(拖拽后调用)
+ reorderModules: function(fromIndex, toIndex) {
+ const order = this.getModuleOrder();
+ if (order.length === 0) {
+ // 如果还没有自定义顺序,则基于当前模块列表生成
+ const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
+ // 排除非显示模块(如调试面板)
+ const displayModules = modules.filter(m => ['m06','m08','m11','home','debug'].includes(m));
+ this.setModuleOrder(displayModules);
+ }
+ // 重新加载最新顺序
+ const currentOrder = this.getModuleOrder();
+ if (fromIndex < 0 || fromIndex >= currentOrder.length || toIndex < 0 || toIndex >= currentOrder.length) return;
+ const [moved] = currentOrder.splice(fromIndex, 1);
+ currentOrder.splice(toIndex, 0, moved);
+ this.setModuleOrder(currentOrder);
+ }
+};
+
+console.log('[preferences] 已加载');
diff --git a/modules/m-channel/channel-router.js b/modules/m-channel/channel-router.js
index 614fc3ed..3a51bc8c 100644
--- a/modules/m-channel/channel-router.js
+++ b/modules/m-channel/channel-router.js
@@ -1,98 +1,222 @@
-// ================== 路由配置 ==================
-const routes = {
- 'home': 'views/home.html',
- 'channel': 'views/channel.html',
- 'about': 'views/about.html'
-};
+// 频道路由(支持真实模块适配器 + 卡片列表首页 + 数据采集钩子)
+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');
+ }
+ });
+ });
+ }
-// 获取当前 hash 中的路径(去掉 #/)
-function getHashPath() {
- const hash = window.location.hash.slice(1) || '/';
- const path = hash.startsWith('/') ? hash.slice(1) : hash;
- return path || 'home';
-}
-
-// ================== 加载视图 ==================
-async function loadView(path) {
- const routerView = document.getElementById('router-view');
- if (!routerView) return;
-
- // 显示加载动画
- routerView.innerHTML = '';
-
- try {
- const viewFile = routes[path];
- if (!viewFile) {
- await load404(routerView);
+ // 页面关闭时结束计时
+ window.addEventListener('beforeunload', function() {
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.endSession();
+ }
+ });
+ },
+
+ 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;
+
+ // 数据采集钩子:开始加载
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.markLoadStart();
+ if (channel !== 'home' && channel !== 'debug' && channel !== 'dashboard') {
+ ChannelAnalytics.recordVisit(channel);
+ }
+ }
+
+ // 如果是真实模块(m06/m08/m11)
+ if (channel === 'm06' || channel === 'm08' || channel === 'm11') {
+ if (window.ModuleAdapter) {
+ ModuleAdapter.loadModule(channel, 'channel-content');
+ // 模块加载完成后记录加载时间
+ setTimeout(() => {
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.markLoadEnd(channel);
+ }
+ }, 100);
+ } else {
+ this.contentEl.innerHTML = '适配器未加载
';
+ }
+ ModuleLifecycle.onLoad(channel);
return;
}
-
- const response = await fetch(viewFile);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
+
+ // 数据面板路由(环节8新增)
+ if (channel === 'dashboard') {
+ fetch('views/channel-dashboard.html')
+ .then(response => response.text())
+ .then(html => {
+ this.contentEl.innerHTML = html;
+ // 渲染图表
+ if (typeof ChannelDashboard !== 'undefined') {
+ ChannelDashboard.render();
+ }
+ ModuleLifecycle.onLoad('dashboard');
+ // 记录加载完成
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.markLoadEnd('dashboard');
+ }
+ })
+ .catch(err => {
+ this.contentEl.innerHTML = '数据面板加载失败
';
+ console.error(err);
+ });
+ return;
}
- const html = await response.text();
- routerView.innerHTML = html;
- } catch (error) {
- console.error('加载视图失败:', error);
- routerView.innerHTML = `
-
- ❌ 加载失败:${error.message}
- 请检查文件是否存在,或刷新重试
-
- `;
- }
-
- // 更新导航高亮和状态栏
- updateActiveNav(path);
- updateStatusBar(path);
-}
-
-// 加载 404 页面
-async function load404(container) {
- try {
- const resp = await fetch('views/404.html');
- if (resp.ok) {
- container.innerHTML = await resp.text();
+
+ // 内置频道
+ if (channel === 'home') {
+ this.renderModuleCards();
+ } else if (channel === 'debug') {
+ this.loadDebugPanel();
} else {
- container.innerHTML = '⚠️ 404 - 页面未找到
';
+ this.contentEl.innerHTML = '未知频道
';
}
- } catch {
- container.innerHTML = '⚠️ 404 - 页面未找到
';
- }
-}
-// 更新导航高亮
-function updateActiveNav(path) {
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- const linkPath = link.getAttribute('href').slice(2);
- if (linkPath === path) {
- link.classList.add('active');
+ // 对于内置频道也记录加载完成
+ if (typeof ChannelAnalytics !== 'undefined' && channel !== 'dashboard') {
+ ChannelAnalytics.markLoadEnd(channel);
}
- });
-}
-
-// 更新状态栏
-function updateStatusBar(path) {
- const statusEl = document.getElementById('current-route');
- if (statusEl) {
- statusEl.textContent = `当前路由:/${path}`;
+ },
+
+ // 渲染所有模块卡片(用于首页)
+ renderModuleCards: function() {
+ const modules = [
+ { id: 'm06', name: '工单管理', icon: '📋', desc: '管理任务和工单' },
+ { id: 'm08', name: '数据统计', icon: '📊', desc: '查看数据统计和报表' },
+ { id: 'm11', name: '组件库', icon: '🧩', desc: '系统组件展示' },
+ { id: 'debug', name: '调试面板', icon: '🐞', desc: '查看事件和模块状态' },
+ { id: 'dashboard', name: '数据面板', icon: '📈', desc: '查看使用统计和性能' }
+ ];
+
+ let html = '';
+ modules.forEach(mod => {
+ const isFavorite = window.ChannelPreferences ?
+ ChannelPreferences.getFavorites().includes(mod.id) : false;
+ html += `
+
+ `;
+ });
+ html += '
';
+
+ this.contentEl.innerHTML = html;
+ ModuleLifecycle.onLoad('home');
+
+ // 触发收藏/拖拽重新初始化
+ if (window.ChannelFavorites) {
+ setTimeout(() => {
+ ChannelFavorites.init();
+ ChannelFavorites.initDragAndDrop();
+ }, 100);
+ }
+ },
+
+ loadDebugPanel: function() {
+ fetch('views/channel-debug.html')
+ .then(response => response.text())
+ .then(html => {
+ this.contentEl.innerHTML = html;
+ ModuleLifecycle.onLoad('debug');
+ this.initDebugPanel();
+ })
+ .catch(err => {
+ this.contentEl.innerHTML = '调试面板加载失败
';
+ });
+ },
+
+ 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('状态已清除,刷新页面生效');
+ });
+ }
+ },
+
+ getVisitedChannels: function() {
+ const visited = [];
+ document.querySelectorAll('.channel-btn.visited').forEach(btn => {
+ visited.push(btn.dataset.channel);
+ });
+ return visited;
}
-}
-
-// 监听 hash 变化
-window.addEventListener('hashchange', () => {
- const path = getHashPath();
- loadView(path);
-});
-
-// 首次加载
-window.addEventListener('DOMContentLoaded', () => {
- if (!window.location.hash) {
- window.location.hash = '#/home';
- } else {
- const path = getHashPath();
- loadView(path);
- }
-});
+};
diff --git a/modules/m-channel/channel-router.js.bak b/modules/m-channel/channel-router.js.bak
new file mode 100644
index 00000000..957038fb
--- /dev/null
+++ b/modules/m-channel/channel-router.js.bak
@@ -0,0 +1,190 @@
+// 频道路由(支持真实模块适配器 + 卡片列表首页)
+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');
+ }
+ });
+ });
+ }
+
+ // 【新增】页面关闭时结束计时
+ window.addEventListener('beforeunload', function() {
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.endSession();
+ }
+ });
+ },
+
+ 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;
+
+ // 如果是真实模块(m06/m08/m11)
+ if (channel === 'm06' || channel === 'm08' || channel === 'm11') {
+ // 【新增】数据采集钩子:标记加载开始并记录访问
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.markLoadStart();
+ ChannelAnalytics.recordVisit(channel);
+ }
+
+ if (window.ModuleAdapter) {
+ ModuleAdapter.loadModule(channel, 'channel-content');
+ } else {
+ this.contentEl.innerHTML = '适配器未加载
';
+ }
+ ModuleLifecycle.onLoad(channel);
+
+ // 【新增】数据采集钩子:标记加载完成
+ if (typeof ChannelAnalytics !== 'undefined') {
+ ChannelAnalytics.markLoadEnd(channel);
+ }
+ return;
+ }
+
+ // 内置频道
+ if (channel === 'home') {
+ this.renderModuleCards(); // 显示模块卡片列表
+ } else if (channel === 'debug') {
+ this.loadDebugPanel();
+ } else {
+ this.contentEl.innerHTML = '未知频道
';
+ }
+ },
+
+ // 渲染所有模块卡片(用于首页)
+ renderModuleCards: function() {
+ const modules = [
+ { id: 'm06', name: '工单管理', icon: '📋', desc: '管理任务和工单' },
+ { id: 'm08', name: '数据统计', icon: '📊', desc: '查看数据统计和报表' },
+ { id: 'm11', name: '组件库', icon: '🧩', desc: '系统组件展示' },
+ { id: 'debug', name: '调试面板', icon: '🐞', desc: '查看事件和模块状态' }
+ ];
+
+ let html = '';
+ modules.forEach(mod => {
+ const isFavorite = window.ChannelPreferences ?
+ ChannelPreferences.getFavorites().includes(mod.id) : false;
+ html += `
+
+ `;
+ });
+ html += '
';
+
+ this.contentEl.innerHTML = html;
+ ModuleLifecycle.onLoad('home');
+
+ // 触发收藏/拖拽重新初始化
+ if (window.ChannelFavorites) {
+ setTimeout(() => {
+ ChannelFavorites.init();
+ ChannelFavorites.initDragAndDrop();
+ }, 100);
+ }
+ },
+
+ loadDebugPanel: function() {
+ fetch('views/channel-debug.html')
+ .then(response => response.text())
+ .then(html => {
+ this.contentEl.innerHTML = html;
+ ModuleLifecycle.onLoad('debug');
+ this.initDebugPanel();
+ })
+ .catch(err => {
+ this.contentEl.innerHTML = '调试面板加载失败
';
+ });
+ },
+
+ 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('状态已清除,刷新页面生效');
+ });
+ }
+ },
+
+ getVisitedChannels: function() {
+ const visited = [];
+ document.querySelectorAll('.channel-btn.visited').forEach(btn => {
+ visited.push(btn.dataset.channel);
+ });
+ return visited;
+ }
+};
diff --git a/modules/m-channel/channel-router.js.bakcat b/modules/m-channel/channel-router.js.bakcat
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/m-channel/channel-state.js b/modules/m-channel/channel-state.js
new file mode 100644
index 00000000..38edffd6
--- /dev/null
+++ b/modules/m-channel/channel-state.js
@@ -0,0 +1,44 @@
+// 状态管理(localStorage)
+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] 已清除');
+ }
+};
diff --git a/modules/m-channel/channel-stats.js b/modules/m-channel/channel-stats.js
new file mode 100644
index 00000000..d4263fe1
--- /dev/null
+++ b/modules/m-channel/channel-stats.js
@@ -0,0 +1,298 @@
+// 频道使用统计管理
+window.ChannelStats = {
+ // 初始化
+ init: function() {
+ console.log('[stats] 初始化');
+ this.bindEvents();
+ },
+
+ // 绑定事件
+ bindEvents: function() {
+ if (!window.EventBus) return;
+
+ // 监听模块加载,开始计时
+ EventBus.on('module:loaded', (data) => {
+ if (data && data.module) {
+ this.recordView(data.module);
+ }
+ });
+
+ // 监听模块卸载,结束计时
+ EventBus.on('module:unloaded', (data) => {
+ if (data && data.module) {
+ if (window.ChannelPreferences) {
+ ChannelPreferences.stopTiming(data.module);
+ }
+ }
+ });
+
+ // 监听页面关闭,停止所有计时
+ window.addEventListener('beforeunload', () => {
+ if (window.ChannelPreferences && window.ChannelPreferences.timing) {
+ Object.keys(ChannelPreferences.timing).forEach(moduleId => {
+ ChannelPreferences.stopTiming(moduleId);
+ });
+ }
+ });
+ },
+
+ // 记录模块访问
+ recordView: function(moduleId) {
+ if (!window.ChannelPreferences) return;
+
+ // 记录使用次数和开始计时
+ ChannelPreferences.recordUsage(moduleId);
+
+ // 发送事件
+ if (window.EventBus) {
+ EventBus.emit('stats:recorded', {
+ module: moduleId,
+ time: Date.now()
+ });
+ }
+ },
+
+ // 获取统计报告
+ getStatsReport: function() {
+ if (!window.ChannelPreferences) return {};
+
+ const stats = ChannelPreferences.getStats();
+ const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
+
+ // 格式化统计数据
+ const report = {
+ totalViews: 0,
+ mostUsed: null,
+ lastUsed: null,
+ moduleDetails: {}
+ };
+
+ let maxCount = 0;
+ let lastTime = 0;
+
+ modules.forEach(moduleId => {
+ const moduleStats = stats[moduleId] || { count: 0, lastUsed: null, totalTime: 0 };
+ report.moduleDetails[moduleId] = {
+ count: moduleStats.count,
+ lastUsed: moduleStats.lastUsed ? new Date(moduleStats.lastUsed).toLocaleString() : '从未使用',
+ totalTime: moduleStats.totalTime ? this.formatTime(moduleStats.totalTime) : '0秒'
+ };
+
+ report.totalViews += moduleStats.count;
+
+ if (moduleStats.count > maxCount) {
+ maxCount = moduleStats.count;
+ report.mostUsed = moduleId;
+ }
+
+ if (moduleStats.lastUsed && moduleStats.lastUsed > lastTime) {
+ lastTime = moduleStats.lastUsed;
+ report.lastUsed = moduleId;
+ }
+ });
+
+ return report;
+ },
+
+ // 格式化时间(秒 -> 可读格式)
+ formatTime: function(seconds) {
+ if (seconds < 60) return `${Math.round(seconds)}秒`;
+ if (seconds < 3600) {
+ const minutes = Math.floor(seconds / 60);
+ const secs = Math.round(seconds % 60);
+ return `${minutes}分${secs}秒`;
+ }
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ return `${hours}小时${minutes}分`;
+ },
+
+ // 渲染统计面板
+ renderStatsPanel: function(container) {
+ if (!container) return;
+
+ const report = this.getStatsReport();
+ const modules = window.ModuleRegistry ? ModuleRegistry.list() : [];
+
+ let html = `
+
+
📊 使用统计
+
+
+ 总访问次数:
+ ${report.totalViews}
+
+ ${report.mostUsed ? `
+
+ 最常用模块:
+ ${this.getModuleName(report.mostUsed)} (${report.moduleDetails[report.mostUsed]?.count || 0}次)
+
+ ` : ''}
+ ${report.lastUsed ? `
+
+ 最近使用:
+ ${this.getModuleName(report.lastUsed)}
+
+ ` : ''}
+
+
+
模块详情
+
+
+
+ | 模块 |
+ 访问次数 |
+ 累计使用时长 |
+ 最后使用 |
+
+
+
+ `;
+
+ modules.forEach(moduleId => {
+ const detail = report.moduleDetails[moduleId] || { count: 0, totalTime: '0秒', lastUsed: '从未使用' };
+ html += `
+
+ | ${this.getModuleName(moduleId)} |
+ ${detail.count} |
+ ${detail.totalTime} |
+ ${detail.lastUsed} |
+
+ `;
+ });
+
+ html += `
+
+
+
+
+
+
+ `;
+
+ container.innerHTML = html;
+
+ // 绑定重置按钮
+ const resetBtn = document.getElementById('reset-stats-btn');
+ if (resetBtn) {
+ resetBtn.addEventListener('click', () => {
+ if (confirm('确定要重置所有统计数据吗?')) {
+ this.resetStats();
+ }
+ });
+ }
+ },
+
+ // 获取模块显示名称
+ getModuleName: function(moduleId) {
+ const names = {
+ 'm06': '工单管理',
+ 'm08': '数据统计',
+ 'm11': '组件库',
+ 'home': '首页',
+ 'debug': '调试面板'
+ };
+ return names[moduleId] || moduleId;
+ },
+
+ // 重置统计
+ resetStats: function() {
+ if (!window.ChannelPreferences) return;
+
+ const config = ChannelPreferences.config;
+ if (config) {
+ config.stats = {};
+ ChannelPreferences.save();
+
+ if (window.EventBus) {
+ EventBus.emit('stats:reset');
+ }
+
+ alert('统计数据已重置');
+
+ // 重新渲染统计面板
+ const container = document.querySelector('.stats-panel-container');
+ if (container) {
+ this.renderStatsPanel(container);
+ }
+ }
+ },
+
+ // 注入样式
+ injectStyles: function() {
+ const style = document.createElement('style');
+ style.textContent = `
+ .stats-panel {
+ padding: 20px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ }
+ .stats-summary {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin: 20px 0;
+ padding: 16px;
+ background: #f8fafc;
+ border-radius: 8px;
+ }
+ .stat-item {
+ font-size: 14px;
+ }
+ .stat-label {
+ color: #64748b;
+ }
+ .stat-value {
+ font-weight: 600;
+ color: #0f172a;
+ margin-left: 8px;
+ }
+ .stats-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 20px 0;
+ }
+ .stats-table th,
+ .stats-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #e2e8f0;
+ }
+ .stats-table th {
+ background: #f1f5f9;
+ font-weight: 600;
+ color: #334155;
+ }
+ .stats-table tr:hover {
+ background: #f8fafc;
+ }
+ .stats-actions {
+ text-align: right;
+ margin-top: 20px;
+ }
+ .stats-reset-btn {
+ padding: 8px 16px;
+ background: #ef4444;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ }
+ .stats-reset-btn:hover {
+ background: #dc2626;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+};
+
+// 自动注入样式
+setTimeout(() => {
+ if (window.ChannelStats) {
+ ChannelStats.injectStyles();
+ }
+}, 100);
+
+console.log('[stats] 已加载');
diff --git a/modules/m-channel/channel-style.css b/modules/m-channel/channel-style.css
index 24da6655..b03191ef 100644
--- a/modules/m-channel/channel-style.css
+++ b/modules/m-channel/channel-style.css
@@ -1,150 +1,62 @@
-/* 基础重置 */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
- background: #f5f7fb;
- color: #1e293b;
- line-height: 1.5;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-/* 导航栏 */
-.channel-nav {
- background: white;
- padding: 1rem 2rem;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- display: flex;
- gap: 2rem;
- border-bottom: 1px solid #e2e8f0;
-}
-
-.nav-link {
- text-decoration: none;
- color: #64748b;
- font-weight: 500;
- padding: 0.5rem 0;
- border-bottom: 2px solid transparent;
- transition: all 0.2s;
-}
-
-.nav-link:hover {
- color: #0284c7;
-}
-
-.nav-link.active {
- color: #0284c7;
- border-bottom-color: #0284c7;
-}
-
-/* 路由视图容器 */
-.router-view {
- flex: 1;
- padding: 2rem;
+/* 频道基础样式 */
+.channel-container {
max-width: 1200px;
margin: 0 auto;
- width: 100%;
- background: white;
- border-radius: 12px 12px 0 0;
- box-shadow: 0 -4px 12px rgba(0,0,0,0.02);
+ padding: 20px;
+ font-family: system-ui, -apple-system, sans-serif;
}
-/* 状态栏 */
-.status-bar {
- background: #1e293b;
- color: #94a3b8;
- padding: 0.75rem 2rem;
- font-size: 0.9rem;
- text-align: center;
- border-top: 1px solid #334155;
+.channel-nav {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ border-bottom: 2px solid #e5e7eb;
+ padding-bottom: 10px;
}
-/* 卡片网格 */
-.card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 1.5rem;
- margin-top: 1.5rem;
-}
-
-.module-card {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- padding: 1.5rem 1rem;
- text-align: center;
- cursor: pointer;
- transition: all 0.2s;
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
-}
-
-.module-card:hover {
- transform: translateY(-4px);
- border-color: #0284c7;
- box-shadow: 0 12px 20px -8px rgba(2, 132, 199, 0.2);
-}
-
-.module-card h3 {
- margin-bottom: 0.5rem;
- color: #0f172a;
-}
-
-.module-card p {
- font-size: 0.9rem;
- color: #64748b;
-}
-
-/* 加载动画 */
-.loader {
- display: inline-block;
- width: 24px;
- height: 24px;
- border: 3px solid #e2e8f0;
- border-top-color: #0284c7;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* 错误提示 */
-.error-message {
- background: #fee2e2;
- border: 1px solid #ef4444;
- color: #b91c1c;
- padding: 1rem;
+.channel-btn {
+ padding: 10px 20px;
+ border: none;
+ background: #f3f4f6;
border-radius: 8px;
- margin: 1rem 0;
-}
-
-/* 模块内容区 */
-.module-content {
- margin-top: 2rem;
- border-top: 1px dashed #cbd5e1;
- padding-top: 2rem;
-}
-
-.back-button {
- background: none;
- border: 1px solid #cbd5e1;
- padding: 0.5rem 1.5rem;
- border-radius: 30px;
cursor: pointer;
- font-size: 0.95rem;
- color: #475569;
+ font-size: 16px;
transition: all 0.2s;
- margin-bottom: 1rem;
}
-.back-button:hover {
- background: #f1f5f9;
- border-color: #94a3b8;
+.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);
}
diff --git a/modules/m-channel/channel-theme.js b/modules/m-channel/channel-theme.js
new file mode 100644
index 00000000..24ee8950
--- /dev/null
+++ b/modules/m-channel/channel-theme.js
@@ -0,0 +1,157 @@
+// 频道主题管理 - 换主题色
+window.ChannelTheme = {
+ // 预设主题
+ themes: {
+ default: {
+ name: '默认蓝',
+ colors: {
+ primary: '#3b82f6',
+ primaryDark: '#2563eb',
+ secondary: '#10b981',
+ background: '#ffffff',
+ surface: '#f9fafb',
+ text: '#1f2937',
+ textLight: '#6b7280',
+ border: '#e5e7eb'
+ }
+ },
+ ocean: {
+ name: '海洋',
+ colors: {
+ primary: '#0891b2',
+ primaryDark: '#0e7490',
+ secondary: '#2dd4bf',
+ background: '#ecfeff',
+ surface: '#ffffff',
+ text: '#164e63',
+ textLight: '#155e75',
+ border: '#a5f3fc'
+ }
+ },
+ forest: {
+ name: '森林',
+ colors: {
+ primary: '#059669',
+ primaryDark: '#047857',
+ secondary: '#fbbf24',
+ background: '#f0fdf4',
+ surface: '#ffffff',
+ text: '#064e3b',
+ textLight: '#065f46',
+ border: '#a7f3d0'
+ }
+ },
+ sunset: {
+ name: '日落',
+ colors: {
+ primary: '#d97706',
+ primaryDark: '#b45309',
+ secondary: '#f43f5e',
+ background: '#fff7ed',
+ surface: '#ffffff',
+ text: '#7c2d12',
+ textLight: '#9a3412',
+ border: '#fed7aa'
+ }
+ },
+ lavender: {
+ name: '薰衣草',
+ colors: {
+ primary: '#8b5cf6',
+ primaryDark: '#7c3aed',
+ secondary: '#ec4899',
+ background: '#f5f3ff',
+ surface: '#ffffff',
+ text: '#4c1d95',
+ textLight: '#5b21b6',
+ border: '#ddd6fe'
+ }
+ }
+ },
+
+ // 当前主题ID
+ currentTheme: 'default',
+
+ // 初始化主题
+ init: function() {
+ console.log('[theme] 初始化');
+
+ // 从偏好设置加载主题
+ if (window.ChannelPreferences) {
+ const savedTheme = ChannelPreferences.getTheme();
+ if (savedTheme && this.themes[savedTheme]) {
+ this.currentTheme = savedTheme;
+ }
+ }
+
+ // 应用主题
+ this.applyTheme(this.currentTheme);
+
+ // 监听主题变化事件
+ if (window.EventBus) {
+ EventBus.on('preferences:changed', (data) => {
+ if (data.key === 'theme') {
+ this.applyTheme(data.value);
+ }
+ });
+ }
+ },
+
+ // 应用主题
+ applyTheme: function(themeId) {
+ if (!this.themes[themeId]) {
+ console.error(`[theme] 未知主题: ${themeId}`);
+ return;
+ }
+
+ this.currentTheme = themeId;
+ const colors = this.themes[themeId].colors;
+
+ // 设置 CSS 自定义属性
+ const root = document.documentElement;
+ for (const [key, value] of Object.entries(colors)) {
+ root.style.setProperty(`--theme-${key}`, value);
+ }
+
+ console.log(`[theme] 应用主题: ${themeId}`);
+
+ // 触发事件
+ if (window.EventBus) {
+ EventBus.emit('theme:changed', { theme: themeId, colors });
+ }
+ },
+
+ // 切换主题
+ setTheme: function(themeId) {
+ if (!this.themes[themeId]) return;
+
+ this.applyTheme(themeId);
+
+ // 保存到偏好设置
+ if (window.ChannelPreferences) {
+ ChannelPreferences.setTheme(themeId);
+ }
+ },
+
+ // 获取所有主题列表
+ getThemes: function() {
+ return Object.entries(this.themes).map(([id, theme]) => ({
+ id,
+ name: theme.name
+ }));
+ },
+
+ // 获取当前主题名称
+ getCurrentThemeName: function() {
+ return this.themes[this.currentTheme]?.name || '默认蓝';
+ }
+};
+
+// 自动初始化
+setTimeout(() => {
+ if (window.ChannelTheme) {
+ ChannelTheme.init();
+ }
+}, 200);
+
+console.log('[theme] 已加载');
diff --git a/modules/m-channel/channel-transition.css b/modules/m-channel/channel-transition.css
new file mode 100644
index 00000000..d705abd0
--- /dev/null
+++ b/modules/m-channel/channel-transition.css
@@ -0,0 +1,48 @@
+/* 过渡动画 */
+.channel-content {
+ transition: opacity 0.3s ease-in-out;
+}
+
+.channel-content.fade-out {
+ opacity: 0;
+}
+
+.channel-content.fade-in {
+ opacity: 1;
+}
+
+/* 滑动动画 */
+.slide-left-enter {
+ transform: translateX(100%);
+}
+
+.slide-left-enter-active {
+ transform: translateX(0);
+ transition: transform 0.3s ease-in-out;
+}
+
+.slide-right-enter {
+ transform: translateX(-100%);
+}
+
+.slide-right-enter-active {
+ transform: translateX(0);
+ transition: transform 0.3s ease-in-out;
+}
+
+.slide-exit {
+ position: absolute;
+ width: 100%;
+}
+
+.slide-exit-active {
+ transition: transform 0.3s ease-in-out;
+}
+
+.slide-exit-left {
+ transform: translateX(-100%);
+}
+
+.slide-exit-right {
+ transform: translateX(100%);
+}
diff --git a/modules/m-channel/channel-ultimate.html b/modules/m-channel/channel-ultimate.html
new file mode 100644
index 00000000..346968f7
--- /dev/null
+++ b/modules/m-channel/channel-ultimate.html
@@ -0,0 +1,1401 @@
+
+
+
+
+
+ 光湖频道 · 终极版
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
主题色
+
+
+
+
+
+
+
+
+
+
统计数据
+
+
+
+
其他
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/m-channel/error-boundary.js b/modules/m-channel/error-boundary.js
new file mode 100644
index 00000000..9ea7037d
--- /dev/null
+++ b/modules/m-channel/error-boundary.js
@@ -0,0 +1,134 @@
+// 错误边界 - 保险丝
+window.ErrorBoundary = {
+ // 包裹一个可能出错的渲染函数
+ wrap: function(renderFn, fallbackComponent, moduleId) {
+ return function(container) {
+ try {
+ return renderFn(container);
+ } catch (error) {
+ console.error(`[error-boundary] 模块 ${moduleId} 渲染失败:`, error);
+ return this.showFallback(container, moduleId, error);
+ }
+ };
+ },
+
+ // 显示降级界面
+ showFallback: function(container, moduleId, error) {
+ if (!container) return;
+
+ container.innerHTML = `
+
+
⚠️
+
+
模块 ${moduleId} 加载失败
+
${error.message || '未知错误'}
+
+
+
+
+
+
+ `;
+
+ // 记录错误到事件总线
+ if (window.EventBus) {
+ EventBus.emit('module:error', {
+ module: moduleId,
+ error: error.message,
+ time: Date.now()
+ });
+ }
+ },
+
+ // 重新加载模块(通过适配器)
+ reloadModule: function(moduleId) {
+ console.log(`[error-boundary] 尝试重载模块: ${moduleId}`);
+
+ // 触发全局重载事件
+ if (window.EventBus) {
+ EventBus.emit('module:reload', { module: moduleId });
+ }
+
+ // 如果存在适配器,调用适配器的重载方法
+ if (window.ModuleAdapter && window.ModuleAdapter.reloadModule) {
+ const container = document.querySelector(`[data-module="${moduleId}"]`) ||
+ document.getElementById(`module-${moduleId}`);
+ if (container) {
+ ModuleAdapter.reloadModule(moduleId, container.id);
+ }
+ } else {
+ // 否则直接刷新页面(简易方案)
+ location.reload();
+ }
+ },
+
+ // 隐藏错误(仅当用户点击“忽略”时)
+ hideError: function(btn) {
+ const errorDiv = btn.closest('.error-boundary');
+ if (errorDiv) {
+ errorDiv.style.display = 'none';
+ }
+ },
+
+ // 添加一些基础样式(会在 channel-style.css 中补充)
+ injectStyles: function() {
+ const style = document.createElement('style');
+ style.textContent = `
+ .error-boundary {
+ padding: 30px;
+ text-align: center;
+ background: #fff3f3;
+ border: 1px solid #ffcdd2;
+ border-radius: 8px;
+ margin: 20px 0;
+ }
+ .error-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+ .error-message h4 {
+ color: #d32f2f;
+ margin: 0 0 10px;
+ }
+ .error-message p {
+ color: #666;
+ margin: 0 0 20px;
+ }
+ .error-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ }
+ .error-retry {
+ padding: 8px 16px;
+ background: #d32f2f;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+ .error-dismiss {
+ padding: 8px 16px;
+ background: #9e9e9e;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+};
+
+// 自动注入样式
+setTimeout(() => {
+ if (window.ErrorBoundary) {
+ ErrorBoundary.injectStyles();
+ }
+}, 100);
+
+console.log('[error-boundary] 已加载');
diff --git a/modules/m-channel/event-bus.js b/modules/m-channel/event-bus.js
new file mode 100644
index 00000000..a4b09f2e
--- /dev/null
+++ b/modules/m-channel/event-bus.js
@@ -0,0 +1,37 @@
+// 事件总线(群聊频道)
+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}`);
+ }
+ });
+ }
+};
diff --git a/modules/m-channel/index.html b/modules/m-channel/index.html
index c0574ad8..59dc02c1 100644
--- a/modules/m-channel/index.html
+++ b/modules/m-channel/index.html
@@ -3,31 +3,40 @@
- M-CHANNEL · 用户频道引擎
+ 光湖频道 · 动态渲染引擎
+
+
-
-
+
+
+
+
+
+
-
-
- ✨ 加载中...
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+