This commit is contained in:
Your Name
2026-05-08 14:13:46 +08:00
27 changed files with 1660 additions and 74 deletions
@@ -1,23 +1,28 @@
<template>
<div class="call-record-panel">
<el-alert
type="info"
show-icon
:closable="false"
class="mb-4"
title="视频通话与录制回放"
>
<!-- <p class="call-record-tip">
下列为与本诊单关联的通话记录医生接通并同步房间号后服务端会尝试自动发起云端混流录制需配置 CAM 密钥与云点播等录制完成后由腾讯云回调写入回放地址同时管理端浏览器会进行本地录制挂断后自动上传并合并到本条记录
</p>
<p class="call-record-tip muted">
回调 URL 示例<code>{{ callbackHint }}</code>
若配置了 <code>trtc.recording_callback_token</code>请在 URL 后附加
<code>?token=你的密钥</code>
</p> -->
</el-alert>
<div class="call-record-toolbar">
<upload
ref="toolbarUploadRef"
type="video"
direct
:multiple="false"
:limit="1"
:show-progress="true"
@success="handleToolbarUploadSuccess"
>
<el-button type="primary">上传视频</el-button>
</upload>
</div>
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无通话记录">
<el-table v-loading="loading" :data="rows" border stripe>
<template #empty>
<div class="call-record-empty">
<div class="call-record-empty__title">暂无通话记录</div>
<div class="call-record-empty__desc">
现在可以直接点击上方上传视频系统会自动生成一条默认通话记录来承载回放
</div>
</div>
</template>
<el-table-column label="开始时间" width="170" prop="start_time_text" />
<el-table-column label="结束时间" width="170" prop="end_time_text" />
<el-table-column label="通话类型" width="100">
@@ -25,7 +30,7 @@
{{ row.call_type === 1 ? '语音' : '视频' }}
</template>
</el-table-column>
<el-table-column label="房间号" width="140">
<el-table-column label="房间号" width="180">
<template #default="{ row }">
<span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span>
<span v-else class="text-gray-400"></span>
@@ -42,12 +47,26 @@
{{ row.recording_status_text || '—' }}
</template>
</el-table-column>
<el-table-column label="录制回放" min-width="280">
<el-table-column label="上传回放" width="180">
<template #default="{ row }">
<upload
type="video"
direct
:multiple="false"
:limit="1"
:show-progress="true"
@success="(response: any) => handleRowUploadSuccess(row, response)"
>
<el-button type="primary" plain size="small">上传视频</el-button>
</upload>
</template>
</el-table-column>
<el-table-column label="录制回放" min-width="320">
<template #default="{ row }">
<div v-if="(row.recording_urls_list || []).length" class="recording-list">
<div
v-for="(url, idx) in row.recording_urls_list"
:key="idx"
:key="`${row.id}-${idx}`"
class="recording-item"
>
<video
@@ -70,20 +89,30 @@
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { getCallRecords } from '@/api/tcm'
import Upload from '@/components/upload/index.vue'
import feedback from '@/utils/feedback'
import { attachLocalCallRecording, createManualCallRecord, getCallRecords } from '@/api/tcm'
import { ref, useTemplateRef, watch } from 'vue'
type CallRecordRow = {
id: number
call_type: number
room_id?: string
status: number
recording_status_text?: string
recording_urls_list?: string[]
start_time_text?: string
end_time_text?: string
duration_text?: string
}
const props = defineProps<{
diagnosisId: number
}>()
const loading = ref(false)
const rows = ref<any[]>([])
const callbackHint = computed(() => {
const origin = typeof window !== 'undefined' ? window.location.origin : ''
return `${origin}/api/trtc/recording-notify`
})
const rows = ref<CallRecordRow[]>([])
const toolbarUploadRef = useTemplateRef<InstanceType<typeof Upload>>('toolbarUploadRef')
const load = async () => {
if (!props.diagnosisId) return
@@ -101,7 +130,7 @@ const load = async () => {
watch(
() => props.diagnosisId,
() => {
load()
void load()
},
{ immediate: true }
)
@@ -120,30 +149,93 @@ function statusText(status: number) {
function isPlayableVideo(url: string) {
if (!url || typeof url !== 'string') return false
return /\.(mp4|webm|ogg)(\?|$)/i.test(url)
return /\.(mp4|webm|ogg|mov|mkv)(\?|$)/i.test(url)
}
async function handleToolbarUploadSuccess(response: any) {
const fileUrl = normalizeUploadUrl(response)
if (!fileUrl) {
feedback.msgError('上传成功但未返回视频地址')
return
}
try {
const record = await createManualCallRecord({ diagnosis_id: props.diagnosisId })
await attachLocalCallRecording({
diagnosis_id: props.diagnosisId,
call_record_id: Number(record?.id || 0),
file_url: fileUrl
} as any)
feedback.msgSuccess('视频回放上传成功')
await load()
} catch (error: any) {
feedback.msgError(error?.message || '写入回放失败')
}
}
async function handleRowUploadSuccess(row: CallRecordRow, response: any) {
const fileUrl = normalizeUploadUrl(response)
if (!fileUrl) {
feedback.msgError('上传成功但未返回视频地址')
return
}
try {
await attachLocalCallRecording({
diagnosis_id: props.diagnosisId,
call_record_id: Number(row.id || 0),
file_url: fileUrl
} as any)
feedback.msgSuccess('视频回放上传成功')
await load()
} catch (error: any) {
feedback.msgError(error?.message || '写入回放失败')
}
}
function normalizeUploadUrl(response: any): string {
return String(response?.data?.uri || response?.data?.url || '').trim()
}
</script>
<style scoped lang="scss">
.call-record-panel {
.call-record-tip {
margin: 0 0 8px;
line-height: 1.5;
font-size: 13px;
&.muted {
color: var(--el-text-color-secondary);
}
code {
font-size: 12px;
word-break: break-all;
}
.call-record-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.call-record-empty {
padding: 28px 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
.call-record-empty__title {
font-size: 14px;
color: var(--el-text-color-primary);
}
.call-record-empty__desc {
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
}
.recording-list {
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
}
.recording-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.recording-video {
max-width: 100%;
max-height: 180px;