453 lines
13 KiB
Vue
453 lines
13 KiB
Vue
<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>
|