feat(m-channel): 环节0~1 SPA路由+模块动态加载完成

This commit is contained in:
juzi0412 2026-03-08 21:07:27 +08:00
parent b8ad5dc2c8
commit e4ef932e28
14 changed files with 472 additions and 0 deletions

45
modules/m-channel/app.js Normal file
View File

@ -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);
}
}

View File

@ -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 = '<div class="loader" style="margin: 2rem auto;"></div>';
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 = `
<div class="error-message">
加载失败${error.message}<br>
<small>请检查文件是否存在或刷新重试</small>
</div>
`;
}
// 更新导航高亮和状态栏
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 = '<div class="error-message">⚠️ 404 - 页面未找到</div>';
}
} catch {
container.innerHTML = '<div class="error-message">⚠️ 404 - 页面未找到</div>';
}
}
// 更新导航高亮
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);
}
});

View File

@ -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;
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M-CHANNEL · 用户频道引擎</title>
<link rel="stylesheet" href="channel-style.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="channel-nav">
<a href="#/home" class="nav-link">🏠 首页</a>
<a href="#/channel" class="nav-link">📺 我的频道</a>
<a href="#/about" class="nav-link"> 关于</a>
</nav>
<!-- 路由容器:动态内容放这里 -->
<main id="router-view" class="router-view">
<p>✨ 加载中...</p>
</main>
<!-- 底部状态栏 -->
<footer class="status-bar">
<span id="current-route">当前路由:/</span>
</footer>
<!-- 脚本区(注意顺序:注册表 → 加载器 → 路由 → 应用入口) -->
<script src="module-registry.js"></script>
<script src="module-loader.js"></script>
<script src="channel-router.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@ -0,0 +1,5 @@
<div class="mock-module">
<h3>📦 模块 A 内容</h3>
<p>这是从 mock-a.html 加载的真实内容~</p>
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
</div>

View File

@ -0,0 +1,5 @@
<div class="mock-module">
<h3>📦 模块 B 内容</h3>
<p>这是从 mock-b.html 加载的真实内容~</p>
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
</div>

View File

@ -0,0 +1,5 @@
<div class="mock-module">
<h3>📦 模块 C 内容</h3>
<p>这是从 mock-c.html 加载的真实内容~</p>
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
</div>

View File

@ -0,0 +1,5 @@
<div class="mock-module">
<h3>📦 模块 D 内容</h3>
<p>这是从 mock-d.html 加载的真实内容~</p>
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
</div>

View File

@ -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 = '<div class="loader" style="margin: 1rem auto;"></div>';
// 检查缓存
if (moduleCache.has(moduleId)) {
console.log(`[cache hit] 模块 ${moduleId} 从缓存加载`);
container.innerHTML = moduleCache.get(moduleId);
return;
}
// 获取模块路径
const modulePath = window.moduleRegistry?.[moduleId];
if (!modulePath) {
container.innerHTML = `<div class="error-message">❌ 模块 ID "${moduleId}" 未在注册表中找到</div>`;
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 = `<div class="error-message">⏰ 加载超时(>5秒请检查网络或文件是否存在</div>`;
} else {
container.innerHTML = `<div class="error-message">❌ 加载失败:${error.message}</div>`;
}
}
}
// 卸载模块(清空容器)
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;

View File

@ -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;

View File

@ -0,0 +1,5 @@
<div class="error-view">
<h2>😵 404 - 页面走丢啦</h2>
<p>你访问的路径不存在~</p>
<p><a href="#/home">👉 点击返回首页</a></p>
</div>

View File

@ -0,0 +1,8 @@
<div class="about-view">
<h2> 关于</h2>
<p>光湖实验室 · HoloLake Era</p>
<p>这是 M-CHANNEL 频道引擎演示页面。</p>
<p>开发者DEV-010 桔子 🍊</p>
<p>广播编号BC-M-CHANNEL-001-JZ</p>
<p>环节0+1SPA路由 + 模块动态加载)</p>
</div>

View File

@ -0,0 +1,29 @@
<div class="channel-view">
<h2>📺 我的频道</h2>
<p>点击下面的卡片,动态加载模块内容~</p>
<!-- 卡片网格4个模块卡片 -->
<div class="card-grid" id="module-grid">
<div class="module-card" data-module="mock-a">
<h3>📦 模块 A</h3>
<p>这是模拟模块 A</p>
</div>
<div class="module-card" data-module="mock-b">
<h3>📦 模块 B</h3>
<p>这是模拟模块 B</p>
</div>
<div class="module-card" data-module="mock-c">
<h3>📦 模块 C</h3>
<p>这是模拟模块 C</p>
</div>
<div class="module-card" data-module="mock-d">
<h3>📦 模块 D</h3>
<p>这是模拟模块 D</p>
</div>
</div>
<!-- 模块内容会加载到这里 -->
<div id="module-display-area" class="module-content">
<!-- 初始为空,点击卡片后填充 -->
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="home-view">
<h2>🏠 首页</h2>
<p>欢迎来到光湖网站~ 这里是首页视图。</p>
<p>你可以在这里放一些全局介绍或者公告。</p>
<hr>
<h3>✨ 当前进度</h3>
<ul>
<li>路由引擎:✅ 已就绪</li>
<li>模块加载器:⏳ 准备中</li>
<li>卡片网格:⏳ 稍后出现</li>
</ul>
</div>