diff --git a/src/api/msg.ts b/src/api/msg.ts new file mode 100644 index 0000000..dd78d08 --- /dev/null +++ b/src/api/msg.ts @@ -0,0 +1,42 @@ +/** + * @file msg.ts + * @description: 消息系统 + * @author: w + * @since: 2026-09-01 + */ + + +import request from '@/utils/request' + +// 查询消息列表 +export function queryMsgList(query?: Object) { + return request({ + url: '/bus/bloodbank/msg/queryMsgList', + method: 'get', + params: query + }) +} +// 获取某个消息详情 +export function getMsgInfo(query?: Object) { + return request({ + url: '/bus/bloodbank/msg/queryOne', + method: 'get', + params: query + }) +} +// 更新消息状态 +export function updateMsgFlag(query?: Object) { + return request({ + url: '/bus/bloodbank/msg/updateFlag', + method: 'get', + params: query + }) +} +// 获取未读消息数量 +export function queryNoReadCount(query?: Object) { + return request({ + url: '/bus/bloodbank/msg/queryNoReadCount', + method: 'get', + params: query + }) +} \ No newline at end of file diff --git a/src/components/WsMsgDetails/index.vue b/src/components/WsMsgDetails/index.vue new file mode 100644 index 0000000..0992d6e --- /dev/null +++ b/src/components/WsMsgDetails/index.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/src/components/WsMsgList/index.vue b/src/components/WsMsgList/index.vue new file mode 100644 index 0000000..c846d35 --- /dev/null +++ b/src/components/WsMsgList/index.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/src/components/WsNotifyPopup/index.vue b/src/components/WsNotifyPopup/index.vue new file mode 100644 index 0000000..2ff9166 --- /dev/null +++ b/src/components/WsNotifyPopup/index.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 485baf6..b980e28 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -32,7 +32,17 @@ +
+ + + + + +
+ + + @@ -52,16 +62,25 @@ import useAppStore from '@/store/modules/app' import useUserStore from '@/store/modules/user' import useSettingsStore from '@/store/modules/settings' import { queryHosList, addHos, upadteHos } from '@/api/system/medicalInstitutions' +import { queryNoReadCount } from '@/api/msg' +import WsMsgList from '@/components/WsMsgList/index.vue' +import emitter from '@/utils/mitt.js' const appStore = useAppStore() const userStore = useUserStore() const settingsStore = useSettingsStore() +const count = ref(0) +const wsMsgListRef = useTemplateRef('wsMsgListRef') const yljgName = ref('输血平台') function toggleSideBar(): void { appStore.toggleSideBar() } + +const openHandle = () => { + wsMsgListRef.value.open() +} function handleCommand(command: string): void { switch (command) { case "setLayout": @@ -132,10 +151,19 @@ async function toggleTheme(event?: MouseEvent): Promise { } } +const getCount = () => { + queryNoReadCount().then(res => { + count.value = res.data || 0 + }) +} + onMounted(() => { queryHosList().then((resp) => { yljgName.value = resp.data.find((item) => item.hospital_code == userStore.sysLoginParam.loginYLJG)?.hospital_name || '输血平台' }) + getCount() + emitter.on("resetCount", getCount) + }) @@ -286,6 +314,15 @@ onMounted(() => { } } } + + .msg_box { + width: 50px; + padding-top: 15px; + + .item { + cursor: pointer; + } + } } } diff --git a/src/layout/index.vue b/src/layout/index.vue index 7f957df..c0dba7d 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -13,6 +13,8 @@ + + @@ -23,8 +25,13 @@ import { AppMain, Navbar, Settings, TagsView } from './components' import useAppStore from '@/store/modules/app' import useSettingsStore from '@/store/modules/settings' import { useSysParam } from '@/hooks/useSysParam' +import { initWebSocket } from '@/utils/webSocket' +import useUserStore from '@/store/modules/user' +import emitter from '@/utils/mitt.js' +const userInfo = useUserStore() // 异步懒加载,首屏不阻塞 const BloodStockNotify = defineAsyncComponent(() => import('@/components/BloodStockNotify/index.vue')) +const WsNotifyPopup = defineAsyncComponent(() => import('@/components/WsNotifyPopup/index.vue')) const { initAllParams } = useSysParam() @@ -78,10 +85,37 @@ function setLayout() { settingRef.value.openSetting() } +const msgRef = useTemplateRef('msgRef') + +const onNewMessage = (newMsg) => { + console.log("🚀 ~ onNewMessage ~ newMsg:", newMsg) + if (newMsg.to == userInfo.name || newMsg.to == 'ALL') { + msgRef.value.open(newMsg) + } +} + + onMounted(() => { // 获取参数值 initAllParams(GLOBAL_PARAM_KEYS) + + // 初始化socket + const url = `ws://47.97.125.165:8907/ws/msgserver/${userInfo.name}` + initWebSocket({ fullUrl: url }) + + + emitter.on("APPLY_NOTICE", onNewMessage) + + // 读取缓存,防止ws消息比组件挂载早,丢失消息 + const cacheRaw = emitter.getCache("APPLY_NOTICE") + if (cacheRaw) { + onNewMessage(cacheRaw) + } +}) + +onBeforeUnmount(() => { + emitter.off("APPLY_NOTICE", onNewMessage) }) diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index f19309b..e06c24b 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -1,10 +1,7 @@ -// WebSocket连接配置 -type WebSocketOptions = { +import emitter from '@/utils/mitt'; + +type WebSocketInternalOptions = { fullUrl: string; - onMessage?: (data: string) => void; - onOpen?: () => void; - onError?: (error: Event) => void; - onClose?: () => void; reconnectInterval?: number; }; @@ -12,74 +9,75 @@ let websocket: WebSocket | null = null; let reconnectTimer: any = null; let isManualClose = false; -// 初始化WebSocket连接 -export function initWebSocket(options: WebSocketOptions) { - const { - fullUrl, - onMessage, - onOpen, - onError, - onClose, - reconnectInterval = 3000 - } = options; +/** 初始化WebSocket */ +export function initWebSocket(options: WebSocketInternalOptions) { + const { fullUrl, reconnectInterval = 3000 } = options; - // 关闭旧连接 + // 关闭已有连接 if (websocket) { websocket.close(); } - // 检查浏览器支持性 if (!('WebSocket' in window)) { console.error('当前浏览器不支持WebSocket'); return; } - // 创建WebSocket连接 try { websocket = new WebSocket(fullUrl); - // 连接成功回调 + // 连接成功 websocket.onopen = () => { clearTimeout(reconnectTimer!); isManualClose = false; - onOpen?.(); + console.log('WebSocket 连接成功'); }; - // 接收消息回调 + // 接收消息,自动解析按 msgType 分发事件 websocket.onmessage = (event) => { - onMessage?.(event.data); + try { + const rawMsg = JSON.parse(event.data); + const msgType = rawMsg.type; + //根据消息类型发送事件 + //APPLY_NOTICE 输血申请保存 + emitter.emit(msgType, rawMsg); + // 重置未读消息数 + emitter.emit('resetCount') + } catch (err) { + console.error('WebSocket消息解析失败:', err); + } }; - // 错误回调 + // 连接异常 websocket.onerror = (error) => { - onError?.(error); + console.error('WebSocket 连接异常:', error); startReconnect(() => initWebSocket(options), reconnectInterval); }; - // 关闭回调 + // 连接关闭 websocket.onclose = () => { - onClose?.(); + console.log('WebSocket 连接关闭'); if (!isManualClose) { startReconnect(() => initWebSocket(options), reconnectInterval); } }; } catch (error) { - console.error('WebSocket连接创建失败:', error); + console.error('WebSocket 创建失败:', error); startReconnect(() => initWebSocket(options), reconnectInterval); } } -// 发送消息 +/** 发送ws消息 */ export function sendWebSocketMessage(message: any): boolean { if (websocket && websocket.readyState === WebSocket.OPEN) { websocket.send(JSON.stringify(message)); return true; } - console.error('WebSocket未连接,无法发送消息'); + console.error('WebSocket未连接,发送失败'); return false; } -// 手动关闭WebSocket +/** 手动关闭ws(登出时调用) */ export function closeWebSocket(): void { isManualClose = true; if (reconnectTimer) { @@ -89,15 +87,15 @@ export function closeWebSocket(): void { websocket = null; } -// 启动重连 +/** 获取当前连接状态 */ +export function getWebSocketState(): number | null { + return websocket?.readyState || null; +} + +// 内部重连私有方法 function startReconnect(connectCallback: () => void, interval: number): void { if (reconnectTimer) { clearTimeout(reconnectTimer); } reconnectTimer = setTimeout(connectCallback, interval); -} - -// 获取当前连接状态 -export function getWebSocketState(): number | null { - return websocket?.readyState || null; } \ No newline at end of file diff --git a/src/views/blood/clinical/index.vue b/src/views/blood/clinical/index.vue index 400fdc0..f2ca6ab 100644 --- a/src/views/blood/clinical/index.vue +++ b/src/views/blood/clinical/index.vue @@ -11,9 +11,9 @@ 检索 新增单据 保存 + @click="saveBloodApply(1)" :loading="saveLoading">保存 通知取血 - 审核 + 审核 多次发血 打印 打印标签 @@ -21,7 +21,7 @@ + :disabled="bloodInfo.check_sign && bloodInfo.check_sign == 'Y'">
申请单信息
@@ -38,7 +38,8 @@ + @search="searchApply" @change="searchApply" + :disabled="bloodInfo.check_sign && bloodInfo.check_sign == 'Y'"> @@ -205,14 +206,16 @@ - + - + @@ -436,11 +439,7 @@ const bloodInfo = ref({ }) const saveLoading = ref(false) -const billStatus = ref([ - { label: '未发血', value: 'A' }, - { label: '已发血', value: 'B' }, - { label: '已作废', value: 'ZF' }, -]) +const auditLoading = ref(false) const searchForm = ref({ stime: dayjs().format('YYYY-MM-DD'), etime: dayjs().format('YYYY-MM-DD'), @@ -566,7 +565,7 @@ const bloodEnter = () => { if (res.data.length == 1) { const row = res.data[0] checkBlood(row) - } else if (res.data.length > 1) { + } else if (res.data.length > 1) { //多条血液数据进行弹窗选择 maskRef.value.open(res.data) } }) @@ -680,11 +679,13 @@ const handleSearch = () => { }) } -const saveBloodApply = () => { +const saveBloodApply = (type: number) => { // 1保存 2审核 //判断发血时间、通知取血时间大于当前时间7天 const isFlag = checkTimeTip(bloodInfo.value.occur_date, bloodInfo.value.notify_time) if (!isFlag) return - saveLoading.value = true + if (type == 1) { + saveLoading.value = true + } const data = { moreSign: moreSign.value, xkOutMain: { @@ -715,10 +716,16 @@ const saveBloodApply = () => { } } - saveBloodInfo(data).then(res => { + return saveBloodInfo(data).then(res => { if (res.code == 0) { - ElMessage.success('保存成功!') - addMatch() + if (type == 1) { + ElMessage.success('保存成功!') + addMatch() + return false + } else { + return true + } + } }).finally(() => { saveLoading.value = false @@ -737,14 +744,14 @@ function checkTimeTip(sendBloodTime, notifyGetBloodTime) { if (notifyDiff > 7) { msg.push('通知取血时间与当前时间超过七天;') } - if (msg) { + if (msg.length) { ElMessageBox.alert(msg.join('
'), '提示', { type: 'warning', dangerouslyUseHTMLString: true }) return false } else { return true } } - +//通知取血 const tzHandle = () => { if (!formData.value.bill_no) return ElMessage.warning('请选择配血单!') notifyMessage({ billNo: bloodInfo.value.bill_no }).then(res => { @@ -755,22 +762,33 @@ const tzHandle = () => { } const auditHandle = () => { - const data = { - applyNo: formData.value.bill_no, - billNo: bloodInfo.value.bill_no, - takePerson: bloodInfo.value.take_person, - userId: userInfo.name - } - bloodOutAudit(data).then(res => { - if (res.code == 0) { - ElMessage.success(res.msg) + if (!bloodInfo.value.bill_no) return ElMessage.warning("该单据还未保存!") + //审核之前保存 防止修改未保存审核单据 + saveBloodApply(2).then(saveFlag => { + if (saveFlag) { + const data = { + applyNo: formData.value.bill_no, + billNo: bloodInfo.value.bill_no, + takePerson: bloodInfo.value.take_person, + userId: userInfo.name + } + auditLoading.value = true + bloodOutAudit(data).then(res => { + if (res.code == 0) { + ElMessage.success(res.msg) + //审核成功后通知取血操作 + tzHandle() + } + }).finally(() => { + auditLoading.value = false + }) } }) } const printHandle = () => { + if (!bloodInfo.value.bill_no) return ElMessage.warning('该单据还未保存!') printLoading.value = true - if (!bloodInfo.value.bill_no) return ElMessage.warning('请选择配血单!') printReport({ billNo: bloodInfo.value.bill_no, templateName: 'BLOOD_OUT' }).then(res => { if (res.data) { printBase64PDF(res.data) diff --git a/src/views/doctor/application/index.vue b/src/views/doctor/application/index.vue index 19287b0..dd12ad1 100644 --- a/src/views/doctor/application/index.vue +++ b/src/views/doctor/application/index.vue @@ -522,10 +522,10 @@ {{ formatDict(scope.row.dept_id, 'ks') }} - - + + - +