Compare commits
8 Commits
test
...
03f408cb76
| Author | SHA1 | Date | |
|---|---|---|---|
| 03f408cb76 | |||
| a894f0f8ee | |||
|
|
f87afba566 | ||
| 6fedfe1e40 | |||
|
|
7827e58aac | ||
| 5d280640e8 | |||
| e7413396b2 | |||
|
|
ce64c4519c |
@@ -155,10 +155,16 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
dto.setDepartmentId(raw.getDepartmentId());
|
dto.setDepartmentId(raw.getDepartmentId());
|
||||||
dto.setRealPatientId(raw.getPatientId());
|
dto.setRealPatientId(raw.getPatientId());
|
||||||
|
|
||||||
// 性别处理:直接读取优先级最高的订单性别字段 (SQL 已处理优先级)
|
// 性别处理:直接使用患者表中的 genderEnum
|
||||||
if (raw.getPatientGender() != null) {
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
String pg = raw.getPatientGender().trim();
|
if (genderEnum != null) {
|
||||||
dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知"));
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dto.setGender("未知");
|
dto.setGender("未知");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import com.openhis.common.enums.ybenums.YbPayment;
|
|||||||
import com.openhis.common.utils.EnumUtils;
|
import com.openhis.common.utils.EnumUtils;
|
||||||
import com.openhis.common.utils.HisPageUtils;
|
import com.openhis.common.utils.HisPageUtils;
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
import com.openhis.common.utils.HisQueryUtils;
|
||||||
|
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||||
|
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||||
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
|
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
|
||||||
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||||
import com.openhis.clinical.domain.Order;
|
import com.openhis.clinical.domain.Order;
|
||||||
@@ -52,6 +54,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -111,6 +114,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
@Resource
|
@Resource
|
||||||
SchedulePoolMapper schedulePoolMapper;
|
SchedulePoolMapper schedulePoolMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
com.openhis.document.service.IEmrService iEmrService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 门诊挂号 - 查询患者信息
|
* 门诊挂号 - 查询患者信息
|
||||||
*
|
*
|
||||||
@@ -256,14 +262,24 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> returnRegister(CancelRegPaymentDto cancelRegPaymentDto) {
|
public R<?> returnRegister(CancelRegPaymentDto cancelRegPaymentDto) {
|
||||||
Encounter byId = iEncounterService.getById(cancelRegPaymentDto.getEncounterId());
|
Encounter byId = iEncounterService.getById(cancelRegPaymentDto.getEncounterId());
|
||||||
|
if (byId == null) {
|
||||||
|
return R.fail(null, "就诊记录不存在");
|
||||||
|
}
|
||||||
if (EncounterStatus.CANCELLED.getValue().equals(byId.getStatusEnum())) {
|
if (EncounterStatus.CANCELLED.getValue().equals(byId.getStatusEnum())) {
|
||||||
return R.fail(null, "该患者已经退号,请勿重复退号");
|
return R.fail(null, "该患者已经退号,请勿重复退号");
|
||||||
}
|
}
|
||||||
// 只有待诊状态才能退号
|
// 只有待诊状态才能退号
|
||||||
if (!EncounterStatus.PLANNED.getValue().equals(byId.getStatusEnum())) {
|
if (!EncounterStatus.PLANNED.getValue().equals(byId.getStatusEnum())) {
|
||||||
return R.fail(null, "该患者医生已接诊,不能退号!");
|
return R.fail(null, "该患者已开始就诊,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊前退号检查:病历、费用明细、班段时间
|
||||||
|
R<?> checkResult = checkPreConsultationRefund(byId);
|
||||||
|
if (checkResult != null) {
|
||||||
|
return checkResult;
|
||||||
}
|
}
|
||||||
iEncounterService.returnRegister(cancelRegPaymentDto.getEncounterId());
|
iEncounterService.returnRegister(cancelRegPaymentDto.getEncounterId());
|
||||||
// 查询账户信息
|
// 查询账户信息
|
||||||
@@ -317,6 +333,149 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
|
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊前退号检查
|
||||||
|
* 检查项:病历记录、费用明细、当日就诊、班段结束时间
|
||||||
|
*
|
||||||
|
* @param encounter 就诊记录
|
||||||
|
* @return null 表示通过检查,否则返回失败原因
|
||||||
|
*/
|
||||||
|
private R<?> checkPreConsultationRefund(Encounter encounter) {
|
||||||
|
Long encounterId = encounter.getId();
|
||||||
|
|
||||||
|
// 当日时间范围:今天 00:00:00 到 明天 00:00:00
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDateTime todayStart = today.atStartOfDay();
|
||||||
|
LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay();
|
||||||
|
Date todayStartDate = Date.from(todayStart.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date tomorrowStartDate = Date.from(tomorrowStart.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
|
||||||
|
// 1. 检查是否有当日病历记录(医生已写病历则不能退号)
|
||||||
|
// 只检查当天的病历,避免误判历史数据
|
||||||
|
// 条件:(recordTime在当天范围内) OR (recordTime为空 AND createTime在当天范围内)
|
||||||
|
long emrCount = iEmrService.count(new LambdaQueryWrapper<com.openhis.document.domain.Emr>()
|
||||||
|
.eq(com.openhis.document.domain.Emr::getEncounterId, encounterId)
|
||||||
|
.and(wrapper -> wrapper
|
||||||
|
.and(w -> w
|
||||||
|
.ge(com.openhis.document.domain.Emr::getRecordTime, todayStartDate)
|
||||||
|
.lt(com.openhis.document.domain.Emr::getRecordTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
.or()
|
||||||
|
.and(w -> w
|
||||||
|
.isNull(com.openhis.document.domain.Emr::getRecordTime)
|
||||||
|
.ge(com.openhis.document.domain.Emr::getCreateTime, todayStartDate)
|
||||||
|
.lt(com.openhis.document.domain.Emr::getCreateTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (emrCount > 0) {
|
||||||
|
return R.fail(null, "该患者已有病历记录,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否有当日费用明细(除挂号费外的其他费用)
|
||||||
|
// 只检查当天的费用明细,避免误判历史数据
|
||||||
|
// 条件:(occurrenceTime在当天范围内) OR (occurrenceTime为空 AND createTime在当天范围内)
|
||||||
|
long chargeItemCount = iChargeItemService.count(new LambdaQueryWrapper<ChargeItem>()
|
||||||
|
.eq(ChargeItem::getEncounterId, encounterId)
|
||||||
|
.ne(ChargeItem::getContextEnum, ChargeItemContext.REGISTER.getValue())
|
||||||
|
.ne(ChargeItem::getStatusEnum, ChargeItemStatus.REFUNDED.getValue())
|
||||||
|
.and(wrapper -> wrapper
|
||||||
|
.and(w -> w
|
||||||
|
.ge(ChargeItem::getOccurrenceTime, todayStartDate)
|
||||||
|
.lt(ChargeItem::getOccurrenceTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
.or()
|
||||||
|
.and(w -> w
|
||||||
|
.isNull(ChargeItem::getOccurrenceTime)
|
||||||
|
.ge(ChargeItem::getCreateTime, todayStartDate)
|
||||||
|
.lt(ChargeItem::getCreateTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (chargeItemCount > 0) {
|
||||||
|
return R.fail(null, "该患者已产生诊疗费用,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否当日就诊(防止隔日财务封账)
|
||||||
|
if (encounter.getCreateTime() != null) {
|
||||||
|
LocalDate encounterDate = encounter.getCreateTime().toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
if (encounterDate.isBefore(today)) {
|
||||||
|
return R.fail(null, "非当日就诊记录,不能退号!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查班段是否已结束(通过预约订单获取班段信息)
|
||||||
|
R<?> shiftCheckResult = checkShiftEnded(encounter);
|
||||||
|
if (shiftCheckResult != null) {
|
||||||
|
return shiftCheckResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 检查通过
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查班段是否已结束
|
||||||
|
* 截止时间 = 班段结束时间
|
||||||
|
*
|
||||||
|
* @param encounter 就诊记录
|
||||||
|
* @return null 表示通过检查,否则返回失败原因
|
||||||
|
*/
|
||||||
|
private R<?> checkShiftEnded(Encounter encounter) {
|
||||||
|
try {
|
||||||
|
// 通过患者、科室、日期查找关联的预约订单
|
||||||
|
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
|
||||||
|
.eq(Order::getPatientId, encounter.getPatientId())
|
||||||
|
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
|
||||||
|
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
|
||||||
|
.orderByDesc(Order::getUpdateTime)
|
||||||
|
.orderByDesc(Order::getCreateTime)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
if (encounter.getOrganizationId() != null) {
|
||||||
|
queryWrapper.eq(Order::getDepartmentId, encounter.getOrganizationId());
|
||||||
|
}
|
||||||
|
if (encounter.getTenantId() != null) {
|
||||||
|
queryWrapper.eq(Order::getTenantId, encounter.getTenantId());
|
||||||
|
}
|
||||||
|
if (encounter.getCreateTime() != null) {
|
||||||
|
LocalDate encounterDate = encounter.getCreateTime().toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
Date startOfDay = Date.from(encounterDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date nextDayStart = Date.from(encounterDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
queryWrapper.ge(Order::getAppointmentDate, startOfDay)
|
||||||
|
.lt(Order::getAppointmentDate, nextDayStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
Order appointmentOrder = orderService.getOne(queryWrapper, false);
|
||||||
|
if (appointmentOrder == null || appointmentOrder.getSlotId() == null) {
|
||||||
|
// 没有关联的预约订单,跳过班段检查(非预约挂号的场景)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取号源槽位
|
||||||
|
ScheduleSlot slot = scheduleSlotMapper.selectById(appointmentOrder.getSlotId());
|
||||||
|
if (slot == null || slot.getPoolId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取号源池(班段信息)
|
||||||
|
SchedulePool pool = schedulePoolMapper.selectById(slot.getPoolId());
|
||||||
|
if (pool == null || pool.getEndTime() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 检查当前时间是否已过班段结束时间
|
||||||
|
LocalTime now = LocalTime.now();
|
||||||
|
if (now.isAfter(pool.getEndTime())) {
|
||||||
|
return R.fail(null, "当前班段已结束,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("检查班段结束时间失败, encounterId={}", encounter.getId(), e);
|
||||||
|
// 异常情况下允许退号,避免阻断正常业务
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询当日就诊数据
|
* 查询当日就诊数据
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ public class DiagnosisTreatmentDto {
|
|||||||
/** 费用套餐名称(JOIN inspection_basic_information.package_name) */
|
/** 费用套餐名称(JOIN inspection_basic_information.package_name) */
|
||||||
private String packageName;
|
private String packageName;
|
||||||
|
|
||||||
|
/** 套餐金额(JOIN inspection_basic_information.package_amount) */
|
||||||
|
private BigDecimal packageAmount;
|
||||||
|
|
||||||
|
/** 套餐服务费(JOIN inspection_basic_information.service_fee) */
|
||||||
|
private BigDecimal serviceFee;
|
||||||
|
|
||||||
/** 下级医技类型ID(关联 inspection_type 子类) */
|
/** 下级医技类型ID(关联 inspection_type 子类) */
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long subItemId;
|
private Long subItemId;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
T1.sub_item_id,
|
T1.sub_item_id,
|
||||||
T3.name AS test_type,
|
T3.name AS test_type,
|
||||||
T5.package_name,
|
T5.package_name,
|
||||||
|
T5.package_amount,
|
||||||
|
T5.service_fee,
|
||||||
T6.name AS sub_item_name
|
T6.name AS sub_item_name
|
||||||
FROM lab_activity_definition T1
|
FROM lab_activity_definition T1
|
||||||
/* 检验类型关联(逻辑关联,无外键) */
|
/* 检验类型关联(逻辑关联,无外键) */
|
||||||
@@ -97,6 +99,8 @@
|
|||||||
T1.sub_item_id,
|
T1.sub_item_id,
|
||||||
T3.name AS test_type,
|
T3.name AS test_type,
|
||||||
T5.package_name,
|
T5.package_name,
|
||||||
|
T5.package_amount,
|
||||||
|
T5.service_fee,
|
||||||
T6.name AS sub_item_name
|
T6.name AS sub_item_name
|
||||||
FROM lab_activity_definition T1
|
FROM lab_activity_definition T1
|
||||||
LEFT JOIN inspection_type T3
|
LEFT JOIN inspection_type T3
|
||||||
|
|||||||
@@ -41,11 +41,18 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 分页查询检验申请单列表(根据就诊ID查询,按申请时间降序)
|
<!-- 分页查询检验申请单列表(根据就诊ID查询,按申请时间降序)
|
||||||
直接查询申请单表,不关联明细表,避免重复记录-->
|
从明细表聚合项目名称和金额-->
|
||||||
<select id="getInspectionApplyListPage" resultType="com.openhis.web.doctorstation.dto.DoctorStationLabApplyDto">
|
<select id="getInspectionApplyListPage" resultType="com.openhis.web.doctorstation.dto.DoctorStationLabApplyDto">
|
||||||
SELECT t1.id AS applicationId,
|
SELECT t1.id AS applicationId,
|
||||||
t1.apply_no AS applyNo,
|
t1.apply_no AS applyNo,
|
||||||
t1.inspection_item AS itemName,
|
(SELECT STRING_AGG(t2.item_name, '+')
|
||||||
|
FROM lab_apply_item t2
|
||||||
|
WHERE t2.apply_no = t1.apply_no AND t2.delete_flag = '0'
|
||||||
|
) AS itemName,
|
||||||
|
(SELECT SUM(t2.item_amount)
|
||||||
|
FROM lab_apply_item t2
|
||||||
|
WHERE t2.apply_no = t1.apply_no AND t2.delete_flag = '0'
|
||||||
|
) AS itemAmount,
|
||||||
t1.apply_doc_name AS applyDocName,
|
t1.apply_doc_name AS applyDocName,
|
||||||
t1.priority_code AS priorityCode,
|
t1.priority_code AS priorityCode,
|
||||||
t1.apply_status AS applyStatus,
|
t1.apply_status AS applyStatus,
|
||||||
|
|||||||
@@ -44,10 +44,12 @@ public interface OrderMapper extends BaseMapper<Order> {
|
|||||||
int updatePayStatus(@Param("orderId") Long orderId, @Param("payStatus") Integer payStatus, @Param("payTime") Date payTime);
|
int updatePayStatus(@Param("orderId") Long orderId, @Param("payStatus") Integer payStatus, @Param("payTime") Date payTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计同一患者在同一科室、同一时段(上午/下午)内的有效预约订单数量
|
* 统计同一患者在同一科室、同一自然日(预约日 00:00~次日 00:00)内的有效预约订单数量。
|
||||||
|
* 匹配规则:优先 {@code department_id}(对应 adm_organization.id);仅当 ID 为空时用 {@code department_name} 兜底。
|
||||||
*
|
*
|
||||||
* @param patientId 患者ID
|
* @param patientId 患者ID
|
||||||
* @param departmentId 科室ID
|
* @param departmentId 科室 ID(order_main.department_id)
|
||||||
|
* @param departmentName 科室名称(ID 为空时与 order_main.department_name 比对)
|
||||||
* @param startTime 时段起始时间(含)
|
* @param startTime 时段起始时间(含)
|
||||||
* @param endTime 时段结束时间(不含)
|
* @param endTime 时段结束时间(不含)
|
||||||
* @param statuses 订单状态集合(如 1=已预约,2=已取号)
|
* @param statuses 订单状态集合(如 1=已预约,2=已取号)
|
||||||
@@ -55,6 +57,7 @@ public interface OrderMapper extends BaseMapper<Order> {
|
|||||||
*/
|
*/
|
||||||
int countPatientDeptOrdersInPeriod(@Param("patientId") Long patientId,
|
int countPatientDeptOrdersInPeriod(@Param("patientId") Long patientId,
|
||||||
@Param("departmentId") Long departmentId,
|
@Param("departmentId") Long departmentId,
|
||||||
|
@Param("departmentName") String departmentName,
|
||||||
@Param("startTime") Date startTime,
|
@Param("startTime") Date startTime,
|
||||||
@Param("endTime") Date endTime,
|
@Param("endTime") Date endTime,
|
||||||
@Param("statuses") List<Integer> statuses);
|
@Param("statuses") List<Integer> statuses);
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
|
|||||||
long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime);
|
long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime);
|
||||||
if (cancelledCount >= config.getCancelAppointmentCount()) {
|
if (cancelledCount >= config.getCancelAppointmentCount()) {
|
||||||
String periodName = getPeriodName(config.getCancelAppointmentType());
|
String periodName = getPeriodName(config.getCancelAppointmentType());
|
||||||
throw new RuntimeException("由于您在" + periodName + "内累计取消预约已达" + cancelledCount + "次,触发系统限制,暂时无法在线预约,请联系分诊台或咨询客服。");
|
int limitCount = config.getCancelAppointmentCount();
|
||||||
|
throw new RuntimeException("由于您在" + periodName + "内累计取消预约已达" + limitCount + "次,触发系统限制,暂时无法在线预约,请联系分诊台或咨询客服。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,25 +184,23 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
|
|||||||
throw new RuntimeException("该排班医生已停诊");
|
throw new RuntimeException("该排班医生已停诊");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.1 同一患者同一天/同一科室/同一时段(上午/下午)不可重复预约
|
// 2.1 同一患者同一天/同一科室不可重复预约(自然日 00:00~次日 00:00,上午+下午共限 1 次;科室以 department_id 为准,无 ID 时用科室名兜底)
|
||||||
if (dto.getPatientId() != null && slot.getDepartmentId() != null && slot.getScheduleDate() != null && slot.getExpectTime() != null) {
|
if (dto.getPatientId() != null && slot.getScheduleDate() != null
|
||||||
boolean isMorning = slot.getExpectTime().isBefore(LocalTime.NOON);
|
&& (slot.getDepartmentId() != null
|
||||||
|
|| (slot.getDepartmentName() != null && !slot.getDepartmentName().isBlank()))) {
|
||||||
LocalDate scheduleDateForCheck = slot.getScheduleDate();
|
LocalDate scheduleDateForCheck = slot.getScheduleDate();
|
||||||
LocalDateTime periodStart = isMorning
|
LocalDateTime periodStart = LocalDateTime.of(scheduleDateForCheck, LocalTime.MIN);
|
||||||
? LocalDateTime.of(scheduleDateForCheck, LocalTime.MIN)
|
LocalDateTime periodEnd = LocalDateTime.of(scheduleDateForCheck.plusDays(1), LocalTime.MIN);
|
||||||
: LocalDateTime.of(scheduleDateForCheck, LocalTime.NOON);
|
|
||||||
LocalDateTime periodEnd = isMorning
|
|
||||||
? LocalDateTime.of(scheduleDateForCheck, LocalTime.NOON)
|
|
||||||
: LocalDateTime.of(scheduleDateForCheck.plusDays(1), LocalTime.MIN);
|
|
||||||
|
|
||||||
Date startTime = Date.from(periodStart.atZone(ZoneId.systemDefault()).toInstant());
|
Date startTime = Date.from(periodStart.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
Date endTime = Date.from(periodEnd.atZone(ZoneId.systemDefault()).toInstant());
|
Date endTime = Date.from(periodEnd.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
|
||||||
// 预约去重以订单为准(order_main),因为预约成功会先落订单;clinical_ticket 不一定在此链路写入
|
// 预约去重以订单为准(order_main),因为预约成功会先落订单;clinical_ticket 不一定在此链路写入
|
||||||
List<Integer> effectiveOrderStatuses = Arrays.asList(AppointmentOrderStatus.BOOKED, AppointmentOrderStatus.CHECKED_IN);
|
List<Integer> effectiveOrderStatuses = Arrays.asList(AppointmentOrderStatus.BOOKED, AppointmentOrderStatus.CHECKED_IN);
|
||||||
int exists = orderMapper.countPatientDeptOrdersInPeriod(dto.getPatientId(), slot.getDepartmentId(), startTime, endTime, effectiveOrderStatuses);
|
int exists = orderMapper.countPatientDeptOrdersInPeriod(dto.getPatientId(), slot.getDepartmentId(), slot.getDepartmentName(),
|
||||||
|
startTime, endTime, effectiveOrderStatuses);
|
||||||
if (exists > 0) {
|
if (exists > 0) {
|
||||||
throw new RuntimeException("该患者已在当前科室该时段存在预约记录,不可重复预约");
|
throw new RuntimeException("该患者已在当前科室当日存在预约记录,不可重复预约");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,7 +222,17 @@
|
|||||||
from order_main
|
from order_main
|
||||||
<where>
|
<where>
|
||||||
and patient_id = #{patientId}
|
and patient_id = #{patientId}
|
||||||
and department_id = #{departmentId}
|
<choose>
|
||||||
|
<when test="departmentId != null">
|
||||||
|
and department_id = #{departmentId}
|
||||||
|
</when>
|
||||||
|
<when test="departmentName != null and departmentName != ''">
|
||||||
|
and trim(department_name) = trim(#{departmentName})
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
and 1 = 0
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
and appointment_time >= #{startTime}
|
and appointment_time >= #{startTime}
|
||||||
and appointment_time < #{endTime}
|
and appointment_time < #{endTime}
|
||||||
<if test="statuses != null and statuses.size() > 0">
|
<if test="statuses != null and statuses.size() > 0">
|
||||||
|
|||||||
@@ -196,7 +196,6 @@
|
|||||||
<td>{{ index + 1 }}</td>
|
<td>{{ index + 1 }}</td>
|
||||||
<td>{{ patient.name }}</td>
|
<td>{{ patient.name }}</td>
|
||||||
<td>{{ patient.identifierNo || patient.medicalCard || patient.id }}</td>
|
<td>{{ patient.identifierNo || patient.medicalCard || patient.id }}</td>
|
||||||
<td>{{ patient.identifierNo }}</td>
|
|
||||||
<td>{{ getGenderText(patient.genderEnum_enumText || patient.genderEnum || patient.gender || patient.sex) }}</td>
|
<td>{{ getGenderText(patient.genderEnum_enumText || patient.genderEnum || patient.gender || patient.sex) }}</td>
|
||||||
<td>{{ patient.idCard }}</td>
|
<td>{{ patient.idCard }}</td>
|
||||||
<td>{{ patient.phone }}</td>
|
<td>{{ patient.phone }}</td>
|
||||||
@@ -313,37 +312,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// 全部号源经过日期过期过滤后的数据(不按医生过滤,不按患者搜索过滤),用于统计医生余号
|
|
||||||
allTicketsForDoctorCount() {
|
|
||||||
let filtered = [...this.tickets];
|
|
||||||
|
|
||||||
// 🎯 只过滤过期号源,不按医生过滤,不按患者搜索过滤
|
|
||||||
// 这样余号统计总是基于该日期下所有号源,得到正确的每个医生余号
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
filtered = filtered.filter(ticket => {
|
|
||||||
// dateTime 格式示例:"2024-01-01 08:00-09:00"
|
|
||||||
const parts = (ticket.dateTime || '').split(' ');
|
|
||||||
if (parts.length < 2) return true; // 如果格式不正确,保留显示
|
|
||||||
|
|
||||||
const dateStr = parts[0];
|
|
||||||
const timeRangeStr = parts[1];
|
|
||||||
if (!dateStr || !timeRangeStr) return true;
|
|
||||||
|
|
||||||
// 提取开始时间
|
|
||||||
const startTimeStr = timeRangeStr.split('-')[0]; // "08:00"
|
|
||||||
if (!startTimeStr) return true;
|
|
||||||
|
|
||||||
// 构建号源开始时间的完整 Date 对象
|
|
||||||
const ticketStartStr = `${dateStr} ${startTimeStr}`;
|
|
||||||
const ticketStart = new Date(ticketStartStr);
|
|
||||||
|
|
||||||
// 只显示开始时间晚于当前时间的号源
|
|
||||||
return ticketStart > now;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
},
|
|
||||||
filteredDoctors() {
|
filteredDoctors() {
|
||||||
let filtered = [...this.doctors];
|
let filtered = [...this.doctors];
|
||||||
|
|
||||||
@@ -361,96 +329,10 @@ export default {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 实时更新余号数量:统计该医生当前筛选条件下剩余可预约(未预约 + 未过期)号源数量
|
|
||||||
// 使用全部未过期号源统计(不按选中医生过滤),这样所有医生余号都正确显示
|
|
||||||
const availableCountMap = {};
|
|
||||||
this.allTicketsForDoctorCount.forEach(ticket => {
|
|
||||||
const doctorId = String(ticket.doctorId || ticket.doctor_id);
|
|
||||||
if (!availableCountMap[doctorId]) {
|
|
||||||
availableCountMap[doctorId] = 0;
|
|
||||||
}
|
|
||||||
// 只有未预约的号源才算作可预约余号
|
|
||||||
if (ticket.status === '未预约') {
|
|
||||||
availableCountMap[doctorId]++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新每个医生的余号数量
|
|
||||||
filtered = filtered.map(doctor => {
|
|
||||||
const actualAvailable = availableCountMap[String(doctor.id)] || 0;
|
|
||||||
return {
|
|
||||||
...doctor,
|
|
||||||
available: actualAvailable
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
},
|
},
|
||||||
// 过滤并排序后的完整号源列表(用于右侧显示)
|
|
||||||
filteredAndSortedTickets() {
|
|
||||||
// 从已经过滤掉过期的全部数据开始
|
|
||||||
let filtered = [...this.allTicketsForDoctorCount];
|
|
||||||
|
|
||||||
// 🎯 根据选中的医生过滤(右侧只显示选中医生的号源)
|
|
||||||
if (this.selectedDoctorId) {
|
|
||||||
const doctorIdStr = String(this.selectedDoctorId);
|
|
||||||
filtered = filtered.filter(ticket => {
|
|
||||||
const ticketDoctorId = String(ticket.doctorId || ticket.doctor_id || '');
|
|
||||||
return ticketDoctorId === doctorIdStr;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎯 根据患者搜索条件过滤
|
|
||||||
if (this.patientName?.trim()) {
|
|
||||||
const keyword = this.patientName.trim().toLowerCase();
|
|
||||||
filtered = filtered.filter(ticket =>
|
|
||||||
(ticket.patientName || '').toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.patientCard?.trim()) {
|
|
||||||
const keyword = this.patientCard.trim().toLowerCase();
|
|
||||||
filtered = filtered.filter(ticket =>
|
|
||||||
(ticket.patientId || '').toLowerCase().includes(keyword) ||
|
|
||||||
(ticket.medicalCard || '').toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.patientPhone?.trim()) {
|
|
||||||
const keyword = this.patientPhone.trim().toLowerCase();
|
|
||||||
filtered = filtered.filter(ticket =>
|
|
||||||
(ticket.phone || '').toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎯 按开始时间升序排序 → 较早的号源排在前面
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
const getStartTime = (ticket) => {
|
|
||||||
const parts = (ticket.dateTime || '').split(' ');
|
|
||||||
if (parts.length < 2) return new Date(0).getTime();
|
|
||||||
const dateStr = parts[0];
|
|
||||||
const timeRangeStr = parts[1];
|
|
||||||
const startTimeStr = (timeRangeStr || '').split('-')[0];
|
|
||||||
if (!startTimeStr) return new Date(0).getTime();
|
|
||||||
const ticketStartStr = `${dateStr} ${startTimeStr}`;
|
|
||||||
return new Date(ticketStartStr).getTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeA = getStartTime(a);
|
|
||||||
const timeB = getStartTime(b);
|
|
||||||
return timeA - timeB;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
},
|
|
||||||
// 🎯 分页:按照用户选择的每页条数分页,返回当前页的数据
|
|
||||||
filteredTickets() {
|
filteredTickets() {
|
||||||
const filtered = this.filteredAndSortedTickets;
|
return [...this.tickets];
|
||||||
const startIndex = (this.currentPage - 1) * this.pageSize;
|
|
||||||
const endIndex = startIndex + this.pageSize;
|
|
||||||
return filtered.slice(startIndex, endIndex);
|
|
||||||
},
|
|
||||||
// 更新总条数为过滤后的实际条数,分页自动处理
|
|
||||||
totalTickets() {
|
|
||||||
return this.filteredAndSortedTickets.length;
|
|
||||||
},
|
},
|
||||||
hasSearchCriteria() {
|
hasSearchCriteria() {
|
||||||
return !!this.patientKeyword?.trim();
|
return !!this.patientKeyword?.trim();
|
||||||
@@ -460,8 +342,6 @@ export default {
|
|||||||
selectDoctor(doctorId) {
|
selectDoctor(doctorId) {
|
||||||
this.selectedDoctorId = this.selectedDoctorId === doctorId ? null : doctorId;
|
this.selectedDoctorId = this.selectedDoctorId === doctorId ? null : doctorId;
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
// 🔧 BugFix: 选择医生后不改变医生列表,余号计算基于 filteredAndSortedTickets 已经正确过滤
|
|
||||||
// 只需要重新获取号源,医生列表保持不变,余号计算会自动正确
|
|
||||||
this.fetchTickets({ refreshDepartments: false, refreshDoctors: false }).catch(() => {});
|
this.fetchTickets({ refreshDepartments: false, refreshDoctors: false }).catch(() => {});
|
||||||
},
|
},
|
||||||
onTypeChange() {
|
onTypeChange() {
|
||||||
@@ -794,7 +674,7 @@ export default {
|
|||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 检测是否为移动设备
|
// 检测是否为移动设备。
|
||||||
checkMobileDevice() {
|
checkMobileDevice() {
|
||||||
this.isMobile = window.innerWidth <= 768;
|
this.isMobile = window.innerWidth <= 768;
|
||||||
},
|
},
|
||||||
@@ -802,20 +682,21 @@ export default {
|
|||||||
return STATUS_CLASS_MAP[status] || 'status-unbooked';
|
return STATUS_CLASS_MAP[status] || 'status-unbooked';
|
||||||
},
|
},
|
||||||
buildQueryParams(page = this.currentPage) {
|
buildQueryParams(page = this.currentPage) {
|
||||||
|
const doctorId =
|
||||||
|
this.selectedDoctorId === null || this.selectedDoctorId === undefined || this.selectedDoctorId === ''
|
||||||
|
? null
|
||||||
|
: String(this.selectedDoctorId);
|
||||||
return {
|
return {
|
||||||
date: this.selectedDate,
|
date: this.selectedDate,
|
||||||
status: null, // 状态过滤在前端做
|
status: this.selectedStatus === 'all' ? null : this.selectedStatus,
|
||||||
type: this.selectedType === 'all' ? null : this.selectedType,
|
type: this.selectedType === 'all' ? null : this.selectedType,
|
||||||
department: this.selectedDepartment === 'all' ? null : this.selectedDepartment,
|
department: this.selectedDepartment === 'all' ? null : this.selectedDepartment,
|
||||||
doctorId: null, // 🎯 关键:永远不传 doctorId 给后端,后端返回全量数据
|
doctorId,
|
||||||
// 医生过滤、状态过滤、患者搜索都在前端做,才能保证所有医生余号统计正确
|
name: this.patientName?.trim() || null,
|
||||||
name: null,
|
card: this.patientCard?.trim() || null,
|
||||||
card: null,
|
phone: this.patientPhone?.trim() || null,
|
||||||
phone: null,
|
page,
|
||||||
// 🎯 获取全量数据到前端,由前端做过滤和分页,保证余号统计总是正确
|
limit: this.pageSize
|
||||||
// 号源数量每个日期每个科室不会太多,全量获取可行
|
|
||||||
page: 1,
|
|
||||||
limit: 10000
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
buildDoctorQueryParams() {
|
buildDoctorQueryParams() {
|
||||||
@@ -843,14 +724,20 @@ export default {
|
|||||||
if (!payload) {
|
if (!payload) {
|
||||||
this.tickets = [];
|
this.tickets = [];
|
||||||
this.allTickets = [];
|
this.allTickets = [];
|
||||||
|
this.totalTickets = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let records = payload.list || payload.records || [];
|
const records = payload.list || payload.records || [];
|
||||||
// 获取全量数据,应用状态过滤后保存所有数据到 tickets
|
const filteredRecords = this.applyStatusFilter(records);
|
||||||
// 过滤、余号统计、分页都由前端完成,保证余号计算正确
|
const total = Number(payload.total);
|
||||||
records = this.applyStatusFilter(records);
|
this.tickets = [...filteredRecords];
|
||||||
this.tickets = [...records];
|
this.allTickets = [...filteredRecords];
|
||||||
this.allTickets = [...records];
|
// 当按状态筛选时,优先使用前端过滤后的数量,避免后端状态未生效导致“显示全部”
|
||||||
|
if (this.selectedStatus && this.selectedStatus !== 'all') {
|
||||||
|
this.totalTickets = this.tickets.length;
|
||||||
|
} else {
|
||||||
|
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
applyStatusFilter(records = []) {
|
applyStatusFilter(records = []) {
|
||||||
if (!Array.isArray(records) || records.length === 0) {
|
if (!Array.isArray(records) || records.length === 0) {
|
||||||
@@ -1005,8 +892,8 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 颜色变量定义 */
|
/* 颜色变量定义 - 使用组件内CSS变量 */
|
||||||
:root {
|
.ticket-management-container {
|
||||||
--primary-color: #1890FF;
|
--primary-color: #1890FF;
|
||||||
--secondary-color: #FF6B35;
|
--secondary-color: #FF6B35;
|
||||||
--status-unbooked: #5A8DEE;
|
--status-unbooked: #5A8DEE;
|
||||||
@@ -1080,7 +967,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
width: 100%;
|
width: 160px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索控件样式 */
|
/* 搜索控件样式 */
|
||||||
@@ -1151,12 +1039,6 @@ export default {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 日期选择器样式 */
|
|
||||||
.date-picker {
|
|
||||||
width: 160px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态筛选器样式 */
|
/* 状态筛选器样式 */
|
||||||
.status-filter {
|
.status-filter {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -1242,50 +1124,50 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 顶部搜索区域改为纵向排列 */
|
/* 顶部搜索区域改为纵向排列 */
|
||||||
.top-search-area {
|
.top-search-area {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 汉堡菜单显示 */
|
/* 汉堡菜单显示 */
|
||||||
.hamburger-menu {
|
.hamburger-menu {
|
||||||
display: block;
|
display: block;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索区域元素宽度100% */
|
/* 搜索区域元素宽度100% */
|
||||||
.date-picker,
|
.date-picker,
|
||||||
.status-filter,
|
.status-filter,
|
||||||
.patient-search,
|
.patient-search,
|
||||||
.card-search,
|
.card-search,
|
||||||
.phone-search,
|
.phone-search,
|
||||||
.search-button,
|
.search-button,
|
||||||
.add-patient {
|
.add-patient {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右侧内容区卡片布局调整 */
|
/* 右侧内容区卡片布局调整 */
|
||||||
.virtual-list {
|
.virtual-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧边栏默认隐藏 */
|
/* 左侧边栏默认隐藏 */
|
||||||
.left-sidebar {
|
.left-sidebar {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右侧内容区占满屏幕 */
|
/* 右侧内容区占满屏幕 */
|
||||||
.right-content {
|
.right-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 确保卡片样式正确应用 */
|
/* 确保卡片样式正确应用 */
|
||||||
.ticket-card {
|
.ticket-card {
|
||||||
@@ -1711,9 +1593,28 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -1781,32 +1682,6 @@ export default {
|
|||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.patient-search-toolbar {
|
.patient-search-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="inspection-application-container">
|
<el-container class="inspection-application-container">
|
||||||
|
|
||||||
<!-- 顶部操作按钮区 - Bug#334: 优化垂直空间利用率 -->
|
<!-- 占位 header,保持 el-container 布局结构 -->
|
||||||
<el-header class="top-action-bar" height="48px">
|
<el-header height="0" />
|
||||||
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
|
|
||||||
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
|
|
||||||
<el-icon><Document /></el-icon>
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
新增
|
|
||||||
</el-button>
|
|
||||||
</el-row>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<!-- 检验信息表格区 -->
|
<!-- 检验信息表格区 -->
|
||||||
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
||||||
<el-card class="table-card" style="width: 100%">
|
<el-card class="table-card" style="width: 100%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-row class="card-header" type="flex" align="middle">
|
<div class="table-card-header-bar">
|
||||||
<el-icon><DocumentChecked /></el-icon>
|
<span class="table-card-title"><el-icon><DocumentChecked /></el-icon> 检验信息</span>
|
||||||
<span>检验信息</span>
|
<span class="table-card-btns">
|
||||||
</el-row>
|
<el-button type="primary" size="default" @click="handleSave" :loading="saving">
|
||||||
|
<el-icon><Document /></el-icon> 保存
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="default" @click="handleNewApplication">
|
||||||
|
<el-icon><Plus /></el-icon> 新增
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
ref="inspectionTableRef"
|
ref="inspectionTableRef"
|
||||||
@@ -512,28 +508,58 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已选项目列表 -->
|
<!-- 已选项目列表 - 支持树形展开 -->
|
||||||
<el-scrollbar class="selected-tree" style="max-height: 220px">
|
<el-scrollbar class="selected-tree" style="max-height: 220px">
|
||||||
<el-list v-if="selectedInspectionItems.length > 0" :data="selectedInspectionItems" class="selected-items-list">
|
<div v-if="selectedInspectionItems.length > 0" class="selected-items-list">
|
||||||
<el-list-item
|
<div
|
||||||
v-for="item in selectedInspectionItems"
|
v-for="item in selectedInspectionItems"
|
||||||
:key="item.itemId"
|
:key="item.itemId"
|
||||||
class="selected-list-item"
|
class="selected-item-wrapper"
|
||||||
>
|
>
|
||||||
<el-row class="selected-item-content" type="flex" align="middle" style="width: 100%">
|
<!-- 主项目行 -->
|
||||||
|
<div
|
||||||
|
:class="['selected-item-content', { 'is-package': item.feePackageId }]"
|
||||||
|
@click="item.feePackageId && togglePackageExpand(item)"
|
||||||
|
>
|
||||||
|
<!-- 套餐展开图标 -->
|
||||||
|
<el-icon
|
||||||
|
v-if="item.feePackageId"
|
||||||
|
:class="['expand-icon', { 'is-expanded': item.expanded }]"
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
</el-icon>
|
||||||
<span class="item-itemName">{{ item.itemName }}</span>
|
<span class="item-itemName">{{ item.itemName }}</span>
|
||||||
<span class="item-price">¥{{ item.itemPrice }}</span>
|
<span class="item-price">¥{{ item.itemPrice }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
style="color: #f56c6c; margin-left: auto"
|
style="color: #f56c6c; margin-left: auto"
|
||||||
@click="removeInspectionItem(item)"
|
@click.stop="removeInspectionItem(item)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-row>
|
</div>
|
||||||
</el-list-item>
|
|
||||||
</el-list>
|
<!-- 套餐明细项(树形展开) -->
|
||||||
|
<div v-if="item.feePackageId && item.expanded" class="package-details">
|
||||||
|
<div v-if="item.loadingDetails" class="loading-details">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.details && item.details.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="detail in item.details"
|
||||||
|
:key="detail.detailId"
|
||||||
|
class="detail-item"
|
||||||
|
>
|
||||||
|
<span class="detail-name">{{ detail.itemName }}</span>
|
||||||
|
<span class="detail-price">¥{{ detail.unitPrice || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-details">暂无明细</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-empty v-if="selectedInspectionItems.length === 0" class="no-selection" description="暂无选择项目" />
|
<el-empty v-if="selectedInspectionItems.length === 0" class="no-selection" description="暂无选择项目" />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -546,15 +572,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
|
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
deleteInspectionApplication, getApplyList,
|
deleteInspectionApplication, getApplyList,
|
||||||
saveInspectionApplication,
|
saveInspectionApplication,
|
||||||
getInspectionTypeList,
|
getInspectionTypeList,
|
||||||
getInspectionItemList,
|
|
||||||
getEncounterDiagnosis,
|
getEncounterDiagnosis,
|
||||||
getInspectionApplyDetail
|
getInspectionApplyDetail
|
||||||
} from '../api'
|
} from '../api'
|
||||||
|
import { getLabActivityDefinitionPage } from '@/api/lab/labActivityDefinition'
|
||||||
|
import { listInspectionPackageDetails } from '@/api/system/inspectionPackage'
|
||||||
import useUserStore from '@/store/modules/user.js'
|
import useUserStore from '@/store/modules/user.js'
|
||||||
// 迁移到 hiprint
|
// 迁移到 hiprint
|
||||||
import { previewPrint } from '@/utils/printUtils.js'
|
import { previewPrint } from '@/utils/printUtils.js'
|
||||||
@@ -806,7 +833,7 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
|
|||||||
params.inspectionTypeId = category.typeId
|
params.inspectionTypeId = category.typeId
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getInspectionItemList(params)
|
const res = await getLabActivityDefinitionPage(params)
|
||||||
|
|
||||||
// 解析数据
|
// 解析数据
|
||||||
let records = []
|
let records = []
|
||||||
@@ -822,22 +849,31 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
|
|||||||
total = records.length
|
total = records.length
|
||||||
}
|
}
|
||||||
|
|
||||||
// 映射数据格式
|
// 映射数据格式,计算套餐价格
|
||||||
const mappedItems = records.map(item => ({
|
const mappedItems = records.map(item => {
|
||||||
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
|
// 计算价格:套餐项目使用 packageAmount + serviceFee,否则使用 retailPrice
|
||||||
itemName: item.name || item.itemName || '',
|
let itemPrice = item.retailPrice || 0
|
||||||
itemPrice: item.retailPrice || item.price || 0,
|
if (item.feePackageId && item.packageAmount) {
|
||||||
itemAmount: item.retailPrice || item.price || 0,
|
itemPrice = (Number(item.packageAmount) || 0) + (Number(item.serviceFee) || 0)
|
||||||
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
|
}
|
||||||
unit: item.unit || '',
|
|
||||||
itemQty: 1,
|
return {
|
||||||
serviceFee: 0,
|
itemId: item.id || Math.random().toString(36).substring(2, 11),
|
||||||
type: category.label,
|
itemName: item.name || '',
|
||||||
isSelfPay: false,
|
itemPrice: itemPrice,
|
||||||
activityId: item.activityId,
|
itemAmount: itemPrice,
|
||||||
code: item.busNo || item.code || item.activityCode,
|
sampleType: item.specimenCode_dictText || '血液',
|
||||||
inspectionTypeId: item.inspectionTypeId || null
|
unit: item.permittedUnitCode || '',
|
||||||
}))
|
itemQty: 1,
|
||||||
|
serviceFee: item.serviceFee || 0,
|
||||||
|
type: category.label,
|
||||||
|
isSelfPay: false,
|
||||||
|
code: item.busNo || '',
|
||||||
|
inspectionTypeId: item.inspectionTypeId || null,
|
||||||
|
feePackageId: item.feePackageId || null,
|
||||||
|
packageName: item.packageName || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 更新分类数据
|
// 更新分类数据
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
@@ -936,21 +972,28 @@ const querySearchInspectionItems = async (queryString, cb) => {
|
|||||||
searchKey: queryString
|
searchKey: queryString
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getInspectionItemList(params)
|
const res = await getLabActivityDefinitionPage(params)
|
||||||
|
|
||||||
let suggestions = []
|
let suggestions = []
|
||||||
if (res.data && res.data.records) {
|
if (res.data && res.data.records) {
|
||||||
// 映射数据格式,与 loadInspectionItemsByType 保持一致
|
// 映射数据格式,与 loadInspectionItemsByType 保持一致
|
||||||
suggestions = res.data.records.map(item => ({
|
suggestions = res.data.records.map(item => {
|
||||||
itemId: item.id || item.activityId,
|
// 计算价格
|
||||||
itemName: item.name || item.itemName || '',
|
let itemPrice = item.retailPrice || 0
|
||||||
itemPrice: item.retailPrice || item.price || 0,
|
if (item.feePackageId && item.packageAmount) {
|
||||||
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
|
itemPrice = (Number(item.packageAmount) || 0) + (Number(item.serviceFee) || 0)
|
||||||
unit: item.unit || '',
|
}
|
||||||
code: item.busNo || item.code || item.activityCode,
|
return {
|
||||||
activityId: item.activityId,
|
itemId: item.id,
|
||||||
inspectionTypeId: item.inspectionTypeId || null
|
itemName: item.name || '',
|
||||||
}))
|
itemPrice: itemPrice,
|
||||||
|
sampleType: item.specimenCode_dictText || '血液',
|
||||||
|
unit: item.permittedUnitCode || '',
|
||||||
|
code: item.busNo || '',
|
||||||
|
inspectionTypeId: item.inspectionTypeId || null,
|
||||||
|
feePackageId: item.feePackageId || null
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(suggestions)
|
cb(suggestions)
|
||||||
@@ -1384,6 +1427,25 @@ const removeInspectionItem = (item) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换套餐展开状态(Bug #325)
|
||||||
|
const togglePackageExpand = async (item) => {
|
||||||
|
item.expanded = !item.expanded
|
||||||
|
if (item.expanded && !item.details && item.feePackageId) {
|
||||||
|
item.loadingDetails = true
|
||||||
|
try {
|
||||||
|
const res = await listInspectionPackageDetails(item.feePackageId)
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
item.details = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取套餐明细失败', error)
|
||||||
|
item.details = []
|
||||||
|
} finally {
|
||||||
|
item.loadingDetails = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空所有选择
|
// 清空所有选择
|
||||||
const clearAllSelected = () => {
|
const clearAllSelected = () => {
|
||||||
selectedInspectionItems.value = []
|
selectedInspectionItems.value = []
|
||||||
@@ -1647,15 +1709,25 @@ defineExpose({
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
|
/* 表格卡片标题行:标题在左,按钮在右 */
|
||||||
.top-action-bar {
|
.table-card-header-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
width: 100%;
|
||||||
background: var(--el-bg-color);
|
}
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
||||||
padding: 0 12px;
|
.table-card-header-bar .table-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card-header-bar .table-card-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@@ -2245,6 +2317,24 @@ defineExpose({
|
|||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-item-content.is-package {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content.is-package:hover {
|
||||||
|
background: #f0f1f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content .expand-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content .expand-icon.is-expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.selected-item-content .item-itemName {
|
.selected-item-content .item-itemName {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -2256,6 +2346,44 @@ defineExpose({
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 套餐明细样式 */
|
||||||
|
.package-details {
|
||||||
|
margin-left: 24px;
|
||||||
|
padding: 4px 0 4px 12px;
|
||||||
|
border-left: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-price {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .loading-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .no-details {
|
||||||
|
padding: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-selection {
|
.no-selection {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
@@ -2336,10 +2464,6 @@ defineExpose({
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-action-bar {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|||||||
@@ -733,10 +733,15 @@ function handleAdd() {
|
|||||||
// 自动填充患者信息
|
// 自动填充患者信息
|
||||||
form.value.patientId = props.patientInfo.patientId
|
form.value.patientId = props.patientInfo.patientId
|
||||||
form.value.encounterId = props.patientInfo.encounterId
|
form.value.encounterId = props.patientInfo.encounterId
|
||||||
form.value.encounterNo = props.patientInfo.busNo
|
form.value.encounterNo = props.patientInfo.identifierNo
|
||||||
form.value.patientName = props.patientInfo.patientName
|
form.value.patientName = props.patientInfo.patientName
|
||||||
form.value.patientGender = props.patientInfo.genderEnum_enumText
|
form.value.patientGender = props.patientInfo.genderEnum_enumText
|
||||||
form.value.patientAge = props.patientInfo.age
|
// el-input-number 只接受 number;age 可能是 "12" / "12岁"
|
||||||
|
const rawAge = props.patientInfo.age
|
||||||
|
const parsedAge = rawAge === null || rawAge === undefined
|
||||||
|
? undefined
|
||||||
|
: Number.parseInt(String(rawAge).replace(/[^\d]/g, ''), 10)
|
||||||
|
form.value.patientAge = Number.isFinite(parsedAge) ? parsedAge : undefined
|
||||||
form.value.applyDoctorName = userStore.nickName
|
form.value.applyDoctorName = userStore.nickName
|
||||||
form.value.applyDeptName = userStore.orgName || props.patientInfo.deptName || ''
|
form.value.applyDeptName = userStore.orgName || props.patientInfo.deptName || ''
|
||||||
|
|
||||||
|
|||||||
@@ -1411,7 +1411,7 @@ const loadObservationItems = async (resetPage = false) => {
|
|||||||
package: item.packageName || '',
|
package: item.packageName || '',
|
||||||
feePackageId: item.feePackageId ? String(item.feePackageId) : null,
|
feePackageId: item.feePackageId ? String(item.feePackageId) : null,
|
||||||
sampleType: item.specimenCode || '',
|
sampleType: item.specimenCode || '',
|
||||||
amount: parseFloat(item.retailPrice || 0),
|
amount: parseFloat(item.packageAmount || 0),
|
||||||
sortOrder: item.sortOrder || null,
|
sortOrder: item.sortOrder || null,
|
||||||
serviceRange: item.serviceRange || '全部',
|
serviceRange: item.serviceRange || '全部',
|
||||||
subItemName: item.subItemName || '',
|
subItemName: item.subItemName || '',
|
||||||
@@ -2067,7 +2067,6 @@ const saveItem = async (item) => {
|
|||||||
|
|
||||||
editingRowId.value = null;
|
editingRowId.value = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存检验项目失败:', error);
|
|
||||||
ElMessage.error('保存失败,请稍后重试');
|
ElMessage.error('保存失败,请稍后重试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user