[BC-M-CHANNEL-006-JZ][DEV-010] 环节8 频道数据面板与性能监控
This commit is contained in:
parent
962cd797be
commit
0534c4e1fe
|
|
@ -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 = `
|
||||
<input type="text" class="channel-search-input" placeholder="搜索模块...">
|
||||
<button class="channel-search-clear">×</button>
|
||||
`;
|
||||
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, '<span class="highlight">$1</span>');
|
||||
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 = `
|
||||
<a href="#/">首页</a> >
|
||||
<a href="#/channel">频道</a> >
|
||||
<span class="current">${moduleName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function createShortcutHint() {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'shortcut-hint';
|
||||
hint.innerHTML = `
|
||||
<kbd>⌘K</kbd> 搜索 ·
|
||||
<kbd>↑</kbd><kbd>↓</kbd> 选择 ·
|
||||
<kbd>Enter</kbd> 打开 ·
|
||||
<kbd>Esc</kbd> 关闭
|
||||
`;
|
||||
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了
|
||||
}
|
||||
})();
|
||||
|
|
@ -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 = '<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>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 = '<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}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 初始化:恢复上次访问的路由 ==================
|
||||
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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = `<div class="error">加载模块 ${moduleName} 失败</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
// 卸载当前模块
|
||||
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;
|
||||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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 = `<div class="error-boundary">模块配置不存在</div>`;
|
||||
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 = `<div class="error-boundary">模块加载失败,请重试 <button onclick="ModuleAdapter.reloadModule('${moduleId}', '${containerId}')">重试</button></div>`;
|
||||
};
|
||||
|
||||
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] 模块适配器已加载');
|
||||
|
|
@ -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] 事件总线拦截器已安装');
|
||||
|
|
|
|||
|
|
@ -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] 初始化完成');
|
||||
});
|
||||
|
|
@ -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,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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>光湖频道 · 动态渲染引擎</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本(顺序很重要!) -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = `<div class="error">加载失败:${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示已加载的模块
|
||||
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;
|
||||
|
|
@ -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,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('🗑️ 所有分析数据已清除');
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖频道 · 完全版</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
<link rel="stylesheet" href="channel-layout.css">
|
||||
<style id="theme-variables"></style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="m06">📋 工单管理</button>
|
||||
<button class="channel-btn" data-channel="m08">📊 数据统计</button>
|
||||
<button class="channel-btn" data-channel="m11">🧩 组件库</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
<button class="channel-btn settings-btn" id="settings-toggle">⚙️ 设置</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 设置面板(侧边栏) -->
|
||||
<div id="settings-panel" class="settings-panel">
|
||||
<div class="settings-header">
|
||||
<h3>频道设置</h3>
|
||||
<button id="settings-close">✕</button>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
<div class="settings-section">
|
||||
<h4>布局模式</h4>
|
||||
<div class="layout-options">
|
||||
<label><input type="radio" name="layout" value="grid" checked> 网格</label>
|
||||
<label><input type="radio" name="layout" value="list"> 列表</label>
|
||||
<label><input type="radio" name="layout" value="compact"> 紧凑</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>主题色</h4>
|
||||
<div class="theme-options">
|
||||
<button class="theme-btn" data-theme="default">默认蓝</button>
|
||||
<button class="theme-btn" data-theme="ocean">海洋</button>
|
||||
<button class="theme-btn" data-theme="forest">森林</button>
|
||||
<button class="theme-btn" data-theme="sunset">日落</button>
|
||||
<button class="theme-btn" data-theme="lavender">薰衣草</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>统计数据</h4>
|
||||
<button id="show-stats-btn">查看使用统计</button>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h4>其他</h4>
|
||||
<button id="reset-preferences-btn">恢复默认设置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计面板(模态框) -->
|
||||
<div id="stats-modal" class="stats-modal">
|
||||
<div class="stats-modal-content">
|
||||
<div class="stats-modal-header">
|
||||
<h3>使用统计</h3>
|
||||
<button id="stats-close">✕</button>
|
||||
</div>
|
||||
<div class="stats-modal-body" id="stats-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本 -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="error-boundary.js"></script>
|
||||
<script src="adapters/module-adapter.js"></script>
|
||||
<script src="adapters/m06-adapter.js"></script>
|
||||
<script src="adapters/m08-adapter.js"></script>
|
||||
<script src="adapters/m11-adapter.js"></script>
|
||||
<script src="channel-preferences.js"></script>
|
||||
<script src="channel-theme.js"></script>
|
||||
<script src="channel-favorites.js"></script>
|
||||
<script src="channel-stats.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="app.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.ChannelPreferences) ChannelPreferences.init();
|
||||
if (window.ChannelTheme) ChannelTheme.init();
|
||||
if (window.ChannelStats) ChannelStats.init();
|
||||
|
||||
// 设置面板开关
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsPanel = document.getElementById('settings-panel');
|
||||
const settingsClose = document.getElementById('settings-close');
|
||||
if (settingsToggle && settingsPanel) {
|
||||
settingsToggle.addEventListener('click', () => {
|
||||
settingsPanel.classList.add('open');
|
||||
});
|
||||
settingsClose.addEventListener('click', () => {
|
||||
settingsPanel.classList.remove('open');
|
||||
});
|
||||
}
|
||||
|
||||
// 布局切换
|
||||
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
const layout = e.target.value;
|
||||
document.body.className = document.body.className.replace(/layout-\w+/, '') + ' layout-' + layout;
|
||||
if (window.ChannelPreferences) ChannelPreferences.setLayout(layout);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 主题切换
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const theme = btn.dataset.theme;
|
||||
if (window.ChannelTheme) ChannelTheme.setTheme(theme);
|
||||
});
|
||||
});
|
||||
|
||||
// 统计面板
|
||||
const showStatsBtn = document.getElementById('show-stats-btn');
|
||||
const statsModal = document.getElementById('stats-modal');
|
||||
const statsClose = document.getElementById('stats-close');
|
||||
if (showStatsBtn && statsModal) {
|
||||
showStatsBtn.addEventListener('click', () => {
|
||||
if (window.ChannelStats) {
|
||||
const body = document.getElementById('stats-modal-body');
|
||||
ChannelStats.renderStatsPanel(body);
|
||||
statsModal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
statsClose.addEventListener('click', () => {
|
||||
statsModal.style.display = 'none';
|
||||
});
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === statsModal) statsModal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复默认
|
||||
const resetBtn = document.getElementById('reset-preferences-btn');
|
||||
if (resetBtn && window.ChannelPreferences) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (confirm('确定恢复所有默认设置吗?')) {
|
||||
ChannelPreferences.reset();
|
||||
document.body.className = document.body.className.replace(/layout-\w+/, '') + ' layout-grid';
|
||||
document.querySelectorAll('input[name="layout"]').forEach(r => r.checked = (r.value === 'grid'));
|
||||
if (window.ChannelTheme) ChannelTheme.setTheme('default');
|
||||
alert('已恢复默认设置');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 拦截事件总线消息
|
||||
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);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.settings-panel { position: fixed; top: 0; right: -400px; width: 380px; height: 100vh; background: white; box-shadow: -2px 0 10px rgba(0,0,0,0.1); transition: right 0.3s ease; z-index: 1000; display: flex; flex-direction: column; }
|
||||
.settings-panel.open { right: 0; }
|
||||
.settings-header { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||||
.settings-body { padding: 20px; overflow-y: auto; }
|
||||
.settings-section { margin-bottom: 30px; }
|
||||
.layout-options label { margin-right: 20px; }
|
||||
.theme-options { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.theme-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 20px; background: white; cursor: pointer; }
|
||||
.stats-modal { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index:2000; }
|
||||
.stats-modal-content { background: white; width: 600px; max-width: 90%; max-height: 80vh; border-radius: 12px; overflow: auto; }
|
||||
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; padding: 20px; }
|
||||
.module-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); cursor: pointer; transition: all 0.2s; }
|
||||
.module-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.module-card-header { padding: 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; }
|
||||
.drag-handle { font-size: 20px; color: #999; cursor: grab; }
|
||||
.favorite-star { margin-left: auto; font-size: 20px; color: #ccc; cursor: pointer; }
|
||||
.favorite-star.active { color: #fbbf24; }
|
||||
.module-card-content { padding: 15px; }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
// 确保所有模块在页面加载后自动初始化
|
||||
(function() {
|
||||
if (window.ChannelPreferences) ChannelPreferences.init();
|
||||
if (window.ChannelTheme) ChannelTheme.init();
|
||||
if (window.ChannelFavorites) {
|
||||
setTimeout(() => {
|
||||
ChannelFavorites.init();
|
||||
ChannelFavorites.initDragAndDrop();
|
||||
}, 100);
|
||||
}
|
||||
if (window.ChannelStats) ChannelStats.init();
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// 确保所有功能自动初始化
|
||||
(function() {
|
||||
console.log('[最终修复] 执行自动初始化');
|
||||
|
||||
// 初始化各个模块(如果尚未初始化)
|
||||
if (window.ChannelPreferences && !window.ChannelPreferences.config) {
|
||||
ChannelPreferences.init();
|
||||
}
|
||||
if (window.ChannelTheme) {
|
||||
ChannelTheme.init();
|
||||
}
|
||||
if (window.ChannelFavorites) {
|
||||
// 延迟一点确保DOM渲染完成
|
||||
setTimeout(() => {
|
||||
ChannelFavorites.init();
|
||||
ChannelFavorites.initDragAndDrop();
|
||||
}, 200);
|
||||
}
|
||||
if (window.ChannelStats) {
|
||||
ChannelStats.init();
|
||||
}
|
||||
|
||||
// 绑定设置按钮事件
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsPanel = document.getElementById('settings-panel');
|
||||
if (settingsToggle && settingsPanel) {
|
||||
// 移除可能存在的旧监听器(避免重复)
|
||||
settingsToggle.replaceWith(settingsToggle.cloneNode(true));
|
||||
const newToggle = document.getElementById('settings-toggle');
|
||||
newToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
settingsPanel.classList.add('open');
|
||||
});
|
||||
console.log('[最终修复] 设置按钮事件已绑定');
|
||||
} else {
|
||||
console.warn('[最终修复] 未找到设置按钮或面板');
|
||||
}
|
||||
|
||||
// 修复拖拽排序保存
|
||||
if (window.ChannelFavorites && window.ChannelFavorites.handleDrop) {
|
||||
// 增强拖拽放置处理,保存排序
|
||||
const originalHandleDrop = ChannelFavorites.handleDrop;
|
||||
ChannelFavorites.handleDrop = function(e) {
|
||||
originalHandleDrop.call(this, e);
|
||||
// 拖拽完成后,保存当前顺序
|
||||
setTimeout(() => {
|
||||
if (window.ChannelPreferences) {
|
||||
const cards = Array.from(document.querySelectorAll('.module-card'));
|
||||
const order = cards.map(card => card.dataset.module);
|
||||
ChannelPreferences.setModuleOrder(order);
|
||||
console.log('[最终修复] 拖拽顺序已保存', order);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = '<p style="color:#666;text-align:center;width:100%;">暂无数据,多点几个模块再来看</p>';
|
||||
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 += '<div class="bar-item">' +
|
||||
'<span class="bar-value">' + mod.visits + '</span>' +
|
||||
'<div class="bar-fill" style="height:' + height + 'px;background:' + color + ';"></div>' +
|
||||
'<span class="bar-label">' + id.replace('m-', '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
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 += '<li><span class="dot" style="background:' + color + ';"></span>' +
|
||||
id.replace('m-', '') + ' ' + pctStr + '% (' + minutes + '分钟)</li>';
|
||||
});
|
||||
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 = '<tr><td colspan="5" style="text-align:center;color:#666;">暂无数据</td></tr>';
|
||||
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 += '<tr>' +
|
||||
'<td>' + id.replace('m-', '') + '</td>' +
|
||||
'<td>' + mod.visits + '</td>' +
|
||||
'<td>' + minutes + '分钟</td>' +
|
||||
'<td class="' + statusClass + '">' + avgLoad + 'ms</td>' +
|
||||
'<td class="' + statusClass + '">' + statusText + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 公开方法
|
||||
return {
|
||||
render: function() {
|
||||
renderBarChart();
|
||||
renderPieChart();
|
||||
renderLineChart();
|
||||
renderPerfTable();
|
||||
},
|
||||
clearData: function() {
|
||||
if (confirm('确定清除所有统计数据吗?')) {
|
||||
ChannelAnalytics.clearAll();
|
||||
this.render();
|
||||
console.log('🗑️ 数据已清除,图表已重置');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -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 = `
|
||||
<input type="text" class="channel-search-input" placeholder="搜索模块...">
|
||||
<button class="channel-search-clear">×</button>
|
||||
`;
|
||||
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, '<span class="highlight">$1</span>');
|
||||
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 = `
|
||||
<a href="#/">首页</a> >
|
||||
<a href="#/channel">频道</a> >
|
||||
<span class="current">${moduleName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function createShortcutHint() {
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'shortcut-hint';
|
||||
hint.innerHTML = `
|
||||
<kbd>⌘K</kbd> 搜索 ·
|
||||
<kbd>↑</kbd><kbd>↓</kbd> 选择 ·
|
||||
<kbd>Enter</kbd> 打开 ·
|
||||
<kbd>Esc</kbd> 关闭
|
||||
`;
|
||||
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}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<span class="bell-icon">🔔</span>
|
||||
<span class="bell-badge" style="display: none;">0</span>
|
||||
`;
|
||||
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 = `
|
||||
<div class="panel-header">
|
||||
<span>通知中心</span>
|
||||
<button class="mark-all-read">全部已读</button>
|
||||
</div>
|
||||
<ul class="notification-list"></ul>
|
||||
`;
|
||||
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 = '<li class="empty-state">暂无新通知</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.sort((a, b) => (a.time > b.time ? -1 : 1));
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<li class="notification-item ${item.read ? 'read' : ''}" data-module="${item.moduleId}">
|
||||
<div class="notification-time">${item.time}</div>
|
||||
<div class="notification-module">${item.moduleId}</div>
|
||||
<div class="notification-summary">${item.summary}</div>
|
||||
</li>
|
||||
`).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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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 = '<div class="loader" style="margin: 2rem auto;"></div>';
|
||||
|
||||
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 = '<div class="error-boundary">适配器未加载</div>';
|
||||
}
|
||||
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 = '<h2>数据面板加载失败</h2>';
|
||||
console.error(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
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();
|
||||
|
||||
// 内置频道
|
||||
if (channel === 'home') {
|
||||
this.renderModuleCards();
|
||||
} else if (channel === 'debug') {
|
||||
this.loadDebugPanel();
|
||||
} else {
|
||||
container.innerHTML = '<div class="error-message">⚠️ 404 - 页面未找到</div>';
|
||||
this.contentEl.innerHTML = '<h2>未知频道</h2>';
|
||||
}
|
||||
} 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');
|
||||
// 对于内置频道也记录加载完成
|
||||
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 = '<div class="module-grid">';
|
||||
modules.forEach(mod => {
|
||||
const isFavorite = window.ChannelPreferences ?
|
||||
ChannelPreferences.getFavorites().includes(mod.id) : false;
|
||||
html += `
|
||||
<div class="module-card" data-module="${mod.id}">
|
||||
<div class="module-card-header">
|
||||
<span class="drag-handle">⋮⋮</span>
|
||||
<span class="module-card-title">${mod.icon} ${mod.name}</span>
|
||||
<span class="favorite-star ${isFavorite ? 'active' : ''}">${isFavorite ? '★' : '☆'}</span>
|
||||
</div>
|
||||
<div class="module-card-content">
|
||||
<p>${mod.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
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 = '<h2>调试面板加载失败</h2>';
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = '<div class="error-boundary">适配器未加载</div>';
|
||||
}
|
||||
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 = '<h2>未知频道</h2>';
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染所有模块卡片(用于首页)
|
||||
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 = '<div class="module-grid">';
|
||||
modules.forEach(mod => {
|
||||
const isFavorite = window.ChannelPreferences ?
|
||||
ChannelPreferences.getFavorites().includes(mod.id) : false;
|
||||
html += `
|
||||
<div class="module-card" data-module="${mod.id}">
|
||||
<div class="module-card-header">
|
||||
<span class="drag-handle">⋮⋮</span>
|
||||
<span class="module-card-title">${mod.icon} ${mod.name}</span>
|
||||
<span class="favorite-star ${isFavorite ? 'active' : ''}">${isFavorite ? '★' : '☆'}</span>
|
||||
</div>
|
||||
<div class="module-card-content">
|
||||
<p>${mod.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
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 = '<h2>调试面板加载失败</h2>';
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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] 已清除');
|
||||
}
|
||||
};
|
||||
|
|
@ -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 = `
|
||||
<div class="stats-panel">
|
||||
<h3>📊 使用统计</h3>
|
||||
<div class="stats-summary">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总访问次数:</span>
|
||||
<span class="stat-value">${report.totalViews}</span>
|
||||
</div>
|
||||
${report.mostUsed ? `
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最常用模块:</span>
|
||||
<span class="stat-value">${this.getModuleName(report.mostUsed)} (${report.moduleDetails[report.mostUsed]?.count || 0}次)</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${report.lastUsed ? `
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最近使用:</span>
|
||||
<span class="stat-value">${this.getModuleName(report.lastUsed)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<h4>模块详情</h4>
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模块</th>
|
||||
<th>访问次数</th>
|
||||
<th>累计使用时长</th>
|
||||
<th>最后使用</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
modules.forEach(moduleId => {
|
||||
const detail = report.moduleDetails[moduleId] || { count: 0, totalTime: '0秒', lastUsed: '从未使用' };
|
||||
html += `
|
||||
<tr>
|
||||
<td>${this.getModuleName(moduleId)}</td>
|
||||
<td>${detail.count}</td>
|
||||
<td>${detail.totalTime}</td>
|
||||
<td>${detail.lastUsed}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="stats-actions">
|
||||
<button id="reset-stats-btn" class="stats-reset-btn">重置统计数据</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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] 已加载');
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] 已加载');
|
||||
|
|
@ -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%);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 = `
|
||||
<div class="error-boundary">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-message">
|
||||
<h4>模块 ${moduleId} 加载失败</h4>
|
||||
<p>${error.message || '未知错误'}</p>
|
||||
</div>
|
||||
<div class="error-actions">
|
||||
<button onclick="ErrorBoundary.reloadModule('${moduleId}')" class="error-retry">
|
||||
重试
|
||||
</button>
|
||||
<button onclick="ErrorBoundary.hideError(this)" class="error-dismiss">
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 记录错误到事件总线
|
||||
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] 已加载');
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -3,31 +3,40 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>M-CHANNEL · 用户频道引擎</title>
|
||||
<title>光湖频道 · 动态渲染引擎</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
<link rel="stylesheet" href="channel-dashboard.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>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="dashboard">📊 数据面板</button> <!-- 新增入口 -->
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 路由容器:动态内容放这里 -->
|
||||
<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="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="channel-enhancements.js"></script>
|
||||
<link rel="stylesheet" href="channel-notifications.css">
|
||||
<script src="channel-notifications.js"></script>
|
||||
|
||||
<!-- 新增:数据采集与面板逻辑 -->
|
||||
<script src="channel-analytics.js"></script>
|
||||
<script src="channel-dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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>光湖频道 · 动态渲染引擎</title>
|
||||
<link rel="stylesheet" href="channel-style.css">
|
||||
<link rel="stylesheet" href="channel-transition.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="channel-container">
|
||||
<nav class="channel-nav">
|
||||
<button class="channel-btn active" data-channel="home">🏠 首页</button>
|
||||
<button class="channel-btn" data-channel="module-a">📦 模块A</button>
|
||||
<button class="channel-btn" data-channel="module-b">📦 模块B</button>
|
||||
<button class="channel-btn" data-channel="module-c">📦 模块C</button>
|
||||
<button class="channel-btn" data-channel="debug">🐞 调试面板</button>
|
||||
</nav>
|
||||
<main id="channel-content" class="channel-content">
|
||||
<!-- 动态内容会加载到这里 -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 核心脚本(顺序很重要!) -->
|
||||
<script src="module-registry.js"></script>
|
||||
<script src="event-bus.js"></script>
|
||||
<script src="module-lifecycle.js"></script>
|
||||
<script src="channel-state.js"></script>
|
||||
<script src="channel-router.js"></script>
|
||||
<script src="module-loader.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
<div class="mock-module">
|
||||
<h3>📦 模块 A 内容</h3>
|
||||
<p>这是从 mock-a.html 加载的真实内容~</p>
|
||||
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h2>模拟模块A</h2>
|
||||
<button onclick="EventBus.emit('mock-a:click', { message: '模块A被点击了' })">
|
||||
发送事件
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
|
||||
<script>
|
||||
EventBus.on('mock-b:reply', function(data) {
|
||||
document.getElementById('output').innerHTML = '收到回复: ' + data.message;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
<div class="mock-module">
|
||||
<h3>📦 模块 B 内容</h3>
|
||||
<p>这是从 mock-b.html 加载的真实内容~</p>
|
||||
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h2>模拟模块B</h2>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
EventBus.on('mock-a:click', function(data) {
|
||||
const div = document.getElementById('messages');
|
||||
div.innerHTML += '<p>收到模块A消息: ' + JSON.stringify(data) + '</p>';
|
||||
|
||||
// 回复消息
|
||||
EventBus.emit('mock-b:reply', { message: '已收到,谢谢!' });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<div class="mock-module">
|
||||
<h3>📦 模块 C 内容</h3>
|
||||
<p>这是从 mock-c.html 加载的真实内容~</p>
|
||||
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
|
||||
</div>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<div class="mock-module">
|
||||
<h3>📦 模块 D 内容</h3>
|
||||
<p>这是从 mock-d.html 加载的真实内容~</p>
|
||||
<button class="back-button" onclick="window.unloadModule()">🔙 返回频道</button>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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 = '<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}`);
|
||||
// 模块加载器
|
||||
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 = `<div class="error-message">⏰ 加载超时(>5秒),请检查网络或文件是否存在</div>`;
|
||||
|
||||
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 = `<div class="error-message">❌ 加载失败:${error.message}</div>`;
|
||||
container.innerHTML = `<div>模块 ${moduleName} 内容</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<div class="error-view">
|
||||
<h2>😵 404 - 页面走丢啦</h2>
|
||||
<p>你访问的路径不存在~</p>
|
||||
<p><a href="#/home">👉 点击返回首页</a></p>
|
||||
</div>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<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,57 @@
|
|||
<div class="dashboard-container" id="analyticsDashboard">
|
||||
<div class="dashboard-header">
|
||||
<h2>📊 频道数据面板</h2>
|
||||
<p>模块使用统计 · 性能监控 · 访问趋势</p>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<!-- 柱状图:模块访问次数 -->
|
||||
<div class="chart-card">
|
||||
<h3>📊 模块访问次数</h3>
|
||||
<div class="bar-chart" id="visitBarChart">
|
||||
<p style="color:#666;text-align:center;width:100%;">暂无数据,多点几个模块再来看</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 饼图:使用时间占比 -->
|
||||
<div class="chart-card">
|
||||
<h3>🥧 使用时间占比</h3>
|
||||
<div class="pie-container" id="timePieChart">
|
||||
<div class="pie-canvas-wrap">
|
||||
<canvas id="pieCanvas" width="160" height="160"></canvas>
|
||||
</div>
|
||||
<ul class="pie-legend" id="pieLegend"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 折线图:7天趋势 -->
|
||||
<div class="chart-card" style="grid-column: 1 / -1;">
|
||||
<h3>📈 最近7天访问趋势</h3>
|
||||
<div class="line-chart-wrap">
|
||||
<canvas id="lineCanvas" width="800" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能表格 -->
|
||||
<div class="chart-card">
|
||||
<h3>⚡ 模块加载性能</h3>
|
||||
<table class="perf-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模块</th>
|
||||
<th>访问次数</th>
|
||||
<th>总停留(分钟)</th>
|
||||
<th>平均加载(ms)</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="perfTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 清除按钮 -->
|
||||
<div class="dashboard-actions">
|
||||
<button class="btn-clear" onclick="ChannelDashboard.clearData()">🗑️ 清除所有数据</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.debug-panel { padding: 20px; }
|
||||
.message-list {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #f9f9f9;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.message-list li {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.debug-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.debug-controls input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
.debug-controls button {
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.debug-controls button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.module-list {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="debug-panel">
|
||||
<h2>🐞 调试面板</h2>
|
||||
|
||||
<div class="module-list">
|
||||
<strong>当前活跃模块:</strong>
|
||||
<span id="active-modules">加载中...</span>
|
||||
</div>
|
||||
|
||||
<h3>事件总线聊天记录</h3>
|
||||
<ul class="message-list" id="debug-messages">
|
||||
<!-- 动态插入消息 -->
|
||||
</ul>
|
||||
|
||||
<div class="debug-controls">
|
||||
<input type="text" id="debug-input" placeholder="输入要发送的消息...">
|
||||
<button id="debug-send">发送测试消息</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-controls">
|
||||
<button id="debug-clear" style="background: #ef4444;">清除状态</button>
|
||||
<button id="debug-refresh" onclick="location.reload()">刷新页面</button>
|
||||
</div>
|
||||
|
||||
<p><small>消息会广播给所有订阅的模块</small></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 更新活跃模块列表
|
||||
function updateActiveModules() {
|
||||
const span = document.getElementById('active-modules');
|
||||
if (span && window.ModuleLifecycle) {
|
||||
const modules = ModuleLifecycle.getActiveModules();
|
||||
span.textContent = modules.length ? modules.join('、') : '无';
|
||||
}
|
||||
}
|
||||
|
||||
// 监听模块加载/卸载事件
|
||||
if (window.EventBus) {
|
||||
EventBus.on('module:loaded', updateActiveModules);
|
||||
EventBus.on('module:unloaded', updateActiveModules);
|
||||
}
|
||||
|
||||
// 初始更新
|
||||
setTimeout(updateActiveModules, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.settings-panel {
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.settings-section {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.settings-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.settings-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.layout-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.layout-btn {
|
||||
padding: 8px 16px;
|
||||
background: #f3f4f6;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.layout-btn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.layout-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.theme-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.theme-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.theme-btn.active {
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 0 2px white, 0 0 0 4px currentColor;
|
||||
}
|
||||
.theme-default { background: #3b82f6; }
|
||||
.theme-ocean { background: #0891b2; }
|
||||
.theme-forest { background: #059669; }
|
||||
.theme-sunset { background: #d97706; }
|
||||
.theme-lavender { background: #8b5cf6; }
|
||||
.reset-btn {
|
||||
padding: 10px 20px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.reset-btn:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
.preview-area {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #9ca3af;
|
||||
}
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.preview-cards {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.preview-card {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
font-size: 12px;
|
||||
}
|
||||
.preview-card .card-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #4b5563;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-panel">
|
||||
<button class="close-btn" onclick="parent.ChannelSettings?.closePanel()">×</button>
|
||||
|
||||
<h2>⚙️ 频道设置</h2>
|
||||
|
||||
<!-- 布局切换 -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-title">📐 布局模式</div>
|
||||
<div class="layout-options" id="layout-options">
|
||||
<button class="layout-btn" data-layout="grid">网格视图</button>
|
||||
<button class="layout-btn" data-layout="list">列表视图</button>
|
||||
<button class="layout-btn" data-layout="compact">紧凑视图</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主题色切换 -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-title">🎨 主题颜色</div>
|
||||
<div class="theme-options" id="theme-options">
|
||||
<button class="theme-btn theme-default" data-theme="default" title="默认蓝"></button>
|
||||
<button class="theme-btn theme-ocean" data-theme="ocean" title="海洋"></button>
|
||||
<button class="theme-btn theme-forest" data-theme="forest" title="森林"></button>
|
||||
<button class="theme-btn theme-sunset" data-theme="sunset" title="日落"></button>
|
||||
<button class="theme-btn theme-lavender" data-theme="lavender" title="薰衣草"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览区域(实时显示效果) -->
|
||||
<div class="preview-area">
|
||||
<div class="preview-title">实时预览</div>
|
||||
<div class="preview-cards">
|
||||
<div class="preview-card">
|
||||
<div class="card-title">工单管理</div>
|
||||
<div>待处理: 3</div>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<div class="card-title">数据统计</div>
|
||||
<div>完成率: 75%</div>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<div class="card-title">组件库</div>
|
||||
<div>24个组件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 恢复默认 -->
|
||||
<div class="settings-section">
|
||||
<button class="reset-btn" id="reset-defaults">恢复默认设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 这个脚本在 iframe 内运行,通过 postMessage 与父页面通信
|
||||
(function() {
|
||||
// 发送消息到父页面
|
||||
function sendMessage(type, data) {
|
||||
window.parent.postMessage({ type, data }, '*');
|
||||
}
|
||||
|
||||
// 请求当前设置
|
||||
sendMessage('settings:request', {});
|
||||
|
||||
// 监听来自父页面的设置更新
|
||||
window.addEventListener('message', function(event) {
|
||||
const data = event.data;
|
||||
if (!data || !data.type) return;
|
||||
|
||||
if (data.type === 'settings:update') {
|
||||
applySettings(data.settings);
|
||||
} else if (data.type === 'settings:active') {
|
||||
// 标记当前激活的选项
|
||||
markActive(data.settings);
|
||||
}
|
||||
});
|
||||
|
||||
// 应用设置(更新预览区样式)
|
||||
function applySettings(settings) {
|
||||
// 这里可以通过 CSS 变量影响预览区颜色
|
||||
if (settings.theme && window.ChannelTheme) {
|
||||
// 实际上我们无法直接调用父页面的 ChannelTheme,但可以通过 postMessage 通知父页面
|
||||
sendMessage('theme:change', { theme: settings.theme });
|
||||
}
|
||||
|
||||
// 更新预览区布局样式(通过给预览卡片添加类)
|
||||
const previewCards = document.querySelector('.preview-cards');
|
||||
if (previewCards) {
|
||||
previewCards.className = 'preview-cards';
|
||||
if (settings.layout) {
|
||||
previewCards.classList.add(`preview-${settings.layout}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记当前激活的按钮
|
||||
function markActive(settings) {
|
||||
// 布局按钮
|
||||
document.querySelectorAll('.layout-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.layout === settings.layout) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 主题按钮
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.theme === settings.theme) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定点击事件
|
||||
document.querySelectorAll('.layout-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const layout = this.dataset.layout;
|
||||
sendMessage('layout:change', { layout });
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const theme = this.dataset.theme;
|
||||
sendMessage('theme:change', { theme });
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('reset-defaults').addEventListener('click', function() {
|
||||
if (confirm('确定要恢复所有默认设置吗?')) {
|
||||
sendMessage('settings:reset', {});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<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