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 = ` + 🔔 + + `; + 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 += ` +
    +
    + ⋮⋮ + ${mod.icon} ${mod.name} + ${isFavorite ? '★' : '☆'} +
    +
    +

    ${mod.desc}

    +
    +
    + `; + }); + 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 += ` +
    +
    + ⋮⋮ + ${mod.icon} ${mod.name} + ${isFavorite ? '★' : '☆'} +
    +
    +

    ${mod.desc}

    +
    +
    + `; + }); + 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 += ` + + + + + + + `; + }); + + html += ` + +
    模块访问次数累计使用时长最后使用
    ${this.getModuleName(moduleId)}${detail.count}${detail.totalTime}${detail.lastUsed}
    +
    + +
    +
    + `; + + 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 · 用户频道引擎 + 光湖频道 · 动态渲染引擎 + + - - +
    + +
    + +
    +
    - -
    -

    ✨ 加载中...

    -
    - - - - - + - + + + + + + + + + + + diff --git a/modules/m-channel/index.html.bak b/modules/m-channel/index.html.bak new file mode 100644 index 00000000..e86e9904 --- /dev/null +++ b/modules/m-channel/index.html.bak @@ -0,0 +1,33 @@ + + + + + + 光湖频道 · 动态渲染引擎 + + + + +
    + +
    + +
    +
    + + + + + + + + + + + diff --git a/modules/m-channel/mock-modules/mock-a.html b/modules/m-channel/mock-modules/mock-a.html index 6515b675..2c43abc4 100644 --- a/modules/m-channel/mock-modules/mock-a.html +++ b/modules/m-channel/mock-modules/mock-a.html @@ -1,5 +1,16 @@ -
    -

    📦 模块 A 内容

    -

    这是从 mock-a.html 加载的真实内容~

    - -
    + + + +

    模拟模块A

    + +
    + + + + diff --git a/modules/m-channel/mock-modules/mock-b.html b/modules/m-channel/mock-modules/mock-b.html index 523f26fc..5236978a 100644 --- a/modules/m-channel/mock-modules/mock-b.html +++ b/modules/m-channel/mock-modules/mock-b.html @@ -1,5 +1,17 @@ -
    -

    📦 模块 B 内容

    -

    这是从 mock-b.html 加载的真实内容~

    - -
    + + + +

    模拟模块B

    +
    + + + + diff --git a/modules/m-channel/mock-modules/mock-c.html b/modules/m-channel/mock-modules/mock-c.html deleted file mode 100644 index 8299489d..00000000 --- a/modules/m-channel/mock-modules/mock-c.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -

    📦 模块 C 内容

    -

    这是从 mock-c.html 加载的真实内容~

    - -
    diff --git a/modules/m-channel/mock-modules/mock-d.html b/modules/m-channel/mock-modules/mock-d.html deleted file mode 100644 index 02ccd25d..00000000 --- a/modules/m-channel/mock-modules/mock-d.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -

    📦 模块 D 内容

    -

    这是从 mock-d.html 加载的真实内容~

    - -
    diff --git a/modules/m-channel/module-lifecycle.js b/modules/m-channel/module-lifecycle.js new file mode 100644 index 00000000..d99ea038 --- /dev/null +++ b/modules/m-channel/module-lifecycle.js @@ -0,0 +1,23 @@ +// 模块生命周期管理 +window.ModuleLifecycle = { + activeModules: new Set(), + + // 模块加载时 + onLoad: function(moduleName) { + this.activeModules.add(moduleName); + console.log(`[生命周期] 模块加载: ${moduleName}`); + EventBus.emit('module:loaded', { module: moduleName, time: Date.now() }); + }, + + // 模块卸载时 + onUnload: function(moduleName) { + this.activeModules.delete(moduleName); + console.log(`[生命周期] 模块卸载: ${moduleName}`); + EventBus.emit('module:unloaded', { module: moduleName, time: Date.now() }); + }, + + // 获取当前活跃模块 + getActiveModules: function() { + return Array.from(this.activeModules); + } +}; diff --git a/modules/m-channel/module-loader.js b/modules/m-channel/module-loader.js index 1d8e199c..e812f52f 100644 --- a/modules/m-channel/module-loader.js +++ b/modules/m-channel/module-loader.js @@ -1,65 +1,36 @@ -// 模块加载器(带缓存、超时、错误处理) -const moduleCache = new Map(); // 缓存已加载的模块HTML - -// 加载模块 -async function loadModule(moduleId, containerId = 'module-display-area') { - const container = document.getElementById(containerId); - if (!container) return; - - // 显示加载动画 - container.innerHTML = '
    '; - - // 检查缓存 - if (moduleCache.has(moduleId)) { - console.log(`[cache hit] 模块 ${moduleId} 从缓存加载`); - container.innerHTML = moduleCache.get(moduleId); - return; - } - - // 获取模块路径 - const modulePath = window.moduleRegistry?.[moduleId]; - if (!modulePath) { - container.innerHTML = `
    ❌ 模块 ID "${moduleId}" 未在注册表中找到
    `; - return; - } - - // 设置超时(5秒) - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - - try { - const response = await fetch(modulePath, { signal: controller.signal }); - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); +// 模块加载器 +window.ModuleLoader = { + loadModule: function(moduleName, containerId) { + console.log(`[loader] 加载模块: ${moduleName}`); + + const module = ModuleRegistry.get(moduleName); + if (!module) { + console.error(`[loader] 模块 ${moduleName} 未注册`); + return; } - - const html = await response.text(); - // 存入缓存 - moduleCache.set(moduleId, html); - console.log(`[cache miss] 模块 ${moduleId} 已加载并缓存`); - - container.innerHTML = html; - } catch (error) { - clearTimeout(timeoutId); - if (error.name === 'AbortError') { - container.innerHTML = `
    ⏰ 加载超时(>5秒),请检查网络或文件是否存在
    `; + + const container = document.getElementById(containerId); + if (!container) { + console.error(`[loader] 容器 ${containerId} 不存在`); + return; + } + + // 如果模块有 init 方法 + if (module.init && typeof module.init === 'function') { + module.init(container); } else { - container.innerHTML = `
    ❌ 加载失败:${error.message}
    `; + container.innerHTML = `
    模块 ${moduleName} 内容
    `; } + + ModuleLifecycle.onLoad(moduleName); + }, + + unloadModule: function(moduleName) { + console.log(`[loader] 卸载模块: ${moduleName}`); + const module = ModuleRegistry.get(moduleName); + if (module && module.destroy && typeof module.destroy === 'function') { + module.destroy(); + } + ModuleLifecycle.onUnload(moduleName); } -} - -// 卸载模块(清空容器) -function unloadModule(containerId = 'module-display-area') { - const container = document.getElementById(containerId); - if (container) { - container.innerHTML = ''; - console.log(`[unload] 模块已卸载 (${containerId})`); - } -} - -// 导出 -window.loadModule = loadModule; -window.unloadModule = unloadModule; +}; diff --git a/modules/m-channel/module-registry.js b/modules/m-channel/module-registry.js index a0a87ff3..c3fb0aef 100644 --- a/modules/m-channel/module-registry.js +++ b/modules/m-channel/module-registry.js @@ -1,7 +1,17 @@ -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 = { + modules: {}, + + register: function(name, module) { + this.modules[name] = module; + console.log(`[registry] 模块 ${name} 已注册`); + }, + + get: function(name) { + return this.modules[name]; + }, + + list: function() { + return Object.keys(this.modules); + } }; -window.moduleRegistry = moduleRegistry; diff --git a/modules/m-channel/views/404.html b/modules/m-channel/views/404.html deleted file mode 100644 index 0ac7792e..00000000 --- a/modules/m-channel/views/404.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -

    😵 404 - 页面走丢啦

    -

    你访问的路径不存在~

    -

    👉 点击返回首页

    -
    diff --git a/modules/m-channel/views/about.html b/modules/m-channel/views/about.html deleted file mode 100644 index cb796452..00000000 --- a/modules/m-channel/views/about.html +++ /dev/null @@ -1,8 +0,0 @@ -
    -

    ℹ️ 关于

    -

    光湖实验室 · HoloLake Era

    -

    这是 M-CHANNEL 频道引擎演示页面。

    -

    开发者:DEV-010 桔子 🍊

    -

    广播编号:BC-M-CHANNEL-001-JZ

    -

    环节:0+1(SPA路由 + 模块动态加载)

    -
    diff --git a/modules/m-channel/views/channel-dashboard.html b/modules/m-channel/views/channel-dashboard.html new file mode 100644 index 00000000..e67784de --- /dev/null +++ b/modules/m-channel/views/channel-dashboard.html @@ -0,0 +1,57 @@ +
    +
    +

    📊 频道数据面板

    +

    模块使用统计 · 性能监控 · 访问趋势

    +
    + +
    + +
    +

    📊 模块访问次数

    +
    +

    暂无数据,多点几个模块再来看

    +
    +
    + + +
    +

    🥧 使用时间占比

    +
    +
    + +
    +
      +
      +
      + + +
      +

      📈 最近7天访问趋势

      +
      + +
      +
      +
      + + +
      +

      ⚡ 模块加载性能

      + + + + + + + + + + + +
      模块访问次数总停留(分钟)平均加载(ms)状态
      +
      + + +
      + +
      +
      diff --git a/modules/m-channel/views/channel-debug.html b/modules/m-channel/views/channel-debug.html new file mode 100644 index 00000000..91f1f882 --- /dev/null +++ b/modules/m-channel/views/channel-debug.html @@ -0,0 +1,93 @@ + + + + + + +
      +

      🐞 调试面板

      + +
      + 当前活跃模块: + 加载中... +
      + +

      事件总线聊天记录

      + + +
      + + +
      + +
      + + +
      + +

      消息会广播给所有订阅的模块

      +
      + + + + diff --git a/modules/m-channel/views/channel-settings.html b/modules/m-channel/views/channel-settings.html new file mode 100644 index 00000000..ac5c5e97 --- /dev/null +++ b/modules/m-channel/views/channel-settings.html @@ -0,0 +1,272 @@ + + + + + + +
      + + +

      ⚙️ 频道设置

      + + +
      +
      📐 布局模式
      +
      + + + +
      +
      + + +
      +
      🎨 主题颜色
      +
      + + + + + +
      +
      + + +
      +
      实时预览
      +
      +
      +
      工单管理
      +
      待处理: 3
      +
      +
      +
      数据统计
      +
      完成率: 75%
      +
      +
      +
      组件库
      +
      24个组件
      +
      +
      +
      + + +
      + +
      +
      + + + + diff --git a/modules/m-channel/views/channel.html b/modules/m-channel/views/channel.html deleted file mode 100644 index 857ba0b9..00000000 --- a/modules/m-channel/views/channel.html +++ /dev/null @@ -1,29 +0,0 @@ -
      -

      📺 我的频道

      -

      点击下面的卡片,动态加载模块内容~

      - - -
      -
      -

      📦 模块 A

      -

      这是模拟模块 A

      -
      -
      -

      📦 模块 B

      -

      这是模拟模块 B

      -
      -
      -

      📦 模块 C

      -

      这是模拟模块 C

      -
      -
      -

      📦 模块 D

      -

      这是模拟模块 D

      -
      -
      - - -
      - -
      -
      diff --git a/modules/m-channel/views/home.html b/modules/m-channel/views/home.html deleted file mode 100644 index de61489a..00000000 --- a/modules/m-channel/views/home.html +++ /dev/null @@ -1,12 +0,0 @@ -
      -

      🏠 首页

      -

      欢迎来到光湖网站~ 这里是首页视图。

      -

      你可以在这里放一些全局介绍或者公告。

      -
      -

      ✨ 当前进度

      - -