Compare commits

..

15 Commits

Author SHA1 Message Date
Ranyunqiao
f93bec967a Merge remote-tracking branch 'origin/develop' into develop 2026-06-16 10:21:42 +08:00
Ranyunqiao
020d1be4be bug 716 718 2026-06-16 10:21:26 +08:00
wangjian963
f7f037aee9 656 [门诊医生站-检查申请] 单击已保存记录回显异常:自动跳转页签错误且“检查方法”数据未回显 2026-06-16 10:14:56 +08:00
6c77ee8f84 fix(#776): guanyu (文件合入) 2026-06-16 09:38:40 +08:00
0855d1153b fix(#776): guanyu (文件合入) 2026-06-16 08:50:32 +08:00
wangjian963
168961e656 654
[住院医生站-手术申请] 申请单保存成功后弹窗未自动关闭
2026-06-15 17:14:46 +08:00
wangjian963
9dc4a12339 Merge remote-tracking branch 'origin/develop' into develop 2026-06-15 16:55:47 +08:00
wangjian963
9bbf7c6c08 651 [住院医生站-手术申请] 无法检索出已启用的手术项目(如:“血管闭合切割刀”) 2026-06-15 16:55:17 +08:00
05088a1d1a fix(#734): guanyu (文件合入) 2026-06-15 16:53:01 +08:00
Ranyunqiao
5e9dbb2f1b Merge remote-tracking branch 'origin/develop' into develop 2026-06-15 16:48:48 +08:00
Ranyunqiao
b25d2fbaa9 bug 588 628 642 700 714 715 2026-06-15 16:48:27 +08:00
690e7ca22c fix(charge): 门诊日结 groupingBy null key 修复
Collectors.groupingBy 遇到 contractNo/busNo 为 null 的元素会抛
NullPointerException: Element cannot be mapped to a null key

修复: 在 groupingBy 前增加 .filter(e -> key != null && !key.isEmpty())
2026-06-15 16:47:44 +08:00
43ab5b4498 fix(flyway): 解除 V42 版本号冲突
merge PR #11 时带入 guanyu commit 01e8cc459 错误恢复的已废弃 V42__bug745
文件(该文件内容本已迁移到 V45,原 V42 应删除)。两个 V42 并存导致 Flyway
启动阻塞:"Found more than one migration with version 42"。

修复:删除冗余的 V42__bug745_fix_mr_sealing_medical_record_id.sql(空 deprecated
文件,实际逻辑在 V45)。保留 V42__add_delete_flag_columns.sql(原始文件,2026-06-11)。

验证:
- mvn clean package 通过
- 后端启动成功(HTTP 404 根路径,Flyway 无冲突)
- 登录 API + 门诊收费列表 API 正常响应
- Jackson 3 Long→String 序列化仍生效
2026-06-15 16:18:00 +08:00
219ac30dc5 fix(#763): guanyu (文件合入) 2026-06-15 15:46:59 +08:00
20f71ec5d9 Merge PR #11: refactor(jackson): Jackson 2 → 3 全项目迁移 2026-06-15 15:43:23 +08:00
20 changed files with 986 additions and 238 deletions

View File

@@ -1,7 +1,7 @@
# HealthLink-HIS 代码模块索引
> 供 LLM 快速定位代码。每个模块列出 Controller → Service → Mapper 关键文件。
> 最后更新: 2026-06-15 12:00 (300 个 Controller)
> 最后更新: 2026-06-16 06:00 (300 个 Controller)
## 关键词 → 模块速查

View File

@@ -1,5 +1,6 @@
package com.core.framework.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
@@ -14,6 +15,9 @@ import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -55,9 +59,18 @@ public class ApplicationConfig {
@Bean
public JsonMapperBuilderCustomizer jacksonObjectMapperCustomization() {
return builder -> {
builder.defaultTimeZone(TimeZone.getDefault());
builder.defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
SimpleModule module = new SimpleModule("HealthLinkLocalDateTime");
module.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
module.addSerializer(LocalDateTime.class, LOCAL_DATE_TIME_SERIALIZER);
module.addSerializer(java.sql.Date.class, new ValueSerializer<java.sql.Date>() {
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void serialize(java.sql.Date value, JsonGenerator gen, SerializationContext ctx) throws JacksonException {
gen.writeString(sdf.format(value));
}
});
builder.addModule(module);
};
}

View File

@@ -2697,21 +2697,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("getSurgeryPage 开始: orgId={}, page={}/{}, searchKey={}", organizationId, pageNo, pageSize, searchKey);
long start = System.currentTimeMillis();
// 无搜索时尝试从 Redis 缓存读取(手术项目变更频率低,适合缓存)
String safeOrgId = organizationId != null ? organizationId.toString() : "";
String cacheKey = "surgery:page:" + safeOrgId + ":" + pageNo + ":" + pageSize;
boolean useCache = (searchKey == null || searchKey.trim().isEmpty());
if (useCache) {
Object cachedObj = redisCache.getCacheObject(cacheKey);
if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
log.info("从 Redis 缓存获取手术项目, key: {}, records: {}", cacheKey,
((IPage<?>) cachedObj).getRecords().size());
return (IPage<SurgeryItemDto>) cachedObj;
}
}
// 使用 MyBatis Plus 分页查询
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getSurgeryPage(
new Page<>(pageNo, pageSize),
PublicationStatus.ACTIVE.getValue(),
@@ -2720,12 +2705,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size());
// 无搜索时将结果写入缓存
if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS);
log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS);
}
return result;
}

View File

@@ -45,6 +45,12 @@ public class SurgeryItemDto {
@JsonProperty("unitCode_dictText")
private String unitCodeDictText;
/** 拼音码(前端穿梭框本地搜索用) */
private String pyStr;
/** 业务编号(前端穿梭框本地搜索用) */
private String busNo;
/** 所需标本编码(来自诊疗目录配置,对应字典 specimen_code 的 dictValue */
private String specimenCode;
}

View File

@@ -378,10 +378,21 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
*/
@Override
public R<?> getInPatientPendingList(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize) {
// 提取deadline手动处理防止自动拼接列名不存在的错误
String deadline = inpatientAdviceParam.getDeadline();
inpatientAdviceParam.setDeadline(null);
// 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
// 手动拼接截止时间条件request_time <= deadline
if (StringUtils.isNotEmpty(deadline)) {
Date deadlineDate = DateUtils.parseDate(deadline);
if (deadlineDate != null) {
queryWrapper.le("request_time", deadlineDate);
}
}
// 患者医嘱分页列表
Page<InpatientAdviceDto> inpatientAdvicePage = atdManageAppMapper.selectInpatientAdvicePage(
new Page<>(pageNo, pageSize), queryWrapper, CommonConstants.TableName.MED_MEDICATION_REQUEST,

View File

@@ -53,7 +53,9 @@ import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@@ -182,18 +184,22 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
// 提取requestStatus手动处理支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
inpatientAdviceParam.setRequestStatus(null);
// deadline 不在 UNION 子查询结果列中,且不映射为查询过滤条件
// 原因end_time 是医嘱结束时间,长期医嘱的 end_time 远在 deadline 之后,
// 使用 <= 过滤会排除所有长期医嘱,导致"未校对"tab 查询为空
// 提取deadline手动处理需要做NULL-safe的end_time比较Bug #763修复
String deadline = inpatientAdviceParam.getDeadline();
inpatientAdviceParam.setDeadline(null);
// 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
// 手动拼接requestStatus条件COMPLETED(3)时同时包含CHECK_VERIFIED(10)和PENDING_RECEIVE(11)
// 手动拼接requestStatus条件
// 1. ACTIVE(2)时:同时包含已停嘱待核对的 PENDING_STOP(13)
// 2. COMPLETED(3)时:同时包含已校对检查 CHECK_VERIFIED(10) 和已接收 PENDING_RECEIVE(11)
// UNION查询外层列名为request_statusT1.status_enum AS request_status不是status_enum
if (requestStatus != null) {
if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
if (RequestStatus.ACTIVE.getValue().equals(requestStatus)) {
queryWrapper.in("request_status",
RequestStatus.ACTIVE.getValue(), RequestStatus.PENDING_STOP.getValue());
} else if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
queryWrapper.in("request_status",
RequestStatus.COMPLETED.getValue(), RequestStatus.CHECK_VERIFIED.getValue(),
RequestStatus.PENDING_RECEIVE.getValue());
@@ -208,6 +214,18 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
}
// 手动拼接deadline条件end_time IS NULL OR end_time <= deadlineBug #763修复
// 住院医嘱的effective_dose_end可能为NULL签发临时医嘱时未设置结束时间
// PostgreSQL中 NULL <= anything 结果为FALSE需要先判断IS NULL
if (deadline != null && !deadline.isEmpty()) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date deadlineTime = sdf.parse(deadline);
queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime));
} catch (java.text.ParseException e) {
// deadline解析失败忽略此条件
}
}
// 患者医嘱分页列表
Page<InpatientAdviceDto> inpatientAdvicePage
= adviceProcessAppMapper.selectInpatientAdvicePage(new Page<>(pageNo, pageSize), queryWrapper,
@@ -401,14 +419,30 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
Date checkDate = new Date();
if (!serviceRequestList.isEmpty()) {
List<Long> serviceReqIds = serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList();
// 先查询服务请求,按 categoryEnum 分流:检查类(23)走 CHECK_VERIFIED其余走 COMPLETED
// 先查询服务请求,进行状态分流:
// 1. 如果是停嘱待核对(PENDING_STOP=13),则核对后转为停止(STOPPED=6)
// 2. 否则按类别分流:检查类(23)走 CHECK_VERIFIED其余走 COMPLETED
List<ServiceRequest> allServiceRequests = serviceRequestService.listByIds(serviceReqIds);
List<Long> stopReqIds = allServiceRequests.stream()
.filter(sr -> RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum()))
.map(ServiceRequest::getId).toList();
List<Long> checkReqIds = allServiceRequests.stream()
.filter(sr -> ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.filter(sr -> !RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum())
&& ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.map(ServiceRequest::getId).toList();
List<Long> otherReqIds = allServiceRequests.stream()
.filter(sr -> !ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.filter(sr -> !RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum())
&& !ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
.map(ServiceRequest::getId).toList();
// 停嘱待核对 → 停止STOPPED=6
if (!stopReqIds.isEmpty()) {
serviceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, stopReqIds)
.set(ServiceRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
.set(ServiceRequest::getPerformerCheckId, practitionerId)
.set(ServiceRequest::getCheckTime, checkDate));
}
// 检查类 → 已校对CHECK_VERIFIED=10
if (!checkReqIds.isEmpty()) {
serviceRequestService.updateCheckVerifiedStatus(checkReqIds, practitionerId, checkDate);
@@ -429,14 +463,50 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
}
}
if (!medRequestList.isEmpty()) {
// 更新药品请求状态已完成
medicationRequestService.updateCompletedStatusBatch(
medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate);
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
List<MedicationRequest> allMedRequests = medicationRequestService.listByIds(medReqIds);
List<Long> stopMedIds = allMedRequests.stream()
.filter(mr -> RequestStatus.PENDING_STOP.getValue().equals(mr.getStatusEnum()))
.map(MedicationRequest::getId).toList();
List<Long> otherMedIds = allMedRequests.stream()
.filter(mr -> !RequestStatus.PENDING_STOP.getValue().equals(mr.getStatusEnum()))
.map(MedicationRequest::getId).toList();
// 停嘱待核对 → 停止STOPPED=6
if (!stopMedIds.isEmpty()) {
medicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
.in(MedicationRequest::getId, stopMedIds)
.set(MedicationRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
.set(MedicationRequest::getPerformerCheckId, practitionerId)
.set(MedicationRequest::getCheckTime, checkDate));
}
// 其他类 → 已完成COMPLETED=3
if (!otherMedIds.isEmpty()) {
medicationRequestService.updateCompletedStatusBatch(otherMedIds, practitionerId, checkDate);
}
}
if (!deviceRequestList.isEmpty()) {
// 更新耗材请求状态已完成
deviceRequestService.updateCompletedStatusBatch(
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList());
List<Long> devReqIds = deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList();
// 同样处理耗材的停嘱核对
List<DeviceRequest> allDevRequests = deviceRequestService.listByIds(devReqIds);
List<Long> stopDevIds = allDevRequests.stream()
.filter(dr -> RequestStatus.PENDING_STOP.getValue().equals(dr.getStatusEnum()))
.map(DeviceRequest::getId).toList();
List<Long> otherDevIds = allDevRequests.stream()
.filter(dr -> !RequestStatus.PENDING_STOP.getValue().equals(dr.getStatusEnum()))
.map(DeviceRequest::getId).toList();
if (!stopDevIds.isEmpty()) {
deviceRequestService.update(new LambdaUpdateWrapper<DeviceRequest>()
.in(DeviceRequest::getId, stopDevIds)
.set(DeviceRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
.set(DeviceRequest::getPerformerCheckId, practitionerId)
.set(DeviceRequest::getCheckTime, checkDate));
}
if (!otherDevIds.isEmpty()) {
deviceRequestService.updateCompletedStatusBatch(otherDevIds);
}
}
return R.ok(null, "校对成功");
}
@@ -512,8 +582,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
if (!deviceRequestList.isEmpty()) {
// 更新耗材请求状态待发送
deviceRequestService.updateDraftStatusBatch(
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(),
practitionerId, checkDate, backReason);
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList());
}
return R.ok(null, "退回成功");
}

View File

@@ -153,6 +153,10 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
// 就诊ID集合
String encounterIds = dispenseFormSearchParam.getEncounterIds();
dispenseFormSearchParam.setEncounterIds(null);
// 汇总单查询不适用的字段清空(汇总单表无 tcm_flag 等列,避免 SQL 报错)
dispenseFormSearchParam.setTcmFlag(null);
dispenseFormSearchParam.setTherapyEnum(null);
dispenseFormSearchParam.setExeTime(null);
// 构建查询条件
QueryWrapper<DispenseFormSearchParam> queryWrapper = HisQueryUtils.buildQueryWrapper(dispenseFormSearchParam,

View File

@@ -235,10 +235,21 @@ public class NurseBillingAppService implements INurseBillingAppService {
inpatientAdviceParam.setEncounterIds(null);
Integer exeStatus = inpatientAdviceParam.getExeStatus();
inpatientAdviceParam.setExeStatus(null);
// 提取deadline手动处理防止自动拼接列名不存在的错误
String deadline = inpatientAdviceParam.getDeadline();
inpatientAdviceParam.setDeadline(null);
// 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
// 手动拼接截止时间条件request_time <= deadline
if (StringUtils.isNotEmpty(deadline)) {
Date deadlineDate = DateUtils.parseDate(deadline);
if (deadlineDate != null) {
queryWrapper.le("request_time", deadlineDate);
}
}
// 手动拼接住院患者id条件
if (encounterIds != null && !encounterIds.isEmpty()) {
List<Long> encounterIdList

View File

@@ -970,7 +970,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// }
// 根据省市医保分组
Map<String, List<PaymentRecDetailAccountResult>> paymentDetailsMapByContract = PaymentRecDetailAccountResultList
.stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
.stream()
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
.collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
// 查询所有的收费项
List<String> chargeItemIdStrs = paymentReconciliationList.stream().map(PaymentReconciliation::getChargeItemIds)
@@ -1043,7 +1045,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// 长大版本要显示出来省市医保的区别
List<Contract> redisContractList = iContractService.getRedisContractList();
Map<String, List<Contract>> contractMapByBusNo
= redisContractList.stream().collect(Collectors.groupingBy(Contract::getBusNo));
= redisContractList.stream()
.filter(e -> e.getBusNo() != null && !e.getBusNo().isEmpty())
.collect(Collectors.groupingBy(Contract::getBusNo));
for (Map.Entry<String, List<PaymentRecDetailAccountResult>> stringListEntry : paymentDetailsMapByContract
.entrySet()) {
String key = stringListEntry.getKey();
@@ -1445,7 +1449,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// 医保人次自费人次计算
List<EncounterAccountDto> list = iEncounterService.getEncounterInfoByTime(startDate, endDate);
Map<String, List<EncounterAccountDto>> encounterAccountDtoMapByContract
= list.stream().collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
= list.stream()
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
.collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
for (Map.Entry<String, List<EncounterAccountDto>> stringListEntry : encounterAccountDtoMapByContract
.entrySet()) {
String key = stringListEntry.getKey();
@@ -1519,7 +1525,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// 根据省市医保分组
Map<String, List<PaymentRecDetailAccountResult>> paymentDetailsMapByContract = PaymentRecDetailAccountResultList
.stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
.stream()
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
.collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
BigDecimal cashSum = BigDecimal.ZERO;// 现金总数 = rmbCashSum + vxCashSum + aliCashSum + uniCashSum
BigDecimal rmbCashSum = BigDecimal.ZERO;// 现金总数
@@ -1535,7 +1543,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// 长大版本要显示出来省市医保的区别
List<Contract> redisContractList = iContractService.getRedisContractList();
Map<String, List<Contract>> contractMapByBusNo
= redisContractList.stream().collect(Collectors.groupingBy(Contract::getBusNo));
= redisContractList.stream()
.filter(e -> e.getBusNo() != null && !e.getBusNo().isEmpty())
.collect(Collectors.groupingBy(Contract::getBusNo));
for (Map.Entry<String, List<PaymentRecDetailAccountResult>> stringListEntry : paymentDetailsMapByContract
.entrySet()) {
@@ -1922,7 +1932,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
// 医保人次自费人次计算
List<EncounterAccountDto> list = iEncounterService.getEncounterInfoByTime(startDate, endDate);
Map<String, List<EncounterAccountDto>> encounterAccountDtoMapByContract
= list.stream().collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
= list.stream()
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
.collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
for (Map.Entry<String, List<EncounterAccountDto>> stringListEntry : encounterAccountDtoMapByContract
.entrySet()) {
String key = stringListEntry.getKey();

View File

@@ -1,3 +0,0 @@
-- DEPRECATED: 本迁移已迁移至 V45__bug745_fix_mr_sealing_medical_record_id.sql
-- 原因:与 V42__add_delete_flag_columns.sql 版本号重复,导致 Flyway 阻塞
-- 此文件保留为空操作以避免 Flyway 校验错误

View File

@@ -903,7 +903,9 @@
t2.ID AS charge_item_definition_id,
t2.price AS price,
t1.permitted_unit_code AS unit_code,
COALESCE(sdd.dict_label, t1.permitted_unit_code) AS unit_code_dict_text
COALESCE(sdd.dict_label, t1.permitted_unit_code) AS unit_code_dict_text,
t1.py_str AS py_str,
t1.bus_no AS bus_no
FROM wor_activity_definition t1
LEFT JOIN adm_charge_item_definition t2
ON t2.instance_id = t1.ID
@@ -915,6 +917,7 @@
AND sdd.dict_type = 'unit_code'
AND sdd.status = '0'
WHERE t1.delete_flag = '0'
AND t1.status_enum = #{statusEnum}
AND (t1.category_code = '手术' OR t1.category_code = '24')
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
@@ -949,6 +952,7 @@
AND sdd.dict_type = 'unit_code'
AND sdd.status = '0'
WHERE t1.delete_flag = '0'
AND t1.status_enum = #{statusEnum}
AND t1.category_code = #{categoryCode}
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')

View File

@@ -18,7 +18,6 @@
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"lint": "eslint . --ext .js,.vue src/",
"format": "prettier --write src/**/*.{vue,js,ts,jsx,tsx,scss,css,json}",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
@@ -82,7 +81,6 @@
"happy-dom": "^20.8.3",
"jsdom": "^28.1.0",
"pg": "^8.18.0",
"prettier": "^3.8.4",
"sass": "^1.100.0",
"typescript": "^5.9.3",
"unplugin-auto-import": "^0.18.6",

View File

@@ -4,11 +4,11 @@
*/
import {hiprint} from 'vue-plugin-hiprint';
import useUserStore from '@/store/modules/user';
import {ElMessage} from 'element-plus';
// 打印模板映射表 .
const TEMPLATE_MAP = {
// CLINIC_CHARGE: () => import('@/views/charge/cliniccharge/components/template.json'),
// DISPOSAL: () => import('@/views/clinicmanagement/disposal/components/disposalTemplate.json'),
//处方签
PRESCRIPTION: () => import('@/components/Print/Prescription.json'),
//处置单
@@ -137,29 +137,53 @@ export async function simplePrintWithDialog(
// 导出模板名称常量
export const PRINT_TEMPLATE = {
// CLINIC_CHARGE: 'CLINIC_CHARGE',
DAY_END: 'DAY_END',
WESTERN_MEDICINE: 'WESTERN_MEDICINE',
IN_HOSPITAL_DISPENSING: 'IN_HOSPITAL_DISPENSING',
//门诊挂号
OUTPATIENT_REGISTRATION: 'OUTPATIENT_REGISTRATION',
// 门诊手术计费
OUTPATIENT_SURGERY_CHARGE: 'OUTPATIENT_SURGERY_CHARGE',
//门诊收费
OUTPATIENT_CHARGE: 'OUTPATIENT_CHARGE',
//处方签
PRESCRIPTION: 'PRESCRIPTION',
//处置单
DISPOSAL: 'DISPOSAL',
//门诊病历
OUTPATIENT_MEDICAL_RECORD: 'OUTPATIENT_MEDICAL_RECORD',
//门诊输液贴
OUTPATIENT_INFUSION: 'OUTPATIENT_INFUSION',
//手术记录
OPERATIVE_RECORD: 'OPERATIVE_RECORD',
//红旗门诊病历
HQOUTPATIENT_MEDICAL_RECORD: 'HQOUTPATIENT_MEDICAL_RECORD',
//预交金
ADVANCE_PAYMENT: 'ADVANCE_PAYMENT',
//中药处方单
CHINESE_MEDICINE_PRESCRIPTION: 'CHINESE_MEDICINE_PRESCRIPTION',
//药房处方单
PHARMACY_PRESCRIPTION: 'PHARMACY_PRESCRIPTION',
//中药医生处方单
DOC_CHINESE_MEDICINE_PRESCRIPTION: 'DOC_CHINESE_MEDICINE_PRESCRIPTION',
// ========== 新增模板原LODOP迁移==========
//腕带
WRIST_BAND: 'WRIST_BAND',
//分诊条
TRIAGE_TICKET: 'TRIAGE_TICKET',
//输液标签
INJECT_LABEL: 'INJECT_LABEL',
//床头卡
BED_CARD: 'BED_CARD',
//护理交接班
CHANGE_SHIFT_BILL: 'CHANGE_SHIFT_BILL',
//医嘱执行单
EXE_ORDER_SHEET: 'EXE_ORDER_SHEET',
//体温单
TEMPERATURE_SHEET: 'TEMPERATURE_SHEET',
//会诊申请单
CONSULTATION: 'CONSULTATION',
};
@@ -182,20 +206,28 @@ export function getPrinterList() {
}
}
import useUserStore from '@/store/modules/user';
import {ElMessage} from 'element-plus';
/**
* 获取当前登录用户ID
* @returns {string} 用户ID
*/
function getCurrentUserId() {
try {
// 从Pinia store中获取当前用户ID
const userStore = useUserStore();
return userStore.id || '';
} catch (e) {
console.error('获取用户ID失败:', e);
return '';
}
}
/**
* 生成打印机缓存键
* @param {string} businessName 打印业务名称
* @returns {string} 缓存键
*/
function getPrinterCacheKey(businessName) {
const userId = getCurrentUserId();
@@ -204,6 +236,8 @@ function getPrinterCacheKey(businessName) {
/**
* 从缓存获取上次选择的打印机
* @param {string} businessName 打印业务名称
* @returns {string} 打印机名称
*/
export function getCachedPrinter(businessName = 'default') {
const cacheKey = getPrinterCacheKey(businessName);
@@ -212,6 +246,8 @@ export function getCachedPrinter(businessName = 'default') {
/**
* 保存打印机选择到缓存
* @param {string} printerName 打印机名称
* @param {string} businessName 打印业务名称
*/
export function savePrinterToCache(printerName, businessName = 'default') {
if (printerName) {
@@ -221,7 +257,13 @@ export function savePrinterToCache(printerName, businessName = 'default') {
}
/**
* 执行打印操作 (带自动二维码生成逻辑)
* 执行打印操作
* @param {Array} data 打印数据
* @param {Object} template 打印模板
* @param {string} printerName 打印机名称(可选)
* @param {Object} options 打印选项(可选)
* @param {string} businessName 打印业务名称(可选)
* @returns {Promise} 打印结果Promise
*/
export function executePrint(data, template, printerName, options = {}, businessName = 'default') {
return new Promise((resolve, reject) => {
@@ -286,6 +328,12 @@ export function executePrint(data, template, printerName, options = {}, business
/**
* 选择打印机并执行打印
* @param {Array} data 打印数据
* @param {Object} template 打印模板
* @param {Function} showPrinterDialog 显示打印机选择对话框的函数
* @param {Object} modal 消息提示对象
* @param {Function} callback 打印完成后的回调函数
* @param {string} businessName 打印业务名称(可选)
*/
export async function selectPrinterAndPrint(
data,
@@ -296,24 +344,29 @@ export async function selectPrinterAndPrint(
businessName = 'default'
) {
try {
// 获取打印机列表
const printerList = getPrinterList();
if (printerList.length === 0) {
modal.msgWarning('未检测到可用打印机');
return;
}
// 获取缓存的打印机
const cachedPrinter = getCachedPrinter(businessName);
let selectedPrinter = '';
// 判断打印机选择逻辑
if (printerList.length === 1) {
selectedPrinter = printerList[0].name;
await executePrint(data, template, selectedPrinter, {}, businessName);
if (callback) callback();
} else if (cachedPrinter && printerList.some((p) => p.name === cachedPrinter)) {
} else if (cachedPrinter && printerList.some((printer) => printer.name === cachedPrinter)) {
selectedPrinter = cachedPrinter;
await executePrint(data, template, selectedPrinter, {}, businessName);
if (callback) callback();
} else {
// 调用显示打印机选择对话框的函数
showPrinterDialog(printerList, async (chosenPrinter) => {
try {
await executePrint(data, template, chosenPrinter, {}, businessName);
@@ -324,40 +377,375 @@ export async function selectPrinterAndPrint(
});
}
} catch (error) {
modal.msgError(error.message || '获取打印机失败');
modal.msgError(error.message || '获取打印机列表失败');
}
}
/**
* 预览打印
*/
// 预览打印
export function previewPrint(elementDom) {
if (elementDom) {
// 初始化已在 main.js 中完成,无需重复调用
// hiprint.init();
const hiprintTemplate = new hiprint.PrintTemplate();
// printByHtml为预览打印
hiprintTemplate.printByHtml(elementDom, {});
} else {
ElMessage.error('加载模版失败');
ElMessage({
type: 'error',
message: '加载模版失败',
});
}
}
/**
* 打印门诊挂号收据
* 打印门诊挂号收据(使用浏览器打印,模板与补打挂号一致)
* @param {Object} data 打印数据
* @param {Object} options 打印选项
* @returns {Promise} 打印结果 Promise
*/
export function printRegistrationReceipt(data, options = {}) {
// 此处保持原有的 HTML 拼接逻辑(略)
return Promise.resolve({ success: true, message: '打印窗口已打开' });
return new Promise((resolve, reject) => {
try {
// 构建打印内容的 HTML
const printContent = `
<div class="print-header">
<div class="header-content">
<div class="document-title">门诊预约挂号凭条</div>
<div class="print-time">打印时间:${data.printTime || new Date().toLocaleString()}</div>
</div>
</div>
<div class="print-section">
<div class="section-title">患者基本信息</div>
<div class="info-row">
<span class="label">患者姓名:</span>
<span class="value">${data.patientName || '-'}</span>
</div>
<div class="info-row">
<span class="label">就诊卡号:</span>
<span class="value">${data.cardNo || data.busNo || '-'}</span>
</div>
<div class="info-row">
<span class="label">身份证号:</span>
<span class="value">${data.idCard ? maskIdCard(data.idCard) : '-'}</span>
</div>
<div class="info-row">
<span class="label">联系电话:</span>
<span class="value">${data.phone || '-'}</span>
</div>
</div>
<div class="print-section">
<div class="section-title">挂号信息</div>
<div class="info-row">
<span class="label">就诊科室:</span>
<span class="value">${data.organizationName || '-'}</span>
</div>
<div class="info-row">
<span class="label">医生姓名:</span>
<span class="value">${data.practitionerName || '-'}</span>
</div>
<div class="info-row">
<span class="label">挂号类型:</span>
<span class="value">${data.healthcareName || '-'}</span>
</div>
<div class="info-row">
<span class="label">挂号时间:</span>
<span class="value">${data.visitTime || data.chargeTime || '-'}</span>
</div>
</div>
<div class="print-section">
<div class="section-title">费用信息</div>
<table class="fee-table">
<thead>
<tr>
<th>项目</th>
<th>数量</th>
<th>单价</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<tr>
<td>挂号费</td>
<td>1</td>
<td>¥${parseFloat(data.price || 0).toFixed(2)}</td>
<td>¥${parseFloat(data.price || 0).toFixed(2)}</td>
</tr>
${parseFloat(data.activityPrice || 0) > 0 ? `
<tr>
<td>诊疗费</td>
<td>1</td>
<td>¥${parseFloat(data.activityPrice || 0).toFixed(2)}</td>
<td>¥${parseFloat(data.activityPrice || 0).toFixed(2)}</td>
</tr>` : ''}
${parseFloat(data.medicalRecordFee || 0) > 0 ? `
<tr>
<td>病历费</td>
<td>1</td>
<td>¥${parseFloat(data.medicalRecordFee || 0).toFixed(2)}</td>
<td>¥${parseFloat(data.medicalRecordFee || 0).toFixed(2)}</td>
</tr>` : ''}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total-label">合计:</td>
<td class="total-value">¥${parseFloat(data.totalPrice || data.amount || 0).toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
<!-- 流水号显示在左下角 -->
<div class="serial-number-bottom-left">
<span class="serial-label">流水号:</span>
<span class="serial-value">${data.serialNo || data.encounterId || '-'}</span>
</div>
<div class="print-footer">
<div class="reminder">温馨提示:请妥善保管此凭条,就诊时请携带。</div>
</div>
<!-- 二维码区域 -->
<div class="qr-code-section">
<div class="qr-code-container">
<div id="qrcode-print" class="qrcode-print"></div>
<div class="qr-code-label">扫码查看挂号信息</div>
</div>
</div>
`;
// 创建新窗口用于打印
const printWindow = window.open('', '_blank');
if (!printWindow) {
reject(new Error('无法打开打印窗口,请检查浏览器弹窗设置'));
return;
}
// 写入打印内容
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>门诊预约挂号凭条</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; margin: 0; }
.print-header { margin-bottom: 20px; position: relative; }
.header-content { text-align: center; }
.document-title { font-size: 16px; font-weight: bold; margin-bottom: 10px; }
.print-time { font-size: 12px; color: #666; text-align: right; }
.print-section { margin-bottom: 20px; }
.section-title { font-size: 14px; font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #ddd; padding-bottom: 5px; }
.info-row { margin-bottom: 8px; font-size: 13px; }
.label { display: inline-block; width: 100px; font-weight: bold; }
.value { display: inline-block; }
.fee-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
.fee-table th, .fee-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.fee-table th { background-color: #f5f5f5; font-weight: bold; }
.total-label { font-weight: bold; text-align: right; }
.total-value { font-weight: bold; color: red; }
.serial-number-bottom-left { position: absolute; bottom: 20px; left: 20px; font-size: 14px; font-weight: bold; }
.serial-number-bottom-left .serial-label { font-weight: bold; margin-right: 5px; }
.serial-number-bottom-left .serial-value { font-weight: bold; color: #333; }
.print-content { position: relative; min-height: 500px; padding-bottom: 60px; }
.print-footer { margin-top: 20px; font-size: 12px; color: #666; }
.reminder { text-align: center; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }
.qr-code-section { margin-top: 20px; display: flex; justify-content: center; align-items: center; padding: 15px; }
.qr-code-container { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.qrcode-print { width: 120px; height: 120px; }
.qr-code-label { font-size: 12px; color: #666; text-align: center; }
@media print { body { padding: 0; } .qr-code-section { page-break-inside: avoid; } }
</style>
</head>
<body>
<div class="print-content">
${printContent}
</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.onload = function() {
setTimeout(() => {
printWindow.print();
resolve({ success: true, message: '打印窗口已打开' });
}, 250);
};
} catch (error) {
console.error('打印门诊挂号收据失败:', error);
reject(error);
}
});
}
/**
* 脱敏身份证号
* @param {string} idCard 身份证号
* @returns {string} 脱敏后的身份证号
*/
function maskIdCard(idCard) {
if (!idCard) return '';
if (idCard.length >= 10) {
const prefix = idCard.substring(0, 6);
const suffix = idCard.substring(idCard.length - 4);
const stars = '*'.repeat(Math.max(0, idCard.length - 10));
return prefix + stars + suffix;
} else if (idCard.length >= 6) {
const prefix = idCard.substring(0, 3);
const suffix = idCard.substring(idCard.length - 1);
return prefix + '*'.repeat(idCard.length - 4) + suffix;
}
return idCard;
}
/**
* 打印入院证
* @param {Object} data 入院证数据
* @returns {Promise}
*/
export function printAdmissionCertificate(data) {
// 此处保持原有的 HTML 拼接逻辑(略)
return Promise.resolve({ success: true, message: '打印窗口已打开' });
return new Promise((resolve, reject) => {
try {
const printContent = `
<div class="certificate">
<div class="title">${data.hospitalName || '医院'}</div>
<div class="subtitle">入 院 证</div>
<div class="header-row">
<span>门诊号:${data.outpatientNo || '—'}</span>
<span>住院号:${data.inpatientNo || '—'}</span>
</div>
<div class="info-grid">
<div class="info-item"><span class="label">姓名:</span><span class="value">${data.patientName || '—'}</span></div>
<div class="info-item"><span class="label">性别:</span><span class="value">${data.gender || '—'}</span></div>
<div class="info-item"><span class="label">年龄:</span><span class="value">${data.age || '—'}</span></div>
<div class="info-item"><span class="label">费用类型:</span><span class="value">${data.feeType || '—'}</span></div>
</div>
<div class="info-grid">
<div class="info-item"><span class="label">身份证号:</span><span class="value">${data.idCard || '—'}</span></div>
<div class="info-item"><span class="label">电话:</span><span class="value">${data.phone || '—'}</span></div>
</div>
<div class="info-grid">
<div class="info-item full-width"><span class="label">住址:</span><span class="value">${data.address || '—'}</span></div>
</div>
<div class="info-grid">
<div class="info-item full-width"><span class="label">联系人:</span><span class="value">${data.contactPerson || '—'}${data.contactRelation ? '' + data.contactRelation + '' : ''} ${data.contactPhone || ''}</span></div>
</div>
<div class="divider"></div>
<div class="info-grid three-col">
<div class="info-item"><span class="label">入院科室:</span><span class="value">${data.department || '—'}</span></div>
<div class="info-item"><span class="label">入院类型:</span><span class="value">${data.admissionType || '—'}</span></div>
<div class="info-item"><span class="label">入院病区:</span><span class="value">${data.ward || '—'}</span></div>
</div>
<div class="info-grid three-col">
<div class="info-item"><span class="label">入院方式:</span><span class="value">${data.admissionMethod || '—'}</span></div>
<div class="info-item"><span class="label">患者病情:</span><span class="value">${data.patientCondition || '—'}</span></div>
</div>
<div class="divider"></div>
<div class="diagnosis-section">
<div class="label">入院诊断:</div>
<div class="diagnosis-item">1. ${data.westernDiagnosis || '—'}(西医)</div>
<div class="diagnosis-item">2. ${data.tcmDiagnosis || '—'}(中医)</div>
</div>
<div class="divider"></div>
<div class="info-grid">
<div class="info-item"><span class="label">开单医生:</span><span class="value">${data.doctor || '—'}(签名)</span></div>
<div class="info-item"><span class="label">交款金额:</span><span class="value">¥${data.paymentAmount || '0.00'} 元(盖章有效)</span></div>
</div>
<div class="info-grid">
<div class="info-item"><span class="label">申请日期:</span><span class="value">${data.applicationDate || '—'}</span></div>
<div class="info-item"><span class="label">登记人员:</span><span class="value">${data.registrar || '—'}(签章)</span></div>
</div>
<div class="divider"></div>
<div class="reminder">
<div class="reminder-title">【温馨提示】</div>
<div class="reminder-item">1. 医保患者请于24小时内持医保卡至住院窗口办理联网逾期无法报销。</div>
<div class="reminder-item">2. 住院期间请勿随身携带贵重物品,请携带必要洗漱用具。</div>
<div class="reminder-item">3. 本证3日内有效。</div>
</div>
</div>
`;
const printWindow = window.open('', '_blank');
if (!printWindow) {
reject(new Error('无法打开打印窗口,请检查浏览器弹窗设置'));
return;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>入院证</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: SimSun, '宋体', serif; padding: 20px; color: #000; }
.certificate { max-width: 700px; margin: 0 auto; }
.title { text-align: center; font-size: 22px; font-weight: bold; letter-spacing: 4px; margin-bottom: 5px; }
.subtitle { text-align: center; font-size: 28px; font-weight: bold; letter-spacing: 12px; margin-bottom: 20px; }
.header-row { display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 14px; }
.info-grid { display: flex; flex-wrap: wrap; margin-bottom: 10px; }
.info-grid.three-col .info-item { width: 33.33%; }
.info-item { width: 50%; margin-bottom: 8px; font-size: 14px; }
.info-item.full-width { width: 100%; }
.info-item .label { font-weight: bold; }
.info-item .value { }
.divider { border-bottom: 1px dashed #999; margin: 12px 0; }
.diagnosis-section { margin-bottom: 10px; font-size: 14px; }
.diagnosis-section .label { font-weight: bold; margin-bottom: 5px; }
.diagnosis-item { margin-left: 20px; margin-bottom: 5px; }
.reminder { margin-top: 15px; font-size: 13px; }
.reminder-title { font-weight: bold; margin-bottom: 5px; }
.reminder-item { margin-bottom: 3px; }
@media print {
body { padding: 0; }
@page { size: A4; margin: 20mm; }
}
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
printWindow.onload = function () {
setTimeout(() => {
printWindow.print();
resolve({ success: true, message: '打印窗口已打开' });
}, 300);
};
} catch (error) {
console.error('打印入院证失败:', error);
reject(error);
}
});
}
/**
* 格式化日期
* @param {string|Date} date 日期
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date) {
if (!date) return '';
@@ -372,7 +760,7 @@ export function formatDate(date) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 默认导出
// 默认导出简化的打印方法
export default {
print: simplePrint,
printWithDialog: simplePrintWithDialog,

View File

@@ -88,6 +88,7 @@
v-loading="loading"
:data="filteredApplicationList"
:max-height="200"
min-width="865"
border
size="small"
:header-cell-style="{ background: '#f5f5f5', color: '#303133', fontWeight: '600' }"
@@ -140,7 +141,7 @@
<vxe-column
title="收费"
field="isCharged"
width="50"
width="65"
align="center"
>
<template #default="{ row }">
@@ -155,7 +156,7 @@
<vxe-column
title="退费"
field="isRefunded"
width="50"
width="65"
align="center"
>
<template #default="{ row }">
@@ -170,7 +171,7 @@
<vxe-column
title="执行"
field="isExecuted"
width="50"
width="65"
align="center"
>
<template #default="{ row }">
@@ -1451,7 +1452,10 @@ const availableMethods = computed(() => {
});
function isStandaloneMethodSelected(method) {
return selectedMethods.value.some((m) => String(m.id) === String(method?.id));
return selectedMethods.value.some((m) =>
String(m.id) === String(method?.id) ||
(m.code && method?.code && String(m.code) === String(method.code))
);
}
function getDisplayMethodName(method) {
@@ -1712,11 +1716,12 @@ watch(() => props.patientInfo, (newVal) => {
watch(() => props.activeTab, async (val) => {
if (val === 'examination') {
getList();
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
// 进入检查tab时自动初始化表单确保输入框可编辑且处于干净状态
// handleAdd 内部已调用 loadClinicalDiag() 获取临床诊断
if (props.patientInfo?.encounterId) {
await loadClinicalDiag();
handleAdd();
}
// 父组件 handleClick 中已调用 getList(),此处不再重复调用
}
});
@@ -1859,7 +1864,8 @@ function handleSave() {
});
}
function handleRowClick(row) {
function handleRowClick({ row, column }) {
// vxe-table v4 cell-click 事件参数为 { row, column, rowIndex, ... },需解构获取实际行数据
Object.assign(form, row);
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
selectedItems.value = [];
@@ -1896,6 +1902,11 @@ function handleRowClick(row) {
packageId: null,
hasChildren: false // #426修复: 树形表格懒加载展开标记后续根据packageId动态设置
};
// Bug #656: 从已保存数据提取检查方法信息(移到 bodyPartCode 外部,确保始终可用)
const savedMethodCode = m.examMethodCode || m.checkMethodCode;
const savedMethodId = m.checkMethodId;
const savedMethodName = m.checkMethodName;
// 加载该项目的检查方法
if (m.bodyPartCode) {
try {
@@ -1916,49 +1927,74 @@ function handleRowClick(row) {
packagePrice: md.packagePrice || null,
serviceFee: md.serviceFee || null
}));
// 回充已保存的检查方法
const methodCode = m.examMethodCode || m.checkMethodCode;
const methodId = m.checkMethodId;
if (methodCode || methodId) {
const found = item.methods.find(md =>
(methodCode && String(md.code) === String(methodCode)) ||
(methodId && String(md.id) === String(methodId))
);
if (found) {
item.selectedMethod = found;
} else {
item.selectedMethod = {
id: methodId || null,
name: m.checkMethodName || '',
code: methodCode || '',
price: 0,
packageName: m.checkMethodPackageName || '',
packageId: null,
packagePrice: null,
serviceFee: null
};
}
}
if (item.selectedMethod || item.packageId || item.packageName) {
item.hasChildren = true;
}
}
} catch (err) {
console.error('加载检查方法失败', err);
}
}
// Bug #656修复: 回充已保存的检查方法(始终执行,不依赖 bodyPartCode 或 API 返回)
if (savedMethodCode || savedMethodId) {
let found = null;
// 1. 优先在该项目的检查方法列表中查找
found = item.methods.find(md =>
(savedMethodCode && String(md.code) === String(savedMethodCode)) ||
(savedMethodId && String(md.id) === String(savedMethodId))
);
// 2. 未找到时,回退到全局 allMethods 中查找(兼容 checkType 不匹配导致方法不在分类结果中的场景)
if (!found) {
found = allMethods.value.find(m =>
(savedMethodCode && String(m.code) === String(savedMethodCode)) ||
(savedMethodId && String(m.id) === String(savedMethodId))
);
}
// 3. 设置 selectedMethod
if (found) {
item.selectedMethod = {
id: found.id,
name: found.name,
code: found.code,
price: found.price || 0,
packageName: found.packageName || '',
packageId: found.packageId || null,
packagePrice: found.packagePrice || null,
serviceFee: found.serviceFee || null
};
} else {
// 4. 创建降级对象:优先用已保存名称,其次用代码作为显示名
item.selectedMethod = {
id: savedMethodId || null,
name: savedMethodName || savedMethodCode || '',
code: savedMethodCode || '',
price: 0,
packageName: m.checkMethodPackageName || '',
packageId: null,
packagePrice: null,
serviceFee: null
};
}
}
if (item.selectedMethod || item.packageId || item.packageName) {
item.hasChildren = true;
}
return item;
}));
// Bug #408修复: 确保明细数据正确加载到selectedItems
// Bug #656修复: 去重键改为 id || code避免 id 为 null 时误判为同一方法导致丢失
const methodMap = new Map();
for (const item of itemsWithMethods) {
if (item.selectedMethod && !methodMap.has(String(item.selectedMethod.id))) {
methodMap.set(String(item.selectedMethod.id), {
...item.selectedMethod,
expanded: false,
packageLoading: false,
packageDetails: []
});
if (item.selectedMethod) {
const dedupKey = item.selectedMethod.id != null
? String(item.selectedMethod.id)
: (item.selectedMethod.code || `__item_${item.id}`);
if (!methodMap.has(dedupKey)) {
methodMap.set(dedupKey, {
...item.selectedMethod,
expanded: false,
packageLoading: false,
packageDetails: []
});
}
}
item.methodPackageDetails = [];
}
@@ -1988,8 +2024,39 @@ function handleRowClick(row) {
syncCategoryChecked();
// Bug #384修复: 回充后更新检查方法显示
updateMethodDisplay();
// Bug #408修复: 加载申请单详情后自动切换到检查明细页签,确保已加载的明细数据可见
activeDetailTab.value = 'applyDetail';
// Bug #656修复: 展开对应检查项目分类节点,使树形结构和检查方法勾选状态正确回显
const firstItem = selectedItems.value[0];
if (firstItem && firstItem.checkType) {
const targetCat = categoryList.value.find(cat =>
(cat.typeName && cat.typeName === firstItem.checkType) ||
(cat.categoryName && cat.categoryName === firstItem.checkType)
);
if (targetCat) {
// 先确保分类树的方法已加载完成,再展开分类
// 注意顺序:先 await handleCategoryExpand再设置 activeNames
// 避免 handleCollapseChange 中异步加载与后续匹配逻辑的竞态条件
await handleCategoryExpand(targetCat);
activeNames.value = targetCat.typeId;
// 将 selectedMethods 中的降级方法id/name 为空)按 code 匹配分类树中的方法,补充名称和真实 ID
if (targetCat.methods && targetCat.methods.length > 0) {
let updated = false;
selectedMethods.value = selectedMethods.value.map(m => {
if ((!m.name || !m.id) && m.code) {
const matched = targetCat.methods.find(cm => String(cm.code) === String(m.code));
if (matched) {
updated = true;
return { ...matched, expanded: false, packageLoading: false, packageDetails: m.packageDetails || [] };
}
}
return m;
});
if (updated) {
updateMethodDisplay();
}
}
}
}
} catch (err) {
console.error('加载申请单详情失败', err);
ElMessage.error('加载申请单详情失败');
@@ -2333,10 +2400,11 @@ function resetCategoryChecked() {
function syncCategoryChecked() {
resetCategoryChecked();
const ids = new Set(selectedItems.value.map(s => s.id));
// 统一转为 String 比较,避免 Number vs String 类型不匹配
const ids = new Set(selectedItems.value.map(s => String(s.id)));
for (const cat of categoryList.value)
for (const item of cat.items)
if (ids.has(item.id)) item.checked = true;
if (ids.has(String(item.id))) item.checked = true;
}
defineExpose({ getList });

View File

@@ -227,7 +227,6 @@
<div style="padding: 10px; position: relative">
<el-tabs
v-model="activeTab"
v-loading="loading"
type="card"
style="width: 100%; height: 100%"
@tab-change="handleClick(activeTab)"
@@ -546,7 +545,6 @@ const diagnosisRef = ref();
const consultationRef = ref();
const infectiousReportRef = ref();
const waitCount = ref(0);
const loading = ref(false);
const { proxy } = getCurrentInstance();
const visitType = ref('');
const firstVisitDate = ref('');
@@ -807,7 +805,6 @@ function handleCardClick(item, index) {
currentEncounterId.value = item.encounterId;
console.log('currentEncounterId.value 设置为:', currentEncounterId.value);
loading.value = true;
patientList.value.forEach((patient) => {
patient.active = patient.encounterId === item.encounterId;
});
@@ -860,9 +857,6 @@ function handleCardClick(item, index) {
eprescriptionRef.value.getList();
consultationRef.value.fetchConsultationList();
// emrRef.value.getDetail(item.encounterId);
setTimeout(() => {
loading.value = false;
}, 200);
});
}

View File

@@ -487,6 +487,33 @@ function getList() {
return;
}
// 格式化工具:将后端返回的日期字符串转为可读格式
// 当时间为 00:00:00 时(历史数据 date 列迁移),只显示日期部分
function formatDisplayDate(val) {
if (!val) return '';
const pad = (n) => String(n).padStart(2, '0');
function formatFull(d) {
const datePart = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
if (h === 0 && m === 0 && s === 0) return datePart;
return `${datePart} ${pad(h)}:${pad(m)}:${pad(s)}`;
}
if (typeof val === 'string') {
if (val.includes('T')) {
const d = new Date(val);
if (!isNaN(d.getTime())) return formatFull(d);
}
// yyyy-MM-dd HH:mm:ss 或 yyyy/MM/dd HH:mm:ss
const m = val.match(/^(\d{4}[-/]\d{1,2}[-/]\d{1,2})(?:\s+(\d{1,2}:\d{2}(?::\d{2})?))?/);
if (m) {
if (m[2] && m[2] !== '00:00:00' && m[2] !== '00:00') return val;
return m[1];
}
}
if (val instanceof Date && !isNaN(val.getTime())) return formatFull(val);
return val;
}
// 先加载西医诊断,再加载中医诊断(避免竞态覆盖)
getEncounterDiagnosis(props.patientInfo.encounterId).then((res) => {
if (res.code == 200) {
@@ -502,6 +529,7 @@ function getList() {
syndromeDefinitionId: '',
syndromeGroupNo: '',
showPopover: false,
diagnosisTime: formatDisplayDate(item.diagnosisTime),
};
if (obj.diagSrtNo == null) {
obj.diagSrtNo = 1;
@@ -540,11 +568,16 @@ function getList() {
iptDiseTypeCode: item.iptDiseTypeCode,
showPopover: false,
diagnosisDoctor: item.diagnosisDoctor || props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: item.diagnosisTime || new Date().toLocaleString('zh-CN')
diagnosisTime: formatDisplayDate(item.diagnosisTime) || new Date().toLocaleString('zh-CN')
});
});
// 将新数据添加到现有列表现有列表
// 先移除已有的中医诊断防止重复追加getList 可能被多次调用)
form.value.diagnosisList = form.value.diagnosisList.filter(
(item) => item.diagnosisSystem !== '中医'
);
// 将新数据添加到现有列表
form.value.diagnosisList.push(...newList);
// 重新排序整个列表
@@ -899,80 +932,109 @@ function handleSaveDiagnosis() {
item.diagSrtNo = index + 1;
});
// 日期格式化工具:将 Date 对象或 ISO 字符串转为后端期望的 yyyy/M/d HH:mm:ss 格式
function formatDateForBackend(val) {
if (!val) return '';
if (typeof val === 'string') {
// 已经是字符串且符合 yyyy/M/d HH:mm:ss 格式则直接返回
if (/^\d{4}\/\d{1,2}\/\d{1,2} \d{1,2}:\d{2}:\d{2}$/.test(val)) return val;
// 已经是 yyyy-MM-dd HH:mm:ss 格式则转换为 yyyy/M/d HH:mm:ss
const m = val.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2})$/);
if (m) return `${m[1]}/${parseInt(m[2])}/${parseInt(m[3])} ${m[4]}`;
// ISO 或其他格式则解析为 Date
val = new Date(val);
}
if (val instanceof Date && !isNaN(val.getTime())) {
const y = val.getFullYear();
const M = val.getMonth() + 1;
const d = val.getDate();
const hh = String(val.getHours()).padStart(2, '0');
const mm = String(val.getMinutes()).padStart(2, '0');
const ss = String(val.getSeconds()).padStart(2, '0');
return `${y}/${M}/${d} ${hh}:${mm}:${ss}`;
}
return '';
}
// 步骤3拆分为西医诊断和中医诊断
const westernList = sortedList.filter((item) => item.diagnosisSystem !== '中医');
const tcmList = sortedList.filter((item) => item.diagnosisSystem === '中医');
const savePromises = [];
// 顺序执行保存,避免并行竞态:
// saveDoctorDiagnosis西医会 deleteEncounterDiagnosisInfos 删除全部诊断,
// saveTcmDiagnosis中医只追加不删除必须等西医保存完成后再保存中医
(async () => {
try {
// 先保存西医诊断(会先清空再插入)
if (westernList.length > 0) {
await saveDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: westernList.map(item => ({
...item,
diagnosisTime: formatDateForBackend(item.diagnosisTime),
onsetDate: formatDateForBackend(item.onsetDate),
})),
});
}
// 保存西医诊断
if (westernList.length > 0) {
savePromises.push(
saveDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: westernList,
})
);
}
// 再逐个保存医诊断(只追加,不清空)
for (const item of tcmList) {
const syndromeGroupNo = item.conditionId
? `${item.conditionId}-${item.tcmSyndromeCode || Date.now()}`
: `${Date.now()}-${item.tcmSyndromeCode || '0'}`;
await saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: [
// 病illness
{
conditionId: item.conditionId || null,
name: item.name,
ybNo: item.ybNo,
definitionId: item.definitionId || null,
diagSrtNo: item.diagSrtNo,
medTypeCode: item.medTypeCode,
maindiseFlag: item.maindiseFlag,
verificationStatusEnum: item.verificationStatusEnum,
diagnosisDesc: item.diagnosisDesc || '',
diagnosisDoctor: item.diagnosisDoctor || '',
diagnosisTime: formatDateForBackend(item.diagnosisTime),
iptDiseTypeCode: item.iptDiseTypeCode,
syndromeGroupNo: syndromeGroupNo,
},
// 证syndrome
{
name: item.tcmSyndromeName,
ybNo: item.tcmSyndromeCode,
definitionId: item.syndromeDefinitionId || null,
diagSrtNo: null,
syndromeGroupNo: syndromeGroupNo,
},
],
});
}
// 保存中医诊断
tcmList.forEach((item) => {
const syndromeGroupNo = item.conditionId
? `${item.conditionId}-${item.tcmSyndromeCode || Date.now()}`
: `${Date.now()}-${item.tcmSyndromeCode || '0'}`;
savePromises.push(
saveTcmDiagnosis({
patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId,
diagnosisChildList: [
// 病illness
{
conditionId: item.conditionId || null,
name: item.name,
ybNo: item.ybNo,
definitionId: item.definitionId || null,
diagSrtNo: item.diagSrtNo,
medTypeCode: item.medTypeCode,
maindiseFlag: item.maindiseFlag,
verificationStatusEnum: item.verificationStatusEnum,
diagnosisDesc: item.diagnosisDesc || '',
diagnosisDoctor: item.diagnosisDoctor || '',
diagnosisTime: item.diagnosisTime || '',
iptDiseTypeCode: item.iptDiseTypeCode,
syndromeGroupNo: syndromeGroupNo,
},
// 证syndrome
{
name: item.tcmSyndromeName,
ybNo: item.tcmSyndromeCode,
definitionId: item.syndromeDefinitionId || null,
diagSrtNo: null,
syndromeGroupNo: syndromeGroupNo,
},
],
})
);
});
// 所有保存完成后刷新
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
getList();
Promise.all(savePromises).then(() => {
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) {
window.open(res2.data, '_blank');
}
});
}).finally(() => {
setTimeout(() => {
isSaving.value = false;
}, 100);
});
} catch (e) {
console.error('保存诊断失败', e);
proxy.$modal.msgError('保存诊断失败');
} finally {
setTimeout(() => {
isSaving.value = false;
}, 100);
}
})();
}
});
}

View File

@@ -150,9 +150,16 @@ const showApplicationFormDialog = (name) => {
onBeforeMount(() => {});
onMounted(() => {});
const applicationFormNameRef = ref();
const submitApplicationForm = () => {
const submitApplicationForm = async () => {
if (applicationFormNameRef.value?.submit) {
applicationFormNameRef.value.submit();
const success = await applicationFormNameRef.value.submit();
if (success) {
applicationFormDialogVisible.value = false;
applicationFormName.value = null;
setTimeout(() => {
emits('refResh');
}, 500);
}
}
};
const submitOk = () => {

View File

@@ -5,7 +5,20 @@
-->
<template>
<div class="surgery-container">
<div class="search-wrapper">
<el-input
v-model="searchQuery"
placeholder="项目代码/名称/拼音码"
clearable
prefix-icon="Search"
style="width: 480px;"
@input="handleSearch"
@clear="handleClear"
/>
<span class="loaded-count">已加载 {{ applicationList.length }} </span>
</div>
<div
v-loading="loading"
class="transfer-wrapper"
style="min-height: 300px;"
>
@@ -14,10 +27,6 @@
v-model="transferValue"
:data="applicationList"
:titles="['待选择', '已选择']"
:format="leftPanelFormat"
filterable
filter-placeholder="项目代码/名称"
:filter-method="filterMethod"
/>
</div>
<div class="bloodTransfusion-form">
@@ -356,12 +365,6 @@ let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
let doctorCache = null; // 医生列表(含默认主刀医生 ID
const transferRef = ref(null);
const dbTotal = ref(0); // 数据库中的手术项目总数
const checkedCount = computed(() => transferValue.value.length);
const leftPanelFormat = computed(() => ({
noChecked: ` 0/${dbTotal.value}`,
hasChecked: ` ${checkedCount.value}/${dbTotal.value}`,
}));
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0 || id == null || id === '') return null;
@@ -393,6 +396,23 @@ const loading = ref(false);
const applicationList = ref([]);
const applicationListAll = ref([]);
const allLoading = ref(false);
// 搜索关键字(远程搜索用)
const searchQuery = ref('');
let searchTimer = null;
/** 远程搜索:输入关键字后 300ms 防抖,调后端 API 搜索 */
const handleSearch = () => {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
getList(searchQuery.value || '');
}, 300);
};
/** 清除搜索:恢复完整缓存列表 */
const handleClear = () => {
searchQuery.value = '';
getList();
};
// 递归查找默认科室
const findTargetDepartment = (selectProjectIds) => {
if (!selectProjectIds || selectProjectIds.length === 0) return '';
@@ -416,13 +436,12 @@ const getList = async (key) => {
if (!key && surgeryRecordsCache && surgeryMappedCache) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
dbTotal.value = surgeryRecordsCache.length;
return;
}
loading.value = true;
return getSurgeryPage({
pageSize: 1000,
keyword: key || undefined,
pageSize: key ? 500 : 100,
searchKey: key || undefined,
})
.then((res) => {
const records = res.data.records;
@@ -440,25 +459,15 @@ const getList = async (key) => {
.catch((e) => {
console.error('手术项目加载失败:', e);
applicationList.value = [];
dbTotal.value = 0;
loading.value = false;
});
};
/**
* el-transfer 内置过滤方法:支持任意字符即时过滤
* 按项目名称/代码进行前端模糊匹配
*/
const filterMethod = (query, item) => {
const q = query.toLowerCase();
const label = (item.label || '').toLowerCase();
const key = String(item.key || '');
return label.includes(q) || key.includes(q);
};
const mapToTransferItem = (item) => ({
key: String(item.adviceDefinitionId),
label: `${item.adviceName} - ${item.unitCode_dictText || item.unitCode || ''}`,
pyStr: item.pyStr || '',
busNo: item.busNo || '',
disabled: false,
});
@@ -605,26 +614,33 @@ watch(() => transferValue.value, (newValue) => {
});
const submit = () => {
if (transferValue.value.length == 0) {
return proxy.$message.error('请选择手术项目');
proxy.$message.error('请选择手术项目');
return Promise.resolve(false);
}
if (!form.targetDepartment) {
return proxy.$message.error('请选择发往科室');
proxy.$message.error('请选择发往科室');
return Promise.resolve(false);
}
// 新增必填校验
if (!form.surgeryLevel) {
return proxy.$message.error('请选择手术等级');
proxy.$message.error('请选择手术等级');
return Promise.resolve(false);
}
if (!form.anesthesiaType) {
return proxy.$message.error('请选择麻醉方式');
proxy.$message.error('请选择麻醉方式');
return Promise.resolve(false);
}
if (!form.surgerySite) {
return proxy.$message.error('请选择手术部位');
proxy.$message.error('请选择手术部位');
return Promise.resolve(false);
}
if (!form.mainSurgeonId) {
return proxy.$message.error('请选择主刀医生');
proxy.$message.error('请选择主刀医生');
return Promise.resolve(false);
}
if (!form.plannedTime) {
return proxy.$message.error('请选择预定手术时间');
proxy.$message.error('请选择预定手术时间');
return Promise.resolve(false);
}
let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId);
@@ -651,7 +667,7 @@ const submit = () => {
const assistant2Doc = doctorOptions.value.find(d => d.id === form.assistant2Id);
form.assistant2Name = assistant2Doc ? assistant2Doc.name : '';
saveSurgery({
return saveSurgery({
activityList: applicationListAllFilter,
patientId: patientInfo.value.patientId, //患者ID
encounterId: patientInfo.value.encounterId, // 就诊ID
@@ -666,9 +682,14 @@ const submit = () => {
applicationList.value = [];
editingRequestFormId.value = '';
emits('submitOk');
return true;
} else {
proxy.$message.error(res.message);
return false;
}
}).catch((err) => {
console.error('手术申请保存失败:', err);
return false;
});
};
/** 递归规范化树形科室 ID 为字符串,确保与 el-tree-select / findTreeItem 兼容 */
@@ -727,6 +748,19 @@ defineExpose({ state, submit, fillForm, getLocationInfo, getDiagnosisList, getLi
height: 100%;
width: 100%;
.search-wrapper {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.loaded-count {
font-size: 13px;
color: #64748B;
white-space: nowrap;
}
}
.transfer-wrapper {
position: relative;
min-height: 300px;

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="inpatientDoctor-order-container" style="width: 100%">
<div style="margin-bottom: 5px" class="order-operate-btn">
<div style="height: 44px; display: flex; align-items: center; flex: none">
@@ -216,10 +216,14 @@
/>
</el-select>
<el-popover
:popper-style="{ padding: '0' }"
:popper-style="advicePopperStyle"
placement="bottom-start"
popper-class="order-advice-popper"
:offset="0"
:visible="scope.row.showPopover"
:width="1200"
:teleported="true"
:popper-options="advicePopperOptions"
>
<adviceBaseList
ref="adviceTableRef"
@@ -255,9 +259,11 @@
<span v-else>{{ scope.row.adviceName }}</span>
</template>
</vxe-column>
<vxe-column title="状态" align="center" field="" width="90">
<vxe-column title="状态" align="center" field="" width="120">
<template #default="scope">
<el-tag v-if="scope.row.chargeStatus == 5" type="info">
<el-tag v-if="scope.row.statusEnum == 6" type="danger">停止</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 13" type="warning">已停嘱(待核对)</el-tag>
<el-tag v-else-if="scope.row.chargeStatus == 5" type="info">
{{ scope.row.chargeStatus_enumText }}
</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 2" type="success">已签发</el-tag>
@@ -268,7 +274,6 @@
<el-tag v-else-if="scope.row.statusEnum == 10" type="primary">已校对</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 11" type="primary">待接收</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 3" type="success">已校对</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 6" type="danger">停止</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 20" type="success">已完成</el-tag>
<el-tag v-else type="info">{{ scope.row.chargeStatus_enumText }}</el-tag>
</template>
@@ -307,15 +312,17 @@
</vxe-column>
<vxe-column title="频次/用法" align="center" field="" width="180">
<template #default="scope">
<span v-if="!scope.row.isEdit && scope.row.adviceType == 1" style="text-align: right">
<span v-if="!scope.row.isEdit && (scope.row.adviceType == 1 || scope.row.adviceType == 7 || scope.row.adviceType == 8)" style="text-align: right">
{{
[
scope.row.rateCode_dictText,
scope.row.dispensePerDuration ? scope.row.dispensePerDuration + '' : '',
scope.row.methodCode_dictText,
]
.filter(Boolean)
.join(' ')
scope.row.adviceType == 8
? (scope.row.rateCode_dictText || scope.row.rateCode || '-')
: [
scope.row.rateCode_dictText,
scope.row.dispensePerDuration ? scope.row.dispensePerDuration + '' : '',
scope.row.methodCode_dictText,
]
.filter(Boolean)
.join(' ')
}}
</span>
</template>
@@ -495,6 +502,24 @@ const loading = ref(false);
// Bug #587: 标记弹窗刚被关闭的行uniqueKey防止关闭弹窗时误触发行展开
const popoverJustClosedByKey = ref(null);
// 医嘱检索下拉浮框对齐:跟踪表格水平滚动偏移与主体区域边界限制
const tableScrollLeft = ref(0);
const mainBoundary = ref(null);
const advicePopperStyle = computed(() => ({
padding: '0',
marginLeft: `-${tableScrollLeft.value}px`,
}));
const advicePopperOptions = computed(() => ({
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: mainBoundary.value || 'viewport',
},
},
],
}));
// 停嘱弹窗
const stopDialogVisible = ref(false);
const stopForm = reactive({
@@ -588,8 +613,9 @@ const adviceTypeList = computed(() => {
hasShownPharmacyConfigWarning.value = true;
}
// 只返回不需要取药科室配置的类别(诊疗、手术、出院带药)
// 只返回不需要取药科室配置的类别(诊疗、耗材、手术、出院带药)
return [
{ label: '耗材', value: 2, adviceType: 2, categoryCode: '' },
{ label: '诊疗', value: 3, adviceType: 3, categoryCode: '' },
{ label: '手术', value: 6, adviceType: 6, categoryCode: '' },
{ label: '出院带药', value: 7, adviceType: 7, categoryCode: '' },
@@ -617,7 +643,8 @@ const adviceTypeList = computed(() => {
typeList.push({ label: '中草药', value: '1-4', adviceType: 1, categoryCode: '4' });
}
// 始终添加诊疗和手术(它们不受取药配置限制)
// 始终添加诊疗、耗材和手术(它们不受取药配置限制)
typeList.push({ label: '耗材', value: 2, adviceType: 2, categoryCode: '' });
typeList.push({ label: '诊疗', value: 3, adviceType: 3, categoryCode: '' });
typeList.push({ label: '手术', value: 6, adviceType: 6, categoryCode: '' });
@@ -642,6 +669,10 @@ const statusOption = [
label: '已签发',
value: 2,
},
{
label: '已停嘱',
value: 13,
},
{
label: '停止',
value: 6,
@@ -655,11 +686,28 @@ const statusOption = [
// loadingInstance removed - using loading ref instead
onMounted(() => {
document.addEventListener('keydown', escKeyListener);
// 监听表格水平滚动,同步更新医嘱检索下拉浮框的水平偏移
nextTick(() => {
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
if (scrollWrapper) {
scrollWrapper.addEventListener('scroll', onTableScroll, { passive: true });
}
// 获取主体区域容器作为 Popper 边界,防止浮框向左移入患者列表
mainBoundary.value = document.querySelector('.inpatientDoctor-home-main');
});
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', escKeyListener);
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
if (scrollWrapper) {
scrollWrapper.removeEventListener('scroll', onTableScroll);
}
});
function onTableScroll(e) {
tableScrollLeft.value = e.target.scrollLeft || 0;
}
watch(
() => expandOrder.value,
(newValue) => {
@@ -758,6 +806,9 @@ function getListInfo(addNewRow) {
// 🔧 修复:同时保存 orgName当 orgId 在科室树中匹配不到时作为兜底显示
// 优先从科室树查找名称,其次用 positionName后端已保存的科室名最后用 contentJson 中的 orgName
orgName: findOrgName(item.positionId || parsedContent?.orgId || item.orgId) || item.positionName || parsedContent?.orgName || undefined,
// 确保文字医嘱的 rateCode / rateCode_dictText 不被 item 中的 null/undefined 覆盖
rateCode: item.rateCode || parsedContent?.rateCode || undefined,
rateCode_dictText: item.rateCode_dictText || parsedContent?.rateCode_dictText || undefined,
// Bug #589: 从contentJson检测出院带药标记恢复类型显示
// 后端存储时adviceType转为1药品通过prescriptionCategory=3标识出院带药
...(parsedContent?.prescriptionCategory == 3 ? {
@@ -919,6 +970,8 @@ function handleAddPrescription() {
statusEnum: 1,
therapyEnum: '2', // 默认为临时医嘱
startTime: defaultStartTime,
requesterId_dictText: userStore.nickName,
requesterId: userStore.id,
});
getGroupMarkers();
nextTick(() => {
@@ -991,6 +1044,13 @@ function clickRowDb({ row, column, event }) {
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
if (row.adviceType == 8) {
if (!row.orgId && userStore.orgId) {
row.orgId = userStore.orgId;
row.positionId = userStore.orgId;
row.orgName = userStore.orgName || findOrgName(userStore.orgId) || '';
}
}
row.isEdit = true;
const index = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey);
rowIndex.value = index;
@@ -1073,6 +1133,14 @@ function handleDiagnosisChange(item) {
function expandTextRow(rowIndex) {
const row = filterPrescriptionList.value[rowIndex];
if (!row) return;
// 自动获取当前用户所在的科室
if (!row.orgId && userStore.orgId) {
row.orgId = userStore.orgId;
row.positionId = userStore.orgId;
row.orgName = userStore.orgName || findOrgName(userStore.orgId) || '';
}
expandOrder.value = [row.uniqueKey];
nextTick(() => {
if (prescriptionRef.value?.setRowExpand) {
@@ -1083,6 +1151,11 @@ function expandTextRow(rowIndex) {
function handleFocus(row, index) {
rowIndex.value = index;
// 同步表格水平滚动偏移,确保浮框位置正确
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
if (scrollWrapper) {
tableScrollLeft.value = scrollWrapper.scrollLeft || 0;
}
// 文字医嘱(type=8)不弹药品搜索框,直接展开填写面板
const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
if (adviceType == 8) {
@@ -1157,6 +1230,8 @@ function selectAdviceBase(key, row) {
// Bug #589: 出院带药需要保留类型和标志setValue中可能被API数据覆盖
adviceType: prevRow.adviceType || undefined,
dischargeFlag: prevRow.dischargeFlag || undefined,
requesterId_dictText: userStore.nickName,
requesterId: userStore.id,
};
try {
setValue(row);
@@ -1650,6 +1725,8 @@ function handleSaveSign(row, index) {
}
// 更新UI状态
row.requesterId_dictText = userStore.nickName;
row.requesterId = userStore.id;
row.isEdit = false;
isAdding.value = false;
collapseAllExpanded();
@@ -2011,6 +2088,8 @@ function handleSaveGroup(orderGroupList) {
// 创建新的处方项目
const newRow = {
...prescriptionList.value[tempIndex],
requesterId_dictText: userStore.nickName,
requesterId: userStore.id,
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
@@ -2070,6 +2149,8 @@ function handleSaveGroup(orderGroupList) {
function handleSaveHistory(value) {
let saveRow = {
...value,
requesterId_dictText: userStore.nickName,
requesterId: userStore.id,
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
@@ -2236,7 +2317,7 @@ function handleStopAdvice() {
// 找出停嘱的
for (let index = 0; index < selectRows.length; index++) {
const item = selectRows[index];
if (item.statusEnum == 6) {
if (item.statusEnum == 6 || item.statusEnum == 13) {
isStop = false;
break;
}
@@ -2974,4 +3055,9 @@ defineExpose({ getListInfo, getDiagnosisInfo });
background: #fff;
}
}
// 医嘱检索下拉浮框teleported 到 body需用 :global 选择
:global(.order-advice-popper) {
transition: margin-left 0.05s ease-out;
}
</style>

View File

@@ -202,7 +202,7 @@
<vxe-column
title="医嘱状态"
field="requestStatus_enumText"
width="100"
width="110"
align="center"
>
<template #default="scope">
@@ -372,6 +372,7 @@ const REQUEST_STATUS_DISPLAY = {
[RequestStatus.ACTIVE]: '已签发',
[RequestStatus.COMPLETED]: '已校对',
[RequestStatus.STOPPED]: '已停止',
[RequestStatus.PENDING_STOP]: '已停嘱',
};
/** 发药状态 → 医嘱状态映射表 */
@@ -395,17 +396,21 @@ const LEGACY_STATUS_TEXT = {
};
const getStatusDisplayText = (row) => {
// 1. 优先使用发药状态
// 1. 优先使用行级别请求状态:如果已停止或停嘱中,直接显示该状态
const requestCode = Number(row?.requestStatus);
if (requestCode === RequestStatus.STOPPED || requestCode === RequestStatus.PENDING_STOP) {
return REQUEST_STATUS_DISPLAY[requestCode];
}
// 2. 其次使用发药状态
const dispenseCode = Number(row?.dispenseStatus);
if (DISPENSE_STATUS_DISPLAY[dispenseCode]) {
return DISPENSE_STATUS_DISPLAY[dispenseCode];
}
// 2. 使用行级别请求状态
const requestCode = Number(row?.requestStatus);
// 3. 最后回退到其他请求状态
if (REQUEST_STATUS_DISPLAY[requestCode]) {
return REQUEST_STATUS_DISPLAY[requestCode];
}
// 3. 兼容旧后端枚举文本(如"已发送"→"已签发"
// 4. 兼容旧后端枚举文本(如"已发送"→"已签发"
return LEGACY_STATUS_TEXT[row?.requestStatus_enumText] || row?.requestStatus_enumText || '';
};