更新
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import { createApp, nextTick, readonly, ref } from 'vue'
|
||||
import {
|
||||
NAME,
|
||||
StoreName,
|
||||
TUIStore,
|
||||
TUICallKitAPI,
|
||||
TUICallType,
|
||||
} from '@trtc/calls-uikit-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
type CallPhase = 'ready' | 'starting' | 'dialing' | 'connected' | 'ended' | 'error'
|
||||
|
||||
interface NormalizedCallConfig {
|
||||
SDKAppID: number
|
||||
userID: string
|
||||
userSig: string
|
||||
targetUserId: string
|
||||
diagnosisId: number | string
|
||||
}
|
||||
|
||||
interface BridgeMessage {
|
||||
source: 'doctor-call'
|
||||
event: 'ready' | 'status' | 'room' | 'hangup' | 'error'
|
||||
diagnosisId?: number | string
|
||||
status?: string
|
||||
roomId?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const phase = ref<CallPhase>('ready')
|
||||
const statusText = ref('等待桌面端发起视频面诊')
|
||||
let activeConfig: NormalizedCallConfig | null = null
|
||||
let endNotified = false
|
||||
let starting = false
|
||||
let emittedRoomId = ''
|
||||
|
||||
function initializeQtWebChannel(): void {
|
||||
const transport = window.qt?.webChannelTransport
|
||||
const QWebChannel = window.QWebChannel
|
||||
if (!transport || typeof QWebChannel !== 'function') return
|
||||
|
||||
try {
|
||||
new QWebChannel(transport, (channel) => {
|
||||
const bridge = channel.objects.qtVideoBridge
|
||||
if (bridge) window.qtVideoBridge = bridge
|
||||
emit({ source: 'doctor-call', event: 'ready' })
|
||||
})
|
||||
} catch {
|
||||
console.warn('[doctor-call] Qt WebChannel 初始化失败,将使用 postMessage 通知')
|
||||
}
|
||||
}
|
||||
|
||||
function postToHost(message: BridgeMessage): boolean {
|
||||
let delivered = false
|
||||
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage(message, '*')
|
||||
delivered = true
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin host may reject access; opener remains available as a fallback.
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage(message, '*')
|
||||
delivered = true
|
||||
}
|
||||
} catch {
|
||||
// The console fallback below is intentionally non-sensitive.
|
||||
}
|
||||
|
||||
return delivered
|
||||
}
|
||||
|
||||
function emit(message: BridgeMessage): void {
|
||||
const bridge = window.qtVideoBridge
|
||||
if (bridge && typeof bridge.notify === 'function') {
|
||||
try {
|
||||
bridge.notify(JSON.stringify(message))
|
||||
return
|
||||
} catch {
|
||||
// A detached WebChannel object is equivalent to an unavailable bridge.
|
||||
}
|
||||
}
|
||||
|
||||
if (!postToHost(message)) {
|
||||
console.info('[doctor-call]', message.event, message.status ?? message.message ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
function cleanString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new Error(`${field} 不能为空`)
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function normalizeConfig(config: DoctorCallConfig): NormalizedCallConfig {
|
||||
if (!config || typeof config !== 'object') throw new Error('通话配置无效')
|
||||
|
||||
const rawSdkAppId = config.SDKAppID ?? config.sdkAppId
|
||||
const SDKAppID = Number(rawSdkAppId)
|
||||
if (!Number.isSafeInteger(SDKAppID) || SDKAppID <= 0) {
|
||||
throw new Error('SDKAppID 必须是正整数')
|
||||
}
|
||||
|
||||
const diagnosisId = config.diagnosisId
|
||||
if (
|
||||
diagnosisId === undefined
|
||||
|| diagnosisId === null
|
||||
|| (typeof diagnosisId === 'string' && diagnosisId.trim() === '')
|
||||
) {
|
||||
throw new Error('diagnosisId 不能为空')
|
||||
}
|
||||
|
||||
return {
|
||||
SDKAppID,
|
||||
userID: cleanString(config.userID ?? config.userId, 'userID'),
|
||||
userSig: cleanString(config.userSig, 'userSig'),
|
||||
targetUserId: cleanString(config.targetUserId ?? config.patientUserId, 'targetUserId'),
|
||||
diagnosisId: typeof diagnosisId === 'string' ? diagnosisId.trim() : diagnosisId,
|
||||
}
|
||||
}
|
||||
|
||||
function safeErrorMessage(error: unknown): string {
|
||||
let message = error instanceof Error ? error.message : '视频通话发生未知错误'
|
||||
if (activeConfig?.userSig) message = message.split(activeConfig.userSig).join('[REDACTED]')
|
||||
return message
|
||||
.replace(/(user\s*sig\s*[:=]\s*)[^\s,;&]+/gi, '$1[REDACTED]')
|
||||
.slice(0, 400)
|
||||
}
|
||||
|
||||
function notifyHangup(status = 'ended'): void {
|
||||
if (endNotified) return
|
||||
endNotified = true
|
||||
phase.value = 'ended'
|
||||
statusText.value = '视频面诊已结束'
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'hangup',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
function readRoomId(): string {
|
||||
const raw = TUIStore.getData(StoreName.CALL, NAME.ROOM_ID)
|
||||
if (raw === undefined || raw === null) return ''
|
||||
const value = String(raw).trim()
|
||||
return value && value !== '0' ? value : ''
|
||||
}
|
||||
|
||||
function emitRoomId(): boolean {
|
||||
const roomId = readRoomId()
|
||||
if (!roomId || roomId === emittedRoomId) return Boolean(roomId)
|
||||
emittedRoomId = roomId
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'room',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
roomId,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function pollRoomId(): Promise<void> {
|
||||
for (let attempt = 0; attempt < 40 && activeConfig && !endNotified; attempt += 1) {
|
||||
if (emitRoomId()) return
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 50))
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusChanged(payload: unknown): void {
|
||||
const value = payload && typeof payload === 'object'
|
||||
? (payload as { newStatus?: unknown }).newStatus
|
||||
: payload
|
||||
const status = typeof value === 'string' ? value : 'unknown'
|
||||
|
||||
if (status === 'connected' || status.startsWith('calling-')) {
|
||||
phase.value = 'connected'
|
||||
statusText.value = '视频面诊进行中'
|
||||
void pollRoomId()
|
||||
} else if (status === 'calling' || status.startsWith('dialing')) {
|
||||
phase.value = 'dialing'
|
||||
statusText.value = '正在等待患者接听'
|
||||
} else if (status === 'idle' && activeConfig && !starting) {
|
||||
notifyHangup(status)
|
||||
}
|
||||
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'status',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
TUICallKitAPI.setCallback({
|
||||
statusChanged: handleStatusChanged,
|
||||
afterCalling: () => notifyHangup('after-calling'),
|
||||
})
|
||||
TUICallKitAPI.enableFloatWindow(false)
|
||||
|
||||
async function start(config: DoctorCallConfig): Promise<void> {
|
||||
if (starting || (activeConfig && !endNotified)) {
|
||||
throw new Error('已有视频通话正在进行')
|
||||
}
|
||||
|
||||
starting = true
|
||||
endNotified = false
|
||||
phase.value = 'starting'
|
||||
statusText.value = '正在初始化安全通话'
|
||||
|
||||
try {
|
||||
const normalized = normalizeConfig(config)
|
||||
activeConfig = normalized
|
||||
emittedRoomId = ''
|
||||
|
||||
await TUICallKitAPI.init({
|
||||
SDKAppID: normalized.SDKAppID,
|
||||
userID: normalized.userID,
|
||||
userSig: normalized.userSig,
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
phase.value = 'dialing'
|
||||
statusText.value = '正在呼叫患者'
|
||||
|
||||
await TUICallKitAPI.calls({
|
||||
userIDList: [normalized.targetUserId],
|
||||
type: TUICallType.VIDEO_CALL,
|
||||
})
|
||||
void pollRoomId()
|
||||
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'status',
|
||||
diagnosisId: normalized.diagnosisId,
|
||||
status: 'dialing',
|
||||
})
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error)
|
||||
phase.value = 'error'
|
||||
statusText.value = message
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'error',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
message,
|
||||
})
|
||||
activeConfig = null
|
||||
endNotified = true
|
||||
throw new Error(message)
|
||||
} finally {
|
||||
starting = false
|
||||
}
|
||||
}
|
||||
|
||||
async function hangup(): Promise<void> {
|
||||
if (!activeConfig || endNotified) return
|
||||
|
||||
try {
|
||||
await TUICallKitAPI.hangup()
|
||||
notifyHangup('local-hangup')
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error)
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'error',
|
||||
diagnosisId: activeConfig.diagnosisId,
|
||||
message,
|
||||
})
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
window.doctorCall = { start, hangup }
|
||||
initializeQtWebChannel()
|
||||
|
||||
createApp(App, {
|
||||
phase: readonly(phase),
|
||||
statusText: readonly(statusText),
|
||||
}).mount('#app')
|
||||
|
||||
emit({ source: 'doctor-call', event: 'ready' })
|
||||
Reference in New Issue
Block a user