88 分诊排队管理-》科室叫号显示屏 表triage_queue_item中添加了联合索引queue_date,organization_id,tenant_id,queue_order。添加了room_no,practitioner_id字段。
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<div class="call-number-display">
|
||||
<div class="call-number-display" ref="screenContainer">
|
||||
<!-- 头部区域 -->
|
||||
<div class="header">
|
||||
<h1>{{ departmentName }}</h1>
|
||||
<div class="time">{{ currentTime }}</div>
|
||||
<div class="header-right">
|
||||
<button class="fullscreen-btn" @click="toggleFullscreen">
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</button>
|
||||
<div class="time">{{ currentTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前呼叫区 -->
|
||||
@@ -31,19 +36,19 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="doctorName in paginatedDoctors" :key="doctorName">
|
||||
<template v-for="doctorName in paginatedDoctors">
|
||||
<template v-if="groupedPatients[doctorName]">
|
||||
<!-- 医生分组标题 -->
|
||||
<tr class="doctor-header">
|
||||
<tr class="doctor-header" :key="`doctor-${doctorName}`">
|
||||
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
||||
</tr>
|
||||
<!-- 患者列表 -->
|
||||
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="patient.id">
|
||||
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="`${doctorName}-${patient.id}`">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ formatPatientName(patient.name) }}</td>
|
||||
<td>{{ patient.name }}</td>
|
||||
<td>{{ getDoctorRoom(doctorName) }}</td>
|
||||
<td :style="{ color: index === 0 ? '#e74c3c' : '#27ae60' }">
|
||||
{{ index === 0 ? '就诊中' : '等待' }}
|
||||
<td :style="{ color: patient.status === 'CALLING' ? '#e74c3c' : '#27ae60' }">
|
||||
{{ patient.status === 'CALLING' ? '就诊中' : '等待' }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -88,49 +93,64 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import request from '@/utils/request'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
// ========== 配置参数 ==========
|
||||
const userStore = useUserStore()
|
||||
const { orgId: userOrgId, tenantId: userTenantId } = storeToRefs(userStore)
|
||||
|
||||
// 从登录用户获取科室ID(避免硬编码;后端已确保 orgId 以字符串返回)
|
||||
const ORGANIZATION_ID = computed(() => (userOrgId.value ? String(userOrgId.value) : ''))
|
||||
const TENANT_ID = computed(() => (userTenantId.value ? Number(userTenantId.value) : 1))
|
||||
const API_BASE_URL = '/triage/queue'
|
||||
// WebSocket 地址(通过 Nginx 代理,路径需要加 /openhis 前缀)
|
||||
const WS_URL = computed(
|
||||
() => `ws://${window.location.hostname}:18080/openhis/ws/call-number-display/${ORGANIZATION_ID.value}`
|
||||
)
|
||||
|
||||
// 响应式数据
|
||||
const currentTime = ref('')
|
||||
const currentCall = ref({
|
||||
number: '1',
|
||||
name: '李*四',
|
||||
room: '3号'
|
||||
})
|
||||
const currentCall = ref(null)
|
||||
const patients = ref([])
|
||||
const loading = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const patientsPerPage = 5
|
||||
const autoScrollInterval = ref(null)
|
||||
const scrollInterval = 5000 // 5秒自动翻页
|
||||
const wsConnection = ref(null) // WebSocket 连接
|
||||
const timeInterval = ref(null)
|
||||
const isFullscreen = ref(false)
|
||||
const screenContainer = ref(null)
|
||||
let tableContainer = null
|
||||
|
||||
// 科室名称
|
||||
const departmentName = ref('心内科叫号显示屏幕')
|
||||
const departmentName = ref('叫号显示屏幕')
|
||||
|
||||
// 计算属性
|
||||
const applyDefaultDepartmentName = () => {
|
||||
if (userStore.orgName) {
|
||||
departmentName.value = `${userStore.orgName} 叫号显示屏`
|
||||
}
|
||||
}
|
||||
|
||||
// 等待总人数(从后端返回)
|
||||
const waitingCount = ref(0)
|
||||
|
||||
// 计算属性:按医生分组的患者列表
|
||||
const groupedPatients = computed(() => {
|
||||
const grouped = {}
|
||||
patients.value.forEach(patient => {
|
||||
if (!grouped[patient.doctor]) {
|
||||
grouped[patient.doctor] = []
|
||||
}
|
||||
grouped[patient.doctor].push(patient)
|
||||
patients.value.forEach(doctorGroup => {
|
||||
grouped[doctorGroup.doctorName] = doctorGroup.patients || []
|
||||
})
|
||||
return grouped
|
||||
})
|
||||
|
||||
const waitingCount = computed(() => {
|
||||
let count = 0
|
||||
Object.values(groupedPatients.value).forEach(group => {
|
||||
count += Math.max(0, group.length - 1) // 排除每个医生组中第一个就诊中的患者
|
||||
})
|
||||
return count
|
||||
})
|
||||
|
||||
// 获取排序后的医生列表
|
||||
const sortedDoctors = computed(() => {
|
||||
return Object.keys(groupedPatients.value).sort()
|
||||
return patients.value.map(group => group.doctorName)
|
||||
})
|
||||
|
||||
// 按医生分组的分页逻辑
|
||||
@@ -159,6 +179,24 @@ const updateTime = () => {
|
||||
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const updateFullscreenState = () => {
|
||||
const isActive = !!document.fullscreenElement
|
||||
isFullscreen.value = isActive
|
||||
document.body.classList.toggle('call-screen-fullscreen', isActive)
|
||||
}
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen()
|
||||
} else if (screenContainer.value && screenContainer.value.requestFullscreen) {
|
||||
await screenContainer.value.requestFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换全屏失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatPatientName = (name) => {
|
||||
if (!name || typeof name !== 'string') return '-'
|
||||
if (name.length === 0) return '-'
|
||||
@@ -166,49 +204,98 @@ const formatPatientName = (name) => {
|
||||
}
|
||||
|
||||
const getDoctorRoom = (doctorName) => {
|
||||
// 根据医生获取固定诊室
|
||||
const doctorRooms = {
|
||||
'张医生': '3号',
|
||||
'李医生': '1号',
|
||||
'王医生': '2号'
|
||||
}
|
||||
return doctorRooms[doctorName] || '1号'
|
||||
// 从后端数据中查找医生的诊室号
|
||||
const doctorGroup = patients.value.find(group => group.doctorName === doctorName)
|
||||
return doctorGroup?.roomNo || '1号'
|
||||
}
|
||||
|
||||
const generateWaitingData = async () => {
|
||||
const ensureUserInfo = async () => {
|
||||
if (!userStore.orgId) {
|
||||
try {
|
||||
await userStore.getInfo()
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
applyDefaultDepartmentName()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示屏数据(从后端API)
|
||||
*/
|
||||
const fetchDisplayData = async () => {
|
||||
try {
|
||||
if (!ORGANIZATION_ID.value) {
|
||||
ElMessage.warning('未获取到登录用户科室信息')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
|
||||
// 确保数组已正确初始化
|
||||
if (!Array.isArray(patients.value)) {
|
||||
patients.value = []
|
||||
console.log('正在获取显示屏数据...', {
|
||||
url: `${API_BASE_URL}/display`,
|
||||
organizationId: ORGANIZATION_ID.value,
|
||||
tenantId: TENANT_ID.value
|
||||
})
|
||||
|
||||
const response = await request({
|
||||
url: `${API_BASE_URL}/display`,
|
||||
method: 'get',
|
||||
params: {
|
||||
organizationId: ORGANIZATION_ID.value,
|
||||
tenantId: TENANT_ID.value
|
||||
}
|
||||
})
|
||||
|
||||
console.log('后端响应:', response)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
const data = response.data
|
||||
|
||||
// 更新科室名称
|
||||
if (data.departmentName && data.departmentName !== '叫号显示屏') {
|
||||
departmentName.value = data.departmentName
|
||||
} else {
|
||||
applyDefaultDepartmentName()
|
||||
}
|
||||
|
||||
// 更新当前叫号信息
|
||||
if (data.currentCall) {
|
||||
currentCall.value = data.currentCall
|
||||
} else {
|
||||
currentCall.value = {
|
||||
number: null,
|
||||
name: '-',
|
||||
room: '-',
|
||||
doctor: '-'
|
||||
}
|
||||
}
|
||||
|
||||
// 更新等候队列(按医生分组)
|
||||
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||
patients.value = data.waitingList
|
||||
console.log('等候队列数据:', data.waitingList)
|
||||
} else {
|
||||
patients.value = []
|
||||
console.log('等候队列为空')
|
||||
}
|
||||
|
||||
// 更新等待人数
|
||||
waitingCount.value = data.waitingCount || 0
|
||||
|
||||
console.log('显示屏数据更新成功', data)
|
||||
ElMessage.success('数据加载成功')
|
||||
} else {
|
||||
throw new Error(response.msg || '获取数据失败')
|
||||
}
|
||||
|
||||
// 模拟API调用获取候诊数据
|
||||
// 实际项目中这里应该调用真实API
|
||||
const mockData = [
|
||||
{ id: 13, name: '李四', type: '专家', doctor: '张医生', status: '就诊中' },
|
||||
{ id: 14, name: '王五', type: '普通', doctor: '李医生', status: '候诊中' },
|
||||
{ id: 15, name: '赵六', type: '专家', doctor: '张医生', status: '候诊中' },
|
||||
{ id: 16, name: '钱七', type: '普通', doctor: '王医生', status: '候诊中' },
|
||||
{ id: 17, name: '孙八', type: '专家', doctor: '李医生', status: '候诊中' },
|
||||
{ id: 18, name: '周九', type: '普通', doctor: '王医生', status: '候诊中' },
|
||||
{ id: 19, name: '吴十', type: '专家', doctor: '张医生', status: '候诊中' },
|
||||
{ id: 20, name: '郑一', type: '普通', doctor: '李医生', status: '候诊中' },
|
||||
{ id: 21, name: '王二', type: '专家', doctor: '王医生', status: '候诊中' },
|
||||
{ id: 22, name: '李三', type: '普通', doctor: '张医生', status: '候诊中' },
|
||||
{ id: 23, name: '赵四', type: '专家', doctor: '李医生', status: '候诊中' },
|
||||
{ id: 24, name: '钱五', type: '普通', doctor: '王医生', status: '候诊中' }
|
||||
]
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
patients.value = mockData
|
||||
} catch (error) {
|
||||
console.error('获取候诊数据失败:', error)
|
||||
ElMessage.error('获取候诊数据失败')
|
||||
// 出错时设置为空数组
|
||||
console.error('获取显示屏数据失败:', error)
|
||||
ElMessage.error('获取显示屏数据失败:' + (error.message || '未知错误'))
|
||||
|
||||
// 出错时设置默认值
|
||||
patients.value = []
|
||||
currentCall.value = { number: null, name: '-', room: '-', doctor: '-' }
|
||||
waitingCount.value = 0
|
||||
applyDefaultDepartmentName()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -259,40 +346,177 @@ const stopAutoScroll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 WebSocket 连接
|
||||
*/
|
||||
const initWebSocket = () => {
|
||||
try {
|
||||
if (!ORGANIZATION_ID.value) {
|
||||
console.warn('未获取到科室ID,跳过 WebSocket 连接')
|
||||
return
|
||||
}
|
||||
console.log('正在连接 WebSocket:', WS_URL.value)
|
||||
wsConnection.value = new WebSocket(WS_URL.value)
|
||||
|
||||
wsConnection.value.onopen = () => {
|
||||
console.log('WebSocket 连接成功')
|
||||
ElMessage.success('实时连接已建立')
|
||||
}
|
||||
|
||||
wsConnection.value.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
console.log('收到 WebSocket 消息:', message)
|
||||
|
||||
if (message.type === 'connected') {
|
||||
console.log('WebSocket 连接确认:', message.message)
|
||||
} else if (message.type === 'update') {
|
||||
// 收到更新消息,刷新显示屏数据
|
||||
console.log('收到更新通知,刷新显示屏数据')
|
||||
handleWebSocketUpdate(message.data)
|
||||
} else if (message.type === 'pong') {
|
||||
// 心跳响应
|
||||
console.log('心跳响应')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析 WebSocket 消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
wsConnection.value.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error)
|
||||
ElMessage.error('实时连接出现错误')
|
||||
}
|
||||
|
||||
wsConnection.value.onclose = () => {
|
||||
console.log('WebSocket 连接关闭,5秒后重连')
|
||||
setTimeout(() => {
|
||||
if (!wsConnection.value || wsConnection.value.readyState === WebSocket.CLOSED) {
|
||||
initWebSocket()
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// 心跳检测:每30秒发送一次 ping
|
||||
setInterval(() => {
|
||||
if (wsConnection.value && wsConnection.value.readyState === WebSocket.OPEN) {
|
||||
wsConnection.value.send('ping')
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化 WebSocket 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 推送的更新数据
|
||||
*/
|
||||
const handleWebSocketUpdate = (data) => {
|
||||
if (!data) return
|
||||
|
||||
// 更新科室名称
|
||||
if (data.departmentName) {
|
||||
departmentName.value = data.departmentName
|
||||
}
|
||||
|
||||
// 更新当前叫号信息
|
||||
if (data.currentCall) {
|
||||
currentCall.value = data.currentCall
|
||||
}
|
||||
|
||||
// 更新等候队列
|
||||
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||
patients.value = data.waitingList
|
||||
}
|
||||
|
||||
// 更新等待人数
|
||||
if (data.waitingCount !== undefined) {
|
||||
waitingCount.value = data.waitingCount
|
||||
}
|
||||
|
||||
console.log('显示屏数据已更新(来自 WebSocket)')
|
||||
|
||||
// 播放语音(如果有新的叫号)
|
||||
if (data.currentCall && data.currentCall.number) {
|
||||
playVoiceNotification(data.currentCall)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放语音通知
|
||||
*/
|
||||
const playVoiceNotification = (callInfo) => {
|
||||
if (!callInfo || !callInfo.number) return
|
||||
|
||||
try {
|
||||
// 使用 Web Speech API 播放语音
|
||||
const utterance = new SpeechSynthesisUtterance(
|
||||
`请${callInfo.number}号${callInfo.name}到${callInfo.room}诊室就诊`
|
||||
)
|
||||
utterance.lang = 'zh-CN'
|
||||
utterance.rate = 0.9 // 语速
|
||||
utterance.pitch = 1.0 // 音调
|
||||
utterance.volume = 1.0 // 音量
|
||||
|
||||
window.speechSynthesis.speak(utterance)
|
||||
} catch (error) {
|
||||
console.error('语音播放失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 WebSocket 连接
|
||||
*/
|
||||
const closeWebSocket = () => {
|
||||
if (wsConnection.value) {
|
||||
wsConnection.value.close()
|
||||
wsConnection.value = null
|
||||
console.log('WebSocket 连接已关闭')
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(async () => {
|
||||
document.addEventListener('fullscreenchange', updateFullscreenState)
|
||||
await ensureUserInfo()
|
||||
// 初始化时间
|
||||
updateTime()
|
||||
// 每分钟更新时间
|
||||
const timeInterval = setInterval(updateTime, 60000)
|
||||
timeInterval.value = setInterval(updateTime, 60000)
|
||||
|
||||
// 获取候诊数据
|
||||
await generateWaitingData()
|
||||
// ✅ 获取初始数据(从后端 API)
|
||||
await fetchDisplayData()
|
||||
|
||||
// ✅ 初始化 WebSocket 连接(实时推送)
|
||||
initWebSocket()
|
||||
|
||||
// 启动自动滚动
|
||||
startAutoScroll()
|
||||
|
||||
// 鼠标悬停时暂停自动滚动
|
||||
const tableContainer = document.querySelector('.table-container')
|
||||
tableContainer = document.querySelector('.table-container')
|
||||
if (tableContainer) {
|
||||
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
||||
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
clearInterval(timeInterval)
|
||||
stopAutoScroll()
|
||||
if (tableContainer) {
|
||||
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
||||
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时的清理工作
|
||||
if (timeInterval.value) {
|
||||
clearInterval(timeInterval.value)
|
||||
timeInterval.value = null
|
||||
}
|
||||
stopAutoScroll()
|
||||
closeWebSocket() // ✅ 关闭 WebSocket 连接
|
||||
if (tableContainer) {
|
||||
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
||||
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
||||
tableContainer = null
|
||||
}
|
||||
document.removeEventListener('fullscreenchange', updateFullscreenState)
|
||||
document.body.classList.remove('call-screen-fullscreen')
|
||||
})
|
||||
|
||||
// 监听页面变化,重置滚动位置
|
||||
@@ -317,6 +541,43 @@ watchEffect(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(body.call-screen-fullscreen) {
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
:global(body.call-screen-fullscreen .sidebar-wrapper),
|
||||
:global(body.call-screen-fullscreen .navbar),
|
||||
:global(body.call-screen-fullscreen .tags-view-container),
|
||||
:global(body.call-screen-fullscreen #tags-view-container),
|
||||
:global(body.call-screen-fullscreen .drawer-bg) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:global(body.call-screen-fullscreen .app-wrapper),
|
||||
:global(body.call-screen-fullscreen .main-wrapper),
|
||||
:global(body.call-screen-fullscreen .content-wrapper),
|
||||
:global(body.call-screen-fullscreen .app-main) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:global(body.call-screen-fullscreen .app-wrapper) {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
:global(body.call-screen-fullscreen .call-number-display) {
|
||||
max-width: none;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
||||
@@ -342,6 +603,28 @@ watchEffect(() => {
|
||||
padding: 5px 15px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* 当前呼叫区 */
|
||||
|
||||
Reference in New Issue
Block a user