Files
zyt/admin/src/components/chat-dialog/index.vue
T
2026-03-11 09:49:47 +08:00

460 lines
12 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 />
<AudioCallPicker />
<VideoCallPicker />
</div>
</template>
</MessageInput>
</Chat>
</div>
<!-- TUICallKit 放在 dialog 外部独立渲染 -->
</UIKitProvider>
</div>
</div>
<TUICallKit
: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 } from '@tencentcloud/call-uikit-vue'
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 { 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, (newConversation) => {
if (newConversation) {
// 更新标题为当前会话的显示名称
patientName.value = newConversation.getShowName() || patientName.value
console.log('会话切换:', newConversation.conversationID, '名称:', patientName.value)
}
})
// 限制 TUICallKit 拖动范围 - 使用事件监听而不是 MutationObserver
const setupCallKitDragLimit = () => {
setTimeout(() => {
const callKitElements = document.querySelectorAll('[class*="TUICallKit"], [id*="tui-call"]')
callKitElements.forEach((element: any) => {
if (!element) return
// 监听鼠标释放事件(拖动结束)
element.addEventListener('mouseup', () => {
setTimeout(() => {
const rect = element.getBoundingClientRect()
const dialogElement = document.querySelector('.chat-dialog .el-dialog')
if (!dialogElement) return
const dialogRect = dialogElement.getBoundingClientRect()
// 获取当前位置
const currentLeft = parseInt(element.style.left || '0')
const currentTop = parseInt(element.style.top || '0')
let newLeft = currentLeft
let newTop = currentTop
let needAdjust = false
// 限制左边界
if (rect.left < dialogRect.left) {
newLeft = currentLeft + (dialogRect.left - rect.left)
needAdjust = true
}
// 限制右边界
if (rect.right > dialogRect.right) {
newLeft = currentLeft - (rect.right - dialogRect.right)
needAdjust = true
}
// 限制上边界
if (rect.top < dialogRect.top) {
newTop = currentTop + (dialogRect.top - rect.top)
needAdjust = true
}
// 限制下边界
if (rect.bottom > dialogRect.bottom) {
newTop = currentTop - (rect.bottom - dialogRect.bottom)
needAdjust = true
}
// 应用新位置(使用过渡动画)
if (needAdjust) {
element.style.transition = 'left 0.3s ease, top 0.3s ease'
element.style.left = `${newLeft}px`
element.style.top = `${newTop}px`
setTimeout(() => {
element.style.transition = ''
}, 300)
}
}, 10)
})
})
}, 1000)
}
// 监听 isReady 变化,当通话组件加载后设置拖动限制
watch(() => isReady.value, (ready) => {
if (ready) {
setupCallKitDragLimit()
}
})
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
loadingText.value = '正在获取签名...'
try {
// 获取医生的签名信息
const res = await getCallSignature({
patient_id: data.patientId,
diagnosis_id: data.diagnosisId || 0
})
console.log('后端返回的签名数据:', res)
if (!res || !res.userSig) {
throw new Error('获取签名失败')
}
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);
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 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;
}
</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;
}
/* 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;
}
.float-window-container{
z-index: 3000;
}
/* 限制 TUICallKit 的拖动范围在对话框内 */
.chat-content {
position: relative !important;
}
/* 确保 TUICallKit 相关的所有容器都在对话框之上 */
body > div[class*="TUICallKit"],
body > div[id*="tui-call"],
body > div[class*="t-call"],
body > .TUICallKit-desktop,
body > .TUICallKit-mobile {
position: fixed !important;
z-index: 300000 !important;
max-width: 90vw !important;
max-height: 90vh !important;
pointer-events: auto !important;
}
/* 确保最小化后的通话窗口也在最上层 */
[class*="TUICallKit"][class*="minimized"],
[class*="TUICallKit"][class*="minimize"],
[class*="call-kit-mini"],
[class*="t-call-mini"] {
z-index: 300000 !important;
pointer-events: auto !important;
}
</style>