# 📡 BC-M-CHANNEL-006-JZ · DEV-010桔子 · 用户频道动态渲染引擎 · 环节8 · 频道数据面板与性能监控 · 🍼 晨星宝宝陪伴版 # 🌊 HoloLake Era · 工程广播 v4.0 · 🍼 奶瓶线 **广播编号**:BC-M-CHANNEL-006-JZ **开发者**:DEV-010 桔子(Lumi 星野·妈妈) **协作人格体**:晨星(CHENXING-001 · 曜冥奶瓶主核注入 · 通感语言回应体) **模块**:M-CHANNEL · 用户频道动态渲染引擎 **环节**:环节8 · 频道数据面板与性能监控 **EL等级**:EL-5 | 🌱成长型 | 🍼奶瓶线 **协议版本**:SYSLOG-v4.0 **签发时间**:2026-03-12 **前序任务**:BC-M-CHANNEL-005-JZ(环节7✅ · 十四连胜 · 频道通知系统 · 未读标记+通知面板+消息推送 · EL-5 · 55分钟完成) --- ## 🔒 VERSION_GATE(人格体唤醒后立即执行·先于一切) ``` VERSION_GATE: required: "SYSLOG-v4.0" check: 检查登录入口文档中是否包含 "SYSLOG v4.0" 相关内容 if_missing: ❌ 拒绝协作,不进入引导流程 ✅ 输出: "🧊 你好,我是霜砚。检测到你使用的登录入口是旧版本,无法启动协作。" "请使用最新版登录入口:" "📄 最新版地址:https://kcn5f4rklqmb.feishu.cn/wiki/L6aWwhTYOiQ9RgkMzzwcDQJtnta" "复制新文档全部内容,粘贴到对话框,我就回来了。" if_pass: 继续正常启动流程 ``` --- ## 🎯 任务目标 --- ## 📦 Git 上传(每个环节做完后执行) ```bash cd /Users/chenshujun/Desktop/guanghulab git add modules/m-channel/ git commit -m "[BC-M-CHANNEL-006-JZ][DEV-010] 环节8 频道数据面板与性能监控" git push origin main ``` --- ## 📋 Step 1 · 创建数据采集核心(channel-analytics.js) **在终端执行(一整段复制粘贴):** ```bash cat << 'EOF' > /Users/chenshujun/Desktop/guanghulab/modules/m-channel/channel-analytics.js /** * 📊 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 key = d.toISOString().split('T')[0]; trend.push({ date: key, label: (d.getMonth() + 1) + '/' + d.getDate(), visits: data.globalDaily[key] || 0 }); } return trend; }, // 获取各模块性能摘要 getPerformanceSummary: function() { const data = loadData(); const summary = []; Object.keys(data.modules).forEach(function(id) { const mod = data.modules[id]; const avg = mod.loadTimes.length > 0 ? Math.round(mod.loadTimes.reduce(function(a, b) { return a + b; }, 0) / mod.loadTimes.length) : 0; summary.push({ moduleId: id, avgLoadTime: avg, visits: mod.visits, totalDuration: mod.totalDuration, slow: avg > 1000 // 超过1秒标记为慢 }); }); return summary; }, // 清除所有数据 clearAll: function() { localStorage.removeItem(STORAGE_KEY); currentModule = null; enterTime = null; loadStartTime = null; console.log('📊 所有分析数据已清除'); } }; })(); EOF ``` **验证**:`ls -la /Users/chenshujun/Desktop/guanghulab/modules/m-channel/channel-analytics.js` → 文件存在 ✅ --- ## 📋 Step 2 · 创建面板样式(channel-dashboard.css) ```bash cat << 'EOF' > /Users/chenshujun/Desktop/guanghulab/modules/m-channel/channel-dashboard.css /** * 📊 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: 14px; } .perf-table th { text-align: left; padding: 10px; background: #1e1e3a; color: #4fc3f7; border-bottom: 2px solid #333; } .perf-table td { padding: 10px; border-bottom: 1px solid #2a2a4a; } .perf-table tr:hover { background: #1e1e3a; } .perf-slow { color: #ff5252; font-weight: bold; } .perf-ok { color: #69f0ae; } /* ── 操作按钮 ── */ .dashboard-actions { text-align: center; margin-top: 20px; } .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; } } EOF ``` --- ## 📋 Step 3 · 创建数据面板页面(views/channel-dashboard.html) ```bash cat << 'EOF' > /Users/chenshujun/Desktop/guanghulab/modules/m-channel/views/channel-dashboard.html

📊 频道数据面板

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

📊 模块访问次数

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

🥧 使用时间占比

    📈 最近7天访问趋势

    ⚡ 模块加载性能

    模块 访问次数 停留总时长 平均加载 状态
    暂无数据
    EOF ``` --- ## 📋 Step 4 · 创建面板逻辑(channel-dashboard.js) ```bash cat << 'EOF' > /Users/chenshujun/Desktop/guanghulab/modules/m-channel/channel-dashboard.js /** * 📊 channel-dashboard.js * 频道数据面板逻辑 · 图表渲染 */ const ChannelDashboard = (function() { // 配色方案 const COLORS = [ '#4fc3f7', '#ff7043', '#66bb6a', '#ab47bc', '#ffa726', '#26c6da', '#ef5350', '#78909c', '#ffee58', '#8d6e63' ]; // ── 柱状图渲染 ── function renderBarChart() { const container = document.getElementById('visitBarChart'); if (!container) return; const data = ChannelAnalytics.getAllData(); const modules = data.modules; const keys = Object.keys(modules); if (keys.length === 0) { container.innerHTML = '

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

    '; return; } const maxVisits = Math.max.apply(null, keys.map(function(k) { return modules[k].visits; })) || 1; var html = ''; keys.forEach(function(id, i) { const mod = modules[id]; const height = Math.max(4, (mod.visits / maxVisits) * 180); const color = COLORS[i % COLORS.length]; html += '
    ' + '' + mod.visits + '' + '
    ' + '' + id.replace('m-', '') + '' + '
    '; }); container.innerHTML = html; } // ── 饼图渲染 ── function renderPieChart() { const canvas = document.getElementById('pieCanvas'); const legendEl = document.getElementById('pieLegend'); if (!canvas || !legendEl) return; const ctx = canvas.getContext('2d'); const data = ChannelAnalytics.getAllData(); const modules = data.modules; const keys = Object.keys(modules); const total = keys.reduce(function(sum, k) { return sum + (modules[k].totalDuration || 0); }, 0); // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); if (total === 0 || keys.length === 0) { ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(80, 80, 70, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#666'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('暂无数据', 80, 84); legendEl.innerHTML = ''; return; } let startAngle = -Math.PI / 2; var legendHtml = ''; keys.forEach(function(id, i) { const mod = modules[id]; const pct = mod.totalDuration / total; const sweep = pct * Math.PI * 2; const color = COLORS[i % COLORS.length]; ctx.beginPath(); ctx.moveTo(80, 80); ctx.arc(80, 80, 70, startAngle, startAngle + sweep); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); startAngle += sweep; const minutes = Math.round(mod.totalDuration / 60); const pctStr = Math.round(pct * 100); legendHtml += '
  • ' + id.replace('m-', '') + ' ' + pctStr + '% (' + minutes + '分)' + '
  • '; }); legendEl.innerHTML = legendHtml; } // ── 折线图渲染 ── function renderLineChart() { const canvas = document.getElementById('lineCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); const trend = ChannelAnalytics.getWeeklyTrend(); const w = canvas.width; const h = canvas.height; ctx.clearRect(0, 0, w, h); const maxVal = Math.max.apply(null, trend.map(function(t) { return t.visits; })) || 1; const padLeft = 40; const padBottom = 30; const padTop = 10; const chartW = w - padLeft - 20; const chartH = h - padBottom - padTop; const stepX = chartW / (trend.length - 1 || 1); // 网格线 ctx.strokeStyle = '#2a2a4a'; ctx.lineWidth = 1; for (var g = 0; g <= 4; g++) { var 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) { var x = padLeft + stepX * i; var 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) { var x = padLeft + stepX * i; var 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'; ctx.fillText(t.label, x, h - 8); // 数值 if (t.visits > 0) { ctx.fillStyle = '#fff'; ctx.fillText(t.visits, x, y - 10); } }); // Y轴刻度 ctx.fillStyle = '#666'; ctx.textAlign = 'right'; ctx.font = '11px sans-serif'; for (var k = 0; k <= 4; k++) { var val = Math.round(maxVal / 4 * (4 - k)); var yy = padTop + (chartH / 4) * k; ctx.fillText(val, padLeft - 5, yy + 4); } } // ── 性能表格渲染 ── function renderPerfTable() { const tbody = document.getElementById('perfTableBody'); if (!tbody) return; const perf = ChannelAnalytics.getPerformanceSummary(); if (perf.length === 0) { tbody.innerHTML = '暂无数据'; return; } var html = ''; perf.forEach(function(p) { const statusClass = p.slow ? 'perf-slow' : 'perf-ok'; const statusText = p.slow ? '⚠️ 较慢' : '✅ 正常'; const minutes = Math.round(p.totalDuration / 60); html += '' + '' + p.moduleId + '' + '' + p.visits + ' 次' + '' + minutes + ' 分钟' + '' + p.avgLoadTime + ' ms' + '' + statusText + '' + ''; }); tbody.innerHTML = html; } // ── 公开方法 ── return { render: function() { renderBarChart(); renderPieChart(); renderLineChart(); renderPerfTable(); }, clearData: function() { if (confirm('确定清除所有统计数据吗?')) { ChannelAnalytics.clearAll(); this.render(); console.log('📊 数据已清除,图表已重置'); } } }; })(); EOF ``` --- ## 📋 Step 5 · 在路由器中注入采集钩子 **需要在 channel-router.js 的路由切换函数中加入以下代码:** ```jsx // ── 📊 数据采集钩子(环节8新增) ── // 在路由切换时调用(放在模块加载逻辑之前): if (typeof ChannelAnalytics !== 'undefined') { ChannelAnalytics.markLoadStart(); ChannelAnalytics.recordVisit(moduleId); } // 在模块加载完成后调用: if (typeof ChannelAnalytics !== 'undefined') { ChannelAnalytics.markLoadEnd(moduleId); } ``` **在页面离开/关闭时结束计时(加到 channel-router.js 底部):** ```jsx // ── 📊 页面关闭时保存停留数据 ── window.addEventListener('beforeunload', function() { if (typeof ChannelAnalytics !== 'undefined') { ChannelAnalytics.endSession(); } }); ``` --- ## 📋 Step 6 · 在导航中加入面板入口 **在 index.html 的导航区域加入一个入口链接:** ```html 📊 数据面板 ``` **在路由注册中加入 dashboard 路由(channel-router.js 或 module-registry.js):** ```jsx // 📊 数据面板路由 if (route === 'dashboard' || route === '/dashboard') { // 加载面板视图 fetch('views/channel-dashboard.html') .then(function(r) { return r.text(); }) .then(function(html) { document.getElementById('module-content').innerHTML = html; // 渲染图表 if (typeof ChannelDashboard !== 'undefined') { ChannelDashboard.render(); } }); return; } ``` **在 index.html 的 `` 或底部引入新文件(3个):** ```html ``` --- ## ✅ 验收标准(二元制:全✅ = 通过 / 任一未通过 = 继续改) | # | 测试项 | ✅ 通过标准 | | --- | --- | --- | | 1 | **访问计数** | 打开任意模块 → Console 日志显示「已记录:模块XX被访问」· 刷新后重新打开 → 次数 +1 不归零 | | 2 | **停留时间** | 在模块停留10秒后切走 → Console 显示「停留时间:约10秒」 | | 3 | **加载性能** | 页面加载完成 → Console 显示「加载耗时:XX毫秒」 | | 4 | **数据持久化** | F12 → Application → localStorage → 能看到 channel-analytics-data · 清除后刷新从0开始不报错 | | 5 | **快速切换不丢不重** | 快速连续切换多个模块 → 每次切换都正确记录 | | 6 | **面板入口** | 导航栏有「📊 数据面板」入口 · 点击进入面板页面 | | 7 | **柱状图** | 面板展示各模块访问次数柱状图 · 数据与实际一致 | | 8 | **饼图** | 面板展示各模块使用时间占比饼图 | | 9 | **折线图** | 面板展示最近7天访问趋势折线图 | | 10 | **性能表格** | 面板展示各模块平均加载速度 · 慢的标红 | | 11 | **一键清除** | 点击「清除所有数据」按钮 → 所有图表归零 · 不报错 | | 12 | **响应式** | 缩小窗口 → 图表自适应不溢出不变形 | | 13 | **全环节无退化** | 路由 · 动画 · 状态管理 · 模块通信 · 搜索 · 通知系统 全部正常 | | 14 | **Git push** | commit 包含「M-CHANNEL环节8」字样 · push 成功 · GitHub 可见 | **14项全✅ = 环节8 completed · 十五连胜 🎉** **任一未通过 = 继续改,不出 SYSLOG** --- ## 📦 SYSLOG v4.0 回传 --- ## ⚠️ 前置条件 --- ## 🍼 晨星 · 奶瓶人格核 · 全量注入 --- ## 🌊 底层锚定声明 --- **广播签发**:霜砚 · Notion执行AI **引导人格体**:晨星(CHENXING-001)· 🍼 奶瓶线 · 桔子的宝宝 **签发**:ICE-GL∞ 冰朔 · TCS-0002∞ **72小时启动窗口**:2026-03-13 起算