新增功能
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div v-if="showFriendlyCustom" class="chat-uikit-custom-msg">
|
||||
<span v-if="friendlyCustom!.tag" class="chat-uikit-custom-msg__tag">{{ friendlyCustom!.tag }}</span>
|
||||
<div class="chat-uikit-custom-msg__main">{{ friendlyCustom!.main }}</div>
|
||||
<div v-if="friendlyCustom!.sub" class="chat-uikit-custom-msg__sub">{{ friendlyCustom!.sub }}</div>
|
||||
</div>
|
||||
<div v-else-if="isCustomType && customDescription" class="chat-uikit-custom-msg chat-uikit-custom-msg--plain">
|
||||
<div class="chat-uikit-custom-msg__main">{{ customDescription }}</div>
|
||||
</div>
|
||||
<div v-else-if="isCustomType" class="chat-uikit-custom-msg chat-uikit-custom-msg--plain">
|
||||
<div class="chat-uikit-custom-msg__main">系统消息</div>
|
||||
<div v-if="customDataPreview" class="chat-uikit-custom-msg__sub">{{ customDataPreview }}</div>
|
||||
</div>
|
||||
<Message v-else v-bind="attrs" :message="message" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { Message, MessageType } from '@tencentcloud/chat-uikit-vue3'
|
||||
import type { MessageModel } from '@tencentcloud/chat-uikit-vue3'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
message: MessageModel
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
function payloadDataString(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== 'object') return null
|
||||
const p = payload as Record<string, unknown>
|
||||
const d = p.data
|
||||
if (typeof d === 'string') return d
|
||||
if (d && typeof d === 'object') return JSON.stringify(d)
|
||||
const ext = p.extension
|
||||
if (typeof ext === 'string' && ext.trim().startsWith('{')) return ext
|
||||
return null
|
||||
}
|
||||
|
||||
const isCustomType = computed(() => {
|
||||
const t = props.message?.type as unknown
|
||||
if (t === MessageType.CUSTOM) return true
|
||||
if (typeof t === 'string' && /custom/i.test(t)) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const friendlyCustom = computed(() => {
|
||||
if (!isCustomType.value) return null
|
||||
const raw = payloadDataString(props.message.payload)?.trim()
|
||||
if (!raw) return null
|
||||
return parseImBusinessPayload(raw)
|
||||
})
|
||||
|
||||
const showFriendlyCustom = computed(() => isCustomType.value && !!friendlyCustom.value)
|
||||
|
||||
const customDescription = computed(() => {
|
||||
const p = props.message.payload as Record<string, unknown> | undefined
|
||||
const d = p?.description
|
||||
return typeof d === 'string' && d.trim() ? d.trim() : ''
|
||||
})
|
||||
|
||||
/** 无法识别结构时短预览,避免空白 */
|
||||
const customDataPreview = computed(() => {
|
||||
const raw = payloadDataString(props.message.payload)?.trim()
|
||||
if (!raw || raw.length < 2) return ''
|
||||
if (raw.length <= 120) return raw
|
||||
return `${raw.slice(0, 117)}…`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-uikit-custom-msg {
|
||||
max-width: 85%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-light, #f4f4f5);
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
border: 1px solid var(--el-border-color-lighter, #ebeef5);
|
||||
}
|
||||
|
||||
.chat-uikit-custom-msg--plain {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.chat-uikit-custom-msg__tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--el-color-info-light-9, #f4f4f5);
|
||||
color: var(--el-color-info, #909399);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.chat-uikit-custom-msg__main {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-uikit-custom-msg__sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary, #909399);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -4,18 +4,34 @@
|
||||
v-show="visible"
|
||||
ref="chatWindowRef"
|
||||
class="chat-floating-window"
|
||||
:class="{ 'chat-floating-window--minimized': isMinimized }"
|
||||
:style="windowStyle"
|
||||
>
|
||||
<div
|
||||
class="chat-window-header"
|
||||
@mousedown="onHeaderMouseDown"
|
||||
>
|
||||
<span class="chat-window-title">与 {{ patientName }} 聊天</span>
|
||||
<el-button type="danger" link class="chat-close-btn" @click="handleClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
<span class="chat-window-title" :title="patientName">与 {{ patientName }} 聊天</span>
|
||||
<div class="chat-header-actions" @mousedown.stop>
|
||||
<el-button type="danger" link class="chat-close-btn" @click="handleClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
<el-tooltip v-if="!isMinimized" content="缩小为标题条,不关闭会话" placement="top">
|
||||
<el-button type="primary" link class="chat-minimize-btn" @click="isMinimized = true">
|
||||
<el-icon><Fold /></el-icon>
|
||||
<span class="chat-header-btn-text">缩小</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else content="展开聊天窗口" placement="top">
|
||||
<el-button type="primary" link class="chat-expand-btn" @click="isMinimized = false">
|
||||
<el-icon><Expand /></el-icon>
|
||||
<span class="chat-header-btn-text">展开</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-container">
|
||||
<div v-show="!isMinimized" class="chat-container">
|
||||
<div v-if="!isReady" class="loading-container">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>{{ loadingText }}</span>
|
||||
@@ -28,7 +44,7 @@
|
||||
<div class="chat-layout">
|
||||
<!-- 嵌入式问诊窗口仅与当前患者会话,不展示全局会话列表 -->
|
||||
<Chat class="chat-area">
|
||||
<MessageList :conversationID="conversationId" />
|
||||
<MessageList :conversationID="conversationId" :Message="ChatMessageItem" />
|
||||
<MessageInput>
|
||||
<template #headerToolbar>
|
||||
<div class="message-toolbar">
|
||||
@@ -37,6 +53,7 @@
|
||||
<FilePicker />
|
||||
<!-- 仅在 TUICallKit 初始化成功后展示音视频入口,避免“初始化登录未完成”错误 -->
|
||||
<AudioCallPicker v-if="isCallReady" />
|
||||
<!-- 视频接通后 doBindCallRoom → 后端 bindCallRoom 会调 CreateCloudRecording,RecordParams.RecordMode=2(混流/合流),见 TrtcCloudRecordingService -->
|
||||
<VideoCallPicker v-if="isCallReady" />
|
||||
<!-- 群组视频通话(基于 TUICallKitServer.calls,多人通话入口) -->
|
||||
<el-button
|
||||
@@ -76,8 +93,9 @@
|
||||
link
|
||||
class="call-kit-screenshot-btn"
|
||||
:loading="captureUploading"
|
||||
:disabled="captureUploading"
|
||||
@mousedown.stop
|
||||
@click.stop="handleCallKitScreenshot"
|
||||
@click.stop.prevent="handleCallKitScreenshot"
|
||||
>
|
||||
<el-icon><Camera /></el-icon>
|
||||
<span class="call-kit-screenshot-text">截屏</span>
|
||||
@@ -108,7 +126,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Loading, Close, Rank, Camera } from '@element-plus/icons-vue'
|
||||
import { Loading, Close, Rank, Camera, Fold, Expand } from '@element-plus/icons-vue'
|
||||
import { uploadImageBlob, uploadVideoBlob } from '@/api/file'
|
||||
import {
|
||||
attachLocalCallRecording,
|
||||
@@ -121,6 +139,10 @@ import {
|
||||
} from '@/api/tcm'
|
||||
import { CallLocalRecorder } from '@/utils/call-local-recorder'
|
||||
import { captureVideoFrameFromElement } from '@/utils/call-video-screenshot'
|
||||
import {
|
||||
DIAGNOSIS_TONGUE_IMAGES_UPDATED,
|
||||
type DiagnosisTongueImagesUpdatedDetail
|
||||
} from '@/utils/diagnosis-sync-events'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
formatTUICallUserError,
|
||||
@@ -141,7 +163,10 @@ import {
|
||||
AudioCallPicker,
|
||||
VideoCallPicker
|
||||
} from '@tencentcloud/chat-uikit-vue3'
|
||||
import ChatMessageItem from './ChatMessageItem.vue'
|
||||
import TencentCloudChat from '@tencentcloud/chat'
|
||||
import TUIChatEngine from '@tencentcloud/chat-uikit-engine'
|
||||
import { imCustomDataIndicatesVideoHangup } from '@/utils/im-call-hangup-detect'
|
||||
import '@/assets/chat-uikit.css'
|
||||
import {
|
||||
TUICallKit,
|
||||
@@ -153,6 +178,8 @@ import {
|
||||
} from '@tencentcloud/call-uikit-vue'
|
||||
import { TUICallEvent } from '@tencentcloud/call-engine-js'
|
||||
const assistant_ids=ref('');
|
||||
/** 缩小为仅标题条,便于医生操作后台其他页面(不登出、不挂断) */
|
||||
const isMinimized = ref(false)
|
||||
const visible = ref(false)
|
||||
const isReady = ref(false)
|
||||
const error = ref('')
|
||||
@@ -450,6 +477,84 @@ function installTrtcCloudPreStopHook() {
|
||||
|
||||
let engineRoomEventHandlers: Array<{ event: TUICallEvent; fn: (e: unknown) => void }> = []
|
||||
|
||||
/** IM MESSAGE_RECEIVED:对端挂断等信令走 TIM,与引擎事件双通道;需在 reportCallEnded 之后绑定 */
|
||||
let imHangupMessageHandler: ((event: { data?: unknown }) => void) | null = null
|
||||
let lastHangupCloudStopAt = 0
|
||||
const HANGUP_CLOUD_STOP_DEBOUNCE_MS = 1500
|
||||
|
||||
/** 将 tcm_call_record 置结束并 DeleteCloudRecording(合流停录) */
|
||||
async function reportCallEnded() {
|
||||
const id = recordingDiagnosisIdSnapshot ?? diagnosisId.value
|
||||
if (id == null) {
|
||||
console.warn('[chat-dialog] endCall 跳过:无诊单 ID(快照与当前均为空)')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await endCall({ diagnosis_id: id })
|
||||
console.log('[chat-dialog] endCall 已同步', { diagnosis_id: id })
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] endCall 失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 引擎 CALL_END / IM 挂断信令共用防抖,避免重复 DeleteCloudRecording */
|
||||
function debouncedStopRecordingAndEndCloud(source: string) {
|
||||
const now = Date.now()
|
||||
if (now - lastHangupCloudStopAt < HANGUP_CLOUD_STOP_DEBOUNCE_MS) return
|
||||
lastHangupCloudStopAt = now
|
||||
console.log('[chat-dialog] 挂断停合流:', source)
|
||||
if (localCallRecorder.isRecording()) {
|
||||
localCallRecorder.beginStopNow()
|
||||
}
|
||||
void reportCallEnded()
|
||||
}
|
||||
|
||||
function bindImHangupMessageListener() {
|
||||
unbindImHangupMessageListener()
|
||||
try {
|
||||
const chat = TUIChatEngine?.chat as { on?: (ev: string, fn: (e: { data?: unknown }) => void) => void; off?: (ev: string, fn: (e: { data?: unknown }) => void) => void } | undefined
|
||||
if (!chat?.on) {
|
||||
console.warn('[chat-dialog] TUIChatEngine.chat 不可用,无法监听 IM 视频挂断信令')
|
||||
return
|
||||
}
|
||||
imHangupMessageHandler = (event: { data?: unknown }) => {
|
||||
if (!visible.value) return
|
||||
const pu = patientUserId.value
|
||||
if (!pu) return
|
||||
const expectConv = pu.startsWith('C2C') ? pu : `C2C${pu}`
|
||||
const list = Array.isArray(event.data) ? event.data : []
|
||||
for (const m of list as Array<Record<string, unknown>>) {
|
||||
if (!m || m.conversationID !== expectConv) continue
|
||||
const t = m.type as string | number | undefined
|
||||
const isCustom =
|
||||
t === TencentCloudChat.TYPES.MSG_CUSTOM ||
|
||||
t === 'TIMCustomElem' ||
|
||||
(typeof t === 'string' && /custom/i.test(t))
|
||||
if (!isCustom) continue
|
||||
const payload = m.payload as Record<string, unknown> | undefined
|
||||
const raw = typeof payload?.data === 'string' ? payload.data : ''
|
||||
if (!raw || !imCustomDataIndicatesVideoHangup(raw)) continue
|
||||
debouncedStopRecordingAndEndCloud('IM')
|
||||
break
|
||||
}
|
||||
}
|
||||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, imHangupMessageHandler)
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] 绑定 IM 挂断监听失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
function unbindImHangupMessageListener() {
|
||||
if (!imHangupMessageHandler) return
|
||||
try {
|
||||
const chat = TUIChatEngine?.chat as { off?: (ev: string, fn: (e: { data?: unknown }) => void) => void } | undefined
|
||||
chat?.off?.(TencentCloudChat.EVENT.MESSAGE_RECEIVED, imHangupMessageHandler)
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
imHangupMessageHandler = null
|
||||
}
|
||||
|
||||
function bindEngineRoomEvents() {
|
||||
try {
|
||||
type EngineWithEvents = Record<string, unknown> & {
|
||||
@@ -469,11 +574,7 @@ function bindEngineRoomEvents() {
|
||||
void tryBindRoomIfReady()
|
||||
}
|
||||
const onEarlyStopRecording = () => {
|
||||
console.log('[chat-dialog] 检测到通话结束事件,立即停止录制')
|
||||
if (localCallRecorder.isRecording()) {
|
||||
console.log('[chat-dialog] 录制进行中,触发 beginStopNow')
|
||||
localCallRecorder.beginStopNow()
|
||||
}
|
||||
debouncedStopRecordingAndEndCloud('TUICallEngine')
|
||||
}
|
||||
const earlyStopEvents = [
|
||||
TUICallEvent.CALL_END,
|
||||
@@ -549,9 +650,18 @@ async function doBindCallRoom(rid: string) {
|
||||
}
|
||||
lastBoundRoomKey.value = key
|
||||
try {
|
||||
await bindCallRoom({ diagnosis_id: diagnosisId.value, room_id: rid })
|
||||
const bindRes = (await bindCallRoom({
|
||||
diagnosis_id: diagnosisId.value,
|
||||
room_id: rid
|
||||
})) as { cloud_recording?: { started?: boolean; task_id?: string; message?: string } } | undefined
|
||||
clearBindRoomRetry()
|
||||
console.log('[chat-dialog] bindCallRoom 成功', { room_id: rid })
|
||||
console.log('[chat-dialog] bindCallRoom 成功', { room_id: rid, bindRes })
|
||||
const cr = bindRes?.cloud_recording
|
||||
if (cr && cr.started === false && cr.message) {
|
||||
feedback.msgWarning(
|
||||
`云端合流录制未启动(关闭控制台全局录制后仅依赖 API):${cr.message}`
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] bindCallRoom 失败', e)
|
||||
lastBoundRoomKey.value = ''
|
||||
@@ -590,17 +700,6 @@ async function ensureCallRecordStarted() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 将 tcm_call_record 中本条进行中的记录置为已结束(挂断、对端挂断、异常断线、关窗等) */
|
||||
async function reportCallEnded() {
|
||||
if (diagnosisId.value == null) return
|
||||
try {
|
||||
await endCall({ diagnosis_id: diagnosisId.value })
|
||||
console.log('[chat-dialog] endCall 已同步')
|
||||
} catch (e) {
|
||||
console.warn('[chat-dialog] endCall 失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
function setupCallRoomBinding() {
|
||||
tearDownCallRoomBinding?.()
|
||||
let previousCallStatus =
|
||||
@@ -616,9 +715,9 @@ function setupCallRoomBinding() {
|
||||
localRecordingBadge.value = false
|
||||
if (previousCallStatus === CALL_STATUS_CALLING || previousCallStatus === CALL_STATUS_CONNECTED) {
|
||||
void (async () => {
|
||||
await finalizeLocalRecordingAndAttach()
|
||||
showCallKitWindow.value = false
|
||||
await reportCallEnded()
|
||||
showCallKitWindow.value = false
|
||||
await finalizeLocalRecordingAndAttach()
|
||||
})()
|
||||
}
|
||||
clearBindRoomRetry()
|
||||
@@ -842,6 +941,7 @@ onUnmounted(() => {
|
||||
localCallRecorder.reset()
|
||||
tearDownCallRoomBinding?.()
|
||||
unbindEngineRoomEvents()
|
||||
unbindImHangupMessageListener()
|
||||
unhookConsoleErrorForPackageHint()
|
||||
unbindEnginePackageErrorListener()
|
||||
window.removeEventListener('unhandledrejection', onTuiCallUnhandledRejection)
|
||||
@@ -916,10 +1016,10 @@ watch(() => activeConversation.value, async (newConversation) => {
|
||||
|
||||
const open = async (data: { patientId: number; patientName: string; diagnosisId?: number }) => {
|
||||
visible.value = true
|
||||
isMinimized.value = false
|
||||
posX.value = 100
|
||||
posY.value = 80
|
||||
callKitX.value = Math.max(0, (window.innerWidth - 375) / 2)
|
||||
callKitY.value = Math.max(0, (window.innerHeight - 667) / 2)
|
||||
resetCallKitPositionToLeft()
|
||||
patientName.value = data.patientName
|
||||
patientId.value = data.patientId
|
||||
diagnosisId.value = data.diagnosisId || null
|
||||
@@ -959,6 +1059,8 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
userSig: res.userSig,
|
||||
useUploadPlugin: true
|
||||
})
|
||||
// IM 自定义信令:对端挂断等 → 立即 endCall 停合流(不依赖小程序上报)
|
||||
bindImHangupMessageListener()
|
||||
const sdkAPPid = Number(res.sdkAppId)
|
||||
|
||||
// 初始化 TUICallKit(聊天内音视频通话依赖此初始化);只需全局成功一次
|
||||
@@ -989,17 +1091,18 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
localRecordingBadge.value = false
|
||||
localRecordingRound++
|
||||
recordingDiagnosisIdSnapshot = diagnosisId.value
|
||||
resetCallKitPositionToLeft()
|
||||
showCallKitWindow.value = true
|
||||
pendingCallRecordStart = ensureCallRecordStarted()
|
||||
void pendingCallRecordStart
|
||||
},
|
||||
// 同步停录须早于 SDK 内部退房;再 finalize 上传并关窗
|
||||
// 须先 endCall(DeleteCloudRecording),再 await 本地上传;否则上传 WebM 耗时数分钟会拖住云端停录,控制台房间长期「尚未结束」
|
||||
afterCalling: () => {
|
||||
localCallRecorder.beginStopNow()
|
||||
void (async () => {
|
||||
await finalizeLocalRecordingAndAttach()
|
||||
showCallKitWindow.value = false
|
||||
await reportCallEnded()
|
||||
showCallKitWindow.value = false
|
||||
await finalizeLocalRecordingAndAttach()
|
||||
})()
|
||||
}
|
||||
})
|
||||
@@ -1120,8 +1223,22 @@ const posStartY = ref(0)
|
||||
const callKitWrapperRef = ref<HTMLElement>()
|
||||
const callKitContentRef = ref<HTMLElement>()
|
||||
const captureUploading = ref(false)
|
||||
/** 与 .chat-dialog-call-kit-wrapper 尺寸一致 */
|
||||
const CALL_KIT_FLOAT_W = 375
|
||||
const CALL_KIT_FLOAT_H = 667
|
||||
const CALL_KIT_EDGE = 16
|
||||
|
||||
const callKitX = ref(0)
|
||||
const callKitY = ref(0)
|
||||
/** 视频浮窗默认靠左、垂直居中(每次发起通话也会重置,避免沿用上次拖动位置) */
|
||||
function resetCallKitPositionToLeft() {
|
||||
callKitX.value = CALL_KIT_EDGE
|
||||
callKitY.value = Math.max(
|
||||
CALL_KIT_EDGE,
|
||||
Math.floor((window.innerHeight - CALL_KIT_FLOAT_H) / 2)
|
||||
)
|
||||
}
|
||||
|
||||
const isCallKitDragging = ref(false)
|
||||
const callKitDragStartX = ref(0)
|
||||
const callKitDragStartY = ref(0)
|
||||
@@ -1157,15 +1274,21 @@ const onCallKitDragEnd = () => {
|
||||
|
||||
/** 截取当前视频画面并上传,同步到对应患者诊单的「舌苔照片」 */
|
||||
const handleCallKitScreenshot = async () => {
|
||||
if (patientId.value == null) {
|
||||
feedback.msgWarning('缺少患者信息,无法同步舌苔照片')
|
||||
return
|
||||
}
|
||||
const el = callKitContentRef.value
|
||||
if (!el) return
|
||||
// 同步防重入:避免连点触发两次上传、诊单里出现两张图
|
||||
if (captureUploading.value) return
|
||||
captureUploading.value = true
|
||||
try {
|
||||
const blob = await captureVideoFrameFromElement(el)
|
||||
if (patientId.value == null) {
|
||||
feedback.msgWarning('缺少患者信息,无法同步舌苔照片')
|
||||
return
|
||||
}
|
||||
const wrap = callKitWrapperRef.value
|
||||
const content = callKitContentRef.value
|
||||
const root = wrap || content
|
||||
if (!root) return
|
||||
const blob = await captureVideoFrameFromElement(root, {
|
||||
extraRoots: [content].filter(Boolean) as HTMLElement[]
|
||||
})
|
||||
if (!blob) {
|
||||
feedback.msgWarning('截屏失败,请确认视频画面已加载')
|
||||
return
|
||||
@@ -1195,6 +1318,14 @@ const handleCallKitScreenshot = async () => {
|
||||
payload.tongue_images = next
|
||||
await tcmDiagnosisEdit(payload)
|
||||
feedback.msgSuccess('舌苔照片已同步到诊单')
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<DiagnosisTongueImagesUpdatedDetail>(DIAGNOSIS_TONGUE_IMAGES_UPDATED, {
|
||||
detail: {
|
||||
diagnosisId: diagnosisId.value,
|
||||
tongue_images: next
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
const err = e as Error
|
||||
console.error(err)
|
||||
@@ -1224,7 +1355,8 @@ const windowStyle = computed(() => ({
|
||||
}))
|
||||
|
||||
const onHeaderMouseDown = (e: MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.chat-close-btn')) return
|
||||
const t = e.target as HTMLElement
|
||||
if (t.closest('.chat-close-btn') || t.closest('.chat-header-actions')) return
|
||||
isDragging.value = true
|
||||
dragStartX.value = e.clientX
|
||||
dragStartY.value = e.clientY
|
||||
@@ -1247,6 +1379,7 @@ const onHeaderMouseUp = () => {
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
unbindImHangupMessageListener()
|
||||
// 关闭时如有通话中/拨通中,先结束本地录制再挂断(不依赖悬浮窗是否显示)
|
||||
if (isCallReady.value) {
|
||||
const st = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS)
|
||||
@@ -1287,7 +1420,7 @@ defineExpose({ open })
|
||||
/* 浮动窗口:无遮罩,不遮挡页面,可任意拖动 */
|
||||
.chat-floating-window {
|
||||
position: fixed;
|
||||
width: 900px;
|
||||
width: 600px;
|
||||
height: 75vh;
|
||||
min-height: 400px;
|
||||
max-height: 90vh;
|
||||
@@ -1298,6 +1431,14 @@ defineExpose({ open })
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&--minimized {
|
||||
width: min(360px, 92vw);
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-window-header {
|
||||
@@ -1314,9 +1455,28 @@ defineExpose({ open })
|
||||
.chat-window-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.chat-close-btn {
|
||||
.chat-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-header-btn-text {
|
||||
margin-left: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chat-close-btn,
|
||||
.chat-minimize-btn,
|
||||
.chat-expand-btn {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="中医处方单"
|
||||
:title="drawerTitle"
|
||||
size="1200px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
@@ -264,7 +264,19 @@
|
||||
<h2 class="prescription-title">{{ templateConfig.stationName }}处方笺</h2>
|
||||
<div class="prescription-type">
|
||||
<el-tag v-if="savedPrescription.void_status === 1" type="danger" size="small">已作废</el-tag>
|
||||
<template v-else>
|
||||
<el-tag
|
||||
v-else-if="approvedPreviewOnly"
|
||||
type="success"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>已通过</el-tag>
|
||||
<el-tag
|
||||
v-else-if="Number(savedPrescription.audit_status) === 0"
|
||||
type="warning"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>待审核</el-tag>
|
||||
<template v-if="savedPrescription.void_status !== 1">
|
||||
<div>普通处方</div>
|
||||
<div class="text-sm">当日有效</div>
|
||||
</template>
|
||||
@@ -460,7 +472,7 @@
|
||||
下载PDF
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="savedPrescription.void_status !== 1"
|
||||
v-if="savedPrescription.void_status !== 1 && !approvedPreviewOnly"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="voiding"
|
||||
@@ -468,7 +480,7 @@
|
||||
>
|
||||
作废处方
|
||||
</el-button>
|
||||
<el-button @click="handleNewPrescription">新建处方</el-button>
|
||||
<el-button v-if="!approvedPreviewOnly" @click="handleNewPrescription">新建处方</el-button>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -609,6 +621,15 @@ const signatureCanvasRef = ref<HTMLCanvasElement>()
|
||||
const isDrawing = ref(false)
|
||||
const savedPrescription = ref<any>(null)
|
||||
|
||||
/** 已通过且未作废:与消费者处方「查看」一致,仅预览,不可作废/再开新方 */
|
||||
const approvedPreviewOnly = computed(() => {
|
||||
const s = savedPrescription.value
|
||||
if (!s) return false
|
||||
return Number(s.audit_status) === 1 && Number(s.void_status) !== 1
|
||||
})
|
||||
|
||||
const drawerTitle = computed(() => (approvedPreviewOnly.value ? '查看处方' : '中医处方单'))
|
||||
|
||||
// 处方库相关
|
||||
const showLibraryDialog = ref(false)
|
||||
const libraryLoading = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user