更新
This commit is contained in:
@@ -55,18 +55,19 @@
|
||||
<ImagePicker />
|
||||
<FilePicker />
|
||||
<!-- 仅在 TUICallKit 初始化成功后展示音视频入口,避免“初始化登录未完成”错误 -->
|
||||
<AudioCallPicker v-if="isCallReady" />
|
||||
<AudioCallPicker v-if="isCallReady && canAudioCall" />
|
||||
<!-- 视频接通后 doBindCallRoom → 后端 bindCallRoom 会调 CreateCloudRecording,RecordParams.RecordMode=2(混流/合流),见 TrtcCloudRecordingService -->
|
||||
<VideoCallPicker v-if="isCallReady" />
|
||||
<VideoCallPicker v-if="isCallReady && canVideoCall" />
|
||||
<!-- 群组视频通话(基于 TUICallKitServer.calls,多人通话入口) -->
|
||||
<el-button
|
||||
v-if="isCallReady"
|
||||
v-if="isCallReady && canVideoCall"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="startGroupVideoCall"
|
||||
>
|
||||
群视频
|
||||
</el-button>
|
||||
<span v-if="appointmentType === 'text'" class="text-consultation-hint">图文问诊,不支持音视频通话</span>
|
||||
</div>
|
||||
</template>
|
||||
</MessageInput>
|
||||
@@ -138,12 +139,15 @@ import {
|
||||
bindCallRoom,
|
||||
endCall,
|
||||
getCallSignature,
|
||||
triggerImChatSync,
|
||||
startCall
|
||||
} from '@/api/tcm'
|
||||
import { CallLocalRecorder } from '@/utils/call-local-recorder'
|
||||
import { createImChatArchiveTrigger } from '@/utils/im-chat-archive-trigger'
|
||||
import { captureVideoFrameFromElement } from '@/utils/call-video-screenshot'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { appointmentTypeDescription } from '@/utils/appointment-type'
|
||||
import { appointmentTypeDescription, canAppointmentVideoCall } from '@/utils/appointment-type'
|
||||
import { checkAppointmentOutgoingCall, registerAppointmentCallGuard } from '@/utils/appointment-call-guard'
|
||||
import {
|
||||
formatTUICallUserError,
|
||||
getTUICallPackageArrearsMessage,
|
||||
@@ -185,7 +189,17 @@ const isReady = ref(false)
|
||||
const error = ref('')
|
||||
const patientName = ref('')
|
||||
const appointmentType = ref<string | null | undefined>('video')
|
||||
const appointmentTypeLabel = computed(() => appointmentTypeDescription(appointmentType.value))
|
||||
const appointmentId = ref(0)
|
||||
const confirmedTypeLabel = ref('')
|
||||
const appointmentTypeLabel = computed(() => confirmedTypeLabel.value || appointmentTypeDescription(appointmentType.value))
|
||||
const serverAllowsVideo = ref(false)
|
||||
const serverAllowsAudio = ref(false)
|
||||
const callDisabledReason = ref('正在确认挂号类型')
|
||||
const canVideoCall = computed(() => serverAllowsVideo.value && canAppointmentVideoCall(appointmentType.value))
|
||||
const canAudioCall = computed(() => serverAllowsAudio.value && (canAppointmentVideoCall(appointmentType.value) || appointmentType.value === 'phone'))
|
||||
let releaseAppointmentCallGuard: (() => void) | undefined
|
||||
let chatContextVersion = 0
|
||||
let outgoingCallType = 2
|
||||
const patientId = ref<number | null>(null)
|
||||
const diagnosisId = ref<number | null>(null)
|
||||
const loadingText = ref('正在初始化...')
|
||||
@@ -197,6 +211,68 @@ const { login, logout } = useLoginState()
|
||||
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const syncChatArchive = createImChatArchiveTrigger(triggerImChatSync, () => {
|
||||
console.warn('[chat-dialog] 聊天记录归档未完成,可在诊单聊天记录中重试同步')
|
||||
})
|
||||
let chatArchiveTimer: ReturnType<typeof setInterval> | undefined
|
||||
let chatArchiveDebounce: ReturnType<typeof setTimeout> | undefined
|
||||
function queueChatArchive() {
|
||||
const id = diagnosisId.value
|
||||
if (!visible.value || !isReady.value || !id) return
|
||||
if (chatArchiveDebounce) clearTimeout(chatArchiveDebounce)
|
||||
chatArchiveDebounce = setTimeout(() => {
|
||||
chatArchiveDebounce = undefined
|
||||
void syncChatArchive(id)
|
||||
}, 500)
|
||||
}
|
||||
function stopChatArchiveWatch(flush = false) {
|
||||
if (chatArchiveTimer) clearInterval(chatArchiveTimer)
|
||||
if (chatArchiveDebounce) clearTimeout(chatArchiveDebounce)
|
||||
chatArchiveTimer = undefined
|
||||
chatArchiveDebounce = undefined
|
||||
if (flush && diagnosisId.value && isReady.value) void syncChatArchive(diagnosisId.value)
|
||||
TUIChatEngine?.chat?.off?.(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, queueChatArchive)
|
||||
}
|
||||
function startChatArchiveWatch() {
|
||||
stopChatArchiveWatch()
|
||||
TUIChatEngine?.chat?.on?.(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, queueChatArchive)
|
||||
chatArchiveTimer = setInterval(queueChatArchive, 15000)
|
||||
queueChatArchive()
|
||||
}
|
||||
|
||||
function syncAppointmentCallPolicy(res: {
|
||||
appointment_type?: string | null; appointment_type_desc?: string;
|
||||
appointment_id?: number; can_video_call?: boolean; can_audio_call?: boolean; call_disabled_reason?: string
|
||||
}) {
|
||||
if ('appointment_type' in res) appointmentType.value = res.appointment_type
|
||||
confirmedTypeLabel.value = res.appointment_type_desc || ''
|
||||
serverAllowsVideo.value = res.can_video_call === true
|
||||
serverAllowsAudio.value = res.can_audio_call === true
|
||||
callDisabledReason.value = res.call_disabled_reason || '当前挂号不支持该通话方式'
|
||||
if (Number(res.appointment_id) > 0) appointmentId.value = Number(res.appointment_id)
|
||||
}
|
||||
|
||||
async function prepareAppointmentCall(callType: number) {
|
||||
const contextVersion = chatContextVersion
|
||||
const allowed = () => callType === 1 ? canAudioCall.value : canVideoCall.value
|
||||
if (!visible.value || !allowed()) {
|
||||
const message = appointmentType.value === 'text' ? '图文问诊不支持音视频通话' : callDisabledReason.value
|
||||
feedback.msgWarning(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
// 发起前重新读取服务端挂号,防止打开聊天后问诊类型已被修改。
|
||||
const res = await getCallSignature({
|
||||
patient_id: patientId.value, diagnosis_id: diagnosisId.value, appointment_id: appointmentId.value
|
||||
})
|
||||
if (!visible.value || chatContextVersion !== contextVersion) throw new Error('当前问诊已切换,请重新发起通话')
|
||||
syncAppointmentCallPolicy(res)
|
||||
if (!allowed()) {
|
||||
feedback.msgWarning(callDisabledReason.value)
|
||||
throw new Error(callDisabledReason.value)
|
||||
}
|
||||
outgoingCallType = callType
|
||||
}
|
||||
|
||||
/** TUICallKit CallStatus(与 @tencentcloud/call-uikit-vue 一致) */
|
||||
const CALL_STATUS_IDLE = 'idle'
|
||||
const CALL_STATUS_CALLING = 'calling'
|
||||
@@ -628,6 +704,7 @@ function installTUICallKitRoomHooks() {
|
||||
const original = server[methodName]
|
||||
if (typeof original !== 'function') return
|
||||
server[methodName] = async function patched(...args: unknown[]) {
|
||||
await checkAppointmentOutgoingCall(args[0])
|
||||
const result = await original.apply(server, args)
|
||||
await flushAndBindRoomAfterTUICallApi(methodName)
|
||||
return result
|
||||
@@ -695,7 +772,8 @@ async function ensureCallRecordStarted() {
|
||||
await startCall({
|
||||
diagnosis_id: diagnosisId.value,
|
||||
patient_id: patientId.value,
|
||||
call_type: 2
|
||||
appointment_id: appointmentId.value,
|
||||
call_type: outgoingCallType
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] startCall 记录失败', e)
|
||||
@@ -939,6 +1017,9 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopChatArchiveWatch(true)
|
||||
chatContextVersion++
|
||||
releaseAppointmentCallGuard?.()
|
||||
clearLocalRecordingStartTimer()
|
||||
localCallRecorder.reset()
|
||||
tearDownCallRoomBinding?.()
|
||||
@@ -1001,7 +1082,8 @@ watch(() => activeConversation.value, async (newConversation) => {
|
||||
try {
|
||||
const res = await getCallSignature({
|
||||
patient_id: newConversation.userProfile.userID.replace("patient_", ""),
|
||||
diagnosis_id: diagnosisId.value
|
||||
diagnosis_id: diagnosisId.value,
|
||||
appointment_id: appointmentId.value
|
||||
})
|
||||
syncLochostVodFromSignature(res)
|
||||
patientUserId.value = res.patientUserId
|
||||
@@ -1016,7 +1098,11 @@ watch(() => activeConversation.value, async (newConversation) => {
|
||||
}
|
||||
})
|
||||
|
||||
const open = async (data: { patientId: number; patientName: string; diagnosisId?: number; appointmentType?: string | null }) => {
|
||||
const open = async (data: { patientId: number; patientName: string; diagnosisId?: number; appointmentId?: number; appointmentType?: string | null }) => {
|
||||
stopChatArchiveWatch(true)
|
||||
const contextVersion = ++chatContextVersion
|
||||
releaseAppointmentCallGuard?.()
|
||||
releaseAppointmentCallGuard = registerAppointmentCallGuard(prepareAppointmentCall)
|
||||
visible.value = true
|
||||
isMinimized.value = false
|
||||
posX.value = 100
|
||||
@@ -1024,6 +1110,11 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
resetCallKitPositionToLeft()
|
||||
patientName.value = data.patientName
|
||||
appointmentType.value = data.appointmentType
|
||||
appointmentId.value = Number(data.appointmentId || 0)
|
||||
confirmedTypeLabel.value = ''
|
||||
serverAllowsVideo.value = false
|
||||
serverAllowsAudio.value = false
|
||||
callDisabledReason.value = '正在确认挂号类型'
|
||||
patientId.value = data.patientId
|
||||
diagnosisId.value = data.diagnosisId || null
|
||||
error.value = ''
|
||||
@@ -1036,8 +1127,11 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
// 获取医生的签名信息
|
||||
const res = await getCallSignature({
|
||||
patient_id: data.patientId,
|
||||
diagnosis_id: data.diagnosisId || 0
|
||||
diagnosis_id: data.diagnosisId || 0,
|
||||
appointment_id: appointmentId.value
|
||||
})
|
||||
if (contextVersion !== chatContextVersion || !visible.value) return
|
||||
syncAppointmentCallPolicy(res)
|
||||
syncLochostVodFromSignature(res)
|
||||
assistant_ids.value = res.assistant_id
|
||||
console.log('后端返回的签名数据:', res)
|
||||
@@ -1123,7 +1217,9 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
|
||||
// 等待 UIKit 初始化完成后创建并激活会话
|
||||
setTimeout(async () => {
|
||||
if (contextVersion !== chatContextVersion || !visible.value) return
|
||||
isReady.value = true
|
||||
startChatArchiveWatch()
|
||||
|
||||
// 使用 nextTick 确保组件已渲染
|
||||
await nextTick()
|
||||
@@ -1164,6 +1260,10 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
|
||||
// 群组视频通话(多人通话):当前医生作为发起方,默认先邀请当前会话患者
|
||||
const startGroupVideoCall = async () => {
|
||||
if (!canVideoCall.value) {
|
||||
feedback.msgWarning(appointmentType.value === 'text' ? '图文问诊不支持视频通话' : callDisabledReason.value)
|
||||
return
|
||||
}
|
||||
if (!isCallReady.value) {
|
||||
feedback.msgWarning('通话服务初始化中,请稍后再试')
|
||||
return
|
||||
@@ -1180,7 +1280,8 @@ const startGroupVideoCall = async () => {
|
||||
console.warn('assistant_id 为空,尝试重新获取')
|
||||
const res = await getCallSignature({
|
||||
patient_id: patientId.value!,
|
||||
diagnosis_id: diagnosisId.value
|
||||
diagnosis_id: diagnosisId.value,
|
||||
appointment_id: appointmentId.value
|
||||
})
|
||||
syncLochostVodFromSignature(res)
|
||||
assistant_ids.value = res.assistant_id
|
||||
@@ -1360,6 +1461,9 @@ const onHeaderMouseUp = () => {
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
stopChatArchiveWatch(true)
|
||||
chatContextVersion++
|
||||
releaseAppointmentCallGuard?.()
|
||||
unbindImHangupMessageListener()
|
||||
// 关闭时如有通话中/拨通中,先结束本地录制再挂断(不依赖悬浮窗是否显示)
|
||||
if (isCallReady.value) {
|
||||
@@ -1529,6 +1633,12 @@ defineExpose({ open })
|
||||
}
|
||||
|
||||
/* 消息工具栏样式 */
|
||||
.text-consultation-hint {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.message-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user