diff --git a/yunkes/src/api/index.ts b/yunkes/src/api/index.ts index 77bf4d8..5b22a5e 100644 --- a/yunkes/src/api/index.ts +++ b/yunkes/src/api/index.ts @@ -22,6 +22,7 @@ import type { SyncRun, SyncState, SyncStatus, + VoiceClip, } from './types' /** Drop entries whose value is `undefined`, `null`, or `''`. */ @@ -191,6 +192,42 @@ export function getCalls(query: CallQuery = {}): Promise> { ) } +// --------------------------------------------------------------------------- +// Voice clips (长语音片段 — read-only mirror of 云客 /wechatweb/call/list) +// --------------------------------------------------------------------------- + +export interface VoiceClipQuery { + /** 员工 userId 精确过滤(modal 从 CallList 行打开时传)。 */ + user_id?: string + /** 员工姓名模糊过滤(云客 modal 的「员工姓名」筛选项)。 */ + user_real_name?: string + /** 本地通道标记(wechat / whatsapp),非上游筛选。 */ + channel?: string + /** ms-epoch inclusive 窗口,与 end_time 比较(录音结束时间)。 */ + start?: number + end?: number + /** 企业串码。 */ + company_code?: string + page?: number + page_size?: number +} + +export function getVoiceClips(query: VoiceClipQuery = {}): Promise> { + return getJson>( + '/voice-clips', + clean({ + user_id: query.user_id, + user_real_name: query.user_real_name, + channel: query.channel, + start: query.start, + end: query.end, + company_code: query.company_code, + page: query.page ?? 1, + page_size: query.page_size ?? 10, + }), + ) +} + // --------------------------------------------------------------------------- // SMS // --------------------------------------------------------------------------- @@ -324,4 +361,5 @@ export type { SyncRun, SyncState, SyncStatus, + VoiceClip, } diff --git a/yunkes/src/api/types.ts b/yunkes/src/api/types.ts index 05dfdd2..d9e4791 100644 --- a/yunkes/src/api/types.ts +++ b/yunkes/src/api/types.ts @@ -148,6 +148,37 @@ export interface PhoneCall { raw_json?: Record | null } +/** + * Voice clip (长语音片段) row — mirrors backend VoiceClipOut. Source: 云客 + * `/wechatweb/call/list`, shared by 微信/WA voice tabs. Read-only in the mirror + * (no matching/ASR yet). `oss_file_name` is the OSS recording URL (played + * directly from the 云客 CDN — see spec §7.3, known 全本地 gap). + */ +export interface VoiceClip { + voice_clip_id: number + user_id: string + user_real_name?: string | null + wx_id?: string | null + talker?: string | null + status?: string | null + is_send?: boolean | null + company_code?: string | null + /** ms-epoch */ + start_time?: number | null + /** ms-epoch */ + end_time?: number | null + /** OSS recording URL (aliyun OSS 直链). */ + oss_file_name?: string | null + /** seconds */ + mp3_duration?: number | null + /** `HH:MM:SS` formatted duration from upstream. */ + duration_format?: string | null + /** `wechat` / `whatsapp` — local tag set at scrape time (not sent upstream). */ + channel: string + /** Verbatim upstream call/list record (camelCase keys). */ + raw_json?: Record | null +} + /** SMS log row (mirrors backend SmsOut). */ export interface Sms { sms_id: string diff --git a/yunkes/src/components/VoiceClipModal.vue b/yunkes/src/components/VoiceClipModal.vue new file mode 100644 index 0000000..533c06b --- /dev/null +++ b/yunkes/src/components/VoiceClipModal.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/yunkes/src/views/CallList.vue b/yunkes/src/views/CallList.vue index 2a13fe4..a7211b0 100644 --- a/yunkes/src/views/CallList.vue +++ b/yunkes/src/views/CallList.vue @@ -12,6 +12,7 @@ import { formatTime, } from '@/utils/format' import { buildCallSections, readField } from '@/utils/callFields' +import VoiceClipModal from '@/components/VoiceClipModal.vue' // Calls aren't tied to a messaging channel — `channel` on a call is the call // type (phone_call / voice_call / wa_voice_call). We keep a LOCAL filter so it @@ -30,6 +31,12 @@ const recordDialogVisible = ref(false) const recordSrc = ref(null) const recordTitle = ref('') +// VoiceClipModal state — opened from 微信/WA 语音 tabs by row.user_id. +const voiceClipModalOpen = ref(false) +const voiceClipUserId = ref('') +const voiceClipChannel = ref<'wechat' | 'whatsapp'>('wechat') +const voiceClipStaffName = ref(null) + const router = useRouter() // All-fields drawer state. @@ -109,6 +116,19 @@ function playRecord(row: PhoneCall): void { recordDialogVisible.value = true } +/** 打开长语音 modal:按当前通话员工 user_id 列出其长语音片段(只读)。 */ +function openVoiceClip(row: PhoneCall): void { + const userId = row.user_id || (cell(row, 'userId') as string | undefined) + if (!userId) { + ElMessage.info('该通话无员工 userId,无法查看长语音') + return + } + voiceClipUserId.value = userId + voiceClipStaffName.value = row.user_id_name ?? null + voiceClipChannel.value = callType.value === 'wa_voice_call' ? 'whatsapp' : 'wechat' + voiceClipModalOpen.value = true +} + function openDrawer(row: PhoneCall): void { drawerCall.value = row drawerOpen.value = true @@ -208,10 +228,13 @@ onMounted(load)
{{ cell(row, 'userPhone') || '--' }}
- + @@ -242,6 +265,9 @@ onMounted(load)
{{ formatTime(row.start_call_time) }}
播放录音 无录音 +
+ 查看长语音 +
@@ -326,6 +352,14 @@ onMounted(load) + + +