feat(m-channel): 环节0~1 SPA路由+模块动态加载完成
This commit is contained in:
parent
b8ad5dc2c8
commit
e4ef932e28
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<div class="error-view">
|
||||
<h2>😵 404 - 页面走丢啦</h2>
|
||||
<p>你访问的路径不存在~</p>
|
||||
<p><a href="#/home">👉 点击返回首页</a></p>
|
||||
</div>
|
||||
|
|
@ -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+1(SPA路由 + 模块动态加载)</p>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue