oss 直传
分片上传
This commit is contained in:
Generated
+45
@@ -19,6 +19,7 @@
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.9",
|
||||
"cos-js-sdk-v5": "^1.10.1",
|
||||
"css-color-function": "^1.3.3",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.4",
|
||||
@@ -6064,6 +6065,16 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cos-js-sdk-v5": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/cos-js-sdk-v5/-/cos-js-sdk-v5-1.10.1.tgz",
|
||||
"integrity": "sha512-a4SRfCY5g6Z35C7OWe9te/S1zk77rVQzfpvZ33gmTdJQzKxbNbEG7Aw/v453XwVMsQB352FIf7KRMm5Ya/wlZQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
@@ -7580,6 +7591,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
|
||||
"integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
@@ -12299,6 +12332,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.1.2.tgz",
|
||||
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.9",
|
||||
"cos-js-sdk-v5": "^1.10.1",
|
||||
"css-color-function": "^1.3.3",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.4",
|
||||
|
||||
@@ -90,3 +90,50 @@ export function fileMove(params: Record<string, any>) {
|
||||
export function fileRename(params: { id: number; name: string }) {
|
||||
return request.post({ url: '/file/rename', params })
|
||||
}
|
||||
|
||||
/** 浏览器直传 OSS - 凭证类型 */
|
||||
export interface OssCredentialsResponse {
|
||||
provider: string
|
||||
fallback: boolean
|
||||
bucket?: string
|
||||
region?: string
|
||||
host?: string
|
||||
cdn_domain?: string
|
||||
key_prefix?: string
|
||||
max_size?: number
|
||||
duration?: number
|
||||
expired_time?: number
|
||||
start_time?: number
|
||||
credentials?: {
|
||||
tmpSecretId: string
|
||||
tmpSecretKey: string
|
||||
sessionToken: string
|
||||
}
|
||||
}
|
||||
|
||||
/** 申请 STS 临时凭证 */
|
||||
export function getOssCredentials(params: { type: 'video' }) {
|
||||
return request.post({
|
||||
url: '/upload/ossCredentials',
|
||||
params
|
||||
}) as Promise<OssCredentialsResponse>
|
||||
}
|
||||
|
||||
/** 直传完成回执:写 file 表 + HEAD 校验 */
|
||||
export function confirmOssUpload(params: {
|
||||
type: 'video'
|
||||
key: string
|
||||
name: string
|
||||
size: number
|
||||
content_type: string
|
||||
cid?: number
|
||||
}) {
|
||||
return request.post({ url: '/upload/ossConfirm', params }) as Promise<{
|
||||
id: number
|
||||
cid: number
|
||||
type: number
|
||||
name: string
|
||||
uri: string
|
||||
url: string
|
||||
}>
|
||||
}
|
||||
|
||||
+13
-2
@@ -229,14 +229,25 @@ export function startCloudRecording(params: { diagnosis_id: number }) {
|
||||
return request.post({ url: '/tcm.diagnosis/startCloudRecording', params })
|
||||
}
|
||||
|
||||
/** 本地录制完成后,将已上传视频的访问地址写入通话记录(忽略取消令牌,避免与其它 POST 互斥取消) */
|
||||
export function attachLocalCallRecording(params: { diagnosis_id: number; file_url: string }) {
|
||||
/** 本地录制完成后,将已上传视频的访问地址写入通话记录(可显式传 call_record_id) */
|
||||
export function attachLocalCallRecording(params: {
|
||||
diagnosis_id: number
|
||||
file_url: string
|
||||
call_record_id?: number
|
||||
}) {
|
||||
return request.post(
|
||||
{ url: '/tcm.diagnosis/attachLocalCallRecording', params },
|
||||
{ ignoreCancelToken: true, isOpenRetry: false }
|
||||
)
|
||||
}
|
||||
|
||||
export function createManualCallRecord(params: { diagnosis_id: number }) {
|
||||
return request.post(
|
||||
{ url: '/tcm.diagnosis/createManualCallRecord', params },
|
||||
{ ignoreCancelToken: true, isOpenRetry: false }
|
||||
)
|
||||
}
|
||||
|
||||
/** 医助旁观当前进行中的视频通话(进房参数,Web TRTC 只拉流) */
|
||||
export function getAssistantWatchCallParams(params: { diagnosis_id: number }) {
|
||||
return request.get({ url: '/tcm.diagnosis/watchCall', params })
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:data="data"
|
||||
:http-request="useDirect ? httpRequest : undefined"
|
||||
:on-progress="handleProgress"
|
||||
:on-success="handleSuccess"
|
||||
:on-exceed="handleExceed"
|
||||
@@ -41,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ElUpload } from 'element-plus'
|
||||
import type { ElUpload, UploadRequestOptions } from 'element-plus'
|
||||
import { computed, defineComponent, ref, shallowRef } from 'vue'
|
||||
|
||||
import config from '@/config'
|
||||
@@ -49,6 +50,10 @@ import { RequestCodeEnum } from '@/enums/requestEnums'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
DirectUploadFallbackError,
|
||||
uploadVideoDirectToCos
|
||||
} from '@/utils/oss-direct-upload'
|
||||
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
@@ -77,6 +82,11 @@ export default defineComponent({
|
||||
showProgress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 视频直传到 OSS(绕开服务器中转,仅 type=video 生效)
|
||||
direct: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['change', 'error', 'success', 'allSuccess'],
|
||||
@@ -92,6 +102,9 @@ export default defineComponent({
|
||||
const visible = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
// 仅 video + direct 时才接管 http-request
|
||||
const useDirect = computed(() => props.direct && props.type === 'video')
|
||||
|
||||
const handleProgress = () => {
|
||||
visible.value = true
|
||||
}
|
||||
@@ -142,12 +155,73 @@ export default defineComponent({
|
||||
return '*'
|
||||
}
|
||||
})
|
||||
|
||||
// 走 COS 直传:成功时模拟老接口的响应 envelope,失败/降级时回到默认 XHR
|
||||
const httpRequest = async (options: UploadRequestOptions) => {
|
||||
visible.value = true
|
||||
try {
|
||||
const data = await uploadVideoDirectToCos({
|
||||
file: options.file,
|
||||
type: 'video',
|
||||
cid: Number((options.data as any)?.cid ?? 0),
|
||||
onProgress(info) {
|
||||
// 触发 ElUpload 内部进度(保持与默认上传一致的体验)
|
||||
;(options as any).onProgress?.({
|
||||
percent: info.percent,
|
||||
loaded: info.loaded,
|
||||
total: info.total
|
||||
})
|
||||
}
|
||||
})
|
||||
;(options as any).onSuccess?.({ code: RequestCodeEnum.SUCCESS, msg: 'ok', data })
|
||||
} catch (err: any) {
|
||||
if (err instanceof DirectUploadFallbackError) {
|
||||
feedback.msgWarning('当前存储不支持直传,已切换为普通上传')
|
||||
await defaultXhrUpload(options)
|
||||
return
|
||||
}
|
||||
;(options as any).onError?.(err instanceof Error ? err : new Error(String(err?.message ?? err)))
|
||||
}
|
||||
}
|
||||
|
||||
// 当 STS 不可用时,回退到 ElUpload 原生 XHR 行为(POST /upload/{type})
|
||||
const defaultXhrUpload = (options: UploadRequestOptions) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', action.value)
|
||||
Object.entries(headers.value).forEach(([k, v]) => xhr.setRequestHeader(k, String(v ?? '')))
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (!e.lengthComputable) return
|
||||
const percent = (e.loaded / e.total) * 100
|
||||
;(options as any).onProgress?.({ percent, loaded: e.loaded, total: e.total })
|
||||
}
|
||||
xhr.onload = () => {
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText)
|
||||
;(options as any).onSuccess?.(json)
|
||||
} catch (e) {
|
||||
;(options as any).onError?.(new Error('上传响应解析失败'))
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
;(options as any).onError?.(new Error('网络错误'))
|
||||
resolve()
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', options.file)
|
||||
Object.entries(options.data || {}).forEach(([k, v]) => formData.append(k, String(v)))
|
||||
xhr.send(formData)
|
||||
})
|
||||
|
||||
return {
|
||||
uploadRefs,
|
||||
action,
|
||||
headers,
|
||||
visible,
|
||||
fileList,
|
||||
useDirect,
|
||||
httpRequest,
|
||||
getAccept,
|
||||
handleProgress,
|
||||
handleSuccess,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import COS from 'cos-js-sdk-v5'
|
||||
|
||||
import {
|
||||
confirmOssUpload,
|
||||
getOssCredentials,
|
||||
type OssCredentialsResponse
|
||||
} from '@/api/file'
|
||||
|
||||
export type DirectUploadType = 'video'
|
||||
|
||||
export interface DirectUploadProgress {
|
||||
/** 0-100 */
|
||||
percent: number
|
||||
loaded: number
|
||||
total: number
|
||||
speed: number
|
||||
}
|
||||
|
||||
export interface DirectUploadResult {
|
||||
id: number
|
||||
cid: number
|
||||
type: number
|
||||
name: string
|
||||
uri: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface DirectUploadOptions {
|
||||
file: File
|
||||
type: DirectUploadType
|
||||
cid?: number
|
||||
onProgress?: (progress: DirectUploadProgress) => void
|
||||
/** 返回 false 时由调用方自行降级到老链路 */
|
||||
onFallback?: (provider: string) => boolean | void
|
||||
}
|
||||
|
||||
const SLICE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const ASYNC_LIMIT = 3
|
||||
|
||||
function buildKey(prefix: string, file: File): string {
|
||||
const ext = (file.name.split('.').pop() || 'mp4').toLowerCase()
|
||||
const ts = Date.now()
|
||||
const rand = Math.random().toString(36).slice(2, 10)
|
||||
return `${prefix}${ts}-${rand}.${ext}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执)
|
||||
* 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。
|
||||
*/
|
||||
export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise<DirectUploadResult> {
|
||||
const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type })
|
||||
|
||||
if (credentials.fallback) {
|
||||
const handled = options.onFallback?.(credentials.provider) ?? false
|
||||
if (!handled) {
|
||||
throw new DirectUploadFallbackError(credentials.provider)
|
||||
}
|
||||
// 调用方自行处理,本函数早退
|
||||
throw new DirectUploadFallbackError(credentials.provider)
|
||||
}
|
||||
|
||||
if (!credentials.credentials || !credentials.bucket || !credentials.region || !credentials.key_prefix) {
|
||||
throw new Error('STS 凭证返回缺失字段')
|
||||
}
|
||||
|
||||
if (credentials.max_size && options.file.size > credentials.max_size) {
|
||||
const mb = Math.round(credentials.max_size / 1024 / 1024)
|
||||
throw new Error(`视频体积超出上限(${mb}MB)`)
|
||||
}
|
||||
|
||||
const cred = credentials.credentials
|
||||
const expiredTime = credentials.expired_time ?? 0
|
||||
const startTime = credentials.start_time ?? Math.floor(Date.now() / 1000)
|
||||
|
||||
const cos = new COS({
|
||||
getAuthorization(_options, callback) {
|
||||
callback({
|
||||
TmpSecretId: cred.tmpSecretId,
|
||||
TmpSecretKey: cred.tmpSecretKey,
|
||||
SecurityToken: cred.sessionToken,
|
||||
StartTime: startTime,
|
||||
ExpiredTime: expiredTime
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const key = buildKey(credentials.key_prefix, options.file)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
cos.uploadFile(
|
||||
{
|
||||
Bucket: credentials.bucket as string,
|
||||
Region: credentials.region as string,
|
||||
Key: key,
|
||||
Body: options.file,
|
||||
SliceSize: SLICE_SIZE,
|
||||
AsyncLimit: ASYNC_LIMIT,
|
||||
onProgress(info) {
|
||||
if (!options.onProgress) return
|
||||
const total = info.total || options.file.size
|
||||
options.onProgress({
|
||||
percent: Math.min(99, Math.round((info.percent || 0) * 100)),
|
||||
loaded: info.loaded || 0,
|
||||
total,
|
||||
speed: info.speed || 0
|
||||
})
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const confirmed = await confirmOssUpload({
|
||||
type: options.type,
|
||||
key,
|
||||
name: options.file.name,
|
||||
size: options.file.size,
|
||||
content_type: options.file.type || '',
|
||||
cid: options.cid ?? 0
|
||||
})
|
||||
|
||||
options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 })
|
||||
|
||||
return confirmed
|
||||
}
|
||||
|
||||
/** 当后端 fallback=true 时抛出,调用方据此降级到老链路 */
|
||||
export class DirectUploadFallbackError extends Error {
|
||||
readonly provider: string
|
||||
constructor(provider: string) {
|
||||
super(`COS 直传不可用,当前驱动: ${provider}`)
|
||||
this.name = 'DirectUploadFallbackError'
|
||||
this.provider = provider
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user