Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
28 KiB
28 KiB
📡 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 上传(每个环节做完后执行)
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)
在终端执行(一整段复制粘贴):
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)
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)
cat << 'EOF' > /Users/chenshujun/Desktop/guanghulab/modules/m-channel/views/channel-dashboard.html
<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>平均加载</th>
<th>状态</th>
</tr>
</thead>
<tbody id="perfTableBody">
<tr><td colspan="5" style="text-align:center;color:#666;">暂无数据</td></tr>
</tbody>
</table>
</div>
<!-- 清除按钮 -->
<div class="dashboard-actions">
<button class="btn-clear" onclick="ChannelDashboard.clearData()">🗑️ 清除所有数据</button>
</div>
</div>
EOF
📋 Step 4 · 创建面板逻辑(channel-dashboard.js)
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 = '<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;
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 += '<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;
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 += '<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 (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 = '<tr><td colspan="5" style="text-align:center;color:#666;">暂无数据</td></tr>';
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 += '<tr>'
+ '<td>' + p.moduleId + '</td>'
+ '<td>' + p.visits + ' 次</td>'
+ '<td>' + minutes + ' 分钟</td>'
+ '<td class="' + statusClass + '">' + p.avgLoadTime + ' 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('📊 数据已清除,图表已重置');
}
}
};
})();
EOF
📋 Step 5 · 在路由器中注入采集钩子
需要在 channel-router.js 的路由切换函数中加入以下代码:
// ── 📊 数据采集钩子(环节8新增) ──
// 在路由切换时调用(放在模块加载逻辑之前):
if (typeof ChannelAnalytics !== 'undefined') {
ChannelAnalytics.markLoadStart();
ChannelAnalytics.recordVisit(moduleId);
}
// 在模块加载完成后调用:
if (typeof ChannelAnalytics !== 'undefined') {
ChannelAnalytics.markLoadEnd(moduleId);
}
在页面离开/关闭时结束计时(加到 channel-router.js 底部):
// ── 📊 页面关闭时保存停留数据 ──
window.addEventListener('beforeunload', function() {
if (typeof ChannelAnalytics !== 'undefined') {
ChannelAnalytics.endSession();
}
});
📋 Step 6 · 在导航中加入面板入口
在 index.html 的导航区域加入一个入口链接:
<!-- 📊 数据面板入口(环节8新增) -->
<a href="#/dashboard" class="nav-item" data-route="dashboard">📊 数据面板</a>
在路由注册中加入 dashboard 路由(channel-router.js 或 module-registry.js):
// 📊 数据面板路由
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 的 <head> 或底部引入新文件(3个):
<link rel="stylesheet" href="channel-dashboard.css">
<script src="channel-analytics.js"></script>
<script src="channel-dashboard.js"></script>
✅ 验收标准(二元制:全✅ = 通过 / 任一未通过 = 继续改)
| # | 测试项 | ✅ 通过标准 |
|---|---|---|
| 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 起算