From e4ef932e282ed4c8bbf7620efe6ec836cab87b5a Mon Sep 17 00:00:00 2001 From: juzi0412 <1824680224@qq.com> Date: Sun, 8 Mar 2026 21:07:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(m-channel):=20=E7=8E=AF=E8=8A=820~1=20SPA?= =?UTF-8?q?=E8=B7=AF=E7=94=B1+=E6=A8=A1=E5=9D=97=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/m-channel/app.js | 45 +++++++ modules/m-channel/channel-router.js | 98 ++++++++++++++ modules/m-channel/channel-style.css | 150 +++++++++++++++++++++ modules/m-channel/index.html | 33 +++++ modules/m-channel/mock-modules/mock-a.html | 5 + modules/m-channel/mock-modules/mock-b.html | 5 + modules/m-channel/mock-modules/mock-c.html | 5 + modules/m-channel/mock-modules/mock-d.html | 5 + modules/m-channel/module-loader.js | 65 +++++++++ modules/m-channel/module-registry.js | 7 + modules/m-channel/views/404.html | 5 + modules/m-channel/views/about.html | 8 ++ modules/m-channel/views/channel.html | 29 ++++ modules/m-channel/views/home.html | 12 ++ 14 files changed, 472 insertions(+) create mode 100644 modules/m-channel/app.js create mode 100644 modules/m-channel/channel-router.js create mode 100644 modules/m-channel/channel-style.css create mode 100644 modules/m-channel/index.html create mode 100644 modules/m-channel/mock-modules/mock-a.html create mode 100644 modules/m-channel/mock-modules/mock-b.html create mode 100644 modules/m-channel/mock-modules/mock-c.html create mode 100644 modules/m-channel/mock-modules/mock-d.html create mode 100644 modules/m-channel/module-loader.js create mode 100644 modules/m-channel/module-registry.js create mode 100644 modules/m-channel/views/404.html create mode 100644 modules/m-channel/views/about.html create mode 100644 modules/m-channel/views/channel.html create mode 100644 modules/m-channel/views/home.html diff --git a/modules/m-channel/app.js b/modules/m-channel/app.js new file mode 100644 index 00000000..486b8f3d --- /dev/null +++ b/modules/m-channel/app.js @@ -0,0 +1,45 @@ +// 应用入口:初始化路由和模块加载器的协作 + +// 当路由变化时,如果离开频道页,自动卸载模块 +window.addEventListener('hashchange', () => { + const path = window.location.hash.slice(2) || 'home'; + if (path !== 'channel') { + // 离开频道页时卸载模块(避免残留) + if (window.unloadModule) window.unloadModule(); + } +}); + +// 页面加载完成后,如果当前是频道页,绑定卡片事件 +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(); + } +}); +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); + }); +} + +function cardClickHandler(e) { + const card = e.currentTarget; + const moduleId = card.dataset.module; + if (moduleId && window.loadModule) { + // 可选:先卸载再加载(但 loadModule 会覆盖,所以不需要显式 unload) + window.loadModule(moduleId); + } +} diff --git a/modules/m-channel/channel-router.js b/modules/m-channel/channel-router.js new file mode 100644 index 00000000..614fc3ed --- /dev/null +++ b/modules/m-channel/channel-router.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/channel-style.css b/modules/m-channel/channel-style.css new file mode 100644 index 00000000..24da6655 --- /dev/null +++ b/modules/m-channel/channel-style.css @@ -0,0 +1,150 @@ +/* 基础重置 */ +* { + 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; + 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); +} + +/* 状态栏 */ +.status-bar { + background: #1e293b; + color: #94a3b8; + padding: 0.75rem 2rem; + font-size: 0.9rem; + text-align: center; + border-top: 1px solid #334155; +} + +/* 卡片网格 */ +.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; + 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; + transition: all 0.2s; + margin-bottom: 1rem; +} + +.back-button:hover { + background: #f1f5f9; + border-color: #94a3b8; +} diff --git a/modules/m-channel/index.html b/modules/m-channel/index.html new file mode 100644 index 00000000..c0574ad8 --- /dev/null +++ b/modules/m-channel/index.html @@ -0,0 +1,33 @@ + + + + + + M-CHANNEL · 用户频道引擎 + + + + + + + +
+

✨ 加载中...

+
+ + + + + + + + + + + diff --git a/modules/m-channel/mock-modules/mock-a.html b/modules/m-channel/mock-modules/mock-a.html new file mode 100644 index 00000000..6515b675 --- /dev/null +++ b/modules/m-channel/mock-modules/mock-a.html @@ -0,0 +1,5 @@ +
+

📦 模块 A 内容

+

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

+ +
diff --git a/modules/m-channel/mock-modules/mock-b.html b/modules/m-channel/mock-modules/mock-b.html new file mode 100644 index 00000000..523f26fc --- /dev/null +++ b/modules/m-channel/mock-modules/mock-b.html @@ -0,0 +1,5 @@ +
+

📦 模块 B 内容

+

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

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

📦 模块 C 内容

+

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

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

📦 模块 D 内容

+

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

+ +
diff --git a/modules/m-channel/module-loader.js b/modules/m-channel/module-loader.js new file mode 100644 index 00000000..1d8e199c --- /dev/null +++ b/modules/m-channel/module-loader.js @@ -0,0 +1,65 @@ +// 模块加载器(带缓存、超时、错误处理) +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}`); + } + + 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秒),请检查网络或文件是否存在
`; + } else { + container.innerHTML = `
❌ 加载失败:${error.message}
`; + } + } +} + +// 卸载模块(清空容器) +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 new file mode 100644 index 00000000..a0a87ff3 --- /dev/null +++ b/modules/m-channel/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/views/404.html b/modules/m-channel/views/404.html new file mode 100644 index 00000000..0ac7792e --- /dev/null +++ b/modules/m-channel/views/404.html @@ -0,0 +1,5 @@ +
+

😵 404 - 页面走丢啦

+

你访问的路径不存在~

+

👉 点击返回首页

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

ℹ️ 关于

+

光湖实验室 · HoloLake Era

+

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

+

开发者:DEV-010 桔子 🍊

+

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

+

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

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

📺 我的频道

+

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

+ + +
+
+

📦 模块 A

+

这是模拟模块 A

+
+
+

📦 模块 B

+

这是模拟模块 B

+
+
+

📦 模块 C

+

这是模拟模块 C

+
+
+

📦 模块 D

+

这是模拟模块 D

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

🏠 首页

+

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

+

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

+
+

✨ 当前进度

+ +