Files
zyt/admin/src/views/tcm/diagnosis/components/AssistantWatchCallDialog.vue
T
2026-03-24 16:32:56 +08:00

286 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="760px"
destroy-on-close
append-to-body
:close-on-click-modal="false"
class="assistant-watch-call-dialog"
@closed="onClosed"
>
<div v-if="loading" class="watch-state">正在连接房间</div>
<div v-else-if="errorMsg" class="watch-state watch-error">{{ errorMsg }}</div>
<div v-show="!loading && !errorMsg" ref="gridRef" class="watch-grid" />
<template #footer>
<span class="watch-hint">仅观看不会开启摄像头与麦克风</span>
<el-button type="primary" @click="visible = false">离开</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import TRTC from 'trtc-sdk-v5'
import type { TRTCStreamType } from 'trtc-sdk-v5'
import { getAssistantWatchCallParams } from '@/api/tcm'
const props = defineProps<{
modelValue: boolean
diagnosisId: number
}>()
const emit = defineEmits<{
'update:modelValue': [boolean]
closed: []
}>()
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit('update:modelValue', v)
})
const gridRef = ref<HTMLElement | null>(null)
const loading = ref(false)
const errorMsg = ref('')
const dialogTitle = ref('旁观视频通话')
type TileEntry = { wrap: HTMLElement; userId: string; streamType: TRTCStreamType }
const tiles = new Map<string, TileEntry>()
let trtc: ReturnType<typeof TRTC.create> | null = null
let joinSession = 0
function tileKey(userId: string, streamType: TRTCStreamType) {
return `${userId}\u0000${String(streamType)}`
}
function remoteLabel(userId: string) {
if (userId.startsWith('patient_')) return '患者'
if (userId.startsWith('doctor_')) return '医护'
return userId
}
async function onRemoteVideoAvailable(event: { userId: string; streamType: TRTCStreamType }) {
if (!trtc || !gridRef.value) return
if (event.streamType !== TRTC.TYPE.STREAM_TYPE_MAIN) return
const key = tileKey(event.userId, event.streamType)
if (tiles.has(key)) return
const wrap = document.createElement('div')
wrap.className = 'watch-tile'
const cap = document.createElement('div')
cap.className = 'watch-tile-cap'
cap.textContent = remoteLabel(event.userId)
const view = document.createElement('div')
view.className = 'watch-tile-view'
wrap.appendChild(cap)
wrap.appendChild(view)
gridRef.value.appendChild(wrap)
tiles.set(key, { wrap, userId: event.userId, streamType: event.streamType })
try {
await trtc.startRemoteVideo({
userId: event.userId,
streamType: event.streamType,
view
})
} catch (e) {
console.warn('[AssistantWatchCall] startRemoteVideo', e)
}
}
async function onRemoteVideoUnavailable(event: { userId: string; streamType: TRTCStreamType }) {
if (!trtc) return
const key = tileKey(event.userId, event.streamType)
const t = tiles.get(key)
if (!t) return
try {
await trtc.stopRemoteVideo({ userId: event.userId, streamType: event.streamType })
} catch {
/* ignore */
}
t.wrap.remove()
tiles.delete(key)
}
function bindTrtcEvents() {
if (!trtc) return
trtc.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, onRemoteVideoAvailable)
trtc.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, onRemoteVideoUnavailable)
}
function unbindTrtcEvents() {
if (!trtc) return
trtc.off(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, onRemoteVideoAvailable)
trtc.off(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, onRemoteVideoUnavailable)
}
async function cleanup() {
unbindTrtcEvents()
if (trtc) {
for (const [, t] of tiles) {
try {
await trtc.stopRemoteVideo({ userId: t.userId, streamType: t.streamType })
} catch {
/* ignore */
}
t.wrap.remove()
}
tiles.clear()
if (gridRef.value) gridRef.value.innerHTML = ''
try {
await trtc.exitRoom()
} catch {
/* ignore */
}
try {
trtc.destroy()
} catch {
/* ignore */
}
trtc = null
} else {
tiles.clear()
if (gridRef.value) gridRef.value.innerHTML = ''
}
}
async function joinRoom() {
const session = ++joinSession
await cleanup()
if (!props.diagnosisId) {
errorMsg.value = '诊单无效'
return
}
loading.value = true
errorMsg.value = ''
dialogTitle.value = '旁观视频通话'
try {
const data = (await getAssistantWatchCallParams({
diagnosis_id: props.diagnosisId
})) as {
sdkAppId: number
userId: string
userSig: string
roomId?: number
strRoomId?: string
patientName?: string
}
if (session !== joinSession) {
return
}
if (data.patientName) {
dialogTitle.value = `旁观视频通话 · ${data.patientName}`
}
trtc = TRTC.create()
bindTrtcEvents()
const roomCfg = {
sdkAppId: data.sdkAppId,
userId: data.userId,
userSig: data.userSig,
autoReceiveAudio: true,
autoReceiveVideo: true,
...(data.roomId != null && data.roomId > 0
? { roomId: data.roomId }
: { strRoomId: data.strRoomId as string })
}
if (!(data.roomId != null && data.roomId > 0) && !data.strRoomId) {
throw new Error('缺少房间号')
}
await trtc.enterRoom(roomCfg)
if (session !== joinSession) {
await cleanup()
return
}
loading.value = false
} catch (e: unknown) {
loading.value = false
let msg = '进入房间失败'
if (typeof e === 'string') msg = e
else if (e && typeof e === 'object') {
const o = e as { msg?: string; message?: string }
if (o.msg) msg = String(o.msg)
else if (o.message) msg = String(o.message)
}
errorMsg.value = msg
await cleanup()
}
}
function onClosed() {
void cleanup()
loading.value = false
errorMsg.value = ''
dialogTitle.value = '旁观视频通话'
emit('closed')
}
watch(
() => [props.modelValue, props.diagnosisId] as const,
([open, id]) => {
if (!open) {
joinSession++
void cleanup()
return
}
if (id > 0) {
void joinRoom()
}
}
)
</script>
<style scoped>
.watch-state {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
font-size: 14px;
}
.watch-error {
color: var(--el-color-danger);
}
.watch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
min-height: 220px;
}
.watch-tile {
background: #0f0f0f;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16 / 10;
display: flex;
flex-direction: column;
}
.watch-tile-cap {
padding: 6px 10px;
font-size: 12px;
color: #e5e5e5;
background: rgba(0, 0, 0, 0.55);
}
.watch-tile-view {
flex: 1;
min-height: 0;
position: relative;
}
.watch-hint {
float: left;
line-height: 32px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>