Files
zyt/admin/src/components/chat-dialog/index.vue
T
2026-03-13 14:08:52 +08:00

453 lines
13 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="`与 ${patientName} 聊天`"
width="900px"
top="10vh"
:close-on-click-modal="false"
@close="handleClose"
:destroy-on-close="true"
class="chat-dialog"
>
<div class="chat-container">
<div v-if="!isReady" class="loading-container">
<el-icon class="is-loading"><Loading /></el-icon>
<span>{{ loadingText }}</span>
</div>
<div v-else-if="error" class="error-container">
<el-alert :title="error" type="error" :closable="false" />
</div>
<div v-else class="chat-content">
<UIKitProvider language="zh-CN" theme="light">
<div class="chat-layout">
<ConversationList class="conversation-list" />
<Chat class="chat-area">
<MessageList :conversationID="conversationId" />
<MessageInput>
<template #headerToolbar>
<div class="message-toolbar">
<EmojiPicker />
<ImagePicker />
<FilePicker />
<!-- 仅在 TUICallKit 初始化成功后展示音视频入口避免初始化登录未完成错误 -->
<AudioCallPicker v-if="isCallReady" />
<VideoCallPicker v-if="isCallReady" />
<!-- 群组视频通话基于 TUICallKitServer.calls多人通话入口 -->
<el-button
v-if="isCallReady"
size="small"
type="primary"
@click="startGroupVideoCall"
>
群视频
</el-button>
</div>
</template>
</MessageInput>
</Chat>
</div>
</UIKitProvider>
</div>
</div>
<!-- 挂载 TUICallKit 组件固定居中显示 -->
<TUICallKit
v-if="isCallReady"
:allowedMinimized="true"
:allowedFullScreen="true"
class="chat-dialog-call-kit"
/>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue'
import { Loading } from '@element-plus/icons-vue'
import { getCallSignature } from '@/api/tcm'
import feedback from '@/utils/feedback'
import useUserStore from '@/stores/modules/user'
import {
ConversationList,
Chat,
MessageList,
MessageInput,
UIKitProvider,
useLoginState,
useConversationListState,
EmojiPicker,
ImagePicker,
FilePicker,
AudioCallPicker,
VideoCallPicker
} from '@tencentcloud/chat-uikit-vue3'
import TencentCloudChat from '@tencentcloud/chat'
import '@/assets/chat-uikit.css'
import { TUICallKit, TUICallKitServer, TUICallType } from '@tencentcloud/call-uikit-vue'
const assistant_ids=ref('');
const visible = ref(false)
const isReady = ref(false)
const error = ref('')
const patientName = ref('')
const patientId = ref<number | null>(null)
const diagnosisId = ref<number | null>(null)
const loadingText = ref('正在初始化...')
const patientUserId = ref('')
const conversationId = ref('')
const isCallReady = ref(false)
const { login, logout } = useLoginState()
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
const userStore = useUserStore()
// 更新用户头像到 IM
const updateUserAvatarToIM = async (avatarUrl: string,appid: number) => {
try {
if (!avatarUrl) {
console.warn('头像 URL 为空,跳过更新')
return
}
console.log('开始更新头像到 IM:', avatarUrl)
// 尝试通过 SDK 实例更新头像
// 在 UIKit 登录后,SDK 实例应该已初始化
const updateResult = await new Promise((resolve, reject) => {
// 延迟执行,确保 SDK 完全初始化
setTimeout(async () => {
try {
// 尝试多种方式获取 SDK 实例
let chat = TencentCloudChat.create( { SDKAppID: appid });
// 调用 updateMyProfile 更新用户头像
const response = await chat.updateMyProfile({
avatar: avatarUrl
})
console.log('IM 头像更新成功:', response)
resolve(response)
} catch (err) {
console.error('更新头像异常:', err)
resolve(null)
}
}, 500)
})
} catch (err: any) {
console.error('IM 头像更新失败:', err.message || err)
// 不中断主流程,头像更新失败不影响聊天功能
}
}
// 监听当前激活的会话变化,更新标题
watch(() => activeConversation.value, async (newConversation) => {
if (newConversation) {
// 更新标题为当前会话的显示名称
patientName.value = newConversation.getShowName() || patientName.value
console.log('会话切换:', newConversation.conversationID, '名称:', patientName.value, newConversation)
if (newConversation.userProfile.userID.includes("doctor")) {
console.log('当前会话是医生,跳过获取 assistant_id')
} else {
try {
const res = await getCallSignature({
patient_id: newConversation.userProfile.userID.replace("patient_", ""),
diagnosis_id: diagnosisId.value
})
patientUserId.value = res.patientUserId
assistant_ids.value = res.assistant_id
console.log('会话切换后获取到 assistant_id:', assistant_ids.value, '完整响应:', res)
} catch (err) {
console.error('获取 assistant_id 失败:', err)
}
}
console.log('会话切换完成,userID:', newConversation.userProfile.userID)
}
})
const open = async (data: { patientId: number; patientName: string; diagnosisId?: number }) => {
visible.value = true
patientName.value = data.patientName
patientId.value = data.patientId
diagnosisId.value = data.diagnosisId || null
error.value = ''
isReady.value = false
isCallReady.value = false
loadingText.value = '正在获取签名...'
try {
// 获取医生的签名信息
const res = await getCallSignature({
patient_id: data.patientId,
diagnosis_id: data.diagnosisId || 0
})
assistant_ids.value = res.assistant_id
console.log('后端返回的签名数据:', res)
console.log('assistant_id:', assistant_ids.value)
if (!res || !res.userSig) {
throw new Error('获取签名失败')
}
if (!res.assistant_id) {
console.warn('警告:后端未返回 assistant_id,群视频通话可能无法正常工作')
}
patientUserId.value = res.patientUserId || `patient_${data.patientId}`
loadingText.value = '正在登录聊天...'
// 登录 Chat UIKit
await login({
sdkAppId: Number(res.sdkAppId),
userId: res.userId,
userSig: res.userSig,
useUploadPlugin: true
})
const sdkAPPid = Number(res.sdkAppId)
// 初始化 TUICallKit(聊天内音视频通话依赖此初始化);只需全局成功一次
if (!(window as any).__TUICallKitInited__) {
loadingText.value = '正在初始化通话能力...'
try {
await TUICallKitServer.init({
userID: res.userId,
userSig: res.userSig,
SDKAppID: sdkAPPid
})
;(window as any).__TUICallKitInited__ = true
} catch (err: any) {
console.warn('TUICallKit 初始化失败,聊天仍可用,音视频通话不可用:', err?.message || err)
}
}
isCallReady.value = !!(window as any).__TUICallKitInited__
console.log('Chat UIKit 登录成功,患者用户ID:', patientUserId.value)
loadingText.value = '加载聊天界面...'
// 等待 UIKit 初始化完成后创建并激活会话
setTimeout(async () => {
isReady.value = true
// 使用 nextTick 确保组件已渲染
await nextTick()
// 增加延迟,确保 TIM SDK 完全初始化
await new Promise(resolve => setTimeout(resolve, 1000))
// 更新用户头像到 IM - 从 Pinia store 获取当前登录用户的头像
const avatar = userStore.userInfo?.avatar
console.log('准备更新头像,avatar:', avatar)
if (avatar) {
await updateUserAvatarToIM(avatar,sdkAPPid)
}
// 创建或获取与患者的单聊会话
try {
const conversation = await createC2CConversation(patientUserId.value)
conversationId.value = conversation.conversationID
console.log('已创建/获取会话:', conversationId.value)
// 激活该会话
setActiveConversation(conversationId.value)
console.log('已激活会话')
} catch (err) {
console.error('创建/激活会话失败:', err)
}
}, 800)
} catch (err: any) {
console.error('初始化聊天失败:', err)
error.value = err.message || '初始化聊天失败'
feedback.msgError(error.value)
}
}
// 群组视频通话(多人通话):当前医生作为发起方,默认先邀请当前会话患者
const startGroupVideoCall = async () => {
if (!isCallReady.value) {
feedback.msgWarning('通话服务初始化中,请稍后再试')
return
}
if (!patientUserId.value) {
feedback.msgWarning('未找到患者通话ID,无法发起群视频')
return
}
try {
// 确保 assistant_id 存在
if (!assistant_ids.value) {
console.warn('assistant_id 为空,尝试重新获取')
const res = await getCallSignature({
patient_id: patientId.value!,
diagnosis_id: diagnosisId.value
})
assistant_ids.value = res.assistant_id
console.log('重新获取 assistant_id:', assistant_ids.value)
}
// 构建用户列表,过滤掉空值
const userIDList = [patientUserId.value, assistant_ids.value].filter(id => id)
if (userIDList.length < 2) {
feedback.msgWarning('助理ID获取失败,无法发起群视频')
console.error('userIDList 不完整:', userIDList, 'assistant_ids:', assistant_ids.value)
return
}
console.log('发起群视频通话,参与者:', userIDList)
await TUICallKitServer.calls({
userIDList,
type: TUICallType.VIDEO_CALL
})
} catch (err: any) {
console.error('发起群视频失败:', err)
feedback.msgError(err?.message || '发起群视频失败')
}
}
const handleClose = async () => {
try {
// 登出 Chat
// await logout()
} catch (err) {
console.error('登出失败:', err)
}
visible.value = false
// isReady.value = false
// error.value = ''
// patientId.value = null
// diagnosisId.value = null
// patientUserId.value = ''
// conversationId.value = ''
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.chat-dialog {
:deep(.el-dialog__body) {
padding: 0;
height: 75vh;
overflow: visible;
}
:deep(.el-dialog__header) {
padding: 16px 20px;
border-bottom: 1px solid #e4e7ed;
}
// 确保对话框有合适的 z-index
:deep(.el-overlay) {
z-index: 2500;
}
// 确保对话框本身的 z-index 低于 TUICallKit
:deep(.el-dialog) {
z-index: 2501;
}
}
.chat-container {
height: 75vh;
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #909399;
.el-icon {
font-size: 32px;
}
}
.error-container {
width: 100%;
padding: 20px;
}
.chat-content {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.chat-layout {
display: flex;
height: 100%;
width: 100%;
.conversation-list {
width: 285px;
flex-shrink: 0;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.empty-chat {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 消息工具栏样式 */
.message-toolbar {
display: flex;
gap: 8px;
align-items: center;
}
/* TUICallKit 容器样式 - 限制在对话框内拖动 */
.chat-dialog-call-kit {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: 600px !important;
height: 400px !important;
max-width: 90% !important;
max-height: 80% !important;
z-index: 300000 !important;
pointer-events: auto !important;
}
</style>
<style scoped lang="scss">
/* 全局样式:确保表情选择器等弹出层有足够高的 z-index */
.el-popper,
.el-picker-panel,
[data-reka-popper],
[data-reka-popper-content-wrapper],
[class*="emoji-picker"],
[class*="picker-popup"],
[class*="tui-picker"],
body > div[id^="el-popper-container"],
body > div[data-reka-focus-guard] {
z-index: 9999 !important;
}
/* 确保聊天对话框内的弹出层也能正确显示 */
.chat-dialog [data-reka-popper-content-wrapper] {
z-index: 9999 !important;
}
</style>