feat(ui): 危急值实时通知优化 - WebSocket推送+声音提醒+快捷处理
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="critical-value-notify">
|
||||||
|
<el-badge :value="pendingCount" :hidden="pendingCount === 0" :max="99">
|
||||||
|
<el-button @click="showDrawer = true" type="danger" plain>
|
||||||
|
⚠️ 危急值 ({{ pendingCount }})
|
||||||
|
</el-button>
|
||||||
|
</el-badge>
|
||||||
|
|
||||||
|
<el-drawer v-model="showDrawer" title="危急值处理" size="450px">
|
||||||
|
<div v-if="pendingList.length === 0" style="text-align:center;padding:40px;color:#999">
|
||||||
|
暂无待处理危急值
|
||||||
|
</div>
|
||||||
|
<div v-for="item in pendingList" :key="item.id" class="notify-item">
|
||||||
|
<el-alert
|
||||||
|
:title="item.patientName + ' - ' + item.itemName"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
>
|
||||||
|
<div style="margin: 8px 0">
|
||||||
|
<div><strong>结果:</strong> <span style="color:#F56C6C;font-weight:bold">{{ item.resultValue }}</span></div>
|
||||||
|
<div><strong>参考范围:</strong> {{ item.referenceRange }}</div>
|
||||||
|
<div><strong>报告时间:</strong> {{ item.reportTime }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button-group style="margin-top: 8px">
|
||||||
|
<el-button size="small" type="primary" @click="quickConfirm(item)">
|
||||||
|
确认接收
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="warning" @click="quickProcess(item)">
|
||||||
|
处理
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getPendingList, confirmCriticalValue, processCriticalValue } from '../api'
|
||||||
|
|
||||||
|
const pendingCount = ref(0)
|
||||||
|
const pendingList = ref([])
|
||||||
|
const showDrawer = ref(false)
|
||||||
|
let ws = null
|
||||||
|
let audio = null
|
||||||
|
|
||||||
|
const loadPending = async () => {
|
||||||
|
const res = await getPendingList()
|
||||||
|
pendingList.value = res.data || []
|
||||||
|
pendingCount.value = pendingList.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickConfirm = async (row) => {
|
||||||
|
await ElMessageBox.confirm('确认接收此危急值?', '确认', { type: 'warning' })
|
||||||
|
await confirmCriticalValue(row.id, { receiverId: 1, receiverName: '当前用户' })
|
||||||
|
ElMessage.success('已确认接收')
|
||||||
|
loadPending()
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickProcess = async (row) => {
|
||||||
|
await ElMessageBox.prompt('请输入处理结果', '处理危急值', { type: 'info' })
|
||||||
|
.then(async ({ value }) => {
|
||||||
|
await processCriticalValue(row.id, { handlerId: 1, handlerName: '当前用户', result: value || '已处理' })
|
||||||
|
ElMessage.success('处理完成')
|
||||||
|
loadPending()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playAlertSound = () => {
|
||||||
|
try {
|
||||||
|
if (!audio) {
|
||||||
|
audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVggoKIeGBGPoKQmJhyaTg3ZIKJi3ldREB/kJyaamA0NGOCi418W0ZAf4+YnmpgMzRjgoqNfFtGQH+QmJ5qYDM0Y4KKjXxbRkB/kJyeamAzNGOCio18W0ZAf5CcnmphMzRjgoqNfFtGQH+QnJ5qYTM0Y4KKjXxbRkB/kJyeamEzNGOCio18W0ZAf5CcnmphMzRjgoqNfFtGQH+QnJ5qYTM0Y4KKjXxbRkB/kJyeamEzNGOCio18W0ZAf5CcnmphMzRjgA==')
|
||||||
|
audio.loop = false
|
||||||
|
}
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initWebSocket = () => {
|
||||||
|
try {
|
||||||
|
const wsUrl = `ws://${window.location.hostname}:18082/ws/critical-value`
|
||||||
|
ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
if (data.type === 'CRITICAL_VALUE') {
|
||||||
|
pendingCount.value++
|
||||||
|
pendingList.value.unshift(data)
|
||||||
|
playAlertSound()
|
||||||
|
ElMessage.error(`新危急值: ${data.patientName} - ${data.itemName}`)
|
||||||
|
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
new Notification('危急值提醒', {
|
||||||
|
body: `${data.patientName} - ${data.itemName}: ${data.resultValue}`,
|
||||||
|
icon: '/favicon.ico'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
setTimeout(initWebSocket, 5000)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('WebSocket连接失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPending()
|
||||||
|
initWebSocket()
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (ws) ws.close()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notify-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
|
<div style="position:fixed;top:60px;right:20px;z-index:1000">
|
||||||
|
<CriticalValueNotify />
|
||||||
|
</div>
|
||||||
<el-row
|
<el-row
|
||||||
:gutter="20"
|
:gutter="20"
|
||||||
class="mb8"
|
class="mb8"
|
||||||
@@ -178,6 +181,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { getPendingList, getOverdueList, confirmCriticalValue, processCriticalValue, closeCriticalValue, getStatistics } from '../api'
|
import { getPendingList, getOverdueList, confirmCriticalValue, processCriticalValue, closeCriticalValue, getStatistics } from '../api'
|
||||||
|
import CriticalValueNotify from '../components/CriticalValueNotify.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
const activeTab = ref('pending')
|
const activeTab = ref('pending')
|
||||||
|
|||||||
Reference in New Issue
Block a user