# baoyu-workbench-frontend-pitfalls — frontend ui

## 109. Tab 分组 + 移动端底栏精简 + AI 页移动端布局（2026-09-19）

用户反馈「Tab 太多」「手机端 AI 页布局不合理」。TABS 每项加 `group`（biz/cust/mine/ai）→ 侧栏按组渲染；移动端底栏只留 4 高频（看板/客户/待办/AI）+「更多」底部抽屉；AI 页移动端把四步闭环+快捷任务收进 `⚙` 折叠区。

**四个坑（实测踩过）**：
1. **移动端 CSS 必须追加在样式表末尾** —— 插进已有 `@media` 块会被**后面出现的基础规则**覆盖（同特异性、后者胜），导致 ⚙ 按钮 `display:none` 失效。
2. **两个断点都要改** —— index.html 用 768px、app.html 用 640px；窄屏下 640 块会覆盖 768 块的同名规则，只改一个无效。
3. **AI 页高度不能只按 `--header-h` 算** —— 实测底栏 64px、顶栏 52px，用 `calc(100dvh - var(--header-h) - 84px)`；验证要断言 `chatInput.bottom <= bottomNav.top`。
4. **按钮图标化时 emoji 要留在 span 外面**：`📋<span class="hbtn-t"> 复制对话</span>`；emoji 若一起包进 span，隐藏后按钮变**空框**。替换锚点要带 `style=`/`title=` 等唯一上下文，否则会命中**注释或弹窗标题**（踩过）。

补丁脚本 `脚本/patch_workbench_tabs_mobile.py`（幂等，含 `TABGROUP_V1` 标记）。



## 107. 客户tab搜索框被侧栏遮挡（2026-09-11）

`#custToolbar` 缺 `margin-left: var(--sidebar-w)`，左 230px 压在 fixed 侧栏下，搜索框左缘点不到。修：补 margin-left + **每个断点显式 `margin-left:0`**（app.html 的 @640 不重置 --sidebar-w，只靠变量会留空档）。**详见 `references/customer-toolbar-occlusion.md`**。



## 106. 搜索框打断中文输入法（2026-09-11）

输入中文到一半整页刷新、候选词消失、焦点丢失：`oninput` 防抖调整页 `render()`，`innerHTML` 重建销毁了 `<input>` 本身 → 输入法组合态中断。修法三段式：①组合期（compositionstart→end）不重渲染，`ev.isComposing` 双保险；②组合事件必须用 `document.addEventListener(..., true)` 委托——inline `oncompositionstart` 在部分内核 property 未编译、根本不触发；③只局部重绘表格容器（`#orderTableWrap`），input 节点存活。**详见 `references/search-input-ime.md`**。



## 102. 看板「环比」基准月写死6月，H2月份环比全错（2026-09-05）

**症状**：看板「环比8月」标签，实际用 6月数据（`h1.total.actuals[5]`）做基准，导致 H2 月份（8月起）环比基准错误（7月碰巧对，因环比正好是6月）。

**根因**：`getKPI()` 里 `var last = (h1.total.actuals[5]) || 0` 写死取 6 月（H1 最后一个月）。非 admin 分支同病（`last: monthlyActuals[5]`）。

**修复**：环比基准动态取上月——上月 ≤6 用 `h1.total.actuals[上月-1]`，上月 ≥7 用 `threeYearMonthly['2026'][上月]`。admin/非admin 两个分支 + `index.html`/`app.html` 两个文件都要改（4处）。

**教训**：任何「环比/月度对比」基准月必须动态算上月，禁止写死固定月份索引（如 `actuals[5]`=6月）。



## 2. 函数重复声明 → 页面永久加载中

症状：spinner 永不消失。`grep -n "function refreshData" page.html`。



## 72. 看板客户数=0——admin 被当普通用户过滤（2026-07-28）

**症状**：管理员登录后看板「客户数」和「联系人」显示 0。

**根因**：`renderDashboard` 中 `mcpFiltered` 按 `userName` 过滤客户，未检查 admin 角色。若 admin 名不在任何客户的 owner/operators 中 → 过滤后为 0。

```javascript
// ❌ admin 也被过滤
var mcpFiltered = userName ? mcpAll.filter(function(c){...}) : mcpAll;

// ✅ admin 看全部
var isAdmin = (state.user || {}).role === 'admin';
var mcpFiltered = isAdmin ? mcpAll : (userName ? mcpAll.filter(...) : mcpAll);
```

**修复**：加 `isAdmin` 判断。联系人计数也受影响（同一行代码）。此 bug 是 #37（数据隔离）的反面——隔离逻辑正确但忘了给 admin 开路。

### 第三种「客户数0」根因：数据文件缺 current_status_operator 字段（2026-08-30）

用户报「客户数 0」时先区分三种根因：① 新人真没客户（正常，别急着排查）；② admin 过滤 bug（本条 #72）；③ **`mcp_customers.json` 数据文件整体缺 `current_status_operator` 字段**（用旧版 `pull_customers.py` 的 COLUMNS 拉的，MQL 里根本没 SELECT 这个字段），导致 `adaptMcpFields` 的 `owner=''`/`operators=[]`，**所有非 admin 用户**客户数全 0——不是只有那一个人。

**判别**（一行命令查字段列表）：
```bash
python3 -c "import json;d=json.load(open('数据/mcp_customers.json'));print(list(d['customers'][0].keys()))"
# 字段列表没有 current_status_operator / owner → 就是它；同时看其他非 admin 用户是否也全 0（系统性数据问题，非单用户）
```

**修复**：重跑当前版 `pull_customers.py`（COLUMNS 已含 `current_status_operator`，MQL 实测返回 `user_value_list`）+ `tcb hosting deploy 数据/mcp_customers.json`。



## 51. `fullYearTax` 从 `shipments.full_year_tax` 取值（值为 0）导致看板累计达成始终 0

**症状**：订单管理"本年"税合计正确（¥335 万），但看板顶部"累计达成"始终 0。`kpi.ytd` 和 `orders.reduce` 都已正确赋值，但看板仍显示 0。

**根因**：`renderDashboard` 中 line ~882 有一行旧代码幸存：
```javascript
var fullYearTax = shipments.full_year_tax || 0;  // ← 值始终为 0
```
备份恢复后这行变回了 `shipments.full_year_tax` 版本（而非 `kpi.ytd` 或 `orders.reduce` 版本）。后续多次修复都没能覆盖这一行，因为所有 grep 和 replace 都针对的是其他位置。

**检测**：
```bash
grep -n "fullYearTax" 工具/index.html | grep "shipments"
# 所有出现都必须审查——renderDashboard、getShipmentOrders、renderOrders 各有定义
```

**修复**：替换为 orders 实时汇总（orders 已在同函数中按 creator 过滤）：
```javascript
var fullYearTax = orders.reduce(function(s,o){return s+(parseFloat(o.taxTotal||o.amount||0)||0)},0);
```



## 42. `var function` 语法错误——整个页面 JS 崩溃（2026-07-27）

**症状**：登录页下拉框展开后完全空白（无用户列表），控制台报 `SyntaxError: Unexpected token 'function'`。所有 JS 均不执行。

**根因**：用 Python 脚本替换 `FEISHU_ACTIVITY_HOME = '...'` 为 `function getFeishuActivityUrl() { ... }` 时，旧代码中有 `var` 前缀残留在新函数声明前，产生 `var function getFeishuActivityUrl()`。

**检测**：
```bash
grep -n "var function" 工具/index.html  # 应无输出
node --check <extracted_script>.js       # 会直接报 SyntaxError
```

**铁律**：替换 `var X = value` 为 `function X()` 时，**必须同时删除 `var`**。替换后立即 `node --check` 验 JS。

**症状**：`shipment_status.json` 的 orders 中没有 `taxTotal`/`amount` 字段，前端看板价税合计始终 0。

**根因**：`shipment_pipeline.py` 的 `process_orders()` 输出 dict（约 line 159-185）只包含订单元数据 + 物流轨迹，**完全没有映射 `金额`/`价税合计` 字段**。即使 `pull_orders.py` 正确拉取了 `field_51d592`，数据在 pipeline 中被丢弃。

**完整数据流**：
```
pull_orders.py → mcp_orders_input.json (含 '金额' 字段)
  → shipment_pipeline.py 读取 → process_orders() 构建输出 dict
  → 必须显式映射: "taxTotal": o.get("金额", "")
  → shipment_status.json → 前端 renderDashboard / renderOrders
```

**修复**：在 `process_orders()` 的输出 dict 中加：
```python
"amount": o.get("金额", ""),
"taxTotal": o.get("金额", ""),
```

**验证**：
```bash
python3 -c "import json;d=json.load(open('数据/shipment_status.json'));\
print(sum(float(o.get('taxTotal') or 0) for o in d['orders']))"
```

### 看板 KPI 隔离
看板顶部 4 个 KPI 卡片（客户数/联系人/价税合计/本月价税）需与订单管理同口径——按 creator 过滤 orders 并实时汇总 `taxTotal`：

```javascript
// renderDashboard: orders 按 creator 过滤（与订单管理一致）
var userName = (state.user || {}).name;
if (userName && ((state.user || {}).role !== 'admin')) {
  orders = orders.filter(function(o) { return o.creator === userName; });
}
// 价税合计：从 orders 实时汇总（MCP 口径，与订单管理同步）
var fullYearTax = orders.reduce(function(s,o){return s+(parseFloat(o.taxTotal||o.amount||0)||0)},0);
// 客户/联系人：也按 creator 过滤 MCP 客户
var mcpFiltered = userName ? mcpAll.filter(function(c){return c.owner===userName||(c.operators||[]).indexOf(userName)>=0;}) : mcpAll;
```

**不要用 `kpi.ytd`（ERP 口径）做价税合计**——ERP 与 MCP 口径不同，数值不一致会让用户困惑。看板所有数据必须与订单管理同一来源。

### 非 Admin Tab 权限
看板默认对非 admin 隐藏，由 `tabs.dashboard` 控制。需在 `finishLogin` 中设 `tabs: {dashboard: true}`。

### 硬编码认证绕过 CloudBase 竞态
特定用户（赵云浩/张立娅）在 `doLogin()` 开头硬编码密码验证 + personnel 文件状态校验，完全跳过 CloudBase DB：

```javascript
if ((name === '赵云浩' || name === '张立娅') && pwd === 'biori2026') {
  return fetch('../数据/pricing/_personnel.json')
    .then(function(r) { return r.json(); })
    .then(function(data) {
      var found = (data.personnel || []).filter(function(p) { return p.name === name && p.status === '在职'; })[0];
      if (!found) throw new Error('人员状态校验失败');
      finishLogin({name: found.name, role: found.role || 'sales', dept: found.dept || '', email: found.email || '', tabs: {dashboard: true}});
    });
}
```

- `references/sf-delivery-pipeline.md` — 顺丰物流推送完整链路（pull_orders / shipment_pipeline / watchdog / 飞书卡片）
- `references/batch-reapply-from-backup.md` — 从备份干净重打补丁模板（逐 replace + 括号验）
