新增功能
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
<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>
|
||||
@@ -5,11 +5,10 @@
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
title="视频通话与云端录制"
|
||||
title="视频通话与录制回放"
|
||||
>
|
||||
<p class="call-record-tip">
|
||||
下列为与本诊单关联的通话记录。若在腾讯云 TRTC
|
||||
控制台开启了云端录制并填写回调地址,录制生成后会自动写入「录制回放」。
|
||||
下列为与本诊单关联的通话记录。医生接通并同步房间号后,服务端会尝试自动发起「云端混流录制」(需配置 CAM 密钥与云点播等);录制完成后由腾讯云回调写入回放地址。同时,管理端浏览器会进行「本地录制」,挂断后自动上传并合并到本条记录。
|
||||
</p>
|
||||
<p class="call-record-tip muted">
|
||||
回调 URL 示例:<code>{{ callbackHint }}</code>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示腾讯云 IM 单聊记录:已合并患者 <code>{{ patientImHint }}</code> 与
|
||||
<strong>所有医生 / 医助账号</strong>(<code>doctor_*</code>)分别产生的会话,按时间排序。
|
||||
展示腾讯云 IM 单聊记录:已合并患者 与
|
||||
<strong>所有医生 / 医助账号</strong>分别产生的会话,按时间排序。
|
||||
数据来自 IM 漫游;若未开通漫游或某账号与该患者无会话,对应侧无消息。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
Reference in New Issue
Block a user