123c23242c
Co-Authored-By: Claude <noreply@anthropic.com>
343 lines
17 KiB
Markdown
343 lines
17 KiB
Markdown
# 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 筛)。
|
||
- 列内可点元素(客户信息→聊天等二级操作)。
|