# baoyu-dashboard-update — restructure guide

## ⚠️ Python批量替换陷阱（文件膨胀 — 2026-06-27 实战教训）

**致命症状**：用 `html.replace()` 替换大段HTML后，文件从57KB膨胀到1.7MB（30倍）。

**根因**：`html.find()` 边界不精确，替换时意外复制了大量重复内容。

**正确方法**：使用 patch 工具做多次精准小手术，每次替换一个精确子串。

**⚠️ 膨胀后立即回滚**：如果文件大小异常（正常 ~50-60KB），不要尝试修复，直接用备份覆盖：
```bash
cp dashboard.html.bak dashboard.html  # 或 .bak2
```
然后用 patch 工具从头开始，每次替换后检查文件大小。
- KPI 每张卡片单独 patch（~5次）
- allSalesData 整块替换（1次）
- chart labels/data 单独 patch（~3次）
- data3Y 整块替换（1次）
- month values 单独 patch（1次）
- 每次 patch 后验证文件大小正常（~50-60KB），异常则回滚


## 删除仪表盘模块的标准流程

当用户要求删除某个模块（如进度预警）时：
1. 删除HTML section（含表格、搜索框、图表div）
2. 删除JS数据数组（如 warnFullData, warnData）
3. 删除JS函数（sortWarn, renderWarnTable）+ 所有调用
4. 删除ECharts图表初始化代码（chartWarnBar = echarts.init...）
5. 删除resize中的对应 .resize() 调用
6. 用 `grep -c` 验证零残留
7. CSS类可以保留（不影响功能）

**⚠️ 正则删除陷阱**：删除代码时若使用 `re.DOTALL` 正则匹配 IIFE 块（如 `(function(){...chartWarnBar.setOption(...)})()`），正则可能误吞相邻的其他图表初始化代码。删除后必须验证：
- `grep -c "echarts\.init"` 统计图表初始化次数，应与HTML容器数一致
- `grep -n` 检查是否有孤立的 `})();` （IIFE闭合残骸）
- 特别检查 `chart-monthly`、`chart-year`、`chart-product`、`chart-gta` 等是否缺失初始化


## 新增Tab模块（含Chart.js图表+可筛表格）标准流程

当用户要求"分析X数据并整合到综合仪表盘"时，按以下流程操作：

### 0. 数据源检查（新增步骤）
**⚠️ 用户可能不是发送原始订单文件，而是发送已聚合的汇总文件。**
检查文件列头：如果只有「区域」「部门」「2026年1-5月」「2025年1-5月」等聚合列而没有逐行的「单据编号」「合同日期」等，说明这是**汇总文件**而非原始订单。
- 汇总文件：直接读取聚合值，不需要去重和分类
- 原始订单：需要按单据编号去重 + 按单据类型/合同编号/是否赠品分类
- **两种文件不能混用**：汇总文件的数据已经是最终值，不要再做二次聚合

### 1. 数据提取
- 从Excel提取 → 聚合去重 → 保存为 JSON (`/tmp/yoy_*.json`)
- 确认列索引：用户可能重新组织过Excel列结构（新增/移位列），**每次必须重新检查列头**
- 例如：旧版Col4=销售员, Col23=区域# → 新版Col4=区域, Col5=销售员, Col24=区域#

### 2. 生成嵌入式JS数据
- 用Python脚本将JSON转为 `const YR2_CUSTS = [...]` 格式的JS数组
- 客户名称含中文特殊字符时用 `json.dumps(ensure_ascii=False)` 输出
- 图表需要聚合数据时，同时生成 `const YR2_REGIONS = {...}` 汇总对象

### 3. HTML注入
- 添加快捷Tab按钮：`<button class="tab" onclick="switchTab('yoyregion')">`
- 添加panel HTML（KPI卡片 + Chart.js canvas + 筛选器 + 表格）
- 在 `switchTab()` 中添加 `if(t==='yoyregion') initYR();`
- 将JS数据+函数块插入 `</script>` 之前

### 4. JS函数命名
- 使用**带版本后缀的函数名**避免与现有函数冲突：`initYR()`, `renderYR()`, `yrSort2()`, `yr2Data`
- 变量也加后缀：`yr2SortCol`, `yr2SortDir`, `yr2Chart`

### 5. ⚠️ 清理旧代码块（关键陷阱）
- 当替换/重建Tab时，旧的数据块和函数定义**必须完全删除**
- **python脚本找边界**：用 `html.find('const YR_DATA = [')` 和 `html.find('const YR2_REGIONS')` 定位块边界
- 验证：`grep -c "function initYR("` 应为1（只有一个定义）
- 验证：`grep -c "YR2_REGIONS"` 应为2（定义+引用各一次）

### 6. 打开验证
- `open` 打开HTML，切换到新Tab
- 检查：KPI卡片渲染、Chart.js图表显示、表格筛选/排序、底部汇总行
- 如果页面空白：F12打开Console检查JS错误
- 常见JS错误：`Uncaught ReferenceError: YR_DATA is not defined` → 旧函数引用了已删除的数据变量
- 常见JS错误：`Uncaught TypeError: Cannot read properties of null` → canvas元素在panel隐藏时被初始化（panel需先display:block再init chart）

### 7. Chart.js初始化顺序
- `switchTab()` 中先设置 `panel.classList.add('active')`（使canvas容器可见）
- 再调用 `initYR()` 创建图表
- 图表容器必须有明确的 `height`（如 `style="height:380px"`），否则Chart.js无法计算尺寸

### 8. Python批量修改HTML模式（推荐首选方案）

**⚠️ 当添加全新Tab时，不要使用patch工具逐段修改。改用Python脚本一次性完成所有插入。**

**⚠️ 2025-06-13 实战教训：即使使用Python批量修改，在已有仪表盘上新增含大量嵌入数据（200+条记录）的Tab仍然极易失败。** 失败模式包括：旧数据块残留（`const YR_DATA` 仍有引用）、函数重复定义（新旧 `initYR()` 覆盖）、括号失衡（`]` 比 `[` 多2个）、以及最隐蔽的——switchTab的标签匹配逻辑依赖 `textContent.includes(tabName)` 而非精确匹配，可能导致Tab高亮错乱。

**当Tab集成尝试2次以上仍失败时，直接输出独立Excel文件**（参考 `baoyu-customer-analysis` skill §15）。Excel输出零风险、立即可用，用户用筛选器替代交互式表格功能。后续有需要时再从Excel反哺到HTML。

**Excel输出模板**（`baoyu-dashboard-update` 独有）：
- Sheet1「区域汇总」：区域 | 部门 | 2025 | 2026 | 同比% | 客户数 | 趋势 + 柱状图
- Sheet2「客户明细」：客户 | 区域 | 2025 | 2026 | 变化 | 变化% | 状态 + 自动筛选 + 冻结表头
- Sheet3「说明」：数据范围、去重逻辑、区域分类
- 色标：绿(C6EFCE)=增长/新增，红(FFC7CE)=下降/流失
- 字体：Consolas数字列右对齐，微软雅黑表头白字蓝底

**从Excel反哺到HTML的最小可行方案**（避免大块JSON嵌入导致的JS错误）：
- 不嵌入完整客户明细（200+条记录极易引入语法错误）
- 仅嵌入区域级汇总数据（14-23条），用简单sortable table展示
- 不依赖Chart.js（避免canvas尺寸/初始化顺序问题）
- Tab代码总量控制在50行JS + 30行HTML以内
- 新增变量/函数使用独立命名空间（`initYOY`, `sortYOY`, `ySort` 等）避免与现有冲突

正确流程（原子操作）：
```python
with open(html_path, 'r') as f: html = f.read()

# 1. 添加Tab按钮
html = html.replace('OLD_TAB_LINE', 'OLD_TAB_LINE\n  <NEW_TAB_BUTTON>')

# 2. 添加Panel HTML（在 </div>\n<div class="toast" 前插入）
panel_marker = '</div>\n<div class="toast"'
html = html.replace(panel_marker, NEW_PANEL_HTML + '\n' + panel_marker)

# 3. 添加 switchTab 分支
html = html.replace(
    "if(t==='kpi') renderKpiTable();\n}",
    "if(t==='kpi') renderKpiTable();\n  if(t==='newtab') initNewTab();\n}"
)

# 4. 嵌入JS数据+函数（在 </script> 之前）
html = html.replace('\n</script>\n</body>', JS_BLOCK + '\n</script>\n</body>')

# 5. 验证后一次性写回
with open(html_path, 'w') as f: f.write(html)
```

**验证清单（写回后立即执行）**：
```python
# 函数定义唯一性
assert html.count('function initNewTab(') == 1, "Duplicate function!"
# 数据变量引用次数（定义+使用，通常>=2）
assert html.count('NEW_DATA_VAR') >= 2, "Data not referenced!"
# 括号平衡（粗略检查）
assert html.count('{') == html.count('}'), "Unbalanced braces!"
# canvas ID 在 HTML 和 JS 中同时存在
assert 'id="newChart"' in html and "getElementById('newChart')" in html
# ⚠️ 最关键：div标签平衡（2025-06-13 致命教训）
import re
div_opens = len(re.findall(r'<div\b', html))
div_closes = len(re.findall(r'</div>', html))
assert div_opens == div_closes, f"<div> mismatch: {div_opens} vs {div_closes}! DOM broken!"
```

**⚠️ HTML div失衡是最隐蔽的致命Bug**（2025-06-13 实战教训）：
- 症状：仪表盘所有Tab看起来正常但数据不显示、按钮点不动、图表空白
- 根因：多次patch操作后，`.container` 闭合 `</div>` 意外丢失，导致所有panel嵌套在错误层级
- 浏览器不会报JS错误——只是DOM结构错乱，JavaScript的 `getElementById` 找到的是错误层级的元素
- 检测方法：`re.findall(r'<div\b', html)` vs `re.findall(r'</div>', html)` 必须相等
- 修复方法：找到 `.container` 闭合位置，确保 `</div>` 出现在 `<div class="toast">` 之前
- 预防：每次修改HTML后都执行div平衡检查，不依赖肉眼审查

**重建Tab时的清理流程**：
当替换已有Tab时，必须先完全清除旧代码再插入新代码：
1. 删除旧Tab按钮行
2. 删除旧Panel HTML（从 `<!-- ═══ 旧Tab名称 ═══ -->` 到下一个 `</div>` 结束）
3. 删除旧JS数据块（`const OLD_DATA = [...]` 到旧函数定义末尾）
4. 删除 `switchTab` 中旧分支
5. 验证旧变量名零残留：`grep -c "OLD_VAR_NAME" dashboard.html` 应为0
6. 然后再执行上面的4步插入流程
