From c31d79f1e2fa86284aebc18f08a6c4a726ab7a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 6 Aug 2026 10:12:36 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Plan=202=20=E2=80=94=20CustomerList=20?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E4=BA=91=E5=AE=A2=EF=BC=88=E9=87=87=E9=9B=86?= =?UTF-8?q?=20+=20TDD=20plan=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit phase0 采集云客「沟通」板块客户列表(wechat wechatdetail/1 + waBusiness/friendlist), dump raw_json 字段覆盖,产出列+交互+字段映射;写 Plan 2(分 channel tab 各 7 列 + 查看聊天记录→ChatTimeline),经 plan-reviewer 评审 Approved。 Co-Authored-By: Claude --- ...2026-08-06-yunke-customerlist-alignment.md | 662 ++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md diff --git a/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md b/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md new file mode 100644 index 0000000..f0ddafc --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-yunke-customerlist-alignment.md @@ -0,0 +1,662 @@ +# CustomerList 列结构 + 交互对齐云客 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把 CustomerList(客户列表)对齐云客「沟通」板块的客户列表——分 微信客户 / WhatsApp客户 两个 tab,各自复刻云客的 7 列布局与「查看聊天记录」交互,与已完成的 StaffList 同款模式。 + +**Architecture:** 纯前端改造(后端能力 Plan 1 已备齐:customer 筛选 `owner_account_id` / `user_id` / `add_time_since` / `last_chat_since` + `add_time` 列)。`CustomerList.vue` 从「统一 17 列 + channel radio」重构为「`el-tabs` 分 channel + 每个 channel 独立 7 列 + 操作列跳 ChatTimeline」(StaffList 同款)。复用 `readField` / `cellTimeText` / `yk-pair` / `yk-staffinfo` / drawer;新增 `fromType` 代码→文案映射。 + +**Tech Stack:** Vue3 + TS + Element Plus + Pinia(前端)。**无后端改动**。 + +**Spec:** `docs/superpowers/specs/2026-08-05-yunke-ui-alignment-design.md` +**参考(模式来源):** `docs/superpowers/plans/2026-08-05-yunke-stafflist-interaction.md`(Plan 1,已完成;CustomerList 照搬其 el-tabs 分 channel + 复合单元格 + drawer 模式) + +--- + +## 已采集事实(Playwright 验证,2026-08-06,crm2.yunkecn.com) + +**微信客户列表** `/cms/wechat/wechatlist/wechatdetail/1/{wxId}?ymdStart=0000-00-00&ymdEnd=YYYY-MM-DD`(title「微信客户」,tabs 微信客户/微信群): +- 列(7):客户微信(昵称 `friendWxNickname` + wxId `friendWxId`) / 数据来源(`fromType` 代码→文案,`1000014`→工作手机) / 客户备注名(`friendWxRemark`) / 最近沟通时间(`lastChatTime`) / 发送条数(`sendNum`) / 接收条数(`receiveNum`) / 操作 +- 交互:**仅操作列可点**——「查看聊天记录」→ 新 tab `/cms/wechat/detail/message/{ownerWxId}/{friendWxId}/1?lastChatTime=...`;「导出HTML聊天记录」→ **不复刻**(spec:不引入装饰按钮) +- 昵称 / 数字列**均不可点**(纯展示)—— 与 StaffList(数字可点 modal)不同,客户列表数字就是简单计数 +- 字段覆盖(9 行):6/7 全覆盖,`friendWxRemark` 2/9(多数空→显示 `--`);`fromType` 取值 `1000014` / `14` / `15` + +**WhatsApp 客户列表** `/cms/wechat/waBusiness/friendlist`(title「客户列表」,tabs 好友列表/群列表): +- 列(7):序号 / 员工信息(`salesNickName` + `salePhone`) / 员工WhatsApp(`userName` + `uid`) / 客户WhatsApp(`nickName` + `phone`) / 添加时间(`createTime`) / 最新消息时间(`lastUpdate`) / 操作 +- 交互:**仅操作列可点**——「查看聊天记录」→ 新 tab 聊天 +- 字段覆盖(10 行):`salesNickName`/`salePhone`/`userName`/`uid`/`nickName`/`phone`/`createTime`/`lastUpdate` 齐全 +- 注:`createTime` 字符串 `"YYYY-MM-DD HH:mm"`,云客作「客户添加时间」显示(实现时用 `cellTimeText` 直显字符串;与 Plan 1 Task 2「createTime 是 owner-staff 字段、不填 add_time」不冲突——这里只是展示该列,不参与 add_time_since 筛选) + +**关键差异(决策依据):** yunkes 现状 CustomerList 是统一 17 列 + channel radio;云客是每 channel 极简 7 列且两 channel 列不同。按 spec「列对齐云客」+ Plan 1 StaffList 已验证的「分 tab + 各自独立列」模式,重构为分 tab(**用户已确认「严格对齐云客」方向**)。yunkes 现状其余字段保留在 drawer(buildCustomerSections 不变)。 + +**运行命令(验证用):** +- 前端:`cd yunkes && npm run dev` +- 前端类型:`cd yunkes && npx vue-tsc -b`(exit 0) +- 后端(验证不受影响):`cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" .venv/Scripts/python.exe -m uvicorn app.api.main:app --port 8000 --log-level warning` +- 后端测试:`cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q`(应仍 ≥330 绿;本 plan 无后端改动) + +--- + +## File Map + +**前端(yunkes/src/):** +- Modify `utils/customerFields.ts` — 新增 `fromTypeLabel()` helper(`sendNum`/`receiveNum` 走 raw_json fallback,**不改 `STRUCTURED_ALIAS`**:`Customer` 类型 / `CustomerOut` 未暴露 `send_num`/`receive_num`) +- Modify `views/CustomerList.vue` — 重构:统一 17 列 → `el-tabs` 分 channel(微信客户 / WhatsApp客户),各自 7 列 + 操作列 `viewChat` + `owner_account_id` query 预筛 + drawer 保留 + +**不改动:** 后端(Plan 1 已备齐)、router、`api/index.ts`(`getCustomers` 已支持所需参数)、`YkStaffDetailModal`(客户列表数字不可点,无 modal) + +--- + +## Task 1: customerFields.ts 扩展(fromTypeLabel helper) + +**Files:** +- Modify: `yunkes/src/utils/customerFields.ts` + +- [ ] **Step 1: 不修改 `STRUCTURED_ALIAS`**(`sendNum`/`receiveNum` 走 `readField` 的 raw_json fallback)。理由:`STRUCTURED_ALIAS` 类型是 `Record`,而 `Customer` 类型(`api/types.ts`)+ 后端 `CustomerOut`(`app/api/schemas.py`)都不暴露 `send_num`/`receive_num` 结构化列(DB 有列但未上 API),加 alias 会让 `vue-tsc -b` 报 `'"send_num"' is not assignable to type 'keyof Customer'`。`sendNum`/`receiveNum` 在 raw_json 以 camelCase 存在,`readField` 的 fallback 路径能拿到 —— 同 StaffList/AccountList 已验证模式(其 `STRUCTURED_ALIAS` 也无这俩条目)。**本 Task 仅做 Step 2。** + +- [ ] **Step 2: 新增 `fromTypeLabel` helper**(文件底部,`yesNoLabel` 下方): +```ts +/** + * 云客 `fromType` 代码 → 中文文案(用于「数据来源」列)。 + * 已知:1000014→工作手机。其余代码(如 '14'/'15')返回原值,避免隐藏; + * 如需补全,从云客界面按来源筛选采集后追加进 FROM_TYPE_LABELS。 + */ +const FROM_TYPE_LABELS: Record = { + '1000014': '工作手机', +} +export function fromTypeLabel(v: unknown): string { + if (v === null || v === undefined || v === '') return '--' + const s = String(v).trim() + return FROM_TYPE_LABELS[s] ?? s +} +``` + +- [ ] **Step 3: 类型检查** +```bash +cd yunkes && npx vue-tsc -b +``` +预期:exit 0。 + +- [ ] **Step 4: Commit** +```bash +git add yunkes/src/utils/customerFields.ts +git commit -m "feat(customerFields): add fromTypeLabel helper" +``` + +--- + +## Task 2: CustomerList.vue 重写(分 tab + 两 channel 7 列 + 操作列 + query 预筛) + +> 整文件重写(统一 17 列 → 分 tab 各自 7 列)。drawer 保留现状结构(avatar + buildCustomerSections + 查看聊天记录)。复用 StaffList 的 `tabState`/`loadTab`/`cellTimeText`/`yk-pair`/`yk-staffinfo` 模式。 + +**Files:** +- Modify: `yunkes/src/views/CustomerList.vue`(整文件替换为下方代码) + +- [ ] **Step 1: 用以下内容整体替换 `CustomerList.vue`** + +```vue + + + + + +``` + +- [ ] **Step 2: 类型检查** +```bash +cd yunkes && npx vue-tsc -b +``` +预期:exit 0。常见报错排查:`fromTypeLabel`/`readField` 未导出 → 确认 Task 1 已加;`Customer` 上无某字段 → 核对 `api/types.ts`;drawer 已用 `viewChatFromDrawer` wrapper 规避 `drawerCustomer: Customer | null` 的窄化问题,**不要**用 `!`。 + +- [ ] **Step 3: Commit** +```bash +git add yunkes/src/views/CustomerList.vue +git commit -m "feat(customers): align CustomerList to 云客 — per-channel tabs, 7 cols each, view-chat action" +``` + +--- + +## Task 3: 端到端浏览器验证 + 收尾 + +> 需前后端都跑起来。后端无改动,仅提供数据。 + +- [ ] **Step 1: 起后端**(新终端) +```bash +cd yunke-server && DB_URL="sqlite+aiosqlite:///./smoke.db" .venv/Scripts/python.exe -m uvicorn app.api.main:app --port 8000 --log-level warning +``` + +- [ ] **Step 2: 起前端** +```bash +cd yunkes && npm run dev +``` + +- [ ] **Step 3: 浏览器验证 `/customers`**(对照采集的云客截图) + - 微信客户 tab:7 列(客户微信[昵称+微信号] / 数据来源 / 客户备注名 / 最近沟通时间 / 发送条数 / 接收条数 / 操作),数据正确填充(非整列 `--`);数据来源列 `1000014` 显示「工作手机」。 + - WhatsApp客户 tab:7 列(序号 / 员工信息 / 员工WhatsApp / 客户WhatsApp / 添加时间 / 最新消息时间 / 操作),数据正确填充;添加时间/最新消息时间字符串正常显示。 + - 切 tab:数据各自加载,互不串。 + - 点「查看聊天记录」→ 跳 ChatTimeline 该客户会话。 + - row-click → drawer 全字段(含被表格精简掉的字段:性别/地区/描述/真实姓名/部门/销售/添加时间(wechat)/消息数 等)。 + +- [ ] **Step 4: 验证 StaffList → CustomerList 预筛联动** + - `/staff` → 微信员工 tab 点某员工「好友数」→ 跳 `/customers?owner_account_id={wxId}`,自动落在**微信客户** tab、仅该员工客户、顶部「员工账号」chip + 清除按钮。 + - `/staff` → WhatsApp员工 tab 点「好友列表」→ 跳 `/customers?owner_account_id={uid}`,落在**WhatsApp客户** tab + chip。 + - 点「清除员工筛选」→ chip 消失、数据恢复全量。 + +- [ ] **Step 5: 后端回归(确认无影响)** +```bash +cd yunke-server && .venv/Scripts/python.exe -m pytest tests/ -q +``` +预期:全 PASS(≥330;本 plan 无后端改动)。 + +- [ ] **Step 6: 截图留档**(微信客户 tab + WhatsApp客户 tab + drawer),更新本 plan 附录或 spec。 + +- [ ] **Step 7: Commit(如有微调)** +```bash +git add -A +git commit -m "test(customers): e2e verify CustomerList alignment vs 云客" +``` + +--- + +## 收尾验收清单 + +- [ ] `cd yunkes && npx vue-tsc -b` exit 0,无 console 报错。 +- [ ] `/customers` 两 tab 列名/顺序/操作按钮与云客一致(截图留档)。 +- [ ] 「查看聊天记录」行为与云客一致(跳 ChatTimeline)。 +- [ ] 数据正确填充(raw_json 读字段,无整列 `--`);字符串时间正确显示。 +- [ ] StaffList「好友数/好友列表」→ CustomerList 预筛 + chip + 清除,联动正确。 +- [ ] drawer 仍展示全字段(精简列未丢信息)。 +- [ ] 后端 pytest 仍 ≥330 绿(无后端改动)。 +- [ ] API 抽查(数据保障):`curl -s "http://localhost:8000/api/customers?channel=wechat&page=1&page_size=3"` 返回的 `raw_json` 关键字段(`friendWxNickname`/`fromType`/`sendNum`/`receiveNum`/`lastChatTime`)大多非空 —— 这是表格「非整列 `--`」的底层保障。 + +--- + +## 采集附录 / 已知缺口 + +- **fromType 文案映射不全**:已知 `1000014`→工作手机;`14`/`15` 显示原代码。如需补全,从云客界面按来源筛选采集后追加 `FROM_TYPE_LABELS`。不阻塞(fallback 不隐藏信息)。 +- **WA 客户 WhatsApp 列号码**:用 `phone`(如 `+233...`);云客显示纯号码(如 `584142462772`),`phone` 已是最佳映射。如要完全一致可改读 `friendUid` 去 `@s.whatsapp.net` 后缀,但 `phone` 更可读,保留。 +- **WA「添加时间」= createTime**:按云客显示(字符串直显)。语义上 createTime 严格说是记录创建时间,与 Plan 1「不把 createTime 当 add_time 填充结构化列」不冲突——此处仅展示,不参与筛选。 +- **不复刻**:导出HTML聊天记录按钮(spec:不引入装饰功能)。 + +--- + +## 后续 Plan(不在本 plan 范围) + +- Plan 3: GroupList / CallList / SmsList(GroupList 同样从云客 `/wechatdetail/2/{wxId}` 与 WA 群列表采集后对齐) +- Plan 4: ChatTimeline 会话列表 + `/api/sessions` 聚合端点 +- Plan 5: SearchView + AccountList 删除