90,分诊排队管理-》医生叫号界面

This commit is contained in:
sindir
2026-01-22 12:14:01 +08:00
parent 8dff5d466a
commit 1dd7ee3428
9 changed files with 931 additions and 91 deletions

View File

@@ -54,6 +54,17 @@ export function leaveEncounter(encounterId) {
});
}
/**
* 重新排序未到诊患者
*/
export const rearrangeMissedNumber = (encounterId) => {
return request({
url: '/doctor-station/main/rearrange-missed-encounter', // 对应Controller的路径
method: 'get', // 注意现有接口都是GET这里和后端保持一致
params: { encounterId } // GET请求用params传参
});
};
/**
* 完诊
*/

View File

@@ -0,0 +1,675 @@
<template>
<el-dialog :model-value="dialogVisible" @update:model-value="val => emit('update:dialogVisible', val)" title=""
width="90%" :close-on-click-modal="false" custom-class="call-dialog-custom">
<!-- 顶部标题栏 -->
<div class="dialog-header">
<div class="header-title">医生叫号界面 - {{ department }}</div>
<div class="header-time">{{ formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<!-- 当前就诊患者区域 -->
<div class="current-patient">
<div class="current-label">当前就诊患者</div>
<div class="current-info">
<div>
<span>患者姓名:</span>
<span class="info-value">{{ getPatientNo(currentCallPatient) }} {{ currentCallPatient.patientName || '暂无'
}}</span>
</div>
<div>
<span>诊室:</span>
<span class="info-value">{{ roomNo || '4号' }}</span>
</div>
</div>
</div>
<!-- 叫号按钮区 -->
<div class="call-buttons">
<el-button class="btn-next" @click="callNextPatient">下一患者</el-button>
<el-button class="btn-recall" @click="showRecallDialog">选呼</el-button>
<el-button class="btn-finish" @click="finishCallPatient">完成</el-button>
<el-button class="btn-skip" @click="skipPatient">跳过</el-button>
<el-button class="btn-requeue" @click="requeuePatient">过号重排</el-button>
<el-button class="btn-seen" @click="markSeenPatient">已就诊</el-button>
</div>
<!-- 选呼对话框 -->
<el-dialog v-model="recallDialogVisible" title="选择患者呼叫" width="600px" :close-on-click-modal="false">
<div style="padding: 20px 0;">
<el-select v-model="selectedPatientEncounterId" filterable remote reserve-keyword placeholder="搜索患者姓名身份证号或就诊ID"
:remote-method="remoteSearchPatient" :loading="patientSearchLoading" clearable
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="patient in patientSearchOptions" :key="patient.encounterId"
:label="`${patient.patientName} (${patient.idCard || '无身份证号'})`" :value="patient.encounterId">
<div style="display: flex; justify-content: space-between;">
<span>{{ patient.patientName }}</span>
<span style="color: #8492a6; font-size: 14px;">{{ patient.idCard || '无身份证号' }}</span>
</div>
</el-option>
</el-select>
<div style="text-align: right;">
<el-button @click="recallDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmRecallPatient"
:disabled="!selectedPatientEncounterId">确定呼叫</el-button>
</div>
</div>
</el-dialog>
<!-- 候诊患者列表标题 -->
<div class="wait-list-title">候诊患者列表</div>
<!-- 候诊患者表格 -->
<el-table :data="sortedWaitPatientList" border style="width: 100%" header-cell-class-name="table-header"
:row-class-name="() => 'table-row'">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column prop="patientName" label="患者" width="180">
<template #default="scope">{{ scope.row.patientName }}</template>
</el-table-column>
<el-table-column prop="typeCode_dictText" label="号别" width="120" />
<el-table-column prop="organizationName" label="诊室" width="120" />
<el-table-column label="医生" width="120">
<template #default="scope">{{ scope.row.jz_practitioner_name || '心内科医生' }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default>等待中</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button size="small" class="btn-receive" @click="callThisPatient(scope.row)">
接诊
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 底部信息栏 -->
<div class="footer-info">
<div>当前时间{{ formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') }}</div>
<div>当前号{{ getPatientNo(currentCallPatient) || '暂无' }}</div>
<div>等待人数{{ currentWaitPatientList.length }}</div>
</div>
</el-dialog>
</template>
<script setup>
// ✅ 核心修复h 从 vue 导入,而非 element-plus
import { ref, watch, onMounted, h, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { receiveEncounter, completeEncounter, leaveEncounter, rearrangeMissedNumber } from '../api.js';
// ===== 1. Props/Emits 定义 =====
const props = defineProps({
dialogVisible: Boolean,
currentPatient: { type: Object, default: () => ({}) },
currentPatientList: { type: Array, default: () => [] }, // 候诊列表
roomNo: { type: String, default: '4号' },
department: { type: String, default: '心内科' } // 科室名称,默认为心内科
});
const emit = defineEmits(['update:dialogVisible', 'callNext', 'reCall', 'finish', 'skip', 'requeue', 'markSeen', 'callThis']);
// ===== 2. 响应式变量 =====
const currentWaitPatientList = ref([]); // 候诊患者列表
const currentCallPatient = ref({}); // 当前就诊患者
const selectedId = ref(''); // 选呼患者ID
// 选呼功能相关变量
const selectedPatientEncounterId = ref('');
const patientSearchOptions = ref([]);
const patientSearchLoading = ref(false);
const patientSearchQuery = ref('');
const recallDialogVisible = ref(false); // 选呼对话框显示/隐藏
// ===== 3. 计算属性 =====
// 按创建时间(create_time)和过号时间(missed_time)排序后的候诊列表
const sortedWaitPatientList = computed(() => {
// 空列表直接返回
if (!currentWaitPatientList.value.length) return [];
// 调试:打印当前时间和排序前的患者列表
const now = new Date();
console.log('当前时间:', now);
console.log('排序前的患者列表:');
currentWaitPatientList.value.forEach(item => {
console.log(`患者 ${item.patientName}:`, {
encounterId: item.encounterId,
missed_time: item.missed_time,
missedTime: item.missedTime,
create_time: item.create_time,
createTime: item.createTime,
statusEnum: item.statusEnum,
registerTime: item.registerTime,
parsed_missed_time: item.missed_time ? new Date(item.missed_time) : null,
parsed_create_time: item.create_time ? new Date(item.create_time) : null,
parsed_registerTime: item.registerTime ? new Date(item.registerTime) : null
});
});
// 判断患者是否为过号患者
const isMissedPatient = (patient) => {
// 检查missed_time或missedTime是否存在且不为null
return patient.missed_time != null || patient.missedTime != null;
};
// 获取患者有效时间的辅助函数
const getPatientTime = (patient) => {
// 调试:打印患者的完整时间信息
console.log(`患者 ${patient.patientName} 的时间信息:`, {
missed_time: patient.missed_time,
missedTime: patient.missedTime,
registerTime: patient.registerTime,
create_time: patient.create_time,
createTime: patient.createTime
});
// 1. 优先使用missed_time过号时间同时处理null和undefined
if (patient.missed_time != null) {
console.log(`患者 ${patient.patientName} 使用missed_time: ${patient.missed_time}`);
return patient.missed_time;
}
if (patient.missedTime != null) {
console.log(`患者 ${patient.patientName} 使用missedTime: ${patient.missedTime}`);
return patient.missedTime;
}
// 2. 其次使用registerTime挂号时间
if (patient.registerTime != null) {
console.log(`患者 ${patient.patientName} 使用registerTime: ${patient.registerTime}`);
return patient.registerTime;
}
// 3. 最后使用create_time或createTime创建时间
if (patient.create_time != null) {
console.log(`患者 ${patient.patientName} 使用create_time: ${patient.create_time}`);
return patient.create_time;
}
if (patient.createTime != null) {
console.log(`患者 ${patient.patientName} 使用createTime: ${patient.createTime}`);
return patient.createTime;
}
// 4. 兜底使用当前时间减去1000年确保排到最前面
console.warn(`患者 ${patient.patientName} 没有有效时间字段,使用默认时间`);
return new Date(0); // 1970-01-01
};
// 核心改进:明确区分过号患者和未过号患者
// 1. 未过号患者排在过号患者前面
// 2. 未过号患者按照registerTime升序排列
// 3. 过号患者按照missed_time升序排列
const sortedList = [...currentWaitPatientList.value].sort((a, b) => {
const aIsMissed = isMissedPatient(a);
const bIsMissed = isMissedPatient(b);
// 调试:打印排序比较
console.log(`排序比较: ${a.patientName}(过号:${aIsMissed}) vs ${b.patientName}(过号:${bIsMissed})`);
// 1. 未过号患者排在过号患者前面
if (aIsMissed !== bIsMissed) {
const result = aIsMissed ? 1 : -1;
console.log(`比较结果: ${result} (${aIsMissed ? 'a是过号患者排后面' : 'b是过号患者排后面'})`);
return result;
}
// 2. 获取a和b的有效时间
const aTime = getPatientTime(a);
const bTime = getPatientTime(b);
// 解析时间确保正确处理UTC时间
const timeA = new Date(aTime);
const timeB = new Date(bTime);
// 确保时间有效
if (isNaN(timeA.getTime())) {
console.log(`患者 ${a.patientName} 时间无效,排后面`);
return 1; // 无效时间排后面
}
if (isNaN(timeB.getTime())) {
console.log(`患者 ${b.patientName} 时间无效,排后面`);
return -1; // 有效时间排前面
}
// 3. 同类型患者按照时间升序排列
const result = timeA - timeB;
console.log(`比较结果: ${result} (时间比较: ${timeA} vs ${timeB})`);
return result;
});
// 调试:打印排序后的患者列表
console.log('排序后的患者列表:');
sortedList.forEach((item, index) => {
const effectiveTime = getPatientTime(item);
const isMissed = isMissedPatient(item);
console.log(`${index + 1}. ${item.patientName}:`, {
is_missed: isMissed,
missed_time: item.missed_time,
registerTime: item.registerTime,
create_time: item.create_time || item.createTime,
effective_time: new Date(effectiveTime)
});
});
return sortedList;
});
// ===== 4. 临时 formatDate 函数(避免外部依赖报错)=====
const formatDate = (date, format = 'YYYY-MM-DD') => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hour = String(d.getHours()).padStart(2, '0');
const minute = String(d.getMinutes()).padStart(2, '0');
const second = String(d.getSeconds()).padStart(2, '0');
if (format === 'YYYY-MM-DD HH:mm:ss') {
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
return `${year}-${month}-${day}`;
};
// ===== 4. 监听 Props 同步数据 =====
watch(() => props.currentPatientList, (newVal) => {
currentWaitPatientList.value = newVal || [];
}, { immediate: true });
watch(() => props.currentPatient, (newVal) => {
currentCallPatient.value = newVal || {};
}, { immediate: true });
// ===== 5. 排队号计算 =====
const getPatientNo = (patient) => {
if (!patient || !patient.encounterId || sortedWaitPatientList.value.length === 0) return '';
const index = sortedWaitPatientList.value.findIndex(item => item.encounterId === patient.encounterId);
return index > -1 ? `${index + 1}` : '';
};
// ===== 6. 按钮核心逻辑 =====
// 6.1 下一患者
const callNextPatient = async () => {
if (sortedWaitPatientList.value.length === 0) {
ElMessage.warning('暂无候诊患者');
return;
}
// 获取第一个患者
const nextPatient = sortedWaitPatientList.value[0];
try {
// 调用接诊API
await receiveEncounter(nextPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = nextPatient;
// 通知父组件
emit('callNext');
ElMessage.success(`已呼叫下一位患者:${nextPatient.patientName}`);
} catch (error) {
console.error('呼叫下一位患者失败:', error);
ElMessage.error(`呼叫下一位患者失败:${error.message || '系统错误'}`);
}
};
// 6.2 显示选呼对话框
const showRecallDialog = () => {
// 清空之前的选择
selectedPatientEncounterId.value = '';
patientSearchOptions.value = [];
// 显示对话框
recallDialogVisible.value = true;
};
// 6.3 远程搜索患者
const remoteSearchPatient = (query) => {
if (query === '') {
patientSearchOptions.value = [];
return;
}
patientSearchLoading.value = true;
// 模拟远程搜索,实际从候诊列表中查找
setTimeout(() => {
// 搜索匹配的患者 - 支持模糊搜索
patientSearchOptions.value = sortedWaitPatientList.value.filter(patient => {
// 患者姓名模糊匹配
const nameMatch = patient.patientName && patient.patientName.toLowerCase().includes(query.toLowerCase());
// 身份证号模糊匹配
const idCardMatch = patient.idCard && patient.idCard.includes(query);
// 就诊ID模糊匹配
const encounterIdMatch = patient.encounterId && patient.encounterId.toString().includes(query);
// 至少满足一个条件
return nameMatch || idCardMatch || encounterIdMatch;
});
patientSearchLoading.value = false;
}, 200);
};
// 6.4 确认呼叫患者
const confirmRecallPatient = async () => {
if (!selectedPatientEncounterId.value) return;
try {
// 查找选中的患者
const selectedPatient = sortedWaitPatientList.value.find(patient => patient.encounterId === selectedPatientEncounterId.value);
if (!selectedPatient) {
ElMessage.warning('未找到选中的患者');
return;
}
// 调用接诊API
await receiveEncounter(selectedPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = selectedPatient;
// 通知父组件
emit('reCall');
// 关闭对话框
recallDialogVisible.value = false;
// 清空选择
selectedPatientEncounterId.value = '';
ElMessage.success(`已呼叫患者:${selectedPatient.patientName}`);
} catch (error) {
console.error('选呼患者失败:', error);
ElMessage.error(`选呼患者失败:${error.message || '系统错误'}`);
}
};
// 6.5 完成
const finishCallPatient = async () => {
if (!currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者');
return;
}
try {
await completeEncounter(currentCallPatient.value.encounterId);
emit('finish');
emit('update:dialogVisible', false);
ElMessage.success('患者已完诊');
} catch (error) {
console.error('完诊失败:', error);
ElMessage.error(`完诊失败:${error.message || '系统错误'}`);
}
};
// 6.6 跳过
const skipPatient = async () => {
if (sortedWaitPatientList.value.length === 0) {
ElMessage.warning('暂无候诊患者');
return;
}
// 获取第一个患者
const nextPatient = sortedWaitPatientList.value[0];
try {
// 调用接诊API
await receiveEncounter(nextPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = nextPatient;
// 通知父组件
emit('skip');
ElMessage.success(`已跳过当前患者,呼叫下一位患者:${nextPatient.patientName}`);
} catch (error) {
console.error('跳过患者失败:', error);
ElMessage.error(`跳过患者失败:${error.message || '系统错误'}`);
}
};
// 6.7 过号重排
const requeuePatient = async () => {
// 1. 校验:当前必须有就诊患者才能重排
if (!currentCallPatient.value || !currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者,无法过号重排');
return;
}
try {
// 2. 显示确认对话框
await ElMessageBox.confirm(
`确定将患者 ${currentCallPatient.value.patientName} 进行过号重排吗?`,
'过号重排确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
draggable: true
}
);
// 3. 调用过号重排接口
console.log('开始调用过号重排接口患者ID:', currentCallPatient.value.encounterId);
const res = await rearrangeMissedNumber(currentCallPatient.value.encounterId);
// 4. 处理后端返回结果
console.log('过号重排接口返回结果:', res);
if (res.code === 200) {
// 5. 直接通知父组件刷新候诊列表,依赖父组件重新获取数据
emit('requeue');
// 6. 提示用户+清空当前就诊患者
ElMessage.success(res.msg || '过号重排成功,患者已排至队尾');
currentCallPatient.value = {};
} else {
// 7. 处理后端返回的错误信息
console.error('过号重排失败,后端返回错误:', res);
ElMessage.error(res.msg || '过号重排失败');
}
} catch (error) {
// 处理取消操作
if (error !== 'cancel') {
console.error('过号重排失败,捕获异常:', error);
ElMessage.error(`过号重排失败:${error.message || '系统错误'}`);
}
}
};
// 6.8 标记已就诊
const markSeenPatient = async () => {
if (!currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者');
return;
}
try {
await leaveEncounter(currentCallPatient.value.encounterId);
emit('markSeen');
emit('update:dialogVisible', false);
ElMessage.success('患者已暂离');
} catch (error) {
console.error('暂离失败:', error);
ElMessage.error(`暂离失败:${error.message || '系统错误'}`);
}
};
// 6.9 接诊指定患者
const callThisPatient = async (row) => {
try {
// 调用接诊API
await receiveEncounter(row.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = row;
// 通知父组件
emit('callThis', row);
ElMessage.success(`已接诊患者:${row.patientName}`);
} catch (error) {
console.error('接诊患者失败:', error);
ElMessage.error(`接诊患者失败:${error.message || '系统错误'}`);
}
};
</script>
<style scoped>
.call-dialog-custom {
--el-dialog-body-padding: 0;
}
.dialog-header {
background-color: #0b8074;
color: #fff;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.header-title {
font-size: 24px;
font-weight: bold;
color: #fff;
}
.header-time {
font-size: 16px;
font-weight: normal;
background-color: rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 20px;
color: #fff;
}
.current-patient {
background-color: #f8fafc;
padding: 20px 20px;
margin-bottom: 20px;
border-radius: 8px;
}
.current-label {
font-size: 20px;
font-weight: 700;
color: #1f7a7a;
margin-bottom: 12px;
}
.current-info {
display: flex;
gap: 60px;
font-size: 18px;
}
.info-value {
font-weight: 800 !important;
color: #1f7a7a !important;
font-size: 26px !important;
background-color: #e6f7ff;
padding: 8px 16px;
border-radius: 8px;
margin-left: 8px;
display: inline-block;
min-width: 200px;
text-align: center;
}
.call-buttons {
padding: 0 20px 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn-next {
background-color: #10b981 !important;
border-color: #10b981 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-recall {
background-color: #f59e0b !important;
border-color: #f59e0b !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-finish {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-skip {
background-color: #f5c518 !important;
border-color: #f5c518 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-requeue {
background-color: #165dff !important;
border-color: #165dff !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-seen {
background-color: #86909c !important;
border-color: #86909c !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-receive {
background-color: #10b981 !important;
border-color: #10b981 !important;
color: #fff !important;
font-size: 14px !important;
padding: 8px 12px !important;
border-radius: 4px;
}
.wait-list-title {
padding: 0 20px 12px;
font-size: 20px;
font-weight: 700;
color: #1f7a7a;
}
:deep(.table-header) {
background-color: #1f7a7a !important;
color: #fff !important;
font-weight: 700 !important;
font-size: 18px !important;
}
:deep(.table-row) {
font-size: 18px !important;
font-weight: 500;
}
:deep(.el-table__cell) {
padding: 16px 0 !important;
}
:deep(.el-table--enable-row-hover .el-table__body tr:hover>td) {
background-color: #f0f9ff !important;
}
.footer-info {
background-color: #1f7a7a;
color: #fff;
padding: 14px 20px;
margin-top: 20px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
</style>