Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

28 KiB
Raw Blame History

📡 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 起算