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

@@ -90,4 +90,10 @@ public interface IDoctorStationMainAppService {
*/ */
List<ReceptionStatisticsDto> getReceptionStatistics(String startTime,String endTime,Long practitionerId); List<ReceptionStatisticsDto> getReceptionStatistics(String startTime,String endTime,Long practitionerId);
/**
* 过号重排
* @param encounterId 就诊ID
* @return 操作结果
*/
R<?> rearrangeMissedEncounter(Long encounterId);
} }

View File

@@ -26,9 +26,11 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -323,4 +325,45 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
practitionerId); practitionerId);
} }
/**
* 过号重排核心实现
*/
@Override
@Transactional(rollbackFor = Exception.class) // 事务保证原子性
public R<?> rearrangeMissedEncounter(Long encounterId) {
// 1. 校验就诊记录是否存在
Encounter encounter = encounterMapper.selectById(encounterId);
if (encounter == null) {
return R.fail("就诊记录不存在");
}
// 2. 校验状态仅「在诊IN_PROGRESS=2」可重排
if (!EncounterStatus.IN_PROGRESS.getValue().equals(encounter.getStatusEnum())) {
return R.fail("仅「在诊」状态的患者可执行过号重排");
}
// 3. 核心更新:改回待诊+更新missed_time
Date now = new Date();
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
int updateCount = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>()
.eq(Encounter::getId, encounterId)
.set(Encounter::getStatusEnum, EncounterStatus.PLANNED.getValue()) // 改回1-待诊
.set(Encounter::getMissedTime, now) // 新增:设置过号时间为当前时间
.set(Encounter::getUpdateBy, practitionerId.toString()) // 操作医生ID
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())); // 防并发
if (updateCount == 0) {
return R.fail("过号重排失败:状态更新异常");
}
// 4. 同步更新接诊参与记录
iEncounterParticipantService.update(new LambdaUpdateWrapper<EncounterParticipant>()
.eq(EncounterParticipant::getEncounterId, encounterId)
.eq(EncounterParticipant::getTypeCode, ParticipantType.ADMITTER.getCode())
.set(EncounterParticipant::getStatusEnum, EncounterActivityStatus.COMPLETED.getValue()));
return R.ok("过号重排成功");
}
} }

View File

@@ -133,6 +133,29 @@ public class DoctorStationMainController {
} }
} }
/**
* 过号重排
*
* @param encounterId 就诊id
* @return 结果
*/
@GetMapping(value = "/rearrange-missed-encounter")
public R<?> rearrangeMissedEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
// 1. 空值校验(和现有接口保持一致)
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
// 2. 字符串转Long和现有接口保持一致
Long id = Long.parseLong(encounterId);
// 3. 调用AppService的过号重排方法
return iDoctorStationMainAppService.rearrangeMissedEncounter(id);
} catch (NumberFormatException e) {
// 4. 格式错误处理(和现有接口保持一致)
return R.fail("就诊ID格式错误");
}
}
/** /**
* 查询处方号列表信息 * 查询处方号列表信息
* *

View File

@@ -1,5 +1,6 @@
package com.openhis.web.doctorstation.dto; package com.openhis.web.doctorstation.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.openhis.common.annotation.Dict; import com.openhis.common.annotation.Dict;
@@ -127,4 +128,9 @@ public class PatientInfoDto {
* 就诊卡号 * 就诊卡号
*/ */
private String identifierNo; private String identifierNo;
/**
* 过号时间
*/
private Date missedTime;
} }

View File

@@ -24,7 +24,8 @@
T10.practitioner_user_id, T10.practitioner_user_id,
T10.jz_practitioner_user_id, T10.jz_practitioner_user_id,
T10.bus_no, T10.bus_no,
T10.identifier_no T10.identifier_no,
T10.missed_time
from from
( (
SELECT T1.tenant_id AS tenant_id, SELECT T1.tenant_id AS tenant_id,
@@ -50,7 +51,8 @@
T1.reception_time AS reception_time, T1.reception_time AS reception_time,
T1.organization_id AS org_id, T1.organization_id AS org_id,
T8.bus_no AS bus_no, T8.bus_no AS bus_no,
T9.identifier_no AS identifier_no T9.identifier_no AS identifier_no,
T1.missed_time AS missed_time
FROM adm_encounter AS T1 FROM adm_encounter AS T1
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0' LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0' LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'

View File

@@ -1,8 +1,6 @@
package com.openhis.administration.domain; package com.openhis.administration.domain;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity; import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
@@ -145,4 +143,9 @@ public class Encounter extends HisBaseEntity {
*/ */
private Long registrarId; private Long registrarId;
/**
* 过号时间
*/
@TableField("missed_time")
private Date missedTime;
} }

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>

View File

@@ -1,11 +1,16 @@
<template> <template>
<div style="display: flex; justify-content: space-between; height: 90vh"> <div style="display: flex; justify-content: space-between; height: 90vh">
<div style="width: 15%; height: 100%; border: 1px solid #eee; border-right: 0"> <div style="width: 15%; height: 100%; border: 1px solid #eee; border-right: 0">
<div style="padding: 10px; border: 1px solid #eee; height: 50px; border-right: 0"> <div
<span>现诊患者</span> style="padding: 10px; border: 1px solid #eee; height: 50px; border-right: 0; display: flex; align-items: center; justify-content: space-between">
<el-badge :value="waitCount > 0 ? waitCount : ''" :max="10" <div style="display: flex; align-items: center;">
style="float: right; color: #409eff; cursor: pointer; margin-right: 10px"> <span style="margin-right: 20px; font-weight: 600;">现诊患者</span>
<span @click="openDrawer"> 患者队列 </span> <el-button type="primary" size="small" @click.stop="handleOpenCallDialog" title="点击打开叫号界面">
<i class="el-icon-bell"></i> 呼叫
</el-button>
</div>
<el-badge :value="waitCount > 0 ? waitCount : ''" :max="10" style="color: #409eff; cursor: pointer;">
<span @click="openDrawer" style="font-weight: 600;"> 患者队列 </span>
</el-badge> </el-badge>
</div> </div>
<div style="width: 100%; padding: 10px"> <div style="width: 100%; padding: 10px">
@@ -15,16 +20,9 @@
<el-button icon="Search" @click="getPatientList" /> <el-button icon="Search" @click="getPatientList" />
</template> </template>
</el-input> </el-input>
<el-date-picker <el-date-picker v-model="registerTime" @change="handleTimeChange" type="date"
v-model="registerTime" style="width: 100%; margin-bottom: 10px" :clearable="false" placeholder="挂号时间" format="YYYY-MM-DD"
@change="handleTimeChange" value-format="YYYY-MM-DD" />
type="date"
style="width: 100%; margin-bottom: 10px"
:clearable="false"
placeholder="挂号时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
<el-scrollbar height="700px"> <el-scrollbar height="700px">
<div v-for="(item, index) in patientList" :class="item.active ? 'patient-card actived' : 'patient-card'" <div v-for="(item, index) in patientList" :class="item.active ? 'patient-card actived' : 'patient-card'"
:key="item.id" @click="handleCardClick(item, index)"> :key="item.id" @click="handleCardClick(item, index)">
@@ -110,27 +108,22 @@
<el-button type="primary" plain @click.stop="handleRefund(patientInfo.encounterId)"> <el-button type="primary" plain @click.stop="handleRefund(patientInfo.encounterId)">
退费 退费
</el-button> </el-button>
<el-button type="primary" plain class="top-layer-btn" @click.stop="getEnPrescription(patientInfo.encounterId)"> <el-button type="primary" plain class="top-layer-btn"
@click.stop="getEnPrescription(patientInfo.encounterId)">
处方单 处方单
</el-button> </el-button>
<el-button type="primary" plain class="top-layer-btn" :disabled="isHospitalizationButtonDisabled" @click.stop="handleHospitalizationClick()" @mouseenter="console.log('办理住院按钮状态:', { patientInfo: patientInfo?.value, hasEncounterId: patientInfo?.value?.encounterId, isDisabled: isHospitalizationButtonDisabled })"> 办理住院 </el-button> <el-button type="primary" plain class="top-layer-btn" :disabled="isHospitalizationButtonDisabled"
@click.stop="handleHospitalizationClick()"
@mouseenter="console.log('办理住院按钮状态:', { patientInfo: patientInfo?.value, hasEncounterId: patientInfo?.value?.encounterId, isDisabled: isHospitalizationButtonDisabled })">
办理住院 </el-button>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
<div style="padding: 10px; position: relative"> <div style="padding: 10px; position: relative">
<el-tabs <el-tabs type="card" style="width: 100%; height: 100%" v-loading="loading" v-model="activeTab"
type="card" @tab-change="handleClick(activeTab)">
style="width: 100%; height: 100%"
v-loading="loading"
v-model="activeTab"
@tab-change="handleClick(activeTab)"
>
<el-tab-pane label="门诊病历" name="hospitalizationEmr"> <el-tab-pane label="门诊病历" name="hospitalizationEmr">
<hospitalizationEmr <hospitalizationEmr :patientInfo="patientInfo" :activeTab="activeTab" @emrSaved="handleEmrSaved" />
:patientInfo="patientInfo"
:activeTab="activeTab"
@emrSaved="handleEmrSaved"
/>
</el-tab-pane> </el-tab-pane>
<!-- <el-tab-pane label="病历" name="emr"> <!-- <el-tab-pane label="病历" name="emr">
<Emr <Emr
@@ -151,12 +144,8 @@
" /> " />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="医嘱" name="prescription"> <el-tab-pane label="医嘱" name="prescription">
<prescriptionlist <prescriptionlist :patientInfo="patientInfo" ref="prescriptionRef" :activeTab="activeTab"
:patientInfo="patientInfo" :outpatientEmrSaved="outpatientEmrSaved" />
ref="prescriptionRef"
:activeTab="activeTab"
:outpatientEmrSaved="outpatientEmrSaved"
/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="中医" name="tcm"> <el-tab-pane label="中医" name="tcm">
<tcmAdvice :patientInfo="patientInfo" ref="tcmRef" /> <tcmAdvice :patientInfo="patientInfo" ref="tcmRef" />
@@ -180,24 +169,17 @@
<el-drawer v-model="drawer" title="患者队列" direction="ltr" @open="handleOpen"> <el-drawer v-model="drawer" title="患者队列" direction="ltr" @open="handleOpen">
<PatientList ref="patientDrawerRef" @toCurrent="handleReceive" /> <PatientList ref="patientDrawerRef" @toCurrent="handleReceive" />
</el-drawer> </el-drawer>
<RefundListDialog <RefundListDialog :open="openRefundListDialog" :encounterId="currentEncounterId"
:open="openRefundListDialog" @close="openRefundListDialog = false" @refresh="() => prescriptionRef.getListInfo()" />
:encounterId="currentEncounterId" <HospitalizationDialog :open="openDialog" :patientInfo="patientInfo" :encounterId="currentEncounterId"
@close="openRefundListDialog = false" :mainDiagnosis="mainDiagnosis" @close="openDialog = false" />
@refresh="() => prescriptionRef.getListInfo()" <PrescriptionInfo :open="openPrescriptionDialog" :precriptionInfo="prescriptionInfo"
/> @close="openPrescriptionDialog = false" />
<HospitalizationDialog <!-- 新增叫号弹窗组件 -->
:open="openDialog" <DoctorCallDialog v-model:dialogVisible="dialogVisible" :current-patient="currentCallPatient"
:patientInfo="patientInfo" :current-patient-list="currentWaitPatientList" :room-no="roomNo"
:encounterId="currentEncounterId" :department="userStore.orgName || patientInfo.organizationName || '心内科'" @callNext="callNext" @reCall="reCall"
:mainDiagnosis="mainDiagnosis" @finish="finishCall" @skip="skip" @requeue="requeue" @markSeen="markSeen" @callThis="callThis" />
@close="openDialog = false"
/>
<PrescriptionInfo
:open="openPrescriptionDialog"
:precriptionInfo="prescriptionInfo"
@close="openPrescriptionDialog = false"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
@@ -221,6 +203,7 @@ import HospitalizationDialog from './components/hospitalizationDialog.vue';
import tcmAdvice from './components/tcm/tcmAdvice.vue'; import tcmAdvice from './components/tcm/tcmAdvice.vue';
import inspectionApplication from './components/inspection/inspectionApplication.vue'; import inspectionApplication from './components/inspection/inspectionApplication.vue';
import surgeryApplication from './components/surgery/surgeryApplication.vue'; import surgeryApplication from './components/surgery/surgeryApplication.vue';
import DoctorCallDialog from './components/callQueue/DoctorCallDialog.vue';
import { formatDate, formatDateStr } from '@/utils/index'; import { formatDate, formatDateStr } from '@/utils/index';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
@@ -253,6 +236,10 @@ const drawer = ref(false);
const openRefundListDialog = ref(false); const openRefundListDialog = ref(false);
const openDialog = ref(false); const openDialog = ref(false);
const openPrescriptionDialog = ref(false); const openPrescriptionDialog = ref(false);
const dialogVisible = ref(false); // 叫号弹窗显示/隐藏
const currentCallPatient = ref({}); // 传给弹窗的【当前就诊患者】
const currentWaitPatientList = ref([]); // 传给弹窗的【候诊患者列表】
const roomNo = ref('4号'); // 诊室号
const saveStatus = ref(false); const saveStatus = ref(false);
const outpatientEmrSaved = ref(false); // 门诊病历保存状态 const outpatientEmrSaved = ref(false); // 门诊病历保存状态
const currentEncounterId = ref(''); const currentEncounterId = ref('');
@@ -327,8 +314,9 @@ const shortcuts = [
const eprescriptionRef = ref(); const eprescriptionRef = ref();
onMounted(() => { onMounted(() => {
getWaitPatient(); getWaitPatient();
}); getWaitPatientList();
getPatientList(); getPatientList();
});
// 获取现诊患者列表 // 获取现诊患者列表
function getPatientList() { function getPatientList() {
queryParams.value.statusEnum = 2; queryParams.value.statusEnum = 2;
@@ -405,6 +393,33 @@ function getWaitPatient() {
waitCount.value = res.data.total; waitCount.value = res.data.total;
}); });
} }
// 新增:获取候诊患者完整列表
function getWaitPatientList() {
queryParams.value.registerTimeSTime = formatDateStr(new Date(), 'YYYY-MM-DD') + ' 00:00:00';
queryParams.value.registerTimeETime = formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59';
queryParams.value.statusEnum = 1; // 筛选【待诊】患者
getList(queryParams.value).then((res) => {
currentWaitPatientList.value = res.data.records;
waitCount.value = res.data.total;
});
}
// 新增:打开叫号弹窗方法
function handleOpenCallDialog() {
// 刷新患者列表和候诊列表,确保数据是最新的
getPatientList();
getWaitPatientList();
// 检查当前选中的患者是否已经完诊,如果已经完诊,就不设置为当前呼叫患者
if (patientInfo.value && patientInfo.value.statusEnum !== 3) { // 3代表已完诊
currentCallPatient.value = patientInfo.value;
} else {
// 如果当前患者已经完诊,就清空当前呼叫患者
currentCallPatient.value = {};
}
dialogVisible.value = true;
}
function handleClick(tab) { function handleClick(tab) {
switch (tab) { switch (tab) {
@@ -716,6 +731,58 @@ const onHospitalization = async () => {
}); });
} }
}; };
// ========== 叫号弹窗所有回调事件 ==========
const callNext = () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 子组件已处理提示,这里无需重复提示
};
const reCall = () => {
// 选呼功能已实现,无需提示
};
const finishCall = () => {
dialogVisible.value = false;
// 刷新患者列表和候诊列表
getPatientList();
getWaitPatientList();
// 清空当前呼叫患者
currentCallPatient.value = {};
// 子组件已处理提示,这里无需重复提示
};
const skip = () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 子组件已处理提示,这里无需重复提示
};
const requeue = () => {
// 1. 重新获取候诊患者列表
getWaitPatientList();
// 2. 刷新现诊患者列表
getPatientList();
// 3. 刷新候诊人数统计
getWaitPatient();
// 4. 清空当前呼叫患者
currentCallPatient.value = {};
// 子组件已处理提示,这里无需重复提示
};
const markSeen = async () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 清空当前呼叫患者
currentCallPatient.value = {};
};
const callThis = (row) => {
handleCardClick(row);
currentCallPatient.value = row;
dialogVisible.value = false;
// 刷新患者列表和候诊列表
getPatientList();
getWaitPatientList();
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -724,6 +791,7 @@ const onHospitalization = async () => {
:deep(.el-descriptions__label) { :deep(.el-descriptions__label) {
font-size: 16px !important; font-size: 16px !important;
} }
:deep(.el-descriptions__content) { :deep(.el-descriptions__content) {
font-size: 16px !important; font-size: 16px !important;
} }
@@ -906,15 +974,18 @@ const onHospitalization = async () => {
left: 0; left: 0;
width: 100%; width: 100%;
height: calc(100% - 50px); height: calc(100% - 50px);
z-index: 998; /* 降低z-index避免覆盖按钮 */ z-index: 998;
/* 降低z-index避免覆盖按钮 */
/* 确保覆盖在内容上方,但不覆盖顶部按钮区域 */ /* 确保覆盖在内容上方,但不覆盖顶部按钮区域 */
cursor: not-allowed; cursor: not-allowed;
background-color: rgba(255, 255, 255, 0.01); background-color: rgba(255, 255, 255, 0.01);
pointer-events: none; /* 默认不捕获鼠标事件,只在真正需要禁用时才启用 */ pointer-events: none;
/* 默认不捕获鼠标事件,只在真正需要禁用时才启用 */
} }
.disabled-wrapper .overlay.overlay-disabled { .disabled-wrapper .overlay.overlay-disabled {
pointer-events: auto; /* 当需要真正禁用时启用指针事件 */ pointer-events: auto;
/* 当需要真正禁用时启用指针事件 */
} }
/* 顶层按钮样式,确保按钮始终在最上层 */ /* 顶层按钮样式,确保按钮始终在最上层 */