Co-Authored-By: Claude <noreply@anthropic.com>
17 KiB
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 行):
callTyperadio(all/phone_call/voice_call)+ 8 列(时间/员工/号码/方向/类型/时长/状态/录音)+ playRecord dialog + openDrawer +cell(row, camelKey)→readField读 raw_json。 - 后端
/api/calls:channel参数直筛PhoneCall.channel;PhoneCallOutfrom_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 + 值。把
callTyperef 默认改'voice_call'(微信语音优先,云客语音通话列表主入口)。typeOptions改为 3 tab(去掉 'all',每 tab 对应一个 channel):
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区):
<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>
function onTabChange(name: TabPaneName): void {
callType.value = name as CallChannel
page.value = 1
load()
}
TabPaneNamefrom 'element-plus'。移除 radio 相关:header subline(:131类型:{{ typeLabelCurrent }})改用{{ tabs.find(t => t.value === callType)?.label }},删typeOptions+typeLabelCurrentcomputed(避免 vue-tsc 坏)。onTabChange可简化为page.value=1; load()(v-model 已更新 callType)。
-
Step 3: load() 用 channel(:51-55 现已
channel: callType.value,保持;但不再传 'all'——每 tab 是具体 channel)。 -
Step 4: 类型检查
cd yunkes && npx vue-tsc -b
- Step 5: Commit
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:
<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 列,有数据)
cd yunkes && npx vue-tsc -b
- Step 3: Commit
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'"包一组:
<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: 类型检查 + 浏览器验证(
/callsWA 语音 tab → 云客 4 列,608 条数据)
cd yunkes && npx vue-tsc -b
- Step 3: Commit
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 列)
cd yunkes && npx vue-tsc -b
- Step 3: Commit
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.tsCALL_FIELD_LABELS 已含部分(ossFileName/duration/startTime 等),但缺 WA/微信语音特有(userWeChatNickName/userWeChatAlias/userPhone/talker/talkerNickName/talkerAlias/weChatId/callFileId/from)。format.tscallTypeLabel 可能不识别 '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):
export type CallChannel = 'phone_call' | 'voice_call' | 'wa_voice_call'
- Step 2: callTypeLabel 加 WA + callStatusLabel 加宽(
format.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):
// 在 CALL_FIELD_LABELS 加:
userWeChatNickName: '员工微信昵称',
userWeChatAlias: '员工微信号',
userPhone: '员工手机',
talker: '客户号',
talkerNickName: '客户昵称',
talkerAlias: '客户微信号',
weChatId: '员工微信ID',
callFileId: '录音文件ID',
from: '通话来源',
isRoom: '是否群通话',
- Step 4: 全类型检查 + 浏览器端到端
cd yunkes && npx vue-tsc -b
浏览器 /calls:① 微信语音 tab → 6 云客列 + 数据;② WA 语音 tab → 4 云客列 + 608 条;③ 电话 tab → 8 列;④ 录音播放(有录音行);⑤ 行点击→drawer 全字段(含新 label)。
- Step 5: Commit
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 -bexit 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 筛)。
- 列内可点元素(客户信息→聊天等二级操作)。