Files
zyt/admin/src/components/video-call/index.vue
T
2026-03-23 18:02:16 +08:00

1026 lines
31 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>
<!-- 可拖拽可调整大小的浮动窗口 -->
<div
v-if="visible"
class="floating-video-window"
:style="windowStyle"
ref="windowRef"
>
<!-- 窗口头部 - 可拖拽区域 -->
<div
class="window-header"
@mousedown="startDrag"
>
<div class="window-title">
<el-icon><VideoCamera /></el-icon>
<span>{{ callInfo.isGroup ? '群组视频通话' : '视频通话' }} - {{ callInfo.patientName }}</span>
</div>
</div>
<!-- 窗口内容 -->
<div class="window-content">
<div class="video-call-container">
<!-- 被拒绝状态 -->
<div v-if="callRejected" class="call-rejected">
<el-icon class="rejected-icon" :size="60" color="#F56C6C"><CircleClose /></el-icon>
<p class="mt-4 text-lg">{{ statusText }}</p>
<p class="text-gray-500">{{ callInfo.patientName }}</p>
</div>
<!-- 初始化前的等待状态 -->
<div v-else-if="!isInitialized" class="call-waiting">
<el-icon class="loading-icon" :size="60"><VideoCamera /></el-icon>
<p class="mt-4">{{ statusText }}</p>
<p class="text-gray-500">{{ callInfo.patientName }}</p>
</div>
<!-- TUICallKit 视频通话界面 -->
<div v-else class="video-wrapper">
<!-- TUICallKit 放在我们的容器中并隐藏其默认全屏 UI -->
<TUICallKit
class="tui-call-kit"
:allowedFullScreen="false"
:allowedMinimized="false"
/>
<!-- 等待接听时的浮层提示 -->
<div v-if="calling && !callConnected" class="waiting-overlay">
<div class="waiting-content">
<el-icon class="waiting-icon" :size="40"><Phone /></el-icon>
<p class="waiting-text">{{ statusText }}</p>
</div>
</div>
</div>
</div>
<!-- 窗口底部操作栏 -->
<div class="window-footer">
<el-button
@click="handleClose"
:disabled="calling && !callRejected"
size="small"
>
{{ callRejected ? '关闭' : '取消' }}
</el-button>
<el-button
v-if="!isInitialized && !callRejected"
type="primary"
@click="startCall"
:loading="initializing"
size="small"
>
开始通话
</el-button>
<el-button
v-else-if="!callRejected"
type="danger"
@click="handleHangup"
size="small"
>
挂断
</el-button>
</div>
</div>
<!-- 调整大小的拖拽点 -->
<div
class="resize-handle resize-handle-se"
@mousedown="startResize"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted, nextTick, computed } from 'vue'
import { VideoCamera, CircleClose, Phone } from '@element-plus/icons-vue'
import { getCallSignature, endCall } from '@/api/tcm'
import feedback from '@/utils/feedback'
import { formatTUICallUserError } from '@/utils/tuicall-error'
import { TUICallKitAPI, TUICallKit, TUICallType, STATUS } from '@trtc/calls-uikit-vue'
interface CallInfo {
diagnosisId: number
patientId: number
patientName: string
userId?: string // 一对一通话时使用
assistantId?: number // 群组通话时的医助ID
userIds?: string[] // 群组通话时的用户ID列表
isGroup: boolean // 是否为群组通话
}
const visible = ref(false)
const isInitialized = ref(false)
const initializing = ref(false)
const calling = ref(false)
const callConnected = ref(false)
const callRejected = ref(false)
const shouldAutoClose = ref(false) // 标记是否应该自动关闭
const statusText = ref('准备通话...')
const callInfo = ref<CallInfo>({
diagnosisId: 0,
patientId: 0,
patientName: '',
userId: '',
isGroup: false
})
// 窗口位置和大小
const windowRef = ref<HTMLElement>()
const windowPosition = ref({ x: 100, y: 100 })
const windowSize = ref({ width: 300, height: 750 })
// 拖拽相关
const isDragging = ref(false)
const dragStart = ref({ x: 10, y: 0 })
// 调整大小相关
const isResizing = ref(false)
const resizeStart = ref({ x: 0, y: 0, width: 0, height: 0 })
// 计算窗口样式
const windowStyle = computed(() => ({
left: `${windowPosition.value.x}px`,
top: `${windowPosition.value.y}px`,
width: `${windowSize.value.width}px`,
height: `${windowSize.value.height}px`
}))
// 开始拖拽
const startDrag = (e: MouseEvent) => {
isDragging.value = true
dragStart.value = {
x: e.clientX - windowPosition.value.x,
y: e.clientY - windowPosition.value.y
}
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
e.preventDefault()
}
const onDrag = (e: MouseEvent) => {
if (!isDragging.value) return
windowPosition.value = {
x: e.clientX - dragStart.value.x,
y: e.clientY - dragStart.value.y
}
}
const stopDrag = () => {
isDragging.value = false
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
}
// 开始调整大小
const startResize = (e: MouseEvent) => {
isResizing.value = true
resizeStart.value = {
x: e.clientX,
y: e.clientY,
width: windowSize.value.width,
height: windowSize.value.height
}
document.addEventListener('mousemove', onResize)
document.addEventListener('mouseup', stopResize)
e.preventDefault()
e.stopPropagation()
}
const onResize = (e: MouseEvent) => {
if (!isResizing.value) return
const deltaX = e.clientX - resizeStart.value.x
const deltaY = e.clientY - resizeStart.value.y
windowSize.value = {
width: Math.max(400, resizeStart.value.width + deltaX),
height: Math.max(300, resizeStart.value.height + deltaY)
}
}
const stopResize = () => {
isResizing.value = false
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
}
// 监听通话状态变化
const handleStatusChange = ({ oldStatus, newStatus }: any) => {
console.log('========== statusChanged 回调触发 ==========')
console.log('状态变化:', {
oldStatus,
newStatus,
calling: calling.value,
callConnected: callConnected.value,
visible: visible.value
})
console.log('STATUS 常量:', {
IDLE: STATUS.IDLE,
DIALING_C2C: STATUS.DIALING_C2C,
CALLING_C2C_VIDEO: STATUS.CALLING_C2C_VIDEO
})
// 通话接通
if (newStatus === STATUS.CALLING_C2C_VIDEO || newStatus === STATUS.CALLING_GROUP_VIDEO ||
newStatus === STATUS.CALLING_C2C_AUDIO || newStatus === STATUS.CALLING_GROUP_AUDIO) {
console.log('>>> 通话已接通')
callRejected.value = false
callConnected.value = true
calling.value = true
shouldAutoClose.value = false // 通话接通时,不自动关闭
statusText.value = '通话中...'
feedback.msgSuccess('通话已接通')
// 如果是多人通话且还有其他用户需要邀请
if (callInfo.value.isGroup && callInfo.value.userIds && callInfo.value.userIds.length > 1) {
inviteOtherUsers()
}
}
// 正在呼叫
if (newStatus === STATUS.DIALING_C2C || newStatus === STATUS.DIALING_GROUP) {
console.log('>>> 正在呼叫')
callConnected.value = false
calling.value = true
shouldAutoClose.value = false
statusText.value = '等待对方接听...'
}
// 通话结束,回到空闲状态
if (newStatus === STATUS.IDLE) {
console.log('>>> 回到空闲状态')
// 从呼叫状态直接回到空闲 - 对方拒绝或未接听
if (oldStatus === STATUS.DIALING_C2C || oldStatus === STATUS.DIALING_GROUP) {
console.log('>>> 对方拒绝或未接听')
callRejected.value = true
callConnected.value = false
// 根据具体情况给出不同的提示
const rejectReasons = [
'对方已拒绝通话',
'对方未接听',
'对方设备不支持视频通话',
'对方网络连接失败',
'对方麦克风或摄像头未授权'
]
// 这里可以根据实际错误码判断具体原因
statusText.value = '对方未能接听'
feedback.msgWarning('通话未能接通,可能原因:对方拒绝、未接听、设备不支持或网络问题')
calling.value = false
shouldAutoClose.value = true
setTimeout(() => {
if (visible.value && shouldAutoClose.value) {
console.log('>>> 5秒后关闭窗口(拒绝)')
visible.value = false
isInitialized.value = false
statusText.value = '准备通话...'
}
}, 5000) // 延长到5秒,让用户看清提示
}
// 从通话中状态回到空闲 - 通话已挂断
else if (oldStatus === STATUS.CALLING_C2C_VIDEO || oldStatus === STATUS.CALLING_GROUP_VIDEO ||
oldStatus === STATUS.CALLING_C2C_AUDIO || oldStatus === STATUS.CALLING_GROUP_AUDIO) {
console.log('>>> 通话中挂断')
feedback.msg('通话已结束')
calling.value = false
callConnected.value = false
shouldAutoClose.value = true
setTimeout(() => {
if (visible.value && shouldAutoClose.value) {
console.log('>>> 1秒后关闭窗口(挂断)')
visible.value = false
isInitialized.value = false
statusText.value = '准备通话...'
}
}, 1000)
}
// 其他情况也关闭窗口
else if (calling.value || callConnected.value) {
console.log('>>> 其他情况结束')
calling.value = false
callConnected.value = false
shouldAutoClose.value = true
setTimeout(() => {
if (visible.value && shouldAutoClose.value) {
console.log('>>> 1秒后关闭窗口(其他)')
visible.value = false
isInitialized.value = false
statusText.value = '准备通话...'
}
}, 1000)
} else {
console.log('>>> 未匹配任何关闭条件')
}
}
}
// 监听通话错误
const handleCallError = (error: any) => {
console.error('========== 通话错误 ==========', error)
let errorMessage = '通话失败'
// 根据错误码给出具体提示
if (error.code) {
switch (error.code) {
case 60006:
errorMessage = '当前环境不支持 WebRTC,请使用 HTTPS 访问或在 localhost 测试'
break
case 60007:
errorMessage = '对方设备不支持视频通话'
break
case 60008:
errorMessage = '对方拒绝了通话'
break
case 60009:
errorMessage = '对方未接听'
break
case 60010:
errorMessage = '对方忙线中'
break
case 60011:
errorMessage = '对方不在线'
break
case 60012:
errorMessage = '网络连接失败'
break
case 60013:
errorMessage = '对方麦克风或摄像头未授权'
break
default:
errorMessage = error.message || '通话失败,请稍后重试'
}
}
feedback.msgError(errorMessage)
callRejected.value = true
statusText.value = errorMessage
calling.value = false
shouldAutoClose.value = true
setTimeout(() => {
if (visible.value && shouldAutoClose.value) {
visible.value = false
isInitialized.value = false
}
}, 5000)
}
// 邀请其他用户加入通话
const inviteOtherUsers = async () => {
if (!callInfo.value.userIds || callInfo.value.userIds.length <= 1) {
return
}
console.log('开始邀请其他用户加入通话')
// 从第二个用户开始邀请
for (let i = 1; i < callInfo.value.userIds.length; i++) {
try {
const userId = callInfo.value.userIds[i]
console.log(`正在邀请用户: ${userId}`)
// 使用 calls 方法邀请用户(在已有通话的情况下会自动转为邀请加入)
await TUICallKitAPI.calls({
userIDList: [userId],
type: TUICallType.VIDEO_CALL
})
console.log(`已成功邀请用户: ${userId}`)
feedback.msgSuccess(`已邀请 ${userId} 加入通话`)
// 等待一下再邀请下一个
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
console.error(`邀请用户失败: ${callInfo.value.userIds[i]}`, error)
feedback.msgWarning(`邀请 ${callInfo.value.userIds[i]} 失败`)
}
}
console.log('所有用户邀请完成')
}
// 通话结束后的回调(兜底机制)
const handleAfterCalling = () => {
console.log('========== afterCalling 回调触发 ==========')
console.log('当前状态:', {
visible: visible.value,
calling: calling.value,
callConnected: callConnected.value,
shouldAutoClose: shouldAutoClose.value,
isInitialized: isInitialized.value
})
// 只有在标记为应该自动关闭时才关闭
// 这样可以避免在通话接通时误触发
if (visible.value && shouldAutoClose.value) {
feedback.msg('通话已结束')
calling.value = false
callConnected.value = false
setTimeout(() => {
if (visible.value && shouldAutoClose.value) {
console.log('通过 afterCalling 关闭窗口')
visible.value = false
isInitialized.value = false
statusText.value = '准备通话...'
}
}, 1000)
} else {
console.log('afterCalling 触发但不关闭窗口(shouldAutoClose=false')
}
}
const open = async (info: CallInfo) => {
// 检查 WebRTC 支持
if (!checkWebRTCSupport()) {
return
}
callInfo.value = info
visible.value = true
isInitialized.value = false
calling.value = false
callConnected.value = false
callRejected.value = false
shouldAutoClose.value = false // 重置自动关闭标志
statusText.value = '准备通话...'
// 居中显示窗口
const screenWidth = window.innerWidth
const screenHeight = window.innerHeight
windowPosition.value = {
x: (screenWidth - windowSize.value.width) / 2,
y: (screenHeight - windowSize.value.height) / 2
}
}
// 检查 WebRTC 支持
const checkWebRTCSupport = () => {
const isHttps = window.location.protocol === 'https:' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1'
const hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
const hasRTCPeerConnection = !!window.RTCPeerConnection
console.log('WebRTC 环境检测:', {
protocol: window.location.protocol,
hostname: window.location.hostname,
isHttps,
hasGetUserMedia,
hasRTCPeerConnection
})
if (!isHttps) {
feedback.msgError('视频通话需要 HTTPS 环境。请使用 HTTPS 协议访问,或在 localhost 下测试。')
return false
}
if (!hasGetUserMedia) {
feedback.msgError('当前浏览器不支持访问摄像头和麦克风,请使用 Chrome、Edge 或 Safari 浏览器。')
return false
}
if (!hasRTCPeerConnection) {
feedback.msgError('当前浏览器不支持 WebRTC,请升级浏览器版本。')
return false
}
return true
}
const startCall = async () => {
let res: any = null // 保存签名响应,供后续使用
try {
initializing.value = true
callRejected.value = false
statusText.value = '正在获取签名...'
// 获取腾讯云签名
try {
res = await getCallSignature({
diagnosis_id: callInfo.value.diagnosisId,
patient_id: callInfo.value.patientId
})
} catch (error: any) {
console.error('获取签名失败:', error)
throw new Error('获取通话签名失败,请检查网络连接或稍后重试')
}
if (!res || !res.userSig) {
throw new Error('获取签名失败,返回数据不完整')
}
console.log('签名获取成功:', {
doctorUserId: res.userId,
patientUserId: res.patientUserId,
sdkAppId: res.sdkAppId
})
statusText.value = '正在初始化通话组件...'
// 设置状态变化回调和通话结束回调
// 注意:4.x 版本移除了 onError 回调,错误处理通过 try-catch 和状态判断
TUICallKitAPI.setCallback({
statusChanged: handleStatusChange,
afterCalling: handleAfterCalling
})
// 禁用浮窗功能,防止 TUICallKit 创建自己的全屏层
TUICallKitAPI.enableFloatWindow(false)
// 初始化 TUICallKit
try {
await TUICallKitAPI.init({
userID: res.userId,
userSig: res.userSig,
SDKAppID: Number(res.sdkAppId)
})
console.log('TUICallKit init 方法调用完成')
} catch (error: any) {
console.error('TUICallKit 初始化失败:', error)
// 根据错误码给出具体提示
if (error.code === 60006) {
throw new Error('当前环境不支持 WebRTC,请使用 HTTPS 访问或在 localhost 测试')
} else if (error.message && error.message.includes('getUserMedia')) {
throw new Error('无法访问摄像头或麦克风,请检查设备权限设置')
} else if (error.message && error.message.includes('network')) {
throw new Error('网络连接失败,请检查网络设置')
} else {
throw new Error(`初始化失败: ${error.message || '未知错误'}`)
}
}
statusText.value = '等待初始化完成...'
await new Promise(resolve => setTimeout(resolve, 2000))
// 生产包中 init 返回后引擎可能尚未挂载,短暂轮询等待 .calls 可用
for (let i = 0; i < 6; i++) {
if (typeof TUICallKitAPI.calls === 'function') break
await new Promise(resolve => setTimeout(resolve, 500))
}
if (typeof TUICallKitAPI.calls !== 'function') {
throw new Error('通话引擎未就绪,请关闭窗口后重试或刷新页面')
}
console.log('初始化完成,显示通话界面')
statusText.value = '准备发起通话...'
// 设置初始化完成标志,显示 TUICallKit 组件
isInitialized.value = true
calling.value = true
// 等待 DOM 更新和组件渲染
await nextTick()
await new Promise(resolve => setTimeout(resolve, 500))
// 发起通话
try {
if (callInfo.value.isGroup && callInfo.value.userIds && callInfo.value.userIds.length > 0) {
// 多人通话 - 依次邀请所有用户
console.log('准备发起多人通话,目标用户:', callInfo.value.userIds)
// 先邀请第一个用户建立通话
await TUICallKitAPI.calls({
userIDList: [callInfo.value.userIds[0]],
type: TUICallType.VIDEO_CALL
})
console.log(`已邀请第一个用户: ${callInfo.value.userIds[0]}`)
if (callInfo.value.userIds.length > 1) {
statusText.value = `等待 ${callInfo.value.userIds[0]} 接听后将邀请其他成员...`
feedback.msgSuccess(`已邀请第一位成员,接通后将邀请其他 ${callInfo.value.userIds.length - 1} 位成员`)
} else {
statusText.value = '等待成员接听...'
feedback.msgSuccess('多人通话已发起,等待成员接听...')
}
console.log('多人通话已发起')
} else {
// 一对一通话 - 使用后端返回的 patientUserId
const targetUserId = res.patientUserId || callInfo.value.userId || ''
console.log('=== 发起一对一通话 ===')
console.log('后端返回的 patientUserId:', res.patientUserId)
console.log('前端传入的 userId:', callInfo.value.userId)
console.log('最终使用的 targetUserId:', targetUserId)
console.log('完整的 res 对象:', res)
if (!targetUserId) {
throw new Error('无法获取患者通话ID')
}
await TUICallKitAPI.calls({
userIDList: [targetUserId],
type: TUICallType.VIDEO_CALL
})
console.log('通话已发起,目标用户:', targetUserId)
statusText.value = '等待对方接听...'
feedback.msgSuccess('通话已发起,等待对方接听...')
}
} catch (error: any) {
console.error('发起通话失败:', error)
// 根据错误类型给出具体提示
if (error.code === 60011) {
throw new Error('对方不在线,无法发起通话')
} else if (error.code === 60010) {
throw new Error('对方忙线中,请稍后再试')
} else if (error.message && error.message.includes('getUserMedia')) {
throw new Error('无法访问摄像头或麦克风,请在浏览器设置中允许访问')
} else {
throw new Error(`发起通话失败: ${error.message || '未知错误'}`)
}
}
} catch (error: any) {
console.error('启动通话失败:', error)
const errorMessage = formatTUICallUserError(error, '启动通话失败,请稍后重试')
feedback.msgError(errorMessage)
callRejected.value = true
statusText.value = errorMessage
isInitialized.value = false
calling.value = false
shouldAutoClose.value = true
// 5秒后自动关闭错误提示窗口
setTimeout(() => {
if (visible.value && shouldAutoClose.value && callRejected.value) {
visible.value = false
statusText.value = '准备通话...'
}
}, 5000)
} finally {
initializing.value = false
}
}
const handleHangup = async () => {
try {
await TUICallKitAPI.hangup()
feedback.msgSuccess('已挂断通话')
await handleCallEnd()
visible.value = false
isInitialized.value = false
calling.value = false
callConnected.value = false
} catch (error) {
console.error('挂断失败:', error)
}
}
const handleCallEnd = async () => {
try {
// 记录通话结束
await endCall({
diagnosis_id: callInfo.value.diagnosisId
})
} catch (error) {
console.error('记录通话结束失败:', error)
}
}
const handleClose = async () => {
if (calling.value && !callRejected.value) {
try {
await feedback.confirm('通话进行中,确定要结束吗?')
} catch {
return
}
// 挂断通话
try {
shouldAutoClose.value = true // 手动关闭时设置标志
await TUICallKitAPI.hangup()
} catch (error) {
console.error('挂断通话失败:', error)
}
}
// 记录通话结束
if (isInitialized.value) {
await handleCallEnd()
}
// 重置所有状态
visible.value = false
isInitialized.value = false
calling.value = false
callConnected.value = false
callRejected.value = false
shouldAutoClose.value = false
statusText.value = '准备通话...'
// 等待一下再销毁,确保 TUICallKit 完全清理
await nextTick()
}
onUnmounted(() => {
// 组件卸载时清理
if (calling.value) {
TUICallKitAPI.hangup().catch(console.error)
}
// 清理事件监听
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
})
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.floating-video-window {
position: fixed;
z-index: 2000;
background: white;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 400px;
min-height: 300px;
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: move;
user-select: none;
position: relative;
z-index: 2001;
.window-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 14px;
}
.window-actions {
display: flex;
gap: 4px;
position: relative;
z-index: 2002;
:deep(.el-button) {
color: white;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
}
.window-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.video-call-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
position: relative;
min-height: 0;
}
.video-wrapper {
width: 100%;
height: 100%;
position: relative;
}
.call-waiting {
text-align: center;
.loading-icon {
animation: pulse 2s ease-in-out infinite;
}
}
.call-rejected {
text-align: center;
.rejected-icon {
animation: shake 0.5s ease-in-out;
}
.text-lg {
font-size: 18px;
font-weight: 500;
color: #F56C6C;
}
.text-gray-500 {
color: #909399;
margin-top: 8px;
}
}
.waiting-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: none;
}
.waiting-content {
text-align: center;
color: white;
.waiting-icon {
animation: ring 1.5s ease-in-out infinite;
margin-bottom: 12px;
}
.waiting-text {
font-size: 16px;
font-weight: 500;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
}
.window-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #e4e7ed;
background: white;
position: relative;
z-index: 2001;
}
/* 调整大小拖拽点 */
.resize-handle {
position: absolute;
background: transparent;
z-index: 2001;
}
.resize-handle-se {
right: 0;
bottom: 0;
width: 16px;
height: 16px;
cursor: se-resize;
&::after {
content: '';
position: absolute;
right: 2px;
bottom: 2px;
width: 12px;
height: 12px;
border-right: 2px solid #dcdfe6;
border-bottom: 2px solid #dcdfe6;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
@keyframes ring {
0%, 100% {
transform: rotate(-15deg);
}
50% {
transform: rotate(15deg);
}
}
.tui-call-kit {
width: 100%;
height: 100%;
}
.mt-4 {
margin-top: 16px;
}
/* 确保 TUICallKit 组件内的视频元素正确显示,但不超过我们的控制层 */
:deep(.tui-call-kit) {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 确保视频容器有正确的尺寸 */
.tui-call-kit-video-container,
.tui-video-container,
.video-container {
width: 100% !important;
height: 100% !important;
}
/* 确保本地和远程视频都正确显示 */
.local-video,
.remote-video {
width: 100% !important;
height: 100% !important;
}
/* 限制 TUICallKit 内部元素的 z-index,防止遮挡我们的控制按钮 */
* {
z-index: auto !important;
}
/* 但保持视频元素的层级 */
video,
.video-container,
.tui-video-container {
z-index: 1 !important;
}
}
/* 全局覆盖 TUICallKit 可能创建的全屏层 - 完全隐藏 */
:global(body > #tui-call-kit-id) {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* 只在我们的窗口内显示 TUICallKit */
.floating-video-window :deep(.tui-call-kit) {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* 只在我们的浮动窗口内显示 TUICallKit */
.floating-video-window .TUICallKit-desktop,
.floating-video-window .TUICallKit-mobile {
display: block !important;
position: static !important;
width: 100% !important;
height: 100% !important;
transform: none !important;
}
/* 确保 Element Plus 的遮罩层不会覆盖视频通话 */
:global(.el-overlay) {
z-index: 1000;
}
</style>