This commit is contained in:
Your Name
2026-03-23 18:02:16 +08:00
parent 18fee2e8f8
commit 250d173c2f
525 changed files with 10659 additions and 793 deletions
+189 -24
View File
@@ -84,8 +84,8 @@
</el-button>
</div>
<div v-if="hasIncomingCall || callConnected" id="call-container" class="call-container">
<TUICallKit />
<div v-if="hasIncomingCall || callConnected" id="call-container" class="call-container phone-call-shell">
<TUICallKit class="TUICallKit-mobile" />
</div>
<div v-else-if="initialized" class="waiting-container">
@@ -98,18 +98,30 @@
</el-empty>
</div>
<!-- 隐藏的 TUICallKit用于接收来电信号 -->
<div v-show="false">
<TUICallKit v-if="initialized && !hasIncomingCall && !callConnected" />
<!-- 离屏挂载手机视口尺寸避免 display:none 宽高为 0与小程序端比例接近 -->
<div
v-if="initialized && !hasIncomingCall && !callConnected"
class="phone-call-kit-host phone-call-kit-host--offscreen"
aria-hidden="true"
>
<TUICallKit class="TUICallKit-mobile" />
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted, computed, onMounted } from 'vue'
import { ref, onUnmounted, computed, onMounted, nextTick } from 'vue'
import { Phone } from '@element-plus/icons-vue'
import { TUICallKitServer, TUICallKit, STATUS } from '@tencentcloud/call-uikit-vue'
import {
TUICallKitServer,
TUICallKit,
STATUS,
TUIStore,
StoreName,
NAME,
CallRole
} from '@tencentcloud/call-uikit-vue'
import { ElMessage, ElNotification } from 'element-plus'
import request from '@/utils/request'
import useUserStore from '@/stores/modules/user'
@@ -128,22 +140,109 @@ const hasIncomingCall = ref(false)
const callConnected = ref(false) // 添加通话连接状态
const incomingCallInfo = ref<any>(null)
const statusCheckTimer = ref<any>(null)
/** 从 TUIStore 同步:解决「未打开本页时来电,进入后 statusChanged 不触发」的问题 */
let storeUnwatch: (() => void) | null = null
// 页面加载时自动初始化
onMounted(() => {
initPatient()
})
// 定时检查状态(作为备用方案)
/**
* 与 SDK 内 CallStatus 一致:idle / calling / connected
* statusChanged 只在状态「变化」时回调;晚进页面时可能已在 calling,需读 Store。
*/
function syncIncomingFromTUIStore(from: string) {
try {
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) as string
const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE) as string
const isGroup = !!TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)
const wasIncoming = hasIncomingCall.value
// 被叫振铃:calling + callee
if (callStatus === 'calling' && callRole === CallRole.CALLEE) {
hasIncomingCall.value = true
incomingCallInfo.value = { isGroup, from }
if (!wasIncoming) {
ElMessage.info('检测到待接听的来电,请点击接听')
ElNotification({
title: '来电提醒',
message: '有待接听的视频通话',
type: 'warning',
duration: 0,
position: 'top-right'
})
}
callConnected.value = false
return
}
// 已接通
if (callStatus === 'connected') {
hasIncomingCall.value = false
incomingCallInfo.value = null
callConnected.value = true
return
}
// 空闲
if (callStatus === 'idle') {
if (hasIncomingCall.value || callConnected.value) {
hasIncomingCall.value = false
callConnected.value = false
incomingCallInfo.value = null
}
}
} catch (e) {
console.warn('syncIncomingFromTUIStore:', from, e)
}
}
function onStoreCallField() {
syncIncomingFromTUIStore('watch')
}
function bindTUIStoreWatch() {
if (storeUnwatch) {
storeUnwatch()
storeUnwatch = null
}
const opts = {
[NAME.CALL_STATUS]: onStoreCallField,
[NAME.CALL_ROLE]: onStoreCallField
}
try {
TUIStore.watch(StoreName.CALL, opts)
storeUnwatch = () => {
try {
TUIStore.unwatch(StoreName.CALL, opts)
} catch {
//
}
}
} catch (e) {
console.warn('TUIStore.watch 失败:', e)
}
}
// 定时从 Store 拉状态(TIM 同步可能有延迟,前 45 秒加密轮询)
const startStatusCheck = () => {
if (statusCheckTimer.value) {
clearInterval(statusCheckTimer.value)
}
let ticks = 0
const maxTicks = 90
statusCheckTimer.value = setInterval(() => {
// 这里可以添加状态检查逻辑
console.log('定时检查 - 当前时间:', new Date().toLocaleTimeString())
}, 5000)
ticks += 1
syncIncomingFromTUIStore('poll')
if (ticks >= maxTicks) {
if (statusCheckTimer.value) {
clearInterval(statusCheckTimer.value)
statusCheckTimer.value = null
}
}
}, 500)
}
// 监听状态变化
@@ -267,10 +366,18 @@ const initPatient = async () => {
ElMessage.info('等待初始化完成...')
// 等待初始化完成
// 等待 TIM/引擎就绪(未在本页时收到的邀请会在登录后由 SDK 同步)
await new Promise(resolve => setTimeout(resolve, 2000))
initialized.value = true
await nextTick()
await new Promise((r) => setTimeout(r, 300))
// 需在 TUICallKit 挂载后订阅 Store(见模板里 v-if="initialized && ..." 的隐藏组件)
bindTUIStoreWatch()
syncIncomingFromTUIStore('post-init')
startStatusCheck()
ElMessage.success('医疗助理端初始化成功,等待来电...')
console.log('=== 医疗助理端初始化完成 ===')
@@ -278,9 +385,6 @@ const initPatient = async () => {
console.log('等待来电中...')
console.log('请在医生端发起群组通话,目标用户ID应该是:', userId)
// 启动状态检查
startStatusCheck()
} catch (error: any) {
console.error('=== 初始化失败 ===', error)
ElMessage.error(error.message || '初始化失败')
@@ -289,6 +393,29 @@ const initPatient = async () => {
}
}
/** 接听后自动关闭本地麦克风(助理端只听不说) */
async function muteLocalMicAfterAccept(): Promise<boolean> {
await nextTick()
await new Promise((r) => setTimeout(r, 400))
try {
const server = TUICallKitServer as typeof TUICallKitServer & {
closeMicrophone?: () => Promise<void>
}
if (typeof server.closeMicrophone === 'function') {
await server.closeMicrophone()
return true
}
const engine = server.getTUICallEngineInstance?.()
if (engine && typeof engine.closeMicrophone === 'function') {
await engine.closeMicrophone()
return true
}
} catch (e) {
console.warn('接听后关闭麦克风失败(可在通话条手动关麦):', e)
}
return false
}
// 接听来电
const acceptCall = async () => {
try {
@@ -297,6 +424,10 @@ const acceptCall = async () => {
ElMessage.success('已接听通话')
hasIncomingCall.value = false
callConnected.value = true // 设置通话连接状态
const muted = await muteLocalMicAfterAccept()
if (muted) {
ElMessage.info('已关闭本地麦克风')
}
} catch (error: any) {
console.error('接听失败:', error)
ElMessage.error(error.message || '接听失败')
@@ -329,6 +460,11 @@ const reset = () => {
onUnmounted(() => {
if (statusCheckTimer.value) {
clearInterval(statusCheckTimer.value)
statusCheckTimer.value = null
}
if (storeUnwatch) {
storeUnwatch()
storeUnwatch = null
}
if (initialized.value) {
@@ -370,12 +506,23 @@ onUnmounted(() => {
justify-content: center;
}
/* 手机视口:375×667(逻辑像素),圆角边框模拟机身 */
.phone-call-shell {
width: 375px;
height: 667px;
max-width: 100%;
margin: 20px auto 0;
padding: 0;
border-radius: 36px;
border: 12px solid #2c2c2e;
box-shadow:
0 0 0 2px #3a3a3c inset,
0 16px 48px rgba(0, 0, 0, 0.22);
background: #000;
box-sizing: border-box;
}
.call-container {
width: 100%;
height: 500px;
margin-top: 20px;
background: #f5f5f5;
border-radius: 8px;
display: flex;
align-items: stretch;
justify-content: stretch;
@@ -391,7 +538,7 @@ onUnmounted(() => {
.waiting-container {
width: 100%;
min-height: 500px;
min-height: 360px;
margin-top: 20px;
background: #f5f5f5;
border-radius: 8px;
@@ -400,11 +547,29 @@ onUnmounted(() => {
justify-content: center;
}
/* 离屏监听:保持真实手机宽高,利于 SDK 内部布局 */
.phone-call-kit-host {
width: 375px;
height: 667px;
overflow: hidden;
border-radius: 28px;
background: #000;
}
.phone-call-kit-host--offscreen {
position: fixed;
left: -9999px;
top: 0;
z-index: -1;
pointer-events: none;
opacity: 0;
}
/* 确保 TUICallKit 组件正确显示 */
:deep(.call-container) {
.tui-call-kit {
width: 100%;
height: 500px;
height: 100%;
}
video {
@@ -434,7 +599,7 @@ onUnmounted(() => {
:deep(.tui-call-kit-window) {
width: 100% !important;
height: 100% !important;
min-height: 500px !important;
min-height: 0 !important;
}
/* 确保视频容器撑满 */