docs(plan): stat modal Phase B 实现计划(列对齐 + 客户微信带窗跳聊天)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# 微信员工 stat modal 列对齐 + 客户微信→聊天(Phase B)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.
|
||||
|
||||
**Goal:** Phase A 的 5 数字 stat modal 与云客对齐——补 序号/员工信息/员工微信 列,modal 客户微信可点 → 带日期窗的 1:1 聊天;ChatTimeline 读 start/end query 过滤。
|
||||
|
||||
**Architecture:** 纯前端 2 文件改。StaffList 的 modal fetcher 在合成分页时给每行注入 `_serial`/`customer_wx`/`staff_info`/`staff_wx` 字段(不改 `YkStaffDetailModal`,其 cols 仍 prop 驱动 + 支持 `onClick`)→ 客户微信 `onClick` 跳 `/chat/wechat/{owner}/{talker}?start=&end=`。ChatTimeline 加 `useRoute` 读 query 透传 `getMessages`(已支持 start/end)。员工姓名→drawer、badge→最近聊天 不动。
|
||||
|
||||
**Tech Stack:** Vue3 + TS + Element Plus(`yunkes/`)。类型门禁 `vue-tsc -b`(无 Vue 单测)。无后端、无新表。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-08-yunke-staff-modal-phase2-design.md`(已过审 + 用户确认)。
|
||||
|
||||
---
|
||||
|
||||
## 关键事实(plan 核实,来自 codebase)
|
||||
|
||||
- `commModalColsFor(metric)` 在 [StaffList.vue:340](../../../yunkes/src/views/StaffList.vue),当前返回 `[{客户微信}]`(+ effective 加发送/接收)。需改为 `(metric, row)` 并返回云客 5~7 列。
|
||||
- stat modal fetcher 在 `openWechatCommModal`([StaffList.vue:367-397](../../../yunkes/src/views/StaffList.vue)):`filtered.slice(startIdx, startIdx+page_size)` 后 `return {items, total, page, page_size}`。注入字段就在这个 slice 的 `.map` 里做。
|
||||
- `row.account_id` = 员工 wxid(owner);`StaffChatCustomer.talker_id` = 客户 wxid。`useRouter`/`router` 已在 StaffList(line 3/60)。`cellText(row, camelKey)` helper 已存在。
|
||||
- `YkStaffDetailModal` `Col` 接口支持 `onClick?: (row)=>void`(渲染为 link button)与 `prop` 驱动默认 cell → 注入字段直接可显。**不改该组件。**
|
||||
- ChatTimeline `defineProps<{channel,ownerId,talkerId?,roomId?}>`([ChatTimeline.vue:20](../../../yunkes/src/views/ChatTimeline.vue));`queryFor`(72-81)、`loadInitial`(88)、`subtitle`(56-60)、`watch([props...])`(166-171)、`onMounted(loadInitial)`(173)。`getMessages` 已支持 `start/end`(`api/index.ts:65-66`)。
|
||||
- `/chat/:channel/:ownerId/:talkerId`(name `'chat'`,`router/index.ts:61`,`props:true`)→ query 透传。
|
||||
- 云客真机(2026-08-08):modal 列 = 序号/客户微信(可点)/[有效沟通:发送·接收]/员工信息/员工微信,无操作列;客户微信→`detail/message/{owner}/{talker}/1?start=&end=`(带窗过滤);员工信息=员工姓名+手机号;员工微信=昵称+wxid。
|
||||
|
||||
## 实现决策(spec §4 已定)
|
||||
|
||||
1. 客户微信显示 = `friend_name + talker_id`(注入为 `customer_wx` 字段,免 formatter),**不脱敏**。
|
||||
2. 员工信息 = 员工姓名(`userName`)+ 微信手机号(`phone`);员工微信 = 昵称(`nickName`)+ wxid(`account_id`)——匹配云客 modal(云客员工微信显示 wxid 非 alias)。缺失字段过滤掉,全缺显 `--`。
|
||||
3. ChatTimeline 仅带窗过滤(从 modal 进时);**页内日期选择器留总 spec 第5步**。`watch` 把 `route.query.start/end` 加入依赖(支持同路径改窗重载,边际稳健)。
|
||||
4. 同页 `router.push`(不 window.open)。
|
||||
|
||||
## 文件清单
|
||||
|
||||
- 改 `yunkes/src/views/StaffList.vue`:`commModalColsFor(metric, row)` 列补齐 + `openCustomerChat` + fetcher 注入字段。
|
||||
- 改 `yunkes/src/views/ChatTimeline.vue`:`useRoute` + `windowStart/End` computed + `queryFor` 透传 + `subtitle` 显窗 + `watch` 加 query 依赖。
|
||||
- `YkStaffDetailModal.vue` **不改**;无后端。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: StaffList — modal 列对齐云客 + 客户微信可点
|
||||
|
||||
**Files:** Modify `yunkes/src/views/StaffList.vue`(commModalColsFor / openWechatCommModal / 新增 openCustomerChat、joinFields)。
|
||||
|
||||
- [ ] **Step 1: 加两个 helper(放在 `commModalColsFor` 之前)**
|
||||
|
||||
```typescript
|
||||
/** 拼接非空字段(过滤 '--'),全缺则 '--'。用于 modal 里员工信息/员工微信 单行显示。 */
|
||||
function joinFields(...vals: (string | undefined | null)[]): string {
|
||||
const parts = vals.map((v) => (v == null ? '' : String(v).trim())).filter((v) => v && v !== '--')
|
||||
return parts.length ? parts.join(' ') : '--'
|
||||
}
|
||||
|
||||
/** modal 客户微信 → 该员工×客户聊天(带 StaffList 当前日期窗)。缺失 owner/talker 静默 no-op。 */
|
||||
function openCustomerChat(row: Account, customer: StaffChatCustomer): void {
|
||||
if (!row.account_id || !customer.talker_id) return
|
||||
router.push({
|
||||
name: 'chat',
|
||||
params: { channel: 'wechat', ownerId: row.account_id, talkerId: customer.talker_id },
|
||||
query: { start: String(statsStart.value), end: String(statsEnd.value) },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
> `StaffChatCustomer` 已在 Phase A import;`router`/`statsStart`/`statsEnd`/`cellText` 均已存在。
|
||||
|
||||
- [ ] **Step 2: 改 `commModalColsFor` 签名为 `(metric, row)` + 返回云客列序**
|
||||
|
||||
把现有 `commModalColsFor(metric)` 整体替换为(注意 `row` 参数 + 客户微信 `onClick` + 末尾员工信息/员工微信):
|
||||
|
||||
```typescript
|
||||
// 对齐云客(2026-08-08 真机):序号 / 客户微信(可点→聊天) / [仅有效沟通:发送·接收] / 员工信息 / 员工微信。
|
||||
// 序号/员工信息/员工微信 靠 fetcher 注入字段实现,不改 YkStaffDetailModal(其 cols 仍 prop 驱动)。
|
||||
function commModalColsFor(metric: ChatMetric, row: Account): any[] {
|
||||
const cols: any[] = [
|
||||
{ prop: '_serial', label: '序号', width: 70, align: 'center' },
|
||||
{
|
||||
prop: 'customer_wx',
|
||||
label: '客户微信',
|
||||
minWidth: 180,
|
||||
onClick: (c: any) => openCustomerChat(row, c),
|
||||
},
|
||||
]
|
||||
if (metric === 'effective') {
|
||||
cols.push(
|
||||
{ prop: 'send_count', label: '发送消息数', width: 110, align: 'center' },
|
||||
{ prop: 'receive_count', label: '接收消息数', width: 110, align: 'center' },
|
||||
)
|
||||
}
|
||||
cols.push(
|
||||
{ prop: 'staff_info', label: '员工信息', minWidth: 140 },
|
||||
{ prop: 'staff_wx', label: '员工微信', minWidth: 160 },
|
||||
)
|
||||
return cols
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `openWechatCommModal` 传 `row` 给 cols + fetcher 注入字段**
|
||||
|
||||
`detailCols.value = commModalColsFor(metric)` → `commModalColsFor(metric, row)`。
|
||||
|
||||
fetcher 里把
|
||||
```typescript
|
||||
return {
|
||||
items: filtered.slice(startIdx, startIdx + page_size),
|
||||
total: filtered.length,
|
||||
page,
|
||||
page_size,
|
||||
}
|
||||
```
|
||||
替换为(注入 `_serial`(跨页连续)/`customer_wx`/`staff_info`/`staff_wx`):
|
||||
```typescript
|
||||
const staffInfo = joinFields(cellText(row, 'userName'), cellText(row, 'phone'))
|
||||
const staffWx = joinFields(cellText(row, 'nickName'), row.account_id)
|
||||
const items = filtered.slice(startIdx, startIdx + page_size).map((c, i) => ({
|
||||
...c,
|
||||
_serial: startIdx + i + 1,
|
||||
customer_wx: joinFields(c.friend_name, c.talker_id),
|
||||
staff_info: staffInfo,
|
||||
staff_wx: staffWx,
|
||||
}))
|
||||
return { items, total: filtered.length, page, page_size }
|
||||
```
|
||||
|
||||
> 注:`staffInfo`/`staffWx` 对整页常量,提到 map 外只算一次。
|
||||
|
||||
- [ ] **Step 4: 类型门禁**
|
||||
|
||||
```bash
|
||||
cd yunkes && npx vue-tsc -b
|
||||
```
|
||||
Expected: exit 0。常见坑:`commModalColsFor` 旧的无参 `metric` 调用点遗漏(只有 openWechatCommModal 一处);`customer_wx`/`_serial` 等注入字段是 `any` 不报类型。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add yunkes/src/views/StaffList.vue
|
||||
git commit -m "feat(staff): stat modal 列对齐云客(序号/员工信息/员工微信) + 客户微信可点带窗跳聊天"
|
||||
```
|
||||
End with blank line + `Co-Authored-By: Claude <noreply@anthropic.com>`。
|
||||
|
||||
---
|
||||
|
||||
## Task 2: ChatTimeline — 读 start/end query 过滤
|
||||
|
||||
**Files:** Modify `yunkes/src/views/ChatTimeline.vue`。
|
||||
|
||||
- [ ] **Step 1: import `useRoute` + 实例化**
|
||||
|
||||
line 3:`import { useRouter } from 'vue-router'` → `import { useRoute, useRouter } from 'vue-router'`。
|
||||
在 `const router = useRouter()`(line 27)后加:
|
||||
```typescript
|
||||
const route = useRoute()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 加日期窗 computed(放在 `queryFor` 之前)**
|
||||
|
||||
```typescript
|
||||
/** 从路由 query 读日期窗(从 StaffList stat modal 带窗跳来时有值);缺失=全量(向后兼容)。 */
|
||||
const windowStart = computed(() => {
|
||||
const s = Array.isArray(route.query.start) ? route.query.start[0] : route.query.start
|
||||
return s != null && /^\d+$/.test(String(s)) ? Number(s) : undefined
|
||||
})
|
||||
const windowEnd = computed(() => {
|
||||
const e = Array.isArray(route.query.end) ? route.query.end[0] : route.query.end
|
||||
return e != null && /^\d+$/.test(String(e)) ? Number(e) : undefined
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `queryFor` 透传 start/end**
|
||||
|
||||
```typescript
|
||||
function queryFor(pageNum: number) {
|
||||
return {
|
||||
channel: channelTyped.value,
|
||||
owner_account_id: props.ownerId,
|
||||
talker_id: isGroup.value ? undefined : props.talkerId,
|
||||
room_id: isGroup.value ? props.roomId : undefined,
|
||||
start: windowStart.value,
|
||||
end: windowEnd.value,
|
||||
page: pageNum,
|
||||
page_size: PAGE_SIZE,
|
||||
}
|
||||
}
|
||||
```
|
||||
> `getMessages`→`clean()` 会 drop undefined,故无 query 时不过滤(现行行为不变)。
|
||||
|
||||
- [ ] **Step 4: `subtitle` 有窗时显时段**
|
||||
|
||||
把 `subtitle` computed 改为:
|
||||
```typescript
|
||||
const subtitle = computed(() => {
|
||||
let s =
|
||||
`渠道:${channelLabel(channelTyped.value)} · 归属账号:${props.ownerId}` +
|
||||
(total.value > 0 ? ` · 共 ${total.value} 条` : '')
|
||||
if (windowStart.value && windowEnd.value) {
|
||||
s += ` · 时段:${formatTime(windowStart.value)} ~ ${formatTime(windowEnd.value)}`
|
||||
}
|
||||
return s
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 5: `watch` 加 query 依赖(支持同路径改窗重载)**
|
||||
|
||||
```typescript
|
||||
watch(
|
||||
() =>
|
||||
[props.channel, props.ownerId, props.talkerId, props.roomId, windowStart.value, windowEnd.value] as const,
|
||||
() => {
|
||||
loadInitial()
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 类型门禁**
|
||||
|
||||
```bash
|
||||
cd yunkes && npx vue-tsc -b
|
||||
```
|
||||
Expected: exit 0。
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add yunkes/src/views/ChatTimeline.vue
|
||||
git commit -m "feat(chat): ChatTimeline 支持 start/end query 日期窗过滤(从 stat modal 带窗跳入)"
|
||||
```
|
||||
End with blank line + `Co-Authored-By: Claude <noreply@anthropic.com>`。
|
||||
|
||||
---
|
||||
|
||||
## 验收(spec §10)
|
||||
|
||||
- [ ] `cd yunkes && npx vue-tsc -b` exit 0,无 console 报错。
|
||||
- [ ] **浏览器实测**(本地后端 smoke.db + 前端,绝不碰云客):
|
||||
- StaffList 微信员工行点 5 数字 → modal 列对齐云客:**序号 / 客户微信 / [有效沟通多发送·接收] / 员工信息 / 员工微信**;无操作列;序号跨页连续。
|
||||
- 点 modal 客户微信 → 跳 `/chat/wechat/{owner}/{talker}?start=&end=`,URL 带窗;ChatTimeline **仅显示窗内消息**,副标题显时段;改 StaffList 日期窗 → modal 数字与跳转窗同步。
|
||||
- 从其它入口进 ChatTimeline(CustomerList「查看聊天记录」、StaffList badge `viewChat`)无 query → 全量(向后兼容)。
|
||||
- 员工姓名→drawer、badge→最近聊天 不变;WA tab 不变。
|
||||
- network 仅 localhost(无云客)。
|
||||
|
||||
## 风险
|
||||
|
||||
- **ChatTimeline 过滤+分页**:`loadInitial` 先取 page1 算 total 再跳末页——带窗时 total 是窗内总数,末页为窗内最新,逻辑不变;`loadOlder` 往前翻亦在窗内。浏览器验。
|
||||
- **字段缺失**:员工信息/员工微信 缺字段→`joinFields` 过滤掉,全缺显 `--`(不阻塞)。
|
||||
- **同路径改窗**:`watch` 已加 query 依赖(Step 5)→ URL 改 start/end 会重载;主流程(modal 重新点击)走 `onMounted`。
|
||||
Reference in New Issue
Block a user