39 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
37 changed files with 776 additions and 182 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

@@ -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();

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;
/**
@@ -142,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

@@ -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

@@ -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

@@ -161,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

@@ -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

@@ -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

@@ -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级切口';