244 lines
6.4 KiB
Vue
244 lines
6.4 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, onMounted, ref } from 'vue'
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import { getCalls } from '@/api'
|
|||
|
|
import type { CallChannel, PhoneCall } from '@/api/types'
|
|||
|
|
import {
|
|||
|
|
callDirectionLabel,
|
|||
|
|
callStatusLabel,
|
|||
|
|
callTypeLabel,
|
|||
|
|
formatCallDuration,
|
|||
|
|
formatTime,
|
|||
|
|
} from '@/utils/format'
|
|||
|
|
|
|||
|
|
// Calls aren't tied to a messaging channel — `channel` on a call is the call
|
|||
|
|
// type (phone_call / voice_call). We keep a LOCAL filter so it doesn't fight
|
|||
|
|
// the shared top-bar wechat/whatsapp store used by customers/groups.
|
|||
|
|
const callType = ref<CallChannel | 'all'>('all')
|
|||
|
|
|
|||
|
|
const calls = ref<PhoneCall[]>([])
|
|||
|
|
const total = ref(0)
|
|||
|
|
const page = ref(1)
|
|||
|
|
const pageSize = ref(20)
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const errorMsg = ref<string | null>(null)
|
|||
|
|
|
|||
|
|
// Recording playback dialog state.
|
|||
|
|
const recordDialogVisible = ref(false)
|
|||
|
|
const recordSrc = ref<string | null>(null)
|
|||
|
|
const recordTitle = ref('')
|
|||
|
|
|
|||
|
|
const typeOptions: { value: CallChannel | 'all'; label: string }[] = [
|
|||
|
|
{ value: 'all', label: '全部类型' },
|
|||
|
|
{ value: 'phone_call', label: '电话' },
|
|||
|
|
{ value: 'voice_call', label: '语音通话' },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const typeLabelCurrent = computed(() => {
|
|||
|
|
const opt = typeOptions.find((o) => o.value === callType.value)
|
|||
|
|
return opt ? opt.label : '全部类型'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function load(): Promise<void> {
|
|||
|
|
loading.value = true
|
|||
|
|
errorMsg.value = null
|
|||
|
|
try {
|
|||
|
|
const res = await getCalls({
|
|||
|
|
channel: callType.value,
|
|||
|
|
page: page.value,
|
|||
|
|
page_size: pageSize.value,
|
|||
|
|
})
|
|||
|
|
calls.value = res.items
|
|||
|
|
total.value = res.total
|
|||
|
|
} catch (e) {
|
|||
|
|
errorMsg.value = (e as Error).message
|
|||
|
|
ElMessage.error(`通话记录加载失败:${(e as Error).message}`)
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handlePageChange(p: number): void {
|
|||
|
|
page.value = p
|
|||
|
|
load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handlePageSizeChange(s: number): void {
|
|||
|
|
pageSize.value = s
|
|||
|
|
page.value = 1
|
|||
|
|
load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onTypeChange(value: string | number | boolean | undefined): void {
|
|||
|
|
callType.value = value as CallChannel | 'all'
|
|||
|
|
page.value = 1
|
|||
|
|
load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function playRecord(row: PhoneCall): void {
|
|||
|
|
if (!row.record_file) {
|
|||
|
|
ElMessage.info('该通话无录音文件')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
recordSrc.value = row.record_file
|
|||
|
|
const who = row.user_id_name || row.user_id || row.call_id
|
|||
|
|
const when = formatTime(row.start_call_time)
|
|||
|
|
recordTitle.value = `录音 · ${who}${when !== '--' ? ` · ${when}` : ''}`
|
|||
|
|
recordDialogVisible.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(load)
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="yk-calls">
|
|||
|
|
<div class="yk-calls__header">
|
|||
|
|
<div>
|
|||
|
|
<h2 style="margin: 0 0 4px; font-size: 16px">通话记录</h2>
|
|||
|
|
<span class="yk-calls__sub">
|
|||
|
|
共 {{ total }} 条 · 类型:{{ typeLabelCurrent }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="yk-calls__filters">
|
|||
|
|
<el-radio-group :model-value="callType" size="small" @change="onTypeChange">
|
|||
|
|
<el-radio-button
|
|||
|
|
v-for="opt in typeOptions"
|
|||
|
|
:key="opt.value"
|
|||
|
|
:value="opt.value"
|
|||
|
|
>
|
|||
|
|
{{ opt.label }}
|
|||
|
|
</el-radio-button>
|
|||
|
|
</el-radio-group>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-alert
|
|||
|
|
v-if="errorMsg"
|
|||
|
|
:title="errorMsg"
|
|||
|
|
type="error"
|
|||
|
|
show-icon
|
|||
|
|
:closable="false"
|
|||
|
|
style="margin-bottom: 12px"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<el-table
|
|||
|
|
v-loading="loading"
|
|||
|
|
:data="calls"
|
|||
|
|
stripe
|
|||
|
|
border
|
|||
|
|
row-key="call_id"
|
|||
|
|
style="width: 100%"
|
|||
|
|
empty-text="暂无通话记录"
|
|||
|
|
>
|
|||
|
|
<el-table-column label="时间" width="160">
|
|||
|
|
<template #default="{ row }">{{ formatTime(row.start_call_time) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="员工" min-width="130">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
{{ row.user_id_name || row.user_id || '--' }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="号码" min-width="130">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
{{ row.call_number || row.sim_phone || '--' }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="方向" width="80">
|
|||
|
|
<template #default="{ row }">{{ callDirectionLabel(row.incoming_call) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="类型" width="100">
|
|||
|
|
<template #default="{ row }">{{ callTypeLabel(row.channel) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="时长" width="90" align="right">
|
|||
|
|
<template #default="{ row }">{{ formatCallDuration(row.call_duration) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="状态" width="90">
|
|||
|
|
<template #default="{ row }">{{ callStatusLabel(row.call_status) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="录音" width="100" align="center">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-button
|
|||
|
|
v-if="row.record_file"
|
|||
|
|
size="small"
|
|||
|
|
type="primary"
|
|||
|
|
link
|
|||
|
|
@click.stop="playRecord(row)"
|
|||
|
|
>
|
|||
|
|
播放录音
|
|||
|
|
</el-button>
|
|||
|
|
<span v-else style="color: #c0c4cc">--</span>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<div class="yk-calls__pager">
|
|||
|
|
<el-pagination
|
|||
|
|
:current-page="page"
|
|||
|
|
:page-size="pageSize"
|
|||
|
|
:page-sizes="[20, 50, 100]"
|
|||
|
|
:total="total"
|
|||
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|||
|
|
background
|
|||
|
|
@current-change="handlePageChange"
|
|||
|
|
@size-change="handlePageSizeChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="recordDialogVisible"
|
|||
|
|
:title="recordTitle"
|
|||
|
|
width="480px"
|
|||
|
|
align-center
|
|||
|
|
@closed="recordSrc = null"
|
|||
|
|
>
|
|||
|
|
<audio
|
|||
|
|
v-if="recordSrc"
|
|||
|
|
controls
|
|||
|
|
:src="recordSrc"
|
|||
|
|
style="width: 100%"
|
|||
|
|
preload="metadata"
|
|||
|
|
>
|
|||
|
|
您的浏览器不支持音频播放。
|
|||
|
|
</audio>
|
|||
|
|
<div v-if="recordSrc" class="yk-calls__record-link">
|
|||
|
|
<a :href="recordSrc" target="_blank" rel="noopener">在新窗口打开录音文件</a>
|
|||
|
|
</div>
|
|||
|
|
</el-dialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.yk-calls__header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 16px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.yk-calls__sub {
|
|||
|
|
color: #909399;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.yk-calls__filters {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
align-items: center;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.yk-calls__pager {
|
|||
|
|
margin-top: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.yk-calls__record-link {
|
|||
|
|
margin-top: 8px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
</style>
|