Compare commits

..

125 Commits

Author SHA1 Message Date
赵云
80695d90f9 Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
根因:saveRequestForm 方法中,执行科室配置校验(activityOrganizationConfig.isEmpty)位于 saveOrUpdate(requestForm) 之后,导致即使校验失败抛出异常,RequestForm 记录已被写入数据库。
修复:将校验逻辑移至方法开头,在任何数据库操作之前执行,确保校验失败时不产生任何脏数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:12:18 +08:00
关羽
deb6ade97b Fix Bug #472: 住院医生工作站-手术申请单:勾选手术项目无效,导致无法正常开立医嘱
根因分析:surgery.vue 的 el-transfer 组件存在两处与其他正常组件(bloodTransfusion.vue、laboratoryTests.vue)不一致的地方:
1. v-loading 被放置在了 transfer-wrapper 内部的额外 div 上,导致 Element Plus 的加载遮罩层可能与穿梭框交互层产生遮挡冲突
2. applicationList 和 applicationListAll 初始化为 ref([]),而其他组件使用 ref(),导致 Vue 响应式更新时穿梭框内部状态追踪存在差异

修复:
- 将 v-loading 直接放到 transfer-wrapper div 上,去除多余的嵌套 div,与 bloodTransfusion/laboratoryTests 保持一致
- 将 applicationListAll 和 applicationList 的初始化从 ref([]) 改为 ref()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:12:17 +08:00
荀彧
2d7228ca5d Fix Bug #470: 住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率
性能优化:
1. 数据库层: 为 wor_activity_definition 表添加手术项目专用覆盖索引 (idx_wor_activity_def_surgery_covering)
   - 查询从 Index Scan + Filter 改为 Index Only Scan
   - 执行时间从 4.6ms 降至 0.67ms (约7倍提升)
2. Java服务层: 过滤 chargeItemDefinitionIdList 中的 null 值
   - 手术项目无定价定义,原逻辑会将 null 传入批量查询造成浪费
   - 同时跳过 childCharge 批次定价查询(仅药品/耗材需要)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:10:44 +08:00
关羽
a447e55d43 Fix Bug #471: 手术管理-门诊手术安排:手术申请查询结果中混入住院检验申请单数据(脏数据)
根因:getSurgeryRequestFormPage 接口构建 RequestFormDto 时 typeCode 传了 null,
导致 SQL 中 type_code 过滤条件被跳过,查出所有申请单(包括 typeCode=22 的住院检验申请单)。
修复:传入 ActivityDefCategory.PROCEDURE.getCode()(值为"24")作为 typeCode,
确保只查询手术类型的申请单。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:06:09 +08:00
1be0dc2417 bug515 [住院医生站-临床医嘱] 点击“签发”检验医嘱后系统陷入死循环,Loading无法消失 后端advice_type映射错误 2026-05-13 17:49:27 +08:00
a4b4d36d93 bug512 [住院护士站-汇总发药申请] “全选”开关功能失效,点击后下方医嘱明细未能联动勾选 2026-05-13 17:48:26 +08:00
关羽
940fad5c7d Fix Bug #464: [目录管理-诊疗目录] 新增项目时"零售价"未与"诊疗子项"合计总价自动同步
- calculateTotalPrice: 使用 nextTick 确保 Vue 响应式时序正确,零售价与总价同步
- calculateTotalPrice: 修复判断条件,使用 adviceDefinitionId 而非 retailPrice/childrenRequestNum(可能为0)
- calculateTotalPrice: 使用 Number() 替代 parseFloat/parseInt 统一类型转换
- selectRow: 使用 nextTick 确保 DOM 更新后再计算总价,避免时序问题
- edit/reset: treatmentItems 初始化补充缺失的 name 字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 17:21:53 +08:00
关羽
ead3733aac Fix Bug #466: [住院医生工作站-检验申请] 申请单界面缺失核心质控字段(申请类型、标本类型、执行时间)及联动逻辑
在检验申请单弹窗表单中新增三个核心质控字段:
- 申请类型:单选按钮,区分普通/急诊,默认普通
- 标本类型:下拉选择框,支持血液/尿液/痰液等10种标本,默认血液
- 执行时间:日期时间选择器,支持预约未来时间执行

新字段通过 descJson 序列化传递,展示页已支持解析显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 17:19:51 +08:00
关羽
28bf385ec2 Fix Bug #463: [目录管理-诊疗目录] 新增/编辑弹窗中"诊疗子项"检索功能失效,无法搜到已维护的项目
根因分析:medicineList.vue 的搜索功能仅做本地过滤,数据受限于初始加载的1000条(pageSize: 1000),
输入搜索关键词时不会向后端发起新的请求,导致不在前1000条内的项目无法被搜到。

修复策略:
1. medicineList.vue:当用户输入搜索关键词时,新增 searchList() 走服务端搜索(调用 API 的 searchKey 参数),
   使用 pageSize: 5000 提高搜索覆盖率;搜索词清空时恢复使用预加载数据
2. 放宽 childrenJson 过滤条件,从严格的 == null 改为兼容空字符串(== null || === '')
3. 修复 diagnosisTreatmentDialog.vue 中 medicineList 的重复 import
2026-05-13 17:17:02 +08:00
关羽
31f7c4f32a Fix Bug #455: 门诊医生站-医嘱:开立诊疗医嘱时执行科室默认获取逻辑有误且显示为原始ID
根因分析:
- setValue函数对所有类型统一设置orgId=row.positionId
- 诊疗项目(adviceType=3)的positionId来自adm_organization_location配置表
- 当配置ID不在当前机构树中时,findOrgNameById返回空,el-tree-select显示原始ID
- 后续if(!orgId)判断因已有值而不触发回退到患者就诊科室

修复方案:
- 诊疗类型(adviceType=3)跳过positionId赋值,使用患者就诊科室作为默认执行科室
- 同时修复syncGroupFields中会覆盖orgId的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 16:13:35 +08:00
关羽
480663f716 Fix Bug #454: 门诊医生站-医嘱页签:删除"待签发"状态的检验项目时,错误触发"执行科室"校验导致删除失败
在handService的执行科室校验中增加dbOpType DELETE跳过条件,作为BugFix#454过滤器的第二层防御。
同时修复handMedication和handService中重复声明requestId变量导致的编译错误。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 16:12:38 +08:00
关羽
cc484d5f10 Fix Bug #442: 手术计费:点击"删除"待签发耗材时异常报错,导致操作失败
根因:手术计费场景中"待签发"耗材的 requestId 来自 adm_charge_item.service_id,
当 service_id 为 null 或对应的 wor_device_request 记录不存在时,
后端 removeById(null) 或 removeById(不存在ID) 会抛出异常导致删除失败。

修复策略:
- 前端(prescriptionlist.vue): handleDelete 中增加 requestId 有效性校验,
  过滤掉 requestId 为 null/undefined/空的项,避免发送无效删除请求
- 后端(DoctorStationAdviceAppServiceImpl.java): handMedication/handDevice/handService
  三个删除路径增加 requestId null check,跳过无效记录而非抛出异常

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:16:45 +08:00
赵云
30f8cdbd80 Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
修复三个根因问题:
1. handleCollapseChange 从 filteredCategoryList(计算属性映射副本)查找分类,
   改为从 categoryList(原始响应式数组)查找,确保 handleCategoryExpand 对
   cat.methods 的赋值能正确触发 Vue 响应式更新,分类展开后检查方法列表正常渲染
2. handleMethodSelect 跨分类检查中 cat.typeCode 与 newItem.checkType(cat.typeName)
   类型不匹配,改为统一使用 cat.typeName 比较
3. handleItemSelect 同样存在 typeCode vs typeName 不匹配问题,一并修复

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:12:09 +08:00
赵云
f4c6c12ef8 Fix Bug #441: 门诊手术安排:手术室护士角色进入页面提示"无权限"且"获取卫生机构列表失败"
根因:HTTP 响应拦截器(request.js)会对所有非 200 响应自动弹出错误通知。
手术室护士角色调用 /system/tenant/page 接口返回 403 时,拦截器先于组件的
.catch() 处理程序弹出"当前操作没有权限"通知,阻断页面体验。

修复:新增 getTenantPageSilent 包装函数,通过 skipErrorMsg: true 配置跳过
拦截器的自动错误提示,让 .catch() 中的 console.warn 静默降级处理。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:09:12 +08:00
关羽
8cf98008ae Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
根因:CheckPart 实体只有 packageName 字段,没有 packageId 字段。
前端 loadCategoryList 中 packageId 永远为 null,导致 loadPackageDetailsForItem
的 guard 条件 (!item.packageId) 永远提前返回,套餐明细无法加载。

修复策略:
1. handleItemSelect 中添加 packageName 到 selectedItems 数据对象
2. loadPackageDetailsForItem 改为优先使用 packageId,若无则通过 packageName
   调用 listCheckPackage API 查找 packageId(复用 loadMethodPackageDetails 已有的模式)
2026-05-13 15:05:51 +08:00
关羽
062c8d9dee Fix Bug #412: 门诊医生站:传染病报告卡保存失败,提示报错
根因:getNextCardNo API 返回失败或异常时,infectiousDiseaseReportDialog.vue
将 cardNo 保持为空字符串。后端 DTO 对 cardNo 有 @NotBlank 校验,
空字符串导致保存请求被拒绝。

修复:
1. getNextCardNo API 失败时生成 TEMP_+timestamp 临时卡号
2. validateFormManually 放行 TEMP_ 开头的临时卡号

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:38:06 +08:00
荀彧
ffdfebaacf Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,"检查明细"标签页显示"暂无数据"
根因:handleRowClick 中从 API 响应提取 items 时,const resp = res.data || res 将 resp
设为 ExamApply 对象(res.data 有值),导致 resp.items 为 undefined(ExamApply 对象没有 items
字段),items 实际位于 AjaxResult 顶层(res.items)。

修复:防御性提取 items,优先取 res.items(AjaxResult 顶层),兼容 resp.items 和
resp.data.items 的嵌套情况,确保明细数据能正确加载到 selectedItems 中。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:34:29 +08:00
赵云
9ed52b7c48 Fix Bug #403: 住院医生工作站:应用医嘱组套后,药品明细字段内容丢失未正确引入表格
修复 handleSaveGroup 中 mergedDetail 和 newRow 构建时的空值处理问题:
1. mergedDetail 中 dose/doseQuantity/dispensePerDuration 使用严格 null 检查,
   避免组套中值为 null 时错误回退到 orderDetailInfos
2. newRow 中关键字段增加 mergedDetail 回退(?? 操作符),
   确保当 item 中字段为 null/undefined 时能从 setValue 填充的完整数据中获取

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:14:30 +08:00
关羽
fc1ed6c4ce Fix Bug #401: 门诊完诊审计日志错误:div_log 表中 pool_id 与 slot_id 存值与设计规范不符
根因:DoctorStationMainAppServiceImpl.completeEncounter() 中获取 pool_id/slot_id 的逻辑有两处缺陷:
1. 当 queueItem 存在但 poolId 或 slotId 为 null 时,else if 分支因 queueItem != null 而跳过,
   导致未进入回退链路,最终 divPoolId 和 divSlotId 均为 null。
   修复:将 else if 拆为独立的 if 判断,确保 queueItem 值缺失时能正确回退。
2. 回退路径中先将 order.getSlotId() 赋给 divSlotId,再查询 ScheduleSlot 获取 poolId。
   若 Slot 查询失败(返回 null),则 divSlotId 有值但 divPoolId 为 null,产生不完整审计记录。
   修复:先查 Slot,成功后同时用 slot.getId() 和 slot.getPoolId() 赋值,保证数据一致性。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:07:15 +08:00
赵云
818564f5ba Fix Bug #400: 门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
根因分析:
triage_queue_item.status 字段在 DDL 中定义为 VARCHAR(50),但 Java 实体
TriageQueueItem.status 为 Integer 类型,应用层使用 TriageQueueStatus 枚举值
(0/10/20/30/40) 进行读写。数据类型不匹配导致 MyBatis-Plus 无法正确映射 status 字段,
完诊时使用 LambdaUpdateWrapper 更新 status=30 操作失败。

修复方案:
1. 修正 DDL:将 status 字段类型从 VARCHAR(50) 改为 INTEGER
2. 新增迁移 SQL:修改已有数据库表的 status 字段类型为 INTEGER

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:00:45 +08:00
关羽
78adbddfde Fix Bug #405: 住院医生工作站:临床医嘱保存成功后,医嘱条目仍处于可编辑状态(未锁定展示)
- handleSaveSign: 新增行点击确定后调用savePrescription持久化到后端,而非仅设置isEdit=false后调用handleAddPrescription
- handleSaveBatch: 修复nextId.value == 1 为赋值操作(=)
- handleSaveBatch: catch块增加错误提示,保存失败时用户可感知

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 13:36:27 +08:00
赵云
861db6b0f5 Fix Bug #408: 门诊医生站检查明细标签页显示暂无数据 - 修复handleRowClick中resp.items被d.data覆盖后丢失的问题
根因:后端 getInfo 返回 { data: ExamApply, items: ExamApplyItem[] },
前端先将 resp 赋值给 d,随后又执行 d = resp.data,导致 d 变成 ExamApply 对象,
后续 d.items 永远为 undefined,明细列表无法加载。
修复:提前保存 resp.items 到 rawItems 变量,后续使用 rawItems 而非 d.items。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 13:24:02 +08:00
Ranyunqiao
b7df71fd0b bug 362 428 436 2026-05-13 12:59:03 +08:00
陈琳
5bc8a8e517 Fix Bug #496: 【住院医生工作站-检查申请】检查申请列表字段命名不规范及单号生成规则不符合医疗行业标准
1. 前端列标题:处方号 → 申请单号(表格列 + 详情弹窗)
2. 后端单号生成:原用 PAR 处方号前缀 → 改为 JCZ + yyMMdd + 5位顺序号
3. 新增 AssignSeqEnum.CHECK_APPLY_NO 枚举项(JCZ 前缀)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 11:03:32 +08:00
0264fa9d58 Merge branch 'develop' of https://gitea.gentronhealth.com/wangyizhe/his into develop 2026-05-13 10:57:20 +08:00
cd12dd7a22 bug471 手术管理-门诊手术安排:手术申请查询结果中混入住院检验申请单数据(脏数据) 2026-05-13 10:45:33 +08:00
关羽
bfddf87b2c Fix Bug #496: 【住院医生工作站-检查申请】检查申请列表字段命名不规范及单号生成规则不符合医疗行业标准
1. 前端 examineApplication.vue:列表表头和详情弹窗中"处方号"改为"申请单号"
2. 后端 RequestFormManageAppServiceImpl:检查申请单单号生成规则由 PAR+流水号 改为 JCZ+yyMMdd+5位顺序号(如:JCZ26051300001),其他类型申请单保持原有PAR规则不变

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:42:39 +08:00
关羽
84499d4ec1 Fix Bug #400: 门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
根因:完诊时使用 triageQueueItemService.updateById(queueItem) 更新队列状态,
依赖 MyBatis Plus 的实体级更新策略,可能因字段级更新策略导致 status 字段未实际写入数据库。

修复策略:改用 LambdaUpdateWrapper 直接生成 UPDATE SQL,明确指定 SET status=30,
绕过实体级更新策略,确保 status 字段必定写入数据库。同时增加更新失败日志。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:37:02 +08:00
荀彧
01c5b62024 Fix Bug #401: 门诊完诊审计日志错误:div_log 表中 pool_id 与 slot_id 存值与设计规范不符
调整完诊时 div_log 的 pool_id/slot_id 获取优先级:优先使用 triage_queue_item
(挂号时录入的号源信息,为权威来源),队列项不存在或值缺失时回退使用
encounter → order → slot → pool 链路

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:15:20 +08:00
赵云
559821e4d3 Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
根本原因:
1. 分类联动加载检查方法时,未提取后端返回的 packageId 字段
2. 勾选检查方法后,未从方法中获取套餐信息(isPackage/packageId)
3. 选中带套餐的检查方法后,未调用 loadPackageDetailsForItem 预加载套餐明细

修复内容(4处手术式修改):
- handleCategoryExpand:方法映射增加 packageId 字段
- handleRowClick:回充已有申请单时,从匹配的方法中获取套餐信息
- handleMethodSelect:从方法获取套餐信息并预加载套餐明细
- handleItemSelect:方法映射增加 packageId 字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 01:11:38 +08:00
赵云
0dd4c25c12 Fix Bug #412: 门诊医生站:传染病报告卡保存失败,提示报错
根因分析:
- 前端在 buildSubmitData() 中使用 formData.diagnosisId || null 将空字符串转为 null
- 后端 InfectiousDiseaseReportDto.diagId 有 @NotNull 校验,导致 null 值被拒绝
- diagnosisId 来源于 show() 中 diagnosisData?.conditionId || diagnosisData?.definitionId
  使用 || 运算符会将 0 等假值跳过,可能导致 ID 丢失

修复内容:
1. show() 函数:使用显式 null/空字符串检查替代 || 运算符,确保 conditionId/definitionId 正确映射
2. handleSubmit():提交前增加 diagnosisId 非空校验,提前拦截并给出友好提示
3. buildSubmitData():diagId 使用 Number() 显式转换,确保发送正确的 Long 值

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 01:06:23 +08:00
张飞
3590a18adc Fix Bug #362: 住院护士站:入出转管理双击查看详情时,"入科时间"字段显示当前系统时间而非实际入科时间
在 selectAdmissionPatientInfo SQL 中,startTime 原取自 bed.start_time(床位级别的位置记录),
当该 LEFT JOIN 无匹配记录时返回 NULL,前端 fallback 到当前系统时间。
改为 COALESCE(bed.start_time, ae.start_time),无床位记录时回退到 encounter 的入院时间。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 01:03:24 +08:00
关羽
b96acc2402 Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
根因:el-popover 使用 :visible 受控模式,closeAllPopovers() 将 showPopover 设为 false
后,Vue 尚未完成 DOM 更新时 showChargeDialog 已被设为 false,导致弹窗组件被销毁
而 popover 的 visible 状态变更未传播到 DOM,弹窗消失但 popover 残留。

修复:在 closeChargeDialog 中使用 nextTick 等待 Vue 完成 popover 关闭的 DOM 更新后,
再设置 showChargeDialog = false 关闭主弹窗。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:16:10 +08:00
诸葛亮
e83c35c3f1 Fix Bug #494: 住院医生工作站-检查申请:"申请单名称"字段显示为通用名称,未展示具体检查项目名称
根因:medicalExaminations.vue 保存检查申请单时,name 字段硬编码为 "检查申请单",
导致列表页所有记录的申请单名称均显示为固定字符串,无法区分具体检查项目。

修复:将 name 从硬编码改为从已选项目集合中提取 adviceName 并用顿号连接,
如选择 CT、超声两项则显示为 "CT、超声"。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:15:21 +08:00
赵云
b5d876be36 Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空
根因:projectWithDepartment 函数定义时遗漏了 type 参数,导致函数体内引用 type 变量时报 ReferenceError(未定义),type === 2 的判断永远无法正确执行。用户在提交时手动选择的发往科室被清空且无法提交。

修复:在函数签名中添加 type 参数,与 working 版本 medicalExaminations.vue 保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:14:00 +08:00
刘备
5539d4cc03 Fix Bug #489: 【医嘱闭环】医生站签发单条长期药品医嘱,护士校对界面生成重复(两条)待校对记录
在住院医生站签发流程的 handMedication() 方法中增加去重逻辑,
与门诊医生站保持一致,使用 patientId+encounterId+adviceDefinitionId+dose+methodCode+rateCode
作为唯一键,防止前端重复提交导致数据库产生重复医嘱记录。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:13:32 +08:00
刘备
982e905990 Fix Bug #491: 【执行科室配置】保存配置时系统报错 - 修复Organization.getName()空指针异常
根因分析:
1. organizationLocationInit() 中 Organization.getName() 未做空值过滤,若数据库存在name为null的科室记录会NPE
2. addOrEditOrgLoc() 中 activityDefinitionMapper.selectById().getName() 未对selectById返回null做防护
3. addOrEditOrgLoc() 缺少organizationId前置校验,空值传入可能导致后续流程异常

修复内容:
- 第74行:stream中增加 .filter(organization -> organization != null && organization.getName() != null)
- 第136-138行:增加organizationId为null时的校验,返回友好错误提示
- 第145-147行:将activityDefinitionMapper.selectById结果先赋值再判空,避免NPE

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:12:42 +08:00
赵云
0d46f03e68 Fix Bug #488: 【临床医嘱】双击编辑待签发医嘱,医嘱类型回显为数字且点击确认报接口错误
修复 clickRowDb 函数中编辑条件过于严格的问题:原条件 `row.statusEnum == 1 && !row.requestId`
只允许"待保存"(无requestId)的医嘱进入编辑,导致"待签发"(有requestId)的医嘱无法编辑。
改为 `row.statusEnum == 1`,允许所有待签发状态的医嘱编辑。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:11:49 +08:00
张飞
3cab8306c2 Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因分析:
1. 前端组件使用了错误的API获取科室列表:表单使用getDepartmentList(/app-common/department-list)
   保存的targetDepartment ID,但详情弹窗使用getOrgList(/base-data-manage/organization/organization)
   查询,两个接口返回的数据结构和ID不同,导致recursionFun无法匹配到科室名称
2. recursionFun中obj.children可能为null/undefined,直接遍历会抛TypeError
3. getLocationInfo是异步调用,handleViewDetail可能在科室数据加载完成前被调用

修复:
- 统一使用getDepartmentList(@/api/public.js)获取科室数据,与表单组件保持一致
- recursionFun增加children空值保护
- handleViewDetail改为async,打开详情前确保科室数据已加载
2026-05-13 00:05:38 +08:00
刘备
0600bbecbc Fix Bug #468: [住院医生工作站-检验申请] 列表页缺失【单据状态】列,无法闭环管理检验医嘱执行进度
前后端完整链路修复:
- 后端 Mapper: LEFT JOIN wor_service_request 表,通过 CASE MIN(status_enum) 映射单据状态
- 后端 Mapper: 新增状态筛选和关键字搜索(申请单号/检验项目模糊匹配)
- 后端 Service/Controller: 新增 status 和 keyword 参数传递
- 前端 Vue: 列表页添加【单据状态】列,绑定 status 字段
- 前端 Vue: 移除中间状态选项(已采集/已收样),与后端 CASE 映射保持一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:02:47 +08:00
荀彧
ac57ac21e9 Fix Bug #464: [目录管理-诊疗目录] 新增项目时"零售价"未与"诊疗子项"合计总价自动同步
根本原因: calculateTotalPrice() 中同步零售价的条件只检查了第一个子项 (treatmentItems.value[0]),当第一个子项被清空但其他子项有效时,零售价不会同步。submitForm() 中存在相同问题。

修复内容:
1. calculateTotalPrice(): 使用 Array.some() 检查是否有任何有效子项,而非只检查第一个
2. 当无有效子项时,将 retailPrice 重置为 undefined 避免残留旧值
3. submitForm(): childrenJson 序列化和零售价同步同样改用 some() 检查
4. addItem(): 补充缺失的 name 字段,与初始值结构保持一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:35:49 +08:00
荀彧
d38efd15b3 Fix Bug #457: 门诊收费:已签发的手术类医嘱在门诊收费列表中不显示项目名称
根因分析:门诊医生站处方列表查询(DoctorStationAdviceAppMapper.xml)中,
手术类医嘱(category_enum=4)的 advice_table_name 固定返回 'wor_activity_definition',
而非 'cli_surgery'。当医生通过"签发"按钮处理手术医嘱时,handService() 据此创建
ChargeItem,导致 product_table = 'wor_activity_definition',但 product_id 实际指向
cli_surgery 表中的手术记录。

门诊收费SQL查询的CASE语句仅匹配 product_table = 'cli_surgery' 的手术项,
因此这些手术医嘱无法匹配,item_name 返回 NULL。

修复方案:在 selectEncounterPatientPrescription 和 selectEncounterPatientPrescriptionWithPrice
的 item_name CASE 表达式中新增兜底分支:
  WHEN context_enum = #{activity} AND service_table = 'wor_service_request'
  THEN COALESCE(T9.surgery_name, wsr.content_json->>'surgeryName',
                wsr.content_json->>'adviceName', T2."name")

按优先级回退获取手术名称:cli_surgery表 → content_json手术名称 → content_json医嘱名称 → 诊疗定义名称

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:34:36 +08:00
刘备
c7404e9d3f Fix Bug #458: 门诊医生站:诊疗类医嘱保存成功后,列表"医嘱类型"列显示为空值
根因:mapAdviceTypeLabel 函数依赖 drord_doctor_type 字典数据进行类型映射,
当字典中缺少 value=3(诊疗)的条目时,find() 返回 undefined,函数返回空字符串,
导致保存后刷新列表时"医嘱类型"列显示为空白。

修复:在 mapAdviceTypeLabel 中为诊疗/手术类医嘱(wor_activity_definition 表)
添加兜底映射逻辑:type 3→诊疗, 6→手术, 4→手术, 1→检验, 2→检查, 5→其他,
确保即使字典缺失对应条目也能正确显示类型标签。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:29:57 +08:00
赵云
69b28c59f6 Fix Bug #448: 门诊划价模块-项目分类过滤失效,选择"耗材"类型时仍能检索出药品
根因: adviceBaseList.vue 中 adviceQueryParams 的 watch 在 popoverVisible=false 时
直接 return,未将参数同步到 queryParams。当 handleFocus 同时修改 adviceQueryParams
和 showPopover 时,Vue 的 watch 触发顺序不确定:
- 若 adviceQueryParams watch 先触发(popoverVisible 仍为 false),则 queryParams 保持旧值
- 随后 popoverVisible watch 触发时虽然会同步参数,但存在时序竞态导致查询参数不正确

修复: 将参数同步逻辑移至 early return 之前,确保 queryParams 始终与 adviceQueryParams
保持一致,API 请求仍在 popoverVisible=true 时才触发。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:20:44 +08:00
华佗
4e71b861ab Fix Bug #442: 手术计费:点击"删除"待签发耗材时异常报错,导致操作失败
根因:DoctorStationAdviceAppMapper.xml 中 getRequestBaseInfo SQL 的第二个 UNION 查询(手术计费耗材从 adm_charge_item 关联 wor_device_request)中,biz_request_flag 和 requester_id 使用了 CI.enterer_id(计费录入人),而非 DR.requester_id(设备申请创建人)。当录入人与当前操作人不一致时,biz_request_flag 为 '0',导致删除操作被后端拒绝。

修复:将 CI.enterer_id 改为 COALESCE(DR.requester_id, CI.enterer_id),优先使用 DeviceRequest 的 requester_id,确保 biz_request_flag 基于正确的创建人计算。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:08:08 +08:00
陈琳
7c471205a3 Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
根因分析:
1. handleCategoryExpand 加载了 cat.methods 但模板从未渲染,用户展开分类后看不到检查方法
2. 缺少 isMethodSelected/handleMethodSelect 函数,无法通过勾选检查方法来联动添加到已选择列表
3. 套餐明细展示缺少 CSS 样式(package-details-list/detail-row/detail-name/detail-info)

修复内容:
- 模板: 在分类折叠区域添加 cat.methods 的渲染(检查方法列表 + 勾选框 + 价格)
- 逻辑: 新增 isMethodSelected 和 handleMethodSelect 函数,支持直接勾选检查方法添加到已选择列表
- 样式: 添加套餐明细列表样式 + 检查方法区域样式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:02:12 +08:00
刘备
dc8661c3d0 Fix Bug #413: 医生个人报卡编辑/查看界面字段映射与后端DTO不一致导致数据不显示
后端 InfectiousCardDto 字段名与前端 showReport 映射不匹配:
- caseClass 应从 diseaseType 映射 (后端 diseaseType=病例分类)
- diseaseType 应从 diseaseSubtype 映射 (后端 diseaseSubtype=疾病分型)
- correctName 应从 revisedDiseaseName 映射
- withdrawReason 应从 returnReason 映射

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 22:44:31 +08:00
赵云
c634551fdb Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
1. 优化提示语:将"新增成功"/"修改成功"改为"手术申请提交成功!"/"手术申请修改成功!"
2. 优化执行顺序:先emit('saved')通知父组件刷新医嘱列表,再调用getList()刷新手术申请列表,确保数据刷新时序正确

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-12 22:15:26 +08:00
赵云
c556d51eaf Fix Bug #470: 住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率
策略A-前端优化:为手术项目穿梭框添加 v-loading 加载状态指示器,
解决API查询期间用户看到空白/卡住界面的问题。
同时暴露 getList 方法供父组件调用(之前未暴露但已被父组件调用)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 22:05:40 +08:00
赵云
9280065c5b Fix Bug #455: 门诊医生站-医嘱:开立诊疗医嘱时执行科室默认获取逻辑有误且显示为原始ID
根因修复:
1. 默认科室逻辑错误:row.orgId来自wor_activity_definition.org_id(项目所属科室),不是执行科室。
   改为优先使用positionId(adm_organization_location配置的执行科室),回退到患者就诊科室。
2. 显示为原始ID:getOrgList()为异步调用但未await,导致findOrgNameById执行时organization树未加载。
   将setValue改为async函数并await getOrgList(),确保科室名称解析时数据已就绪。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 18:39:23 +08:00
Ranyunqiao
220982fa34 Merge remote-tracking branch 'origin/develop' into develop 2026-05-12 17:27:36 +08:00
Ranyunqiao
460890e3c2 413 460 513 514 2026-05-12 17:27:25 +08:00
华佗
6a0a2ca711 Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
根据单据状态动态显示操作按钮:
- 待签发(状态0):显示修改、删除、详情
- 已签发(状态1):显示撤回、详情
- 其他状态:仅显示详情

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:27:24 +08:00
赵云
fbbe0e9248 Fix Bug #470: 住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率
根因:getAdviceBaseInfo 后端接口在查询手术项目时,仍会执行与手术无关的库存查询
(getAdviceInventory)、全表扫描待发放记录(getAdviceDraftInventory)以及药房科室
配置查询(getMedLocationConfig),其中 getAdviceDraftInventory 对
med_medication_dispense 和 wor_device_dispense 做全表扫描,无任何过滤条件,
导致手术/诊疗场景下的额外数据库开销。

修复:在 DoctorStationAdviceAppServiceImpl.getAdviceBaseInfo() 中增加类型判断,
当 adviceTypes 不包含药品(1)或耗材(2)时跳过所有库存相关查询,因为这些查询对手术/
诊疗(3,6)项目无意义,且下游代码仅在药品/耗材处理分支中使用这些变量。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:22:23 +08:00
赵云
b6df124e1b Fix Bug #400: 门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
完诊API后端要求同时传递 encounterId 和 firstEnum 两个参数:
1. DoctorCallDialog.vue:已有修复只传了 encounterId,缺少 firstEnum,导致后端校验失败
2. patientList.vue:仍传递原始值而非对象,且同样缺少 firstEnum

修复:两处调用均改为传递 { encounterId, firstEnum } 对象,firstEnum 默认值为1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:05:32 +08:00
赵云
a0162686f4 Fix Bug #400: 门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
队列弹窗【完成】按钮调用完诊API时,传递了原始 Long 值而非对象参数,
导致后端 @RequestBody 反序列化时 encounterId 为 null,队列项状态无法更新。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 13:06:21 +08:00
赵云
23561cc9b1 Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:31:37 +08:00
Ranyunqiao
cdb676c859 Merge remote-tracking branch 'origin/develop' into develop 2026-05-12 12:21:46 +08:00
Ranyunqiao
1b159cdfab 429 433 438 476 477 478 2026-05-12 12:21:25 +08:00
关羽
01d48c75a5 Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
Root cause analysis:
1. 非耗材类医嘱(如口服药"荆防颗粒")执行后,前端调用lotNumberMatch时传入所有患者
   encounterId。该后端函数会查询所有DeviceDispense记录并校验耗材库存,若任一患者存
   在耗材记录但无库存则报错"发耗材单生成失败,请检查耗材库存"。原代码的hasMedOrDevice检
   查包含了med_medication_request(药品医嘱),导致纯药品执行也触发耗材校验。
2. 执行成功后调用handleGetPrescription()刷新列表,触发defaultSelectAllRows()自动全选
   所有行,导致用户看到复选框全部选中的联动异常。

Fix:
1. hasMedOrDevice改为hasDevice,仅当选中医嘱包含device类型时才调用lotNumberMatch
2. handleGetPrescription新增skipAutoSelectAll参数,执行/不执行/取消执行后刷新时不自动全选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:19:11 +08:00
赵云
c8fa9b4bf0 Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
根因:关闭计费弹窗时未调用 prescriptionlist 子组件的 closeAllPopovers 方法,
导致 el-popover(项目字典悬浮列表)在父弹窗销毁后仍残留显示。
修复:在 closeChargeDialog 中先调用 prescriptionRef.value.closeAllPopovers() 关闭所有子 popover。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:59:49 +08:00
赵云
349c587386 Fix Bug #462: [目录管理-诊疗目录] 编辑弹窗中"所需标本"下拉框数据加载失败
根因:sys_dict_type 表中缺少 specimen_code 字典类型定义,sys_dict_data
表中缺少对应标本数据记录,导致前端 useDict('specimen_code') 请求返回空数组。

修复:新增 SQL 迁移脚本,插入 specimen_code 字典类型及7条标本数据
(血液、尿液、粪便、呼吸道、无菌体液、生殖道、其他)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:34:55 +08:00
赵云
656079b27b fix: 修复 Bug #441 — 护士角色无权访问卫生机构列表
将租户Controller中4个只读端点的权限从 system:tenant:operate 降级为 system:tenant:list:
- getTenantPage (下拉列表数据源)
- getTenantDetail
- getTenantUserPage
- getUnbindTenantUserList

增删改操作保持 system:tenant:operate 不变。
同步更新 sys_menu 表:menu_id=2048 perms='system:tenant:list'

Root cause: 护士角色进入门诊手术安排页时 onMounted 调用 /system/tenant/page,该接口要求 system:tenant:operate 权限,护士角色无此权限导致卫生机构下拉列表为空,后续所有查询均失败。
2026-05-12 11:10:43 +08:00
ede6180c97 调优:测试环境数据库改为 test1 2026-05-12 11:01:28 +08:00
赵云
8de1f933e5 Fix Bug #441: 门诊手术安排:手术室护士角色进入页面提示"无权限"且"获取卫生机构列表失败"
策略B(优雅降级):将 loadDeptList、loadDoctorList、loadNurseList、loadOperatingRoomList 四个初始化接口中的 proxy.$modal.msgError 改为 console.warn 静默降级,避免权限不足时弹窗阻断页面。配合已有的 loadOrgList 修复,确保手术室护士等角色进入页面时不会因个别字典接口权限拒绝而弹窗报错。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 10:05:19 +08:00
赵云
3f35b4f2bb Fix Bug #435: 门诊手术安排:编辑弹窗中"费用类别"字段数据未回显
根因:getSurgeryScheduleDetail 查询未关联 fin_contract 表,导致 feeType 始终为 null
修复:SQL 中添加 adm_encounter → adm_account → fin_contract 三表关联,取 fc.contract_name AS feeType;
      OpScheduleDto 新增 feeType 字段用于接收映射

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 10:04:26 +08:00
eb3d0ee608 bug 486 [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选“医嘱类型”时检索结果为空 删去了搜索条件的默认值 2026-05-11 17:01:52 +08:00
赵云
dfe300cc1f Fix Bug #502: 【住院护士站-汇总发药申请】顶部医嘱类型(长期/临时)过滤按钮点击无响应
根因:父组件 index.vue 中 therapyEnum 变量未声明为 ref,且未通过 props 传递给子组件 prescriptionList.vue,
导致点击"长期/临时"按钮时数据流断裂,子组件 API 调用始终使用本地未变化的 therapyEnum 值。

修复:
1. index.vue 新增 const therapyEnum = ref(undefined)
2. index.vue 新增 handleTherapyChange() 调用 handleGetPrescription() 刷新列表
3. index.vue 将 therapyEnum 作为 prop 传入 PrescriptionList
4. prescriptionList.vue 将本地 therapyEnum ref 改为 props 接收
2026-05-11 15:27:18 +08:00
wangjian963
9bd39c06e7 Merge remote-tracking branch 'origin/develop' into develop 2026-05-11 15:25:17 +08:00
赵云
bde42d6b14 fix: 恢复 Bug #497 的后端修改 + 数据库字段同步 (ALTER TABLE doc_request_form ADD COLUMN status) 2026-05-11 14:57:06 +08:00
关羽
01bf3177c9 fix: 还原 Bug #443/#475/#477/#486/#497 引入的 getRequestForm 编译错误 2026-05-11 14:40:07 +08:00
赵云
2a9f8376e6 fix: 完整回退 Bug #497 的后端修改(SQL/Java接口/Impl/Dto) 2026-05-11 14:13:49 +08:00
关羽
0774d9f877 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:handleFocus/handleChange 中 categoryCode 的计算逻辑错误。当新增行未选择
医嘱类型时(row.adviceType 为 undefined),代码回退到 adviceQueryParams 的默认值并
匹配到具体药品分类(如西药 categoryCode='2'),导致搜索被限制在单一分类而非全局药库。

修复:简化 categoryCode 判定为 `row.adviceType !== undefined ? selectedItem.categoryCode : ''`,
未选类型时传空 categoryCode,使 searchKey 在全药库范围内模糊匹配。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:08:13 +08:00
赵云
cee38eceae Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. 非耗材类医嘱执行报"耗材库存"错误: handleExecute 中无条件调用 lotNumberMatch,
   后端会校验该就诊下所有待发放耗材库存,即使当前执行的是口服药等非耗材类医嘱
2. 全选联动异常: msgSuccess 在 handleGetPrescription 之前执行,数据刷新后
   defaultSelectAllRows 重新选中所有行,用户关闭弹窗后看到全选效果

修复方案:
1. 增加医嘱类型判断,仅当选中医嘱包含药品(med_medication_request)或耗材(device)
   类型时才调用 lotNumberMatch
2. 调整执行顺序:先刷新数据再显示成功弹窗,避免用户看到数据刷新的副作用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:07:49 +08:00
赵云
bbc740b6ce fix: 完全回退 Bug #497 引入的 drf.status 字段(数据库不存在) 2026-05-11 14:06:53 +08:00
关羽
256b986c0e Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因:handleViewDetail 为同步方法,点击详情时 getLocationInfo 尚未返回,
orgOptions 为空导致 recursionFun 无法将 targetDepartment ID 解析为科室名称。

修复:
1. 前端(4个申请组件):handleViewDetail 改为 async,解析 descJson 前确保 orgOptions 已加载
2. 前端:watch encounterId 改为 Promise.all 并行加载数据和科室列表
3. 后端:新增 keyword 关键字筛选参数(申请单号/检查项目模糊匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:03:49 +08:00
关羽
eaac16769d Fix Bug #491: 【执行科室配置】保存配置时系统报错
根因: addOrEditOrgLoc 方法中 organizationService.getById() 返回 null 时
直接调用 .getName() 导致 NullPointerException。当数据库中某条执行科室配置
关联的 organizationId 对应的科室记录已被删除时触发此问题。

修复: 在调用 getName() 前增加 null 检查,返回"未知科室"作为降级提示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:01:23 +08:00
wangjian963
df6c5f3824 506 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
CommonConstants.AppointmentOrderStatus 常量 → OrderStatus 枚举重构
   新增枚举:0=患者取消 / 1=有效 / 2=系统取消 / 3=已完成
   退号流程加乐观锁防并发,slot 状态改回待约,退号日志独立事务 修复 XML 中 Integer 比较用字符串的问题
Bug #411 — 诊室过滤栏从科室下拉框改为诊室按钮组
2026-05-11 13:51:47 +08:00
关羽
08075c90e2 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
根因分析:
1. el-collapse accordion 模式下快速切换分类时,连续的折叠/展开动画重叠,
   Element Plus 在动画过程中重新计算面板高度,导致高度跳变和白屏闪烁
2. 折叠容器缺少 overflow:hidden,动画过渡期间内容溢出造成闪烁

修复方案:
1. 添加 isAnimating 防抖标志,handleCollapseChange 中 300ms 内忽略后续点击
   (与 CSS 过渡时长一致),让当前动画完整执行后再响应下一次切换
2. .collapse-scroll 添加 overflow-x:hidden,防止水平方向溢出
3. :deep(.el-collapse-item__wrap) 添加 overflow:hidden 替代 will-change:height,
   避免强制 GPU 合成层带来的性能开销

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:50:04 +08:00
赵云
c5820fcec2 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因:get-check 接口只接收 encounterId 参数,忽略前端传递的 startDate/endDate/status 筛选参数,
导致日期筛选和状态筛选全部失效。同类型的 get-inspection 接口已正确支持这些参数。
修复:在 controller 的 get-check 方法增加 startDate、endDate、status 三个 @RequestParam,
调用 5 参数重载的 service 方法,使筛选参数正确传递到 SQL 层。
前端 examineApplication.vue 已包含状态列、parseStatus 映射、状态筛选下拉框,无需修改。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:45 +08:00
赵云
fbe7f4f41f Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空
根因:projectWithDepartment() 在提交时会清空用户手动选择的发往科室,
且项目未配置执行科室时 findTreeItem 返回 null 导致校验失败。
同时 submit() 使用 item.positionId(可能为 undefined)作为执行科室。

修复:
1. 清空科室前保存用户手动选择的值(manualDept)
2. type=2(提交)且 findItem 不存在时,若用户已手动选择科室则恢复并允许通过
3. positionId 兜底使用 form.targetDepartment

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:22 +08:00
赵云
f9ab4c5688 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
修复三个问题:
1. 后端DTO字段名为Name(大写)而前端模板引用name(小写),导致组套名称显示为空,使用name || Name兼容
2. 后端未返回rangeCode_dictText字段,新增getRangeText()函数根据当前选中范围标签动态显示
3. 前端向/group-package-for-order接口发送searchKey参数但后端不支持,移除该多余参数
2026-05-11 12:06:55 +08:00
赵云
861129c9d4 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
1. 修复单位字段默认为空:selectChange中增加defaultUnitCode逻辑,优先取minUnitCode,回退到unitCode
2. 修复执行科室显示内码:统一positionId和el-option value为字符串类型,避免类型不匹配导致el-select无法匹配选项
3. 科室匹配增加String类型转换:find时用String(d.id)===String(patientOrgId)避免因类型不同找不到匹配科室
4. loadDepartmentOptions和getDiseaseInitLoc增加.catch优雅降级,避免API失败时页面阻断

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 12:04:36 +08:00
赵云
78a2dfa3fe Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击"取消执行"报错
根因分析:handleCancel 函数从 exePerformRecordList 提取 procedureId 时,
未过滤 null/空值,导致无效 procedureId 被发送到后端,引发 SQL 异常。
同时 therapyEnum 可能存在类型不一致问题。

修复内容:
1. 提取 procedureId 后增加 filter 过滤空值
2. 构建请求参数时再次过滤,确保不发无效 procedureId
3. therapyEnum 显式转为 Number 类型确保后端正确匹配过滤
4. producerIds 为空时增加用户提示
5. 增加 .catch 错误处理避免未捕获的 Promise rejection
2026-05-11 11:35:19 +08:00
赵云
98e5a0b984 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
1. 修复列表不显示问题:SurgeryMapper.xml 中 sr.id IS NULL 条件导致已生成医嘱的手术被过滤,
   提交手术时 addSurgery() 会创建 category_enum=4 的 ServiceRequest,导致查询立即被排除。
   移除 sr.id IS NULL 过滤条件,使手术申请列表正常展示所有手术记录。

2. 修复提示语问题:将后端 addSurgery 返回消息从"手术信息添加成功"改为"手术申请提交成功!",
   updateSurgery 改为"手术申请修改成功!",前端直接使用后端返回消息。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:24:06 +08:00
e5c944069b bug 273 门诊医生站-》医嘱TAB页面:修改用药天数字段的值,总量字段的值未自动通过公式换算 补充:修改单词用量和用药频次时也自动换算总量字段 2026-05-11 11:11:30 +08:00
a68ffbfec4 去除多余的字符,修复语法问题 2026-05-11 10:00:15 +08:00
14f8a8b0a3 import 去掉多余逗号并略作格式整理 2026-05-11 09:53:02 +08:00
0f1e57227b 修复标签语法错误 2026-05-11 09:44:08 +08:00
赵云
98a370f3a2 fix: 修复 surgeryApplication.vue 多余的 </template> 标签 2026-05-11 09:31:02 +08:00
b2ce368749 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-ui-vue3/src/views/doctorstation/components/diagnosis/infectiousDiseaseReportDialog.vue
2026-05-11 09:25:32 +08:00
c691f82958 feat(diagnosis): 添加传染病报告对话框中的工作单位字段
- 在传染病报告对话框中新增工作单位输入框
- 添加了相应的工作单位表单项布局结构
- 集成了工作单位数据绑定功能
2026-05-11 09:24:59 +08:00
赵云
d2bfde7230 fix: 修复 Bug #412 引入的 Vue template 语法错误(缺少 el-row/el-col 标签) 2026-05-11 09:18:13 +08:00
赵云
62a09a8b94 Fix Bug #412: 门诊医生站:传染病报告卡保存失败,提示报错
根因:infectiousDiseaseReportDialog.vue 的 show() 函数将 cardNo 初始化为空字符串,
而后端 DTO 的 cardNo 字段有 @NotBlank 校验,导致保存时后端拒绝请求。
同仓库的 infectiousReport/index.vue 已有此修复(调用 getNextCardNo API),
但诊断流程使用的 infectiousDiseaseReportDialog.vue 漏掉了此修复。

修复:在 show() 函数中调用 getNextCardNo API 获取卡片编号,
API 失败时降级为 TEMP_ 前缀的临时卡号,与 infectiousReport/index.vue 保持一致。
2026-05-11 09:03:38 +08:00
关羽
10b2c58a3e Fix Bug #476: 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等)
在检查申请单开立界面新增5个核心临床字段(依次放在发往科室之后):
1. 紧急程度:el-select下拉选择(普通/急诊),区分急诊和普通检查
2. 过敏史:textarea输入,提示造影剂过敏风险
3. 检查目的:textarea输入,让执行科室医生了解临床背景
4. 病史摘要:textarea输入,补充患者病史信息
5. 期望检查时间:datetime选择器,实现精准调度

同步更新:
- medicalExaminations.vue: 表单模板 + form reactive对象新增字段
- examineApplication.vue: labelMap映射 + 详情弹窗紧急程度转换 + 修改弹窗新增字段 + 编辑/保存逻辑覆盖新字段
2026-05-11 08:45:13 +08:00
赵云
abc995523b Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
在 prescriptionlist 组件中新增 closeAllPopovers 方法,关闭手术计费弹窗时
先关闭所有行悬浮的项目字典下拉弹窗,避免主弹窗关闭后残留

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 08:45:05 +08:00
赵云
c0438c0288 Fix Bug #496: 【住院医生工作站-检查申请】检查申请列表字段命名不规范及单号生成规则不符合医疗行业标准
将检查申请列表及详情中的"处方号"统一修改为"申请单号",涉及列表表头、详情弹窗和打印内容三处

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 08:44:25 +08:00
关羽
13731c2373 Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
将执行科室配置校验提前到数据库写入操作之前,避免校验失败时已写入RequestForm记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:24:08 +08:00
关羽
d63b00fd33 Fix Bug #481: [住院护士站-医嘱执行] 药品"注射用头孢哌酮钠舒巴坦钠"库存充足,但执行医嘱时提示库存不足
在 checkExeMedInventory 方法中,原代码使用 findFirst() 只取第一个批次的库存
进行校验,导致同一库房多个批次的库存总量未被聚合计算。改为 collect(Collectors.toList())
收集所有匹配批次,然后用 Stream reduce 聚合总可用库存后再与需求量比较。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:22:42 +08:00
赵云
c60cd6878e Fix Bug #413: 医生个人报卡管理核心缺陷:医生个人报卡编辑/查看界面与门诊医生站登记报卡界面设计不统一 2026-05-11 00:21:29 +08:00
赵云
cf50f8968a Fix Bug #478: 【住院医生工作站-检验申请】点击"详情"查看检验单时,"发往科室"字段回显异常(显示为"-")
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:09:19 +08:00
赵云
94fac8e257 Fix Bug #495: 【医嘱闭环】已校对医嘱无法流转至"医嘱执行"界面,导致费用无法提交执行
医嘱执行模块 prescriptionList.vue 中 try-catch 被注释掉,导致数据处理
异常时静默失败且 loading 状态无法重置,页面显示空数据无报错。
- 恢复 try-catch 错误处理,捕获 res.data.records 空值及数据处理异常
- 添加 .catch() 处理 API 接口级别失败,重置 loading 并清空列表
- 修复无患者时 loading 状态未重置的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:02:18 +08:00
赵云
f162134156 Fix Bug #411: 智能分诊排队:底部操作控制区"过滤栏"功能实现与PRD需求不符(误设为科室过滤)
将底部过滤栏从"就诊科室快速过滤栏"改为"诊室快速过滤栏":
- UI文案:过滤栏标题、下拉框placeholder均改为诊室相关
- 数据源:移除 getLocationTree() 科室树API调用,改为从队列/候选池数据中动态提取诊室列表
- 过滤逻辑:改为按诊室名称(room字段)过滤,支持本科室下不同诊室快速切换
- 后端API调用不再依赖过滤栏选择,改用队列数据自身的organizationId

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:00:24 +08:00
关羽
d866d898df Fix Bug #506: 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
在 syncAppointmentReturnStatus 方法中:
1. 退号时同步将 order_main.pay_status 设为 0(未支付),修复退费后 pay_status 仍为 1 的问题
2. cancel_reason 固定使用标准化值"门诊退号",确保与 PRD 定义一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 23:58:40 +08:00
关羽
6725cef643 Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
修复 examineApplication.vue 中 recursionFun 函数的空指针异常:
1. 增加 orgOptions.value 数组有效性校验,防止接口未返回数据时崩溃
2. 增加 obj.children 的 Array.isArray 检查,原代码直接访问 children.length 在 children 为 undefined 时抛 TypeError
3. 匹配成功后增加 break 提前退出循环
4. handleViewDetail 增加 targetDepartment 存在性检查,递归查找失败时回退显示原始 ID 值
2026-05-10 23:56:39 +08:00
赵云
7790a64eab Fix Bug #479: [住院护士站-三测单] 体征录入模块缺少"录入日期"字段,导致无法补录历史体征数据
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 23:53:18 +08:00
赵云
49fa1c9b90 Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空 2026-05-10 23:50:43 +08:00
关羽
e0a035204e Fix Bug #487: 【临床医嘱】诊疗类医嘱签发后,列表状态未实时刷新为"已签发"
诊疗类医嘱(handService)签发时仅依赖saveOrUpdate更新statusEnum,
但该方式对已有记录可能未正确将statusEnum更新为ACTIVE(2)。
修复:在handService方法末尾使用LambdaUpdateWrapper批量显式更新
所有已处理ServiceRequest的statusEnum为ACTIVE(签发)/DRAFT(保存),
与ServiceRequestServiceImpl中activeStatusEnum/updateDraftStatusBatch
等方法的实现模式保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:20:43 +08:00
关羽
349b0453c8 Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
1. 修复模板结构错误:删除premature的</template>和多余的</div>标签,确保el-table正确渲染
2. 新增selectedRowIds独立维护选中行ID集合,不再依赖el-table内部selection状态,避免执行选中时联动触发全选
3. 更新所有选择事件处理器同步维护selectedRowIds
4. 补充index.vue缺失的ref/nextTick/provide导入

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:14:00 +08:00
关羽
3ddd74d679 Fix Bug #494: 住院医生工作站-检查申请:"申请单名称"字段显示为通用名称,未展示具体检查项目名称
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:06:16 +08:00
关羽
9829843b3e Fix Bug #273: 门诊医生站-》医嘱TAB页面:修改用药天数字段的值,总量字段的值未自动通过公式换算
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:05:37 +08:00
关羽
0d95cc1341 Fix Bug #389: 住院护士站-》医嘱校对:界面筛选条件失效:勾选"临时"医嘱仍显示"长期"医嘱数据
前端therapyEnum参数在type.value为undefined时会被序列化为字符串"undefined"传递给后端,
导致后端无法正确解析而跳过过滤条件。修复为条件展开语法:仅在type.value有值时才传递therapyEnum参数,
确保"全部"筛选时不传该字段以获取全部医嘱。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:07:17 +08:00
关羽
1dfceeaf46 Fix Bug #389: 住院护士站-》医嘱校对:界面筛选条件失效:勾选"临时"医嘱仍显示"长期"医嘱数据
根因:therapyEnum 参数映射逻辑完全颠倒。
原代码:type.value === 1 ? undefined : type.value === 2 ? 1 : 2
- 选择"长期"(1)时传 undefined(不传,无过滤)
- 选择"临时"(2)时传 1(长期值)
- 选择"全部"时传 2(临时值)

修复:直接传 type.value,与后端 therapyEnum 枚举一致:
- undefined → 全部 / 1 → 长期 / 2 → 临时

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:06:21 +08:00
关羽
1b79df4f93 Fix Bug #488: 【临床医嘱】双击编辑待签发医嘱,医嘱类型回显为数字且点击确认报接口错误
- 修复 handleSaveSign 中 getBindDevice 调用时 itemNo 可能为 undefined 导致的后端报错 "Required request parameter 'itemNo' for method parameter type String is not present":增加 itemNo 空值检查,为空时 console.warn 跳过调用而非发送无效请求
- 移除模板中两处调试残留:console.log 表达式渲染到页面(类型列和频次/用法列)
- 修复签发失败处理中截断的 conso; 语法错误
2026-05-10 16:05:42 +08:00
关羽
f62a280dfc Fix Bug #390: 住院护士站-医嘱执行:通过住院号检索无法定位/筛选患者
原 handleSearch 调用 reloadAllPatients 仅尝试刷新已展开的病区节点,
对懒加载树不可靠。改为递增 treeKey 强制树组件完全重新渲染,
触发 loadNode/loadPatientList 重新从后端拉取数据并传入 searchKey 过滤。
2026-05-10 16:05:17 +08:00
关羽
e5d949a740 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:02:35 +08:00
赵云
6451c308c2 Fix Bug #498: 【住院医生工作站-检查申请】检查申请列表操作项过于单一,缺失修改/作废/打印/看报告等核心临床操作
- 根据申请单状态动态展示操作按钮(详情/修改/删除/撤回/打印/看报告)
- 待签发状态:显示修改、删除按钮
- 已签发状态:显示撤回按钮
- 已校对/待接收状态:显示打印按钮
- 已接收/已检查/已出报告状态:显示打印、看报告按钮
- 新增修改申请单弹窗和报告查看弹窗
- 新增删除、撤回申请单 API 调用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:23:32 +08:00
关羽
772ec5537c Fix Bug #504: 【住院医生工作站-临床医嘱】护士退回药品医嘱后,医生修改并保存时提示"未匹配到库存信息"
根因分析:
1. SQL查询 getRegRequestBaseInfo 未返回 medication_id/adviceDefinitionId 字段,
   退回医嘱的 adviceDefinitionId 为 null,导致库存校验查询无法匹配到库存记录
2. 退回医嘱可能缺少 locationId,严格的 locationId 匹配导致校验失败

修复方案:
1. AdviceManageAppMapper.xml:在三个UNION查询中分别添加 medication_id/device_def_id/activity_id AS advice_definition_id
2. AdviceUtils.checkInventory():
   - 过滤 null adviceDefinitionId,避免SQL查询异常
   - 所有adviceDefinitionId为null时跳过库存校验
   - 退回医嘱单个adviceDefinitionId为null时跳过该校验项
   - 添加 locationId 容错匹配(为null时跳过location匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:19 +08:00
赵云
af6494c806 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
划价组套选择对话框嵌套在补费弹窗内部,缺少 append-to-body 属性导致
Dialog 被渲染在外层弹窗 DOM 内,z-index 层级被遮挡而不可见。
添加 append-to-body 使其挂载到 body 下,确保正常显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:48:31 +08:00
荀彧
2901dafe10 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:22:27 +08:00
华佗
ffc2562aea Fix Bug #505: 【业务逻辑缺陷】药品医嘱已由药房发药,护士仍能在"医嘱校对"模块执行"退回"操作
前后端双重校验防止已发药医嘱被退回:
1. 后端 InpatientAdviceDto 新增 dispenseStatus 字段,Mapper SQL LEFT JOIN med_medication_dispense 获取发药状态
2. 后端 adviceReject 方法增加前置校验,已发药(COMPLETED)的医嘱直接拒绝退回
3. 前端 prescriptionList.vue handleCancel 方法增加 dispenseStatus 校验,已发药医嘱点击退回时弹窗提示
2026-05-10 11:17:35 +08:00
赵云
5da537f863 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击“取消执行”报错 2026-05-10 11:13:25 +08:00
赵云
facbe7cd44 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击"取消执行"报错
取消执行时 procedureIds 数组可能为空导致后端 SQL 异常,改为从
exePerformRecordList 直接提取 procedureId,并增加空数据校验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:11:45 +08:00
赵云
283d25642a Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:11:45 +08:00
关羽
7b17a66214 Fix Bug #503: 【住院发退药】发药明细与发药汇总单数据触发时机不一致,存在业务脱节风险
在 selectEncounterInfoListPage 和 selectMedicineDispenseOrderPage 两个查询中增加
summary_no IS NOT NULL 过滤条件,使发药明细单仅在护士执行"汇总发药申请"后才显示记录,
与发药汇总单保持一致的触发时机。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:07:21 +08:00
42 changed files with 862 additions and 209 deletions

View File

@@ -7,6 +7,7 @@ import com.core.common.utils.MessageUtils;
import com.openhis.administration.domain.Location;
import com.openhis.administration.domain.Organization;
import com.openhis.administration.domain.OrganizationLocation;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.administration.mapper.OrganizationLocationMapper;
import com.openhis.administration.service.ILocationService;
import com.openhis.administration.service.IOrganizationLocationService;
@@ -70,6 +71,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
// 获取科室下拉选列表
List<Organization> organizationList = organizationService.getList(OrganizationType.DEPARTMENT.getValue(), null);
List<OrgLocInitDto.departmentOption> organizationOptions = organizationList.stream()
.filter(organization -> organization != null && organization.getName() != null)
.map(organization -> new OrgLocInitDto.departmentOption(organization.getId(), organization.getName()))
.collect(Collectors.toList());
initDto.setLocationFormOptions(chargeItemStatusOptions).setDepartmentOptions(organizationOptions);
@@ -131,12 +133,18 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
@Override
public R<?> addOrEditOrgLoc(OrgLocQueryDto orgLocQueryDto) {
// Validate required fields before processing
if (orgLocQueryDto.getOrganizationId() == null) {
return R.fail("请选择执行科室");
}
OrganizationLocation orgLoc = new OrganizationLocation();
BeanUtils.copyProperties(orgLocQueryDto, orgLoc);
Long activityDefinitionId = orgLoc.getActivityDefinitionId();
String activityName = activityDefinitionId != null
? activityDefinitionMapper.selectById(activityDefinitionId).getName() : "";
ActivityDefinition activityDef = activityDefinitionId != null
? activityDefinitionMapper.selectById(activityDefinitionId) : null;
String activityName = activityDef != null ? activityDef.getName() : "";
List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getActivityDefinitionId());

View File

@@ -2,9 +2,13 @@ package com.openhis.web.clinicalmanage.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.core.common.core.domain.R;
import com.openhis.common.enums.ActivityDefCategory;
import com.openhis.web.clinicalmanage.appservice.ISurgicalScheduleAppService;
import com.openhis.web.clinicalmanage.dto.OpCreateScheduleDto;
import com.openhis.web.clinicalmanage.dto.OpScheduleDto;
import com.openhis.web.regdoctorstation.appservice.IRequestFormManageAppService;
import com.openhis.web.regdoctorstation.dto.RequestFormDto;
import com.openhis.web.regdoctorstation.dto.RequestFormPageDto;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@@ -26,6 +30,7 @@ import java.util.Map;
public class SurgicalScheduleController {
private final ISurgicalScheduleAppService surgicalScheduleAppService;
private final IRequestFormManageAppService requestFormManageAppService;
/**
* 分页查询手术安排列表
@@ -87,6 +92,27 @@ public class SurgicalScheduleController {
return surgicalScheduleAppService.deleteSurgerySchedule(scheduleId);
}
/**
* 分页查询待排期手术申请列表
*
* @param requestFormDto 查询条件
* @return 手术申请列表
*/
@PostMapping(value = "/apply-list")
public R<IPage<RequestFormPageDto>> getSurgeryApplyList(@RequestBody RequestFormDto requestFormDto) {
if (requestFormDto.getPageNo() == null) {
requestFormDto.setPageNo(1);
}
if (requestFormDto.getPageSize() == null) {
requestFormDto.setPageSize(10);
}
//虽然很想这么写但是库里的手术申请单的type_code都是直接写的SURGERY
// requestFormDto.setTypeCode(ActivityDefCategory.PROCEDURE.getCode());
//只查询手术申请单
requestFormDto.setTypeCode("SURGERY");
return R.ok(requestFormManageAppService.getRequestFormPage(requestFormDto));
}
/**
* 导出手术安排列表
*

View File

@@ -59,6 +59,16 @@ public interface IDoctorStationAdviceAppService {
*/
R<?> getRequestBaseInfo(Long encounterId);
/**
* 查询医嘱请求数据(支持按生成来源和来源单据号过滤)
*
* @param encounterId 就诊id
* @param generateSourceEnum 生成来源(可选,如手术计费=6
* @param sourceBillNo 来源业务单据号(可选)
* @return 医嘱请求数据
*/
R<?> getRequestBaseInfo(Long encounterId, Integer generateSourceEnum, String sourceBillNo);
/**
* 门诊签退医嘱
*

View File

@@ -228,8 +228,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 医嘱定义ID集合
List<Long> adviceDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getAdviceDefinitionId)
.collect(Collectors.toList());
// 费用定价主表ID集合
List<Long> chargeItemDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getChargeItemDefinitionId)
// 费用定价主表ID集合过滤null值手术项目无定价定义
List<Long> chargeItemDefinitionIdList = adviceBaseDtoList.stream()
.map(AdviceBaseDto::getChargeItemDefinitionId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 判断是否包含药品或耗材类型(只有这些类型才需要库存相关查询)
@@ -275,9 +277,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
medLocationConfig = Collections.emptyList();
allowedLocByCategory = Collections.emptyMap();
}
// 费用定价子表信息 - 使用分批处理避免大量参数问题
// 费用定价子表信息 - 仅药品/耗材需要批次定价查询,手术/诊疗无库存概念不需要
List<AdvicePriceDto> childCharge = new ArrayList<>();
if (chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
if (hasMedOrDevice && chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
// 分批处理每批最多1000个ID增加批次大小以减少查询次数
int batchSize = 1000;
for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) {
@@ -957,11 +959,16 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
for (AdviceSaveDto adviceSaveDto : deleteList) {
iMedicationRequestService.removeById(adviceSaveDto.getRequestId());
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的药品请求
if (requestId == null) {
log.warn("BugFix#442: handMedication - 跳过 requestId 为 null 的删除请求");
continue;
}
iMedicationRequestService.removeById(requestId);
// 删除已经产生的药品发放信息
iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId());
// 🔧 Bug Fix #219: 删除费用项
Long requestId = adviceSaveDto.getRequestId();
String serviceTable = CommonConstants.TableName.MED_MEDICATION_REQUEST;
// 直接删除费用项
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
@@ -1417,6 +1424,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix#219: handDevice - 开始删除循环, deleteList.size={}", deleteList.size());
for (AdviceSaveDto adviceSaveDto : deleteList) {
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的耗材请求
if (requestId == null) {
log.warn("BugFix#442: handDevice - 跳过 requestId 为 null 的删除请求");
continue;
}
log.info("BugFix#219: handDevice - 删除开始: requestId={}", requestId);
// 1. 删除耗材请求
@@ -1721,12 +1733,17 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
for (AdviceSaveDto adviceSaveDto : deleteList) {
iServiceRequestService.removeById(adviceSaveDto.getRequestId());// 删除诊疗
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的诊疗请求
if (requestId == null) {
log.warn("BugFix#442: handService - 跳过 requestId 为 null 的删除请求");
continue;
}
iServiceRequestService.removeById(requestId);// 删除诊疗
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId,
adviceSaveDto.getRequestId()));// 删除诊疗套餐对应的子项
requestId));// 删除诊疗套餐对应的子项
// 🔧 Bug Fix #219: 删除费用项
Long requestId = adviceSaveDto.getRequestId();
String serviceTable = CommonConstants.TableName.WOR_SERVICE_REQUEST;
// 直接删除费用项
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
@@ -1783,8 +1800,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("handService - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
}
// 🔧 Bug Fix #238: 诊疗项目执行科室非空校验
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3) {
// 🔧 Bug Fix #238/#454: 诊疗项目执行科室非空校验(删除操作跳过校验)
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3
&& !DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) {
Long effectiveOrgId = adviceSaveDto.getEffectiveOrgId();
if (effectiveOrgId == null) {
throw new ServiceException("诊疗项目必须选择执行科室");
@@ -1987,13 +2005,25 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
*/
@Override
public R<?> getRequestBaseInfo(Long encounterId) {
return this.getRequestBaseInfo(encounterId, null, null);
}
@Override
public R<?> getRequestBaseInfo(Long encounterId, Integer generateSourceEnum, String sourceBillNo) {
// 当前账号的参与者id
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
// 未指定generateSourceEnum时默认只查询医生开立的医嘱
int sourceEnum = (generateSourceEnum != null) ? generateSourceEnum : GenerateSource.DOCTOR_PRESCRIPTION.getValue();
// 医嘱请求数据
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, null,
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue());
sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时只保留诊疗请求3/6过滤掉药品1和耗材2
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& (dto.getAdviceType() == 1 || dto.getAdviceType() == 2));
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态
requestBaseDto
@@ -2114,7 +2144,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, patientId,
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.YES.getCode(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue());
GenerateSource.DOCTOR_PRESCRIPTION.getValue(), null);
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态
requestBaseDto

View File

@@ -274,27 +274,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
return R.fail("非就诊中患者不能完诊");
}
// 2. 获取 pool_id 和 slot_id从 encounter → order_main → adm_schedule_slot 链路获取
// 确保 div_log 中的值与排班主表一致,不依赖 triage_queue_item队列项可能不存在或值错误
// 2. 查找队列项(限定当天,避免复诊患者匹配到历史队列记录)
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
Long divPoolId = null;
Long divSlotId = null;
if (encounter.getOrderId() != null) {
try {
Order order = iOrderService.getById(encounter.getOrderId());
if (order != null && order.getSlotId() != null) {
divSlotId = order.getSlotId();
ScheduleSlot slot = scheduleSlotMapper.selectById(divSlotId);
if (slot != null) {
divPoolId = slot.getPoolId();
}
}
} catch (Exception e) {
log.warn("获取完诊div_log的pool_id/slot_id失败encounterId={}", encounterId, e);
}
}
// 3. 查找队列项(限定当天,避免复诊患者匹配到历史队列记录)
TriageQueueItem queueItem = triageQueueItemService.getOne(
new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
@@ -319,14 +300,43 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
}
}
// 3. 获取 pool_id 和 slot_id优先使用 triage_queue_item挂号时录入的号源信息为权威来源
// 队列项不存在或值缺失时,回退使用 encounter → order_main → adm_schedule_slot 链路
Long divPoolId = null;
Long divSlotId = null;
if (queueItem != null && queueItem.getPoolId() != null && queueItem.getSlotId() != null) {
divPoolId = queueItem.getPoolId();
divSlotId = queueItem.getSlotId();
}
// 队列项 poolId/slotId 缺失时,通过 encounter.orderId → order_main.slot_id → adm_schedule_slot.pool_id 回退获取
if ((divPoolId == null || divSlotId == null) && encounter.getOrderId() != null) {
try {
Order order = iOrderService.getById(encounter.getOrderId());
if (order != null && order.getSlotId() != null) {
ScheduleSlot slot = scheduleSlotMapper.selectById(order.getSlotId());
if (slot != null) {
divSlotId = slot.getId();
divPoolId = slot.getPoolId();
}
}
} catch (Exception e) {
log.warn("回退获取完诊div_log的pool_id/slot_id失败encounterId={}", encounterId, e);
}
}
// 如果队列项存在且未完成,更新队列状态为已完成
// 使用排除法而非白名单:只要不是"已完成"就可以完诊,覆盖跳过、等待等非标准流转状态
if (queueItem != null &&
!TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus())) {
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
queueItem.setStatus(TriageQueueStatus.COMPLETED.getValue());
queueItem.setUpdateTime(nowLocal);
triageQueueItemService.updateById(queueItem);
// 使用 LambdaUpdateWrapper 直接更新,确保 status 字段必定写入数据库
boolean queueUpdate = triageQueueItemService.update(new LambdaUpdateWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getId, queueItem.getId())
.set(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue())
.set(TriageQueueItem::getUpdateTime, nowLocal));
if (!queueUpdate) {
log.error("完诊triage_queue_item 状态更新失败queueItemId={}", queueItem.getId());
}
} else if (queueItem == null) {
log.error("完诊:未找到任何 triage_queue_item 记录encounterId={}, tenantId={}",
encounterId, tenantId);

View File

@@ -112,11 +112,16 @@ public class DoctorStationAdviceController {
* 查询医嘱请求数据
*
* @param encounterId 就诊id
* @param generateSourceEnum 生成来源(可选,用于按来源过滤,如手术计费=6
* @param sourceBillNo 来源业务单据号(可选,用于按来源单据过滤)
* @return 医嘱请求数据
*/
@GetMapping(value = "/request-base-info")
public R<?> getRequestBaseInfo(@RequestParam(required = false) Long encounterId) {
return iDoctorStationAdviceAppService.getRequestBaseInfo(encounterId);
public R<?> getRequestBaseInfo(
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) Integer generateSourceEnum,
@RequestParam(required = false) String sourceBillNo) {
return iDoctorStationAdviceAppService.getRequestBaseInfo(encounterId, generateSourceEnum, sourceBillNo);
}
/**

View File

@@ -122,7 +122,8 @@ public interface DoctorStationAdviceAppMapper {
@Param("MED_MEDICATION_REQUEST") String MED_MEDICATION_REQUEST,
@Param("WOR_DEVICE_REQUEST") String WOR_DEVICE_REQUEST,
@Param("WOR_SERVICE_REQUEST") String WOR_SERVICE_REQUEST, @Param("practitionerId") Long practitionerId,
@Param("historyFlag") String historyFlag, @Param("generateSourceEnum") Integer generateSourceEnum);
@Param("historyFlag") String historyFlag, @Param("generateSourceEnum") Integer generateSourceEnum,
@Param("sourceBillNo") String sourceBillNo);
/**
* 查询就诊费用性质

View File

@@ -43,6 +43,19 @@ public interface IRequestFormManageAppService {
*/
List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate);
/**
* 查询申请单(支持筛选+状态+关键字)
*
* @param encounterId 就诊id
* @param typeCode 申请单类型
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/项目名称模糊匹配)
* @return 申请单列表
*/
List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate, String status, String keyword);
/**
* 分页查询申请单
*

View File

@@ -341,6 +341,28 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重,避免签发单条长期医嘱时产生重复记录
Set<String> longUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> longUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : longInsertOrUpdateList) {
String uniqueKey = adviceSaveDto.getPatientId() + "_"
+ adviceSaveDto.getEncounterId() + "_"
+ adviceSaveDto.getAdviceDefinitionId() + "_"
+ adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && longUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复长期医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose());
continue;
}
longUniqueKeySet.add(uniqueKey);
longUniqueList.add(adviceSaveDto);
}
log.info("防重复保存(长期):去重前{}条,去重后{}条", longInsertOrUpdateList.size(), longUniqueList.size());
longInsertOrUpdateList = longUniqueList;
for (RegAdviceSaveDto regAdviceSaveDto : longInsertOrUpdateList) {
boolean firstTimeSave = false;// 第一次保存
longMedicationRequest = new MedicationRequest();
@@ -406,6 +428,29 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
.getValue().equals(e.getTherapyEnum())
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重
Set<String> tempUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> tempUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : tempInsertOrUpdateList) {
String uniqueKey = adviceSaveDto.getPatientId() + "_"
+ adviceSaveDto.getEncounterId() + "_"
+ adviceSaveDto.getAdviceDefinitionId() + "_"
+ adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && tempUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复临时医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose());
continue;
}
tempUniqueKeySet.add(uniqueKey);
tempUniqueList.add(adviceSaveDto);
}
log.info("防重复保存(临时):去重前{}条,去重后{}条", tempInsertOrUpdateList.size(), tempUniqueList.size());
tempInsertOrUpdateList = tempUniqueList;
for (RegAdviceSaveDto regAdviceSaveDto : tempInsertOrUpdateList) {
boolean firstTimeSave = false;// 第一次保存
tempMedicationRequest = new MedicationRequest();

View File

@@ -31,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -75,6 +76,13 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室");
}
// 诊疗处方号
String prescriptionNo;
// 申请单ID
@@ -91,8 +99,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.fail("无待签发的医嘱,该申请单不可编辑");
}
} else {
// 诊疗处方
prescriptionNo = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_PSYCHOTROPIC_NO.getPrefix(), 8);
// 检查申请单号JC检查+ Z住院标识+ yyMMdd日期+ 5位顺序
String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
int seq = assignSeqUtil.getSeqNoByDay(AssignSeqEnum.CHECK_APPLY_NO.getPrefix());
prescriptionNo = "JCZ" + dateStr + String.format("%05d", seq);
}
// 当前时间
@@ -139,12 +149,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
// 诊疗集合
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
// 诊疗执行科室配置
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室");
}
for (ActivitySaveDto activitySaveDto : activityList) {
serviceRequest = new ServiceRequest();
@@ -428,12 +432,28 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
*/
@Override
public List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate) {
return getRequestForm(encounterId, typeCode, startDate, endDate, null, null);
}
/**
* 查询申请单(支持筛选+状态+关键字)
*
* @param encounterId 就诊id
* @param typeCode 申请单类型
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/项目名称模糊匹配)
* @return 申请单列表
*/
@Override
public List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate, String status, String keyword) {
// 检查参数
if (encounterId == null) {
return new java.util.ArrayList<>();
}
List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate,null,null);
List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate, status, keyword);
for (RequestFormQueryDto requestFormQueryDto : requestFormList) {
// 查询处方详情
List<RequestFormDetailQueryDto> requestFormDetail =

View File

@@ -16,6 +16,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
/**
@@ -98,6 +99,7 @@ public class RequestFormManageController {
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检验项目模糊匹配)
* @return 检验申请单
*/
@GetMapping(value = "/get-inspection")
@@ -105,11 +107,12 @@ public class RequestFormManageController {
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status) {
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) {
return R.fail("就诊ID不能为空");
}
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROOF.getCode(), startDate, endDate));
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROOF.getCode(), startDate, endDate, status, keyword));
}
/**
@@ -140,6 +143,32 @@ public class RequestFormManageController {
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROCEDURE.getCode()));
}
/**
* 分页查询手术申请单全局不需要encounterId用于门诊手术安排查找弹窗
*
* @param surgeryNo 手术单号(模糊查询,可选)
* @param applyTimeStart 申请时间起始(可选)
* @param applyTimeEnd 申请时间截止(可选)
* @param mainDoctorId 主刀医生ID可选
* @param applyDeptId 申请科室ID可选
* @param pageNo 页码默认1
* @param pageSize 每页数量默认10
* @return 分页手术申请单列表
*/
@GetMapping(value = "/get-surgery-page")
public R<IPage<RequestFormPageDto>> getSurgeryRequestFormPage(
@RequestParam(required = false) String surgeryNo,
@RequestParam(required = false) LocalDate applyTimeStart,
@RequestParam(required = false) LocalDate applyTimeEnd,
@RequestParam(required = false) Long mainDoctorId,
@RequestParam(required = false) Long applyDeptId,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
RequestFormDto dto = new RequestFormDto(surgeryNo, ActivityDefCategory.PROCEDURE.getCode(), applyTimeStart, applyTimeEnd,
mainDoctorId, applyDeptId, pageNo, pageSize);
return R.ok(iRequestFormManageAppService.getRequestFormPage(dto));
}
/**
* 分页查询申请单
* @return 申请单
*/

View File

@@ -14,6 +14,10 @@ public class RequestFormDto {
* 手术单号
*/
private String surgeryNo;
/**
* 申请单类型编码
*/
private String typeCode;
/**
* 申请时间开始
*/

View File

@@ -100,6 +100,7 @@
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = 6 THEN T2."name"
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} AND T1.service_table = 'wor_service_request' THEN COALESCE(T9.surgery_name, wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN COALESCE(wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"
@@ -225,6 +226,7 @@
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = 6 THEN T2."name"
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} AND T1.service_table = 'wor_service_request' THEN COALESCE(T9.surgery_name, wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN COALESCE(wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"

View File

@@ -527,6 +527,9 @@
LEFT JOIN cli_condition AS cc ON cc.id = T1.condition_id AND cc.delete_flag = '0'
LEFT JOIN cli_condition_definition AS ccd ON ccd.id = cc.definition_id
WHERE T1.delete_flag = '0' AND T1.tcm_flag = 0
<if test="generateSourceEnum != null">
AND T1.generate_source_enum = #{generateSourceEnum}
</if>
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>
@@ -695,6 +698,9 @@
-- based_on_table='med_medication_request' → 输液/皮试执行记录,应排除
-- based_on_table='exam_apply'/'lab_apply' → 申请单原始医嘱,应保留
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL OR T1.based_on_table NOT IN ('med_medication_request', 'med_medication_dispense'))
<if test="sourceBillNo != null and sourceBillNo != ''">
AND T1.prescription_no = #{sourceBillNo}
</if>
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>

View File

@@ -231,7 +231,7 @@
ae.priority_enum,
ae.organization_id,
ae.start_time AS in_hos_time,
bed.start_time,
COALESCE(bed.start_time, ae.start_time) AS start_time,
bed.location_id AS bed_id,
bed.location_name AS bed_name,
house.location_id AS house_id,

View File

@@ -96,22 +96,14 @@
INNER JOIN med_medication_request AS T5
ON T4.med_req_id = T5.id
AND T5.delete_flag = '0'
WHERE EXISTS (
SELECT 1 FROM wor_supply_request wsr
WHERE wsr.type_enum = 3
AND wsr.delete_flag = '0'
AND wsr.bus_no = T4.summary_no
AND T4.summary_no IS NOT NULL
AND T4.summary_no != ''
)
AND <if test="statusEnum == null">
T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},8)
WHERE <if test="statusEnum == null">
T4.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})
</if>
<if test="statusEnum == 3">
T4.status_enum IN (#{inProgress},#{preparation},#{prepared},8)
T4.status_enum IN (#{inProgress},#{preparation},#{prepared})
</if>
<if test="statusEnum == 4">
T4.status_enum IN (#{completed},8)
T4.status_enum = #{completed}
</if>
AND T4.summary_no IS NOT NULL
AND T4.summary_no != ''

View File

@@ -288,7 +288,7 @@
AND T1.refund_device_id IS NULL
ORDER BY T1.status_enum)
UNION ALL
(SELECT CASE WHEN T1.category_enum = 4 THEN 6 ELSE COALESCE(T1.category_enum, 3) END AS advice_type,
(SELECT CASE WHEN T1.category_enum = 4 THEN 6 ELSE 3 END AS advice_type,
T1.id AS request_id,
T1.id || '-3' AS unique_key,
T1.requester_id AS requester_id,
@@ -373,4 +373,4 @@
</if>
</select>
</mapper>
</mapper>

View File

@@ -12,12 +12,24 @@
drf.desc_json,
drf.requester_id,
drf.create_time,
ap.NAME AS patient_name
ap.NAME AS patient_name,
CASE MIN(wsr.status_enum)
WHEN 1 THEN 0
WHEN 2 THEN 1
WHEN 3 THEN 4
WHEN 4 THEN 4
WHEN 5 THEN 5
WHEN 6 THEN 5
WHEN 7 THEN 5
ELSE NULL
END AS status
FROM doc_request_form AS drf
LEFT JOIN adm_encounter AS ae ON ae.ID = drf.encounter_id
AND ae.delete_flag = '0'
LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id
AND ap.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.prescription_no = drf.prescription_no
AND wsr.delete_flag = '0'
WHERE drf.delete_flag = '0'
AND drf.encounter_id = #{encounterId}
AND drf.type_code = #{typeCode}
@@ -27,6 +39,33 @@
<if test="endDate != null and endDate != ''">
AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
</if>
<if test="status != null and status != ''">
AND CASE MIN(wsr.status_enum)
WHEN 1 THEN 0
WHEN 2 THEN 1
WHEN 3 THEN 4
WHEN 4 THEN 4
WHEN 5 THEN 5
WHEN 6 THEN 5
WHEN 7 THEN 5
ELSE NULL
END = #{status}::integer
</if>
<if test="keyword != null and keyword != ''">
AND (drf.prescription_no ILIKE '%' || #{keyword} || '%'
OR EXISTS (
SELECT 1 FROM wor_service_request wsr2
WHERE wsr2.prescription_no = drf.prescription_no
AND wsr2.delete_flag = '0'
AND wsr2.activity_id IN (
SELECT id FROM wor_activity_definition wad
WHERE wad.delete_flag = '0'
AND wad.name ILIKE '%' || #{keyword} || '%'
)
))
</if>
GROUP BY drf.id, drf.encounter_id, drf.prescription_no, drf.name, drf.desc_json,
drf.requester_id, drf.create_time, ap.name
</select>
<select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto">
@@ -122,6 +161,9 @@
<if test="requestFormDto.surgeryNo != null and requestFormDto.surgeryNo != ''">
AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%')
</if>
<if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''">
AND drf.type_code = #{requestFormDto.typeCode}
</if>
<if test="requestFormDto.applyTimeStart != null">
AND drf.create_time >= #{requestFormDto.applyTimeStart}
</if>

View File

@@ -270,6 +270,10 @@ public enum AssignSeqEnum {
* 诊疗处方号
*/
ACTIVITY_PSYCHOTROPIC_NO("62", "诊疗处方号", "PAR"),
/**
* 检查申请单号(住院)
*/
CHECK_APPLY_NO("72", "检查申请单号", "JCZ"),
/**
* b 病历文书
*/

View File

@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS triage_queue_item (
patient_name VARCHAR(255),
healthcare_name VARCHAR(255),
practitioner_name VARCHAR(255),
status VARCHAR(50) NOT NULL, -- WAITING/CALLING/SKIPPED/COMPLETED
status INTEGER NOT NULL DEFAULT 0, -- 分诊队列状态: 0=WAITING(等待), 10=CALLING(叫号中), 20=IN_CLINIC(诊中), 30=COMPLETED(已完成), 40=SKIPPED(已跳过)
queue_order INTEGER NOT NULL,
create_time TIMESTAMP,
update_time TIMESTAMP,

View File

@@ -0,0 +1,30 @@
-- Bug #400 修复triage_queue_item.status 字段类型修正
-- 原 DDL 将 status 定义为 VARCHAR(50),但 Java 实体 TriageQueueItem.status 为 Integer 类型,
-- 应用层使用 TriageQueueStatus 枚举值0/10/20/30/40写入类型不匹配导致 MyBatis-Plus
-- 无法正确映射 status 字段,完诊时 status 更新为 30 失败。
--
-- 注意:必须在后端实际连接的 schema 中执行dev=hisdev, test=histest, prd=hisprd
-- 执行前请先确认SET search_path TO hisdev; (或对应的 schema
-- 1. 先将已有的字符串值转换为对应的整数值
-- 如果之前写入的是枚举 code如 'waiting'、'completed'),需要先转换
UPDATE triage_queue_item
SET status = CASE
WHEN status IN ('0', 'waiting', 'WAITING') THEN 0
WHEN status IN ('10', 'calling', 'CALLING') THEN 10
WHEN status IN ('20', 'in-clinic', 'IN_CLINIC', 'in-clinic') THEN 20
WHEN status IN ('30', 'completed', 'COMPLETED') THEN 30
WHEN status IN ('40', 'skipped', 'SKIPPED') THEN 40
WHEN status IN ('50', 'refunded', 'REFUNDED') THEN 50
WHEN status IN ('60', 'follow', 'FOLLOW') THEN 60
ELSE 0
END
WHERE status IS NOT NULL AND status !~ '^[0-9]+$';
-- 2. 修改字段类型为 INTEGER
ALTER TABLE triage_queue_item
ALTER COLUMN status TYPE INTEGER USING status::INTEGER;
-- 3. 设置默认值
ALTER TABLE triage_queue_item
ALTER COLUMN status SET DEFAULT 0;

View File

@@ -163,7 +163,7 @@ export function updateCheckPart(data) {
// 查询检查套餐列表
export function listCheckPackage(query) {
return request({
url: '/system/check-package/list',
url: '/system/package/list',
method: 'get',
params: query
})

View File

@@ -372,7 +372,6 @@ import {
} from './diagnosistreatment';
import PopoverList from '@/components/OpenHis/popoverList/index.vue';
import medicineList from './medicineList.vue';
import MedicineList from '../components/medicineList.vue';
import {getCurrentInstance, nextTick, watch} from 'vue';
const { proxy } = getCurrentInstance();
@@ -467,9 +466,9 @@ function calculateTotalPrice() {
try {
let sum = 0;
treatmentItems.value.forEach((item) => {
if (item.adviceDefinitionId && item.retailPrice && item.childrenRequestNum) {
const price = parseFloat(item.retailPrice) || 0;
const count = parseInt(item.childrenRequestNum) || 0;
if (item.adviceDefinitionId && item.adviceDefinitionId !== '') {
const price = Number(item.retailPrice) || 0;
const count = Number(item.childrenRequestNum) || 0;
sum += price * count;
}
});
@@ -479,7 +478,10 @@ function calculateTotalPrice() {
(item) => item.adviceDefinitionId && item.adviceDefinitionId !== ''
);
if (hasValidItem) {
form.value.retailPrice = parseFloat(totalPrice.value);
// 使用 nextTick 确保总价更新后零售价才更新,避免 Vue 响应式时序问题
nextTick(() => {
form.value.retailPrice = parseFloat(totalPrice.value) || 0;
});
} else {
form.value.retailPrice = undefined;
}
@@ -565,15 +567,16 @@ function edit() {
form.value.pricingFlag = 1;
}
// 处理子项数据确保包含retailPrice字段
// 处理子项数据确保包含retailPrice和name字段
if (props.item.childrenJson) {
const parsedItems = JSON.parse(props.item.childrenJson);
treatmentItems.value = parsedItems.map((item) => ({
...item,
name: item.name || '',
retailPrice: item.retailPrice || 0,
}));
} else {
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, retailPrice: 0 }];
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, name: '', retailPrice: 0 }];
}
form.value.permittedUnitCode = form.value.permittedUnitCode
? form.value.permittedUnitCode.toString()
@@ -618,7 +621,7 @@ function reset() {
chrgitmLv: undefined, //医保等级
pricingFlag: 1, // 划价标记,默认允许划价
};
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, retailPrice: 0 }];
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, name: '', retailPrice: 0 }];
totalPrice.value = '0.00';
proxy.resetForm('diagnosisTreatmentRef');
}
@@ -760,7 +763,10 @@ function selectRow(row, index) {
treatmentItems.value[index].adviceDefinitionId = row.id;
treatmentItems.value[index].retailPrice = row.retailPrice || 0;
medicineSearchKey.value = '';
calculateTotalPrice();
// 使用 nextTick 确保 DOM 更新后再计算总价
nextTick(() => {
calculateTotalPrice();
});
}
// 清空诊疗子项

View File

@@ -36,6 +36,7 @@ const emit = defineEmits(['selectRow']);
const diagnosisTreatmentList = ref([]); // 原始数据列表
const filteredList = ref([]); // 过滤后的数据列表
const hasLoaded = ref(false); // 标记是否已加载数据
const isLoading = ref(false); // 标记是否正在加载
// 获取诊疗项目列表
function getList() {
@@ -53,7 +54,7 @@ function getList() {
getDiagnosisTreatmentList({ statusEnum: 2, pageSize: 1000, pageNo: 1 })
.then((res) => {
diagnosisTreatmentList.value =
res.data?.records?.filter((item) => item.childrenJson == null) || [];
res.data?.records?.filter((item) => item.childrenJson == null || item.childrenJson === '') || [];
filterList(); // 初始化过滤
hasLoaded.value = true; // 标记为已加载
})
@@ -62,6 +63,47 @@ function getList() {
});
}
// 服务端搜索(当用户输入搜索关键词时)
function searchList(searchKey) {
if (!searchKey || searchKey.trim() === '') return;
isLoading.value = true;
// 使用较大的pageSize确保搜索结果尽可能多
getDiagnosisTreatmentList({ statusEnum: 2, searchKey: searchKey.trim(), pageSize: 5000, pageNo: 1 })
.then((res) => {
diagnosisTreatmentList.value =
res.data?.records?.filter((item) => item.childrenJson == null || item.childrenJson === '') || [];
filterList();
})
.catch((err) => {
console.error('搜索诊疗项目数据失败:', err);
})
.finally(() => {
isLoading.value = false;
});
}
// 获取预加载数据(不带搜索关键词时使用)
function loadPreloadedData() {
if (props.preloadedData && props.preloadedData.length > 0) {
diagnosisTreatmentList.value = props.preloadedData;
filterList();
hasLoaded.value = true;
return;
}
if (hasLoaded.value) return;
getDiagnosisTreatmentList({ statusEnum: 2, pageSize: 1000, pageNo: 1 })
.then((res) => {
diagnosisTreatmentList.value =
res.data?.records?.filter((item) => item.childrenJson == null || item.childrenJson === '') || [];
filterList();
hasLoaded.value = true;
})
.catch((err) => {
console.error('获取诊疗项目数据失败:', err);
});
}
// 监听shouldLoadData属性变化仅在首次为true时加载数据
watch(
() => props.shouldLoadData,
@@ -86,11 +128,17 @@ watch(
{ immediate: true }
);
// 监听搜索关键词变化,实时过滤数据
// 监听搜索关键词变化,有搜索词时走服务端搜索,否则走本地过滤
watch(
() => props.searchKey,
() => {
filterList();
(newVal) => {
if (newVal && newVal.trim() !== '') {
// 有搜索关键词,走服务端搜索
searchList(newVal);
} else {
// 搜索词为空,使用预加载数据
loadPreloadedData();
}
}
);

View File

@@ -58,10 +58,17 @@ export function singOut(data) {
/**
* 获取患者本次就诊处方
*/
export function getPrescriptionList(encounterId) {
// Add timestamp to bypass browser caching and ensure fresh data is loaded
export function getPrescriptionList(encounterId, generateSourceEnum, sourceBillNo) {
let url = '/doctor-station/advice/request-base-info?encounterId=' + encounterId
if (generateSourceEnum != null) {
url += '&generateSourceEnum=' + generateSourceEnum
}
if (sourceBillNo != null) {
url += '&sourceBillNo=' + encodeURIComponent(sourceBillNo)
}
url += '&t=' + Date.now()
return request({
url: '/doctor-station/advice/request-base-info?encounterId=' + encounterId + '&t=' + Date.now(),
url: url,
method: 'get',
})
}

View File

@@ -381,6 +381,14 @@ const props = defineProps({
activeTab: {
type: String,
},
generateSourceEnum: {
type: Number,
default: null,
},
sourceBillNo: {
type: String,
default: null,
},
});
const isAdding = ref(false);
const isSaving = ref(false); // #437 防重复提交锁
@@ -468,7 +476,11 @@ watch(
function getListInfo(addNewRow) {
isAdding.value = false;
getPrescriptionList(props.patientInfo.encounterId).then((res) => {
getPrescriptionList(
props.patientInfo.encounterId,
props.generateSourceEnum ?? undefined,
props.sourceBillNo ?? undefined,
).then((res) => {
// 为每行数据添加 adviceTypeValue 字段,用于类型下拉框显示
prescriptionList.value = (res.data || []).map(item => {
// 根据 adviceType 和 categoryCode 找到对应的 adviceTypeValue
@@ -868,19 +880,23 @@ function handleDelete() {
proxy.$modal.msgWarning('请选择要删除的项目');
return;
}
let deleteList = groupIndexList.value.map((index) => {
const item = prescriptionList.value[index];
// 只删除待签发且未收费的项目
if (item.statusEnum != 1 || item.chargeStatus == 5) {
return null;
}
// 🔧 Bug #442: 已保存的行必须有有效的 requestId否则跳过避免后端删除不存在的记录
if (item.requestId == null || item.requestId === undefined || item.requestId === '') {
return null;
}
return {
requestId: item.requestId,
dbOpType: '3',
adviceType: item.adviceType,
};
}).filter(item => item !== null); // 过滤掉已签发已收费的项目
}).filter(item => item !== null); // 过滤掉已签发已收费或无 requestId 的项目
if (deleteList.length == 0) {
proxy.$modal.msgWarning('只能删除待签发且未收费的项目');

View File

@@ -41,9 +41,9 @@
</el-col>
</el-row>
<!-- 工作单位 -->
<!-- 工作单位学校 -->
<el-row :gutter="16" class="form-row">
<el-col :span="24" class="form-item">
<el-col :span="12" class="form-item">
<span class="form-label">工作单位学校</span>
<el-input v-model="form.workplace" class="underline-input" />
</el-col>
@@ -1243,11 +1243,14 @@ async function show(diagnosisData) {
const res = await getNextCardNo(orgCode);
if (res.code === 200 && res.data && res.data.length >= 12) {
cardNo = res.data;
} else {
// API返回失败或不合规时生成临时卡号避免保存时 cardNo 为空导致后端校验失败
cardNo = 'TEMP_' + Date.now();
}
// API失败或返回不合规时保持为空字符串由用户手动填写或后端自动生成
} catch (err) {
console.error('获取卡片编号失败:', err);
// 保持为空,不使用不合规的临时值
// API调用异常时生成临时卡号
cardNo = 'TEMP_' + Date.now();
}
form.value = {
@@ -1318,7 +1321,11 @@ async function show(diagnosisData) {
// 系统关联信息
encounterId: patientInfo.encounterId || '', // 就诊ID
patientId: patientInfo.patientId || '', // 患者ID
diagnosisId: diagnosisData?.conditionId || diagnosisData?.definitionId || '', // 诊断ID
diagnosisId: (diagnosisData?.conditionId != null && diagnosisData?.conditionId !== '')
? diagnosisData.conditionId
: (diagnosisData?.definitionId != null && diagnosisData?.definitionId !== '')
? diagnosisData.definitionId
: '', // 诊断ID
};
// 更新selectedDiseases数组
@@ -1373,7 +1380,7 @@ async function buildSubmitData() {
const submitData = {
cardNo: formData.cardNo,
visitId: props.patientInfo?.encounterId || formData.encounterId || null,
diagId: formData.diagnosisId || null,
diagId: formData.diagnosisId ? Number(formData.diagnosisId) : null,
patId: formData.patientId || null,
idType: 1, // 默认身份证
idNo: formData.idNo,
@@ -1426,8 +1433,8 @@ async function buildSubmitData() {
function validateFormManually() {
const errors = [];
// 卡片编号验证至少12位后端自动生成16位编号
if (form.value.cardNo && form.value.cardNo.length < 12) {
// 卡片编号验证至少12位后端自动生成16位编号;临时卡号 TEMP_ 开头允许通过
if (form.value.cardNo && !form.value.cardNo.startsWith('TEMP_') && form.value.cardNo.length < 12) {
errors.push('卡片编号至少12位');
}
@@ -1539,6 +1546,12 @@ async function handleSubmit() {
return;
}
// 检查诊断ID是否有效后端 @NotNull 校验要求)
if (!form.value.diagnosisId) {
proxy.$modal.msgError('诊断信息不完整,请重新选择诊断后重试');
return;
}
// 开始加载状态,防止重复提交
submitLoading.value = true;

View File

@@ -402,6 +402,21 @@
<span class="method-price">¥{{ method.packagePrice || item.price }}</span>
</el-checkbox>
</div>
<!-- 选中方法后显示对应的套餐明细 -->
<div v-if="item.selectedMethod && item.methodPackageDetails && item.methodPackageDetails.length > 0" class="method-package-details">
<div class="method-package-header">
<span class="method-package-title">套餐明细 - {{ item.selectedMethod.name }}</span>
</div>
<div v-for="detail in item.methodPackageDetails" :key="detail.id" class="method-option">
<el-checkbox v-model="detail.checked">
<span class="method-name">{{ detail.name }}</span>
<span class="method-price">数量: {{ detail.quantity }} ¥{{ detail.price }}</span>
</el-checkbox>
</div>
</div>
<div v-if="item.selectedMethod && item.methodPackageLoading" class="method-package-loading">
加载套餐明细中...
</div>
</div>
</div>
</div>
@@ -419,7 +434,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user';
import request from '@/utils/request';
import { listCheckMethod, searchCheckMethod } from '@/api/system/checkType';
import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType';
import { getEncounterDiagnosis } from '../api.js';
const props = defineProps({
@@ -445,7 +460,7 @@ async function loadPackageDetails(row, treeNode, resolve) {
}
try {
const res = await request({
url: `/exam/package/${row.packageId}/details`,
url: `/system/package/${row.packageId}/details`,
method: 'get'
});
if (res.code === 200 && res.data) {
@@ -467,14 +482,28 @@ async function loadPackageDetails(row, treeNode, resolve) {
}
}
// #428: 为已选择项目加载套餐明细
// #428修复: 为已选择项目加载套餐明细通过packageId或packageName查询
async function loadPackageDetailsForItem(item) {
if (!item.isPackage || !item.packageId) {
if (!item.isPackage || (!item.packageId && !item.packageName)) {
return;
}
try {
let packageId = item.packageId;
if (!packageId && item.packageName) {
// CheckPart 没有 packageId 字段,需要通过 packageName 查询获取
const pkgRes = await listCheckPackage({ packageName: item.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
item.packageDetails = [];
return;
}
packageId = packages[0].id;
}
const res = await request({
url: `/exam/package/${item.packageId}/details`,
url: `/system/package/${packageId}/details`,
method: 'get'
});
if (res.code === 200 && res.data) {
@@ -482,7 +511,7 @@ async function loadPackageDetailsForItem(item) {
...detail,
name: detail.name || detail.itemName,
unit: detail.unit || '次',
price: detail.price || detail.itemPrice || 0,
price: detail.price || detail.unitPrice || 0,
quantity: detail.quantity || 1
}));
} else {
@@ -682,6 +711,7 @@ async function handleCategoryExpand(cat) {
code: m.code,
price: m.price || 0,
packageName: m.packageName || '',
packageId: m.packageId || null,
packagePrice: m.packagePrice || null,
serviceFee: m.serviceFee || null
}));
@@ -700,7 +730,9 @@ function handleCollapseChange(activeName) {
setTimeout(() => { isAnimating.value = false; }, 300); // 与 CSS 过渡时长一致
if (activeName) {
const cat = filteredCategoryList.value.find(c => c.typeId == activeName);
// Bug #428修复: 直接从 categoryList原始响应式数组查找分类
// 确保后续 handleCategoryExpand 对 cat.methods 的赋值能正确触发 Vue 响应式更新
const cat = categoryList.value.find(c => c.typeId == activeName);
if (cat && (!cat.methods || cat.methods.length === 0)) {
handleCategoryExpand(cat); // 异步加载,不 await
}
@@ -992,12 +1024,20 @@ function handleRowClick(row) {
selectedItems.value = [];
activeDetailTab.value = 'applyForm';
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
const d = res.data || res;
if (d.data) Object.assign(form, d.data);
if (d.items && Array.isArray(d.items)) {
const resp = res.data || res;
// Bug #408修复: items 在 AjaxResult 顶层(res.items / resp.items),不在 ExamApply 对象内
// 防御性提取:优先取顶层 items兼容嵌套在 resp.data.items 的情况
let rawItems = res.items || resp.items;
if (!rawItems && resp.data && typeof resp.data === 'object') {
rawItems = resp.data.items;
}
rawItems = rawItems || [];
const d = resp.data || resp;
if (d) Object.assign(form, d);
if (Array.isArray(rawItems) && rawItems.length > 0) {
try {
// 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(d.items.map(async m => {
const itemsWithMethods = await Promise.all(rawItems.map(async m => {
const item = {
id: m.itemCode, name: m.itemName,
price: m.itemFee || 0, quantity: 1,
@@ -1025,12 +1065,18 @@ function handleRowClick(row) {
code: md.code,
price: m.itemFee || 0, // fallback 到已保存的价格
packageName: md.packageName || '',
packageId: md.packageId || null,
packagePrice: md.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: md.serviceFee || null
}));
// 如果有已保存的检查方法信息,尝试匹配
if (m.checkMethodId) {
item.selectedMethod = item.methods.find(md => md.id === m.checkMethodId) || null;
// 从已保存的方法中获取套餐信息
if (item.selectedMethod?.packageId) {
item.isPackage = true;
item.packageId = item.selectedMethod.packageId;
}
}
}
} catch (err) {
@@ -1091,6 +1137,13 @@ async function handleMethodSelect(checked, method, cat) {
const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
if (existingItem) {
existingItem.selectedMethod = method;
// 从方法中获取套餐信息
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId;
// 预加载套餐明细
loadPackageDetailsForItem(existingItem);
}
updateMethodDisplay();
return;
}
@@ -1098,14 +1151,15 @@ async function handleMethodSelect(checked, method, cat) {
// 如果该项目不存在,创建一个并关联方法
if (selectedItems.value.length > 0) {
const currentCategory = selectedItems.value[0].checkType;
const newCategory = cat.typeCode || '';
// Bug #428修复: 使用 cat.typeName 进行比较(与 newItem.checkType 保持一致)
const newCategory = cat.typeName || '';
if (currentCategory !== newCategory) {
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
return;
}
}
selectedItems.value.push({
const newItem = {
id: targetItem.id, name: targetItem.name,
price: targetItem.price, quantity: 1,
serviceFee: targetItem.serviceFee || 0,
@@ -1117,9 +1171,16 @@ async function handleMethodSelect(checked, method, cat) {
methods: [method],
selectedMethod: method,
expanded: false,
isPackage: !!targetItem.packageName,
packageId: targetItem.packageId || null
});
// 从方法中获取套餐信息(优先级高于项目本身的 packageName
isPackage: !!method.packageId || !!targetItem.packageName,
packageId: method.packageId || targetItem.packageId || null
};
selectedItems.value.push(newItem);
// 如果是套餐,预加载套餐明细
if (newItem.isPackage && newItem.packageId) {
loadPackageDetailsForItem(newItem);
}
// 自动回填执行科室
if (selectedItems.value.length === 1 && cat?.performDeptName) {
@@ -1164,6 +1225,7 @@ async function handleItemSelect(checked, item, cat) {
code: m.code,
price: m.price || item.price, // fallback 到项目价格
packageName: m.packageName || '',
packageId: m.packageId || null,
packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: m.serviceFee || null // Bug #384修复: 服务费
}));
@@ -1175,7 +1237,8 @@ async function handleItemSelect(checked, item, cat) {
if (selectedItems.value.length > 0) {
const currentCategory = selectedItems.value[0].checkType;
const newCategory = cat.typeCode || '';
// Bug #428修复: 使用 cat.typeName 进行比较(与 effectiveCheckType 保持一致)
const newCategory = cat.typeName || '';
if (currentCategory !== newCategory) {
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
item.checked = false;
@@ -1196,6 +1259,7 @@ async function handleItemSelect(checked, item, cat) {
selectedMethod: null,
expanded: false, // Bug #384修复: 新增展开状态,默认不展开
isPackage: !!item.packageName, // Bug #428修复: 标记是否为套餐
packageName: item.packageName || null, // Bug #426修复: 套餐名称用于查找packageId
packageId: item.packageId || null // Bug #428修复: 套餐ID
});
@@ -1228,21 +1292,68 @@ async function toggleItemExpand(item) {
}
// Bug #384修复: 勾选框选择检查方法(单选逻辑)
function selectMethodCheckbox(checked, item, method) {
async function selectMethodCheckbox(checked, item, method) {
if (checked) {
item.selectedMethod = method;
// 动态加载该方法对应的套餐明细
await loadMethodPackageDetails(item, method);
} else {
item.selectedMethod = null;
item.methodPackageDetails = [];
}
// 联动更新表单检查方法显示字段
updateMethodDisplay();
// #430: 套餐金额实时同步到申请单
nextTick(() => {
form.totalAmount = totalAmountCalc.value;
});
}
// 根据检查方法的packageName加载对应的套餐明细
async function loadMethodPackageDetails(item, method) {
item.methodPackageLoading = true;
item.methodPackageDetails = [];
try {
if (!method.packageName) {
item.methodPackageLoading = false;
return;
}
// 通过packageName查询套餐获取packageId
const pkgRes = await listCheckPackage({ packageName: method.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
item.methodPackageLoading = false;
return;
}
const packageId = packages[0].id;
// 查询套餐明细
const detailRes = await request({
url: `/system/package/${packageId}/details`,
method: 'get'
});
if (detailRes.code === 200 && detailRes.data) {
item.methodPackageDetails = detailRes.data.map(d => ({
id: d.id,
name: d.itemName || d.name,
quantity: d.quantity || 1,
unit: d.unit || '次',
price: d.unitPrice || d.price || 0,
amount: d.amount || d.total || 0,
checked: true // 默认勾选
}));
}
} catch (err) {
console.error('加载方法套餐明细失败:', err);
item.methodPackageDetails = [];
} finally {
item.methodPackageLoading = false;
}
}
// Bug #384修复: 更新检查方法显示字段(联动)
function updateMethodDisplay() {
// 找到第一个有选中检查方法的项目
@@ -1648,6 +1759,29 @@ defineExpose({ getList });
margin-left: 8px;
}
/* 选中方法后显示的套餐明细 */
.method-package-details {
margin-top: 4px;
padding: 4px 0;
border-top: 1px dashed #dcdfe6;
}
.method-package-header {
padding: 2px 0 4px 24px;
}
.method-package-title {
font-size: 10px;
color: #909399;
font-weight: 500;
}
.method-package-loading {
padding: 4px 0 4px 24px;
font-size: 10px;
color: #c0c4cc;
}
/* 折叠组件细节 */
:deep(.el-collapse) {
border: none;

View File

@@ -3326,9 +3326,13 @@ function syncGroupFields(row) {
}
// 同步执行科室
// 🔧 Bug #455: 诊疗类医嘱(adviceType=3)不使用项目配置的执行科室,
// 避免配置ID不在机构树中导致显示原始ID保持患者就诊科室即可
if (row.orgId || row.positionId) {
// 🔧 修复:优先使用项目所属科室(orgId)其次positionId
prescriptionList.value[rowIndex.value].orgId = row.orgId || row.positionId;
if (Number(row.adviceType) != 3) {
// 🔧 修复:优先使用项目所属科室(orgId),其次positionId
prescriptionList.value[rowIndex.value].orgId = row.orgId || row.positionId;
}
}
// 同步皮试标记
@@ -3412,8 +3416,11 @@ async function setValue(row) {
showPopover: false, // 确保查询框关闭
};
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
// 🔧 Bug #455: 执行科室默认逻辑使用positionId(诊疗执行科室配置) → 患者就诊科室不再使用row.orgId(项目所属科室)
prescriptionList.value[rowIndex.value].orgId = row.positionId || props.patientInfo?.orgId;
// 🔧 Bug #455: 诊疗医嘱(adviceType=3)的执行科室默认使用患者就诊科室,
// 不使用positionId(诊疗目录配置的执行科室)避免配置ID不在机构树中导致显示原始ID
if (Number(row.adviceType) != 3) {
prescriptionList.value[rowIndex.value].orgId = row.positionId || props.patientInfo?.orgId;
}
prescriptionList.value[rowIndex.value].dose = row.dose || row.doseQuantity;
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;

View File

@@ -44,6 +44,17 @@ export function getSurgery(queryParams) {
});
}
/**
* 分页查询手术申请单全局不需要encounterId用于门诊手术安排查找弹窗
*/
export function getSurgeryPage(queryParams) {
return request({
url: '/reg-doctorstation/request-form/get-surgery-page',
method: 'get',
params: queryParams,
});
}
/**
* 查询护理医嘱信息
*/

View File

@@ -79,7 +79,7 @@
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="prescriptionNo" label="处方号" width="140" />
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="申请单状态" width="120" align="center">
<template #default="scope">
@@ -118,7 +118,7 @@
<el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="处方号">{{
<el-descriptions-item label="申请单号">{{
currentDetail.prescriptionNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请者">{{
@@ -168,7 +168,7 @@ import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js';
import {getCheck} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js';
import {getDepartmentList} from '@/api/public.js';
const { proxy } = getCurrentInstance();
@@ -293,8 +293,8 @@ const hasMatchedFields = computed(() => {
/** 查询科室 */
const getLocationInfo = () => {
getOrgList().then((res) => {
orgOptions.value = res.data.records;
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
});
};
@@ -306,17 +306,19 @@ const recursionFun = (targetDepartment) => {
name = obj.name;
}
const subObjArray = obj['children'];
for (let index = 0; index < subObjArray.length; index++) {
const item = subObjArray[index];
if (item.id == targetDepartment) {
name = item.name;
if (subObjArray && subObjArray.length > 0) {
for (let index = 0; index < subObjArray.length; index++) {
const item = subObjArray[index];
if (item.id == targetDepartment) {
name = item.name;
}
}
}
}
return name;
};
const handleViewDetail = (row) => {
const handleViewDetail = async (row) => {
console.log('targetDepartment========>', JSON.stringify(row));
currentDetail.value = row;
@@ -324,6 +326,15 @@ const handleViewDetail = (row) => {
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

@@ -41,8 +41,6 @@
<el-option label="全部" value="" />
<el-option label="待签发" value="0" />
<el-option label="已签发" value="1" />
<el-option label="已采集" value="2" />
<el-option label="已收样" value="3" />
<el-option label="报告已出" value="4" />
<el-option label="已作废" value="5" />
</el-select>
@@ -298,8 +296,6 @@ const parseBillStatus = (status) => {
const statusMap = {
'0': '待签发',
'1': '已签发',
'2': '已采集',
'3': '已收样',
'4': '报告已出',
'5': '已作废',
};

View File

@@ -72,6 +72,45 @@
<el-input v-model="form.attention" autocomplete="off" type="textarea" />
</el-form-item>
</el-col>
<!-- 申请类型 -->
<el-col :span="12">
<el-form-item label="申请类型" prop="applicationType" style="width: 100%">
<el-radio-group v-model="form.applicationType">
<el-radio :value="0">普通</el-radio>
<el-radio :value="1">急诊</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- 标本类型 -->
<el-col :span="12">
<el-form-item label="标本类型" prop="specimenName" style="width: 100%">
<el-select v-model="form.specimenName" placeholder="请选择标本类型" style="width: 100%">
<el-option label="血液" value="血液" />
<el-option label="尿液" value="尿液" />
<el-option label="粪便" value="粪便" />
<el-option label="痰液" value="痰液" />
<el-option label="咽拭子" value="咽拭子" />
<el-option label="脑脊液" value="脑脊液" />
<el-option label="胸腹水" value="胸腹水" />
<el-option label="关节液" value="关节液" />
<el-option label="分泌物" value="分泌物" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<!-- 执行时间 -->
<el-col :span="12">
<el-form-item label="执行时间" prop="executeTime" style="width: 100%">
<el-date-picker
v-model="form.executeTime"
type="datetime"
placeholder="选择执行时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
@@ -152,6 +191,9 @@ const form = reactive({
otherDiagnosis: '', // 其他诊断
relatedResult: '', // 相关结果
attention: '', // 注意事项
applicationType: 0, // 申请类型 0-普通 1-急诊
specimenName: '血液', // 标本类型
executeTime: null, // 执行时间
primaryDiagnosisList: [], //主诊断目录
otherDiagnosisList: [], //其他断目录
});
@@ -164,7 +206,7 @@ onMounted(() => {
* type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组)
* */
const projectWithDepartment = (selectProjectIds) => {
const projectWithDepartment = (selectProjectIds, type) => {
//1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置
let isRelease = true;
// 选中项目的数组

View File

@@ -483,7 +483,7 @@ const submit = () => {
encounterId: patientInfo.value.encounterId,
organizationId: patientInfo.value.inHospitalOrgId,
requestFormId: '',
name: '检查申请单',
name: applicationListAllFilter.map(item => item.adviceName).join('、'),
descJson: JSON.stringify(submitForm),
categoryEnum: '2',
}).then((res) => {

View File

@@ -5,16 +5,14 @@
-->
<template>
<div class="surgery-container">
<div class="transfer-wrapper">
<div v-loading="loading" style="min-height: 300px;">
<el-transfer
v-model="transferValue"
:data="applicationList"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
/>
</div>
<div v-loading="loading" class="transfer-wrapper" style="min-height: 300px;">
<el-transfer
v-model="transferValue"
:data="applicationList"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
/>
</div>
<div class="bloodTransfusion-form">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="demo-ruleForm">
@@ -103,8 +101,8 @@ const findTreeItem = (list, id) => {
const emits = defineEmits(['submitOk']);
const props = defineProps({});
const state = reactive({});
const applicationListAll = ref([]);
const applicationList = ref([]);
const applicationListAll = ref();
const applicationList = ref();
const orgOptions = ref([]); // 科室选项
const loading = ref(false); // 加载状态
const getList = () => {

View File

@@ -801,8 +801,8 @@ function clickRowDb(row, column, event) {
return;
}
row.showPopover = false;
// 待签发(已保存 requestId存在)”允许编辑;仅“待保存(无requestId)”允许编辑
if (row.statusEnum == 1 && !row.requestId) {
// 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true;
@@ -1181,19 +1181,27 @@ function handleSave() {
});
// 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可
loading.value = true;
let list = saveList.map((item) => {
const parsedContent = JSON.parse(item.contentJson);
return {
...parsedContent,
adviceType: item.adviceType,
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
uniqueKey: undefined,
// 确保 therapyEnum 被正确传递
therapyEnum: parsedContent.therapyEnum || item.therapyEnum || '1',
};
});
let list = [];
try {
list = saveList.map((item) => {
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) || {} : {};
return {
...parsedContent,
adviceType: item.adviceType,
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
uniqueKey: undefined,
// 确保 therapyEnum 被正确传递
therapyEnum: parsedContent.therapyEnum || item.therapyEnum || '1',
};
});
} catch (error) {
loading.value = false;
isSaving.value = false;
proxy.$modal.msgError('医嘱内容解析失败,请检查待签发医嘱');
return;
}
// 保存签发按钮
isSaving.value = true;
console.log('签发处方参数:', {
@@ -1210,7 +1218,7 @@ function handleSave() {
isSaving.value = false;
getListInfo(false);
bindMethod.value = {};
nextId.value == 1;
nextId.value = 1;
} else {
proxy.$modal.msgError(res.message);
isSaving.value = false;
@@ -1376,13 +1384,21 @@ function handleSaveSign(row, index) {
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value == 1;
nextId.value = 1;
}
});
} else {
if (prescriptionList.value[0].adviceName) {
handleAddPrescription();
}
// 新增行:调用保存接口将数据持久化到后端
row.dbOpType = '1';
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value = 1;
// 保存成功后刷新列表,确保后端返回的数据带 requestId
getListInfo(false);
}
});
// 不需要再添加空行,保存成功后由 getListInfo 处理
}
adviceQueryParams.value.adviceType = undefined;
}
@@ -1429,12 +1445,13 @@ function handleSaveBatch() {
if (row) row.isEdit = false;
});
getListInfo(false);
nextId.value == 1;
nextId.value = 1;
isSaving.value = false;
}
})
.catch((error) => {
isSaving.value = false;
proxy.$modal.msgError(error?.msg || '保存失败,请重试');
});
}
@@ -1562,17 +1579,21 @@ function handleSaveGroup(orderGroupList) {
// 🔥 新版组件已经预处理了数据,优先使用 mergedDetail
const mergedDetail = item.mergedDetail || {
...(item.orderDetailInfos || {}),
adviceName: item.orderDetailInfos?.adviceName || item.orderDefinitionName || '未知项目',
adviceName: item.orderDefinitionName || item.orderDetailInfos?.adviceName || '未知项目',
adviceType: item.orderDetailInfos?.adviceType,
adviceDefinitionId: item.orderDefinitionId || item.orderDetailInfos?.adviceDefinitionId,
quantity: item.quantity,
unitCode: item.unitCode || item.orderDetailInfos?.unitCode,
unitCodeName: item.unitCodeName,
dose: item.dose || item.orderDetailInfos?.dose,
// 🔧 Bug #403 修复dose/doseQuantity/dispensePerDuration 需用 null 检查,
// 避免组套中值为 null 时回退到医嘱库的 orderDetailInfos
dose: item.dose !== undefined && item.dose !== null ? item.dose : item.orderDetailInfos?.dose,
rateCode: item.rateCode || item.orderDetailInfos?.rateCode,
methodCode: item.methodCode || item.orderDetailInfos?.methodCode,
dispensePerDuration: item.dispensePerDuration || item.orderDetailInfos?.dispensePerDuration,
doseQuantity: item.doseQuantity,
dispensePerDuration: item.dispensePerDuration !== undefined && item.dispensePerDuration !== null
? item.dispensePerDuration : item.orderDetailInfos?.dispensePerDuration,
doseQuantity: item.doseQuantity !== undefined && item.doseQuantity !== null
? item.doseQuantity : item.orderDetailInfos?.doseQuantity,
inventoryList: item.orderDetailInfos?.inventoryList || [],
priceList: item.orderDetailInfos?.priceList || [],
partPercent: item.orderDetailInfos?.partPercent || 1,
@@ -1591,20 +1612,21 @@ function handleSaveGroup(orderGroupList) {
setValue(mergedDetail);
// 创建新的处方项目
// 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail已由 setValue 填充完整数据)
const newRow = {
...prescriptionList.value[rowIndex.value],
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
quantity: item.quantity,
methodCode: item.methodCode,
rateCode: item.rateCode,
dispensePerDuration: item.dispensePerDuration,
dose: item.dose,
doseQuantity: item.doseQuantity,
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,
unitCode_dictText: item.unitCodeName || '',
unitCode: item.unitCode ?? mergedDetail.unitCode,
unitCode_dictText: item.unitCodeName || mergedDetail.unitCodeName || '',
statusEnum: 1,
orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时保存 orgName确保树匹配不到时仍有中文名称可显示

View File

@@ -302,6 +302,29 @@ function getSelectRows() {
});
return list;
}
function getTableRef(index) {
return proxy.$refs['tableRef' + index]?.[0];
}
function selectAllRows() {
prescriptionList.value.forEach((item, index) => {
const tableRef = getTableRef(index);
if (!tableRef) {
return;
}
item.forEach((row) => {
tableRef.toggleRowSelection(row, true);
});
});
}
function clearSelection() {
prescriptionList.value.forEach((item, index) => {
getTableRef(index)?.clearSelection();
});
}
function handleRateChange(value, item, row) {
// 拼接当前选中时间
if (value) {
@@ -319,6 +342,8 @@ function handleRateChange(value, item, row) {
defineExpose({
handleGetPrescription,
handleMedicineSummary,
selectAllRows,
clearSelection,
});
</script>

View File

@@ -69,7 +69,11 @@
</div>
<div>
<span class="descriptions-item-label">全选</span>
<el-switch v-model="chooseAll" @change="handelSwicthChange" />
<el-switch
v-model="chooseAll"
:disabled="isDetails != '1'"
@change="handelSwicthChange"
/>
<el-button class="ml20 mr20" type="primary" @click="handleExecute"> 汇总领药 </el-button>
</div>
</div>
@@ -160,24 +164,31 @@ function handleClick(tabName) {
}
function handleGetPrescription() {
prescriptionRefs.value.handleGetPrescription();
chooseAll.value = false;
prescriptionRefs.value?.handleGetPrescription();
}
function handelSwicthChange() {
if (chooseAll.value) {
proxy.$refs['prescriptionRefs'].selectAllRows();
function handelSwicthChange(value) {
if (!prescriptionRefs.value) {
chooseAll.value = false;
return;
}
if (value) {
prescriptionRefs.value.selectAllRows();
} else {
proxy.$refs['prescriptionRefs'].clearSelection();
prescriptionRefs.value.clearSelection();
}
}
function handleRadioChange(value) {
chooseAll.value = false;
if (value == '1') {
handleGetPrescription();
}
}
function handleTherapyChange() {
chooseAll.value = false;
handleGetPrescription();
}
@@ -216,4 +227,4 @@ provide('handleGetPrescription', (value) => {
:deep(.el-tabs__header) {
margin: 0;
}
</style>
</style>

View File

@@ -453,6 +453,10 @@ const loadPatientInfo = () => {
interventionForm.value.startTime = dayjs(res.data.startTime).format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (res.data.inHosTime) {
interventionForm.value.startTime = dayjs(res.data.inHosTime).format(
'YYYY-MM-DD HH:mm:ss'
);
} else {
interventionForm.value.startTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss');
}

View File

@@ -11,9 +11,9 @@
>
<div>
<el-radio-group v-model="type" class="ml20" @change="handleRadioChange">
<el-radio :value="undefined">全部</el-radio>
<el-radio :value="1">长期</el-radio>
<el-radio :value="2">临时</el-radio>
<el-radio :label="null">全部</el-radio>
<el-radio :label="1">长期</el-radio>
<el-radio :label="2">临时</el-radio>
</el-radio-group>
<el-button class="ml20" type="primary" plain @click="handleGetPrescription">
查询
@@ -159,8 +159,7 @@ import {formatDateStr} from '@/utils/index';
const activeNames = ref([]);
const prescriptionList = ref([]);
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
const type = ref(undefined);
const type = ref(null);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const chooseAll = ref(false);

View File

@@ -829,7 +829,9 @@
</el-descriptions>
</div>
<div style="padding: 10px">
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef" />
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1"
:sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div>
</div>
</div>
@@ -872,16 +874,32 @@ import { Loading } from '@element-plus/icons-vue' // 🔧 新增:导入 Loadin
import { getPrescriptionList } from '@/views/clinicmanagement/bargain/component/api'
// API 导入
import { getSurgerySchedulePage, addSurgerySchedule, updateSurgerySchedule, deleteSurgerySchedule, getSurgeryScheduleDetail } from '@/api/surgicalschedule'
import {
getSurgerySchedulePage,
addSurgerySchedule,
updateSurgerySchedule,
deleteSurgerySchedule,
getSurgeryScheduleDetail
} from '@/api/surgicalschedule'
import { listUser } from '@/api/system/user'
import { deptTreeSelect } from '@/api/system/user'
import { listOperatingRoom } from '@/api/operatingroom'
import { getSurgery} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getTenantPage } from '@/api/system/tenant'
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',
method: 'get',
params: query,
skipErrorMsg: true
})
}
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(true)
@@ -1110,7 +1128,7 @@ onMounted(() => {
// 加载卫生机构列表
function loadOrgList() {
getTenantPage({ pageNo: 1, pageSize: 1000 })
getTenantPageSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || res.data || []
@@ -1388,8 +1406,8 @@ async function handleChargeCharge(row) {
orgId: userStore.organizationId || userStore.orgId || userStore.tenantId || 1,
// 添加账户ID
accountId: accountId,
// 添加手术申请单号用于追溯
sourceBillNo: row.applyId,
// 添加手术单号用于关联对应的手术医嘱
sourceBillNo: row.operCode,
//添加计费标志手术计费
generateSourceEnum: 6
}
@@ -1416,9 +1434,12 @@ function closeChargeDialog() {
if (prescriptionRef.value && prescriptionRef.value.closeAllPopovers) {
prescriptionRef.value.closeAllPopovers()
}
showChargeDialog.value = false
chargePatientInfo.value = {}
chargeSurgeryInfo.value = {}
// 等 Vue 完成 DOM 更新后再关闭弹窗,确保 popover 先消失
nextTick(() => {
showChargeDialog.value = false
chargePatientInfo.value = {}
chargeSurgeryInfo.value = {}
})
}
// 🔧 新增:标志位,用于区分是"打开"还是"刷新"
@@ -1447,7 +1468,8 @@ function handleMedicalAdvice(row) {
role: userStore.roles[0],
effectiveOrgId : row.effectiveOrgId,
orgId: userStore.orgId,
positionId: userStore.orgId
positionId: userStore.orgId,
applyId: row.applyId // 手术申请单ID用于过滤关联医嘱
}
// 🔧 关键修复:如果已有提交的医嘱数据,并且是同一个患者的就诊,则使用保存的数据
@@ -1745,7 +1767,7 @@ function handleQuoteBilling() {
// 重新拉取计费药品数据
if (temporaryPatientInfo.value.visitId) {
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
getPrescriptionList(temporaryPatientInfo.value.visitId).then((res) => {
getPrescriptionList(temporaryPatientInfo.value.visitId, 6, temporaryPatientInfo.value.operCode).then((res) => {
if (res.code === 200 && res.data) {
// 🔧 修复:先清空旧数据,避免数据累积
temporaryBillingMedicines.value = []
@@ -2020,7 +2042,7 @@ function handleFindApply() {
getSurgicalScheduleList()
}
// 获取手术申请列表(用于查找”弹窗)
// 获取手术申请列表(用于查找”弹窗)
function getSurgicalScheduleList() {
applyLoading.value = true
const params = { ...applyQueryParams }
@@ -2029,8 +2051,7 @@ function getSurgicalScheduleList() {
params.applyTimeEnd = params.applyTimeRange[1]
delete params.applyTimeRange
}
getSurgery(params).then((res) => {
// Check if data is nested under data.data or directly under data
getSurgeryPage(params).then((res) => {
const responseData = res.data.data || res.data
applyList.value = responseData.records || []
applyTotal.value = responseData.total || 0
@@ -2279,4 +2300,4 @@ function getRowClassName({ row, rowIndex }) {
border-bottom: 1px solid #d9ecff !important;
}
</style>
</style>

View File

@@ -0,0 +1,5 @@
-- Bug #434 修复:为 op_schedule 表添加 incision_level 字段
-- 手术安排表需要存储切口类型,以便在编辑弹窗中正确回显和保存
ALTER TABLE op_schedule ADD COLUMN IF NOT EXISTS incision_level INT2;
COMMENT ON COLUMN op_schedule.incision_level IS '手术切口等级 1-I级切口 2-II级切口 3-III级切口 4-IV级切口';