35 Commits

Author SHA1 Message Date
关羽
2823f8eb05 Fix Bug #481: [住院护士站-医嘱执行] 药品库存充足但执行时提示库存不足
根因: AdviceUtils.checkExeMedInventory() 中硬编码 performLocation == locationId 的匹配条件,
当医嘱的 performLocation 指向的药房没有该药品库存时(库存实际在其他药房),匹配失败导致"库存不足"错误。

修复策略: 采用两步匹配法 -
1. 先按 performLocation 匹配指定药房的库存(添加 null 容错)
2. 若指定药房无匹配,则放宽条件跨所有药房聚合库存

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:14:08 +08:00
赵云
487b05c845 Fix Bug #465: [住院医生工作站-检验申请] 检验项目选择列表被限制为500项,导致医生无法检索并开立其余800多项
根因分析:
1. 前端参数名错误:getList 发送 pageNum 但后端期望 pageNo,导致分页参数被忽略
2. 后端 MyBatis Plus 分页拦截器单页最多返回500条,前端用 pageSize:9999 无效
3. 总共有1300+条检验项目,但只返回了前500条

修复方案:
- 修正参数名为 pageNo 匹配后端接口
- 使用分页循环拉取全部数据(每页500条,循环直到数据全部拉取)
- 添加 try/catch 错误处理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:12:41 +08:00
关羽
71f99da69a Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
修复 handleExecute 中 hasDevice 判断逻辑错误:原代码用 includes('device') 判断
adviceTable 是否包含耗材类医嘱,但 adviceTable 实际取值为 med_medication_request
(药品)或 wor_service_request(诊疗/耗材),均不含 "device" 字符串,导致 hasDevice
恒为 false。

改为检查 adviceTable === 'wor_service_request',确保仅当选中医嘱包含诊疗类(可能
绑定耗材)时才调用 lotNumberMatch,纯药品医嘱不再触发耗材库存校验。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:43:27 +08:00
荀彧
15a65063a3 Fix Bug #470: 住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率
添加模块级缓存(surgeryRecordsCache + surgeryMappedCache),首次打开弹窗请求API后
缓存数据,后续打开直接复用,避免重复请求500条手术项目列表导致加载缓慢。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:18:29 +08:00
关羽
72fdafb032 Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
根因:后端返回字段名为 status,而操作列条件判断使用了 scope.row.billStatus,
billStatus 为 undefined 导致所有状态条件判断失败,仅显示固定的"详情"按钮。
修复:将操作列条件中的 billStatus 统一改为 status。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:14:51 +08:00
关羽
56b8d0e98d Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
- 将requestFormId判空改为requestFormId != null && requestFormId != 0L,防止前端空字符串反序列化为0L误入编辑场景
- 在循环中逐个校验activityList中的项目是否都配置了执行科室,将所有校验前置到任何数据库操作之前,避免部分通过后在save中途抛异常
- 统一使用isEdit标志变量替代重复的null检查

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:12:06 +08:00
赵云
f13734a19c Fix Bug #466: [住院医生工作站-检验申请] 申请单界面缺失核心质控字段(申请类型、标本类型、执行时间)及联动逻辑
修复展示页面字段名称不匹配问题:
- parsePriorityCode 读取 priorityCode 但表单保存为 applicationType,导致列表始终显示"普通"
- labelMap 缺少 executeTime/specimenName/applicationType,导致详情页不显示新增字段
- 详情弹窗中 applicationType 数字编码(0/1)未转换为可读文本(普通/急诊)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:09:37 +08:00
关羽
6081412072 Fix Bug #461: [系统管理-执行科室配置] 保存项目配置后,项目名称回显为ID码,未显示正确名称
根因:getList() 中 filteredOptions 只取 allImplementDepartmentList 的前100项,
当保存的项目ID不在前100项中时,el-select 找不到匹配选项,直接回显ID值而非名称。
修复:在映射记录时,将后端返回的 activityDefinitionId_dictText(项目名称)补充到
filteredOptions 中,确保 el-select 能正确匹配并显示项目名称。
2026-05-14 02:04:24 +08:00
赵云
4a2f13cb19 Fix Bug #444: 【手术管理-门诊手术安排】生成临时医嘱界面,"已引用计费药品"列表过滤非药品项目
根因分析:
- 后端 /doctor-station/advice/request-base-info 接口返回所有类型的医嘱请求数据
- adviceType 字段区分:1=药品, 2=耗材, 3=诊疗项目
- 前端 handleMedicalAdvice 和 handleQuoteBilling 两处过滤逻辑均未按 adviceType 过滤
- 导致手术诊疗项目(如"小腿烧伤扩创交腿皮瓣修复术")和检查项目(如"心脏彩色多普勒超声")出现在"已引用计费药品"列表中

修复方案:
- 在两处 filter 中增加 adviceType !== 1 的过滤条件,只保留药品类型数据
2026-05-14 01:11:44 +08:00
关羽
522bc238aa Fix Bug #441: 门诊手术安排:手术室护士角色进入页面提示"无权限"且"获取卫生机构列表失败"
根因:loadDeptList/loadDoctorList/loadNurseList/loadOperatingRoomList 调用的API
没有设置 skipErrorMsg: true,当手术室护士等角色无权限时,axios响应拦截器会
弹出错误提示。只有 getTenantPageSilent 设置了 skipErrorMsg,其他均未设置。

修复:为所有字典加载API创建静默包装函数(deptTreeSelectSilent/listUserSilent/
listOperatingRoomSilent),统一使用 skipErrorMsg: true 跳过拦截器错误弹窗,
在 catch 块中静默降级为空数组。
2026-05-14 01:07:03 +08:00
关羽
bea2f27b15 Fix Bug #446: 【手术管理-门诊手术安排】临时医嘱生成后界面非法关闭且按钮名称/功能显示不一致
修复 isSignedProp 父组件状态变化未同步到子组件本地 isSigned ref 的问题。
ref(props.isSignedProp) 仅在组件初始化时读取一次,父组件后续更新 temporarySigned
时子组件的 isSigned 不会自动更新,导致按钮文本和签名状态显示不一致。
添加 watch 监听 isSignedProp 变化,确保父子组件签名状态同步。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 00:25:42 +08:00
荀彧
95da4c2a57 Fix Bug #448: 门诊划价模块-项目分类过滤失效,选择"耗材"类型时仍能检索出药品
根因:getList() 直接发送 queryParams.value 到后端,其中包含 undefined 值的
属性(如 adviceType 未选择时)。后端接收 adviceType=null 后回退到查询所有类型
(List.of(1,2,3)),导致药品出现在耗材的搜索结果中。

修复:显式构建 requestParams 对象,仅包含已定义的过滤参数(adviceType、
categoryCode、searchKey),避免 undefined 值被发送到后端。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 00:21:06 +08:00
赵云
53e3e9c4c0 Fix Bug #403: 住院医生工作站:应用医嘱组套后,药品明细字段内容丢失未正确引入表格
根因分析:
1. orderGroupDrawer.vue 中 handleUseOrderGroup 的 mergedDetail 对象缺少
   categoryCode、minUnitCode、doseUnitCode、partPercent、partAttributeEnum、
   unitConversionRatio、defaultLotNumber 等关键字段,导致 setValue 和价格计算逻辑失效
2. 使用 || 替代 ?? 作为数字字段(如 doseQuantity=0)的回退操作符,导致值为 0 时被错误覆盖
3. handleSaveGroup 中价格计算使用 item.unitCode 查找 unitInfo,但 item.unitCode 可能为
   undefined,而 setValue 已正确填充了 prescriptionList 中的 unitCode

修复内容:
- mergedDetail 先展开 orderDetail(包含所有药品基础字段),再用组套用户覆盖值覆盖
- 所有数字字段回退从 || 改为 ??,确保 0 值不被覆盖
- 新增 doseQuantity 的 ?? 回退逻辑到 orderDetail.doseQuantity
- 新增 groupId、groupOrder、orgId、orgName、therapyEnum 到 mergedDetail
- handleSaveGroup 使用 baseRow 变量避免对象自引用问题
- 价格计算使用 newRow.unitCode(已由 setValue 填充)而非 item.unitCode

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 00:19:05 +08:00
关羽
55eba1a0b1 Fix Bug #443: 手术计费:点击"签发"耗材时异常报错
**根因分析**:
前端 handleSave 签发时,仅从 contentJson 解析数据后发送,缺少 encounterId、patientId、locationId 等关键字段。这些字段在 API 响应顶层(不在 contentJson 中),导致后端签发时字段为空。

**修复方案**:
- 前端:在 handleSave 的 saveList 映射中补充 encounterId、patientId、locationId(对应 positionId)、adviceType 等顶层关键字段
- 后端:handDevice 中 locationId 为空时,优先查询已有 DeviceRequest 的 performLocation 作为回退,避免覆盖为默认科室
2026-05-14 00:11:29 +08:00
关羽
1525740ab5 Fix Bug #435: 门诊手术安排编辑弹窗费用类别字段数据未回显
根因:op_schedule 表缺少 fee_type 字段,导致编辑时无法从数据库读取费用类别。
- 新增 fee_type 列到 op_schedule 表并回填历史数据
- OpSchedule 实体类新增 feeType 字段
- 详情查询SQL改为直接从 op_schedule.fee_type 读取,替代原脆弱的 fin_contract 关联链

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 00:06:34 +08:00
关羽
96102e8b64 Fix Bug #401: div_log 表中 pool_id 与 slot_id 存值不符
根因分析:
1. 候选池API(getCurrentDayEncounter)未返回 poolId/slotId 字段,导致护士从候选池
   添加患者到队列时 poolId/slotId 始终为 null
2. triage_queue_item 中 pool_id/slot_id 为 null 后,完诊时 div_log 也写入 null 值
3. 医生站完诊和护士站完诊各自独立写入 div_log,导致同一患者产生两条 COMPLETE 记录

修复方案:
1. CurrentDayEncounterTencentDto 新增 poolId/slotId 字段
2. TencentAppMapper.xml 通过 encounter.order_id → order_main.slot_id →
   adm_schedule_slot.pool_id 链路获取正确的号源信息
3. DoctorStationMainAppServiceImpl 增加防重复逻辑:队列项已为 COMPLETED 状态时
   不再重复写入 div_log(说明护士站已处理过)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:27:08 +08:00
关羽
49df72121f Fix Bug #412: 门诊医生站传染病报告卡保存失败,提示报错
后端 saveInfectiousDiseaseReport 中 getCardNo().trim() 存在空指针异常风险,
增加 cardNo 空值校验避免 NPE 导致保存失败。
前端 buildSubmitData 中 diseaseCode 在 selectedClassA/B/C 为空时会变成 null,
增加从 selectedDiseases 兜底取值逻辑确保 diseaseCode 始终有值。
2026-05-13 23:18:53 +08:00
荀彧
65d1716ca9 Fix Bug #413: 医生个人报卡管理核心缺陷:医生个人报卡编辑/查看界面与门诊医生站登记报卡界面设计不统一
根因:infectiousDiseaseReportDialog.vue 中所有输入框使用了 class="underline-input" 但缺少对应的CSS定义,
导致输入框显示为 Element Plus 默认的完整边框样式,而非预期的下划线样式。
下拉框(underline-select)和地址选择器(address-selects)有对应的下划线CSS定义,但普通输入框没有,
造成编辑/查看界面与登记界面的排版视觉不一致。

修复:新增 .underline-input CSS 类定义,与 .underline-select 保持一致的下划线样式。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 22:16:37 +08:00
关羽
509a0a788f Fix Bug #442: 手术计费:点击"删除"待签发耗材时异常报错,导致操作失败
根因:删除按钮的 handleDelete 函数缺少 bizRequestFlag 过滤逻辑,
与签发/签退按钮的处理逻辑不一致。当用户勾选非本人创建的医嘱(bizRequestFlag != '1')
时,前端仍将其加入删除列表发送至后端,后端尝试删除不存在的 DeviceRequest 记录时报错。

修复:在 handleDelete 的 deleteList 构建中增加 bizRequestFlag 过滤,
排除非本人创建的医嘱,与 changeCheck/handleSave/handleSingOut 逻辑保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 22:14:23 +08:00
关羽
6c4a8e3c14 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
**后端开发重点**:优先搜索 Java/Spring 后端代码。
关键词:Controller, Service, Mapper, API, 接口, 数据查询
搜索目录:openhis-server-new/src/, his-repo/src/

修复三个问题:
1. 单位字段显示为空:getUnitCodeOptions 中 unitCode/minUnitCode 未做 String() 转换,与 selectUnitCode(String类型)比较时类型不匹配导致 el-select 无法正确选中;unitCodeChange 中同样存在类型比较问题
2. 执行科室/位置默认值未生效:departmentOptions 在 onMounted 异步加载,用户快速点击项目时数据尚未加载完成导致默认 positionId 为空;增加 watch 监听科室/位置选项加载完成,自动补填默认值;弹窗打开时重新加载确保数据最新
3. 下拉模糊搜索不生效:优化 filterOptions/getFilteredOptions 过滤逻辑,增加拼音首字母(pyStr)搜索支持
2026-05-13 20:55:38 +08:00
荀彧
7ada54510d Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
修复搜索功能失效:loadGroupSets 未传递 groupSetSearchText 搜索关键字,
导致搜索框和搜索按钮点击后无任何过滤效果。新增客户端过滤逻辑,
在API返回数据后根据搜索关键字对组套名称进行过滤,同时保持searchKey
参数传递以便后端后续扩展支持。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:31:25 +08:00
荀彧
1602615820 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:31:12 +08:00
关羽
af15f2ae06 Fix Bug #502: 【住院护士站-汇总发药申请】顶部医嘱类型(长期/临时)过滤按钮点击无响应
补充修复:汇总视图(SummaryMedicineList)未接收 therapyEnum 参数,
导致切换到"汇总"tab 后长期/临时过滤按钮失效。

修复内容:
1. SummaryMedicineList 新增 therapyEnum prop
2. getMedicineSummary 调用时传递 therapyEnum 参数
3. index.vue 将 therapyEnum 传入 SummaryMedicineList 组件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:22:27 +08:00
荀彧
b1d5ae97b1 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
修复策略A(直接修复代码逻辑),采用4个手术式修改消除抖动根因:

1. 方法列表区域 v-if → v-show:避免异步加载后 DOM 突然插入导致高度跳变
2. CSS transition: all → height/max-height:明确过渡属性,防止子元素意外动画
3. .collapse-scroll 添加 min-height: 120px:固定最小高度,避免 flex 容器高度突变
4. handleCategoryExpand 添加 currentActiveCategory 守卫:快速切换分类时忽略过期请求响应,防止旧数据覆盖导致闪烁

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:12:06 +08:00
关羽
e5cd7bd792 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因:/reg-doctorstation/request-form/get-check 接口只接收 encounterId 参数,前端传递的 startDate/endDate/status 筛选参数被后端完全忽略,导致日期范围和状态筛选不生效。
修复:Controller 增加 startDate、endDate、status、keyword 参数,透传至已有的重载 Service 方法(SQL 和 DTO 中 status 字段已存在)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:08:41 +08:00
关羽
294d7a5d11 Fix Bug #499: 【住院医生工作站-检查申请】检查申请列表缺失查询过滤功能,不符合临床高效检索要求
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:06:30 +08:00
荀彧
e78a32a5ec Fix Bug #494: 住院医生工作站-检查申请:"申请单名称"字段显示为通用名称,未展示具体检查项目名称
根因:提交检查申请单时,applicationListAllFilter 的 map 映射未包含 adviceName 字段,
导致 name 构造为 undefined、undefined,保存到数据库为空字符串。
修复:改为从 applicationListAll(原始数据)中按 adviceDefinitionId 查找项目名称。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:21:46 +08:00
赵云
dc9f47c534 Fix Bug #489: 【医嘱闭环】医生站签发单条长期药品医嘱,护士校对界面生成重复(两条)待校对记录
根因分析:
1. SQL查询JOIN倍增:selectInpatientAdvicePage 中多个LEFT JOIN(如adm_encounter_location、
   adm_encounter_participant、med_medication)可能产生重复行,外层查询无DISTINCT去重
2. 防重复逻辑缺陷:handMedication() 中仅对 DbOpType.INSERT 进行去重,若同一请求中包含
   INSERT 和 UPDATE(编辑后签发的医嘱),两者uniqueKey相同但UPDATE不受去重限制,
   导致 UPDATE 修改旧记录 + INSERT 创建新记录 = 两条数据

修复方案:
1. 在AdviceProcessAppMapper.xml的UNION两侧子查询中添加SELECT DISTINCT,防止JOIN倍增
2. 在AdviceManageAppServiceImpl.java中移除dbOpType限制,对所有医嘱(INSERT+UPDATE)
   统一按uniqueKey去重,确保同一业务医嘱只处理一次

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:19:47 +08:00
关羽
5bf1e4151c Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
根因:el-popover 使用 append-to-body 将浮层渲染到 body 下,而 prescriptionlist 组件
使用 v-if="showChargeDialog" 控制渲染。当 closeChargeDialog() 通过 nextTick 将
showChargeDialog 置为 false 时,Vue 立即销毁组件,但 append-to-body 的 popover DOM
元素因关闭过渡动画尚未完成而残留在 body 下。

修复:
1. 移除 prescriptionlist 上的 v-if="showChargeDialog",让组件保持挂载状态
   (外层 el-dialog 的 v-model 已控制可见性,v-if 冗余)
2. closeChargeDialog() 同步关闭弹窗并清空数据,不再使用 nextTick 延迟

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:15:41 +08:00
关羽
b7365b6b06 Fix Bug #488: 【临床医嘱】双击编辑待签发医嘱,医嘱类型回显为数字且点击确认报接口错误
1. getRowSelectValue: 校验行数据是否在选项列表中,不存在时返回undefined避免el-select回显原始数字
2. filterPrescriptionList: 复合值'1-2'过滤时提取adviceType部分比较,避免类型过滤失效
3. handleSaveSign: 严格校验itemNo非空且为有效字符串,trim检查并显式String()转换,避免后端报缺参错误

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:07:45 +08:00
关羽
c5c3bcae34 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:医嘱录入框(el-input)缺少@input事件绑定,导致用户输入关键字时不会触发handleChange搜索,
只有点击/回车时才会刷新搜索结果。对比门诊医生站prescriptionlist.vue(第624行)有@input="handleChange",
住院医生站order/index.vue遗漏了此事件绑定。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:07:12 +08:00
关羽
bacddc6d3f Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因分析(与testApplication.vue对比发现):
1. getLocationInfo不是async函数,handleViewDetail中使用new Promise手动包装getDepartmentList
   作为降级方案,如果API调用失败则Promise永远不resolve(缺少catch),导致后续逻辑挂起
2. recursionFun缺少空值保护和break语句,可能在找到匹配后继续无效遍历

修复:
- getLocationInfo改为async/await模式(与testApplication.vue保持一致)
- handleViewDetail使用await getLocationInfo()替代不可靠的Promise包装
- recursionFun增加空值提前返回和break优化
2026-05-13 18:19:53 +08:00
荀彧
31cac09126 Fix Bug #476: 检查申请单详情界面缺失紧急程度、过敏史、检查目的等核心字段
在 examineApplication.vue 的 labelMap 中补充 urgencyLevel、allergyHistory、
examinationPurpose、expectedExaminationTime、medicalHistorySummary、allergyConfirmed
共6个缺失字段的中文标签映射,并新增 transformField 函数将 urgencyLevel 的
emergency/routine 转换为"急诊"/"普通"显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:18:04 +08:00
荀彧
abc3bdd0c0 Fix Bug #478: 【住院医生工作站-检验申请】点击"详情"查看检验单时,"发往科室"字段回显异常(显示为"-")
根因:testApplication.vue 中 getLocationInfo() 调用了 getDepartmentList(),
但该函数未从 '@/api/public.js' 导入。第192行错误地导入了未使用的 getOrgList,
导致运行时 ReferenceError,orgOptions 始终为空,recursionFun() 返回空字符串,
最终 targetDepartment 显示为 "-"。

修复:将未使用的 getOrgList 导入替换为正确的 getDepartmentList 导入。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:17:01 +08:00
赵云
0f85e95d24 Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
根因:saveRequestForm 方法中,执行科室配置校验(activityOrganizationConfig.isEmpty)位于 saveOrUpdate(requestForm) 之后,导致即使校验失败抛出异常,RequestForm 记录已被写入数据库。
修复:将校验逻辑移至方法开头,在任何数据库操作之前执行,确保校验失败时不产生任何脏数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:13:10 +08:00
31 changed files with 655 additions and 187 deletions

View File

@@ -1539,11 +1539,22 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId());// 开方人科室
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间
// 发放耗材房若前端未传locationId使用登录用户科室作为默认值
// 发放耗材房若前端未传locationId优先沿用已有DeviceRequest的performLocation否则使用登录用户科室)
Long locId = adviceSaveDto.getLocationId();
if (locId == null) {
locId = SecurityUtils.getLoginUser().getOrgId();
log.info("耗材locationId为空使用登录用户科室作为默认值: locationId={}", locId);
// 尝试从已有DeviceRequest获取原始的performLocation
if (adviceSaveDto.getRequestId() != null) {
DeviceRequest existingDevice = iDeviceRequestService.getById(adviceSaveDto.getRequestId());
if (existingDevice != null && existingDevice.getPerformLocation() != null) {
locId = existingDevice.getPerformLocation();
log.info("耗材locationId为空使用已有DeviceRequest的performLocation: locationId={}", locId);
}
}
// 如果已有记录也没有performLocation则使用登录用户科室作为兜底
if (locId == null) {
locId = SecurityUtils.getLoginUser().getOrgId();
log.info("耗材locationId为空且无已有记录使用登录用户科室作为默认值: locationId={}", locId);
}
}
deviceRequest.setPerformLocation(locId);
deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id

View File

@@ -580,7 +580,11 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
@Override
public R<?> saveInfectiousDiseaseReport(InfectiousDiseaseReportDto infectiousDiseaseReportDto) {
// 检查卡片编号唯一性(新增时检查,编辑时排除当前记录)
String cardNo = infectiousDiseaseReportDto.getCardNo().trim();
String cardNo = infectiousDiseaseReportDto.getCardNo();
if (cardNo == null || cardNo.trim().isEmpty()) {
return R.fail("卡片编号不能为空");
}
cardNo = cardNo.trim();
LambdaQueryWrapper<InfectiousDiseaseReport> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(InfectiousDiseaseReport::getCardNo, cardNo);
long count = iInfectiousDiseaseReportService.count(queryWrapper);

View File

@@ -343,19 +343,24 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
}
// 写入 div_log 审计日志(独立于队列项,确保每次完诊都生成记录)
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog divLog = new DivLog()
.setPoolId(divPoolId)
.setSlotId(divSlotId)
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction("COMPLETE")
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(divLog);
} catch (Exception e) {
log.error("写入div_log审计日志失败", e);
// 防重复:若队列项已是 COMPLETED 状态,说明护士站已处理过并写过分诊日志,不再重复写入
boolean queueAlreadyCompleted = queueItem != null
&& TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus());
if (!queueAlreadyCompleted) {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog divLog = new DivLog()
.setPoolId(divPoolId)
.setSlotId(divSlotId)
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction("COMPLETE")
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(divLog);
} catch (Exception e) {
log.error("写入div_log审计日志失败", e);
}
}
// 4. 更新状态、完成时间以及初复诊标识

View File

@@ -178,15 +178,26 @@ public class AdviceUtils {
// 生命提示信息集合
List<String> tipsList = new ArrayList<>();
for (MedicationRequestUseExe medicationRequestUseExe : medUseExeList) {
// 聚合同一位置所有批次的库存总量
// 第一步:按 performLocation 匹配指定药房的库存
List<AdviceInventoryDto> matchedInventories = adviceInventory.stream()
.filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId())
&& CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable())
&& medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId())
&& (medicationRequestUseExe.getPerformLocation() == null
|| medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId()))
// 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件
&& (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber())
|| medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber())))
.collect(Collectors.toList());
// 第二步:如果指定药房没有匹配到库存,则放宽条件查询所有药房的库存
if (matchedInventories.isEmpty()) {
matchedInventories = adviceInventory.stream()
.filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId())
&& CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable())
// 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件
&& (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber())
|| medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber())))
.collect(Collectors.toList());
}
// 匹配到库存信息
if (!matchedInventories.isEmpty()) {
// 聚合所有批次的可用库存

View File

@@ -341,7 +341,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重,避免签发单条长期医嘱时产生重复记录
// 防重复保存:对所有医嘱进行去重(包括 INSERT 和 UPDATE 混合场景),避免签发单条医嘱时产生重复记录
Set<String> longUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> longUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : longInsertOrUpdateList) {
@@ -351,10 +351,10 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
+ adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && longUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复长期医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}",
if (longUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复长期医嘱(跨操作类型),跳过 - patientId={}, encounterId={}, adviceDefinitionId={}, dbOpType={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose());
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDbOpType());
continue;
}
longUniqueKeySet.add(uniqueKey);
@@ -429,7 +429,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重
// 防重复保存:对所有医嘱进行去重(包括 INSERT 和 UPDATE 混合场景),避免签发时产生重复记录
Set<String> tempUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> tempUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : tempInsertOrUpdateList) {
@@ -439,10 +439,10 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
+ adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && tempUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复临时医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}",
if (tempUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复临时医嘱(跨操作类型),跳过 - patientId={}, encounterId={}, adviceDefinitionId={}, dbOpType={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose());
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDbOpType());
continue;
}
tempUniqueKeySet.add(uniqueKey);

View File

@@ -76,6 +76,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 申请单ID前端空字符串可能反序列化为0L需同时判0
Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L;
// 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
@@ -83,12 +87,23 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
throw new ServiceException("请先配置当前时间段的执行科室");
}
// 逐个校验activityList中的项目是否都配置了执行科室避免部分通过后在循环中抛异常导致事务复杂化
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
if (activityList != null && !activityList.isEmpty()) {
for (ActivitySaveDto activitySaveDto : activityList) {
Long positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
}
}
// 诊疗处方号
String prescriptionNo;
// 申请单ID
Long requestFormId = requestFormSaveDto.getRequestFormId();
// 编辑场景
if (requestFormId != null) {
if (isEdit) {
RequestForm requestFormInfo = iRequestFormService.getById(requestFormId);
prescriptionNo = requestFormInfo.getPrescriptionNo();
// 该申请单存在的待发送医嘱个数
@@ -132,7 +147,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
iRequestFormService.saveOrUpdate(requestForm);
// 编辑场景时,先删除掉原有诊疗项目及账单再新增
if (requestFormId != null) {
if (isEdit) {
List<Long> serviceRequestIds = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo))
.stream().map(ServiceRequest::getId).collect(Collectors.toList());
@@ -146,8 +161,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
ServiceRequest serviceRequest;
ChargeItem chargeItem;
// 诊疗集合
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
for (ActivitySaveDto activitySaveDto : activityList) {

View File

@@ -82,14 +82,23 @@ public class RequestFormManageController {
* 查询检查申请单
*
* @param encounterId 就诊id
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检查项目名称模糊匹配)
* @return 检查申请单
*/
@GetMapping(value = "/get-check")
public R<?> getCheckRequestForm(@RequestParam(required = false) Long encounterId) {
public R<?> getCheckRequestForm(
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) {
return R.fail("就诊ID不能为空");
}
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode()));
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode(), startDate, endDate, status, keyword));
}
/**

View File

@@ -138,4 +138,12 @@ public class CurrentDayEncounterTencentDto {
*/
private String englishName;
/** 号源池ID用于分诊队列 div_log 审计日志) */
@JsonSerialize(using = ToStringSerializer.class)
private Long poolId;
/** 号源槽位ID用于分诊队列 div_log 审计日志) */
@JsonSerialize(using = ToStringSerializer.class)
private Long slotId;
}

View File

@@ -89,15 +89,12 @@
cs.apply_doctor_name AS apply_doctor_name,
drf.create_time AS apply_time,
os.surgery_nature AS surgeryType,
fc.contract_name AS feeType,
os.fee_type AS feeType,
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
LEFT JOIN adm_organization o ON cs.org_id = o.id
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id AND ae.delete_flag = '0'
LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0'
LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0'
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
LEFT JOIN (
SELECT patient_id, identifier_no

View File

@@ -155,7 +155,7 @@
ii.performer_check_id,
ii.category_code,
ii.dispense_status
FROM (( SELECT T1.encounter_id,
FROM (( SELECT DISTINCT T1.encounter_id,
T1.tenant_id,
#{medMedicationRequest} AS advice_table,
T1.id AS request_id,
@@ -293,7 +293,7 @@
T1.sort_number,
T1.group_id )
UNION
( SELECT T1.encounter_id,
( SELECT DISTINCT T1.encounter_id,
T1.tenant_id,
#{worServiceRequest} AS advice_table,
T1.id AS request_id,

View File

@@ -27,7 +27,9 @@
T9.payment_id,
T9.picture_url,
T9.birth_date,
t9.english_name
t9.english_name,
t9.slot_id,
t9.pool_id
from (
SELECT T1.tenant_id AS tenant_id,
T1.id AS encounter_id,
@@ -51,7 +53,9 @@
T13.id AS payment_id,
ai.picture_url AS picture_url,
T8.birth_date AS birth_date,
tx.staff_english_name AS english_name
tx.staff_english_name AS english_name,
om_slot.slot_id AS slot_id,
om_slot.pool_id AS pool_id
FROM adm_encounter AS T1
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
@@ -91,6 +95,8 @@
AND T13.status_enum = ${paymentStatus}
LEFT JOIN adm_invoice AS ai
ON ai.reconciliation_id = T13.id AND ai.delete_flag = '0'
LEFT JOIN order_main AS om ON T1.order_id = om.id AND om.delete_flag = '0'
LEFT JOIN adm_schedule_slot AS om_slot ON om.slot_id = om_slot.id
WHERE T1.delete_flag = '0'
AND T1.class_enum = #{classEnum}
AND T10.context_enum = #{register}

View File

@@ -193,6 +193,9 @@ public class OpSchedule extends HisBaseEntity {
/** 外请专家姓名 */
private String externalExpertName;
/** 费用类别 */
private String feeType;
/** 备注信息 */
private String remark;

View File

@@ -226,8 +226,14 @@ function getList() {
getDiagnosisTreatmentList(queryParams.value).then((res) => {
loading.value = false;
catagoryList.value = res.data.records.map(record => {
// 为每一行初始化 filteredOptions确保显示框能正确显示项目名称
const filteredOptions = allImplementDepartmentList.value.slice(0, 100);
// 确保后端返回的项目名称选项存在于 filteredOptions 中,避免 el-select 因找不到选项而回显为 ID
if (record.activityDefinitionId && !filteredOptions.some(o => o.value === record.activityDefinitionId)) {
filteredOptions.push({
value: record.activityDefinitionId,
label: record.activityDefinitionId_dictText || record.activityDefinitionId
});
}
return {
...record,
loading: false,

View File

@@ -115,22 +115,44 @@ function getList() {
console.log('[adviceBaseList] getList() 跳过:未选择患者');
return; // 不执行API调用
}
// 只有在弹窗打开时才执行查询
if (!props.popoverVisible) {
console.log('[adviceBaseList] getList() 跳过:弹窗未打开');
return;
}
queryParams.value.organizationId = props.patientInfo.orgId;
console.log('[adviceBaseList] getList() 请求参数:', JSON.stringify(queryParams.value));
getAdviceBaseInfo(queryParams.value).then((res) => {
// 🔧 Bug #448 修复:显式构建请求参数,确保 adviceType 正确传递
// 不直接使用 queryParams.value避免 undefined 值被发送到后端导致过滤失效
const requestParams = {
pageSize: queryParams.value.pageSize,
pageNum: queryParams.value.pageNum,
organizationId: props.patientInfo.orgId,
};
// 只在 adviceType 有值时添加0 是无效值undefined/null 会导致后端查询所有类型)
if (queryParams.value.adviceType != null && queryParams.value.adviceType !== 0) {
requestParams.adviceType = queryParams.value.adviceType;
}
// 只在 categoryCode 有值时添加
if (queryParams.value.categoryCode) {
requestParams.categoryCode = queryParams.value.categoryCode;
}
// 只在 searchKey 有值时添加
if (queryParams.value.searchKey) {
requestParams.searchKey = queryParams.value.searchKey;
}
console.log('[adviceBaseList] getList() 请求参数:', JSON.stringify(requestParams));
getAdviceBaseInfo(requestParams).then((res) => {
console.log('[adviceBaseList] getList() 响应数据:', {
total: res.data?.total,
recordsCount: res.data?.records?.length || 0,
firstRecord: res.data?.records?.[0]?.adviceName || '无数据',
adviceType: queryParams.value.adviceType
adviceType: requestParams.adviceType
});
adviceBaseList.value = res.data.records || [];
total.value = res.data.total || 0;

View File

@@ -887,6 +887,10 @@ function handleDelete() {
if (item.statusEnum != 1 || item.chargeStatus == 5) {
return null;
}
// 🔧 Bug #442: 非本人创建的医嘱不允许删除(与签发/签退逻辑保持一致)
if (Number(item.bizRequestFlag) !== 1 && item.bizRequestFlag) {
return null;
}
// 🔧 Bug #442: 已保存的行必须有有效的 requestId否则跳过避免后端删除不存在的记录
if (item.requestId == null || item.requestId === undefined || item.requestId === '') {
return null;
@@ -896,7 +900,7 @@ function handleDelete() {
dbOpType: '3',
adviceType: item.adviceType,
};
}).filter(item => item !== null); // 过滤掉已签发、已收费或无 requestId 的项目
}).filter(item => item !== null); // 过滤掉已签发、已收费、非本人创建或无 requestId 的项目
if (deleteList.length == 0) {
proxy.$modal.msgWarning('只能删除待签发且未收费的项目');
@@ -1032,6 +1036,14 @@ function handleSave() {
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
// 🔧 Bug #443: 补充顶层关键字段(这些不在 contentJson 中,需从 API 响应顶层提取)
encounterId: item.encounterId,
patientId: item.patientId,
locationId: item.positionId,
adviceType: item.adviceType,
adviceTableName: item.adviceTableName,
adviceDefinitionId: item.adviceDefinitionId,
chargeItemId: item.chargeItemId,
};
});
savePrescriptionSign({

View File

@@ -1366,6 +1366,9 @@ async function buildSubmitData() {
} else if (formData.otherDisease) {
// 其他传染病使用自定义编码
diseaseCode = 'OTHER';
} else if (formData.selectedDiseases && formData.selectedDiseases.length > 0) {
// 兜底:如果 ClassA/B/C 都为空但 selectedDiseases 有值,取第一个作为 diseaseCode
diseaseCode = formData.selectedDiseases[0];
}
// 转换年龄单位:岁=1, 月=2, 天=3
@@ -1775,6 +1778,33 @@ defineExpose({ show, showReport, close: handleClose });
color: #999;
}
/* 输入框下划线样式(与 underline-select 保持一致) */
.underline-input :deep(.el-input__wrapper) {
border: none;
border-bottom: 1px solid #dcdfe6;
border-radius: 0;
box-shadow: none;
background: transparent;
}
.underline-input :deep(.el-input__wrapper:hover) {
border-bottom-color: #c0c4cc;
}
.underline-input :deep(.el-input__wrapper.is-focus) {
border-bottom-color: #409eff;
}
.underline-input :deep(.el-input__inner) {
font-size: 12px;
color: #666;
}
.underline-input :deep(.el-input__inner::placeholder) {
font-size: 12px;
color: #999;
}
/* 街道下拉框下划线样式 */
.underline-select {
width: 100%;

View File

@@ -3,15 +3,56 @@
<!-- ====== 顶部卡片申请单列表 ====== -->
<div class="top-section">
<div class="section-header">
<span class="section-title">检查项目 ({{ applicationList.length }})</span>
<span class="section-title">检查项目 ({{ filteredApplicationList.length }})</span>
<div class="header-actions">
<el-button type="primary" @click="handleAdd" icon="Plus">新增</el-button>
<el-button type="success" @click="handleSave" icon="Finished">保存</el-button>
</div>
</div>
<!-- Bug #499: 查询过滤工具栏 -->
<div class="search-toolbar">
<el-form :inline="true" size="small">
<el-form-item label="日期范围">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable style="width: 140px">
<el-option
v-for="opt in statusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关键字">
<el-input
v-model="searchForm.keyword"
placeholder="申请单号 / 检查项目"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" icon="Search">搜索</el-button>
<el-button @click="handleResetSearch" icon="Refresh">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table
v-loading="loading"
:data="applicationList"
:data="filteredApplicationList"
:max-height="200"
highlight-current-row
@row-click="handleRowClick"
@@ -335,8 +376,9 @@
加载中...
</div>
<!-- Bug #428修复: 渲染分类联动加载的检查方法列表 -->
<!-- Bug #500修复: v-if 改为 v-show避免方法列表加载时 DOM 突然插入导致高度跳变 -->
<div
v-if="cat.methods && cat.methods.length > 0"
v-show="cat.methods && cat.methods.length > 0"
class="method-section"
>
<div class="method-section-title">检查方法</div>
@@ -452,6 +494,74 @@ const activeDetailTab = ref('applyForm');
const applicationList = ref([]);
const selectedItems = ref([]);
// Bug #499: 查询过滤状态
const searchForm = reactive({
dateRange: [],
applyStatus: '',
keyword: ''
});
// 申请单状态选项
const statusOptions = [
{ label: '已开单', value: 0 },
{ label: '已收费', value: 1 },
{ label: '已预约', value: 2 },
{ label: '已签到', value: 3 },
{ label: '部分报告', value: 4 },
{ label: '已完告', value: 5 },
{ label: '已作废', value: 6 }
];
// Bug #499: 过滤后的申请单列表
const filteredApplicationList = computed(() => {
let result = applicationList.value;
// 日期范围过滤
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
const start = searchForm.dateRange[0];
const end = searchForm.dateRange[1];
result = result.filter(item => {
const d = item.applyTime;
if (!d) return false;
const dateStr = d.length > 10 ? d.substring(0, 10) : d;
return dateStr >= start && dateStr <= end;
});
}
// 状态过滤
if (searchForm.applyStatus !== '' && searchForm.applyStatus !== null && searchForm.applyStatus !== undefined) {
result = result.filter(item => item.applyStatus === searchForm.applyStatus);
}
// 关键字过滤(申请单号、申检部位、检查项目名)
if (searchForm.keyword) {
const kw = searchForm.keyword.toLowerCase();
result = result.filter(item => {
return (item.applyNo || '').toLowerCase().includes(kw)
|| (item.inspectionArea || '').toLowerCase().includes(kw);
});
}
return result;
});
// Bug #499: 搜索与重置
function handleSearch() {
// 过滤逻辑由 computed 自动处理
}
function handleResetSearch() {
const now = new Date();
const end = now.toISOString().substring(0, 10);
const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().substring(0, 10);
searchForm.dateRange = [start, end];
searchForm.applyStatus = '';
searchForm.keyword = '';
}
// 初始化默认日期范围为近一周
handleResetSearch();
// 🔧 BugFix#426: 懒加载套餐明细
async function loadPackageDetails(row, treeNode, resolve) {
if (!row.isPackage || !row.packageId) {
@@ -573,6 +683,7 @@ const categoryList = ref([]); // 原始分类+项目数据
const dictSearchKey = ref('');
const activeNames = ref(''); // 当前展开的折叠项
const categoryLoadingSet = ref(new Set()); // Bug #500: 正在加载方法的分类集合
const currentActiveCategory = ref(null); // Bug #500: 记录当前激活的分类,忽略过期请求响应
const isAnimating = ref(false); // Bug #500: 防止快速切换时折叠动画重叠导致抖动
const allMethods = ref([]);
@@ -690,15 +801,18 @@ const availableMethods = computed(() => {
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
// #428: 分类展开时联动加载检查方法
// Bug #500: 使用 categoryLoadingSet 替代 dictLoading避免切换分类时整个区域闪烁
// Bug #500: 添加 currentActiveCategory 守卫,忽略过期请求响应,防止快速切换时数据闪烁
async function handleCategoryExpand(cat) {
if (!cat || !cat.typeName) return;
// 如果已加载过或正在加载中,跳过
if ((cat.methods && cat.methods.length > 0) || categoryLoadingSet.value.has(cat.typeId)) return;
categoryLoadingSet.value.add(cat.typeId);
currentActiveCategory.value = cat.typeId;
try {
const res = await searchCheckMethod({ checkType: cat.typeName });
// 忽略过期请求:用户已切换到其他分类,丢弃当前响应
if (currentActiveCategory.value !== cat.typeId) return;
let data = res?.data?.data || res?.data || res?.rows || res;
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
data = res.data.data;
@@ -717,18 +831,22 @@ async function handleCategoryExpand(cat) {
}));
}
} catch (err) {
if (currentActiveCategory.value !== cat.typeId) return;
console.error('加载分类检查方法失败', err);
} finally {
categoryLoadingSet.value.delete(cat.typeId);
}
}
// Bug #500: 添加防抖逻辑,快速切换时跳过中间状态的动画,避免高度跳变和白屏闪烁
// Bug #500修复: 添加防抖逻辑,快速切换时跳过中间状态的动画,避免高度跳变和白屏闪烁
function handleCollapseChange(activeName) {
if (isAnimating.value) return; // 动画进行中,忽略后续点击
isAnimating.value = true;
setTimeout(() => { isAnimating.value = false; }, 300); // 与 CSS 过渡时长一致
// Bug #500修复: 记录当前激活的分类,用于 handleCategoryExpand 中忽略过期请求
currentActiveCategory.value = activeName || null;
if (activeName) {
// Bug #428修复: 直接从 categoryList原始响应式数组查找分类
// 确保后续 handleCategoryExpand 对 cat.methods 的赋值能正确触发 Vue 响应式更新
@@ -1443,6 +1561,19 @@ defineExpose({ getList });
gap: 8px;
}
/* Bug #499: 查询过滤工具栏 */
.search-toolbar {
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
}
.search-toolbar :deep(.el-form-item) {
margin-bottom: 8px;
}
.search-toolbar :deep(.el-form-item__label) {
font-size: 12px;
}
/* 底部区域:左表单 + 右分类 */
.bottom-section {
display: flex;
@@ -1520,6 +1651,7 @@ defineExpose({ getList });
flex: 1;
overflow-y: auto;
overflow-x: hidden; /* Bug #500: 防止切换时水平方向溢出导致抖动 */
min-height: 120px; /* Bug #500: 固定最小高度,避免分类切换时 flex 容器高度突变 */
}
.empty-hint {
color: #909399;
@@ -1792,10 +1924,10 @@ defineExpose({ getList });
height: auto;
line-height: 1.5;
}
/* Bug #500: 折叠内容添加平滑过渡动画,避免切换时高度跳变 */
/* Bug #500修复: 折叠内容使用明确属性过渡,避免 transition: all 导致子元素意外动画 */
:deep(.el-collapse-item__content) {
padding-bottom: 4px;
transition: all 0.3s ease;
transition: height 0.3s ease, max-height 0.3s ease;
}
/* Bug #500: 折叠面板动画容器,添加 overflow:hidden 防止展开时内容溢出导致闪烁 */
:deep(.el-collapse-item__wrap) {

View File

@@ -589,36 +589,46 @@ function handleUseOrderGroup(row) {
minUnitPrice: orderDetail.minUnitPrice,
inventoryList: orderDetail.inventoryList || [],
priceList: orderDetail.priceList || [],
partPercent: orderDetail.partPercent || 1,
partPercent: orderDetail.partPercent ?? 1,
partAttributeEnum: orderDetail.partAttributeEnum,
unitConversionRatio: orderDetail.unitConversionRatio,
// 🔧 Bug #218 修复positionId 可能存储在 item 本身,优先使用 item.positionId
positionId: item.positionId || orderDetail.positionId,
positionId: item.positionId ?? orderDetail.positionId,
defaultLotNumber: orderDetail.defaultLotNumber,
// 单位信息
unitCode: item.unitCode || orderDetail.unitCode,
unitCode: item.unitCode ?? orderDetail.unitCode,
categoryCode: item.categoryCode ?? orderDetail.categoryCode,
unitCodeName: item.unitCodeName || orderDetail.unitCode_dictText,
minUnitCode: orderDetail.minUnitCode,
doseUnitCode: orderDetail.doseUnitCode,
// 合并后的完整对象(用于 setValue
// 先展开 orderDetail 获取所有药品基础字段categoryCode、minUnitCode、doseUnitCode、
// partPercent、partAttributeEnum、unitConversionRatio、defaultLotNumber 等),
// 再用组套用户覆盖值覆盖,确保单次剂量/频次/用法/用药天数/总量等不被丢失
mergedDetail: {
...orderDetail,
adviceName: orderDetail.adviceName || item.orderDefinitionName || '未知项目',
adviceType: orderDetail.adviceType,
quantity: item.quantity,
unitCode: item.unitCode || orderDetail.unitCode,
unitCode: item.unitCode ?? orderDetail.unitCode,
categoryCode: item.categoryCode ?? orderDetail.categoryCode,
unitCodeName: item.unitCodeName,
dose: item.dose || orderDetail.dose,
rateCode: item.rateCode || orderDetail.rateCode,
methodCode: item.methodCode || orderDetail.methodCode,
dispensePerDuration: item.dispensePerDuration || orderDetail.dispensePerDuration,
doseQuantity: item.doseQuantity,
inventoryList: orderDetail.inventoryList || [],
priceList: orderDetail.priceList || [],
partPercent: orderDetail.partPercent || 1,
// 🔧 Bug #218 修复positionId 可能存储在 item 本身,优先使用 item.positionId
positionId: item.positionId || orderDetail.positionId,
defaultLotNumber: orderDetail.defaultLotNumber,
dose: item.dose ?? orderDetail.dose,
rateCode: item.rateCode ?? orderDetail.rateCode,
methodCode: item.methodCode ?? orderDetail.methodCode,
dispensePerDuration: item.dispensePerDuration ?? orderDetail.dispensePerDuration,
doseQuantity: item.doseQuantity ?? orderDetail.doseQuantity,
// 🔧 Bug #218 / #403 修复positionId 可能存储在 item 本身,优先使用 item.positionId
positionId: item.positionId ?? orderDetail.positionId,
// 执行科室:优先使用组套明细中保存的 orgId
orgId: item.orgId ?? orderDetail.orgId,
orgName: item.orgName ?? orderDetail.orgName,
// 组号(保留组套中的分组信息)
groupId: item.groupId,
groupOrder: item.groupOrder,
therapyEnum: item.therapyEnum ?? orderDetail.therapyEnum ?? '1',
}
};
});

View File

@@ -1133,13 +1133,17 @@ function submitForm() {
// 新增手术
addSurgery(form.value).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请提交成功!')
proxy.$modal.msgSuccess('手术申请提交成功!')
// 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false
emit('saved') // 通知父组件刷新医嘱列表
// 刷新手术申请列表
getList()
// 刷新手术申请列表,使用 nextTick 确保数据一致性
proxy.$nextTick(() => {
if (props.patientInfo?.encounterId) {
getList()
}
})
} else {
proxy.$modal.msgError(res.msg || '新增手术失败,请检查表单信息')
}
@@ -1151,13 +1155,17 @@ function submitForm() {
// 修改手术
updateSurgery(form.value).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请修改成功!')
proxy.$modal.msgSuccess('手术申请修改成功!')
// 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false
emit('saved') // 通知父组件刷新医嘱列表
// 刷新手术申请列表
getList()
// 刷新手术申请列表,使用 nextTick 确保数据一致性
proxy.$nextTick(() => {
if (props.patientInfo?.encounterId) {
getList()
}
})
} else {
proxy.$modal.msgError(res.msg || '更新手术失败,请检查表单信息')
}

View File

@@ -137,7 +137,7 @@
<el-descriptions title="申请单描述" :column="2">
<template v-for="(value, key) in descJsonData" :key="key">
<el-descriptions-item v-if="isFieldMatched(key)" :label="getFieldLabel(key)">
{{ value || '-' }}
{{ transformField(key, value) || '-' }}
</el-descriptions-item>
</template>
</el-descriptions>
@@ -270,6 +270,12 @@ const parseStatus = (status) => {
const labelMap = {
categoryType: '项目类别',
targetDepartment: '发往科室',
urgencyLevel: '紧急程度',
allergyHistory: '过敏史',
examinationPurpose: '检查目的',
expectedExaminationTime: '期望检查时间',
medicalHistorySummary: '病史摘要',
allergyConfirmed: '过敏确认',
symptom: '症状',
sign: '体征',
clinicalDiagnosis: '临床诊断',
@@ -278,6 +284,17 @@ const labelMap = {
attention: '注意事项',
};
// Fields that need value transformation before display
const transformField = (key, value) => {
if (key === 'urgencyLevel') {
return value === 'emergency' ? '急诊' : '普通';
}
if (key === 'allergyConfirmed') {
return value === true || value === 'true' ? '已口头确认' : '未确认';
}
return value;
};
const isFieldMatched = (key) => {
return key in labelMap;
};
@@ -292,49 +309,46 @@ const hasMatchedFields = computed(() => {
});
/** 查询科室 */
const getLocationInfo = () => {
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
});
const getLocationInfo = async () => {
const res = await getDepartmentList();
orgOptions.value = res.data || [];
};
const recursionFun = (targetDepartment) => {
if (!targetDepartment) return '';
let name = '';
for (let index = 0; index < orgOptions.value.length; index++) {
const obj = orgOptions.value[index];
if (obj.id == targetDepartment) {
name = obj.name;
break;
}
const subObjArray = obj['children'];
if (subObjArray && subObjArray.length > 0) {
for (let index = 0; index < subObjArray.length; index++) {
const item = subObjArray[index];
for (let i = 0; i < subObjArray.length; i++) {
const item = subObjArray[i];
if (item.id == targetDepartment) {
name = item.name;
break;
}
}
}
if (name) break;
}
return name;
};
const handleViewDetail = async (row) => {
console.log('targetDepartment========>', JSON.stringify(row));
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row;
// 解析 descJson
if (row.descJson) {
try {
const obj = JSON.parse(row.descJson);
// 确保科室数据已加载
if (!orgOptions.value || orgOptions.value.length === 0) {
await new Promise((resolve) => {
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
resolve();
});
});
}
obj.targetDepartment = recursionFun(obj.targetDepartment);
descJsonData.value = obj;
} catch (e) {

View File

@@ -103,11 +103,11 @@
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="160">
<template #default="scope">
<template v-if="scope.row.billStatus == 0 || scope.row.status == 0">
<template v-if="scope.row.status == 0">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
<template v-else-if="scope.row.billStatus == 1 || scope.row.status == 1">
<template v-else-if="scope.row.status == 1">
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
</template>
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
@@ -187,7 +187,7 @@ import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js';
import {getInspection, deleteRequestForm, withdrawRequestForm} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js';
import {getDepartmentList} from '@/api/public.js';
const { proxy } = getCurrentInstance();
@@ -285,6 +285,9 @@ const labelMap = {
otherDiagnosis: '其他诊断',
relatedResult: '相关结果',
attention: '注意事项',
applicationType: '申请类型',
specimenName: '标本类型',
executeTime: '执行时间',
};
/**
@@ -311,8 +314,8 @@ const parsePriorityCode = (descJson) => {
if (!descJson) return '-';
try {
const obj = JSON.parse(descJson);
// priorityCode: 0-普通, 1-急
return obj.priorityCode === 1 ? '急' : '普通';
// applicationType: 0-普通, 1-急
return obj.applicationType === 1 ? '急' : '普通';
} catch (e) {
console.error('解析 descJson 失败:', e);
return '-';
@@ -391,6 +394,9 @@ const handleViewDetail = async (row) => {
try {
const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment);
// 转换申请类型编码为可读文本
if (obj.applicationType === 0) obj.applicationType = '普通';
else if (obj.applicationType === 1) obj.applicationType = '急诊';
descJsonData.value = obj;
} catch (e) {
console.error('解析 descJson 失败:', e);

View File

@@ -144,42 +144,56 @@ const applicationListAll = ref();
const applicationList = ref();
const loading = ref(false);
const orgOptions = ref([]); // 科室选项
const getList = () => {
const getList = async () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationList.value = [];
return;
}
loading.value = true;
getApplicationList({
pageSize: 9999,
pageNum: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], //1 药品 2耗材 3诊疗
})
.then((res) => {
if (res.code === 200) {
applicationListAll.value = res.data.records;
applicationList.value = res.data.records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
console.log('applicationList========>', JSON.stringify(res.data.records));
} else {
try {
const allRecords = [];
let currentPage = 1;
const pageSize = 500;
// 分页拉取全部数据后端单页最多500条
while (true) {
const res = await getApplicationList({
pageSize,
pageNo: currentPage,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
});
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationList.value = [];
return;
}
})
.finally(() => {
loading.value = false;
const records = res.data?.records || [];
allRecords.push(...records);
// 当前页不足 pageSize 或已无数据,说明已全部拉取
if (records.length < pageSize) break;
currentPage++;
}
applicationListAll.value = allRecords;
applicationList.value = allRecords.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
} catch (e) {
proxy.$message.error('获取检验项目列表失败');
applicationList.value = [];
} finally {
loading.value = false;
}
};
const transferValue = ref([]);
const form = reactive({

View File

@@ -483,7 +483,10 @@ const submit = () => {
encounterId: patientInfo.value.encounterId,
organizationId: patientInfo.value.inHospitalOrgId,
requestFormId: '',
name: applicationListAllFilter.map(item => item.adviceName).join('、'),
name: transferValue.value.map(id => {
const item = applicationListAll.value?.find(i => i.adviceDefinitionId === id);
return item?.adviceName || '';
}).filter(Boolean).join('、'),
descJson: JSON.stringify(submitForm),
categoryEnum: '2',
}).then((res) => {

View File

@@ -86,6 +86,9 @@ import {getApplicationList, saveSurgery} from './api';
import {ElMessage} from 'element-plus';
const { proxy } = getCurrentInstance();
// 模块级缓存:避免每次打开弹窗都重新请求手术项目列表
let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
@@ -110,6 +113,12 @@ const getList = () => {
applicationList.value = [];
return;
}
// 命中缓存时直接使用,避免重复请求导致加载缓慢
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
return;
}
loading.value = true;
getApplicationList({
pageSize: 500,
@@ -132,6 +141,9 @@ const getList = () => {
key: item.adviceDefinitionId,
};
});
// 写入模块缓存,后续打开弹窗直接复用
surgeryRecordsCache = res.data.records;
surgeryMappedCache = applicationList.value;
} else {
console.warn('获取手术项目列表失败:', res.message);
applicationList.value = [];

View File

@@ -197,6 +197,7 @@
style="width: 62%"
v-model="scope.row.adviceName"
placeholder="请选择项目"
@input="handleChange"
@click="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown="
@@ -709,9 +710,17 @@ function loadConfiguredCategories() {
// 数据过滤
const filterPrescriptionList = computed(() => {
const pList = prescriptionList.value.filter((item) => {
// 修复 Bug #488orderClassCode 可能是复合值 '1-2',需提取 adviceType 部分进行比较
let matchAdviceType = true;
if (orderClassCode.value) {
const filterAdviceType = String(orderClassCode.value).includes('-')
? parseInt(String(orderClassCode.value).split('-')[0])
: orderClassCode.value;
matchAdviceType = filterAdviceType == item.adviceType;
}
return (
(!therapyEnum.value || therapyEnum.value == item.therapyEnum) &&
(!orderClassCode.value || orderClassCode.value == item.adviceType) &&
matchAdviceType &&
(!orderStatus.value || (orderStatus.value == item.statusEnum && item.requestId))
);
});
@@ -739,12 +748,28 @@ function getRowDisabled(row) {
/**
* 将行的 adviceType + categoryCode 映射为 el-select 的选中值
* 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值
* 修复 Bug #488当行的 adviceType 在当前配置中找不到匹配选项时,返回最接近的可用值,避免回显为纯数字
*/
function getRowSelectValue(row) {
if (row.adviceType == 1 && row.categoryCode) {
return '1-' + row.categoryCode;
const compositeValue = '1-' + row.categoryCode;
// 检查复合值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === compositeValue)) {
return compositeValue;
}
// 配置的 categoryCode 已变更,回退到第一个药品选项
const firstPharmacy = adviceTypeList.value.find(item => String(item.value).startsWith('1-'));
if (firstPharmacy) {
return firstPharmacy.value;
}
return row.adviceType;
}
return row.adviceType;
// 诊疗/手术等非药品类型,检查其值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === row.adviceType)) {
return row.adviceType;
}
// 不在选项中的值(如已废弃的 adviceType返回 undefined 让 el-select 显示为空
return undefined;
}
// 新增医嘱
@@ -1319,11 +1344,12 @@ function handleCancelEdit(row, index) {
function handleSaveSign(row, index) {
if (row.adviceType != 2) {
// 修复 Bug #488严格校验 itemNo确保非空且为有效字符串才发起请求
let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId;
if (!itemNo) {
if (!itemNo || String(itemNo).trim() === '') {
console.warn('绑定设备检查跳过itemNo为空adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + '');
} else {
getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => {
getBindDevice({ typeCode: row.adviceType, itemNo: String(itemNo) }).then((res) => {
if (res.data.length == 0) {
return;
}
@@ -1613,20 +1639,14 @@ function handleSaveGroup(orderGroupList) {
// 创建新的处方项目
// 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail已由 setValue 填充完整数据)
// 先取 setValue 填充的行数据作为基础
const baseRow = prescriptionList.value[rowIndex.value];
const newRow = {
...prescriptionList.value[rowIndex.value],
...baseRow,
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
quantity: item.quantity ?? mergedDetail.quantity,
methodCode: item.methodCode ?? mergedDetail.methodCode,
rateCode: item.rateCode ?? mergedDetail.rateCode,
dispensePerDuration: item.dispensePerDuration ?? mergedDetail.dispensePerDuration,
dose: item.dose ?? mergedDetail.dose,
doseQuantity: item.doseQuantity ?? mergedDetail.doseQuantity,
executeNum: 1,
unitCode: item.unitCode ?? mergedDetail.unitCode,
unitCode_dictText: item.unitCodeName || mergedDetail.unitCodeName || '',
statusEnum: 1,
orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时保存 orgName确保树匹配不到时仍有中文名称可显示
@@ -1635,19 +1655,33 @@ function handleSaveGroup(orderGroupList) {
conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value,
encounterDiagnosisId: encounterDiagnosisId.value,
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || '1',
therapyEnum: baseRow?.therapyEnum || '1',
};
// 覆盖关键字段:优先使用 item 的值,其次 mergedDetail已由 setValue 填充),最后 baseRow
newRow.quantity = item.quantity ?? mergedDetail.quantity ?? baseRow.quantity;
newRow.methodCode = item.methodCode ?? mergedDetail.methodCode ?? baseRow.methodCode;
newRow.rateCode = item.rateCode ?? mergedDetail.rateCode ?? baseRow.rateCode;
newRow.dispensePerDuration = item.dispensePerDuration ?? mergedDetail.dispensePerDuration ?? baseRow.dispensePerDuration;
newRow.dose = item.dose ?? mergedDetail.dose ?? baseRow.dose;
newRow.doseQuantity = item.doseQuantity ?? mergedDetail.doseQuantity ?? baseRow.doseQuantity;
newRow.unitCode = item.unitCode ?? mergedDetail.unitCode ?? baseRow.unitCode;
newRow.unitCode_dictText = item.unitCodeName || mergedDetail.unitCodeName || baseRow.unitCode_dictText || '';
// 计算价格和总量
const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode);
// 🔧 Bug #403 修复:使用 newRow.unitCode已由 setValue 填充)而非 item.unitCode
// 使用 ?? 替代 || 计算 partPercent确保值为 0 时不会被错误替换
const finalUnitCode = newRow.unitCode;
const unitInfo = unitCodeList.value.find((k) => k.value == finalUnitCode);
const finalQuantity = newRow.quantity;
const partPercent = item.orderDetailInfos?.partPercent ?? mergedDetail.partPercent ?? baseRow.partPercent ?? 1;
if (unitInfo && unitInfo.type == 'minUnit') {
newRow.price = newRow.minUnitPrice;
newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity;
newRow.totalPrice = ((finalQuantity || 0) * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = finalQuantity || 0;
} else {
newRow.price = newRow.unitPrice;
newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity * (item.orderDetailInfos?.partPercent || mergedDetail.partPercent || 1);
newRow.totalPrice = ((finalQuantity || 0) * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = (finalQuantity || 0) * partPercent;
}
newRow.contentJson = JSON.stringify(newRow);

View File

@@ -481,12 +481,43 @@ watch(
(visible) => {
if (visible) {
executeTime.value = formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss');
// 弹窗打开时重新加载科室和位置选项,确保数据最新
loadDepartmentOptions();
getDiseaseInitLoc(16);
} else {
resetData();
}
}
);
// 监听科室选项加载完成,为已添加的诊疗项目设置默认执行科室
watch(
() => departmentOptions.value,
(depts) => {
if (!depts || depts.length === 0) return;
feeItemsList.value.forEach(item => {
if (item.adviceType === 3 && !item.positionId) {
const patientOrgId = props.patientInfo.organizationId;
const matched = depts.find(d => String(d.id) === String(patientOrgId));
item.positionId = matched ? String(matched.id) : String(depts[0].id);
}
});
}
);
// 监听位置选项加载完成,为已添加的耗材项目设置默认位置
watch(
() => locationOptions.value,
(locs) => {
if (!locs || locs.length === 0) return;
feeItemsList.value.forEach(item => {
if (item.adviceType === 2 && !item.positionId) {
item.positionId = String(locs[0].value);
}
});
}
);
// 加载科室选项
function loadDepartmentOptions() {
getOrgList()
@@ -527,23 +558,27 @@ function getDiseaseInitLoc() {
locationOptions.value = [];
});
}
// 下拉框模糊搜索过滤
// 下拉框模糊搜索过滤自定义filter-method配合element-plus filterable使用
function filterOptions(val, row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey;
filterKeywords.value[key] = val;
if (!val || val.trim() === '') {
delete filterKeywords.value[key];
} else {
filterKeywords.value[key] = val.toLowerCase();
}
}
function getFilteredOptions(row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey;
const keyword = filterKeywords.value[key];
const options = optionsKey === 'departmentOptions' ? departmentOptions.value : locationOptions.value;
if (!keyword || keyword.trim() === '') {
if (!keyword) {
return options;
}
const lower = keyword.toLowerCase();
return options.filter(item => {
const name = (item.name || item.label || '').toLowerCase();
const id = String(item.id || item.value || '').toLowerCase();
return name.includes(lower) || id.includes(lower);
const py = (item.pyStr || '').toLowerCase();
return name.includes(keyword) || id.includes(keyword) || py.includes(keyword);
});
}
// 获取组套类型文本
@@ -553,9 +588,9 @@ function getItemType_Text(type) {
}
function getUnitCodeOptions(row) {
const unitCodes = [
{ code: row.unitCode, codeText: row.unitCode_dictText },
{ code: row.minUnitCode, codeText: row.minUnitCode_dictText },
];
{ code: String(row.unitCode), codeText: row.unitCode_dictText },
{ code: String(row.minUnitCode), codeText: row.minUnitCode_dictText },
].filter(item => item.code);
// 使用 Set 来跟踪已经存在的 code
const seenCodes = new Set();
const uniqueUnitCodes = unitCodes.filter((item) => {
@@ -575,11 +610,11 @@ function unitCodeChange(row) {
// 获取价格
const price = row.priceList?.[0]?.price || 0;
// 根据选择的单位调整单价
if (row.selectUnitCode === row.unitCode) {
// 根据选择的单位调整单价(统一用字符串比较)
if (String(row.selectUnitCode) === String(row.unitCode)) {
// 如果选择的是大单位 (如 "")
row.unitPrice = price.toFixed(6); // 单价就是原价
} else if (row.selectUnitCode === row.minUnitCode) {
} else if (String(row.selectUnitCode) === String(row.minUnitCode)) {
// 如果选择的是小单位 (如 "")
row.unitPrice = (price / (row.partPercent || 1)).toFixed(6); // 单价 = 原价 / 拆零比
}
@@ -771,15 +806,32 @@ function openGroupSetDialog() {
function loadGroupSets() {
groupSetLoading.value = true;
getOrderGroup({ organizationId: orgId.value })
const params = { organizationId: orgId.value };
// 传递搜索关键字,后端 /group-package-for-order 虽不直接支持 searchKey
// 但保持参数传递以便后续扩展
if (groupSetSearchText.value && groupSetSearchText.value.trim()) {
params.searchKey = groupSetSearchText.value.trim();
}
getOrderGroup(params)
.then((res) => {
const data = res?.data || {};
let rawList = [];
if (groupSetRange.value === 1) {
groupSetList.value = data.personalList || [];
rawList = data.personalList || [];
} else if (groupSetRange.value === 2) {
groupSetList.value = data.organizationList || [];
rawList = data.organizationList || [];
} else {
groupSetList.value = data.hospitalList || [];
rawList = data.hospitalList || [];
}
// 客户端过滤:根据搜索关键字过滤组套名称
const keyword = groupSetSearchText.value?.trim()?.toLowerCase();
if (keyword) {
groupSetList.value = rawList.filter(item => {
const name = (item.name || item.Name || '').toLowerCase();
return name.includes(keyword);
});
} else {
groupSetList.value = rawList;
}
})
.catch(() => {

View File

@@ -66,13 +66,21 @@ const props = defineProps({
type: Number,
default: 1,
},
therapyEnum: {
type: Number,
default: undefined,
},
});
handleGetPrescription();
function handleGetPrescription() {
loading.value = true;
let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(',');
getMedicineSummary({}).then((res) => {
const params = {};
if (props.therapyEnum !== undefined) {
params.therapyEnum = props.therapyEnum;
}
getMedicineSummary(params).then((res) => {
medicineSummaryFormList.value = res.data.records;
loading.value = false;
});

View File

@@ -85,7 +85,7 @@
:deadline="deadline"
:therapyEnum="therapyEnum"
/>
<SummaryMedicineList v-else />
<SummaryMedicineList v-else :therapyEnum="therapyEnum" />
<!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick">
<el-tab-pane
v-for="tab in prescriptionTabs"

View File

@@ -471,11 +471,13 @@ function handleExecute() {
console.log(list, 'list');
adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => {
if (res.code == 200) {
// 仅当选中医嘱中包含耗材类医嘱时,才调用耗材批号匹配(排除纯药品医嘱场景)
const hasDevice = list.some((item) =>
String(item.adviceTable || '').includes('device'),
// 仅当选中医嘱中包含诊疗类医嘱(可能绑定耗材)时,才调用耗材批号匹配
// adviceTable 取值为 med_medication_request药品或 wor_service_request诊疗/耗材)
// 原代码用 includes('device') 判断有误,两个表名均不含 "device" 字符串
const hasServiceRequest = list.some((item) =>
String(item.adviceTable || '') === 'wor_service_request',
);
if (hasDevice) {
if (hasServiceRequest) {
lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error);
});

View File

@@ -829,7 +829,7 @@
</el-descriptions>
</div>
<div style="padding: 10px">
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef"
<prescriptionlist :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1"
:sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div>
@@ -881,16 +881,13 @@ import {
deleteSurgerySchedule,
getSurgeryScheduleDetail
} from '@/api/surgicalschedule'
import { listUser } from '@/api/system/user'
import { deptTreeSelect } from '@/api/system/user'
import { listOperatingRoom } from '@/api/operatingroom'
import { getSurgeryPage} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getContract } from '@/views/inpatientDoctor/home/components/api.js'
import request from '@/utils/request'
import SurgeryCharge from '../charge/surgerycharge/index.vue'
import TemporaryMedical from './temporaryMedical.vue'
// 静默获取卫生机构列表(跳过拦截器错误提示,手术室护士等角色可能无此权限)
// 静默获取字典列表(跳过拦截器错误提示,手术室护士等角色可能无此权限)
function getTenantPageSilent(query) {
return request({
url: '/system/tenant/page',
@@ -900,6 +897,36 @@ function getTenantPageSilent(query) {
})
}
// 静默获取科室树(跳过拦截器错误提示)
function deptTreeSelectSilent(params = {}) {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: { typeEnum: 2, ...params },
skipErrorMsg: true
})
}
// 静默获取用户列表(跳过拦截器错误提示)
function listUserSilent(query) {
return request({
url: '/base-data-manage/practitioner/user-practitioner-page',
method: 'get',
params: query,
skipErrorMsg: true
})
}
// 静默获取手术室列表(跳过拦截器错误提示)
function listOperatingRoomSilent(query) {
return request({
url: '/base-data-manage/operating-room/list',
method: 'get',
params: query,
skipErrorMsg: true
})
}
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(true)
@@ -1148,7 +1175,7 @@ function loadOrgList() {
// 加载科室列表
function loadDeptList() {
deptTreeSelect()
deptTreeSelectSilent()
.then(res => {
if (res.code === 200) {
const tree = res.data?.records || res.data || []
@@ -1168,7 +1195,7 @@ function loadDeptList() {
// 加载医生列表
function loadDoctorList() {
listUser({ pageNo: 1, pageSize: 1000 })
listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1188,7 +1215,7 @@ function loadDoctorList() {
// 加载护士列表
function loadNurseList() {
listUser({ pageNo: 1, pageSize: 1000 })
listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1208,7 +1235,7 @@ function loadNurseList() {
// 加载手术室列表
function loadOperatingRoomList() {
listOperatingRoom({ pageNo: 1, pageSize: 1000, statusEnum: 1 })
listOperatingRoomSilent({ pageNo: 1, pageSize: 1000, statusEnum: 1 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1434,12 +1461,10 @@ function closeChargeDialog() {
if (prescriptionRef.value && prescriptionRef.value.closeAllPopovers) {
prescriptionRef.value.closeAllPopovers()
}
// 等 Vue 完成 DOM 更新后再关闭弹窗,确保 popover 先消失
nextTick(() => {
showChargeDialog.value = false
chargePatientInfo.value = {}
chargeSurgeryInfo.value = {}
})
// 清空数据,避免下次打开时使用缓存
showChargeDialog.value = false
chargePatientInfo.value = {}
chargeSurgeryInfo.value = {}
}
// 🔧 新增:标志位,用于区分是"打开"还是"刷新"
@@ -1515,6 +1540,8 @@ function handleMedicalAdvice(row) {
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== row.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
@@ -1773,10 +1800,12 @@ function handleQuoteBilling() {
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
return medicineName && medicineName.trim() !== '';

View File

@@ -312,6 +312,13 @@ const getMethodCodeDict = computed(() => {
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)
// 🔧 修复 Bug #446: 同步父组件 isSignedProp 的变化到本地 isSigned
// ref(props.isSignedProp) 只在初始化时读取一次,父组件后续更新不会自动同步
watch(() => props.isSignedProp, (newVal) => {
isSigned.value = newVal
})
const signatureTime = ref('')
const showSignDialog = ref(false)
const signPassword = ref('')