更新
This commit is contained in:
@@ -67,9 +67,31 @@
|
||||
@mousedown="onCallKitDragStart"
|
||||
>
|
||||
<span class="drag-handle-text">视频通话</span>
|
||||
<el-icon class="drag-handle-icon"><Rank /></el-icon>
|
||||
<div class="call-kit-drag-handle-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="call-kit-screenshot-btn"
|
||||
:loading="captureUploading"
|
||||
@mousedown.stop
|
||||
@click.stop="handleCallKitScreenshot"
|
||||
>
|
||||
<el-icon><Camera /></el-icon>
|
||||
<span class="call-kit-screenshot-text">截屏</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
class="call-kit-close-btn"
|
||||
@mousedown.stop
|
||||
@click.stop="handleCallKitClose"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
<el-icon class="drag-handle-icon"><Rank /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="call-kit-content">
|
||||
<div ref="callKitContentRef" class="call-kit-content">
|
||||
<TUICallKit
|
||||
:allowedMinimized="true"
|
||||
:allowedFullScreen="true"
|
||||
@@ -82,10 +104,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch, computed } from 'vue'
|
||||
import { Loading, Close, Rank } from '@element-plus/icons-vue'
|
||||
import { getCallSignature } from '@/api/tcm'
|
||||
import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Loading, Close, Rank, Camera } from '@element-plus/icons-vue'
|
||||
import { uploadImageBlob } from '@/api/file'
|
||||
import {
|
||||
bindCallRoom,
|
||||
getCallSignature,
|
||||
startCall,
|
||||
tcmDiagnosisDetail,
|
||||
tcmDiagnosisEdit
|
||||
} from '@/api/tcm'
|
||||
import { captureVideoFrameFromElement } from '@/utils/call-video-screenshot'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
formatTUICallUserError,
|
||||
getTUICallPackageArrearsMessage,
|
||||
isTUICallPackageAbilityError
|
||||
} from '@/utils/tuicall-error'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import {
|
||||
ConversationList,
|
||||
@@ -103,7 +138,15 @@ import {
|
||||
} from '@tencentcloud/chat-uikit-vue3'
|
||||
import TencentCloudChat from '@tencentcloud/chat'
|
||||
import '@/assets/chat-uikit.css'
|
||||
import { TUICallKit, TUICallKitServer, TUICallType } from '@tencentcloud/call-uikit-vue'
|
||||
import {
|
||||
TUICallKit,
|
||||
TUICallKitServer,
|
||||
TUICallType,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
NAME
|
||||
} from '@tencentcloud/call-uikit-vue'
|
||||
import { TUICallEvent } from '@tencentcloud/call-engine-js'
|
||||
const assistant_ids=ref('');
|
||||
const visible = ref(false)
|
||||
const isReady = ref(false)
|
||||
@@ -120,6 +163,445 @@ const { login, logout } = useLoginState()
|
||||
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
|
||||
const userStore = useUserStore()
|
||||
|
||||
/** TUICallKit CallStatus(与 @tencentcloud/call-uikit-vue 一致) */
|
||||
const CALL_STATUS_IDLE = 'idle'
|
||||
const CALL_STATUS_CALLING = 'calling'
|
||||
const CALL_STATUS_CONNECTED = 'connected'
|
||||
|
||||
/** 通话绑定 TRTC 房间号到诊单(云端录制回调按 room_id 关联) */
|
||||
const lastBoundRoomKey = ref('')
|
||||
let tearDownCallRoomBinding: (() => void) | null = null
|
||||
let bindRoomRetryTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let bindRoomRetryAttempts = 0
|
||||
const BIND_ROOM_MAX_ATTEMPTS = 40
|
||||
const BIND_ROOM_RETRY_MS = 100
|
||||
|
||||
/** 从 TUIStore 读取房间号;默认 0 表示尚未分配 */
|
||||
function readRoomIdFromStore(): string {
|
||||
const raw = TUIStore.getData(StoreName.CALL, NAME.ROOM_ID)
|
||||
if (raw === null || raw === undefined) return ''
|
||||
if (typeof raw === 'number') {
|
||||
if (raw === 0) return ''
|
||||
return String(raw)
|
||||
}
|
||||
const s = String(raw).trim()
|
||||
if (!s || s === '0') return ''
|
||||
return s
|
||||
}
|
||||
|
||||
/** 引擎实例上可能存在房间号(不同版本字段名不一,仅作兜底) */
|
||||
function readRoomIdFromEngineFallback(): string {
|
||||
try {
|
||||
const eng = TUICallKitServer.getTUICallEngineInstance?.() as Record<string, unknown> | null
|
||||
if (!eng) return ''
|
||||
const candidates = [eng.roomId, eng.roomID, eng._roomId, eng._roomID, eng.strRoomID]
|
||||
for (const c of candidates) {
|
||||
if (c === null || c === undefined || c === '') continue
|
||||
const s = String(c).trim()
|
||||
if (s && s !== '0') return s
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function readAnyRoomId(): string {
|
||||
return readRoomIdFromStore() || readRoomIdFromEngineFallback()
|
||||
}
|
||||
|
||||
/** 从引擎 calls/call/groupCall 的 Promise 返回值里解析房间号(含 data 嵌套) */
|
||||
function normalizeRoomPair(roomID: unknown, strRoomID: unknown): string {
|
||||
if (strRoomID != null && String(strRoomID).trim() !== '' && String(strRoomID) !== '0') {
|
||||
return String(strRoomID).trim()
|
||||
}
|
||||
if (typeof roomID === 'number' && roomID > 0) return String(roomID)
|
||||
if (typeof roomID === 'string') {
|
||||
const t = roomID.trim()
|
||||
if (t && t !== '0') return t
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function extractRoomFromEngineResult(ret: unknown): string {
|
||||
if (ret == null || typeof ret !== 'object') return ''
|
||||
const r = ret as Record<string, unknown>
|
||||
const direct =
|
||||
normalizeRoomPair(r.roomID, r.strRoomID) ||
|
||||
normalizeRoomPair(r.roomId, r.strRoomId)
|
||||
if (direct) return direct
|
||||
const d = r.data
|
||||
if (d && typeof d === 'object') {
|
||||
const dd = d as Record<string, unknown>
|
||||
const nested =
|
||||
normalizeRoomPair(dd.roomID, dd.strRoomID) ||
|
||||
normalizeRoomPair(dd.roomId, dd.strRoomId)
|
||||
if (nested) return nested
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function clearBindRoomRetry() {
|
||||
if (bindRoomRetryTimer) {
|
||||
clearTimeout(bindRoomRetryTimer)
|
||||
bindRoomRetryTimer = null
|
||||
}
|
||||
bindRoomRetryAttempts = 0
|
||||
}
|
||||
|
||||
/** 在 calling / connected 阶段轮询房间号(ROOM_ID 可能比 status 晚一拍) */
|
||||
function scheduleBindRoomRetry() {
|
||||
clearBindRoomRetry()
|
||||
const tick = () => {
|
||||
bindRoomRetryTimer = null
|
||||
const status = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS)
|
||||
if (status === CALL_STATUS_IDLE) return
|
||||
if (status !== CALL_STATUS_CALLING && status !== CALL_STATUS_CONNECTED) return
|
||||
const rid = readAnyRoomId()
|
||||
if (rid) {
|
||||
void doBindCallRoom(rid)
|
||||
return
|
||||
}
|
||||
bindRoomRetryAttempts++
|
||||
if (bindRoomRetryAttempts >= BIND_ROOM_MAX_ATTEMPTS) {
|
||||
console.warn(
|
||||
'[chat-dialog] 仍未解析到有效房间号(已重试',
|
||||
BIND_ROOM_MAX_ATTEMPTS,
|
||||
'次)'
|
||||
)
|
||||
return
|
||||
}
|
||||
bindRoomRetryTimer = setTimeout(tick, BIND_ROOM_RETRY_MS)
|
||||
}
|
||||
bindRoomRetryTimer = setTimeout(tick, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 TUICallKitServer.call / calls / groupCall 执行完毕后,引擎已返回 roomId,
|
||||
* _updateCallStoreAfterCall 会写入 TUIStore;工具栏多走 call() 而非 calls(),必须 hook 这三者。
|
||||
*/
|
||||
async function flushAndBindRoomAfterTUICallApi(apiName: string) {
|
||||
if (patientId.value == null || diagnosisId.value == null) return
|
||||
await nextTick()
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const rid = readAnyRoomId()
|
||||
if (rid) {
|
||||
await doBindCallRoom(rid)
|
||||
return
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
}
|
||||
console.warn('[chat-dialog] 发起通话后仍未解析到房间号(API:', apiName, ')')
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 TUICallEngine 上包装 calls/call/groupCall:房间号往往在 Promise 返回值里,
|
||||
* 仅靠 TUIStore 轮询可能拿不到或晚一拍。
|
||||
*/
|
||||
function installEngineRoomCapture() {
|
||||
const w = window as unknown as Record<string, unknown>
|
||||
if (w.__zytEngineRoomCapture__) return
|
||||
const eng = TUICallKitServer.getTUICallEngineInstance?.() as
|
||||
| (Record<string, unknown> & { __zytEngineRoomCapture__?: boolean })
|
||||
| null
|
||||
| undefined
|
||||
if (!eng || typeof eng.calls !== 'function') return
|
||||
w.__zytEngineRoomCapture__ = true
|
||||
eng.__zytEngineRoomCapture__ = true
|
||||
const wrap = (methodName: 'calls' | 'call' | 'groupCall') => {
|
||||
const original = eng[methodName]
|
||||
if (typeof original !== 'function') return
|
||||
eng[methodName] = async function patched(this: unknown, ...args: unknown[]) {
|
||||
const ret = await (original as (...a: unknown[]) => Promise<unknown>).apply(eng, args)
|
||||
const fromRet = extractRoomFromEngineResult(ret)
|
||||
if (fromRet) {
|
||||
void doBindCallRoom(fromRet)
|
||||
} else if (import.meta.env.DEV) {
|
||||
console.debug('[chat-dialog] engine.' + methodName + ' 返回未含 roomID,将回退读 Store', ret)
|
||||
}
|
||||
await flushAndBindRoomAfterTUICallApi('engine.' + methodName)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
wrap('calls')
|
||||
wrap('call')
|
||||
wrap('groupCall')
|
||||
}
|
||||
|
||||
let engineRoomEventHandlers: Array<{ event: TUICallEvent; fn: (e: unknown) => void }> = []
|
||||
|
||||
function bindEngineRoomEvents() {
|
||||
try {
|
||||
type EngineWithEvents = Record<string, unknown> & {
|
||||
__zytRoomEvents?: boolean
|
||||
on?: (event: TUICallEvent | string, fn: (e: unknown) => void) => void
|
||||
}
|
||||
const eng = TUICallKitServer.getTUICallEngineInstance?.() as EngineWithEvents | null | undefined
|
||||
const subscribe = eng?.on
|
||||
if (!eng || typeof subscribe !== 'function' || eng.__zytRoomEvents) return
|
||||
eng.__zytRoomEvents = true
|
||||
const onRoomHint = (ev: unknown) => {
|
||||
const fromEv = extractRoomFromEngineResult(ev)
|
||||
if (fromEv) {
|
||||
void doBindCallRoom(fromEv)
|
||||
return
|
||||
}
|
||||
void tryBindRoomIfReady()
|
||||
}
|
||||
const events = [TUICallEvent.USER_ENTER, TUICallEvent.ON_CALL_BEGIN] as const
|
||||
for (const event of events) {
|
||||
subscribe.call(eng, event, onRoomHint)
|
||||
engineRoomEventHandlers.push({ event, fn: onRoomHint })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] 绑定引擎房间事件失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
function unbindEngineRoomEvents() {
|
||||
try {
|
||||
const eng = TUICallKitServer.getTUICallEngineInstance?.() as
|
||||
| ({ off?: (e: TUICallEvent, fn: (ev: unknown) => void) => void } & {
|
||||
__zytRoomEvents?: boolean
|
||||
})
|
||||
| null
|
||||
| undefined
|
||||
if (!eng?.off) {
|
||||
engineRoomEventHandlers = []
|
||||
return
|
||||
}
|
||||
for (const { event, fn } of engineRoomEventHandlers) {
|
||||
eng.off(event, fn)
|
||||
}
|
||||
eng.__zytRoomEvents = false
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
engineRoomEventHandlers = []
|
||||
}
|
||||
|
||||
function installTUICallKitRoomHooks() {
|
||||
const w = window as unknown as Record<string, unknown>
|
||||
if (w.__zytTUICallRoomHooks__) return
|
||||
w.__zytTUICallRoomHooks__ = true
|
||||
const server = TUICallKitServer as unknown as Record<string, (...args: unknown[]) => Promise<unknown>>
|
||||
const wrap = (methodName: 'calls' | 'call' | 'groupCall') => {
|
||||
const original = server[methodName]
|
||||
if (typeof original !== 'function') return
|
||||
server[methodName] = async function patched(...args: unknown[]) {
|
||||
const result = await original.apply(server, args)
|
||||
await flushAndBindRoomAfterTUICallApi(methodName)
|
||||
return result
|
||||
}
|
||||
}
|
||||
wrap('calls')
|
||||
wrap('call')
|
||||
wrap('groupCall')
|
||||
}
|
||||
|
||||
async function doBindCallRoom(rid: string) {
|
||||
if (patientId.value == null || diagnosisId.value == null) return
|
||||
const key = `${diagnosisId.value}:${rid}`
|
||||
if (key === lastBoundRoomKey.value) {
|
||||
clearBindRoomRetry()
|
||||
return
|
||||
}
|
||||
lastBoundRoomKey.value = key
|
||||
try {
|
||||
await bindCallRoom({ diagnosis_id: diagnosisId.value, room_id: rid })
|
||||
clearBindRoomRetry()
|
||||
console.log('[chat-dialog] bindCallRoom 成功', { room_id: rid })
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] bindCallRoom 失败', e)
|
||||
lastBoundRoomKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function tryBindRoomIfReady() {
|
||||
if (patientId.value == null || diagnosisId.value == null) return
|
||||
const status = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS)
|
||||
if (status === CALL_STATUS_IDLE) {
|
||||
clearBindRoomRetry()
|
||||
lastBoundRoomKey.value = ''
|
||||
return
|
||||
}
|
||||
if (status !== CALL_STATUS_CALLING && status !== CALL_STATUS_CONNECTED) return
|
||||
|
||||
await nextTick()
|
||||
const rid = readAnyRoomId()
|
||||
if (rid) {
|
||||
await doBindCallRoom(rid)
|
||||
return
|
||||
}
|
||||
scheduleBindRoomRetry()
|
||||
}
|
||||
|
||||
async function ensureCallRecordStarted() {
|
||||
if (patientId.value == null || diagnosisId.value == null) return
|
||||
try {
|
||||
await startCall({
|
||||
diagnosis_id: diagnosisId.value,
|
||||
patient_id: patientId.value,
|
||||
call_type: 2
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] startCall 记录失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
function setupCallRoomBinding() {
|
||||
tearDownCallRoomBinding?.()
|
||||
const onCallStatus = (newData?: string) => {
|
||||
if (newData === CALL_STATUS_IDLE) {
|
||||
clearBindRoomRetry()
|
||||
lastBoundRoomKey.value = ''
|
||||
return
|
||||
}
|
||||
if (newData === CALL_STATUS_CONNECTED) {
|
||||
void tryBindRoomIfReady()
|
||||
}
|
||||
}
|
||||
const onRoomId = () => {
|
||||
void tryBindRoomIfReady()
|
||||
}
|
||||
TUIStore.watch(StoreName.CALL, {
|
||||
[NAME.CALL_STATUS]: onCallStatus,
|
||||
[NAME.ROOM_ID]: onRoomId
|
||||
})
|
||||
tearDownCallRoomBinding = () => {
|
||||
try {
|
||||
TUIStore.unwatch(StoreName.CALL, {
|
||||
[NAME.CALL_STATUS]: onCallStatus,
|
||||
[NAME.ROOM_ID]: onRoomId
|
||||
})
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
clearBindRoomRetry()
|
||||
tearDownCallRoomBinding = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 防止 console + 引擎事件 与 unhandledrejection 连续弹多次 */
|
||||
let lastPackageToastAt = 0
|
||||
const PACKAGE_TOAST_DEBOUNCE_MS = 2600
|
||||
|
||||
function notifyPackageArrearsOnce() {
|
||||
const now = Date.now()
|
||||
if (now - lastPackageToastAt < PACKAGE_TOAST_DEBOUNCE_MS) return
|
||||
lastPackageToastAt = now
|
||||
feedback.msgError(getTUICallPackageArrearsMessage())
|
||||
showCallKitWindow.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK 在 calls 失败时往往只 console.error(见 CallService),Chat UIKit 工具栏又会吞掉 Promise,
|
||||
* 故需:1) 挂钩 console 识别套餐文案;2) 监听 TUICallEngine 的 error 事件。
|
||||
*/
|
||||
let savedConsoleError: typeof console.error | null = null
|
||||
let consoleErrorHooked = false
|
||||
let engineErrorHandler: ((e: unknown) => void) | null = null
|
||||
|
||||
function hookConsoleErrorForPackageHint() {
|
||||
if (consoleErrorHooked) return
|
||||
savedConsoleError = console.error.bind(console)
|
||||
consoleErrorHooked = true
|
||||
console.error = (...args: unknown[]) => {
|
||||
savedConsoleError!(...args)
|
||||
if (!visible.value || !isCallReady.value) return
|
||||
const text = args
|
||||
.map((a) => {
|
||||
if (typeof a === 'string') return a
|
||||
if (a instanceof Error) return a.message
|
||||
try {
|
||||
return JSON.stringify(a)
|
||||
} catch {
|
||||
return String(a)
|
||||
}
|
||||
})
|
||||
.join(' ')
|
||||
if (isTUICallPackageAbilityError(text)) notifyPackageArrearsOnce()
|
||||
}
|
||||
}
|
||||
|
||||
function unhookConsoleErrorForPackageHint() {
|
||||
if (consoleErrorHooked && savedConsoleError) {
|
||||
console.error = savedConsoleError
|
||||
}
|
||||
consoleErrorHooked = false
|
||||
savedConsoleError = null
|
||||
}
|
||||
|
||||
function bindEnginePackageErrorListener() {
|
||||
try {
|
||||
const engine = TUICallKitServer.getTUICallEngineInstance?.()
|
||||
if (!engine?.on) return
|
||||
if (engineErrorHandler) {
|
||||
engine.off?.('error', engineErrorHandler)
|
||||
}
|
||||
engineErrorHandler = (payload: unknown) => {
|
||||
if (!visible.value || !isCallReady.value) return
|
||||
if (isTUICallPackageAbilityError(payload)) notifyPackageArrearsOnce()
|
||||
}
|
||||
engine.on('error', engineErrorHandler)
|
||||
} catch (e) {
|
||||
console.warn('绑定 TUICallEngine error 监听失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function unbindEnginePackageErrorListener() {
|
||||
try {
|
||||
const engine = TUICallKitServer.getTUICallEngineInstance?.()
|
||||
if (engine?.off && engineErrorHandler) {
|
||||
engine.off('error', engineErrorHandler)
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
engineErrorHandler = null
|
||||
}
|
||||
|
||||
/** 医生端进入会话后发给患者的信令:患者端不展示气泡,仅用于更新「已进入诊室」状态 */
|
||||
async function sendDoctorEnteredConsultRoomSignal(sdkAppId: number, toUserId: string) {
|
||||
try {
|
||||
const chat = TencentCloudChat.create({ SDKAppID: sdkAppId })
|
||||
const payload = {
|
||||
businessID: 'doctor_entered_consult_room',
|
||||
time: Date.now()
|
||||
}
|
||||
const message = chat.createCustomMessage({
|
||||
to: toUserId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
payload: {
|
||||
data: JSON.stringify(payload)
|
||||
}
|
||||
})
|
||||
await chat.sendMessage(message)
|
||||
console.log('已发送医生进入诊室信令(患者端聊天列表不展示)')
|
||||
} catch (e) {
|
||||
console.warn('发送医生进入诊室信令失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 未捕获 Promise(若存在) */
|
||||
const onTuiCallUnhandledRejection = (e: PromiseRejectionEvent) => {
|
||||
if (!visible.value || !isCallReady.value) return
|
||||
if (isTUICallPackageAbilityError(e.reason)) notifyPackageArrearsOnce()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
hookConsoleErrorForPackageHint()
|
||||
window.addEventListener('unhandledrejection', onTuiCallUnhandledRejection)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
tearDownCallRoomBinding?.()
|
||||
unbindEngineRoomEvents()
|
||||
unhookConsoleErrorForPackageHint()
|
||||
unbindEnginePackageErrorListener()
|
||||
window.removeEventListener('unhandledrejection', onTuiCallUnhandledRejection)
|
||||
})
|
||||
|
||||
// 更新用户头像到 IM
|
||||
const updateUserAvatarToIM = async (avatarUrl: string,appid: number) => {
|
||||
try {
|
||||
@@ -244,15 +726,28 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
;(window as any).__TUICallKitInited__ = true
|
||||
} catch (err: any) {
|
||||
console.warn('TUICallKit 初始化失败,聊天仍可用,音视频通话不可用:', err?.message || err)
|
||||
if (isTUICallPackageAbilityError(err)) {
|
||||
notifyPackageArrearsOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
isCallReady.value = !!(window as any).__TUICallKitInited__
|
||||
if (isCallReady.value) {
|
||||
// 仅在通话时显示视频窗口,无通话时隐藏
|
||||
// 仅在通话时显示视频窗口,无通话时隐藏;发起通话前落库、接通后绑定房间号(云端录制)
|
||||
TUICallKitServer.setCallback({
|
||||
beforeCalling: () => { showCallKitWindow.value = true },
|
||||
afterCalling: () => { showCallKitWindow.value = false }
|
||||
beforeCalling: () => {
|
||||
showCallKitWindow.value = true
|
||||
void ensureCallRecordStarted()
|
||||
},
|
||||
afterCalling: () => {
|
||||
showCallKitWindow.value = false
|
||||
}
|
||||
})
|
||||
installTUICallKitRoomHooks()
|
||||
installEngineRoomCapture()
|
||||
bindEngineRoomEvents()
|
||||
setupCallRoomBinding()
|
||||
bindEnginePackageErrorListener()
|
||||
}
|
||||
console.log('Chat UIKit 登录成功,患者用户ID:', patientUserId.value)
|
||||
|
||||
@@ -285,6 +780,8 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
// 激活该会话
|
||||
setActiveConversation(conversationId.value)
|
||||
console.log('已激活会话')
|
||||
|
||||
await sendDoctorEnteredConsultRoomSignal(sdkAPPid, patientUserId.value)
|
||||
} catch (err) {
|
||||
console.error('创建/激活会话失败:', err)
|
||||
}
|
||||
@@ -338,7 +835,11 @@ const startGroupVideoCall = async () => {
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error('发起群视频失败:', err)
|
||||
feedback.msgError(err?.message || '发起群视频失败')
|
||||
if (isTUICallPackageAbilityError(err)) {
|
||||
notifyPackageArrearsOnce()
|
||||
} else {
|
||||
feedback.msgError(formatTUICallUserError(err, '发起群视频失败'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +855,8 @@ const posStartY = ref(0)
|
||||
|
||||
// TUICallKit 视频窗口拖动
|
||||
const callKitWrapperRef = ref<HTMLElement>()
|
||||
const callKitContentRef = ref<HTMLElement>()
|
||||
const captureUploading = ref(false)
|
||||
const callKitX = ref(0)
|
||||
const callKitY = ref(0)
|
||||
const isCallKitDragging = ref(false)
|
||||
@@ -389,6 +892,67 @@ const onCallKitDragEnd = () => {
|
||||
document.removeEventListener('mouseup', onCallKitDragEnd)
|
||||
}
|
||||
|
||||
/** 截取当前视频画面并上传,同步到对应患者诊单的「舌苔照片」 */
|
||||
const handleCallKitScreenshot = async () => {
|
||||
if (patientId.value == null) {
|
||||
feedback.msgWarning('缺少患者信息,无法同步舌苔照片')
|
||||
return
|
||||
}
|
||||
const el = callKitContentRef.value
|
||||
if (!el) return
|
||||
captureUploading.value = true
|
||||
try {
|
||||
const blob = await captureVideoFrameFromElement(el)
|
||||
if (!blob) {
|
||||
feedback.msgWarning('截屏失败,请确认视频画面已加载')
|
||||
return
|
||||
}
|
||||
const data = await uploadImageBlob(blob, `callshot-${Date.now()}.jpg`)
|
||||
const path = data.url || data.uri
|
||||
if (!path) {
|
||||
feedback.msgError('上传未返回图片路径')
|
||||
return
|
||||
}
|
||||
if (diagnosisId.value == null) {
|
||||
feedback.msgWarning('请先保存诊单后再从视频同步舌苔照片')
|
||||
return
|
||||
}
|
||||
const detail = await tcmDiagnosisDetail({ id: diagnosisId.value })
|
||||
if (Number(detail.patient_id) !== Number(patientId.value)) {
|
||||
feedback.msgError('患者与当前诊单不匹配,无法同步')
|
||||
return
|
||||
}
|
||||
const previous: string[] = [...((detail.tongue_images || []) as string[])]
|
||||
if (previous.length >= 9) {
|
||||
feedback.msgWarning('舌苔照片已达上限 9 张')
|
||||
return
|
||||
}
|
||||
const next = [...previous, path]
|
||||
const payload = JSON.parse(JSON.stringify(detail)) as Record<string, unknown>
|
||||
payload.tongue_images = next
|
||||
await tcmDiagnosisEdit(payload)
|
||||
feedback.msgSuccess('舌苔照片已同步到诊单')
|
||||
} catch (e: unknown) {
|
||||
const err = e as Error
|
||||
console.error(err)
|
||||
feedback.msgError(err?.message || '上传失败')
|
||||
} finally {
|
||||
captureUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭视频浮窗并挂断当前通话 */
|
||||
const handleCallKitClose = async () => {
|
||||
if (isCallReady.value) {
|
||||
try {
|
||||
await TUICallKitServer.hangup()
|
||||
} catch (err) {
|
||||
console.warn('关闭视频窗口时挂断失败:', err)
|
||||
}
|
||||
}
|
||||
showCallKitWindow.value = false
|
||||
}
|
||||
|
||||
const windowStyle = computed(() => ({
|
||||
left: `${posX.value}px`,
|
||||
top: `${posY.value}px`
|
||||
@@ -580,6 +1144,36 @@ defineExpose({ open })
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.call-kit-drag-handle-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.call-kit-screenshot-btn {
|
||||
padding: 4px 6px;
|
||||
color: #79bbff;
|
||||
|
||||
&:hover {
|
||||
color: #a0cfff;
|
||||
}
|
||||
}
|
||||
|
||||
.call-kit-screenshot-text {
|
||||
margin-left: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.call-kit-close-btn {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: #f56c6c;
|
||||
|
||||
&:hover {
|
||||
color: #f89898;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle-icon {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
|
||||
Reference in New Issue
Block a user