feat(分诊队列): 实现分诊队列核心功能与日志记录

新增分诊队列相关服务接口与实现,包括队列管理、叫号操作和日志记录
添加DivLogService和CallRecordService用于记录分诊操作和叫号历史
在CurrentDayEncounterDto和TriageQueueItem中增加seqNo字段用于显示预约序号
实现分诊操作日志记录功能,包括添加队列、移除队列、叫号、完成等操作
新增CallType枚举定义叫号类型,并实现叫号记录功能
优化队列状态映射逻辑,支持更多状态类型显示
This commit is contained in:
wangjian963
2026-04-29 17:05:17 +08:00
parent 2b0acce1db
commit d4d05267ad
19 changed files with 933 additions and 303 deletions

View File

@@ -40,7 +40,9 @@ import com.openhis.web.chargemanage.dto.PatientMetadata;
import com.openhis.web.chargemanage.dto.PractitionerMetadata;
import com.openhis.web.chargemanage.dto.ReprintRegistrationDto;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.triageandqueuemanage.domain.DivLog;
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.service.DivLogService;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
import com.openhis.web.paymentmanage.appservice.IPaymentRecService;
import com.openhis.web.paymentmanage.dto.CancelPaymentDto;
@@ -111,6 +113,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
@Resource
com.openhis.triageandqueuemanage.service.TriageQueueItemService triageQueueItemService;
@Resource
DivLogService divLogService;
@Resource
ScheduleSlotMapper scheduleSlotMapper;
@@ -807,6 +812,23 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
queueItem.setDeleteFlag("1");
queueItem.setUpdateTime(LocalDateTime.now());
triageQueueItemService.updateById(queueItem);
// 写入分诊操作日志:诊前退号
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog divLog = new DivLog()
.setPoolId(queueItem.getPoolId())
.setSlotId(queueItem.getSlotId())
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction("REFUND")
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(divLog);
} catch (Exception e) {
log.error("写入分诊退号日志失败encounterId={}", encounterId, e);
}
log.info("退号成功已移除分诊队列记录encounterId={}, queueItemId={}", encounterId, queueItem.getId());
}

View File

@@ -170,4 +170,9 @@ public class CurrentDayEncounterDto {
*/
private String clinicRoom;
/**
* 预约序号(来自 adm_schedule_slot.seq_no
*/
private Integer seqNo;
}

View File

@@ -5,22 +5,32 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.triageandqueuemanage.domain.CallRecord;
import com.openhis.triageandqueuemanage.domain.DivLog;
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
import com.openhis.triageandqueuemanage.service.CallRecordService;
import com.openhis.triageandqueuemanage.service.DivLogService;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
import com.openhis.common.enums.CallType;
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
import com.openhis.web.triageandqueuemanage.dto.*;
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.core.common.core.domain.model.LoginUser;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
@@ -39,13 +49,22 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
@Resource
private TriageQueueItemService triageQueueItemService;
@Resource
private CallNumberSseManager callNumberSseManager;
@Resource
private TriageCandidateExclusionService triageCandidateExclusionService;
@Resource
private DivLogService divLogService;
@Resource
private CallRecordService callRecordService;
@Resource
private ScheduleSlotMapper scheduleSlotMapper;
@Override
public R<?> list(Long organizationId, LocalDate date) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
@@ -65,31 +84,33 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
List<TriageQueueItem> list = triageQueueItemService.list(wrapper);
// 通过 slotId 查询 seqNo 并填充到返回结果中(用于选呼显示预约号)
if (list != null && !list.isEmpty()) {
Map<Long, Integer> seqNoMap = buildSlotSeqNoMap(list);
list.forEach(item -> {
if (item.getSlotId() != null && seqNoMap.containsKey(item.getSlotId())) {
item.setSeqNo(seqNoMap.get(item.getSlotId()));
}
});
}
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
if (list != null && !list.isEmpty()) {
int beforeSize = list.size();
list = list.stream()
.filter(item -> !STATUS_COMPLETED.equals(item.getStatus()))
.collect(java.util.stream.Collectors.toList());
if (beforeSize != list.size()) {
System.out.println(">>> [TriageQueue] list() 警告:过滤掉了 " + (beforeSize - list.size()) + " 条 COMPLETED 状态的记录");
}
}
// 调试日志:检查状态值
if (list != null && !list.isEmpty()) {
System.out.println(">>> [TriageQueue] list() 返回 " + list.size() + " 条记录(已排除 COMPLETED");
for (int i = 0; i < Math.min(3, list.size()); i++) {
TriageQueueItem item = list.get(i);
System.out.println(" [" + i + "] patientName=" + item.getPatientName()
+ ", status=" + item.getStatus()
+ ", organizationId=" + item.getOrganizationId());
}
}
return R.ok(list);
}
/**
* 将候选池患者的挂号移入队列操作
* 并同时写入移入队列操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> add(TriageQueueAddReq req) {
@@ -133,6 +154,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
.setPoolId(it.getPoolId()) // ✅ 号源池ID用于div_log审计
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID用于div_log审计
.setSeqNo(it.getSeqNo()) // ✅ 预约序号(用于叫号显示)
.setStatus(STATUS_WAITING)
.setQueueOrder(++maxOrder)
.setDeleteFlag("0")
@@ -140,7 +162,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.setUpdateTime(LocalDateTime.now());
triageQueueItemService.save(qi);
// 写入分诊日志
writeDivLog(it.getPoolId(), it.getSlotId(), "ADD_QUEUE");
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
new LambdaQueryWrapper<TriageCandidateExclusion>()
@@ -171,17 +194,26 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
return R.ok(added);
}
/**
* 移除队列操作并同时写入移除操作日志
*
* @param id
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> remove(Long id) {
if (id == null) return R.fail("id 不能为空");
TriageQueueItem item = triageQueueItemService.getById(id);
if (item == null) return R.fail("队列项不存在");
if (item.getStatus() != null && item.getStatus() != 0) {
return R.fail("仅等待状态的患者可移出队列,当前状态码:" + item.getStatus());
}
// 逻辑删除队列项
item.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
triageQueueItemService.updateById(item);
// 写入分诊日志
writeDivLog(item.getPoolId(), item.getSlotId(), "REMOVE_QUEUE");
// 从排除列表中删除记录,使患者重新出现在候选池中
Integer tenantId = item.getTenantId();
LocalDate exclusionDate = item.getQueueDate();
@@ -239,6 +271,12 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
return R.ok(true);
}
/**
* 智能分诊选呼功能,在进行选呼操作时同时写入叫号记录和选呼操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> call(TriageQueueActionReq req) {
@@ -253,7 +291,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
// 写入分诊日志和叫号记录
writeDivLog(selected.getPoolId(), selected.getSlotId(), "CALL");
writeCallRecord(selected.getId(), selected.getPractitionerId(), CallType.CALL, selected.getRoomNo());
return R.ok(true);
} else if (STATUS_CALLING.equals(selected.getStatus())) {
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
@@ -264,6 +304,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
}
/**
* 智能分诊完成操作
* 在进行完成操作后同时写入叫号记录和完成操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> complete(TriageQueueActionReq req) {
@@ -283,7 +330,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
} else {
// 如果没有提供 id通过查询条件查找兼容旧逻辑
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
System.out.println(">>> [TriageQueue] complete() 开始执行(不限制日期,通过查询条件), tenantId=" + tenantId + ", orgId=" + orgId);
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
@@ -297,8 +343,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
calling = triageQueueItemService.getOne(callingWrapper, false);
System.out.println(">>> [TriageQueue] complete() 查询叫号中患者(不限制日期): orgId=" + orgId + ", 结果=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null"));
if (calling == null) {
return R.fail("当前没有叫号中的患者");
@@ -328,8 +372,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
System.out.println(">>> [TriageQueue] complete() 查询等待患者(不限制日期): actualOrgId=" + actualOrgId + ", 结果=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ")" : "null"));
if (next != null) {
next.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
@@ -340,16 +382,45 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 完成后推送 SSE 消息(实时通知显示屏刷新)
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
// 写入分诊日志和叫号记录
writeDivLog(calling.getPoolId(), calling.getSlotId(), "COMPLETE");
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.COMPLETE, calling.getRoomNo());
return R.ok(true);
}
/**
* 智能队列重排序功能操作单元
* 在进行队列重排序时同时在div_log表中写入重排序日志和叫号记录
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> requeue(TriageQueueActionReq req) {
return doRequeue(req, "REQUEUE");
}
/**
* 智能分诊跳过功能操作单元内部包含将队列正在叫号状态的号进行跳过
* 同时将跳过操作日志写入div_log表中以及写入叫号记录表。
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> skip(TriageQueueActionReq req) {
// 当前业务"跳过"按"过号重排"处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
return doRequeue(req, "SKIP");
}
/**
* 过号重排/跳过的核心逻辑
* @param action 日志动作REQUEUE 或 SKIP
*/
private R<?> doRequeue(TriageQueueActionReq req, String action) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
TriageQueueItem calling = null;
if (req != null && req.getId() != null) {
calling = triageQueueItemService.getById(req.getId());
@@ -358,7 +429,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
// 验证状态
if (!STATUS_CALLING.equals(calling.getStatus())) {
return R.fail("只能对\"叫号中\"状态的患者进行过号重排,当前患者状态为:" + calling.getStatus());
return R.fail("只能对\"叫号中\"状态的患者进行" + ("SKIP".equals(action) ? "跳过" : "过号重排") + ",当前患者状态为:" + calling.getStatus());
}
} else {
// 如果没有提供 id通过查询条件查找兼容旧逻辑
@@ -379,12 +450,11 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (calling == null) return R.fail("当前没有叫号中的患者");
}
// 使用实际找到的科室ID
Long actualOrgId = calling.getOrganizationId();
// 关键改进:在执行"跳过"操作之前,先检查是否有等待中的患者(判断队列状态)
// 如果没有等待中的患者,就不应该执行"过号重排"操作
// 关键改进:在执行跳过/重排操作之前,先检查是否有等待中的患者(判断队列状态)
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
.eq(TriageQueueItem::getDeleteFlag, "0")
@@ -393,22 +463,14 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getStatus, STATUS_SKIPPED))
.orderByAsc(TriageQueueItem::getQueueOrder)
.last("LIMIT 1");
// 如果指定了科室ID则按科室过滤否则查询所有科室全科模式
if (actualOrgId != null) {
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
// 调试日志:检查查询结果
System.out.println(">>> [TriageQueue] requeue() 查询等待中的患者:");
System.out.println(">>> - 科室ID: " + actualOrgId);
System.out.println(">>> - 找到的等待患者: " + (next != null ? next.getPatientName() + " (状态: " + next.getStatus() + ")" : "null"));
// 如果找不到等待中的患者,直接返回失败(不执行跳过操作)
if (next == null) {
System.out.println(">>> [TriageQueue] requeue() 失败:没有等待中的患者");
return R.fail("当前没有等待中的患者");
}
@@ -433,27 +495,20 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
triageQueueItemService.updateById(next);
recalcOrders(actualOrgId, null);
// ✅ 过号重排后推送 SSE 消息(实时通知显示屏刷新)
// 推送 SSE 消息
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
// 写入分诊日志和叫号记录
writeDivLog(calling.getPoolId(), calling.getSlotId(), action);
writeCallRecord(calling.getId(), calling.getPractitionerId(),
"SKIP".equals(action) ? CallType.SKIP : CallType.REQUEUE, calling.getRoomNo());
return R.ok(true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> skip(TriageQueueActionReq req) {
// 当前业务“跳过”按“过号重排”处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
return requeue(req);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> next(TriageQueueActionReq req) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
TriageQueueItem calling = null;
System.out.println(">>> [TriageQueue] next() 开始执行(不限制日期), tenantId=" + tenantId);
// 关键改进:如果提供了 id优先使用 id 直接查找(像 call 方法一样)
if (req != null && req.getId() != null) {
@@ -513,14 +568,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
// 调试日志:打印查询条件和结果
System.out.println(">>> [TriageQueue] next() 查询条件(不限制日期): tenantId=" + tenantId
+ ", actualOrgId=" + actualOrgId
+ ", deleteFlag=0"
+ ", status IN (WAITING, SKIPPED)");
System.out.println(">>> [TriageQueue] next() 查询结果: calling=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null")
+ ", next=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ", queueDate=" + next.getQueueDate() + ")" : "null"));
if (next == null) {
if (actualOrgId != null) {
@@ -535,6 +582,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (next.getOrganizationId() != null) {
recalcOrders(next.getOrganizationId(), null);
}
// 写入叫号记录
if (calling != null) {
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.NEXT, calling.getRoomNo());
}
writeCallRecord(next.getId(), next.getPractitionerId(), CallType.NEXT, next.getRoomNo());
return R.ok(true);
}
@@ -608,7 +662,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getDeleteFlag, "0")
.orderByAsc(TriageQueueItem::getQueueOrder)
);
// 通过 slotId 批量查询 seqNo方案 B不修改 triage_queue_item 表结构)
Map<Long, Integer> slotSeqNoMap = buildSlotSeqNoMap(allItems);
CallNumberDisplayResp resp = new CallNumberDisplayResp();
// 1. 获取科室名称(从第一条数据中取)
@@ -626,7 +683,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (callingItem != null) {
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
currentCall.setNumber(callingItem.getQueueOrder());
Integer displayNo = resolveDisplayNumber(callingItem, slotSeqNoMap);
currentCall.setNumber(displayNo);
currentCall.setName(maskPatientName(callingItem.getPatientName()));
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
currentCall.setDoctor(callingItem.getPractitionerName());
@@ -680,7 +738,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
patient.setId(item.getId());
patient.setName(maskPatientName(item.getPatientName()));
patient.setStatus(item.getStatus());
patient.setQueueOrder(item.getQueueOrder());
patient.setQueueOrder(resolveDisplayNumber(item, slotSeqNoMap));
patients.add(patient);
// 统计等待人数(不包括 CALLING 状态)
@@ -748,6 +806,82 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
System.err.println("推送显示屏更新失败:" + e.getMessage());
}
}
/**
* 写入分诊操作日志
*
* @param poolId 号源池ID
* @param slotId 号源槽位ID
* @param action 操作动作ADD_QUEUE/REMOVE_QUEUE/CALL/COMPLETE/SKIP/REQUEUE
*/
private void writeDivLog(Long poolId, Long slotId, String action) {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog log = new DivLog()
.setPoolId(poolId)
.setSlotId(slotId)
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction(action)
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(log);
} catch (Exception e) {
log.error("写入分诊日志失败", e);
}
}
/**
* 写入叫号记录
*
* @param queueId 队列ID
* @param doctorId 医生ID
* @param callType 叫号类型枚举
* @param room 诊室号
*/
private void writeCallRecord(Long queueId, Long doctorId, CallType callType, String room) {
try {
CallRecord record = new CallRecord()
.setQueueId(queueId)
.setDoctorId(doctorId)
.setCallTime(LocalDateTime.now())
.setCallType(callType.getValue().toString())
.setRoom(room)
.setCreateAt(LocalDateTime.now());
callRecordService.save(record);
} catch (Exception e) {
log.error("写入叫号记录失败", e);
}
}
/**
* 通过 slotId 批量查询 seqNo返回 slotId -> seqNo 映射
*/
private Map<Long, Integer> buildSlotSeqNoMap(List<TriageQueueItem> items) {
List<Long> slotIds = items.stream()
.map(TriageQueueItem::getSlotId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (slotIds.isEmpty()) {
return Collections.emptyMap();
}
List<ScheduleSlot> slots = scheduleSlotMapper.selectSeqNoBySlotIds(slotIds);
return slots.stream()
.filter(s -> s.getId() != null && s.getSeqNo() != null)
.collect(Collectors.toMap(ScheduleSlot::getId, ScheduleSlot::getSeqNo, (a, b) -> a));
}
/**
* 计算患者在叫号显示屏上应显示的号码:优先 seqNo预约序号否则 queueOrder排队号
*/
private Integer resolveDisplayNumber(TriageQueueItem item, Map<Long, Integer> slotSeqNoMap) {
if (item == null) return null;
if (item.getSlotId() != null && slotSeqNoMap.containsKey(item.getSlotId())) {
return slotSeqNoMap.get(item.getSlotId());
}
return item.getQueueOrder();
}
}

View File

@@ -19,6 +19,8 @@ public class TriageQueueEncounterItem {
private Long poolId;
/** 号源槽位ID关联 adm_schedule_slot.id用于 div_log 审计日志) */
private Long slotId;
/** 预约序号(来自 adm_schedule_slot.seq_no用于叫号显示 */
private Integer seqNo;
}

View File

@@ -71,7 +71,8 @@
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment,
T9.slot_id AS slotId,
T9.pool_id AS poolId
T9.pool_id AS poolId,
T9.seq_no AS seqNo
from (
SELECT T1.tenant_id AS tenant_id,
T1.id AS encounter_id,
@@ -100,6 +101,7 @@
T18.identifier_no AS identifier_no,
T1.order_id AS order_id,
om.slot_id AS slot_id,
ss.seq_no AS seq_no,
ss.pool_id AS pool_id,
sp.clinic_room AS clinic_room -- Bug #410从号源池获取诊室
FROM adm_encounter AS T1

View File

@@ -0,0 +1,58 @@
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 叫号类型
*
* @author wangjian963
* @date 2026-04-29
*/
@Getter
@AllArgsConstructor
public enum CallType implements HisEnumInterface {
/** 手动叫号(选呼) */
CALL(10, "CALL", "手动叫号(选呼)"),
/** 手动叫号(下一患者) */
NEXT(20, "NEXT", "手动叫号(下一患者)"),
/** 自动叫号 */
AUTO_CALL(30, "AUTO_CALL", "自动叫号"),
/** 跳过 */
SKIP(40, "SKIP", "跳过"),
/** 完成 */
COMPLETE(50, "COMPLETE", "完成"),
/** 重排 */
REQUEUE(60, "REQUEUE", "重排");
/** 状态码 */
private Integer value;
/** 英文标识 */
private String code;
/** 中文描述 */
private String info;
/**
* 根据状态码获取对应的叫号类型枚举
*
* @param value 状态码
* @return 对应的枚举值,未匹配时返回 null
*/
public static CallType getByValue(Integer value) {
if (value == null) {
return null;
}
for (CallType val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -61,4 +61,9 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
*/
List<DoctorAvailabilityDTO> selectDoctorAvailabilitySummary(@Param("query") TicketQueryDTO query);
/**
* 批量查询槽位序号(用于分诊叫号显示)
*/
List<ScheduleSlot> selectSeqNoBySlotIds(@Param("slotIds") List<Long> slotIds);
}

View File

@@ -0,0 +1,42 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@TableName(value = "call_record")
@EqualsAndHashCode(callSuper = false)
public class CallRecord {
@TableId(type = IdType.AUTO)
private Long recordId;
/** 队列ID (FK triage_queue_item.id) */
private Long queueId;
/** 医生ID */
private Long doctorId;
/** 叫号时间 */
private LocalDateTime callTime;
/**
* 叫号类型,使用 {@link com.openhis.common.enums.CallType} 枚举值
* 10-CALL(选呼), 20-NEXT(下一患者), 30-AUTO_CALL(自动叫号),
* 40-SKIP(跳过), 50-COMPLETE(完成), 60-REQUEUE(重排)
*/
private String callType;
/** 诊室(冗余) */
private String room;
/** 创建时间 */
private LocalDateTime createAt;
}

View File

@@ -0,0 +1,41 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@TableName(value = "div_log")
@EqualsAndHashCode(callSuper = false)
public class DivLog {
@TableId(type = IdType.AUTO)
private Long logId;
/** 号源池ID */
private Long poolId;
/** 号源槽位ID */
private Long slotId;
/** 操作人ID */
private Long opUserId;
/** 操作动作ADD_QUEUE/REMOVE_QUEUE/CALL/REFUND/COMPLETE/SKIP/REQUEUE */
private String action;
/** 操作时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateAt;
/** 创建时间 */
private LocalDateTime createdAt;
}

View File

@@ -1,6 +1,7 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -40,7 +41,10 @@ public class TriageQueueItem {
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
*/
private Integer status;
private Integer queueOrder; //排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
private Integer queueOrder; //排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
@TableField(exist = false)
private Integer seqNo; // 预约序号(来自 adm_schedule_slot.seq_no非数据库字段通过 JOIN 查询)
private LocalDateTime createTime;
private LocalDateTime updateTime;

View File

@@ -0,0 +1,9 @@
package com.openhis.triageandqueuemanage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.openhis.triageandqueuemanage.domain.CallRecord;
import org.springframework.stereotype.Repository;
@Repository
public interface CallRecordMapper extends BaseMapper<CallRecord> {
}

View File

@@ -0,0 +1,9 @@
package com.openhis.triageandqueuemanage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.openhis.triageandqueuemanage.domain.DivLog;
import org.springframework.stereotype.Repository;
@Repository
public interface DivLogMapper extends BaseMapper<DivLog> {
}

View File

@@ -0,0 +1,7 @@
package com.openhis.triageandqueuemanage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.openhis.triageandqueuemanage.domain.CallRecord;
public interface CallRecordService extends IService<CallRecord> {
}

View File

@@ -0,0 +1,7 @@
package com.openhis.triageandqueuemanage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.openhis.triageandqueuemanage.domain.DivLog;
public interface DivLogService extends IService<DivLog> {
}

View File

@@ -0,0 +1,11 @@
package com.openhis.triageandqueuemanage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.triageandqueuemanage.domain.CallRecord;
import com.openhis.triageandqueuemanage.mapper.CallRecordMapper;
import com.openhis.triageandqueuemanage.service.CallRecordService;
import org.springframework.stereotype.Service;
@Service
public class CallRecordServiceImpl extends ServiceImpl<CallRecordMapper, CallRecord> implements CallRecordService {
}

View File

@@ -0,0 +1,11 @@
package com.openhis.triageandqueuemanage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.triageandqueuemanage.domain.DivLog;
import com.openhis.triageandqueuemanage.mapper.DivLogMapper;
import com.openhis.triageandqueuemanage.service.DivLogService;
import org.springframework.stereotype.Service;
@Service
public class DivLogServiceImpl extends ServiceImpl<DivLogMapper, DivLog> implements DivLogService {
}

View File

@@ -437,5 +437,15 @@
p.doctor_name ASC
</select>
<select id="selectSeqNoBySlotIds" resultType="com.openhis.appointmentmanage.domain.ScheduleSlot">
SELECT id, seq_no
FROM adm_schedule_slot
WHERE id IN
<foreach collection="slotIds" item="slotId" open="(" separator="," close=")">
#{slotId}
</foreach>
AND delete_flag = '0'
</select>
</mapper>

View File

@@ -37,7 +37,7 @@ export function getCandidatePool(params) {
pageNo: params?.pageNo || 1,
pageSize: params?.pageSize || 10000,
searchKey: params?.searchKey || '',
statusEnum: params?.statusEnum || -1 // -1表示排除退号记录正常挂号
statusEnum: params?.statusEnum ?? 1 // 1=PLANNED(待诊),已挂号未接诊的患者;不传或传-1会返回已接诊的患者
},
skipErrorMsg: true // 跳过错误提示,由组件处理
})
@@ -169,4 +169,4 @@ export function getLocationTree(query) {
method: 'get',
params: query
})
}
}

View File

@@ -3,15 +3,22 @@
<!-- 顶部标题栏 -->
<div class="header-section">
<div class="header-left">
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
</div>
<div class="header-right">
<el-button type="primary" @click="handleRefresh">
<el-button
type="primary"
@click="handleRefresh"
>
<el-icon><Refresh /></el-icon>
刷新
</el-button>
<el-button @click="handleExit">退出</el-button>
<el-button @click="handleConfig">后台配置</el-button>
<el-button @click="handleExit">
退出
</el-button>
<el-button @click="handleConfig">
后台配置
</el-button>
</div>
</div>
@@ -31,28 +38,67 @@
style="width: 100%"
@selection-change="handleCandidateSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="sequenceNo" label="序号" width="80" align="center" />
<el-table-column prop="patientName" label="患者" width="100" align="center" />
<el-table-column prop="age" label="年龄" width="80" align="center" />
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
<el-table-column prop="room" label="诊室" width="120" align="center" />
<el-table-column prop="doctor" label="医生" width="120" align="center" />
<el-table-column prop="matchingRule" label="命中规则" min-width="150" align="center" />
<el-table-column
type="selection"
width="55"
align="center"
/>
<el-table-column
prop="sequenceNo"
label="序号"
width="80"
align="center"
/>
<el-table-column
prop="patientName"
label="患者"
width="100"
align="center"
/>
<el-table-column
prop="age"
label="年龄"
width="80"
align="center"
/>
<el-table-column
prop="appointmentType"
label="号别"
width="100"
align="center"
/>
<el-table-column
prop="room"
label="诊室"
width="120"
align="center"
/>
<el-table-column
prop="doctor"
label="医生"
width="120"
align="center"
/>
<el-table-column
prop="matchingRule"
label="命中规则"
min-width="150"
align="center"
/>
</el-table>
</div>
<div class="candidate-actions">
<el-button
type="primary"
@click="handleAddToQueue"
:disabled="selectedCandidates.length === 0"
@click="handleAddToQueue"
>
加入队列 >>
</el-button>
<el-button
type="primary"
@click="handleAddAllToQueue"
:disabled="filteredCandidatePoolList.length === 0"
@click="handleAddAllToQueue"
>
一键加入队列
</el-button>
@@ -75,13 +121,48 @@
highlight-current-row
@row-click="handleQueueRowClick"
>
<el-table-column prop="queueOrder" label="队序" width="80" align="center" />
<el-table-column prop="patientName" label="患者" width="100" align="center" />
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
<el-table-column prop="room" label="诊室" width="120" align="center" />
<el-table-column prop="doctor" label="医生" width="120" align="center" />
<el-table-column prop="waitingTime" label="等待" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<el-table-column
prop="queueOrder"
label="队序"
width="80"
align="center"
/>
<el-table-column
prop="patientName"
label="患者"
width="100"
align="center"
/>
<el-table-column
prop="appointmentType"
label="号别"
width="100"
align="center"
/>
<el-table-column
prop="room"
label="诊室"
width="120"
align="center"
/>
<el-table-column
prop="doctor"
label="医生"
width="120"
align="center"
/>
<el-table-column
prop="waitingTime"
label="等待"
width="100"
align="center"
/>
<el-table-column
prop="status"
label="状态"
width="100"
align="center"
>
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ scope.row.status }}
@@ -94,25 +175,25 @@
<div class="queue-actions-left">
<el-button
type="danger"
@click="handleRemoveFromQueue"
:disabled="!selectedQueueRow"
size="small"
@click="handleRemoveFromQueue"
>
<< 移出队列
&lt;&lt; 移出队列
</el-button>
<el-button
type="info"
@click="handleMoveUp"
:disabled="!selectedQueueRow || !canMoveUp"
size="small"
@click="handleMoveUp"
>
</el-button>
<el-button
type="info"
@click="handleMoveDown"
:disabled="!selectedQueueRow || !canMoveDown"
size="small"
@click="handleMoveDown"
>
</el-button>
@@ -120,15 +201,15 @@
<div class="queue-actions-right">
<el-button
:type="showOnlyWaiting ? 'primary' : ''"
@click="showOnlyWaiting = true"
size="small"
@click="showOnlyWaiting = true"
>
只显示等待
</el-button>
<el-button
:type="!showOnlyWaiting ? 'primary' : ''"
@click="showOnlyWaiting = false"
size="small"
@click="showOnlyWaiting = false"
>
显示全部状态
</el-button>
@@ -141,7 +222,9 @@
<div class="footer-section">
<!-- 就诊科室快速过滤栏 -->
<div class="filter-section">
<div class="filter-label"> 就诊科室快速过滤栏</div>
<div class="filter-label">
就诊科室快速过滤栏
</div>
<div class="filter-select-wrapper">
<el-select
v-model="selectedDept"
@@ -167,24 +250,53 @@
<!-- 叫号控制板 -->
<div class="call-control-section">
<div class="call-control-label"> 叫号控制板</div>
<div class="call-control-label">
叫号控制板
</div>
<div class="call-control-content">
<div class="current-call-display">
当前呼叫: {{ currentCall.number }} {{ currentCall.name }} 诊室: {{ currentCall.room }}
</div>
<div class="control-buttons">
<el-button type="primary" @click="handleSelectCall">选呼</el-button>
<el-button type="success" @click="handleNextPatient">下一患者</el-button>
<el-button type="warning" @click="handleSkip">跳过</el-button>
<el-button type="primary" @click="handleComplete">完成</el-button>
<el-button type="info" @click="handleRequeue">过号重排</el-button>
<el-button
type="primary"
@click="handleSelectCall"
>
选呼
</el-button>
<el-button
type="success"
@click="handleNextPatient"
>
下一患者
</el-button>
<el-button
type="warning"
@click="handleSkip"
>
跳过
</el-button>
<el-button
type="primary"
@click="handleComplete"
>
完成
</el-button>
<el-button
type="info"
@click="handleRequeue"
>
过号重排
</el-button>
</div>
</div>
</div>
<!-- LED显示 -->
<div class="led-section">
<div class="led-label"> LED:</div>
<div class="led-label">
LED:
</div>
<div class="led-display">
[{{ currentCall.number }}]{{ currentCall.name }}请到{{ currentCall.room }}({{ callType }})
</div>
@@ -197,205 +309,333 @@
</div>
</div>
<!-- 后台配置弹窗 -->
<el-dialog
v-model="configDialogVisible"
width="90%"
top="5vh"
>
<template #header>
<div class="config-dialog-header">
<div class="config-dialog-title">智能分诊规则引擎配置 - 心内科</div>
<div class="config-topbar-actions">
<el-button type="primary" @click="handleAddRule">新增规则</el-button>
<el-button type="primary" @click="handleSaveAllRules">保存全部</el-button>
<el-button @click="handleTestRule">测试规则</el-button>
<!-- 后台配置弹窗 -->
<el-dialog
v-model="configDialogVisible"
width="90%"
top="5vh"
>
<template #header>
<div class="config-dialog-header">
<div class="config-dialog-title">
智能分诊规则引擎配置 - 心内科
</div>
<div class="config-topbar-actions">
<el-button
type="primary"
@click="handleAddRule"
>
新增规则
</el-button>
<el-button
type="primary"
@click="handleSaveAllRules"
>
保存全部
</el-button>
<el-button @click="handleTestRule">
测试规则
</el-button>
</div>
</div>
</template>
<div class="config-container">
<div class="config-left">
<el-scrollbar height="560px">
<div
v-for="(item, idx) in rules"
:key="idx"
class="rule-card"
:class="{ active: idx === editingIndex }"
@click="handleSelectRule(idx)"
>
<div class="rule-title">
规则{{ idx + 1 }}
</div>
<div class="rule-sub">
prio={{ item.priority }}
</div>
<div class="rule-sub">
{{ item.name }}
</div>
<div class="rule-actions">
<el-button
size="small"
@click.stop="handleSelectRule(idx)"
>
编辑
</el-button>
<el-button
size="small"
@click.stop="handleDeleteRule(idx)"
>
删除
</el-button>
<el-button
size="small"
:disabled="idx === 0"
@click.stop="handleRuleMoveUp(idx)"
>
</el-button>
<el-button
size="small"
:disabled="idx === rules.length - 1"
@click.stop="handleRuleMoveDown(idx)"
>
</el-button>
</div>
</div>
</el-scrollbar>
</div>
<div class="config-right">
<el-form
label-width="110px"
class="config-form"
>
<el-form-item
label="规则名称:"
required
>
<el-input
v-model="ruleForm.name"
placeholder="请输入规则名称"
/>
</el-form-item>
<el-form-item label="科室:">
<el-select
v-model="ruleForm.dept"
class="config-fullwidth"
disabled
>
<el-option
label="心内科"
value="心内科"
/>
</el-select>
</el-form-item>
<el-form-item label="规则描述:">
<el-input
v-model="ruleForm.desc"
placeholder="请输入规则描述"
/>
</el-form-item>
<el-form-item
label="优先级:"
required
>
<el-input v-model="ruleForm.priority" />
</el-form-item>
<el-form-item label="周几生效:">
<el-checkbox-group v-model="ruleForm.weeks">
<el-checkbox
v-for="w in weekOptions"
:key="w.value"
:label="w.value"
>
{{ w.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="条件表达式(JSON):">
<el-input
v-model="ruleForm.expr"
type="textarea"
:rows="8"
placeholder="{&quot;age&quot;:&quot;>=60&quot;,&quot;regType&quot;:&quot;专家&quot;}"
/>
</el-form-item>
<div class="config-inline-actions">
<el-button @click="handleQuickGenerate">
快速生成器
</el-button>
<el-button @click="handleValidateRule">
语法检查
</el-button>
</div>
</el-form>
</div>
</div>
</template>
<div class="config-container">
<div class="config-left">
<el-scrollbar height="560px">
<div
v-for="(item, idx) in rules"
:key="idx"
class="rule-card"
:class="{ active: idx === editingIndex }"
@click="handleSelectRule(idx)"
<template #footer>
<div class="config-footer">
<el-button
type="primary"
@click="handleSaveCurrentRule"
>
<div class="rule-title">规则{{ idx + 1 }}</div>
<div class="rule-sub">prio={{ item.priority }}</div>
<div class="rule-sub">{{ item.name }}</div>
<div class="rule-actions">
<el-button size="small" @click.stop="handleSelectRule(idx)">编辑</el-button>
<el-button size="small" @click.stop="handleDeleteRule(idx)">删除</el-button>
<el-button size="small" @click.stop="handleRuleMoveUp(idx)" :disabled="idx === 0"></el-button>
<el-button size="small" @click.stop="handleRuleMoveDown(idx)" :disabled="idx === rules.length - 1"></el-button>
</div>
</div>
</el-scrollbar>
</div>
保存
</el-button>
<el-button @click="configDialogVisible = false">
取消
</el-button>
</div>
</template>
</el-dialog>
<div class="config-right">
<el-form label-width="110px" class="config-form">
<el-form-item label="规则名称:" required>
<el-input v-model="ruleForm.name" placeholder="请输入规则名称" />
</el-form-item>
<el-form-item label="科室:">
<el-select v-model="ruleForm.dept" class="config-fullwidth" disabled>
<el-option label="心内科" value="心内科" />
<!-- 快速生成器对话框 -->
<el-dialog
v-model="quickGeneratorDialogVisible"
title="快速生成器"
width="600px"
:close-on-click-modal="false"
>
<el-form
:model="quickGeneratorForm"
label-width="120px"
>
<el-form-item label="年龄条件:">
<div style="display: flex; align-items: center; gap: 10px;">
<el-select
v-model="quickGeneratorForm.ageOperator"
style="width: 100px;"
>
<el-option
label=">="
value=">="
/>
<el-option
label="<="
value="<="
/>
<el-option
label="="
value="="
/>
<el-option
label=">"
value=">"
/>
<el-option
label="<"
value="<"
/>
</el-select>
</el-form-item>
<el-form-item label="规则描述:">
<el-input v-model="ruleForm.desc" placeholder="请输入规则描述" />
</el-form-item>
<el-form-item label="优先级:" required>
<el-input v-model="ruleForm.priority" />
</el-form-item>
<el-form-item label="周几生效:">
<el-checkbox-group v-model="ruleForm.weeks">
<el-checkbox v-for="w in weekOptions" :key="w.value" :label="w.value">
{{ w.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="条件表达式(JSON):">
<el-input
v-model="ruleForm.expr"
type="textarea"
:rows="8"
placeholder='{"age":">=60","regType":"专家"}'
<el-input-number
v-model="quickGeneratorForm.ageValue"
:min="0"
:max="150"
:precision="0"
placeholder="年龄"
style="width: 150px;"
/>
</el-form-item>
<div class="config-inline-actions">
<el-button @click="handleQuickGenerate">快速生成器</el-button>
<el-button @click="handleValidateRule">语法检查</el-button>
<el-button
type="text"
@click="quickGeneratorForm.ageOperator = null; quickGeneratorForm.ageValue = null"
>
清除
</el-button>
</div>
</el-form>
</div>
</div>
</el-form-item>
<template #footer>
<div class="config-footer">
<el-button type="primary" @click="handleSaveCurrentRule">保存</el-button>
<el-button @click="configDialogVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
<!-- 快速生成器对话框 -->
<el-dialog
v-model="quickGeneratorDialogVisible"
title="快速生成器"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="quickGeneratorForm" label-width="120px">
<el-form-item label="年龄条件:">
<div style="display: flex; align-items: center; gap: 10px;">
<el-select v-model="quickGeneratorForm.ageOperator" style="width: 100px;">
<el-option label=">=" value=">=" />
<el-option label="<=" value="<=" />
<el-option label="=" value="=" />
<el-option label=">" value=">" />
<el-option label="<" value="<" />
<el-form-item label="号别/类型:">
<el-select
v-model="quickGeneratorForm.regType"
placeholder="请选择号别"
clearable
style="width: 100%"
>
<el-option
label="专家"
value="专家"
/>
<el-option
label="普通"
value="普通"
/>
<el-option
label="特需"
value="特需"
/>
<el-option
label="急诊"
value="急诊"
/>
</el-select>
<el-input-number
v-model="quickGeneratorForm.ageValue"
:min="0"
:max="150"
:precision="0"
placeholder="年龄"
style="width: 150px;"
</el-form-item>
<el-form-item label="科室:">
<el-select
v-model="quickGeneratorForm.dept"
placeholder="请选择科室"
clearable
style="width: 100%"
>
<el-option
label="心内科"
value="心内科"
/>
<el-option
label="心外科"
value="心外科"
/>
<el-option
label="神经内科"
value="神经内科"
/>
</el-select>
</el-form-item>
<el-form-item label="医生:">
<el-input
v-model="quickGeneratorForm.doctor"
placeholder="请输入医生姓名(支持模糊匹配)"
clearable
/>
</el-form-item>
<el-form-item label="自定义条件:">
<el-input
v-model="quickGeneratorForm.customKey"
placeholder="字段名gender"
style="width: 150px; margin-right: 10px;"
/>
<el-input
v-model="quickGeneratorForm.customValue"
placeholder="字段值(如:男)"
style="width: 200px;"
/>
<el-button
type="text"
@click="quickGeneratorForm.ageOperator = null; quickGeneratorForm.ageValue = null"
@click="quickGeneratorForm.customKey = ''; quickGeneratorForm.customValue = ''"
>
清除
</el-button>
</el-form-item>
<el-form-item label="预览JSON:">
<el-input
:value="previewJson"
type="textarea"
:rows="4"
readonly
style="font-family: monospace;"
/>
</el-form-item>
</el-form>
<template #footer>
<div style="text-align: right;">
<el-button @click="quickGeneratorDialogVisible = false">
取消
</el-button>
<el-button
type="primary"
@click="handleApplyQuickGenerate"
>
应用
</el-button>
</div>
</el-form-item>
<el-form-item label="号别/类型:">
<el-select
v-model="quickGeneratorForm.regType"
placeholder="请选择号别"
clearable
style="width: 100%"
>
<el-option label="专家" value="专家" />
<el-option label="普通" value="普通" />
<el-option label="特需" value="特需" />
<el-option label="急诊" value="急诊" />
</el-select>
</el-form-item>
<el-form-item label="科室:">
<el-select
v-model="quickGeneratorForm.dept"
placeholder="请选择科室"
clearable
style="width: 100%"
>
<el-option label="心内科" value="心内科" />
<el-option label="心外科" value="心外科" />
<el-option label="神经内科" value="神经内科" />
</el-select>
</el-form-item>
<el-form-item label="医生:">
<el-input
v-model="quickGeneratorForm.doctor"
placeholder="请输入医生姓名(支持模糊匹配)"
clearable
/>
</el-form-item>
<el-form-item label="自定义条件:">
<el-input
v-model="quickGeneratorForm.customKey"
placeholder="字段名gender"
style="width: 150px; margin-right: 10px;"
/>
<el-input
v-model="quickGeneratorForm.customValue"
placeholder="字段值(如:男)"
style="width: 200px;"
/>
<el-button
type="text"
@click="quickGeneratorForm.customKey = ''; quickGeneratorForm.customValue = ''"
>
清除
</el-button>
</el-form-item>
<el-form-item label="预览JSON:">
<el-input
:value="previewJson"
type="textarea"
:rows="4"
readonly
style="font-family: monospace;"
/>
</el-form-item>
</el-form>
<template #footer>
<div style="text-align: right;">
<el-button @click="quickGeneratorDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleApplyQuickGenerate">应用</el-button>
</div>
</template>
</el-dialog>
</template>
</el-dialog>
</div>
</template>
@@ -627,29 +867,40 @@ const parseAge = (ageStr) => {
}
// 后端队列状态 -> 前端展示状态
// 后端状态码0=WAITING, 10=CALLING, 20=IN_CLINIC, 30=COMPLETED, 40=SKIPPED, 50=REFUNDED, 60=FOLLOW
const mapBackendStatusToFrontend = (status) => {
if (!status) {
if (status === null || status === undefined) {
console.warn('【心内科】状态映射:收到空状态值')
return '等待'
}
// 转换为大写并去除空格,确保匹配
const normalizedStatus = String(status).trim().toUpperCase()
if (normalizedStatus === 'CALLING') return '叫号中'
if (normalizedStatus === 'WAITING') return '等待'
if (normalizedStatus === 'SKIPPED') return '跳过'
if (normalizedStatus === 'COMPLETED') return '已完成'
console.warn('【心内科】状态映射:未知状态值', status, '-> 默认返回"等待"')
return '等待'
const numStatus = Number(status)
switch (numStatus) {
case 0: return '等待'
case 10: return '叫号中'
case 20: return '诊中'
case 30: return '已完成'
case 40: return '跳过'
case 50: return '已退费'
case 60: return '已随访'
default:
console.warn('【心内科】状态映射:未知状态值', status, '-> 默认返回"等待"')
return '等待'
}
}
// 前端状态 -> 后端状态(目前仅展示用)
// 前端状态 -> 后端状态
const mapFrontendStatusToBackend = (status) => {
if (!status) return 'WAITING'
if (status === '叫号中') return 'CALLING'
if (status === '等待') return 'WAITING'
if (status === '跳过') return 'SKIPPED'
if (status === '已完成') return 'COMPLETED'
return 'WAITING'
if (!status) return 0
switch (status) {
case '叫号中': return 10
case '等待': return 0
case '诊中': return 20
case '已完成': return 30
case '跳过': return 40
case '已退费': return 50
case '已随访': return 60
default: return 0
}
}
// 从数据库加载队列