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>
|
||||
<div class="app-container">
|
||||
<div style="position:fixed;top:60px;right:20px;z-index:1000">
|
||||
<CriticalValueNotify />
|
||||
</div>
|
||||
<el-row
|
||||
:gutter="20"
|
||||
class="mb8"
|
||||
@@ -178,6 +181,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getPendingList, getOverdueList, confirmCriticalValue, processCriticalValue, closeCriticalValue, getStatistics } from '../api'
|
||||
import CriticalValueNotify from '../components/CriticalValueNotify.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const activeTab = ref('pending')
|
||||
|
||||
Reference in New Issue
Block a user