更新
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;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="中医处方单"
|
||||
size="900px"
|
||||
size="1200px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
@@ -52,6 +52,16 @@
|
||||
<el-input v-model="formData.tongue" placeholder="舌象" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="舌象详情" prop="tongue_image">
|
||||
<el-input v-model="formData.tongue_image" placeholder="舌象详情" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉象详情" prop="pulse_condition">
|
||||
<el-input v-model="formData.pulse_condition" placeholder="脉象详情" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="临床诊断" prop="clinical_diagnosis">
|
||||
<el-input
|
||||
@@ -69,6 +79,7 @@
|
||||
<div class="section-title">
|
||||
中药处方 (RP)
|
||||
<el-button type="primary" size="small" @click="handleAddHerb">添加中药</el-button>
|
||||
<el-button type="success" size="small" @click="showLibraryDialog = true">从处方库导入</el-button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<el-form-item label="处方价格" prop="amount" class="!mb-0">
|
||||
@@ -109,32 +120,113 @@
|
||||
|
||||
<div class="info-section">
|
||||
<el-row :gutter="16">
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="处方类型" prop="prescription_type">
|
||||
<el-select v-model="formData.prescription_type" placeholder="请选择处方类型" class="!w-full">
|
||||
<el-option label="浓缩水丸" value="浓缩水丸" />
|
||||
<el-option label="饮片" value="饮片" />
|
||||
<el-option label="颗粒" value="颗粒" />
|
||||
<el-option label="丸剂" value="丸剂" />
|
||||
<el-option label="散剂" value="散剂" />
|
||||
<el-option label="膏方" value="膏方" />
|
||||
<el-option label="汤剂" value="汤剂" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服用天数" prop="dose_count">
|
||||
<el-form-item label="服用天数" prop="usage_days">
|
||||
<el-input-number v-model="formData.usage_days" :min="1" :max="365" class="!w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- <el-col :span="8">
|
||||
<el-form-item label="剂数" prop="dose_count">
|
||||
<el-input-number v-model="formData.dose_count" :min="1" :max="999" class="!w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="类型" prop="dose_unit">
|
||||
<el-select v-model="formData.dose_unit" placeholder="请选择类型" class="!w-full">
|
||||
<el-option label="浓缩水丸" value="浓缩水丸" />
|
||||
<el-option label="饮片" value="饮片" />
|
||||
<el-form-item label="剂量单位" prop="dose_unit">
|
||||
<el-select v-model="formData.dose_unit" placeholder="请选择" class="!w-full">
|
||||
<el-option label="剂" value="剂" />
|
||||
<el-option label="丸" value="丸" />
|
||||
<el-option label="袋" value="袋" />
|
||||
<el-option label="盒" value="盒" />
|
||||
<el-option label="瓶" value="瓶" />
|
||||
<el-option label="膏" value="膏" />
|
||||
<el-option label="贴" value="贴" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col> -->
|
||||
<el-col :span="24">
|
||||
<el-form-item label="用法" prop="usage_instruction">
|
||||
<el-input v-model="formData.usage_instruction" placeholder="例如:水煎服,一日二次" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服用时间" prop="usage_time">
|
||||
<el-select v-model="formData.usage_time" placeholder="请选择服用时间" class="!w-full">
|
||||
<el-option label="饭前" value="饭前" />
|
||||
<el-option label="饭后" value="饭后" />
|
||||
<el-option label="饭中" value="饭中" />
|
||||
<el-option label="空腹" value="空腹" />
|
||||
<el-option label="睡前" value="睡前" />
|
||||
<el-option label="晨起" value="晨起" />
|
||||
<el-option label="随时" value="随时" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服用说明" prop="usage_instruction">
|
||||
<el-select v-model="formData.usage_instruction" placeholder="请选择服用说明" class="!w-full">
|
||||
<el-option label="饭前" value="饭前" />
|
||||
<el-option label="饭后" value="饭后" />
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服用方式" prop="usage_way">
|
||||
<el-select v-model="formData.usage_way" placeholder="请选择服用方式" class="!w-full">
|
||||
<el-option label="温水送服" value="温水送服" />
|
||||
<el-option label="开水冲服" value="开水冲服" />
|
||||
<el-option label="黄酒送服" value="黄酒送服" />
|
||||
<el-option label="淡盐水送服" value="淡盐水送服" />
|
||||
<el-option label="米汤送服" value="米汤送服" />
|
||||
<el-option label="嚼服" value="嚼服" />
|
||||
<el-option label="含化" value="含化" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="忌口" prop="dietary_taboo">
|
||||
<el-select v-model="formData.dietary_taboo" multiple placeholder="请选择忌口内容(可多选)" class="!w-full">
|
||||
<el-option label="辛辣食物" value="辛辣食物" />
|
||||
<el-option label="生冷食物" value="生冷食物" />
|
||||
<el-option label="油腻食物" value="油腻食物" />
|
||||
<el-option label="海鲜" value="海鲜" />
|
||||
<el-option label="牛羊肉" value="牛羊肉" />
|
||||
<el-option label="鸡蛋" value="鸡蛋" />
|
||||
<el-option label="豆制品" value="豆制品" />
|
||||
<el-option label="酒类" value="酒类" />
|
||||
<el-option label="浓茶" value="浓茶" />
|
||||
<el-option label="咖啡" value="咖啡" />
|
||||
<el-option label="烟草" value="烟草" />
|
||||
<el-option label="萝卜" value="萝卜" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="其他说明" prop="usage_notes">
|
||||
<el-input v-model="formData.usage_notes" type="textarea" :rows="2" placeholder="其他服用注意事项" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="医师" prop="doctor_name">
|
||||
<el-input v-model="formData.doctor_name" placeholder="医师姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否共享" prop="is_shared">
|
||||
<el-switch
|
||||
v-model="formData.is_shared"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="所有人可见"
|
||||
inactive-text="仅自己可见"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="医师签名" prop="doctor_signature" required>
|
||||
<div class="signature-pad-wrap">
|
||||
@@ -219,7 +311,20 @@
|
||||
<div class="prescription-footer">
|
||||
<div class="footer-left">
|
||||
<div class="footer-dosage">
|
||||
服用{{ savedPrescription.dose_count }}天, {{ savedPrescription.dose_unit || '浓缩水丸' }}, {{ savedPrescription.usage_instruction }}
|
||||
服用{{ savedPrescription.usage_days || savedPrescription.dose_count }}天, {{ savedPrescription.prescription_type }}
|
||||
{{ savedPrescription.usage_instruction }}
|
||||
|
||||
</div>
|
||||
<div class="footer-dosage">
|
||||
服用时间: {{ savedPrescription.usage_time ?' ' + savedPrescription.usage_time : '' }}
|
||||
<text style="margin-left: 20px;"></text>服用方式: {{ savedPrescription.usage_way ? ' ' + savedPrescription.usage_way : '' }}
|
||||
</div>
|
||||
|
||||
<div v-if="savedPrescription.dietary_taboo && savedPrescription.dietary_taboo.length" class="footer-dietary">
|
||||
忌口:{{ Array.isArray(savedPrescription.dietary_taboo) ? savedPrescription.dietary_taboo.join('、') : savedPrescription.dietary_taboo }}
|
||||
</div>
|
||||
<div v-if="savedPrescription.usage_notes" class="footer-notes">
|
||||
{{ savedPrescription.usage_notes }}
|
||||
</div>
|
||||
<!-- <div class="footer-amount">金额 {{ savedPrescription.amount }}</div> -->
|
||||
</div>
|
||||
@@ -374,17 +479,83 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 处方库选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showLibraryDialog"
|
||||
title="从处方库导入"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="librarySearchName"
|
||||
placeholder="请输入处方名称搜索"
|
||||
clearable
|
||||
@keyup.enter="searchLibrary"
|
||||
@clear="searchLibrary"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="searchLibrary" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="libraryLoading"
|
||||
:data="libraryList"
|
||||
height="400"
|
||||
@row-click="handleSelectLibrary"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<el-table-column label="处方名称" prop="prescription_name" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="药材数量" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.herbs?.length || 0 }}味
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="药材明细" min-width="300" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.herbs && row.herbs.length > 0">
|
||||
{{ row.herbs.map((h: any) => `${h.name} ${h.dosage}g`).join('、') }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">暂无药材</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建人" prop="creator_name" width="100" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click.stop="handleImportLibrary(row)">
|
||||
导入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<el-pagination
|
||||
v-model:current-page="libraryPage"
|
||||
v-model:page-size="libraryPageSize"
|
||||
:total="libraryTotal"
|
||||
:page-sizes="[10, 15, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadLibraryList"
|
||||
@size-change="loadLibraryList"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import feedback from '@/utils/feedback'
|
||||
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail } from '@/api/tcm'
|
||||
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail, prescriptionLibraryLists } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { Download, Document } from '@element-plus/icons-vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
interface Herb {
|
||||
name: string
|
||||
@@ -394,6 +565,8 @@ interface Herb {
|
||||
interface PrescriptionForm {
|
||||
diagnosis_id: number
|
||||
appointment_id: number
|
||||
prescription_name: string
|
||||
prescription_type: string
|
||||
patient_name: string
|
||||
gender: number
|
||||
gender_desc: string
|
||||
@@ -403,15 +576,23 @@ interface PrescriptionForm {
|
||||
prescription_date: string
|
||||
pulse: string
|
||||
tongue: string
|
||||
tongue_image: string
|
||||
pulse_condition: string
|
||||
clinical_diagnosis: string
|
||||
case_record: any
|
||||
herbs: Herb[]
|
||||
dose_count: number
|
||||
dose_unit: string
|
||||
usage_days: number
|
||||
usage_instruction: string
|
||||
usage_time: string
|
||||
usage_way: string
|
||||
dietary_taboo: string[]
|
||||
usage_notes: string
|
||||
amount: number
|
||||
doctor_name: string
|
||||
doctor_signature: string
|
||||
is_shared: number
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
@@ -426,6 +607,15 @@ const signatureCanvasRef = ref<HTMLCanvasElement>()
|
||||
const isDrawing = ref(false)
|
||||
const savedPrescription = ref<any>(null)
|
||||
|
||||
// 处方库相关
|
||||
const showLibraryDialog = ref(false)
|
||||
const libraryLoading = ref(false)
|
||||
const libraryList = ref<any[]>([])
|
||||
const librarySearchName = ref('')
|
||||
const libraryPage = ref(1)
|
||||
const libraryPageSize = ref(15)
|
||||
const libraryTotal = ref(0)
|
||||
|
||||
const templateConfig = reactive({
|
||||
stationName: '成都双流甄养堂互联网医院',
|
||||
showDisclaimer: true,
|
||||
@@ -435,6 +625,8 @@ const templateConfig = reactive({
|
||||
const formData = reactive<PrescriptionForm>({
|
||||
diagnosis_id: 0,
|
||||
appointment_id: 0,
|
||||
prescription_name: '',
|
||||
prescription_type: '浓缩水丸',
|
||||
patient_name: '',
|
||||
gender: 0,
|
||||
gender_desc: '女',
|
||||
@@ -444,15 +636,23 @@ const formData = reactive<PrescriptionForm>({
|
||||
prescription_date: '',
|
||||
pulse: '',
|
||||
tongue: '',
|
||||
tongue_image: '',
|
||||
pulse_condition: '',
|
||||
clinical_diagnosis: '',
|
||||
case_record: null as any,
|
||||
herbs: [],
|
||||
dose_count: 1,
|
||||
dose_unit: '浓缩水丸',
|
||||
usage_instruction: '饭前',
|
||||
dose_unit: '剂',
|
||||
usage_days: 7,
|
||||
usage_instruction: '水煎服,一日二次',
|
||||
usage_time: '饭前',
|
||||
usage_way: '温水送服',
|
||||
dietary_taboo: [],
|
||||
usage_notes: '',
|
||||
amount: 0,
|
||||
doctor_name: '',
|
||||
doctor_signature: ''
|
||||
doctor_signature: '',
|
||||
is_shared: 0
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
@@ -599,6 +799,8 @@ const open = async (data: any, options?: { templateId?: number; stationName?: st
|
||||
|
||||
formData.diagnosis_id = Number(diagnosisId)
|
||||
formData.appointment_id = appointmentId
|
||||
formData.prescription_name = ''
|
||||
formData.prescription_type = '浓缩水丸'
|
||||
formData.patient_name = data.patient_name || diagnosis.patient_name || ''
|
||||
formData.gender = data.patient_gender ?? diagnosis.gender ?? 0
|
||||
formData.gender_desc = formData.gender === 1 ? '男' : '女'
|
||||
@@ -608,15 +810,23 @@ const open = async (data: any, options?: { templateId?: number; stationName?: st
|
||||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||||
formData.pulse = diagnosis.pulse || ''
|
||||
formData.tongue = diagnosis.tongue_coating || diagnosis.tongue || ''
|
||||
formData.tongue_image = ''
|
||||
formData.pulse_condition = ''
|
||||
formData.clinical_diagnosis = buildClinicalDiagnosis(diagnosis)
|
||||
formData.case_record = caseRecord
|
||||
formData.herbs = []
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = '浓缩水丸'
|
||||
formData.usage_instruction = '饭前'
|
||||
formData.dose_unit = '剂'
|
||||
formData.usage_days = 7
|
||||
formData.usage_instruction = '水煎服,一日二次'
|
||||
formData.usage_time = '饭前'
|
||||
formData.usage_way = '温水送服'
|
||||
formData.dietary_taboo = []
|
||||
formData.usage_notes = ''
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || ''
|
||||
formData.doctor_signature = ''
|
||||
formData.is_shared = 0
|
||||
|
||||
savedPrescription.value = null
|
||||
visible.value = true
|
||||
@@ -721,6 +931,59 @@ const handleRemoveHerb = (index: number) => {
|
||||
formData.herbs.splice(index, 1)
|
||||
}
|
||||
|
||||
// 处方库相关方法
|
||||
const loadLibraryList = async () => {
|
||||
libraryLoading.value = true
|
||||
try {
|
||||
const res = await prescriptionLibraryLists({
|
||||
page_no: libraryPage.value,
|
||||
page_size: libraryPageSize.value,
|
||||
prescription_name: librarySearchName.value,
|
||||
is_public: ''
|
||||
})
|
||||
libraryList.value = res.lists || []
|
||||
libraryTotal.value = res.count || 0
|
||||
} catch (error) {
|
||||
console.error('加载处方库失败:', error)
|
||||
feedback.msgError('加载处方库失败')
|
||||
} finally {
|
||||
libraryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchLibrary = () => {
|
||||
libraryPage.value = 1
|
||||
loadLibraryList()
|
||||
}
|
||||
|
||||
const handleSelectLibrary = (row: any) => {
|
||||
// 点击行也可以导入
|
||||
handleImportLibrary(row)
|
||||
}
|
||||
|
||||
const handleImportLibrary = (row: any) => {
|
||||
if (!row.herbs || row.herbs.length === 0) {
|
||||
feedback.msgWarning('该处方没有药材信息')
|
||||
return
|
||||
}
|
||||
|
||||
// 深拷贝药材数据并导入
|
||||
const importedHerbs = JSON.parse(JSON.stringify(row.herbs))
|
||||
formData.herbs = importedHerbs
|
||||
|
||||
feedback.msgSuccess(`已导入处方「${row.prescription_name}」,共${importedHerbs.length}味药材`)
|
||||
showLibraryDialog.value = false
|
||||
}
|
||||
|
||||
// 监听处方库弹窗打开,自动加载数据
|
||||
watch(showLibraryDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
librarySearchName.value = ''
|
||||
libraryPage.value = 1
|
||||
loadLibraryList()
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.clinical_diagnosis?.trim()) {
|
||||
feedback.msgWarning('请输入临床诊断')
|
||||
@@ -751,6 +1014,8 @@ const handleSave = async () => {
|
||||
const res = await prescriptionAdd({
|
||||
diagnosis_id: formData.diagnosis_id,
|
||||
appointment_id: formData.appointment_id,
|
||||
prescription_name: formData.prescription_name,
|
||||
prescription_type: formData.prescription_type,
|
||||
patient_name: formData.patient_name,
|
||||
gender: formData.gender,
|
||||
age: formData.age,
|
||||
@@ -759,15 +1024,23 @@ const handleSave = async () => {
|
||||
prescription_date: formData.prescription_date,
|
||||
pulse: formData.pulse,
|
||||
tongue: formData.tongue,
|
||||
tongue_image: formData.tongue_image,
|
||||
pulse_condition: formData.pulse_condition,
|
||||
clinical_diagnosis: formData.clinical_diagnosis,
|
||||
case_record: formData.case_record,
|
||||
herbs: formData.herbs,
|
||||
dose_count: formData.dose_count,
|
||||
dose_unit: formData.dose_unit || '浓缩水丸',
|
||||
dose_unit: formData.dose_unit,
|
||||
usage_days: formData.usage_days,
|
||||
usage_instruction: formData.usage_instruction,
|
||||
usage_time: formData.usage_time,
|
||||
usage_way: formData.usage_way,
|
||||
dietary_taboo: formData.dietary_taboo,
|
||||
usage_notes: formData.usage_notes,
|
||||
amount: formData.amount,
|
||||
doctor_name: formData.doctor_name,
|
||||
doctor_signature: formData.doctor_signature
|
||||
doctor_signature: formData.doctor_signature,
|
||||
is_shared: formData.is_shared
|
||||
})
|
||||
const id = res?.id
|
||||
if (id) {
|
||||
@@ -825,6 +1098,8 @@ const handleNewPrescription = async () => {
|
||||
// 将患者信息、诊断信息填入表单,新建处方时保留
|
||||
formData.diagnosis_id = saved.diagnosis_id ?? formData.diagnosis_id
|
||||
formData.appointment_id = saved.appointment_id ?? formData.appointment_id
|
||||
formData.prescription_name = ''
|
||||
formData.prescription_type = saved.prescription_type || '浓缩水丸'
|
||||
formData.patient_name = saved.patient_name || ''
|
||||
formData.gender = saved.gender ?? 0
|
||||
formData.gender_desc = saved.gender === 1 ? '男' : '女'
|
||||
@@ -834,13 +1109,21 @@ const handleNewPrescription = async () => {
|
||||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||||
formData.pulse = saved.pulse || ''
|
||||
formData.tongue = saved.tongue || ''
|
||||
formData.tongue_image = ''
|
||||
formData.pulse_condition = ''
|
||||
formData.clinical_diagnosis = saved.clinical_diagnosis || ''
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = saved.dose_unit || '浓缩水丸'
|
||||
formData.usage_instruction = '饭前'
|
||||
formData.dose_unit = saved.dose_unit || '剂'
|
||||
formData.usage_days = 7
|
||||
formData.usage_instruction = '水煎服,一日二次'
|
||||
formData.usage_time = '饭前'
|
||||
formData.usage_way = '温水送服'
|
||||
formData.dietary_taboo = []
|
||||
formData.usage_notes = ''
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || saved.doctor_name || ''
|
||||
formData.doctor_signature = ''
|
||||
formData.is_shared = 0
|
||||
formData.herbs = []
|
||||
// 新建处方时重新获取病历(旧处方可能没有 case_record)
|
||||
formData.case_record = saved.case_record && Object.keys(saved.case_record).length > 0
|
||||
|
||||
@@ -95,6 +95,7 @@ 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 {
|
||||
@@ -646,7 +647,7 @@ const startCall = async () => {
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('启动通话失败:', error)
|
||||
const errorMessage = error.message || '启动通话失败,请稍后重试'
|
||||
const errorMessage = formatTUICallUserError(error, '启动通话失败,请稍后重试')
|
||||
feedback.msgError(errorMessage)
|
||||
callRejected.value = true
|
||||
statusText.value = errorMessage
|
||||
|
||||
Reference in New Issue
Block a user