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

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

@@ -24,6 +24,13 @@ public interface IDiagTreatMAppService {
*/ */
R<?> getDiseaseTreatmentInit(); R<?> getDiseaseTreatmentInit();
/**
* 根据ybNo查询诊疗目录(用于医保编码唯一性校验)
*
* @return
*/
R<?> getDiseaseTreatmentByYbNo(String ybNo);
/** /**
* 查询诊疗目录分页列表 * 查询诊疗目录分页列表
* *

View File

@@ -157,6 +157,18 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
return R.ok(diagnosisTreatmentInitDto); return R.ok(diagnosisTreatmentInitDto);
} }
/**
* 根据ybNo查询诊疗目录(用于医保编码唯一性校验)
*
* @return
*/
@Override
public R<?> getDiseaseTreatmentByYbNo(String ybNo) {
LambdaQueryWrapper<ActivityDefinition> queryWrapper = new LambdaQueryWrapper<ActivityDefinition>().eq(ActivityDefinition::getYbNo, ybNo);
List<ActivityDefinition> activityDefinitionList = activityDefinitionService.list(queryWrapper);
return R.ok(activityDefinitionList);
}
/** /**
* 查询诊疗目录分页列表 * 查询诊疗目录分页列表
* *

View File

@@ -40,6 +40,16 @@ public class DiagnosisTreatmentController {
return diagTreatMAppService.getDiseaseTreatmentInit(); return diagTreatMAppService.getDiseaseTreatmentInit();
} }
/**
* 根据ybNo查询诊疗目录(用于医保编码唯一性校验)
*
* @return
*/
@GetMapping("/information/{ybNo}")
public R<?> getDiseaseTreatmentByYbNo(@PathVariable String ybNo) {
return diagTreatMAppService.getDiseaseTreatmentByYbNo(ybNo);
}
/** /**
* 查询诊疗目录分页列表 * 查询诊疗目录分页列表
* *

View File

@@ -116,7 +116,11 @@
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="医保编码" prop="conditionCode"> <el-form-item label="医保编码" prop="conditionCode">
<el-input v-model="form.ybNo" placeholder="" /> <el-input
v-model="form.ybNo"
placeholder=""
clearable
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -363,6 +367,7 @@ import {
deptTreeSelect, deptTreeSelect,
editDiagnosisTreatment, editDiagnosisTreatment,
getDiagnosisTreatmentList, getDiagnosisTreatmentList,
getDiseaseTreatmentByYbNo,
locationTreeSelect, locationTreeSelect,
} from './diagnosistreatment'; } from './diagnosistreatment';
import PopoverList from '@/components/OpenHis/popoverList/index.vue'; import PopoverList from '@/components/OpenHis/popoverList/index.vue';
@@ -454,6 +459,7 @@ const treatmentItems = ref([
const medicineSearchKey = ref(''); const medicineSearchKey = ref('');
const isFirstOpen = ref(true); // 标记是否首次打开弹窗 const isFirstOpen = ref(true); // 标记是否首次打开弹窗
const totalPrice = ref('0.00'); // 总价 const totalPrice = ref('0.00'); // 总价
const isValidatingYbNo = ref(false); // 标记是否正在校验医保编码
// 计算总价 // 计算总价
function calculateTotalPrice() { function calculateTotalPrice() {
@@ -597,8 +603,31 @@ function reset() {
proxy.resetForm('diagnosisTreatmentRef'); proxy.resetForm('diagnosisTreatmentRef');
} }
async function validateYbNoUnique(ybNo, currentId = null) {
if (!ybNo || ybNo.trim() === '') {
return true; // 空值不进行校验
}
try {
const response = await getDiseaseTreatmentByYbNo(ybNo);
const data = response.data;
if (data && data.length > 0) {
// 检查是否存在相同的医保编码,排除当前编辑的记录
const existingRecord = data.find(item => item.id !== currentId);
if (existingRecord) {
return false; // 医保编码已存在
}
}
return true; // 医保编码唯一
} catch (error) {
console.error('医保编码校验失败:', error);
return true; // 校验失败时允许提交,由后端处理
}
}
/** 提交按钮 */ /** 提交按钮 */
function submitForm() { async function submitForm() {
form.value.ybFlag ? (form.value.ybFlag = 1) : (form.value.ybFlag = 0); form.value.ybFlag ? (form.value.ybFlag = 1) : (form.value.ybFlag = 0);
form.value.ybMatchFlag ? (form.value.ybMatchFlag = 1) : (form.value.ybMatchFlag = 0); form.value.ybMatchFlag ? (form.value.ybMatchFlag = 1) : (form.value.ybMatchFlag = 0);
form.value.ruleId ? (form.value.ruleId = 1) : (form.value.ruleId = 0); form.value.ruleId ? (form.value.ruleId = 1) : (form.value.ruleId = 0);
@@ -606,8 +635,26 @@ function submitForm() {
treatmentItems.value.length > 0 && treatmentItems.value[0].adviceDefinitionId != '' treatmentItems.value.length > 0 && treatmentItems.value[0].adviceDefinitionId != ''
? JSON.stringify(treatmentItems.value) ? JSON.stringify(treatmentItems.value)
: undefined; : undefined;
proxy.$refs['diagnosisTreatmentRef'].validate((valid) => { proxy.$refs['diagnosisTreatmentRef'].validate(async (valid) => {
if (valid) { if (valid) {
// 医保编码唯一性校验
if (form.value.ybNo) {
try {
isValidatingYbNo.value = true;
const isUnique = await validateYbNoUnique(form.value.ybNo, form.value.id);
if (!isUnique) {
proxy.$modal.msgWarning('医保编码已存在,请输入其他医保编码');
return;
}
} catch (error) {
console.error('医保编码校验失败:', error);
proxy.$modal.msgError('医保编码校验失败,请稍后重试');
return;
} finally {
isValidatingYbNo.value = false;
}
}
if (form.value.id != undefined) { if (form.value.id != undefined) {
editDiagnosisTreatment(form.value).then((response) => { editDiagnosisTreatment(form.value).then((response) => {
// 触发自定义事件,并传递数据给父组件 // 触发自定义事件,并传递数据给父组件

View File

@@ -104,3 +104,13 @@ export function getYbDiagnosisTreatmentList (queryParams) {
params: queryParams, params: queryParams,
}); });
} }
// 根据医保编码查询诊疗目录(用于医保编码唯一性校验)
export function getDiseaseTreatmentByYbNo (ybNo) {
return request ({
url: '/data-dictionary/diagnosis-treatment/information/' + ybNo,
method: 'get',
});
}

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>