Files
yunke/docs/superpowers/plans/2026-08-07-yunke-plan4b-calllist-ui.md
T
2026-08-19 16:48:22 +08:00

343 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan 4b: CallList UI 对齐(tab + 列结构)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:** 把 `CallList.vue` 从"单 radio 筛选 + 通用 8 列"改成对齐云客语音通话列表的 **tab 结构(微信语音 / WA 语音 / 电话)+ 各 tab 云客列结构**,纯前端,用 Plan 4a 抓取的真实数据(voice_call 微信 / wa_voice_call WA / phone_call 电话)。
**Architecture:** `CallList.vue` 用 `el-tabs`(微信语音/WA 语音/电话),tab 切换改 `getCalls` 的 `channel` 参数;每 tab 用对齐云客的列集(`el-table-column` 按 tab 渲染,复用现有 `cell()`/`readField()` 读 `raw_json` 字段 + 录音播放 dialog + 详情 drawer)。后端零改动(`/api/calls` 已支持 channel 筛选,`wa_voice_call` 是新 channel 值,PhoneCallOut 含 raw_json)。
**Tech Stack:** Vue3 + TS + Element Plus(纯前端)。
**Spec/事实依据:**
- `docs/superpowers/specs/2026-08-06-yunke-comm-operations-audit.md` §3(微信 voice-call-list 列)+ §6(WA wavoicecalllist 列)
- **Plan 4a 真实数据**:smoke.db `phone_call` 表含 `channel='voice_call'`(微信 2193)+ `channel='wa_voice_call'`(WA 608)+ `channel='phone_call'`(电话)。raw_json 含云客原字段(`userWeChatNickName`/`userWeChatAlias`/`userPhone`/`talker`/`talkerNickName`/`talkerAlias`/`ossFileName`/`duration`/`startTime`/`endTime`/`weChatId`/`callFileId`/`isRoom`/`callType`/`isSend`/`callStatus`/`from`)。
- **CallList.vue 现状**(402 行):`callType` radio(all/phone_call/voice_call)+ 8 列(时间/员工/号码/方向/类型/时长/状态/录音)+ playRecord dialog + openDrawer + `cell(row, camelKey)`→`readField` 读 raw_json。
- **后端 `/api/calls`**:`channel` 参数直筛 `PhoneCall.channel`;PhoneCallOut `from_attributes` 含 raw_json。前端 `getCalls({channel, page, page_size})`。
---
## 范围
**本 plan 覆盖(4b 纯前端 tab+列)**:
- `CallList.vue` 改 `el-tabs`(微信语音 / WA 语音 / 电话),tab→channel 映射(`voice_call` / `wa_voice_call` / `phone_call`)。
- 各 tab 对齐云客列结构:
- 微信语音 tab:通话信息 / 客户信息 / 员工微信 / 员工信息 / 通话录音 / 通话时长(audit §3)
- WA 语音 tab:状态 / 客户信息 / 员工信息 / 通话时间和录音(audit §6)
- 电话 tab:保留现有通用列(电话非云客沟通板块,不强对齐)
- 复用录音播放 dialog + 详情 drawer。
**不在本 plan 范围(留 4c)**:
- 长语音列表 modal(`/wechatweb/call/list` 抓取 + voice_clip 表 + 编辑/手动匹配 UI)。
- 统计卡片(todayStatistics)、录音异常检查(`/call/check/{id}` 代理)、批量语音下载/全部导出/高级筛选工具栏。
- 微信好友/微信群二级 tab(isRoom)—— 本 plan 微信语音 tab 混合展示好友+群通话;若需分开,4c 加 client-side `raw_json.isRoom` 筛选。
- 列内可点元素(客户信息→聊天等)—— audit 标注采信,4c 随二级操作。
### 字段映射(云客列 → yunkes PhoneCall/raw_json)
| 云客列 | 子字段 | yunkes 来源 |
|---|---|---|
| 通话信息 | 通话状态 / 通话类型 / 通话来源 | `call_status`(结构化)/ `channel`(voice_call/wa_voice_call)/ `raw_json.from` |
| 客户信息 | 昵称 / 号 | `plan_customer_name`(←talkerNickName)/ `call_number`(←talker) |
| 员工微信 | 昵称 / 号 | `raw_json.userWeChatNickName` / `raw_json.userWeChatAlias`(或 weChatId) |
| 员工信息 | 姓名 / 手机 | `user_id_name` / `raw_json.userPhone` |
| 状态(WA)| 通话状态 + 时长 | `call_status` / `call_duration` |
| 通话录音 | 时长 / 播放 | `call_duration`(或 raw_json.duration)/ `record_file`(←ossFileName)→ playRecord |
| 通话时间和录音 | 结束时间 + 录音 | `raw_json.endTime`(或 startTime)/ `record_file` |
| 通话时长 | 时长 | `call_duration` |
---
## 运行命令(验证用)
- 前端:`cd yunkes && npm run dev`
- 类型检查:`cd yunkes && npx vue-tsc -b`
- 数据确认(后端跑):`curl -s "http://localhost:8000/api/calls?channel=wa_voice_call&page_size=2" | head -c 400`(含 raw_json 字段)
---
## File Map
- Modify `yunkes/src/views/CallList.vue` — 改 el-tabs + 每 tab 列定义 + tab→channel
- Modify `yunkes/src/utils/callFields.ts` — `CALL_FIELD_LABELS` 补 WA/微信语音 raw_json 字段(userWeChatNickName/userWeChatAlias/userPhone/talker/talkerNickName/talkerAlias/weChatId/callFileId/from 等中文 label)
- Modify `yunkes/src/utils/format.ts` — 若 `callTypeLabel` 不识别 `'wa_voice_call'`,加映射(WA 语音通话)
---
## Task 1: CallList 改 el-tabs + tab→channel 映射
> 事实:CallList.vue 现用 `el-radio-group`(callType: 'all'/'phone_call'/'voice_call')+ `onTypeChange`。`getCalls({channel: callType, page, page_size})`。改用 `el-tabs` 三 tab(微信语音 voice_call / WA 语音 wa_voice_call / 电话 phone_call),tab 切换改 channel + 重新 load。
**Files:** Modify `yunkes/src/views/CallList.vue`
- [ ] **Step 1: callType 改 tab + 值**。把 `callType` ref 默认改 `'voice_call'`(微信语音优先,云客语音通话列表主入口)。`typeOptions` 改为 3 tab(去掉 'all',每 tab 对应一个 channel):
```ts
const callType = ref<CallChannel>('voice_call') // 默认微信语音
const tabs: { value: CallChannel; label: string }[] = [
{ value: 'voice_call', label: '微信语音' },
{ value: 'wa_voice_call', label: 'WA 语音' },
{ value: 'phone_call', label: '电话' },
]
```
> `CallChannel` 类型需加 `'wa_voice_call'`(见 Task 5 类型)。先确认 `@/api/types` 的 `CallChannel` 定义(现 `'phone_call' | 'voice_call'`),加 `'wa_voice_call'`。
- [ ] **Step 2: 模板 radio 改 el-tabs**(CallList.vue :134-144 的 `yk-calls__filters` 区):
```html
<el-tabs v-model="callType" class="yk-calls__tabs" @tab-change="onTabChange">
<el-tab-pane v-for="t in tabs" :key="t.value" :label="t.label" :name="t.value" />
</el-tabs>
```
```ts
function onTabChange(name: TabPaneName): void {
callType.value = name as CallChannel
page.value = 1
load()
}
```
> `TabPaneName` from 'element-plus'。移除 radio 相关:**header subline(:131 `类型:{{ typeLabelCurrent }}`)改用 `{{ tabs.find(t => t.value === callType)?.label }}`**,删 `typeOptions` + `typeLabelCurrent` computed(避免 vue-tsc 坏)。`onTabChange` 可简化为 `page.value=1; load()`(v-model 已更新 callType)。
- [ ] **Step 3: load() 用 channel**(:51-55 现已 `channel: callType.value`,保持;但不再传 'all'——每 tab 是具体 channel)。
- [ ] **Step 4: 类型检查**
```bash
cd yunkes && npx vue-tsc -b
```
- [ ] **Step 5: Commit**
```bash
git add yunkes/src/views/CallList.vue yunkes/src/api/types.ts
git commit -m "$(cat <<'EOF'
feat(calls): CallList 改 el-tabs(微信语音/WA 语音/电话)+ CallChannel 加 wa_voice_call
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
---
## Task 2: 微信语音 tab 列对齐云客
> 事实:CallList.vue 现 8 列固定。云客微信 voice-call-list 列(audit §3):通话信息 / 客户信息 / 员工微信 / 员工信息 / 通话录音 / 通话时长。字段读 raw_json(`cell(row, 'userWeChatNickName')` 等)。微信语音 tab(callType==='voice_call')用云客列,其他 tab 用各自列(Task 3/4)。
**Files:** Modify `yunkes/src/views/CallList.vue`
- [ ] **Step 1: 微信语音 tab 列**。在 el-table 内,微信语音列用 `v-if="callType === 'voice_call'"` 包一组 el-table-column:
```html
<template v-if="callType === 'voice_call'">
<el-table-column label="通话信息" min-width="150">
<template #default="{ row }">
<div>通话状态:{{ callStatusLabel(row.call_status) }}</div>
<div class="yk-cell__sub">类型:{{ callTypeLabel(row.channel) }}</div>
<div class="yk-cell__sub">来源:{{ cell(row, 'from') ?? '--' }}</div>
</template>
</el-table-column>
<el-table-column label="客户信息" min-width="150">
<template #default="{ row }">
<div>{{ row.plan_customer_name || cell(row, 'talkerNickName') || '--' }}</div>
<div class="yk-cell__sub">{{ row.call_number || cell(row, 'talker') || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="员工微信" min-width="150">
<template #default="{ row }">
<div>{{ cell(row, 'userWeChatNickName') || '--' }}</div>
<div class="yk-cell__sub">{{ cell(row, 'userWeChatAlias') || cell(row, 'weChatId') || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="员工信息" min-width="130">
<template #default="{ row }">
<div>{{ row.user_id_name || '--' }}</div>
<div class="yk-cell__sub">{{ cell(row, 'userPhone') || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="通话录音" width="110" align="center">
<template #default="{ row }">
<el-button v-if="row.record_file || cell(row, 'recordFile')" size="small" type="primary" link @click.stop="playRecord(row)">播放</el-button>
<span v-else style="color:#c0c4cc">--</span>
</template>
</el-table-column>
<el-table-column label="通话时长" width="100" align="right">
<template #default="{ row }">{{ formatCallDuration(row.call_duration) }}</template>
</el-table-column>
</template>
```
> `callStatusLabel`/`callTypeLabel`/`formatCallDuration`/`cell`/`playRecord` 已在 CallList import/定义。`yk-cell__sub` 加样式(小字次要行,:style 区加 `.yk-cell__sub{color:#909399;font-size:12px}`)。
- [ ] **Step 2: 类型检查 + 浏览器验证**(`/calls` 微信语音 tab → 云客 6 列,有数据)
```bash
cd yunkes && npx vue-tsc -b
```
- [ ] **Step 3: Commit**
```bash
git add yunkes/src/views/CallList.vue
git commit -m "$(cat <<'EOF'
feat(calls): 微信语音 tab 对齐云客列(通话信息/客户/员工微信/员工信息/录音/时长)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
---
## Task 3: WA 语音 tab 列对齐云客
> 事实:云客 WA wavoicecalllist 列(audit §6):状态 / 客户信息 / 员工信息 / 通话时间和录音 / 操作。本 plan 操作列留 4c(无操作按钮,仅展示),其余列对齐。
**Files:** Modify `yunkes/src/views/CallList.vue`
- [ ] **Step 1: WA 语音 tab 列**。`v-if="callType === 'wa_voice_call'"` 包一组:
```html
<template v-if="callType === 'wa_voice_call'">
<el-table-column label="状态" width="160">
<template #default="{ row }">
<div>{{ callStatusLabel(row.call_status) }}</div>
<div class="yk-cell__sub">时长 {{ formatCallDuration(row.call_duration) }}</div>
</template>
</el-table-column>
<el-table-column label="客户信息" min-width="160">
<template #default="{ row }">
<div>{{ row.plan_customer_name || cell(row, 'talkerNickName') || '--' }}</div>
<div class="yk-cell__sub">{{ cell(row, 'talkerAlias') || row.call_number || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="员工信息" min-width="160">
<template #default="{ row }">
<div>{{ row.user_id_name || '--' }} · {{ cell(row, 'userPhone') || '--' }}</div>
<div class="yk-cell__sub">{{ cell(row, 'userWeChatNickName') || '--' }} · {{ cell(row, 'userWeChatAlias') || '--' }}</div>
</template>
</el-table-column>
<el-table-column label="通话时间和录音" min-width="180">
<template #default="{ row }">
<div>{{ formatTime(row.start_call_time) }}</div>
<el-button v-if="row.record_file || cell(row, 'ossFileName')" size="small" type="primary" link @click.stop="playRecord(row)" style="margin-top:2px">播放录音</el-button>
<span v-else class="yk-cell__sub">无录音</span>
</template>
</el-table-column>
</template>
```
> audit WA 操作列(手动匹配/录音异常检查)本 plan 不做(4c)。WA call_status 极性:Plan 4a 翻转了 incoming_call(isSend),callStatusLabel 用 call_status(接通/未接通,未翻转)——确认 callStatusLabel 对 WA 的 call_status(1接通/2未接通)显示对(format.ts 的 callStatusLabel)。
- [ ] **Step 2: 类型检查 + 浏览器验证**(`/calls` WA 语音 tab → 云客 4 列,608 条数据)
```bash
cd yunkes && npx vue-tsc -b
```
- [ ] **Step 3: Commit**
```bash
git add yunkes/src/views/CallList.vue
git commit -m "$(cat <<'EOF'
feat(calls): WA 语音 tab 对齐云客列(状态/客户/员工/通话时间和录音)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
---
## Task 4: 电话 tab 保留现有通用列
> 事实:电话(phone_call)非云客沟通板块(云客电话 CRM 是另一套),yunkes 现有 8 列对电话适用。保留。
**Files:** Modify `yunkes/src/views/CallList.vue`
- [ ] **Step 1: 电话 tab 列**。`v-if="callType === 'phone_call'"` 包现有 8 列(时间/员工/号码/方向/类型/时长/状态/录音)。把现有 :167-207 的列移进这个 v-if template(保留 playRecord)。
- [ ] **Step 2: 类型检查 + 浏览器验证**(电话 tab → 现有 8 列)
```bash
cd yunkes && npx vue-tsc -b
```
- [ ] **Step 3: Commit**
```bash
git add yunkes/src/views/CallList.vue
git commit -m "$(cat <<'EOF'
feat(calls): 电话 tab 保留通用 8 列(按 channel 分离列集)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
---
## Task 5: callFields/format label 补 + 类型 + 验证
> 事实:`callFields.ts` CALL_FIELD_LABELS 已含部分(ossFileName/duration/startTime 等),但缺 WA/微信语音特有(userWeChatNickName/userWeChatAlias/userPhone/talker/talkerNickName/talkerAlias/weChatId/callFileId/from)。`format.ts` callTypeLabel 可能不识别 'wa_voice_call'。
**Files:**
- Modify `yunkes/src/utils/callFields.ts`
- Modify `yunkes/src/utils/format.ts`
- Modify `yunkes/src/api/types.ts`
- [ ] **Step 1: CallChannel 类型加 wa_voice_call**(`api/types.ts`):
```ts
export type CallChannel = 'phone_call' | 'voice_call' | 'wa_voice_call'
```
- [ ] **Step 2: callTypeLabel 加 WA + callStatusLabel 加宽**(`format.ts`):
```ts
// callTypeLabel 加:'wa_voice_call' -> 'WA 语音通话'
// callStatusLabel 加宽:WA 上游 callStatus 1=接通/2=未接通(现有 1=已接通/0=未接通)。
// 改为:1 -> '已接通';0 | 2 -> '未接通';default -> `状态 ${v}`。
// 先确认各 channel 极性(WA callStatus=2 实测是未接通);若 phone_call 用 0=未接通则兼容(0|2 都未接通)。
```
> 实测 WA callStatus:Phase 0 采集 callStatus=1 有录音(接通)/callStatus=2 无录音(未接通)。format.ts 现 callStatusLabel 只认 1/0,WA 的 2 会显示"状态 2"——加宽到 0|2=未接通。
- [ ] **Step 3: CALL_FIELD_LABELS 补 WA/微信语音字段**(`callFields.ts:24`):
```ts
// 在 CALL_FIELD_LABELS 加:
userWeChatNickName: '员工微信昵称',
userWeChatAlias: '员工微信号',
userPhone: '员工手机',
talker: '客户号',
talkerNickName: '客户昵称',
talkerAlias: '客户微信号',
weChatId: '员工微信ID',
callFileId: '录音文件ID',
from: '通话来源',
isRoom: '是否群通话',
```
- [ ] **Step 4: 全类型检查 + 浏览器端到端**
```bash
cd yunkes && npx vue-tsc -b
```
浏览器 `/calls`:① 微信语音 tab → 6 云客列 + 数据;② WA 语音 tab → 4 云客列 + 608 条;③ 电话 tab → 8 列;④ 录音播放(有录音行);⑤ 行点击→drawer 全字段(含新 label)。
- [ ] **Step 5: Commit**
```bash
git add yunkes/src/api/types.ts yunkes/src/utils/format.ts yunkes/src/utils/callFields.ts
git commit -m "$(cat <<'EOF'
feat(calls): CallChannel 加 wa_voice_call + callFields/format label 补 WA/微信语音字段
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
---
## 收尾验收
- [ ] `vue-tsc -b` exit 0。
- [ ] `/calls` 三 tab(微信语音/WA 语音/电话)切换正常,各 tab 列对齐云客。
- [ ] 微信语音 tab 6 列(通话信息/客户信息/员工微信/员工信息/通话录音/通话时长)+ 数据(2193 条)。
- [ ] WA 语音 tab 4 列(状态/客户信息/员工信息/通话时间和录音)+ 数据(608 条)。
- [ ] 录音播放 dialog 工作(有 record_file 行)。
- [ ] 详情 drawer 全字段(新 label 显示)。
- [ ] tab 切换重新 load(channel 变)。
---
## 不在本 plan 范围(Plan 4c)
- 长语音列表 modal(call/list 抓取 + voice_clip 表 + 编辑/手动匹配 UI)。
- 统计卡片(todayStatistics 抓取/展示)。
- 录音异常检查(/call/check/{id} 后端代理 + 按钮)。
- 批量语音下载 / 全部导出 / 高级筛选工具栏。
- 微信好友/微信群二级 tab(raw_json.isRoom client 筛)。
- 列内可点元素(客户信息→聊天等二级操作)。