叫号显示屏页面开发,诊疗目录新增或修改时添加医保编码唯一性校验。

This commit is contained in:
2026-01-04 14:24:33 +08:00
parent ddf1553846
commit 1311e87e13
6 changed files with 645 additions and 3 deletions

View File

@@ -0,0 +1,556 @@
<template>
<div class="call-number-display">
<!-- 头部区域 -->
<div class="header">
<h1>{{ departmentName }}</h1>
<div class="time">{{ currentTime }}</div>
</div>
<!-- 当前呼叫区 -->
<div class="current-call">
<div class="call-box">
<div class="call-text">
<span class="highlight">{{ currentCall?.number || '-' }}</span>
<span class="highlight">{{ currentCall?.name || '-' }}</span>
<span class="highlight">{{ currentCall?.room || '-' }}</span> 诊室就诊
</div>
</div>
</div>
<!-- 候诊信息区 -->
<div class="waiting-area">
<h2 class="section-title">候诊信息</h2>
<div class="table-container" ref="tableContainer">
<table class="waiting-table">
<thead style="position: sticky; top: 0; background: #f0f7ff; z-index: 1;">
<tr>
<th>序号</th>
<th>患者</th>
<th>诊室</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<template v-for="(group, doctorName) in groupedPatients" :key="doctorName">
<!-- 医生分组标题 -->
<tr class="doctor-header">
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
</tr>
<!-- 患者列表 -->
<tr v-for="(patient, index) in group" :key="patient.id">
<td>{{ index + 1 }}</td>
<td>{{ formatPatientName(patient.name) }}</td>
<td>{{ getDoctorRoom(doctorName) }}</td>
<td :style="{ color: index === 0 ? '#e74c3c' : '#27ae60' }">
{{ index === 0 ? '就诊中' : '等待' }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- 分页控制 -->
<div class="pagination-controls">
<button
id="prevPage"
@click="previousPage"
:disabled="currentPage === 1 || loading"
>上一页</button>
<span id="pageInfo">{{ currentPage }}/{{ totalPages }}</span>
<button
id="nextPage"
@click="nextPage"
:disabled="currentPage === totalPages || loading"
>下一页</button>
</div>
</div>
<!-- 辅助信息区 -->
<div class="info-bar">
<div class="info-item">
<span class="icon"></span>
<span>当前时间: {{ currentTime }}</span>
</div>
<div class="info-item">
<span class="icon">🔢</span>
<span>当前号: {{ currentCall?.number || '-' }}</span>
</div>
<div class="info-item">
<span class="icon">👥</span>
<span>等待人数: {{ waitingCount }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
// 响应式数据
const currentTime = ref('')
const currentCall = ref({
number: '1',
name: '李*四',
room: '3号'
})
const patients = ref([])
const loading = ref(false)
const currentPage = ref(1)
const patientsPerPage = 5
const autoScrollInterval = ref(null)
const scrollInterval = 5000 // 5秒自动翻页
// 科室名称
const departmentName = ref('心内科叫号显示屏幕')
// 计算属性
const groupedPatients = computed(() => {
const grouped = {}
patients.value.forEach(patient => {
if (!grouped[patient.doctor]) {
grouped[patient.doctor] = []
}
grouped[patient.doctor].push(patient)
})
return grouped
})
const waitingCount = computed(() => {
let count = 0
Object.values(groupedPatients.value).forEach(group => {
count += Math.max(0, group.length - 1) // 排除每个医生组中第一个就诊中的患者
})
return count
})
const totalPages = computed(() => {
const totalPatients = patients.value.length
return Math.ceil(totalPatients / patientsPerPage) || 1
})
// 方法
const updateTime = () => {
const now = dayjs()
currentTime.value = now.format('YYYY-MM-DD HH:mm')
}
const formatPatientName = (name) => {
if (!name || typeof name !== 'string') return '-'
if (name.length === 0) return '-'
return name.charAt(0) + '*' + name.slice(-1)
}
const getDoctorRoom = (doctorName) => {
// 根据医生获取固定诊室
const doctorRooms = {
'张医生': '3号',
'李医生': '1号',
'王医生': '2号'
}
return doctorRooms[doctorName] || '1号'
}
const generateWaitingData = async () => {
try {
loading.value = true
// 确保数组已正确初始化
if (!Array.isArray(patients.value)) {
patients.value = []
}
// 模拟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('获取候诊数据失败')
// 出错时设置为空数组
patients.value = []
} finally {
loading.value = false
}
}
const previousPage = () => {
if (currentPage.value > 1) {
currentPage.value--
scrollToTop()
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
scrollToTop()
}
}
const scrollToTop = () => {
nextTick(() => {
const container = document.querySelector('.table-container')
if (container && container.scrollTo) {
container.scrollTo({
top: 0,
behavior: 'smooth'
})
}
})
}
const startAutoScroll = () => {
stopAutoScroll()
autoScrollInterval.value = setInterval(() => {
if (currentPage.value < totalPages.value) {
currentPage.value++
} else {
currentPage.value = 1
}
scrollToTop()
}, scrollInterval)
}
const stopAutoScroll = () => {
if (autoScrollInterval.value) {
clearInterval(autoScrollInterval.value)
autoScrollInterval.value = null
}
}
// 生命周期钩子
onMounted(async () => {
// 初始化时间
updateTime()
// 每分钟更新时间
const timeInterval = setInterval(updateTime, 60000)
// 获取候诊数据
await generateWaitingData()
// 启动自动滚动
startAutoScroll()
// 鼠标悬停时暂停自动滚动
const 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(() => {
// 组件卸载时的清理工作
stopAutoScroll()
})
// 监听页面变化,重置滚动位置
watchEffect(() => {
scrollToTop()
})
</script>
<style lang="scss" scoped>
.call-number-display {
width: 100%;
max-width: 1200px;
background-color: #fff;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #eaeaea;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 12px;
margin-bottom: 20px;
h1 {
font-size: 2rem;
font-weight: 600;
letter-spacing: 1px;
margin: 0;
}
.time {
font-size: 1.5rem;
font-weight: 500;
background: rgba(255, 255, 255, 0.2);
padding: 5px 15px;
border-radius: 30px;
}
}
/* 当前呼叫区 */
.current-call {
text-align: center;
background: #f8f9fa;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
.call-box {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(74, 144, 226, 0.15);
border: 1px solid #e0e7ff;
animation: pulse 2s infinite;
.call-text {
font-size: 2.2rem;
font-weight: 700;
color: #4a90e2;
letter-spacing: 2px;
}
}
}
/* 候诊信息区 */
.waiting-area {
flex: 1;
padding: 0;
margin-bottom: 20px;
.section-title {
font-size: 1.4rem;
color: #555;
margin-bottom: 20px;
padding-left: 10px;
border-left: 4px solid #4a90e2;
font-weight: 600;
}
.table-container {
max-height: 400px;
overflow-y: auto;
scroll-behavior: smooth;
border-radius: 10px;
border: 1px solid #eaeaea;
}
.waiting-table {
width: 100%;
border-collapse: collapse;
background: white;
th,
.doctor-header td {
background-color: #f0f7ff;
color: #4a90e2;
font-weight: 600;
text-align: left;
padding: 15px 20px;
font-size: 1.1rem;
}
td {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
font-size: 1.05rem;
}
tr:nth-child(even) {
background-color: #fafcff;
}
tr:hover {
background-color: #f0f7ff;
}
.doctor-header {
background-color: #f0f7ff !important;
font-weight: bold;
}
}
.pagination-controls {
display: flex;
justify-content: center;
margin-top: 15px;
gap: 10px;
button {
padding: 8px 16px;
border: 1px solid #eaeaea;
border-radius: 4px;
background: #f8f9fa;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
&:hover:not(:disabled) {
background: #e9ecef;
border-color: #4a90e2;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
span {
padding: 8px 16px;
background: #f8f9fa;
border-radius: 4px;
font-weight: 500;
}
}
}
/* 辅助信息区 */
.info-bar {
background: #2c3e50;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
border-radius: 12px;
.info-item {
display: flex;
align-items: center;
font-size: 1.1rem;
.icon {
margin-right: 8px;
}
span {
margin-left: 10px;
font-weight: 500;
}
}
}
/* 动画效果 */
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.4);
}
70% {
box-shadow: 0 0 0 15px rgba(74, 144, 226, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0);
}
}
.highlight {
color: #e74c3c;
font-weight: bold;
}
/* 响应式设计 */
@media (max-width: 768px) {
.call-number-display {
padding: 10px;
margin: 0;
}
.header {
flex-direction: column;
text-align: center;
gap: 10px;
padding: 15px 20px;
h1 {
font-size: 1.5rem;
}
.time {
font-size: 1.2rem;
}
}
.current-call {
padding: 15px;
.call-box {
padding: 15px;
.call-text {
font-size: 1.8rem;
}
}
}
.info-bar {
flex-direction: column;
gap: 10px;
text-align: center;
.info-item {
justify-content: center;
}
}
.waiting-table {
th, td {
padding: 10px 15px;
font-size: 0.9rem;
}
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 1.2rem;
}
.current-call .call-box .call-text {
font-size: 1.5rem;
letter-spacing: 1px;
}
.waiting-table th,
.waiting-table td {
padding: 8px 12px;
font-size: 0.8rem;
}
}
</style>