127 Commits

Author SHA1 Message Date
关羽
5cfb091d0b Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
- 使用 Number() 做 statusEnum 类型转换并用 === 严格比较,避免前后端类型不一致导致双击无响应
- 使用 splice 替代直接赋值更新 prescriptionList,确保 Vue 响应式系统能正确触发渲染更新
- 使用 nextTick 包裹 expandOrder 设置,确保数据更新后再设置展开状态,保证 el-table 正确识别 row-key
- 增加 findIndex 返回 -1 时的错误处理,给用户可见提示而非静默失败
2026-05-15 01:28:07 +08:00
关羽
5bd49fb580 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:28:07 +08:00
关羽
d3d144bb18 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
根因分析:
1. 执行科室显示内码:loadDepartmentOptions 只读取树形结构的第一层(records[0].children),
   但后端返回的是多层嵌套的树形数据。修复为递归扁平化 flattenTree() 提取所有科室节点。
2. 单位字段为空:getUnitCodeOptions 中 unitCode_dictText/minUnitCode_dictText 为 null 时,
   option 的 codeText 为 null 导致 el-select 无法显示。修复为 fallback 到 code 值本身。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:08:05 +08:00
荀彧
6d766b09a7 Fix Bug #498: 【住院医生工作站-检查申请】检查申请列表状态筛选HAVING子句与SELECT映射不一致导致筛选失败
SELECT的CASE映射将status_enum=4映射为3(待接收),但HAVING子句将status_enum=4映射为4,
导致按"待接收"或"已接收"状态筛选时无结果返回。同时修正status_enum=5/6/7的映射从5→7。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:08:05 +08:00
关羽
b3de1dec38 Fix Bug #495: 【医嘱闭环】已校对医嘱无法流转至"医嘱执行"界面,导致费用无法提交执行
后端SQL查询未过滤requestStatus(医嘱请求状态),导致医嘱执行页面的"待执行"tab
返回所有状态医嘱而非仅返回已校对(status=3)的医嘱。修复方式:
1. AdviceProcessAppMapper.java: 新增requestStatus参数
2. AdviceProcessAppMapper.xml: 在med_medication_request和wor_service_request子查询的
   WHERE条件中增加 AND T1.status_enum = #{requestStatus} 过滤
3. AdviceProcessAppServiceImpl.java: 保存requestStatus并传递给mapper,
   替代原注释"后端SQL已通过CASE条件处理"的错误假设

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:21:39 +08:00
荀彧
64325a8493 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表调整"申请单状态"列位置至申请单号后
将列表中的"申请单状态"列从申请者列之后移至申请单号之后,使列顺序为:申请单号→申请单状态→申请者→操作,与检验申请列表保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:21:39 +08:00
荀彧
609f3c3f8a Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
根因分析:FeeDialog组件模板有两个根元素(两个el-dialog),在Vue 3中虽支持多根组件,
但Element Plus的嵌套el-dialog配合append-to-body在多根场景下可能出现渲染/挂载问题。

修复方案:
1. 将两个el-dialog包裹在单一根<div>中,确保组件挂载行为与项目中其他正常工作的嵌套弹窗一致
2. 内层弹窗增加destroy-on-close,确保每次打开时DOM完全重建,避免残留状态导致的不显示问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:21:39 +08:00
荀彧
8af3c6c31c Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
根因分析:
1. el-collapse-item__content 上的 transition: height/max-height 0.3s 与 Element Plus
   内部 accordion 动画冲突,造成"双重动画"效果,表现为切换分类时高度跳变
2. collapse-scroll 的 min-height: 120px 过小,切换内容较少的分类时容器收缩导致布局抖动
3. 分类内"加载中..."提示使用 v-if,出现/消失时引起 collapse content 高度突变

修复策略:
- 移除 el-collapse-item__content 和 el-collapse-item 的自定义 transition 属性,
  让 el-collapse 使用原生动画,消除双重动画
- 增大 collapse-scroll 的 min-height 从 120px 到 350px,确保切换时容器不收缩
- 将加载提示的 v-if 改为 v-show,避免 DOM 插入/移除引起高度跳变

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:21:39 +08:00
荀彧
c24520db74 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:04:59 +08:00
关羽
091bf16df2 Fix Bug #488: 【临床医嘱】双击编辑待签发医嘱,医嘱类型回显为数字且点击确认报接口错误
问题1-医嘱类型回显为数字: 编辑待签发医嘱时,当行的adviceType值(如3/诊疗)
不在当前adviceTypeList选项列表中时,el-select会回显为纯数字。
修复:新增hasAdviceTypeOption和getAdviceTypeLabel函数,当类型无匹配选项时
显示el-tag标签而非空下拉框,避免数字回显。

问题2-点击确认报itemNo接口错误: getBindDevice接口调用无catch处理,
接口失败时promise rejection阻断主流程保存。
修复:为getBindDevice调用链添加.catch()静默降级,确保绑定设备接口失败
不影响医嘱主流程保存。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:04:59 +08:00
关羽
214e82ae29 Fix Bug #476: 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等)
补充打印功能中缺失的核心临床字段:紧急程度、期望检查时间、过敏史、检查目的、病史摘要,
并对urgencyLevel等编码字段应用transformField进行值转换显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:04:59 +08:00
关羽
655436bea9 Fix Bug #467: [住院医生工作站-检验申请] 列表显示信息不规范:标题术语错误且单据名称未展示具体检验项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 00:04:59 +08:00
关羽
4069c452ba Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. 耗材库存报错:lotNumberMatch() 按 encounterId 查询 ALL 待发放 DeviceDispense,
   不区分是否为本次执行的医嘱。若该就诊存在其他未执行的耗材记录且库存为零,
   整个调用就会失败,导致非耗材类医嘱执行也被拦截。
2. 全选联动异常:toggleRowSelection() 程序化选中会触发 @select 事件,
   handleRowSelect 中调用 selectAllCheckboxesInRow 导致级联全选。

修复方案:
- 后端:lotNumberMatch 新增 requestIdList 可选参数,当传入时通过 DeviceRequest.basedOnId
  过滤仅校验与本次执行医嘱关联的耗材记录,避免其他未执行医嘱干扰
- 前端:handleExecute 传入 selectedRequestIds(仅诊疗类医嘱的 requestId)
- 前端:新增 skipSelectCascade 标志,程序化 toggleRowSelection 时阻止 handleRowSelect
  触发 selectAllCheckboxesInRow,消除级联反馈环

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:21:32 +08:00
关羽
683ce749f3 Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
合并验证逻辑:移除独立的"配置列表是否为空"检查,改为在遍历 activityList
时统一验证每个项目的执行科室配置。所有验证在任何数据库操作之前完成,
确保验证失败时不会产生脏数据。同时增加 activityList 为空时的明确提示。
2026-05-14 22:08:37 +08:00
赵云
0c74e66c91 Fix Bug #458: 门诊医生站:诊疗类医嘱保存成功后,列表"医嘱类型"列显示为空值
增强 mapAdviceTypeLabel 函数的兜底映射:在原有表名匹配兜底的基础上,
新增不依赖表名的最终兜底映射(1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术),
确保即使字典缺失或表名不匹配也能正确显示类型标签。

同时修复 getListInfo 中 adviceType_dictText 的空字符串判断逻辑,
使用显式 trim() 检查替代 || 运算符,避免后端返回空字符串时未被重新计算。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:37 +08:00
赵云
0209e8ae99 Fix Bug #451: 门诊医生站-提交新增手术申请后列表刷新失败
根因:子组件 submitForm 成功后同时调用 getList() 和 emit('saved'),
父组件 @saved 也调用 getList(),导致两个并发请求产生竞态条件;
若后端事务尚未完全提交,getList() 查询可能失败并弹出 msgError。

修复:
1. 子组件移除直接 getList() 调用,统一由父组件 @saved 刷新
2. 父组件添加 500ms 延迟确保后端事务已提交
3. getList() 错误处理改为 console.warn 优雅降级,避免阻断弹窗

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:37 +08:00
荀彧
9c75858afc Fix Bug #463: [目录管理-诊疗目录] 新增/编辑弹窗中"诊疗子项"检索功能失效,无法搜到已维护的项目
根因:ActivityDefinitionManageMapper.xml 中 getDiseaseTreatmentPage 查询使用 INNER JOIN
关联 adm_charge_item_definition 价格表,导致 55 个没有价格记录的诊疗项目被完全排除
在搜索结果之外。改为 LEFT JOIN 后,即使项目暂无价格记录也能被搜索到。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:37 +08:00
荀彧
b849d4c605 Fix Bug #445: 门诊手术安排-已生成医嘱的计费项目未从待生成列表剔除
根因:submit后本地过滤逻辑中submittedKeys的字段名不匹配
- originalMedicine中的名称字段是adviceName而非medicineName,导致
  名称+规格+数量的匹配键无法正确匹配已提交项
- 修复:增加chargeItemId作为首选匹配标识(后端唯一ID最可靠),
  名称匹配增加adviceName字段兜底

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
荀彧
a4de88a6f9 Fix Bug #444: 门诊手术医嘱界面"已引用计费药品"列表未正常显示药品名称
根因分析:
1. 前端 handleMedicalAdvice 调用 getPrescriptionList 时只传 encounterId,
   未传 generateSourceEnum=6 和 sourceBillNo(手术单号),
   后端默认查询 generateSourceEnum=1(医生开立),漏掉了手术计费创建的药品记录
2. 后端 getRequestBaseInfo 在 sourceBillNo 存在时会过滤掉药品(adviceType=1),
   进一步阻断了手术计费药品的返回

修复方案:
- 前端:handleMedicalAdvice 传参 (visitId, 6, operCode) 匹配手术计费记录
- 后端:移除手术计费场景的药品过滤,前端各组件已按 adviceType 自行过滤

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
关羽
ad21c566f0 Fix Bug #452: 领用出库模块选择药品时提示"仓库数量为0,无法调用",与实际库存数据不符
严格批号查询返回记录但 orgQuantity=0 时,原代码直接调用 applyFromDto 并弹出警告,
未回退到非严格查询(不含 lotNumber)获取同仓库其他有库存的批号。
修复:在 applyFromDto 之前检查 orgQuantity > 0,数量为0时回退到非严格查询。
2026-05-14 22:08:36 +08:00
荀彧
21ac9e80f2 Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
修复 loadMethodPackageDetails 函数中套餐明细 API 地址错误(/system/package/ → /system/check-type/package/),导致套餐明细加载失败返回 404

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
赵云
9cfaa7dc41 Fix Bug #402: 住院医生站诊断录入:点击保存诊断后,列表出现重复记录且部分条目元数据缺失
根因:后端 saveDoctorDiagnosis 先删除所有 tcm_flag=0 的记录,再用旧 encounterDiagnosisId
调用 saveOrUpdate,由于记录已删除,UPDATE 失败后 fallback 到 INSERT 导致重复记录。

修复:
1. 后端:不再设置 encounterDiagnosisId,确保 saveOrUpdate 始终执行 INSERT
2. 前端:getList() 后对诊断列表按 ybNo/name 去重,防止重复显示
3. 前端:保存前补全 diagnosisDoctor 和 diagnosisTime 元数据
4. 前端:修复 getTcmDiagnosis 的空值安全访问(res.data?.illness?.length)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
赵云
eb18a2c800 Fix Bug #435: 门诊手术安排:编辑弹窗中"费用类别"字段数据未回显
根因:getSurgeryScheduleDetail 的 SQL 查询中引用了 fc.contract_name 但
未 JOIN fin_contract 表(以及关联的 adm_encounter、adm_account),导致
PostgreSQL 报错 "missing FROM-clause entry for table fc",接口返回失败,
前端费用类别字段无法获取数据。

修复:添加缺失的三表 JOIN(adm_encounter → adm_account → fin_contract),
并移除重复的 os.fee_type AS feeType 别名。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
cf5431cbbf bug499 【住院医生工作站-检查申请】检查申请列表缺失查询过滤功能,不符合临床高效检索要求 数据库语句报错修复 2026-05-14 22:08:36 +08:00
荀彧
a429f77113 Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
1. handleMethodSelect 中新增/更新已选项时,设置 expanded=true 使套餐明细自动展开
2. toggleItemExpand 中改用 packageDetailsDisplay/carrier.packageDetails 判断是否已加载明细
   (原代码检查非响应式的 item.packageDetails,导致重复加载或加载判断失效)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:08:36 +08:00
关羽
ffa709cf4e Fix Bug #443: 手术计费:点击"签发"耗材时异常报错
根因:签发耗材时 handDevice 方法会重复调用 saveOrUpdate 更新已有的 DeviceRequest 记录,
仅设置了部分字段(可能为 null),导致关键字段 performLocation(发放库房)被覆盖为空。
随后 handleDeviceDispense 创建 DeviceDispense 时 locationId 为 null,触发报错。

修复:签发操作(SIGN_ADVICE)跳过 handDevice 处理。因为耗材请求在保存时已创建完成,
签发只需更新状态(下方批量更新逻辑已处理),无需重新走 insert/update 流程。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 18:15:34 +08:00
关羽
571f254d0e Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,“检查明细”标签页显示“暂无数据”
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:19:52 +08:00
关羽
560813d009 Fix Bug #412: 门诊医生站:传染病报告卡保存失败,提示报错
BeanUtils.copyProperties 不支持 DTO 中 LocalDate/LocalDateTime 到实体中 java.util.Date 的类型转换,导致 onsetDate、diagDate、reportDate、deathDate 等日期字段在拷贝后为 null。新增手动类型转换逻辑确保日期字段正确保存。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:16:39 +08:00
31c2acb4ef bug516 2026-05-14 16:47:21 +08:00
关羽
254de01d2e Fix Bug #523: [住院医生站-临床医嘱] 待保存医嘱总金额显示缺失且编辑态单位选择框变为数字控件
- 总金额列显示横杠: 在 setValue 中为药品类医嘱初始化 totalPrice(有 quantity 时按单价计算,否则为 '0'),确保待保存医嘱的总金额列能正常回显
- 单位选择框变数字控件: setValue 中将 unitCode/doseUnitCode/minUnitCode 统一转为 String 类型,避免 el-select 因值类型不匹配而渲染异常

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:31:02 +08:00
关羽
e21122edf0 Fix Bug #516: [住院医生站-临床医嘱-检验申请] 检验申请单手动填写的"发往科室"与生成的医嘱执行科室不一致
根因:后端 RequestFormManageAppServiceImpl.saveRequestForm() 完全忽略了前端传入的 positionId(用户手动选择的发往科室),
始终从 activityOrganizationConfig 配置表中查询执行科室,导致用户手动选择的科室被默认配置覆盖。

修复:在收集执行科室时优先使用 activitySaveDto.getPositionId()(前端传入的用户选择),
仅在前端未传入时 fallback 到配置表中的执行科室。

**后端开发重点**:优先搜索 Java/Spring 后端代码。
关键词:Controller, Service, Mapper, API, 接口, 数据查询
搜索目录:openhis-server-new/src/, his-repo/src/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:30:13 +08:00
关羽
e9576ddfa8 Fix Bug #517: [库房管理-领用管理] 业务逻辑校验缺失:允许保存并提交领用数量大于库存数量(零库存领用)的单据
在 RequisitionIssueAppServiceImpl.addOrEditIssueReceipt() 中新增库存校验逻辑,
批量保存时校验领用数量是否超过源仓库实际库存,不足时抛出 ServiceException 阻断保存。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:26:35 +08:00
关羽
b435de9e7b Fix Bug #519: [门诊医生站-诊断-报卡] 已完成传染病报卡的诊断在再次点保存时重复弹出报卡界面
根因:handleInfectiousDiseaseReport() 仅根据诊断名称匹配传染病,未校验该诊断是否已有已提交的报卡记录。

修复方案:
1. 后端 DiagnosisQueryDto 新增 hasInfectiousReport 字段
2. getEncounterDiagnosis SQL 通过 EXISTS 子查询关联 infectious_card 表,
   判断是否存在 status >= 1(已提交/已审核/已上报)的报卡记录
3. 前端 handleInfectiousDiseaseReport() 过滤掉 hasInfectiousReport === 1 的诊断,不再弹出报卡

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:16:46 +08:00
赵云
bc13fd6968 Fix Bug #522: [住院护士站-三测单] 体征录入点击保存后缺乏执行反馈且窗口异常自动关闭
- 添加保存成功提示(proxy.msgSuccess)
- 移除保存成功后自动关闭弹窗的逻辑(closeDialog),保持弹窗开启方便护士核对历史记录和继续录入

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:16:31 +08:00
赵云
d9ad63397b Fix Bug #518: [门诊医生工作站-诊断-传染病报卡] 报卡页面缺失"性别、出生日期、实足年龄"核心字段
根因:infectiousDiseaseReportDialog.vue 读取患者性别时使用了错误的字段名
patientInfo.sex || patientInfo.genderName,但门诊医生站API返回的字段是
genderEnum(数字:1=男,2=女)和genderEnum_enumText(文本:男/女)。
新增 normalizeSexFromPatientInfo 函数,兼容HIS系统所有可能的性别字段命名。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:15:14 +08:00
关羽
bb3e1e300d Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
根因:FeeDialog 内嵌套的"划价组套选择" el-dialog 使用了 append-to-body 但未设置 z-index。
在 Element Plus 中,外层补费弹窗 z-index 约 2000,遮罩层 z-index 2001。内层组套弹窗虽通过
append-to-body 挂载到 body,但其 z-index 2002 可能被外层遮罩遮挡(渲染时序问题),导致弹窗
实际渲染但不可见,表现为"无任何响应"。

修复:为内层 el-dialog 添加 :z-index="3000",确保其渲染在外层弹窗遮罩之上。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:09:53 +08:00
关羽
46358ea03d Fix Bug #455: 门诊医生站-医嘱:开立诊疗医嘱时执行科室默认获取逻辑有误且显示为原始ID
移除else分支中对orgId和positionName的条件判断,确保诊疗类医嘱的执行科室
始终使用患者就诊科室,不被诊疗目录配置的positionId覆盖。
之前的if (!orgId)条件导致目录已配置positionId时不会被覆盖,
若目录配置的ID不在机构树中则显示原始ID。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:08:26 +08:00
b5c308d9cb 修复bug 2026-05-14 15:54:00 +08:00
关羽
adfeb8f5e5 Fix Bug #510: [住院医生工作站] 进入页面报错
修复模板中的 Markdown 代码块标记污染(```vue/``` 作为文本渲染),
并恢复被意外移除的 SummaryDrugApplication 组件导入及 ref 声明,
解决页面加载时组件未定义错误和页面渲染异常。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 15:28:21 +08:00
赵云
fd9309f125 Fix Bug #494: 住院医生工作站-检查申请:申请单名称显示为通用名称,未展示具体检查项目名称
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 15:19:00 +08:00
关羽
46affb424e Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
1. FeeDialog.vue - getUnitCodeOptions 修复:当 unitCode/minUnitCode 为 null 但对应字典文本存在时,使用文本作为选项值兜底,确保单位下拉框不显示为空
2. newfeeDetailQuery.vue - getLocationInfo 修复:从单一 records[0].children 解析改为支持树形/扁平/数组多种响应结构,并添加 catch 兜底置空数组
3. newfeeDetailQuery.vue - selectOrg 修复:查找失败时返回 '-' 而非显示原始 orgId 内码,空值同样返回 '-'

**后端开发重点**:优先搜索 Java/Spring 后端代码。
关键词:Controller, Service, Mapper, API, 接口, 数据查询
搜索目录:openhis-server-new/src/, his-repo/src/
2026-05-14 15:12:07 +08:00
荀彧
6dcee26b54 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据
- 提交成功后直接调用 getList() 刷新手术申请列表,不再仅依赖父组件的 emit('saved') 事件
- 修复原因:父组件通过 surgeryRef?.getList() 可选链调用可能因 ref 未就绪或时序问题导致静默跳过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 15:11:43 +08:00
关羽
a282234bb0 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因分析:
1. SQL CASE 映射不完整:status_enum=3(COMPLETED) 直接映射为应用状态 4(已接收),
   跳过了 2(已校对) 和 3(待接收)
2. status_enum=8 在数据中存在但枚举类中缺失定义
3. 前端已完整实现状态列和交互逻辑,问题在后端返回的状态值不正确

修复内容:
- RequestFormManageAppMapper.xml: 重构 SQL CASE 语句
  - status_enum=3 + performer_check_id 有值 → 2(已校对),利用护士校对标记区分
  - status_enum=3 + performer_check_id 为空 → 4(已接收)
  - status_enum=4(ON_HOLD) → 3(待接收)
  - status_enum=5/6/7 → 7(已作废)
  - status_enum=8 → 6(已出报告)
- RequestStatus.java: 补充 COMPLETED_REPORT(8) 枚举值

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:26:38 +08:00
荀彧
52fc64c71d Fix Bug #477: 住院医生工作站-检查申请详情弹窗中"发往科室"字段显示为短横线
根因: examineApplication.vue 的 handlePrint 函数调用了未定义的 recursionFun 方法
(ReferenceError),handleViewDetail 使用 findTreeItem 但该方法对后端返回的扁平
科室列表解析不够健壮。与 testApplication.vue 对比后,发现缺少 recursionFun 函数定义。

修复策略: 新增 recursionFun 函数(与 testApplication.vue 一致实现),并在
handleViewDetail 和 handlePrint 中统一使用该方法将 targetDepartment ID 转换为科室名称。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:21:51 +08:00
关羽
0bd1277307 Fix Bug #495: 【医嘱闭环】已校对医嘱无法流转至"医嘱执行"界面,导致费用无法提交执行
Root cause: In getInpatientAdvicePage(), encounterIds and exeStatus were nullified
before buildQueryWrapper to prevent auto-generated SQL conditions, but requestStatus
was NOT nullified. HisQueryUtils.buildQueryWrapper uses reflection to add eq conditions
for ALL non-null fields, so requestStatus: 3 became an extra SQL filter
"AND request_status = 3" that was not intended for the 医嘱执行 page.

The 医嘱执行 page uses exeStatus (not requestStatus) for execution state filtering.
The SQL already handles verified/unverified order filtering via a CASE condition
on status_enum and performer_check_id. The requestStatus parameter is only meant
for frontend tab selection and should not be used as a SQL filter here.

Fix: Nullify requestStatus before buildQueryWrapper, same as encounterIds/exeStatus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:14:56 +08:00
赵云
e0e4c2bcc6 Fix Bug #471: 手术管理-门诊手术安排:手术申请查询结果中混入住院检验申请单数据(脏数据)
根因:门诊手术安排查询弹窗调用 /reg-doctorstation/request-form/get-surgery-page 接口,
SQL 过滤 type_code = '24',但实际手术申请单的 type_code 存储为 'SURGERY'(非'24'),
导致查询返回0条手术记录。同时检验申请单(type_code='22')使用 PAR 前缀处方号,在缺少
type_code 有效过滤时可能混入结果。

修复:将 SQL 过滤器从 type_code = #{typeCode} 改为 type_code IN (#{typeCode}, 'SURGERY'),
兼容两种 type_code 值,确保只返回手术申请单,排除检验/检查申请单数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:13:47 +08:00
关羽
41bea23116 Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
1. 修复删除/撤回接口参数错误:前端传prescriptionNo但后端期望requestFormId
2. 修改handleEdit从占位提示改为打开编辑弹窗,复用laboratoryTests组件并传入editData
3. laboratoryTests新增editData prop和编辑模式:支持descJson表单回显、已选项目回填、提交时携带requestFormId

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:12:28 +08:00
关羽
12382503f4 Fix Bug #489: 【医嘱闭环】医生站签发单条长期药品医嘱,护士校对界面生成重复(两条)待校对记录
Root cause: The SQL query in AdviceProcessAppMapper.xml used a plain LEFT JOIN with
med_medication_dispense on med_req_id. When a single medication request had multiple
dispense records (e.g., from repeated executions or summary operations), the JOIN
produced multiple rows per request — up to 222 rows for one request. SELECT DISTINCT
could not deduplicate because dispense_status values differed across rows.

Fix: Replace the plain LEFT JOIN with LEFT JOIN LATERAL (subquery ORDER BY create_time
DESC LIMIT 1) to fetch only the most recent dispense record per medication request.
This ensures exactly one row per request regardless of how many dispense records exist.

Verified: SQL query now returns 0 duplicate rows across all medication requests.
2026-05-14 13:10:51 +08:00
赵云
ae50a7042e Fix Bug #468: [住院医生工作站-检验申请] 列表页新增【单据状态】列
列表页增加单据状态列(位于申请单号之后),使用 el-tag 显示状态:
- applyStatus=0 显示"待开立"(灰色标签)
- applyStatus=1 显示"已开立"(绿色标签)
- 已收费且已执行显示"已执行"(绿色标签)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:06:59 +08:00
赵云
9b1ac64cd6 Fix Bug #464: [目录管理-诊疗目录] 新增项目时"零售价"未与"诊疗子项"合计总价自动同步
根因:calculateTotalPrice中form.value.retailPrice赋值被nextTick包裹,
在多调用方(watcher/selectRow/addItem)并发时产生竞态,导致零售价更新丢失
修复:移除nextTick,改为同步赋值确保零售价实时同步总价

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:03:46 +08:00
Ranyunqiao
6367654ada 476 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等) 2026-05-14 12:56:04 +08:00
关羽
360256e589 Fix Bug #465: [住院医生工作站-检验申请] 检验项目选择列表被限制为500项,导致医生无法检索并开立其余800多项
问题根因:
- 前端使用 pageSize=500 分页拉取数据,el-transfer 组件客户端过滤在 1400+ 条数据下存在渲染和搜索性能问题
- 数据库实际有 1400 项已启用的检验类诊疗项目,但仅加载了 500 项

修复方案:
1. 改用 pageSize=9999 一次性拉取全部数据,消除分页导致的 500 项截断
2. 新增顶部搜索框,支持按项目名称/拼音首拼/业务编号实时过滤
3. 使用 computed 属性动态生成 transfer 组件数据,搜索时自动过滤
4. 显示总数统计(未搜索时显示总数,搜索时显示匹配数/总数)
5. 移除不再需要的 applicationList 变量引用和 onBeforeMount 空调用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:23:13 +08:00
荀彧
feb033b857 Fix Bug #462: [目录管理-诊疗目录] 编辑弹窗中"所需标本"下拉框数据加载失败,显示为"无数据" Fix: selectDictDataByType方法移除Redis缓存读取逻辑,直接查询数据库避免缓存为空数据导致前端下拉框无数据 2026-05-14 12:15:47 +08:00
wangjian963
79cce458ee Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-server-new/openhis-application/src/main/java/com/openhis/web/clinicalmanage/dto/OpScheduleDto.java
2026-05-14 12:01:33 +08:00
wangjian963
1140912f3a Fix Bug #437: 【门诊手术计费】保存签章TOCTOU竞态致重复提交,且耗材计费项目缺失/重复、手术单号未关联
Fix: 频次总量计算改用字典store动态读取,el-input-number新增@input实时计算
2026-05-14 12:00:18 +08:00
250f9ce258 Merge remote-tracking branch 'origin/develop' into develop 2026-05-14 11:48:42 +08:00
0d6f891b47 fix bug434:门诊手术安排:编辑弹窗中“切口类型”字段未正确回显数据
bug426:门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细
bug439:领用出库:选择领用药品后“总库存数量”列数据未显示
bug457:门诊收费:已签发的手术类医嘱在门诊收费列表中不显示项目名称
2026-05-14 11:48:22 +08:00
Ranyunqiao
e68be3be79 Merge remote-tracking branch 'origin/develop' into develop 2026-05-14 11:47:41 +08:00
Ranyunqiao
eab0119c19 bug362 413 498 504 507 2026-05-14 11:47:18 +08:00
关羽
3ad9ff85d4 Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. lotNumberMatch 调用传入了所有在科患者的 encounterId(来自 patientInfoList),
   而非仅选中医嘱对应的 encounterId。若其他患者存在 PREPARATION 状态的耗材发放记录
   但无匹配库存,API 返回"发耗材单生成失败,请检查耗材库存"错误
2. handleExecute 缺少 .catch() 处理器,API 调用失败时 UI 状态不一致,
   导致列表刷新后全选联动异常

修复策略:
- lotNumberMatch 仅传入选中医嘱对应的 encounterId(去重过滤),避免无关患者耗材记录干扰
- 新增空选择校验,未选中医嘱时提示用户而非直接调接口
- 为 handleExecute 添加 .catch() 处理器,API 失败时给出友好提示
- lotNumberMatch 增加 .then() 检查返回码,确保 error 被正确捕获

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:31:30 +08:00
关羽
ab2f580d60 Fix Bug #453: 住院医生站-临床医嘱:开立医嘱时输入"级护理"检索结果显示"暂无数据"
根因分析:
1. adviceTypes 参数曾被序列化为 URL 编码字符串 '1%2C2%2C3%2C6',后端无法解析为 List<Integer>,
   导致 SQL 查询返回空结果。Bug #486 已修复此问题(改为数组格式)。
2. 补充修复:当行未选择医嘱类型时(adviceType='' 或 undefined),parseInt('') 返回 NaN,
   导致 adviceTypes=[NaN],所有子查询被跳过。改为传入空字符串,让 refresh 函数根据
   searchKey 自动选择跨类型搜索。
3. 增加 catch 块错误日志,避免 API 失败时静默吞掉错误。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:26:10 +08:00
荀彧
665d4ae47a Fix Bug #451: 门诊医生站-提交新增手术申请后列表刷新失败
submitForm 提交成功后同时触发 emit('saved') 和 proxy.$nextTick(getList()),
导致两次并发调用 getList(),其中一次失败弹出"数据加载失败"错误提示。
移除冗余的 nextTick(getList()) 调用,由父组件 @saved 事件统一负责刷新。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:20:30 +08:00
关羽
d43a06c343 Fix Bug #475: 【住院医生工作站】开立检查申请单报错后仍生成申请记录
根因:saveRequestForm方法的预校验循环和主循环分别独立查询activityOrganizationConfig获取positionId,
存在数据不一致风险——预校验通过但主循环中positionId查找失败时,RequestForm已被保存导致脏数据。

修复:将预校验循环中查到的positionId缓存到Map中,主循环直接使用缓存结果,
避免重复查询导致的数据不一致问题。确保所有校验通过后再执行任何数据库操作。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:15:20 +08:00
赵云
a7a33eb5f6 Fix Bug #445: 【手术管理-门诊手术安排】临时医嘱生成界面逻辑错误:已生成医嘱的计费项目未从"待生成"列表中剔除
根因:提交成功后,父组件使用 requestId/chargeItemId 匹配已提交项目来过滤
待生成列表,但这些字段在新建医嘱时往往为空,导致匹配失败,已生成的项目
仍保留在"待生成"列表中。

修复:
1. handleTemporaryMedicalSubmit: 改用稳定的字段组合(药品名称+规格+数量)
   匹配已提交项目,从 temporaryBillingMedicines 中移除
2. handleMedicalAdvice: 首次打开弹窗时过滤掉已有 requestId 的项目
3. handleQuoteBilling: 引用计费/刷新时同样过滤掉已有 requestId 的项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:06:38 +08:00
赵云
444397e868 Fix Bug #435: 门诊手术安排:编辑弹窗中"费用类别"字段数据未回显
根因:OpCreateScheduleDto 缺少 feeType 字段,导致新建手术安排时 BeanUtils.copyProperties 无法复制该字段,
保存到数据库后 fee_type 为空字符串/null,编辑时详情查询返回 null 导致前端不显示。

修复:在 OpCreateScheduleDto 新增 feeType 字段,使创建流程完整传递费用类别数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:28:48 +08:00
荀彧
d964155fb8 Fix Bug #428: 门诊医生站-检查申请:未实现分类联动检查方法及套餐明细展示与勾选逻辑
- 分类对象初始化时增加 methods: [],确保 Vue 响应式追踪分类下检查方法的加载
- handleMethodSelect 创建新项目时复制 cat.methods 全部方法数组(原只放单个方法),允许用户在右侧面板切换其他方法
- handleMethodSelect 新增/更新项目时同步 packageName 字段,确保 toggleItemExpand 能通过名称查找并加载套餐明细

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:15:06 +08:00
关羽
b88e011459 Fix Bug #441: 门诊手术安排:手术室护士角色进入页面提示"无权限"且"获取卫生机构列表失败"
根因:响应拦截器中 skipErrorMsg: true 仅抑制了弹窗提示,但仍返回 Promise.reject,
导致 .catch() 路径仍可能触发错误消息或异常行为。
修复:当 skipErrorMsg 为 true 且返回业务错误码(403/500/601等)时,改为 Promise.resolve(res.data),
让 .then() 分支通过 res.code !== 200 判断实现静默降级,不触发 .catch()。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:12:47 +08:00
关羽
492a51d282 Fix Bug #455: 门诊医生站-医嘱:开立诊疗医嘱时执行科室默认获取逻辑有误且显示为原始ID
根因:setValue() 中通过展开运算符(...JSON.parse(JSON.stringify(row)))将诊疗目录
的 positionId/orgId 带入处方列表,后续条件判断只处理非诊疗类型(advicetype != 3),
导致诊疗类的 catalog ID 未被覆盖,且该 ID 不在机构树中,el-tree-select 显示原始数字。

修复:
1. setValue() 中显式为诊疗类(adviceType=3)设置 orgId/positionId 为患者就诊科室,
   并同步 positionName 为机构树中的名称
2. handleSaveGroup() 组套应用时同样对诊疗类使用患者就诊科室,不使用目录配置的ID

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 10:08:20 +08:00
赵云
34774411eb Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
- 已选择面板的套餐项增加"套餐"标签,便于用户识别
- 展开/收起图标改为 ArrowRight 旋转样式,符合标准交互习惯
- toggleItemExpand 函数增加 packageName 兜底判断,不强制依赖 isPackage 标记
- loadPackageDetailsForItem 添加 loading 状态和更健壮的 packageId 解析逻辑
- 新增 expanded-content 和 package-loading-hint CSS 样式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:54:21 +08:00
关羽
0f1b29fcea Fix Bug #452: 领用出库模块选择药品时提示"仓库数量为0,无法调用",与实际库存数据不符
根因:药品目录列表中返回的lotNumber是任意仓库中的批号,但getCount查询时用该lotNumber过滤用户选择的仓库库存。若该批号在目标仓库不存在(但其他批号存在),则返回0条记录导致误报"仓库数量为0"。
修复:在领用出库的handleLocationClick中移除getCount的lotNumber参数,改为查询该药品在所选仓库的所有批号库存。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:25:13 +08:00
荀彧
d64ca5b8ee feat: 手术管理列表点击行高亮 (highlight-current-row) 2026-05-14 09:24:22 +08:00
关羽
faa0b1a61f Fix Bug #446: 【手术管理-门诊手术安排】临时医嘱生成后界面非法关闭且按钮名称/功能显示不一致
根因: handleMedicalAdvice 中盲目重置 temporarySigned.value = false,导致重新打开医嘱弹窗时按钮状态错误。

修复:
1. index.vue: 根据已有医嘱数据是否有 requestId 来决定 temporarySigned 状态,而非盲目重置
2. temporaryMedical.vue: 新增 allItemsSubmitted 计算属性,当所有计费药品已提交(requestId)时显示"已签发"按钮并禁用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:18:31 +08:00
关羽
33c76c786c Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,"检查明细"标签页显示"暂无数据"
根因:handleRowClick 中 const resp = res.data || res 对 Axios 拦截器已解包的响应
进行二次解包,导致 resp 被赋为 ExamApply 实体对象(不含 items),后续 items 提取
逻辑始终返回空数组,明细列表无法加载。

修复:用 res.code !== undefined 判定 res 是否已是 AjaxResult 体,若是则直接使用,
否则再执行 res.data 解包。items 和数据提取统一从正确层级取值,避免二次解包。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:17:42 +08:00
赵云
1a770ca0ee Fix Bug #402: 住院医生站诊断录入:点击保存诊断后,列表出现重复记录且部分条目元数据缺失
根因分析:
1. 前端保存按钮无防重复点击保护,连续点击会发送多个请求
2. 保存成功后前端使用本地排序数据更新,未从服务器重新加载,导致前后端数据不一致
3. 后端 saveDoctorDiagnosis 保存后未回写 encounterDiagnosisId,后续保存无法正确更新已有记录

修复方案:
- 前端:在 handleSaveDiagnosis 入口增加 isSaving 守卫,防止重复提交
- 前端:保存成功后调用 getList() 从服务器重新加载数据,确保前后端一致
- 后端:saveOrUpdate 后回写 encounterDiagnosisId 到返回参数,前端可跟踪记录ID

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:11:12 +08:00
关羽
ba9c18b6a4 Fix Bug #443: 手术计费:点击"签发"耗材时异常报错
根因分析:getRequestBaseInfo SQL查询的UNION 2(门诊术中计费耗材)缺少generate_source_enum过滤条件,导致手术计费弹窗显示所有来源的耗材(包括医生站开立的项目)。当用户尝试签发非手术计费创建的耗材时,后端handDevice处理失败。

修复内容:
1. 后端SQL:在UNION 2的WHERE子句中添加generate_source_enum过滤,确保手术计费弹窗仅显示手术计费来源的耗材
2. 前端JS:handleSave函数补充.then/.catch错误处理,避免显示笼统的"后端程序异常",改为展示具体错误信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:03:38 +08:00
关羽
ecc5c75418 Fix Bug #405: 住院医生工作站:临床医嘱保存成功后,医嘱条目仍处于可编辑状态(未锁定展示)
根因:handleSaveBatch 保存成功后,原有修复通过 uniqueKey 查找 prescriptionList 中的行并设置 isEdit=false,
但由于 saveList 中的 item 本身就是 prescriptionList 中对象的同一引用,通过 find(uniqueKey) 查找存在匹配失败的风险。

修复:直接对 saveList 中的对象引用设置 isEdit=false(同引用无需查找),并兜底遍历所有 statusEnum==1 的行锁定。
同时清空 expandOrder 展开状态,确保医嘱行完全回到只读展示模式。
2026-05-14 08:58:42 +08:00
荀彧
164ac604fb Fix Bug #401: 门诊完诊审计日志错误:div_log 表中 pool_id 与 slot_id 存值与设计规范不符
根因:completeEncounter 方法中先将队列状态更新为 COMPLETED,再用内存中
的 queueItem.getStatus() 判断是否已完诊,导致 queueAlreadyCompleted 始终为 true,
div_log 审计日志永远不会被写入。

修复:在更新队列状态之前记录原始完成状态(queueWasAlreadyCompleted),
用该值判断是否需要写入 div_log,确保完诊时正确生成审计日志。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 08:52:04 +08:00
关羽
d19ceab70f Fix Bug #510: [住院医生工作站] 进入页面报错
根因:order/index.vue 中 getList() 在模块顶层执行(非生命周期钩子),
组件导入时立即触发 API 调用,此时患者尚未选择导致 encounterId 为 undefined;
同时 getListInfo() 缺少患者选择守护检查,多处 API 以空参数调用后端引发循环报错。

修复:
1. 将 getList() 从模块顶层移至 onMounted() 生命周期钩子
2. 在 getListInfo() 开头添加 patientInfo.encounterId 守护检查
2026-05-14 06:19:44 +08:00
关羽
753768a1f0 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
1. getList() 增加 res.code === 200 校验,避免API返回错误数据时静默赋值导致列表不更新
2. 父组件 @saved 事件处理器增加 surgeryRef?.getList(),确保提交后父组件侧也触发列表刷新
3. 统一响应处理模式,与 inspectionApplication 等组件保持一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:17:41 +08:00
关羽
49889e9140 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
后端SQL修复: DoctorStationAdviceAppMapper.xml 中诊疗项 min_unit_code 硬编码为空字符串,
改为使用 permitted_unit_code,使前端单位下拉框有可用选项

前端修复:
1. api.js getOrgList 添加 pageSize:100 参数,确保获取足够科室数据
2. FeeDialog.vue loadDepartmentOptions 增加回退逻辑:当树形结构无children时使用扁平records

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:13:56 +08:00
关羽
3c3428e0b1 Fix Bug #499: 【住院医生工作站-检查申请】检查申请列表缺失查询过滤功能,不符合临床高效检索要求
- 新增关键字搜索输入框(申请单号/检查项目名称模糊匹配)
- 设置日期范围默认为近7天
- 关键字搜索支持回车触发查询

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:09:12 +08:00
关羽
db05a30795 Fix Bug #502: 【住院护士站-汇总发药申请】顶部医嘱类型(长期/临时)过滤按钮点击无响应
根因:汇总视图(SummaryMedicineList)没有ref属性,handleGetPrescription()只调用了prescriptionRefs.value?.handleGetPrescription(),
当isDetails=='2'时PrescriptionList被v-if隐藏,prescriptionRefs.value为null,导致汇总列表不刷新。

修复:1. 给SummaryMedicineList添加ref="summaryMedicineRefs"
      2. handleGetPrescription()根据isDetails值调用对应的子组件刷新方法

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:06:36 +08:00
关羽
e2feb4850c Fix Bug #498: 【住院医生工作站-检查申请】检查申请列表操作项过于单一,缺失修改/作废/打印/看报告等核心临床操作
根据申请单状态动态展示操作按钮:
- 待签发:详情、修改、删除
- 已签发:详情、撤回
- 已校对/待接收:详情、打印
- 已接收/已检查:详情、看报告
- 已出报告:详情、打印、看报告
- 已作废:详情

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:05:26 +08:00
荀彧
02f2a14178 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因:SQL 查询使用 CASE MIN(wsr.status_enum) 计算状态,但聚合函数 MIN() 出现在 WHERE 子句中,
PostgreSQL 语法错误导致状态筛选时查询失败。且计算状态仅映射 5 种值(缺少"待接收"=3、"已出报告"=6)。

修复:改为直接使用 doc_request_form.status 字段(数据库已存在该列),
SELECT 和 WHERE 均使用 drf.status,支持完整 0-7 状态流转。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:43:07 +08:00
荀彧
1c87c39473 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
- 新增 el-empty 空状态提示:当组套列表为空时显示"暂无划价组套数据",避免用户看到空白表格误认为页面无响应
- 改进错误处理:API 失败时弹出 ElMessage.warning 提示用户,替代之前仅 console.warn 的静默处理
- 添加调试日志:openGroupSetDialog 入口添加 console.log 便于排查按钮点击是否触发

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:15:03 +08:00
赵云
7c28a98d02 Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
根因: el-popover 通过 teleport 渲染在 document.body 上,closeChargeDialog() 调用
closeAllPopovers() 后立即设置 showChargeDialog=false,dialog 在 Vue 完成 popover DOM 清
理前就开始卸载,导致 teleported popover 残留。

修复:
1. closeChargeDialog 改为 async,closeAllPopovers 后 await nextTick() 确保 popover 可
   见性变更的 DOM 更新完成后再关闭 dialog
2. el-dialog 添加 destroy-on-close 属性,确保关闭时完整销毁内容区及所有子组件的 teleport
2026-05-14 05:06:58 +08:00
荀彧
8e042cae93 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
移除了 handleCollapseChange 中的 isAnimating 防抖锁。该锁会阻塞后续点击的 handleCollapseChange 回调执行,
导致快速切换分类时 currentActiveCategory 未被更新,过期 API 响应可能覆盖数据,以及 accordion 状态与业务逻辑不同步。
改为始终更新 currentActiveCategory 守卫,真正依靠 handleCategoryExpand 中的过期请求忽略机制来防止数据闪烁。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:06:08 +08:00
关羽
415a76af49 Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空
原因:projectWithDepartment 函数在 watch 触发时(type=1)若项目未配置执行科室,
立即弹出"未找到项目执行的科室"错误,干扰用户操作;且提交时(type=2)的错误提示
分支没有区分"用户已手动选择"和"用户未选择"两种情况。

修复:将 findItem 未找到时的错误弹窗限制在 type=2(提交)且用户未手动选择科室时触发,
type=1(选择项目变化)时仅清空科室字段让用户自行选择,不再弹窗阻断。
2026-05-14 05:05:51 +08:00
赵云
6dc9788d8c Fix Bug #487: 【临床医嘱】诊疗类医嘱签发后,列表状态未实时刷新为"已签发"
根因分析:诊疗类(活动)医嘱签发时,后端handService()的批量状态更新
未区分签发/保存场景,导致statusEnum字段在签发时可能未被正确更新为
ACTIVE(2);前端依赖后端刷新,缺乏乐观更新机制。

修复方案:
- 前端:签发成功后立即将saveList中对应医嘱的statusEnum设为2(乐观更新),
  再执行getListInfo从后端刷新
- 后端:handService()中分离签发/保存的批量更新逻辑,签发时显式设置
  statusEnum=ACTIVE、authoredTime和signCode,并添加日志

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 04:44:10 +08:00
关羽
319224cdac Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:adviceTypes 参数使用逗号分隔字符串 '1,2,3,6',经 tansParams 序列化后变成
adviceTypes=1%2C2%2C3%2C6(URL编码的逗号),Spring MVC 无法将其正确解析为 List<Integer>,
导致后端 SQL 返回空结果。改为数组 [1,2,3,6] 后,tansParams 正确序列化为
adviceTypes=1&adviceTypes=2&adviceTypes=3&adviceTypes=6,后端可正常解析。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 04:17:42 +08:00
关羽
66d42f415a Fix Bug #477: 住院检查申请详情弹窗中"发往科室"字段显示异常
根因:recursionFun 使用嵌套循环搜索科室树,但 API 返回扁平列表导致匹配失败。
修复:改用递归 findTreeItem 搜索(与 medicalExaminations.vue 一致),添加 API 错误处理,
并在 ID 匹配失败时回退显示原始值而非空白。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:22:12 +08:00
荀彧
fd0132ba80 Fix Bug #467: [住院医生工作站-检验申请] 列表显示信息不规范:标题术语错误且单据名称未展示具体检验项目
1. 详情弹窗中"处方号"改为"申请单号",符合住院检验业务术语规范
2. 列表"申请单名称"列改为从 requestFormDetailList 动态构建:
   - 单一项目:显示"项目名称+数量"
   - 多个项目:显示"首项目名称+数量等X项"
   解决此前统一显示"检验申请单"无法区分单据内容的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:15:10 +08:00
关羽
6907c7dbc8 Fix Bug #481: [住院护士站-医嘱执行] 药品库存充足但执行时提示库存不足
根因: AdviceUtils.checkExeMedInventory() 中硬编码 performLocation == locationId 的匹配条件,
当医嘱的 performLocation 指向的药房没有该药品库存时(库存实际在其他药房),匹配失败导致"库存不足"错误。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -86,17 +86,12 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService {
return dictDataMapper.selectDictDataByTypeWithSearch(dictType, trimmedKey); return dictDataMapper.selectDictDataByTypeWithSearch(dictType, trimmedKey);
} }
// 否则使用原有方法(带缓存) // 直接查询数据库,避免缓存中为空数据导致前端下拉框显示"无数据"
List<SysDictData> dictDatas = DictUtils.getDictCache(dictType); List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(dictType);
if (StringUtils.isNotEmpty(dictDatas)) {
return dictDatas;
}
dictDatas = dictDataMapper.selectDictDataByType(dictType);
if (StringUtils.isNotEmpty(dictDatas)) { if (StringUtils.isNotEmpty(dictDatas)) {
DictUtils.setDictCache(dictType, dictDatas); DictUtils.setDictCache(dictType, dictDatas);
return dictDatas;
} }
return null; return dictDatas;
} }
/** /**

View File

@@ -366,7 +366,7 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
serviceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());// 治疗类型 serviceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());// 治疗类型
serviceRequest.setQuantity(BigDecimal.valueOf(1)); // 请求数量 serviceRequest.setQuantity(BigDecimal.valueOf(1)); // 请求数量
serviceRequest.setUnitCode(""); // 请求单位编码 serviceRequest.setUnitCode(""); // 请求单位编码
serviceRequest.setCategoryEnum(4); // 请求类型4-手术 serviceRequest.setCategoryEnum(24); // 请求类型:24-手术(新值域,避开 adviceType 碰撞)
serviceRequest.setActivityId(surgeryId); // 手术ID作为诊疗定义id serviceRequest.setActivityId(surgeryId); // 手术ID作为诊疗定义id
serviceRequest.setPatientId(surgeryDto.getPatientId()); // 患者 serviceRequest.setPatientId(surgeryDto.getPatientId()); // 患者
serviceRequest.setRequesterId(practitionerId); // 开方医生 serviceRequest.setRequesterId(practitionerId); // 开方医生

View File

@@ -7,7 +7,6 @@ import com.core.common.core.domain.R;
import com.core.common.core.domain.model.LoginUser; import com.core.common.core.domain.model.LoginUser;
import com.core.common.utils.SecurityUtils; import com.core.common.utils.SecurityUtils;
import com.openhis.administration.domain.Patient; import com.openhis.administration.domain.Patient;
import com.openhis.administration.service.IOrganizationService;
import com.openhis.administration.service.IPatientService; import com.openhis.administration.service.IPatientService;
import com.openhis.clinical.domain.Surgery; import com.openhis.clinical.domain.Surgery;
import com.openhis.clinical.service.ISurgeryService; import com.openhis.clinical.service.ISurgeryService;
@@ -28,7 +27,6 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -204,6 +202,8 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
return R.fail("新增手术安排失败"); return R.fail("新增手术安排失败");
} }
syncSurgeryIncisionLevel(opSchedule.getOperCode(), opCreateScheduleDto.getIncisionLevel());
// Bug #247 修复:更新手术申请单状态为已排期 (1) // Bug #247 修复:更新手术申请单状态为已排期 (1)
if (opCreateScheduleDto.getApplyId() != null) { if (opCreateScheduleDto.getApplyId() != null) {
try { try {
@@ -300,6 +300,8 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
return R.fail("修改手术安排失败"); return R.fail("修改手术安排失败");
} }
syncSurgeryIncisionLevel(opScheduleDto.getOperCode(), opScheduleDto.getIncisionLevel());
return R.ok("修改手术安排成功"); return R.ok("修改手术安排成功");
} }
@@ -433,6 +435,28 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
return scheduleDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return scheduleDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} }
/**
* 同步手术申请表中的切口类型
*/
private void syncSurgeryIncisionLevel(String surgeryNo, Integer incisionLevel) {
if (surgeryNo == null || surgeryNo.isEmpty() || incisionLevel == null) {
return;
}
LambdaQueryWrapper<Surgery> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Surgery::getSurgeryNo, surgeryNo)
.eq(Surgery::getDeleteFlag, "0");
Surgery surgery = surgeryService.getOne(queryWrapper);
if (surgery == null) {
log.warn("未找到需要同步切口类型的手术申请记录 - surgeryNo: {}", surgeryNo);
return;
}
surgery.setIncisionLevel(incisionLevel);
surgery.setUpdateTime(new Date());
surgeryService.updateById(surgery);
}
/** /**
* 填充手术申请中缺失的名称字段 * 填充手术申请中缺失的名称字段
* 在创建手术安排时调用确保关联的cli_surgery表中的名称字段有值 * 在创建手术安排时调用确保关联的cli_surgery表中的名称字段有值

View File

@@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Data @Data
@@ -85,6 +84,11 @@ public class OpCreateScheduleDto {
*/ */
private String surgerySite; private String surgerySite;
/**
* 切口类型
*/
private Integer incisionLevel;
/** /**
* 入院时间 * 入院时间
*/ */
@@ -261,6 +265,11 @@ public class OpCreateScheduleDto {
*/ */
private String remark; private String remark;
/**
* 费用类别
*/
private String feeType;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.openhis.surgicalschedule.domain.OpSchedule; import com.openhis.surgicalschedule.domain.OpSchedule;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate; import java.time.LocalDate;
@@ -93,6 +94,12 @@ public class OpScheduleDto extends OpSchedule {
* 手术类型 * 手术类型
*/ */
private String surgeryType; private String surgeryType;
/**
* 切口类型
*/
private Integer incisionLevel;
/** /**
* 申请科室 * 申请科室
*/ */
@@ -107,8 +114,4 @@ public class OpScheduleDto extends OpSchedule {
*/ */
private String createByName; private String createByName;
/**
* 费用类别
*/
private String feeType;
} }

View File

@@ -200,9 +200,10 @@ public interface ICommonService {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
R<?> lotNumberMatch(List<Long> encounterIdList); R<?> lotNumberMatch(List<Long> encounterIdList, List<Long> requestIdList);
/** /**
* 根据机构ID获取机构名称 * 根据机构ID获取机构名称

View File

@@ -39,8 +39,10 @@ import com.openhis.web.common.dto.*;
import com.openhis.web.common.mapper.CommonAppMapper; import com.openhis.web.common.mapper.CommonAppMapper;
import com.openhis.web.pharmacymanage.dto.InventoryDetailDto; import com.openhis.web.pharmacymanage.dto.InventoryDetailDto;
import com.openhis.workflow.domain.DeviceDispense; import com.openhis.workflow.domain.DeviceDispense;
import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.InventoryItem; import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.service.IDeviceDispenseService; import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
import com.openhis.workflow.service.IInventoryItemService; import com.openhis.workflow.service.IInventoryItemService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -99,6 +101,9 @@ public class CommonServiceImpl implements ICommonService {
@Resource @Resource
private IDeviceDispenseService deviceDispenseService; private IDeviceDispenseService deviceDispenseService;
@Resource
private IDeviceRequestService deviceRequestService;
/** /**
* 获取药房列表 * 获取药房列表
* *
@@ -678,10 +683,11 @@ public class CommonServiceImpl implements ICommonService {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
@Override @Override
public R<?> lotNumberMatch(List<Long> encounterIdList) { public R<?> lotNumberMatch(List<Long> encounterIdList, List<Long> requestIdList) {
// 查询患者待发放的药品信息 // 查询患者待发放的药品信息
List<MedicationDispense> medicationDispenseList = medicationDispenseService List<MedicationDispense> medicationDispenseList = medicationDispenseService
.list(new LambdaQueryWrapper<MedicationDispense>().in(MedicationDispense::getEncounterId, encounterIdList) .list(new LambdaQueryWrapper<MedicationDispense>().in(MedicationDispense::getEncounterId, encounterIdList)
@@ -798,10 +804,27 @@ public class CommonServiceImpl implements ICommonService {
} }
} }
// 查询患者待发放的耗材信息 // 查询患者待发放的耗材信息
List<DeviceDispense> deviceDispenseList = deviceDispenseService LambdaQueryWrapper<DeviceDispense> deviceDispenseQuery = new LambdaQueryWrapper<DeviceDispense>()
.list(new LambdaQueryWrapper<DeviceDispense>().in(DeviceDispense::getEncounterId, encounterIdList) .in(DeviceDispense::getEncounterId, encounterIdList)
.eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue()) .eq(DeviceDispense::getStatusEnum, DispenseStatus.PREPARATION.getValue())
.eq(DeviceDispense::getDeleteFlag, DelFlag.NO.getCode())); .eq(DeviceDispense::getDeleteFlag, DelFlag.NO.getCode());
// 若传入requestIdList则仅查询与指定医嘱请求关联的耗材避免其他未执行医嘱的耗材记录干扰
if (requestIdList != null && !requestIdList.isEmpty()) {
List<Long> deviceReqIds = deviceRequestService
.list(new LambdaQueryWrapper<DeviceRequest>()
.in(DeviceRequest::getBasedOnId, requestIdList)
.eq(DeviceRequest::getBasedOnTable, CommonConstants.TableName.WOR_SERVICE_REQUEST))
.stream()
.map(DeviceRequest::getId)
.collect(java.util.stream.Collectors.toList());
if (!deviceReqIds.isEmpty()) {
deviceDispenseQuery.in(DeviceDispense::getDeviceReqId, deviceReqIds);
} else {
// 无关联的耗材请求,直接跳过耗材校验
deviceDispenseQuery.eq(DeviceDispense::getId, -1L);
}
}
List<DeviceDispense> deviceDispenseList = deviceDispenseService.list(deviceDispenseQuery);
// 耗材批号匹配 // 耗材批号匹配
if (deviceDispenseList != null && !deviceDispenseList.isEmpty()) { if (deviceDispenseList != null && !deviceDispenseList.isEmpty()) {
// 获取待发放的耗材id // 获取待发放的耗材id

View File

@@ -274,10 +274,13 @@ public class CommonAppController {
* 批号匹配 * 批号匹配
* *
* @param encounterIdList 就诊id列表 * @param encounterIdList 就诊id列表
* @param requestIdList 医嘱请求id列表可选用于限定仅校验与当前执行医嘱关联的耗材
* @return 处理结果 * @return 处理结果
*/ */
@GetMapping("/lot-number-match") @GetMapping("/lot-number-match")
public R<?> lotNumberMatch(@RequestParam(value = "encounterIdList") List<Long> encounterIdList) { public R<?> lotNumberMatch(
return commonService.lotNumberMatch(encounterIdList); @RequestParam(value = "encounterIdList") List<Long> encounterIdList,
@RequestParam(value = "requestIdList", required = false) List<Long> requestIdList) {
return commonService.lotNumberMatch(encounterIdList, requestIdList);
} }
} }

View File

@@ -35,6 +35,7 @@ import com.openhis.medication.service.IMedicationDispenseService;
import com.openhis.medication.service.IMedicationRequestService; import com.openhis.medication.service.IMedicationRequestService;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper; import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService; import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
import com.openhis.web.doctorstation.appservice.IDoctorStationInspectionLabApplyService;
import com.openhis.web.doctorstation.dto.*; import com.openhis.web.doctorstation.dto.*;
import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper; import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper;
import com.openhis.web.doctorstation.utils.AdviceUtils; import com.openhis.web.doctorstation.utils.AdviceUtils;
@@ -47,12 +48,15 @@ import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.domain.ServiceRequest; import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.*; import com.openhis.workflow.service.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -62,6 +66,9 @@ import java.util.stream.Collectors;
@Service @Service
public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAppService { public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAppService {
private static final Pattern INSPECTION_APPLY_NO_JSON =
Pattern.compile("\"applyNo\"\\s*:\\s*\"([^\"]+)\"");
@Resource @Resource
AssignSeqUtil assignSeqUtil; AssignSeqUtil assignSeqUtil;
@@ -118,6 +125,13 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
@Resource @Resource
IInventoryItemService inventoryItemService; IInventoryItemService inventoryItemService;
/**
* 与检验申请实现存在循环依赖,需延迟注入;删除诊疗医嘱时按 contentJson 级联作废检验申请单。
*/
@Resource
@Lazy
private IDoctorStationInspectionLabApplyService iDoctorStationInspectionLabApplyService;
// 缓存 key 前缀 // 缓存 key 前缀
private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:"; private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:";
// 缓存过期时间(小时) // 缓存过期时间(小时)
@@ -714,8 +728,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
/** /**
* 处理耗材请求 * 处理耗材请求
* 🔧 BugFix #443: 签发时跳过 handDevice避免重复创建 DeviceDispense 并覆盖关键字段(如 performLocation
* 签发时只需更新状态(下方 sign-advice 批量更新逻辑已处理)
*/ */
if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) {
this.handDevice(deviceList, curDate, adviceOpType); this.handDevice(deviceList, curDate, adviceOpType);
}
// 签发时,把草稿状态的账单更新为待收费 // 签发时,把草稿状态的账单更新为待收费
if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !adviceSaveList.isEmpty()) { if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !adviceSaveList.isEmpty()) {
@@ -1529,21 +1547,44 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
} }
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号 deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号
deviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型 deviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型
// 🔧 BugFix #498: categoryEnum=22(检查) 走 ServiceRequest不走 DeviceRequest
// 检查申请单的诊疗定义ID存在 activityId不在 adviceDefinitionId
// deviceDefId 对应耗材定义ID不能用诊疗定义ID填充
if (adviceSaveDto.getCategoryEnum() == 22) {
log.info("handDevice skip - 检查申请单(categoryEnum=22) 走 ServiceRequest 路径,跳过 DeviceRequest 保存");
continue; // 跳过本次循环,不走耗材请求路径
} else if (adviceSaveDto.getAdviceDefinitionId() != null) {
deviceRequest.setDeviceDefId(adviceSaveDto.getAdviceDefinitionId());// 耗材定义id deviceRequest.setDeviceDefId(adviceSaveDto.getAdviceDefinitionId());// 耗材定义id
} else {
log.warn("handDevice - deviceDefId 为空adviceDefinitionId=null, categoryEnum={}", adviceSaveDto.getCategoryEnum());
}
deviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者 deviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生 deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId());// 开方人科室 deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId());// 开方人科室
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间 deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间
// 发放耗材房若前端未传locationId使用登录用户科室作为默认值 // 发放耗材房若前端未传locationId优先沿用已有DeviceRequest的performLocation否则使用登录用户科室)
Long locId = adviceSaveDto.getLocationId(); Long locId = adviceSaveDto.getLocationId();
if (locId == null) {
// 尝试从已有DeviceRequest获取原始的performLocation
if (adviceSaveDto.getRequestId() != null) {
DeviceRequest existingDevice = iDeviceRequestService.getById(adviceSaveDto.getRequestId());
if (existingDevice != null && existingDevice.getPerformLocation() != null) {
locId = existingDevice.getPerformLocation();
log.info("耗材locationId为空使用已有DeviceRequest的performLocation: locationId={}", locId);
}
}
// 如果已有记录也没有performLocation则使用登录用户科室作为兜底
if (locId == null) { if (locId == null) {
locId = SecurityUtils.getLoginUser().getOrgId(); locId = SecurityUtils.getLoginUser().getOrgId();
log.info("耗材locationId为空使用登录用户科室作为默认值: locationId={}", locId); log.info("耗材locationId为空且无已有记录,使用登录用户科室作为默认值: locationId={}", locId);
}
} }
deviceRequest.setPerformLocation(locId); deviceRequest.setPerformLocation(locId);
deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id
@@ -1684,6 +1725,21 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
} }
} }
/**
* 从诊疗医嘱 contentJson 中解析检验申请单号(检验保存时写入形如 {"applyNo":"..."})。
*/
private String extractInspectionApplyNoFromContentJson(String contentJson) {
if (StringUtils.isBlank(contentJson) || !contentJson.contains("applyNo")) {
return null;
}
Matcher m = INSPECTION_APPLY_NO_JSON.matcher(contentJson);
if (!m.find()) {
return null;
}
String applyNo = m.group(1).trim();
return StringUtils.isBlank(applyNo) ? null : applyNo;
}
/** /**
* 处理诊疗 * 处理诊疗
*/ */
@@ -1732,6 +1788,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
} }
} }
} }
// 检验申请单在医嘱 contentJson 中写入 applyNo从医嘱删除时需先级联作废检验单避免检验页签仍显示孤儿申请
Map<String, List<Long>> labApplyNoToRequestIds = new LinkedHashMap<>();
for (AdviceSaveDto adviceSaveDto : deleteList) { for (AdviceSaveDto adviceSaveDto : deleteList) {
Long requestId = adviceSaveDto.getRequestId(); Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的诊疗请求 // 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的诊疗请求
@@ -1740,6 +1798,35 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
continue; continue;
} }
iServiceRequestService.removeById(requestId);// 删除诊疗 iServiceRequestService.removeById(requestId);// 删除诊疗
ServiceRequest existing = iServiceRequestService.getById(adviceSaveDto.getRequestId());
if (existing == null) {
continue;
}
String applyNo = extractInspectionApplyNoFromContentJson(existing.getContentJson());
if (StringUtils.isNotBlank(applyNo)) {
labApplyNoToRequestIds.computeIfAbsent(applyNo, k -> new ArrayList<>())
.add(adviceSaveDto.getRequestId());
}
}
Set<Long> labCascadeSkippedRequestIds = new HashSet<>();
for (Map.Entry<String, List<Long>> e : labApplyNoToRequestIds.entrySet()) {
R<?> delLab = iDoctorStationInspectionLabApplyService.deleteInspectionLabApply(e.getKey());
if (delLab != null && R.isSuccess(delLab)) {
labCascadeSkippedRequestIds.addAll(e.getValue());
log.info("handService - 级联作废检验申请单 applyNo={},已跳过重复删除的医嘱 requestIds={}",
e.getKey(), e.getValue());
} else {
String msg = delLab != null && StringUtils.isNotEmpty(delLab.getMsg()) ? delLab.getMsg() : "删除检验申请单失败";
log.warn("handService - 级联作废检验申请单未成功 applyNo={} msg={},将回退为仅删除当前医嘱记录",
e.getKey(), msg);
}
}
for (AdviceSaveDto adviceSaveDto : deleteList) {
if (labCascadeSkippedRequestIds.contains(adviceSaveDto.getRequestId())) {
continue;
}
Long requestId = adviceSaveDto.getRequestId();
iServiceRequestService.removeById(requestId);// 删除诊疗
iServiceRequestService.remove( iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId, new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId,
requestId));// 删除诊疗套餐对应的子项 requestId));// 删除诊疗套餐对应的子项
@@ -1824,6 +1911,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
} }
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
serviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
@@ -2019,11 +2107,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(), CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
sourceEnum, sourceBillNo); sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时只保留诊疗请求3/6过滤掉药品1和耗材2 // 🔧 修复 Bug #444: 移除手术计费场景的药品过滤。
if (sourceBillNo != null && !sourceBillNo.isEmpty()) { // 原过滤会导致门诊手术医嘱界面无法获取手术计费创建的药品记录。
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null // 前端各组件已根据自身业务逻辑做了正确的 adviceType 过滤。
&& (dto.getAdviceType() == 1 || dto.getAdviceType() == 2));
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) { for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态 // 请求状态
requestBaseDto requestBaseDto

View File

@@ -36,6 +36,9 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date; import java.util.Date;
import java.util.*; import java.util.*;
@@ -243,7 +246,8 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
EncounterDiagnosis encounterDiagnosis; EncounterDiagnosis encounterDiagnosis;
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) { for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
encounterDiagnosis = new EncounterDiagnosis(); encounterDiagnosis = new EncounterDiagnosis();
encounterDiagnosis.setId(saveDiagnosisChildParam.getEncounterDiagnosisId()); // 注意:不设置 encounterDiagnosisId,因为上面已经删除了所有记录
// 如果设置旧的 IDsaveOrUpdate 会尝试 UPDATE 不存在的记录导致失败或重复插入
encounterDiagnosis.setEncounterId(encounterId); encounterDiagnosis.setEncounterId(encounterId);
encounterDiagnosis.setConditionId(saveDiagnosisChildParam.getConditionId()); encounterDiagnosis.setConditionId(saveDiagnosisChildParam.getConditionId());
encounterDiagnosis.setMaindiseFlag(saveDiagnosisChildParam.getMaindiseFlag()); encounterDiagnosis.setMaindiseFlag(saveDiagnosisChildParam.getMaindiseFlag());
@@ -261,8 +265,10 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
// 设置创建时间,避免数据库约束错误 // 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date()); encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis); iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
// 回写就诊诊断ID供前端后续更新使用
saveDiagnosisChildParam.setEncounterDiagnosisId(encounterDiagnosis.getId());
} }
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"})); return R.ok(saveDiagnosisParam, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
} }
@@ -580,7 +586,11 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
@Override @Override
public R<?> saveInfectiousDiseaseReport(InfectiousDiseaseReportDto infectiousDiseaseReportDto) { public R<?> saveInfectiousDiseaseReport(InfectiousDiseaseReportDto infectiousDiseaseReportDto) {
// 检查卡片编号唯一性(新增时检查,编辑时排除当前记录) // 检查卡片编号唯一性(新增时检查,编辑时排除当前记录)
String cardNo = infectiousDiseaseReportDto.getCardNo().trim(); String cardNo = infectiousDiseaseReportDto.getCardNo();
if (cardNo == null || cardNo.trim().isEmpty()) {
return R.fail("卡片编号不能为空");
}
cardNo = cardNo.trim();
LambdaQueryWrapper<InfectiousDiseaseReport> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<InfectiousDiseaseReport> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(InfectiousDiseaseReport::getCardNo, cardNo); queryWrapper.eq(InfectiousDiseaseReport::getCardNo, cardNo);
long count = iInfectiousDiseaseReportService.count(queryWrapper); long count = iInfectiousDiseaseReportService.count(queryWrapper);
@@ -592,6 +602,25 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
InfectiousDiseaseReport infectiousDiseaseReport = new InfectiousDiseaseReport(); InfectiousDiseaseReport infectiousDiseaseReport = new InfectiousDiseaseReport();
BeanUtils.copyProperties(infectiousDiseaseReportDto, infectiousDiseaseReport); BeanUtils.copyProperties(infectiousDiseaseReportDto, infectiousDiseaseReport);
// BeanUtils.copyProperties 不支持 LocalDate/LocalDateTime 到 java.util.Date 的类型转换,需手动处理
if (infectiousDiseaseReportDto.getOnsetDate() != null) {
infectiousDiseaseReport.setOnsetDate(
Date.from(infectiousDiseaseReportDto.getOnsetDate().atStartOfDay(ZoneId.systemDefault()).toInstant()));
}
if (infectiousDiseaseReportDto.getDiagDate() != null) {
infectiousDiseaseReport.setDiagDate(
Date.from(infectiousDiseaseReportDto.getDiagDate().atZone(ZoneId.systemDefault()).toInstant()));
}
// deathDate / reportDate 同理
if (infectiousDiseaseReportDto.getDeathDate() != null) {
infectiousDiseaseReport.setDeathDate(
Date.from(infectiousDiseaseReportDto.getDeathDate().atStartOfDay(ZoneId.systemDefault()).toInstant()));
}
if (infectiousDiseaseReportDto.getReportDate() != null) {
infectiousDiseaseReport.setReportDate(
Date.from(infectiousDiseaseReportDto.getReportDate().atStartOfDay(ZoneId.systemDefault()).toInstant()));
}
/** /**
* 设置创建人、删除状态、租户ID * 设置创建人、删除状态、租户ID
*/ */

View File

@@ -326,6 +326,9 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
// 如果队列项存在且未完成,更新队列状态为已完成 // 如果队列项存在且未完成,更新队列状态为已完成
// 使用排除法而非白名单:只要不是"已完成"就可以完诊,覆盖跳过、等待等非标准流转状态 // 使用排除法而非白名单:只要不是"已完成"就可以完诊,覆盖跳过、等待等非标准流转状态
// Bug #401在更新前记录队列原始完成状态用于判断是否需要写入 div_log
boolean queueWasAlreadyCompleted = queueItem != null
&& TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus());
if (queueItem != null && if (queueItem != null &&
!TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus())) { !TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus())) {
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
@@ -343,6 +346,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
} }
// 写入 div_log 审计日志(独立于队列项,确保每次完诊都生成记录) // 写入 div_log 审计日志(独立于队列项,确保每次完诊都生成记录)
// Bug #401使用更新前记录的原始状态判断避免自身更新后将状态改为 COMPLETED 导致误判为"已完成"
if (!queueWasAlreadyCompleted) {
try { try {
LoginUser loginUser = SecurityUtils.getLoginUser(); LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog divLog = new DivLog() DivLog divLog = new DivLog()
@@ -357,6 +362,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
} catch (Exception e) { } catch (Exception e) {
log.error("写入div_log审计日志失败", e); log.error("写入div_log审计日志失败", e);
} }
}
// 4. 更新状态、完成时间以及初复诊标识 // 4. 更新状态、完成时间以及初复诊标识
Date now = new Date(); Date now = new Date();

View File

@@ -96,4 +96,9 @@ public class DiagnosisQueryDto {
*/ */
private String diagnosisDoctor; private String diagnosisDoctor;
/**
* 是否已有传染病报卡0-无1-有)
*/
private Integer hasInfectiousReport;
} }

View File

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

View File

@@ -178,6 +178,8 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
inpatientAdviceParam.setEncounterIds(null); inpatientAdviceParam.setEncounterIds(null);
Integer exeStatus = inpatientAdviceParam.getExeStatus(); Integer exeStatus = inpatientAdviceParam.getExeStatus();
inpatientAdviceParam.setExeStatus(null); inpatientAdviceParam.setExeStatus(null);
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
inpatientAdviceParam.setRequestStatus(null);
// 构建查询条件 // 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
@@ -196,7 +198,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(), ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(),
ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(),
ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(), ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue()); GenerateSource.DOCTOR_PRESCRIPTION.getValue(), requestStatus);
inpatientAdvicePage.getRecords().forEach(e -> { inpatientAdvicePage.getRecords().forEach(e -> {
// 是否皮试 // 是否皮试
e.setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getSkinTestFlag())); e.setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getSkinTestFlag()));

View File

@@ -69,5 +69,6 @@ public interface AdviceProcessAppMapper {
@Param("active") Integer active, @Param("bed") Integer bed, @Param("admittingDoctor") String admittingDoctor, @Param("active") Integer active, @Param("bed") Integer bed, @Param("admittingDoctor") String admittingDoctor,
@Param("personalCashAccount") String personalCashAccount, @Param("billable") Integer billable, @Param("personalCashAccount") String personalCashAccount, @Param("billable") Integer billable,
@Param("billed") Integer billed, @Param("refunded") Integer refunded, @Param("imp") Integer imp, @Param("billed") Integer billed, @Param("refunded") Integer refunded, @Param("imp") Integer imp,
@Param("doctorPrescription") Integer doctorPrescription); @Param("doctorPrescription") Integer doctorPrescription,
@Param("requestStatus") Integer requestStatus);
} }

View File

@@ -31,6 +31,7 @@ public class NursingRecordController {
* 获取住院患者信息 分页显示 * 获取住院患者信息 分页显示
* *
* @param nursingSearchParam 查询参数 * @param nursingSearchParam 查询参数
*
* @param searchKey 模糊查询 * @param searchKey 模糊查询
* @param pageNo 当前页码 * @param pageNo 当前页码
* @param pageSize 查询条数 * @param pageSize 查询条数

View File

@@ -3,29 +3,38 @@
*/ */
package com.openhis.web.inventorymanage.appservice.impl; package com.openhis.web.inventorymanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.core.common.exception.ServiceException;
import com.core.common.utils.*; import com.core.common.utils.*;
import com.core.common.utils.bean.BeanUtils; import com.core.common.utils.bean.BeanUtils;
import com.openhis.administration.domain.DeviceDefinition;
import com.openhis.administration.domain.Practitioner; import com.openhis.administration.domain.Practitioner;
import com.openhis.administration.service.IDeviceDefinitionService;
import com.openhis.administration.service.IPractitionerService; import com.openhis.administration.service.IPractitionerService;
import com.openhis.common.constant.CommonConstants; import com.openhis.common.constant.CommonConstants;
import com.openhis.common.constant.PromptMsgConstant; import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.*; import com.openhis.common.enums.*;
import com.openhis.common.utils.EnumUtils; import com.openhis.common.utils.EnumUtils;
import com.openhis.common.utils.HisQueryUtils; import com.openhis.common.utils.HisQueryUtils;
import com.openhis.medication.domain.MedicationDefinition;
import com.openhis.medication.service.IMedicationDefinitionService;
import com.openhis.web.common.dto.UnitDto; import com.openhis.web.common.dto.UnitDto;
import com.openhis.web.inventorymanage.appservice.IRequisitionIssueAppService; import com.openhis.web.inventorymanage.appservice.IRequisitionIssueAppService;
import com.openhis.web.inventorymanage.dto.*; import com.openhis.web.inventorymanage.dto.*;
import com.openhis.web.inventorymanage.mapper.RequisitionIssueMapper; import com.openhis.web.inventorymanage.mapper.RequisitionIssueMapper;
import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.domain.SupplyRequest; import com.openhis.workflow.domain.SupplyRequest;
import com.openhis.workflow.service.IInventoryItemService;
import com.openhis.workflow.service.ISupplyRequestService; import com.openhis.workflow.service.ISupplyRequestService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -48,6 +57,15 @@ public class RequisitionIssueAppServiceImpl implements IRequisitionIssueAppServi
@Autowired @Autowired
private IPractitionerService practitionerService; private IPractitionerService practitionerService;
@Autowired
private IInventoryItemService inventoryItemService;
@Autowired
private IMedicationDefinitionService medicationDefinitionService;
@Autowired
private IDeviceDefinitionService deviceDefinitionService;
@Autowired @Autowired
private AssignSeqUtil assignSeqUtil; private AssignSeqUtil assignSeqUtil;
@@ -167,6 +185,10 @@ public class RequisitionIssueAppServiceImpl implements IRequisitionIssueAppServi
// 单据号取得 // 单据号取得
List<String> busNoList = requisitionIssueDtoList.stream().map(IssueDto::getBusNo).collect(Collectors.toList()); List<String> busNoList = requisitionIssueDtoList.stream().map(IssueDto::getBusNo).collect(Collectors.toList());
// 库存校验:领用数量不能超过源仓库实际库存
this.validateRequisitionStock(requisitionIssueDtoList);
// 请求数据取得 // 请求数据取得
List<SupplyRequest> requestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0)); List<SupplyRequest> requestList = supplyRequestService.getSupplyByBusNo(busNoList.get(0));
if (!requestList.isEmpty()) { if (!requestList.isEmpty()) {
@@ -328,4 +350,73 @@ public class RequisitionIssueAppServiceImpl implements IRequisitionIssueAppServi
} }
} }
} }
/**
* 校验领用数量是否超过源仓库实际库存
*
* @param requisitionIssueDtoList 领用出库单据列表
*/
private void validateRequisitionStock(List<IssueDto> requisitionIssueDtoList) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
for (IssueDto issueDto : requisitionIssueDtoList) {
Long itemId = issueDto.getItemId();
String lotNumber = issueDto.getLotNumber();
Long sourceLocationId = issueDto.getSourceLocationId();
BigDecimal reqQuantity = issueDto.getItemQuantity();
String itemUnit = issueDto.getUnitCode();
String itemTable = issueDto.getItemTable();
// 根据物品类型查询定义信息(拆零比、常规单位、最小单位)
BigDecimal partPercent = BigDecimal.ONE;
String unitCode = itemUnit;
String minUnitCode = itemUnit;
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(itemTable)) {
MedicationDefinition medDef = medicationDefinitionService.getById(itemId);
if (medDef != null) {
unitCode = medDef.getUnitCode();
minUnitCode = medDef.getMinUnitCode();
if (medDef.getPartPercent() != null) {
partPercent = medDef.getPartPercent();
}
}
} else if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(itemTable)) {
DeviceDefinition devDef = deviceDefinitionService.getById(itemId);
if (devDef != null) {
unitCode = devDef.getUnitCode();
minUnitCode = devDef.getMinUnitCode();
if (devDef.getPartPercent() != null) {
partPercent = devDef.getPartPercent();
}
}
}
// 计算领用数量折合最小单位的值
BigDecimal reqQuantityInMinUnit;
if (itemUnit.equals(unitCode)) {
// 领用单位 = 包装单位,需乘以拆零比
reqQuantityInMinUnit = reqQuantity.multiply(partPercent);
} else {
// 领用单位 = 最小单位,无需换算
reqQuantityInMinUnit = reqQuantity;
}
// 查询源仓库实际库存(按物品编号、批号、仓库匹配)
List<InventoryItem> inventoryItemList = inventoryItemService.selectInventoryByItemId(
itemId, lotNumber, sourceLocationId, tenantId);
// 累加匹配批号的总库存库存表quantity字段为最小单位
BigDecimal totalStock = BigDecimal.ZERO;
for (InventoryItem inventoryItem : inventoryItemList) {
if (inventoryItem.getLocationId().equals(sourceLocationId)) {
totalStock = totalStock.add(inventoryItem.getQuantity());
}
}
// 比较领用数量与库存
if (reqQuantityInMinUnit.compareTo(totalStock) > 0) {
throw new ServiceException("操作失败,库存数量不足");
}
}
}
} }

View File

@@ -63,4 +63,20 @@ public interface IRequestFormManageAppService {
* @return 申请单 * @return 申请单
*/ */
IPage<RequestFormPageDto> getRequestFormPage(RequestFormDto requestFormDto); IPage<RequestFormPageDto> getRequestFormPage(RequestFormDto requestFormDto);
/**
* 删除申请单(仅待签发状态可删除)
*
* @param requestFormId 申请单ID
* @return 结果
*/
R<?> deleteRequestForm(Long requestFormId);
/**
* 撤回申请单(已签发状态撤回至待签发)
*
* @param requestFormId 申请单ID
* @return 结果
*/
R<?> withdrawRequestForm(Long requestFormId);
} }

View File

@@ -341,7 +341,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType()))) && (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList()); .collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重,避免签发单条长期医嘱时产生重复记录 // 防重复保存:对所有医嘱进行去重(包括 INSERT 和 UPDATE 混合场景),避免签发单条医嘱时产生重复记录
Set<String> longUniqueKeySet = new HashSet<>(); Set<String> longUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> longUniqueList = new ArrayList<>(); List<RegAdviceSaveDto> longUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : longInsertOrUpdateList) { for (RegAdviceSaveDto adviceSaveDto : longInsertOrUpdateList) {
@@ -351,10 +351,10 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
+ adviceSaveDto.getDose() + "_" + adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_" + adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode(); + adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && longUniqueKeySet.contains(uniqueKey)) { if (longUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复长期医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}", log.warn("防重复保存:检测到重复长期医嘱(跨操作类型),跳过 - patientId={}, encounterId={}, adviceDefinitionId={}, dbOpType={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(), adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose()); adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDbOpType());
continue; continue;
} }
longUniqueKeySet.add(uniqueKey); longUniqueKeySet.add(uniqueKey);
@@ -429,7 +429,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
&& (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType()))) && (DbOpType.INSERT.getCode().equals(e.getDbOpType()) || DbOpType.UPDATE.getCode().equals(e.getDbOpType())))
.collect(Collectors.toList()); .collect(Collectors.toList());
// 防重复保存:对新增医嘱进行去重 // 防重复保存:对所有医嘱进行去重(包括 INSERT 和 UPDATE 混合场景),避免签发时产生重复记录
Set<String> tempUniqueKeySet = new HashSet<>(); Set<String> tempUniqueKeySet = new HashSet<>();
List<RegAdviceSaveDto> tempUniqueList = new ArrayList<>(); List<RegAdviceSaveDto> tempUniqueList = new ArrayList<>();
for (RegAdviceSaveDto adviceSaveDto : tempInsertOrUpdateList) { for (RegAdviceSaveDto adviceSaveDto : tempInsertOrUpdateList) {
@@ -439,10 +439,10 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
+ adviceSaveDto.getDose() + "_" + adviceSaveDto.getDose() + "_"
+ adviceSaveDto.getMethodCode() + "_" + adviceSaveDto.getMethodCode() + "_"
+ adviceSaveDto.getRateCode(); + adviceSaveDto.getRateCode();
if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && tempUniqueKeySet.contains(uniqueKey)) { if (tempUniqueKeySet.contains(uniqueKey)) {
log.warn("防重复保存:检测到重复临时医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}", log.warn("防重复保存:检测到重复临时医嘱(跨操作类型),跳过 - patientId={}, encounterId={}, adviceDefinitionId={}, dbOpType={}",
adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(), adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(),
adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose()); adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDbOpType());
continue; continue;
} }
tempUniqueKeySet.add(uniqueKey); tempUniqueKeySet.add(uniqueKey);
@@ -710,11 +710,21 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
// 批量更新诊疗医嘱状态(使用 update 确保状态字段必定更新) // 批量更新诊疗医嘱状态(使用 update 确保状态字段必定更新)
if (!processedRequestIds.isEmpty()) { if (!processedRequestIds.isEmpty()) {
// 🔧 Bug #487 修复:签发时额外设置 authoredTime确保签发时间被记录
if (is_sign) {
iServiceRequestService.update(null, iServiceRequestService.update(null,
new LambdaUpdateWrapper<ServiceRequest>() new LambdaUpdateWrapper<ServiceRequest>()
.set(ServiceRequest::getStatusEnum, .set(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()) .set(ServiceRequest::getAuthoredTime, authoredTime)
.set(ServiceRequest::getSignCode, signCode)
.in(ServiceRequest::getId, processedRequestIds)); .in(ServiceRequest::getId, processedRequestIds));
log.info("签发诊疗医嘱成功requestIds: {}, signCode: {}", processedRequestIds, signCode);
} else {
iServiceRequestService.update(null,
new LambdaUpdateWrapper<ServiceRequest>()
.set(ServiceRequest::getStatusEnum, RequestStatus.DRAFT.getValue())
.in(ServiceRequest::getId, processedRequestIds));
}
} }
} }
@@ -751,6 +761,8 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (is_sign) { if (is_sign) {
deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间 deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间
} }
// 保存或签发时都需要设置耗材定义ID防止 sign 分支 deviceDefId 为空触发 NOT NULL 约束)
deviceRequest.setDeviceDefId(regAdviceSaveDto.getAdviceDefinitionId());
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
@@ -788,6 +800,8 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (is_sign) { if (is_sign) {
deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间 deviceRequest.setReqAuthoredTime(authoredTime); // 医嘱签发时间
} }
// 保存或签发时都需要设置耗材定义ID防止 sign 分支 deviceDefId 为空触发 NOT NULL 约束)
deviceRequest.setDeviceDefId(regAdviceSaveDto.getAdviceDefinitionId());
// 保存时处理的字段属性 // 保存时处理的字段属性
if (is_save) { if (is_save) {
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));

View File

@@ -1,6 +1,7 @@
package com.openhis.web.regdoctorstation.appservice.impl; package com.openhis.web.regdoctorstation.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
@@ -76,19 +77,45 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) { public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 诊疗执行科室配置校验(必须在任何数据库操作之前 // 申请单ID前端空字符串可能反序列化为0L需同时判0
Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L;
// 校验所有activityList中的项目是否都配置了执行科室并收集positionId供后续使用
// 必须在任何数据库操作之前完成全部校验,避免部分保存后异常导致脏数据
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
if (activityList == null || activityList.isEmpty()) {
throw new ServiceException("请选择检查项目");
}
// 🔧 Bug #475: 查询诊疗执行科室配置
List<ActivityOrganizationConfigDto> activityOrganizationConfig = List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode); requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室"); // 缓存校验结果,先全部验证通过后再进行数据库操作
// 优先使用前端传入的positionId用户手动选择的发往科室仅在未选择时使用配置的执行科室
java.util.Map<Long, Long> activityIdToPositionIdMap = new java.util.HashMap<>();
for (ActivitySaveDto activitySaveDto : activityList) {
// 优先使用前端传入的positionId用户手动选择的科室
Long frontendPositionId = activitySaveDto.getPositionId();
if (frontendPositionId != null) {
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), frontendPositionId);
continue;
}
// 前端未传入时,使用配置的执行科室
Long configPositionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (configPositionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), configPositionId);
} }
// 诊疗处方号 // 诊疗处方号
String prescriptionNo; String prescriptionNo;
// 申请单ID
Long requestFormId = requestFormSaveDto.getRequestFormId();
// 编辑场景 // 编辑场景
if (requestFormId != null) { if (isEdit) {
RequestForm requestFormInfo = iRequestFormService.getById(requestFormId); RequestForm requestFormInfo = iRequestFormService.getById(requestFormId);
prescriptionNo = requestFormInfo.getPrescriptionNo(); prescriptionNo = requestFormInfo.getPrescriptionNo();
// 该申请单存在的待发送医嘱个数 // 该申请单存在的待发送医嘱个数
@@ -132,7 +159,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
iRequestFormService.saveOrUpdate(requestForm); iRequestFormService.saveOrUpdate(requestForm);
// 编辑场景时,先删除掉原有诊疗项目及账单再新增 // 编辑场景时,先删除掉原有诊疗项目及账单再新增
if (requestFormId != null) { if (isEdit) {
List<Long> serviceRequestIds = iServiceRequestService List<Long> serviceRequestIds = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo)) .list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo))
.stream().map(ServiceRequest::getId).collect(Collectors.toList()); .stream().map(ServiceRequest::getId).collect(Collectors.toList());
@@ -146,8 +173,6 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
ServiceRequest serviceRequest; ServiceRequest serviceRequest;
ChargeItem chargeItem; ChargeItem chargeItem;
// 诊疗集合
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId); log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
for (ActivitySaveDto activitySaveDto : activityList) { for (ActivitySaveDto activitySaveDto : activityList) {
@@ -166,9 +191,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
serviceRequest.setEncounterId(encounterId); // 就诊id serviceRequest.setEncounterId(encounterId); // 就诊id
serviceRequest.setAuthoredTime(curDate); // 请求签发时间 serviceRequest.setAuthoredTime(curDate); // 请求签发时间
Long positionId = activityOrganizationConfig.stream() Long positionId = activityIdToPositionIdMap.get(activitySaveDto.getAdviceDefinitionId());
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) { if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室"); throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
} }
@@ -279,7 +302,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
surgeryServiceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); surgeryServiceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
surgeryServiceRequest.setQuantity(BigDecimal.valueOf(1)); surgeryServiceRequest.setQuantity(BigDecimal.valueOf(1));
surgeryServiceRequest.setUnitCode(""); surgeryServiceRequest.setUnitCode("");
surgeryServiceRequest.setCategoryEnum(4); // 4-手术 surgeryServiceRequest.setCategoryEnum(24); // 24-手术(新值域,避开 adviceType 碰撞)
// 优先从 activityList 获取手术 ID // 优先从 activityList 获取手术 ID
if (activityList != null && !activityList.isEmpty()) { if (activityList != null && !activityList.isEmpty()) {
Long activityId = activityList.get(0).getAdviceDefinitionId(); Long activityId = activityList.get(0).getAdviceDefinitionId();
@@ -475,4 +498,90 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return requestFormManageAppMapper.getRequestFormPage(requestFormDto, page); return requestFormManageAppMapper.getRequestFormPage(requestFormDto, page);
} }
@Override
public R<?> deleteRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest含子项
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
// 校验:只有待签发(status=0)的申请单可删除
boolean allDraft = serviceRequests.stream()
.allMatch(sr -> RequestStatus.DRAFT.getValue().equals(sr.getStatusEnum()));
if (!allDraft) {
return R.fail("只有待签发状态的申请单可删除");
}
List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList());
// 1. 删除关联的费用项
for (Long srId : serviceRequestIds) {
iChargeItemService.deleteByServiceTableAndId(
CommonConstants.TableName.WOR_SERVICE_REQUEST, srId);
}
// 2. 删除子项 ServiceRequestparentId 非空)
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds)
.isNotNull(ServiceRequest::getParentId));
// 3. 删除主项 ServiceRequest
iServiceRequestService.removeByIds(serviceRequestIds);
// 4. 删除申请单
iRequestFormService.removeById(requestFormId);
log.info("检查申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("删除成功");
}
@Override
public R<?> withdrawRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
// 校验:只有已签发(status=2)的申请单可撤回
boolean allActive = serviceRequests.stream()
.allMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!allActive) {
return R.fail("只有已签发状态的申请单可撤回");
}
// 将所有 ServiceRequest 状态改回待签发(DRAFT=0)
List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList());
iServiceRequestService.update(
new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()),
new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds));
log.info("检查申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("撤回成功");
}
} }

View File

@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 申请单管理 controller * 申请单管理 controller
@@ -82,14 +83,23 @@ public class RequestFormManageController {
* 查询检查申请单 * 查询检查申请单
* *
* @param encounterId 就诊id * @param encounterId 就诊id
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检查项目名称模糊匹配)
* @return 检查申请单 * @return 检查申请单
*/ */
@GetMapping(value = "/get-check") @GetMapping(value = "/get-check")
public R<?> getCheckRequestForm(@RequestParam(required = false) Long encounterId) { public R<?> getCheckRequestForm(
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) { if (encounterId == null) {
return R.fail("就诊ID不能为空"); return R.fail("就诊ID不能为空");
} }
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode())); return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode(), startDate, endDate, status, keyword));
} }
/** /**
@@ -176,4 +186,26 @@ public class RequestFormManageController {
public R<IPage<RequestFormPageDto>> getRequestFormPage(@RequestBody RequestFormDto requestFormDto) { public R<IPage<RequestFormPageDto>> getRequestFormPage(@RequestBody RequestFormDto requestFormDto) {
return R.ok(iRequestFormManageAppService.getRequestFormPage(requestFormDto)); return R.ok(iRequestFormManageAppService.getRequestFormPage(requestFormDto));
} }
/**
* 删除申请单(仅待签发状态可删除)
*
* @param data 包含 requestFormId 的请求体
* @return 结果
*/
@PostMapping(value = "/delete")
public R<?> deleteRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.deleteRequestForm(data.get("requestFormId"));
}
/**
* 撤回申请单(已签发状态撤回至待签发)
*
* @param data 包含 requestFormId 的请求体
* @return 结果
*/
@PostMapping(value = "/withdraw")
public R<?> withdrawRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.withdrawRequestForm(data.get("requestFormId"));
}
} }

View File

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

View File

@@ -97,6 +97,10 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(
wsr.content_json::json->>'surgeryName',
wsr.content_json::json->>'adviceName',
T9sr.surgery_name)
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 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 = 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.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
@@ -108,6 +112,7 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 THEN T2.yb_no WHEN T1.context_enum = 6 THEN T2.yb_no
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
@@ -118,6 +123,7 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(T9sr.id, wsr.activity_id)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0 WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = 6 THEN T2.id WHEN T1.context_enum = 6 THEN T2.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0 WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
@@ -159,6 +165,11 @@
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0' LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0' LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0' LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
LEFT JOIN cli_surgery AS T9sr ON T1.context_enum = 6
AND T1.service_table = 'wor_service_request'
AND wsr.activity_id IS NOT NULL
AND wsr.activity_id = T9sr.id
AND T9sr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsrp ON wsrp.id = wsr.parent_id AND wsrp.delete_flag = '0' LEFT JOIN wor_service_request AS wsrp ON wsrp.id = wsr.parent_id AND wsrp.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId} WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (0 AND T1.status_enum IN (0
@@ -223,6 +234,10 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(
wsr.content_json::json->>'surgeryName',
wsr.content_json::json->>'adviceName',
T9sr.surgery_name)
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 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 = 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.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
@@ -234,6 +249,7 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 THEN T2.yb_no WHEN T1.context_enum = 6 THEN T2.yb_no
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
@@ -244,6 +260,7 @@
CASE CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(T9sr.id, wsr.activity_id)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0 WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = 6 THEN T2.id WHEN T1.context_enum = 6 THEN T2.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0 WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
@@ -286,6 +303,11 @@
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0' LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0' LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0' LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
LEFT JOIN cli_surgery AS T9sr ON T1.context_enum = 6
AND T1.service_table = 'wor_service_request'
AND wsr.activity_id IS NOT NULL
AND wsr.activity_id = T9sr.id
AND T9sr.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId} WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (0 AND T1.status_enum IN (0
, #{planned} , #{planned}

View File

@@ -71,7 +71,7 @@
</if> </if>
AND os.delete_flag = '0' AND os.delete_flag = '0'
</where> </where>
ORDER BY os.create_time DESC ORDER BY os.create_time DESC, os.schedule_id DESC
</select> </select>
<!-- 根据ID查询手术安排详情--> <!-- 根据ID查询手术安排详情-->
<select id="getSurgeryScheduleDetail" resultType="com.openhis.web.clinicalmanage.dto.OpScheduleDto"> <select id="getSurgeryScheduleDetail" resultType="com.openhis.web.clinicalmanage.dto.OpScheduleDto">
@@ -89,16 +89,17 @@
cs.apply_doctor_name AS apply_doctor_name, cs.apply_doctor_name AS apply_doctor_name,
drf.create_time AS apply_time, drf.create_time AS apply_time,
os.surgery_nature AS surgeryType, os.surgery_nature AS surgeryType,
cs.incision_level AS incisionLevel,
fc.contract_name AS feeType, fc.contract_name AS feeType,
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os FROM op_schedule os
LEFT JOIN adm_patient ap ON os.patient_id = ap.id LEFT JOIN adm_patient ap ON os.patient_id = ap.id
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0' INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
LEFT JOIN adm_organization o ON cs.org_id = o.id LEFT JOIN adm_organization o ON cs.org_id = o.id
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id AND ae.delete_flag = '0' LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
LEFT JOIN adm_encounter ae ON ae.id = os.visit_id AND ae.delete_flag = '0'
LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0' LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0'
LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0' LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0'
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
LEFT JOIN ( LEFT JOIN (
SELECT patient_id, identifier_no SELECT patient_id, identifier_no
FROM ( FROM (
@@ -186,7 +187,7 @@
<if test="dto.applyDeptId != null and dto.applyDeptId != ''"> AND cs.apply_dept_id = #{dto.applyDeptId}</if> <if test="dto.applyDeptId != null and dto.applyDeptId != ''"> AND cs.apply_dept_id = #{dto.applyDeptId}</if>
<if test="dto.patientName != null and dto.patientName != ''"> AND ap.name LIKE CONCAT('%', #{dto.patientName}, '%')</if> <if test="dto.patientName != null and dto.patientName != ''"> AND ap.name LIKE CONCAT('%', #{dto.patientName}, '%')</if>
</where> </where>
ORDER BY os.create_time DESC ORDER BY os.create_time DESC, os.schedule_id DESC
</select> </select>
<!-- 查询时间段内该手术室是否被占用--> <!-- 查询时间段内该手术室是否被占用-->
<select id="isScheduleConflict" resultType="java.lang.Boolean"> <select id="isScheduleConflict" resultType="java.lang.Boolean">

View File

@@ -42,8 +42,8 @@
T5.package_name, T5.package_name,
T6.name as sub_item_name T6.name as sub_item_name
FROM wor_activity_definition T1 FROM wor_activity_definition T1
/* 只JOIN必要的价格表使用INNER JOIN避免笛卡尔积 */ /* 价格表使用LEFT JOIN避免因缺少价格记录导致搜索不到项目 */
INNER JOIN adm_charge_item_definition T2 LEFT JOIN adm_charge_item_definition T2
ON T1.id = T2.instance_id ON T1.id = T2.instance_id
AND T2.instance_table = 'wor_activity_definition' AND T2.instance_table = 'wor_activity_definition'
/* 检验类型关联 */ /* 检验类型关联 */

View File

@@ -239,7 +239,7 @@
NULL AS activity_type_dictText, NULL AS activity_type_dictText,
-- 前端"包装单位"列显示使用单位permitted_unit_code -- 前端"包装单位"列显示使用单位permitted_unit_code
T1.permitted_unit_code AS unit_code, T1.permitted_unit_code AS unit_code,
'' AS min_unit_code, T1.permitted_unit_code AS min_unit_code,
'' AS volume, '' AS volume,
'' AS method_code, '' AS method_code,
'' AS rate_code, '' AS rate_code,
@@ -539,7 +539,8 @@
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
ORDER BY T1.status_enum,T1.sort_number) ORDER BY T1.status_enum,T1.sort_number)
UNION ALL UNION ALL
-- 🔧 新增:查询门诊术中计费生成的耗材数据(这些数据存在于 adm_charge_item 和 wor_device_request -- 🔧 查询仅存在于 adm_charge_item 的"孤儿"耗材数据DeviceRequest 缺失或 generate_source_enum 未设置
-- 正常 DeviceRequestgenerate_source_enum 已赋值)由下方 Part 3 统一负责,此处不做重复覆盖避免 UNION ALL 重复行
(SELECT 2 AS advice_type, (SELECT 2 AS advice_type,
CI.service_id AS request_id, CI.service_id AS request_id,
CI.service_id || '-ci-dev' AS unique_key, CI.service_id || '-ci-dev' AS unique_key,
@@ -583,6 +584,9 @@
LEFT JOIN adm_location AS AL ON AL.id = DR.perform_location AND AL.delete_flag = '0' LEFT JOIN adm_location AS AL ON AL.id = DR.perform_location AND AL.delete_flag = '0'
WHERE CI.delete_flag = '0' WHERE CI.delete_flag = '0'
AND CI.service_table = 'wor_device_request' AND CI.service_table = 'wor_device_request'
<if test="generateSourceEnum != null">
AND DR.generate_source_enum IS NULL <!-- 仅匹配孤儿记录normal DeviceRequest 由 Part 3 负责,避免 UNION ALL 重复 -->
</if>
<if test="historyFlag == '0'.toString()"> <if test="historyFlag == '0'.toString()">
AND CI.encounter_id = #{encounterId} AND CI.encounter_id = #{encounterId}
</if> </if>

View File

@@ -134,7 +134,11 @@
T2.yb_no, T2.yb_no,
T1.onset_date AS onsetDate, T1.onset_date AS onsetDate,
T1.diagnosis_time AS diagnosisTime, T1.diagnosis_time AS diagnosisTime,
T1.doctor AS diagnosisDoctor T1.doctor AS diagnosisDoctor,
CASE WHEN EXISTS (
SELECT 1 FROM infectious_card T4
WHERE T4.diag_id = T2.id AND T4.delete_flag = '0' AND T4.status >= 1
) THEN 1 ELSE 0 END AS hasInfectiousReport
FROM adm_encounter_diagnosis AS T1 FROM adm_encounter_diagnosis AS T1
LEFT JOIN cli_condition AS T2 ON T2.ID = T1.condition_id LEFT JOIN cli_condition AS T2 ON T2.ID = T1.condition_id
AND T2.delete_flag = '0' AND T2.tcm_flag = 0 AND T2.delete_flag = '0' AND T2.tcm_flag = 0

View File

@@ -264,7 +264,6 @@
WHERE ael.status_enum = #{active} WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0' AND ael.delete_flag = '0'
AND ael.form_enum = #{bed} AND ael.form_enum = #{bed}
LIMIT 1
) AS bed ON bed.encounter_id = ae.id ) AS bed ON bed.encounter_id = ae.id
LEFT JOIN ( LEFT JOIN (
SELECT ael.encounter_id, SELECT ael.encounter_id,
@@ -275,7 +274,6 @@
WHERE ael.status_enum = #{active} WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0' AND ael.delete_flag = '0'
AND ael.form_enum = #{house} AND ael.form_enum = #{house}
LIMIT 1
) AS house ON house.encounter_id = ae.id ) AS house ON house.encounter_id = ae.id
LEFT JOIN ( LEFT JOIN (
SELECT ael.encounter_id, SELECT ael.encounter_id,
@@ -286,7 +284,6 @@
WHERE ael.status_enum = #{active} WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0' AND ael.delete_flag = '0'
AND ael.form_enum = #{ward} AND ael.form_enum = #{ward}
LIMIT 1
) AS ward ON ward.encounter_id = ae.id ) AS ward ON ward.encounter_id = ae.id
LEFT JOIN ( LEFT JOIN (
SELECT aep.encounter_id, SELECT aep.encounter_id,

View File

@@ -155,7 +155,7 @@
ii.performer_check_id, ii.performer_check_id,
ii.category_code, ii.category_code,
ii.dispense_status ii.dispense_status
FROM (( SELECT T1.encounter_id, FROM (( SELECT DISTINCT T1.encounter_id,
T1.tenant_id, T1.tenant_id,
#{medMedicationRequest} AS advice_table, #{medMedicationRequest} AS advice_table,
T1.id AS request_id, T1.id AS request_id,
@@ -280,12 +280,17 @@
aa.balance_amount aa.balance_amount
) AS personal_account ) AS personal_account
ON personal_account.encounter_id = ae.id ON personal_account.encounter_id = ae.id
LEFT JOIN med_medication_dispense mmd LEFT JOIN LATERAL (
ON mmd.med_req_id = T1.id SELECT status_enum
AND mmd.delete_flag = '0' FROM med_medication_dispense
WHERE med_req_id = T1.id AND delete_flag = '0'
ORDER BY create_time DESC
LIMIT 1
) mmd ON true
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
AND T1.generate_source_enum = #{doctorPrescription} AND T1.generate_source_enum = #{doctorPrescription}
AND T1.status_enum = #{requestStatus}
AND CASE WHEN T1.status_enum = #{draft} AND CASE WHEN T1.status_enum = #{draft}
THEN T1.performer_check_id IS NOT NULL THEN T1.performer_check_id IS NOT NULL
ELSE 1=1 END ELSE 1=1 END
@@ -293,7 +298,7 @@
T1.sort_number, T1.sort_number,
T1.group_id ) T1.group_id )
UNION UNION
( SELECT T1.encounter_id, ( SELECT DISTINCT T1.encounter_id,
T1.tenant_id, T1.tenant_id,
#{worServiceRequest} AS advice_table, #{worServiceRequest} AS advice_table,
T1.id AS request_id, T1.id AS request_id,
@@ -418,6 +423,7 @@
AND af.delete_flag = '0' AND af.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.generate_source_enum = #{doctorPrescription} AND T1.generate_source_enum = #{doctorPrescription}
AND T1.status_enum = #{requestStatus}
AND CASE WHEN T1.status_enum = #{draft} AND CASE WHEN T1.status_enum = #{draft}
THEN T1.performer_check_id IS NOT NULL THEN T1.performer_check_id IS NOT NULL
ELSE 1=1 END ELSE 1=1 END

View File

@@ -288,7 +288,7 @@
AND T1.refund_device_id IS NULL AND T1.refund_device_id IS NULL
ORDER BY T1.status_enum) ORDER BY T1.status_enum)
UNION ALL UNION ALL
(SELECT CASE WHEN T1.category_enum = 4 THEN 6 ELSE 3 END AS advice_type, (SELECT CASE WHEN T1.category_enum IN (4, 24) THEN 6 ELSE 3 END AS advice_type,
T1.id AS request_id, T1.id AS request_id,
T1.id || '-3' AS unique_key, T1.id || '-3' AS unique_key,
T1.requester_id AS requester_id, T1.requester_id AS requester_id,

View File

@@ -8,19 +8,25 @@
SELECT drf.id AS request_form_id, SELECT drf.id AS request_form_id,
drf.encounter_id, drf.encounter_id,
drf.prescription_no, drf.prescription_no,
drf.NAME, COALESCE(
(SELECT STRING_AGG(DISTINCT wad.name, '、')
FROM wor_service_request wsr2
LEFT JOIN wor_activity_definition wad ON wad.id = wsr2.activity_id AND wad.delete_flag = '0'
WHERE wsr2.prescription_no = drf.prescription_no AND wsr2.delete_flag = '0'),
drf.name
) AS name,
drf.desc_json, drf.desc_json,
drf.requester_id, drf.requester_id,
drf.create_time, drf.create_time,
ap.NAME AS patient_name, ap.NAME AS patient_name,
CASE MIN(wsr.status_enum) CASE
WHEN 1 THEN 0 WHEN MIN(wsr.status_enum) = 1 THEN 0
WHEN 2 THEN 1 WHEN MIN(wsr.status_enum) = 2 THEN 1
WHEN 3 THEN 4 WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
WHEN 4 THEN 4 WHEN MIN(wsr.status_enum) = 3 THEN 4
WHEN 5 THEN 5 WHEN MIN(wsr.status_enum) = 4 THEN 3
WHEN 6 THEN 5 WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
WHEN 7 THEN 5 WHEN MIN(wsr.status_enum) = 8 THEN 6
ELSE NULL ELSE NULL
END AS status END AS status
FROM doc_request_form AS drf FROM doc_request_form AS drf
@@ -39,18 +45,6 @@
<if test="endDate != null and endDate != ''"> <if test="endDate != null and endDate != ''">
AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second') AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
</if> </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 != ''"> <if test="keyword != null and keyword != ''">
AND (drf.prescription_no ILIKE '%' || #{keyword} || '%' AND (drf.prescription_no ILIKE '%' || #{keyword} || '%'
OR EXISTS ( OR EXISTS (
@@ -66,12 +60,28 @@
</if> </if>
GROUP BY drf.id, drf.encounter_id, drf.prescription_no, drf.name, drf.desc_json, GROUP BY drf.id, drf.encounter_id, drf.prescription_no, drf.name, drf.desc_json,
drf.requester_id, drf.create_time, ap.name drf.requester_id, drf.create_time, ap.name
<if test="status != null and status != ''">
HAVING CASE
WHEN MIN(wsr.status_enum) = 1 THEN 0
WHEN MIN(wsr.status_enum) = 2 THEN 1
WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
WHEN MIN(wsr.status_enum) = 3 THEN 4
WHEN MIN(wsr.status_enum) = 4 THEN 3
WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
WHEN MIN(wsr.status_enum) = 8 THEN 6
ELSE NULL
END = #{status}::integer
</if>
</select> </select>
<select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto"> <select id="getRequestFormDetail" resultType="com.openhis.web.regdoctorstation.dto.RequestFormDetailQueryDto">
SELECT wsr.quantity, SELECT wsr.quantity,
wsr.unit_code, wsr.unit_code,
COALESCE(wad.NAME, wsr.content_json::jsonb->>'surgeryName') AS advice_name, COALESCE(
wad.NAME,
wsr.content_json::jsonb->>'surgeryName',
'检验项目'
) AS advice_name,
aci.total_price aci.total_price
FROM wor_service_request AS wsr FROM wor_service_request AS wsr
LEFT JOIN wor_activity_definition AS wad ON wad.ID = wsr.activity_id LEFT JOIN wor_activity_definition AS wad ON wad.ID = wsr.activity_id
@@ -81,6 +91,7 @@
AND aci.delete_flag = '0' AND aci.delete_flag = '0'
WHERE wsr.delete_flag = '0' WHERE wsr.delete_flag = '0'
AND wsr.prescription_no = #{prescriptionNo} AND wsr.prescription_no = #{prescriptionNo}
ORDER BY wsr.id
</select> </select>
<select id="getActivityOrganizationConfig" <select id="getActivityOrganizationConfig"
@@ -162,7 +173,7 @@
AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%') AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%')
</if> </if>
<if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''"> <if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''">
AND drf.type_code = #{requestFormDto.typeCode} AND drf.type_code IN (#{requestFormDto.typeCode}, 'SURGERY')
</if> </if>
<if test="requestFormDto.applyTimeStart != null"> <if test="requestFormDto.applyTimeStart != null">
AND drf.create_time >= #{requestFormDto.applyTimeStart} AND drf.create_time >= #{requestFormDto.applyTimeStart}

View File

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

View File

@@ -49,6 +49,11 @@ public enum RequestStatus implements HisEnumInterface {
*/ */
ENDED(7, "ended", "不执行"), ENDED(7, "ended", "不执行"),
/**
* 已出报告
*/
COMPLETED_REPORT(8, "completed_report", "已出报告"),
/** /**
* 未知 * 未知
*/ */

View File

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

View File

@@ -1,3 +1,5 @@
import useDictStore from '@/store/modules/dict';
// 日期格式化 // 日期格式化
export function parseTime(time, pattern) { export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) { if (arguments.length === 0 || !time) {
@@ -275,30 +277,13 @@ export function blobValidate(data) {
// 按照频次天数计算总数量 // 按照频次天数计算总数量
export function calculateQuantityByDays(frequency, days) { export function calculateQuantityByDays(frequency, days) {
// const dict = useDict('rate_code').rate_code.value const dicts = useDictStore().getDict('rate_code');
// const rate = dict.find(item => item.value === frequency).remark if (!dicts) return;
// if(rate){ const dict = dicts.find(item => item.value === frequency);
// return Math.floor(Number(rate) * days) if (!dict?.remark) return;
// } else { const rate = Number(dict.remark);
// return undefined if (isNaN(rate) || !rate) return;
// } const quantity = rate * days;
const frequencyMap = {
ST: 1,
QD: 1, // 每日一次
BID: 2, // 每日两次
TID: 3, // 每日三次
QID: 4, // 每日四次
QN: 1, // 每晚一次
QOD: 1 / 2, // 每隔一日一次
QW: 1 / 7, // 每周一次
BIW: 2 / 7, // 每周两次
TIW: 3 / 7, // 每周三次
QOW: 1 / 14, // 隔周一次
};
if (!frequencyMap[frequency]) {
return;
}
const quantity = frequencyMap[frequency] * days;
return quantity < 1 ? 1 : Math.ceil(quantity); return quantity < 1 ? 1 : Math.ceil(quantity);
} }

View File

@@ -178,22 +178,25 @@ service.interceptors.request.use(config => {
} }
return Promise.reject('无效的会话,或者会话已过期,请重新登录。') return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) { } else if (code === 500) {
// 检查是否需要跳过错误提示 // 检查是否需要跳过错误提示(静默请求:返回响应让.then()处理)
if (!res.config?.skipErrorMsg) { if (res.config?.skipErrorMsg) {
ElMessage({ message: msg, type: 'error' }) return Promise.resolve(res.data)
} }
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg)) return Promise.reject(new Error(msg))
} else if (code === 601) { } else if (code === 601) {
// 检查是否需要跳过错误提示 // 检查是否需要跳过错误提示(静默请求:返回响应让.then()处理)
if (!res.config?.skipErrorMsg) { if (res.config?.skipErrorMsg) {
ElMessage({ message: msg, type: 'warning' }) return Promise.resolve(res.data)
} }
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg)) return Promise.reject(new Error(msg))
} else if (code !== 200) { } else if (code !== 200) {
// 检查是否需要跳过错误提示 // 检查是否需要跳过错误提示(静默请求:返回响应让.then()处理)
if (!res.config?.skipErrorMsg) { if (res.config?.skipErrorMsg) {
ElNotification.error({ title: msg }) return Promise.resolve(res.data)
} }
ElNotification.error({ title: msg })
return Promise.reject('error') return Promise.reject('error')
} else { } else {
return Promise.resolve(res.data) return Promise.resolve(res.data)

View File

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

View File

@@ -473,15 +473,12 @@ function calculateTotalPrice() {
} }
}); });
totalPrice.value = sum.toFixed(2); totalPrice.value = sum.toFixed(2);
// Bug #464: 零售价与诊疗子项合计总价实时同步 // Bug #464: 零售价与诊疗子项合计总价实时同步直接赋值不使用nextTick避免多调用方竞争
const hasValidItem = treatmentItems.value.some( const hasValidItem = treatmentItems.value.some(
(item) => item.adviceDefinitionId && item.adviceDefinitionId !== '' (item) => item.adviceDefinitionId && item.adviceDefinitionId !== ''
); );
if (hasValidItem) { if (hasValidItem) {
// 使用 nextTick 确保总价更新后零售价才更新,避免 Vue 响应式时序问题
nextTick(() => {
form.value.retailPrice = parseFloat(totalPrice.value) || 0; form.value.retailPrice = parseFloat(totalPrice.value) || 0;
});
} else { } else {
form.value.retailPrice = undefined; form.value.retailPrice = undefined;
} }
@@ -763,10 +760,7 @@ function selectRow(row, index) {
treatmentItems.value[index].adviceDefinitionId = row.id; treatmentItems.value[index].adviceDefinitionId = row.id;
treatmentItems.value[index].retailPrice = row.retailPrice || 0; treatmentItems.value[index].retailPrice = row.retailPrice || 0;
medicineSearchKey.value = ''; medicineSearchKey.value = '';
// 使用 nextTick 确保 DOM 更新后再计算总价
nextTick(() => {
calculateTotalPrice(); calculateTotalPrice();
});
} }
// 清空诊疗子项 // 清空诊疗子项

View File

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

View File

@@ -461,7 +461,7 @@ watch(
console.log(prescriptionList.value,"prescriptionList.value") console.log(prescriptionList.value,"prescriptionList.value")
if(newValue&&newValue.length>0){ if(newValue&&newValue.length>0){
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}) })
console.log(saveList,"prescriptionList.value") console.log(saveList,"prescriptionList.value")
if (saveList.length == 0) { if (saveList.length == 0) {
@@ -887,6 +887,10 @@ function handleDelete() {
if (item.statusEnum != 1 || item.chargeStatus == 5) { if (item.statusEnum != 1 || item.chargeStatus == 5) {
return null; return null;
} }
// 🔧 Bug #442: 非本人创建的医嘱不允许删除(与签发/签退逻辑保持一致)
if (Number(item.bizRequestFlag) !== 1 && item.bizRequestFlag) {
return null;
}
// 🔧 Bug #442: 已保存的行必须有有效的 requestId否则跳过避免后端删除不存在的记录 // 🔧 Bug #442: 已保存的行必须有有效的 requestId否则跳过避免后端删除不存在的记录
if (item.requestId == null || item.requestId === undefined || item.requestId === '') { if (item.requestId == null || item.requestId === undefined || item.requestId === '') {
return null; return null;
@@ -896,7 +900,7 @@ function handleDelete() {
dbOpType: '3', dbOpType: '3',
adviceType: item.adviceType, adviceType: item.adviceType,
}; };
}).filter(item => item !== null); // 过滤掉已签发、已收费或无 requestId 的项目 }).filter(item => item !== null); // 过滤掉已签发、已收费、非本人创建或无 requestId 的项目
if (deleteList.length == 0) { if (deleteList.length == 0) {
proxy.$modal.msgWarning('只能删除待签发且未收费的项目'); proxy.$modal.msgWarning('只能删除待签发且未收费的项目');
@@ -1011,7 +1015,7 @@ function handleSave() {
return; return;
} }
let saveList = prescriptionList.value.filter((item) => { let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag) return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
}); });
// let saveList = prescriptionList.value // let saveList = prescriptionList.value
// .filter((item) => { // .filter((item) => {
@@ -1032,6 +1036,14 @@ function handleSave() {
requestId: item.requestId, requestId: item.requestId,
dbOpType: '1', dbOpType: '1',
groupId: item.groupId, groupId: item.groupId,
// 🔧 Bug #443: 补充顶层关键字段(这些不在 contentJson 中,需从 API 响应顶层提取)
encounterId: item.encounterId,
patientId: item.patientId,
locationId: item.positionId,
adviceType: item.adviceType,
adviceTableName: item.adviceTableName,
adviceDefinitionId: item.adviceDefinitionId,
chargeItemId: item.chargeItemId,
}; };
}); });
savePrescriptionSign({ savePrescriptionSign({
@@ -1047,7 +1059,12 @@ function handleSave() {
groupIndexList.value = [] groupIndexList.value = []
groupList.value = [] groupList.value = []
nextId.value = 1; nextId.value = 1;
} else {
proxy.$modal.msgError(res?.msg || '签发失败,请重试');
} }
}).catch((error) => {
console.error('签发失败:', error);
proxy.$modal.msgError(error?.response?.data?.msg || error?.message || '签发失败,请重试');
}); });
} }
@@ -1063,9 +1080,12 @@ function handleSaveSign(row, index) {
proxy.$modal.msgWarning('诊疗项目必须选择执行科室'); proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
return; return;
} }
isSaving.value = true; // #437 立即加锁,消除 TOCTOU 竞态
proxy.$refs['formRef' + index].validate((valid) => { proxy.$refs['formRef' + index].validate((valid) => {
if (valid) { if (!valid) {
isSaving.value = true; // #437 加 isSaving.value = false; // 验证失败释放
return;
}
row.isEdit = false; row.isEdit = false;
isAdding.value = false; isAdding.value = false;
expandOrder.value = []; expandOrder.value = [];
@@ -1083,7 +1103,7 @@ function handleSaveSign(row, index) {
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo; cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
} }
console.log('cleanRow', cleanRow) console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }).then((res) => { savePrescription({ adviceSaveList: [cleanRow] }, '1').then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功'); proxy.$modal.msgSuccess('保存成功');
getListInfo(false); getListInfo(false);
@@ -1097,8 +1117,7 @@ function handleSaveSign(row, index) {
}).finally(() => { }).finally(() => {
isSaving.value = false; // #437 释放锁 isSaving.value = false; // #437 释放锁
}); });
} })
});
} }
// 签退 // 签退

View File

@@ -692,6 +692,7 @@ async function handleFoodDiseasesCheck() {
/** /**
* 传染病报告卡处理 * 传染病报告卡处理
* 通过诊断名称自动识别并勾选传染病报告卡中的疾病 * 通过诊断名称自动识别并勾选传染病报告卡中的疾病
* 修复 Bug #519跳过已有已提交报卡的诊断
*/ */
function handleInfectiousDiseaseReport() { function handleInfectiousDiseaseReport() {
// 疾病名称到报卡编码的映射(根据传染病报告卡弹窗中的疾病列表) // 疾病名称到报卡编码的映射(根据传染病报告卡弹窗中的疾病列表)
@@ -743,8 +744,9 @@ function handleInfectiousDiseaseReport() {
'手足口病': '0311', '手足口病': '0311',
}; };
// 获取所有诊断名称对应的报卡编码 // 获取所有诊断名称对应的报卡编码,但跳过已有已提交报卡的诊断
const allSelectedDiseases = form.value.diagnosisList const allSelectedDiseases = form.value.diagnosisList
.filter(d => d.name && d.hasInfectiousReport !== 1)
.map(d => diseaseNameToCode[d.name] || null) .map(d => diseaseNameToCode[d.name] || null)
.filter(code => code); .filter(code => code);
@@ -752,9 +754,9 @@ function handleInfectiousDiseaseReport() {
return; return;
} }
// 优先使用主诊断 // 优先使用主诊断(同样跳过已有报卡的)
const mainDiagnosis = form.value.diagnosisList.find(d => d.maindiseFlag === 1); const mainDiagnosis = form.value.diagnosisList.find(d => d.maindiseFlag === 1 && d.hasInfectiousReport !== 1);
const firstDiagnosis = form.value.diagnosisList[0]; const firstDiagnosis = form.value.diagnosisList.find(d => d.hasInfectiousReport !== 1) || form.value.diagnosisList[0];
const diagnosisToShow = { const diagnosisToShow = {
...(mainDiagnosis || firstDiagnosis), ...(mainDiagnosis || firstDiagnosis),

View File

@@ -49,6 +49,40 @@
</el-col> </el-col>
</el-row> </el-row>
<!-- 性别出生日期或实足年龄 -->
<el-row :gutter="16" class="form-row">
<el-col :span="7" class="form-item">
<span class="form-label required">性别</span>
<el-radio-group v-model="form.sex" class="gender-radio-group">
<el-radio value="男"></el-radio>
<el-radio value="女"></el-radio>
<el-radio value="未知">未知</el-radio>
</el-radio-group>
</el-col>
<el-col :span="10" class="form-item">
<span class="form-label required">出生日期</span>
<div class="birth-input-group">
<el-input v-model="form.birthYear" class="birth-input year" placeholder="年" maxlength="4" />
<span class="birth-separator"></span>
<el-input v-model="form.birthMonth" class="birth-input month" placeholder="月" maxlength="2" />
<span class="birth-separator"></span>
<el-input v-model="form.birthDay" class="birth-input day" placeholder="日" maxlength="2" />
<span class="birth-separator"></span>
</div>
</el-col>
<el-col :span="7" class="form-item">
<span class="form-label"> 实足年龄</span>
<div class="age-input-group">
<el-input v-model="form.age" class="age-input" placeholder="年龄" />
<el-select v-model="form.ageUnit" class="age-unit-select">
<el-option label="岁" value="岁" />
<el-option label="月" value="月" />
<el-option label="天" value="天" />
</el-select>
</div>
</el-col>
</el-row>
<!-- 联系电话紧急联系人电话 --> <!-- 联系电话紧急联系人电话 -->
<el-row :gutter="16" class="form-row"> <el-row :gutter="16" class="form-row">
<el-col :span="12" class="form-item"> <el-col :span="12" class="form-item">
@@ -1000,6 +1034,17 @@ function normalizeSex(value) {
return '未知'; return '未知';
} }
function normalizeSexFromPatientInfo(patientInfo) {
// 优先使用文本字段
if (patientInfo.genderEnum_enumText) return patientInfo.genderEnum_enumText;
if (patientInfo.genderName) return patientInfo.genderName;
if (patientInfo.sex) return normalizeSex(patientInfo.sex);
// 使用数字枚举字段
if (patientInfo.genderEnum === 1) return '男';
if (patientInfo.genderEnum === 2) return '女';
return '未知';
}
function normalizeAgeUnit(value) { function normalizeAgeUnit(value) {
const ageUnitMap = { const ageUnitMap = {
1: '岁', 1: '岁',
@@ -1261,7 +1306,7 @@ async function show(diagnosisData) {
patName: patientInfo.patientName || patientInfo.name || '', // 患者姓名 patName: patientInfo.patientName || patientInfo.name || '', // 患者姓名
parentName: '', // 家长姓名14岁以下患者必填 parentName: '', // 家长姓名14岁以下患者必填
idNo: patientInfo.idCard, // 身份证号 idNo: patientInfo.idCard, // 身份证号
sex: patientInfo.sex || patientInfo.genderName || '男', // 性别 sex: normalizeSexFromPatientInfo(patientInfo), // 性别
// 出生日期信息 // 出生日期信息
birthYear: birthInfo.year, // 出生年份 birthYear: birthInfo.year, // 出生年份
@@ -1366,6 +1411,9 @@ async function buildSubmitData() {
} else if (formData.otherDisease) { } else if (formData.otherDisease) {
// 其他传染病使用自定义编码 // 其他传染病使用自定义编码
diseaseCode = 'OTHER'; diseaseCode = 'OTHER';
} else if (formData.selectedDiseases && formData.selectedDiseases.length > 0) {
// 兜底:如果 ClassA/B/C 都为空但 selectedDiseases 有值,取第一个作为 diseaseCode
diseaseCode = formData.selectedDiseases[0];
} }
// 转换年龄单位:岁=1, 月=2, 天=3 // 转换年龄单位:岁=1, 月=2, 天=3
@@ -1775,6 +1823,33 @@ defineExpose({ show, showReport, close: handleClose });
color: #999; color: #999;
} }
/* 输入框下划线样式(与 underline-select 保持一致) */
.underline-input :deep(.el-input__wrapper) {
border: none;
border-bottom: 1px solid #dcdfe6;
border-radius: 0;
box-shadow: none;
background: transparent;
}
.underline-input :deep(.el-input__wrapper:hover) {
border-bottom-color: #c0c4cc;
}
.underline-input :deep(.el-input__wrapper.is-focus) {
border-bottom-color: #409eff;
}
.underline-input :deep(.el-input__inner) {
font-size: 12px;
color: #666;
}
.underline-input :deep(.el-input__inner::placeholder) {
font-size: 12px;
color: #999;
}
/* 街道下拉框下划线样式 */ /* 街道下拉框下划线样式 */
.underline-select { .underline-select {
width: 100%; width: 100%;
@@ -1962,4 +2037,53 @@ defineExpose({ show, showReport, close: handleClose });
display: flex !important; display: flex !important;
justify-content: center !important; justify-content: center !important;
} }
/* 性别单选按钮组 */
.gender-radio-group {
display: flex;
gap: 12px;
padding-top: 4px;
}
/* 出生日期输入组 */
.birth-input-group {
display: flex;
align-items: center;
gap: 2px;
}
.birth-input {
text-align: center;
}
.birth-input.year {
width: 70px;
}
.birth-input.month,
.birth-input.day {
width: 50px;
}
.birth-separator {
color: #606266;
font-size: 13px;
margin: 0 2px;
}
/* 年龄输入组 */
.age-input-group {
display: flex;
align-items: center;
gap: 6px;
}
.age-input {
width: 70px;
text-align: center;
}
.age-unit-select {
width: 65px;
}
</style> </style>

View File

@@ -56,6 +56,13 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" /> <el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
<el-table-column label="单据状态" prop="applyStatus" width="100" align="center" header-align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.applyStatus)" size="small">
{{ getStatusLabel(scope.row.applyStatus, scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center"> <el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.itemName }}</span> <span>{{ scope.row.itemName }}</span>
@@ -1445,6 +1452,26 @@ const formatAmount = (amount) => {
return num.toFixed(2) return num.toFixed(2)
} }
// 单据状态标签文字
const getStatusLabel = (applyStatus, row) => {
// applyStatus: 0=待开立, 1=已开立(已签发)
// 结合收费/执行标记推导更丰富的状态
if (applyStatus === 1) {
// 已收费后根据执行标记判断
if (row.needExecute === true) {
return '已执行'
}
return '已开立'
}
return '待开立'
}
// 单据状态标签颜色
const getStatusType = (applyStatus) => {
if (applyStatus === 1) return 'success'
return 'info'
}
// 格式化日期时间为字符串 YYYY-MM-DD HH:mm:ss // 格式化日期时间为字符串 YYYY-MM-DD HH:mm:ss
const formatDateTime = (date) => { const formatDateTime = (date) => {
if (!date) return '' if (!date) return ''

View File

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

View File

@@ -315,6 +315,7 @@
data-prop="dispensePerDuration"> data-prop="dispensePerDuration">
<el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1" <el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1"
controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)" controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)"
@input="calculateTotalAmount(scope.row, scope.$index)"
@change="calculateTotalAmount(scope.row, scope.$index)" @change="calculateTotalAmount(scope.row, scope.$index)"
@keyup.enter.prevent=" @keyup.enter.prevent="
handleEnter('dispensePerDuration', scope.row, scope.$index) handleEnter('dispensePerDuration', scope.row, scope.$index)
@@ -874,7 +875,7 @@ import { ArrowDown, Search, Memo, Minus, Plus, Edit, Delete } from '@element-plu
import printUtils, { getPrinterList, PRINT_TEMPLATE, savePrinterToCache, } from '@/utils/printUtils'; import printUtils, { getPrinterList, PRINT_TEMPLATE, savePrinterToCache, } from '@/utils/printUtils';
import Template from "@/views/inpatientDoctor/home/emr/components/template.vue"; import Template from "@/views/inpatientDoctor/home/emr/components/template.vue";
const emit = defineEmits(['selectDiagnosis']); const emit = defineEmits(['selectDiagnosis', 'inspectionListRefresh']);
const total = ref(0); const total = ref(0);
const queryParams = ref({}); const queryParams = ref({});
const prescriptionList = ref([]); const prescriptionList = ref([]);
@@ -1025,7 +1026,7 @@ const mapAdviceTypeLabel = (type, adviceTableName) => {
return found.label; return found.label;
} }
// 🔧 Bug #458 Fix: 诊疗/手术类型字典缺失时的兜底,避免保存后"医嘱类型"列显示为空 // 🔧 Bug #458 Fix: 诊疗/手术类型字典缺失或标签为空时的兜底
if (adviceTableName === 'wor_activity_definition' || adviceTableName === 'wor_service_request') { if (adviceTableName === 'wor_activity_definition' || adviceTableName === 'wor_service_request') {
if (type === 6) return '手术'; if (type === 6) return '手术';
if (type === 4) return '手术'; if (type === 4) return '手术';
@@ -1035,6 +1036,15 @@ const mapAdviceTypeLabel = (type, adviceTableName) => {
return '诊疗'; return '诊疗';
} }
// 🔧 Bug #458 Fix: 兜底映射,确保所有有效 adviceType 都有显示标签
// 不依赖字典数据和表名,直接返回标准类型名称
if (type === 3) return '诊疗';
if (type === 6) return '手术';
if (type === 4) return '耗材';
if (type === 1) return '西药';
if (type === 2) return '中成药';
if (type === 5) return '会诊';
return ''; return '';
}; };
@@ -1662,7 +1672,11 @@ function getListInfo(addNewRow) {
// 检查 adviceTableName如果是耗材表则应该是耗材类型 // 检查 adviceTableName如果是耗材表则应该是耗材类型
const adviceTableName = contentJson?.adviceTableName || item.adviceTableName; const adviceTableName = contentJson?.adviceTableName || item.adviceTableName;
let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(adviceType, adviceTableName); // 🔧 Bug #458 Fix: 后端可能返回空字符串的 adviceType_dictText需重新计算
const backendDictText = item.adviceType_dictText;
let adviceType_dictText = (backendDictText && backendDictText.trim())
? backendDictText
: mapAdviceTypeLabel(adviceType, adviceTableName);
// 如果是会诊类型,设置为会诊类型 // 如果是会诊类型,设置为会诊类型
if (isConsultation) { if (isConsultation) {
@@ -2083,6 +2097,21 @@ function getOrgList() {
}); });
} }
/** 诊疗医嘱关联检验申请时 contentJson 含 applyNo */
function getInspectionApplyNoFromAdviceRow(row) {
if (!row || row.adviceType !== 3) {
return null;
}
try {
const raw = row.contentJson;
const j = raw ? (typeof raw === 'string' ? JSON.parse(raw) : raw) : {};
const no = j && j.applyNo != null ? String(j.applyNo).trim() : '';
return no || null;
} catch (e) {
return null;
}
}
function handleDelete() { function handleDelete() {
let selectRows = prescriptionRef.value.getSelectionRows(); let selectRows = prescriptionRef.value.getSelectionRows();
console.log('BugFix#219: handleDelete called, selectRows=', selectRows); console.log('BugFix#219: handleDelete called, selectRows=', selectRows);
@@ -2262,12 +2291,31 @@ function handleDelete() {
} }
if (deleteList.length > 0) { if (deleteList.length > 0) {
const hasLabLinked = deleteList.some((d) => {
const row = normalRows.find((r) => r.requestId === d.requestId);
return row && getInspectionApplyNoFromAdviceRow(row);
});
const runApiDelete = () => {
savePrescription({ adviceSaveList: deleteList }).then((res) => { savePrescription({ adviceSaveList: deleteList }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess('删除成功'); proxy.$modal.msgSuccess('删除成功');
getListInfo(false); getListInfo(false);
emit('inspectionListRefresh');
} }
}); });
};
if (hasLabLinked) {
proxy.$modal
.confirm(
'删除此医嘱将同时作废关联的检验申请单(检验页签中的同单申请及同单下相关医嘱)。是否继续?',
'删除确认',
{ type: 'warning' }
)
.then(runApiDelete)
.catch(() => {});
} else {
runApiDelete();
}
} else if (consultationRows.length == 0) { } else if (consultationRows.length == 0) {
proxy.$modal.msgWarning('所选医嘱不可删除,请先撤回后再删除'); proxy.$modal.msgWarning('所选医嘱不可删除,请先撤回后再删除');
return; return;
@@ -3418,7 +3466,12 @@ async function setValue(row) {
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText); console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
// 🔧 Bug #455: 诊疗医嘱(adviceType=3)的执行科室默认使用患者就诊科室, // 🔧 Bug #455: 诊疗医嘱(adviceType=3)的执行科室默认使用患者就诊科室,
// 不使用positionId(诊疗目录配置的执行科室)避免配置ID不在机构树中导致显示原始ID // 不使用positionId(诊疗目录配置的执行科室)避免配置ID不在机构树中导致显示原始ID
if (Number(row.adviceType) != 3) { if (Number(row.adviceType) == 3) {
// 覆盖 catalog 传来的 positionId/orgId使用患者就诊科室
prescriptionList.value[rowIndex.value].orgId = props.patientInfo?.orgId;
prescriptionList.value[rowIndex.value].positionId = props.patientInfo?.orgId;
prescriptionList.value[rowIndex.value].positionName = findOrgNameById(props.patientInfo?.orgId) || props.patientInfo?.orgName || '';
} else {
prescriptionList.value[rowIndex.value].orgId = row.positionId || props.patientInfo?.orgId; prescriptionList.value[rowIndex.value].orgId = row.positionId || props.patientInfo?.orgId;
} }
prescriptionList.value[rowIndex.value].dose = row.dose || row.doseQuantity; prescriptionList.value[rowIndex.value].dose = row.dose || row.doseQuantity;
@@ -3551,13 +3604,10 @@ async function setValue(row) {
prescriptionList.value[rowIndex.value].categoryEnum = 31; // 会诊的category_enum设置为31 prescriptionList.value[rowIndex.value].categoryEnum = 31; // 会诊的category_enum设置为31
} else { } else {
// 诊疗类型adviceType == 3 // 诊疗类型adviceType == 3
// 🔧 Bug Fix #238: 诊疗项目默认使用患者就诊科室 // 🔧 Bug #455: 诊疗项目执行科室强制使用患者就诊科室
if (!prescriptionList.value[rowIndex.value].orgId) { // 不使用目录配置的执行科室可能是错误ID或占位符导致显示原始ID
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId; prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId;
} prescriptionList.value[rowIndex.value].positionName = findOrgNameById(props.patientInfo.orgId) || props.patientInfo.orgName || '';
if (!prescriptionList.value[rowIndex.value].positionName) {
prescriptionList.value[rowIndex.value].positionName = findOrgNameById(prescriptionList.value[rowIndex.value].orgId) || props.patientInfo.orgName || '';
}
// 🔧 Bug #218 修复使用组套中维护的quantity如果没有则默认1 // 🔧 Bug #218 修复使用组套中维护的quantity如果没有则默认1
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1; prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
// 🔧 Bug #144 修复:安全访问 priceList防止 orderDetailInfos 为空时出错 // 🔧 Bug #144 修复:安全访问 priceList防止 orderDetailInfos 为空时出错
@@ -3652,7 +3702,10 @@ function handleSaveGroup(orderGroupList) {
unitCode_dictText: item.unitCodeName || '', unitCode_dictText: item.unitCodeName || '',
statusEnum: 1, statusEnum: 1,
// 🔧 修复执行科室逻辑:优先使用 orgId(所属科室),其次 positionId // 🔧 修复执行科室逻辑:优先使用 orgId(所属科室),其次 positionId
orgId: item.orderDetailInfos?.orgId || mergedDetail.orgId || item.positionId || item.orderDetailInfos?.positionId || mergedDetail.positionId, // 🔧 Bug #455: 诊疗类(adviceType=3)使用患者就诊科室不使用目录配置的ID
orgId: item.adviceType === 3
? props.patientInfo?.orgId
: (item.orderDetailInfos?.orgId || mergedDetail.orgId || item.positionId || item.orderDetailInfos?.positionId || mergedDetail.positionId),
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1', dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1',
conditionId: conditionId.value, conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value, conditionDefinitionId: conditionDefinitionId.value,

View File

@@ -633,10 +633,14 @@ function getList() {
pageSize: 100, pageSize: 100,
encounterId: props.patientInfo.encounterId encounterId: props.patientInfo.encounterId
}).then((res) => { }).then((res) => {
surgeryList.value = res.data.records || [] if (res.code === 200) {
surgeryList.value = res.data?.records || []
} else {
console.warn('手术列表加载失败(可能无权限或数据异常):', res.msg)
surgeryList.value = []
}
}).catch(error => { }).catch(error => {
console.error('获取手术列表失败:', error) console.warn('手术列表请求异常:', error)
proxy.$modal.msgError('数据加载失败,请稍后重试')
surgeryList.value = [] surgeryList.value = []
}).finally(() => { }).finally(() => {
loading.value = false loading.value = false
@@ -1133,13 +1137,14 @@ function submitForm() {
// 新增手术 // 新增手术
addSurgery(form.value).then((res) => { addSurgery(form.value).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请提交成功!') proxy.$modal.msgSuccess('手术申请提交成功!')
// 保存麻醉方式 // 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum) sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false open.value = false
emit('saved') // 通知父组件刷新医嘱列表 // 子组件自身主动刷新列表(立即),确保数据展示不依赖父组件事件
// 刷新手术申请列表
getList() getList()
// 通知父组件刷新医嘱列表(父组件也会带延迟再次刷新手术列表作为兜底)
emit('saved')
} else { } else {
proxy.$modal.msgError(res.msg || '新增手术失败,请检查表单信息') proxy.$modal.msgError(res.msg || '新增手术失败,请检查表单信息')
} }
@@ -1151,13 +1156,14 @@ function submitForm() {
// 修改手术 // 修改手术
updateSurgery(form.value).then((res) => { updateSurgery(form.value).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请修改成功!') proxy.$modal.msgSuccess('手术申请修改成功!')
// 保存麻醉方式 // 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum) sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false open.value = false
emit('saved') // 通知父组件刷新医嘱列表 // 子组件自身主动刷新列表(立即)
// 刷新手术申请列表
getList() getList()
// 由父组件 @saved 事件负责刷新列表
emit('saved')
} else { } else {
proxy.$modal.msgError(res.msg || '更新手术失败,请检查表单信息') proxy.$modal.msgError(res.msg || '更新手术失败,请检查表单信息')
} }

View File

@@ -138,7 +138,8 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="医嘱" name="prescription"> <el-tab-pane label="医嘱" name="prescription">
<prescriptionlist :patientInfo="patientInfo" ref="prescriptionRef" :activeTab="activeTab" <prescriptionlist :patientInfo="patientInfo" ref="prescriptionRef" :activeTab="activeTab"
:outpatientEmrSaved="outpatientEmrSaved" /> :outpatientEmrSaved="outpatientEmrSaved"
@inspectionListRefresh="refreshInspectionListFromAdvice" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="中医" name="tcm"> <el-tab-pane label="中医" name="tcm">
<tcmAdvice :patientInfo="patientInfo" ref="tcmRef" /> <tcmAdvice :patientInfo="patientInfo" ref="tcmRef" />
@@ -152,7 +153,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="手术申请" name="surgery"> <el-tab-pane label="手术申请" name="surgery">
<surgeryApplication :patientInfo="patientInfo" :activeTab="activeTab" ref="surgeryRef" <surgeryApplication :patientInfo="patientInfo" :activeTab="activeTab" ref="surgeryRef"
@saved="() => prescriptionRef?.getListInfo()" /> @saved="() => { prescriptionRef?.getListInfo(); setTimeout(() => surgeryRef?.getList(), 500) }" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="电子处方" name="eprescription"> <el-tab-pane label="电子处方" name="eprescription">
<eprescriptionlist :patientInfo="patientInfo" ref="eprescriptionRef" /> <eprescriptionlist :patientInfo="patientInfo" ref="eprescriptionRef" />
@@ -312,6 +313,9 @@ const patientDrawerRef = ref();
const prescriptionRef = ref(); const prescriptionRef = ref();
const tcmRef = ref(); const tcmRef = ref();
const inspectionRef = ref(); const inspectionRef = ref();
function refreshInspectionListFromAdvice() {
inspectionRef.value?.getList?.();
}
const examinationRef = ref(); const examinationRef = ref();
const surgeryRef = ref(); const surgeryRef = ref();
const emrRef = ref(); const emrRef = ref();

View File

@@ -51,7 +51,7 @@ const currentSelectRow = ref<any>({});
const queryParams = ref({ const queryParams = ref({
pageSize: 100, pageSize: 100,
pageNo: 1, pageNo: 1,
adviceTypes: '1,2,3,6', adviceTypes: [1, 2, 3, 6],
searchKey: '', searchKey: '',
organizationId: '', organizationId: '',
categoryCode: '', categoryCode: '',
@@ -88,10 +88,10 @@ const tableColumns = computed<TableColumn[]>(() => [
function refresh(adviceType: any, categoryCode: string, searchKey: string) { function refresh(adviceType: any, categoryCode: string, searchKey: string) {
// 有搜索词时跨类型搜索,避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目 // 有搜索词时跨类型搜索,避免用户输入"级护理"但因当前adviceType为药品而搜不到诊疗类护理项目
if (searchKey) { if (searchKey) {
queryParams.value.adviceTypes = '1,2,3,6'; queryParams.value.adviceTypes = [1, 2, 3, 6];
} else { } else {
queryParams.value.adviceTypes = queryParams.value.adviceTypes =
adviceType !== undefined && adviceType !== '' ? String(adviceType) : '1,2,3,6'; adviceType !== undefined && adviceType !== '' ? [parseInt(adviceType)] : [1, 2, 3, 6];
} }
queryParams.value.categoryCode = categoryCode || ''; queryParams.value.categoryCode = categoryCode || '';
queryParams.value.searchKey = searchKey || ''; queryParams.value.searchKey = searchKey || '';
@@ -131,7 +131,8 @@ function getList() {
} }
}); });
}) })
.catch(() => { .catch((err) => {
console.warn('医嘱基础信息加载失败:', err);
adviceBaseList.value = []; adviceBaseList.value = [];
}) })
.finally(() => { .finally(() => {

View File

@@ -49,6 +49,15 @@
<el-option label="已作废" value="7" /> <el-option label="已作废" value="7" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键字">
<el-input
v-model="filterForm.keyword"
placeholder="申请单号 / 检查项目名称"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading"> <el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon> <el-icon><Search /></el-icon>
@@ -77,18 +86,53 @@
</template> </template>
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" /> <el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" /> <el-table-column label="申请单名称" width="140">
<el-table-column prop="createTime" label="创建时间" width="160" />
<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"> <template #default="scope">
<span>{{ parseStatus(scope.row.status) }}</span> <span>{{ buildApplicationName(scope.row) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" fixed="right"> <el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column label="申请单状态" width="120" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)" effect="plain" round>
{{ parseStatus(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" width="280" align="center" fixed="right">
<template #default="scope">
<!-- 详情 - 所有状态都显示 -->
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button> <el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
<!-- 待签发修改删除 -->
<template v-if="scope.row.status === '0' || scope.row.status === 0">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
<!-- 已签发撤回 -->
<template v-else-if="scope.row.status === '1' || scope.row.status === 1">
<el-button link type="warning" @click="handleRecall(scope.row)">撤回</el-button>
</template>
<!-- 已校对/待接收打印 -->
<template v-else-if="scope.row.status === '2' || scope.row.status === 2 || scope.row.status === '3' || scope.row.status === 3">
<el-button link type="primary" @click="handlePrint(scope.row)">打印</el-button>
</template>
<!-- 已接收/已检查看报告 -->
<template v-else-if="scope.row.status === '4' || scope.row.status === 4 || scope.row.status === '5' || scope.row.status === 5">
<el-button link type="success" @click="handleViewReport(scope.row)">看报告</el-button>
</template>
<!-- 已出报告打印看报告 -->
<template v-else-if="scope.row.status === '6' || scope.row.status === 6">
<el-button link type="primary" @click="handlePrint(scope.row)">打印</el-button>
<el-button link type="success" @click="handleViewReport(scope.row)">看报告</el-button>
</template>
<!-- 已作废无额外按钮 -->
<template v-else-if="scope.row.status === '7' || scope.row.status === 7">
</template>
<!-- 其他/未知状态仅详情 -->
<template v-else>
</template>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -137,7 +181,7 @@
<el-descriptions title="申请单描述" :column="2"> <el-descriptions title="申请单描述" :column="2">
<template v-for="(value, key) in descJsonData" :key="key"> <template v-for="(value, key) in descJsonData" :key="key">
<el-descriptions-item v-if="isFieldMatched(key)" :label="getFieldLabel(key)"> <el-descriptions-item v-if="isFieldMatched(key)" :label="getFieldLabel(key)">
{{ value || '-' }} {{ transformField(key, value) || '-' }}
</el-descriptions-item> </el-descriptions-item>
</template> </template>
</el-descriptions> </el-descriptions>
@@ -160,15 +204,102 @@
<el-button @click="detailDialogVisible = false">关闭</el-button> <el-button @click="detailDialogVisible = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 修改申请单弹窗 - 复用检查申请单组件 -->
<el-dialog
v-model="editDialogVisible"
title="修改检查申请"
width="1200px"
destroy-on-close
top="5vh"
:close-on-click-modal="false"
>
<MedicalExaminations
v-if="editDialogVisible"
ref="editFormRef"
:is-edit-mode="true"
:edit-data="editingRow"
:external-patient-info="patientInfo"
/>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditSubmit">确认</el-button>
</template>
</el-dialog>
<!-- 查看报告弹窗 -->
<el-dialog
v-model="reportDialogVisible"
:title="'检查报告 - ' + (reportData?.prescriptionNo || '')"
width="900px"
destroy-on-close
top="5vh"
:close-on-click-modal="false"
>
<div v-loading="reportLoading" class="report-viewer">
<!-- 报告基本信息 -->
<div v-if="reportData" class="report-viewer-container">
<el-descriptions title="报告信息" :column="2" border size="small">
<el-descriptions-item label="患者姓名">{{ reportData.patientName || reportRow?.patientName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请单号">{{ reportData.prescriptionNo || reportRow?.prescriptionNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请单名称">{{ reportData.name || reportRow?.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="报告时间">{{ reportData.reportTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="诊断意见" :span="2">{{ reportData.diagnosis || reportData.conclusion || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 报告详细内容 -->
<div v-if="reportData.content" class="report-content-section">
<div class="section-title">报告内容</div>
<div class="report-content-text">{{ reportData.content }}</div>
</div>
<!-- 影像预览 - PACS链接 -->
<div v-if="reportData.imageUrl || reportData.pacsUrl" class="report-image-section">
<div class="section-title">
影像预览
<el-button
v-if="reportData.imageUrl || reportData.pacsUrl"
type="primary"
size="small"
plain
style="margin-left: 12px;"
@click="openPacsLink"
>
<el-icon><Link /></el-icon>
打开PACS影像
</el-button>
</div>
<iframe
v-if="reportData.imageUrl"
:src="reportData.imageUrl"
class="report-iframe"
frameborder="0"
/>
<el-empty v-else-if="!reportData.imageUrl && reportData.pacsUrl" description="点击上方按钮打开PACS影像" :image-size="60" />
</div>
<!-- 完全无数据时的提示 -->
<el-empty v-if="!reportData.content && !reportData.imageUrl && !reportData.pacsUrl" description="暂无详细报告数据" :image-size="60" />
</div>
<!-- 未获取到报告 -->
<el-empty v-else description="暂未生成报告" :image-size="80" />
</div>
<template #footer>
<el-button @click="reportDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import {computed, getCurrentInstance, ref, watch} from 'vue'; import {computed, getCurrentInstance, ref, watch, nextTick} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue'; import {Refresh, Search, Link} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js'; import {patientInfo} from '../../store/patient.js';
import {getCheck} from './api'; import {getCheck, deleteRequestForm, withdrawRequestForm, getTestResult} from './api';
import {getDepartmentList} from '@/api/public.js'; import {getDepartmentList} from '@/api/public.js';
import {getApplicationList, saveCheckd} from '../order/applicationForm/api';
import MedicalExaminations from '../order/applicationForm/medicalExaminations.vue';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -178,11 +309,38 @@ const detailDialogVisible = ref(false);
const currentDetail = ref(null); const currentDetail = ref(null);
const descJsonData = ref(null); const descJsonData = ref(null);
const orgOptions = ref([]); const orgOptions = ref([]);
const editForm = ref({
name: '',
targetDepartment: '',
symptom: '',
sign: '',
clinicalDiagnosis: '',
otherDiagnosis: '',
relatedResult: '',
attention: '',
examinationPurpose: '',
medicalHistorySummary: '',
});
// 报告弹窗相关
const reportDialogVisible = ref(false);
const reportLoading = ref(false);
const reportData = ref(null);
const reportRow = ref(null);
// 获取近7天的日期范围作为默认值
const getDefaultDateRange = () => {
const now = new Date();
const endDate = now.toISOString().split('T')[0];
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
return [startDate, endDate];
};
// 筛选表单数据 // 筛选表单数据
const filterForm = ref({ const filterForm = ref({
dateRange: [], // [startDate, endDate] dateRange: getDefaultDateRange(), // 默认近一周
status: '', // 申请单状态 status: '', // 申请单状态
keyword: '', // 关键字搜索
}); });
const fetchData = async () => { const fetchData = async () => {
@@ -207,11 +365,18 @@ const fetchData = async () => {
params.status = filterForm.value.status; params.status = filterForm.value.status;
} }
// 添加关键字搜索
if (filterForm.value.keyword && filterForm.value.keyword.trim()) {
params.keyword = filterForm.value.keyword.trim();
}
const res = await getCheck(params); const res = await getCheck(params);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const raw = res.data?.records || res.data; const raw = res.data?.records || res.data;
const list = Array.isArray(raw) ? raw : [raw]; const list = Array.isArray(raw) ? raw : [raw];
console.log('API返回的原始数据:', JSON.stringify(list, null, 2));
tableData.value = list.filter(Boolean); tableData.value = list.filter(Boolean);
console.log('tableData设置后的第一条:', tableData.value[0]);
} else { } else {
tableData.value = []; tableData.value = [];
} }
@@ -243,8 +408,9 @@ const handleSearch = async () => {
* 重置按钮处理 * 重置按钮处理
*/ */
const handleReset = () => { const handleReset = () => {
filterForm.value.dateRange = []; filterForm.value.dateRange = getDefaultDateRange();
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
fetchData(); fetchData();
}; };
@@ -267,9 +433,51 @@ const parseStatus = (status) => {
return statusMap[String(status)] || '-'; return statusMap[String(status)] || '-';
}; };
/**
* 根据申请单详情构建申请单名称
* 单一项目:显示项目名称
* 多个项目:显示首个项目名称+"等X项"
*/
const buildApplicationName = (row) => {
const details = row.requestFormDetailList;
if (!details || details.length === 0) {
return row.name || '-';
}
if (details.length === 1) {
return details[0].adviceName || row.name || '-';
}
const first = details[0];
return `${first.adviceName || ''}${details.length}`;
};
/**
* 获取状态标签类型 - 参考临床医嘱样式
* @param {string|number} status - 状态码
* @returns {string} el-tag type
*/
const getStatusTagType = (status) => {
const typeMap = {
'0': 'primary', // 待签发 - 蓝色
'1': 'success', // 已签发 - 绿色
'2': 'success', // 已校对 - 绿色
'3': 'primary', // 待接收 - 蓝色
'4': 'primary', // 已接收 - 蓝色
'5': 'success', // 已检查 - 绿色
'6': 'success', // 已出报告 - 绿色
'7': 'danger', // 已作废 - 红色
};
return typeMap[String(status)] || 'info';
};
const labelMap = { const labelMap = {
categoryType: '项目类别', categoryType: '项目类别',
targetDepartment: '发往科室', targetDepartment: '发往科室',
urgencyLevel: '紧急程度',
allergyHistory: '过敏史',
examinationPurpose: '检查目的',
expectedExaminationTime: '期望检查时间',
medicalHistorySummary: '病史摘要',
allergyConfirmed: '过敏确认',
symptom: '症状', symptom: '症状',
sign: '体征', sign: '体征',
clinicalDiagnosis: '临床诊断', clinicalDiagnosis: '临床诊断',
@@ -278,6 +486,17 @@ const labelMap = {
attention: '注意事项', attention: '注意事项',
}; };
// Fields that need value transformation before display
const transformField = (key, value) => {
if (key === 'urgencyLevel') {
return value === 'emergency' ? '急诊' : '普通';
}
if (key === 'allergyConfirmed') {
return value === true || value === 'true' ? '已口头确认' : '未确认';
}
return value;
};
const isFieldMatched = (key) => { const isFieldMatched = (key) => {
return key in labelMap; return key in labelMap;
}; };
@@ -292,50 +511,68 @@ const hasMatchedFields = computed(() => {
}); });
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = () => { const getLocationInfo = async () => {
getDepartmentList().then((res) => { try {
orgOptions.value = res.data || []; const res = await getDepartmentList();
}); orgOptions.value = Array.isArray(res.data) ? res.data : [];
} catch (e) {
console.warn('科室列表加载失败:', e.message);
orgOptions.value = [];
}
};
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
for (const item of list) {
if (item.id == id) return item;
if (item.children && item.children.length > 0) {
const found = findTreeItem(item.children, id);
if (found) return found;
}
}
return null;
}; };
const recursionFun = (targetDepartment) => { const recursionFun = (targetDepartment) => {
if (!targetDepartment) return '';
let name = ''; let name = '';
for (let index = 0; index < orgOptions.value.length; index++) { for (let index = 0; index < orgOptions.value.length; index++) {
const obj = orgOptions.value[index]; const obj = orgOptions.value[index];
if (obj.id == targetDepartment) { if (obj.id == targetDepartment) {
name = obj.name; name = obj.name;
break;
} }
const subObjArray = obj['children']; const subObjArray = obj['children'];
if (subObjArray && subObjArray.length > 0) { if (subObjArray && subObjArray.length > 0) {
for (let index = 0; index < subObjArray.length; index++) { for (let i = 0; i < subObjArray.length; i++) {
const item = subObjArray[index]; const item = subObjArray[i];
if (item.id == targetDepartment) { if (item.id == targetDepartment) {
name = item.name; name = item.name;
break;
} }
} }
} }
if (name) break;
} }
return name; return name;
}; };
const handleViewDetail = async (row) => { const handleViewDetail = async (row) => {
console.log('targetDepartment========>', JSON.stringify(row)); // 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row; currentDetail.value = row;
// 解析 descJson // 解析 descJson
if (row.descJson) { if (row.descJson) {
try { try {
const obj = JSON.parse(row.descJson); const obj = JSON.parse(row.descJson);
// 确保科室数据已加载 // 将发往科室 ID 转换为名称
if (!orgOptions.value || orgOptions.value.length === 0) { if (obj.targetDepartment) {
await new Promise((resolve) => {
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
resolve();
});
});
}
obj.targetDepartment = recursionFun(obj.targetDepartment); obj.targetDepartment = recursionFun(obj.targetDepartment);
}
descJsonData.value = obj; descJsonData.value = obj;
} catch (e) { } catch (e) {
console.error('解析 descJson 失败:', e); console.error('解析 descJson 失败:', e);
@@ -347,6 +584,396 @@ const handleViewDetail = async (row) => {
detailDialogVisible.value = true; detailDialogVisible.value = true;
}; };
/**
* 删除申请单 - 仅待签发状态可用
*/
const handleDelete = async (row) => {
try {
await proxy.$modal?.confirm?.('确认删除该检查申请单?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const res = await deleteRequestForm({ requestFormId: row.requestFormId });
if (res.code === 200) {
proxy.$modal?.msgSuccess?.('删除成功');
fetchData();
} else {
proxy.$modal?.msgError?.(res.msg || '删除失败');
}
} catch (e) {
// 用户取消操作,不做处理
}
};
/**
* 撤回申请单 - 仅已签发状态可用
*/
const handleRecall = async (row) => {
try {
await proxy.$modal?.confirm?.('确认撤回该申请单?撤回后状态将变更为"待签发"。', '提示', {
confirmButtonText: '确定撤回',
cancelButtonText: '取消',
type: 'warning',
});
const res = await withdrawRequestForm({ requestFormId: row.requestFormId });
if (res.code === 200) {
proxy.$modal?.msgSuccess?.('撤回成功');
fetchData();
} else {
proxy.$modal?.msgError?.(res.msg || '撤回失败');
}
} catch (e) {
// 用户取消操作,不做处理
}
};
/**
* 打印申请单 - 已校对/待接收/已接收/已检查状态可用
* 打印内容与详情展示一致,并包含申请单条码
*/
const handlePrint = async (row) => {
try {
proxy.$modal?.msgInfo?.('正在生成打印预览...');
// 确保科室数据已加载,用于解析发往科室名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await new Promise((resolve) => {
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
resolve();
});
});
}
// 解析 descJson
let descData = {};
if (row.descJson) {
try {
const obj = JSON.parse(row.descJson);
// 将发往科室ID转换为名称
if (obj.targetDepartment) {
obj.targetDepartment = recursionFun(obj.targetDepartment);
}
descData = obj;
} catch (e) {
console.error('解析 descJson 失败:', e);
}
}
// 构建诊疗项目表格行
let detailRowsHtml = '';
const detailList = row.requestFormDetailList || [];
if (detailList.length > 0) {
detailList.forEach((item, index) => {
detailRowsHtml += `
<tr>
<td style="text-align:center;padding:6px 8px;border:1px solid #ddd;">${index + 1}</td>
<td style="padding:6px 8px;border:1px solid #ddd;">${item.adviceName || '-'}</td>
<td style="text-align:center;padding:6px 8px;border:1px solid #ddd;">${item.quantity || '-'}</td>
<td style="padding:6px 8px;border:1px solid #ddd;">${item.unitCode_dictText || '-'}</td>
<td style="text-align:right;padding:6px 8px;border:1px solid #ddd;">${item.totalPrice != null ? '¥' + Number(item.totalPrice).toFixed(2) : '-'}</td>
</tr>`;
});
}
// 构建 descJson 字段行(与详情弹窗展示的字段一致)
const fieldKeys = ['targetDepartment', 'urgencyLevel', 'expectedExaminationTime', 'allergyHistory', 'examinationPurpose', 'medicalHistorySummary', 'symptom', 'sign', 'clinicalDiagnosis', 'otherDiagnosis', 'relatedResult', 'attention'];
let descFieldsHtml = '';
fieldKeys.forEach((key) => {
const label = labelMap[key] || key;
const value = transformField(key, descData[key]);
if (value != null && value !== '') {
descFieldsHtml += `
<div class="info-row">
<span class="label">${label}</span>
<span class="value">${value}</span>
</div>`;
}
});
// 构建完整打印HTML
const printContent = `
<div class="print-wrapper">
<!-- 标题 -->
<div class="print-header">
<div class="print-title">检查申请单</div>
<div class="print-meta">打印时间:${new Date().toLocaleString()}</div>
</div>
<!-- 基本信息 -->
<div class="print-section">
<div class="section-title">基本信息</div>
<div class="info-grid">
<div class="info-row"><span class="label">患者姓名:</span><span class="value">${row.patientName || '-'}</span></div>
<div class="info-row"><span class="label">申请单名称:</span><span class="value">${row.name || '-'}</span></div>
<div class="info-row"><span class="label">申请单状态:</span><span class="value">${parseStatus(row.status)}</span></div>
<div class="info-row"><span class="label">创建时间:</span><span class="value">${row.createTime || '-'}</span></div>
<div class="info-row"><span class="label">申请单号:</span><span class="value">${row.prescriptionNo || '-'}</span></div>
<div class="info-row"><span class="label">申请者:</span><span class="value">${row.requesterId_dictText || '-'}</span></div>
<div class="info-row"><span class="label">就诊ID</span><span class="value">${row.encounterId || '-'}</span></div>
<div class="info-row"><span class="label">申请单ID</span><span class="value">${row.requestFormId || '-'}</span></div>
</div>
</div>
${descFieldsHtml ? `
<!-- 申请单描述 -->
<div class="print-section">
<div class="section-title">申请单描述</div>
${descFieldsHtml}
</div>` : ''}
${detailRowsHtml ? `
<!-- 诊疗项目 -->
<div class="print-section">
<div class="section-title">诊疗项目</div>
<table class="detail-table">
<thead>
<tr>
<th style="width:50px;padding:8px;border:1px solid #ddd;background:#f5f7fa;text-align:center;">序号</th>
<th style="padding:8px;border:1px solid #ddd;background:#f5f7fa;text-align:left;">医嘱名称</th>
<th style="width:60px;padding:8px;border:1px solid #ddd;background:#f5f7fa;text-align:center;">数量</th>
<th style="width:60px;padding:8px;border:1px solid #ddd;background:#f5f7fa;text-align:center;">单位</th>
<th style="width:80px;padding:8px;border:1px solid #ddd;background:#f5f7fa;text-align:right;">总价</th>
</tr>
</thead>
<tbody>${detailRowsHtml}</tbody>
</table>
</div>` : ''}
<!-- 条码区 -->
<div class="barcode-section">
<div class="barcode-container">
<div class="barcode-number">${row.prescriptionNo || ''}</div>
<div class="barcode-label">申请单号 / 扫码核验</div>
</div>
</div>
<div class="print-footer">
<div class="footer-line">本申请单仅供院内使用,请勿外传</div>
</div>
</div>
`;
// 打开新窗口打印
const printWindow = window.open('', '_blank');
if (!printWindow) {
proxy.$modal?.msgError?.('无法打开打印窗口,请检查浏览器弹窗设置');
return;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>检查申请单 - 打印</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
font-size: 12px;
color: #333;
padding: 20px;
}
.print-wrapper {
max-width: 210mm;
margin: 0 auto;
}
.print-header {
text-align: center;
padding-bottom: 12px;
margin-bottom: 16px;
border-bottom: 2px solid #333;
}
.print-title {
font-size: 20px;
font-weight: bold;
letter-spacing: 4px;
margin-bottom: 6px;
}
.print-meta {
font-size: 11px;
color: #666;
}
.print-section {
margin-bottom: 16px;
}
.section-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
padding-bottom: 4px;
border-bottom: 1px solid #ddd;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
}
.info-row {
font-size: 12px;
line-height: 1.8;
}
.info-row .label {
font-weight: 600;
color: #555;
}
.info-row .value {
color: #333;
}
.detail-table {
width: 100%;
border-collapse: collapse;
margin-top: 6px;
}
.detail-table th {
font-size: 11px;
font-weight: 600;
color: #555;
}
.detail-table td {
font-size: 12px;
}
.barcode-section {
margin-top: 24px;
padding-top: 16px;
border-top: 1px dashed #ccc;
text-align: center;
}
.barcode-container {
display: inline-block;
padding: 12px 24px;
border: 2px solid #333;
border-radius: 6px;
background: #fff;
}
.barcode-number {
font-family: 'Libre Barcode 128', 'Libre Barcode 39', 'Code128', 'C', monospace;
font-size: 42px;
letter-spacing: 4px;
color: #000;
line-height: 1.2;
}
.barcode-label {
font-size: 10px;
color: #888;
margin-top: 4px;
letter-spacing: 2px;
}
.print-footer {
margin-top: 20px;
text-align: center;
}
.footer-line {
font-size: 10px;
color: #aaa;
}
@media print {
body { padding: 0; }
.print-wrapper { max-width: none; }
.barcode-section { page-break-inside: avoid; }
}
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
printWindow.document.close();
// 等待内容渲染后打印
printWindow.onload = function() {
setTimeout(() => {
printWindow.print();
proxy.$modal?.closeAll?.();
}, 300);
};
} catch (error) {
console.error('打印失败:', error);
proxy.$modal?.msgError?.('打印失败: ' + (error.message || '未知错误'));
}
};
/**
* 查看报告 - 仅已出报告状态可用
* 调用报告查询接口弹窗展示结构化报告或影像预览PACS链接
*/
const handleViewReport = async (row) => {
reportRow.value = row;
reportDialogVisible.value = true;
reportLoading.value = true;
reportData.value = null;
try {
const res = await getTestResult({ prescriptionNo: row.prescriptionNo });
if (res.code === 200) {
if (res.data) {
// 支持两种返回格式:
// 1. res.data 为对象(结构化报告):含 patientName, prescriptionNo, reportTime, diagnosis/content, imageUrl/pacsUrl
// 2. res.data 为字符串报告URL映射到 imageUrl 以支持iframe预览
if (typeof res.data === 'string') {
reportData.value = {
prescriptionNo: row.prescriptionNo,
imageUrl: res.data,
};
} else if (typeof res.data === 'object') {
reportData.value = {
...res.data,
prescriptionNo: res.data.prescriptionNo || row.prescriptionNo,
};
}
} else {
reportData.value = null;
}
} else {
reportData.value = null;
proxy.$modal?.msgWarning?.(res.msg || '暂未生成报告');
}
} catch (e) {
console.error('获取报告失败:', e);
reportData.value = null;
proxy.$modal?.msgError?.('获取报告失败');
} finally {
reportLoading.value = false;
}
};
/**
* 打开PACS影像链接
*/
const openPacsLink = () => {
const url = reportData.value?.pacsUrl || reportData.value?.imageUrl;
if (url) {
window.open(url, '_blank');
}
};
// ========== 修改申请单相关 ==========
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editingRow = ref(null);
// 修改申请单 - 复用检查申请单弹窗
const handleEdit = (row) => {
editingRow.value = { ...row };
editDialogVisible.value = true;
// 弹窗打开后手动调用getList确保数据加载
nextTick(() => {
editFormRef.value?.getList?.();
editFormRef.value?.getLocationInfo?.();
editFormRef.value?.getDiagnosisList?.();
});
};
// 编辑弹窗确认提交
const handleEditSubmit = () => {
// 调用MedicalExaminations组件的submit方法
if (editFormRef.value?.submit) {
editFormRef.value.submit();
}
};
watch( watch(
() => patientInfo.value?.encounterId, () => patientInfo.value?.encounterId,
(val) => { (val) => {
@@ -355,8 +982,9 @@ watch(
getLocationInfo(); getLocationInfo();
} else { } else {
tableData.value = []; tableData.value = [];
filterForm.value.dateRange = []; filterForm.value.dateRange = getDefaultDateRange();
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
} }
}, },
{ immediate: true } { immediate: true }
@@ -470,4 +1098,96 @@ defineExpose({
overflow: auto; overflow: auto;
} }
} }
// 报告弹窗样式
.report-viewer {
min-height: 200px;
padding: 8px 0;
}
.report-viewer-container {
.report-content-section {
margin-top: 16px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #eee;
.section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
display: flex;
align-items: center;
}
.report-content-text {
font-size: 13px;
line-height: 1.8;
color: #606266;
white-space: pre-wrap;
}
}
.report-image-section {
margin-top: 16px;
.section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #303133;
display: flex;
align-items: center;
}
.report-iframe {
width: 100%;
height: 480px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
}
}
// 状态标签样式 - 参考临床医嘱
:deep(.el-tag) {
border-radius: 2px;
padding: 0 8px;
height: 24px;
line-height: 22px;
font-size: 12px;
border-width: 1px;
}
:deep(.el-tag--info.is-plain) {
background: #f4f4f5;
border-color: #e9e9eb;
color: #909399;
}
:deep(.el-tag--primary.is-plain) {
background: #ecf5ff;
border-color: #d9ecff;
color: #409eff;
}
:deep(.el-tag--success.is-plain) {
background: #f0f9eb;
border-color: #e1f3d8;
color: #67c23a;
}
:deep(.el-tag--warning.is-plain) {
background: #fdf6ec;
border-color: #faecd8;
color: #e6a23c;
}
:deep(.el-tag--danger.is-plain) {
background: #fef0f0;
border-color: #fde2e2;
color: #f56c6c;
}
</style> </style>

View File

@@ -82,7 +82,11 @@
</template> </template>
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" /> <el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" /> <el-table-column label="申请单名称" width="140">
<template #default="scope">
<span>{{ buildApplicationName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" /> <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 label="单据状态" width="100" align="center"> <el-table-column label="单据状态" width="100" align="center">
@@ -103,11 +107,11 @@
<el-table-column prop="requesterId_dictText" label="申请者" width="120" /> <el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="160"> <el-table-column label="操作" align="center" fixed="right" width="160">
<template #default="scope"> <template #default="scope">
<template v-if="scope.row.billStatus == 0 || scope.row.status == 0"> <template v-if="scope.row.status == 0">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button> <el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button> <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template> </template>
<template v-else-if="scope.row.billStatus == 1 || scope.row.status == 1"> <template v-else-if="scope.row.status == 1">
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button> <el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
</template> </template>
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button> <el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
@@ -132,12 +136,12 @@
currentDetail.patientName || '-' currentDetail.patientName || '-'
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="申请单名称">{{ <el-descriptions-item label="申请单名称">{{
currentDetail.name || '-' buildApplicationName(currentDetail)
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ <el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-' currentDetail.createTime || '-'
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="处方号">{{ <el-descriptions-item label="申请单号">{{
currentDetail.prescriptionNo || '-' currentDetail.prescriptionNo || '-'
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="申请者">{{ <el-descriptions-item label="申请者">{{
@@ -179,6 +183,26 @@
<el-button @click="detailDialogVisible = false">关闭</el-button> <el-button @click="detailDialogVisible = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 编辑检验申请单弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑检验申请单"
width="1200px"
destroy-on-close
top="5vh"
:close-on-click-modal="false"
>
<LaboratoryTests
ref="editFormRef"
@submitOk="handleEditSubmitOk"
:editData="editRowData"
/>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditForm">确认</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -187,13 +211,18 @@ import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue'; import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js'; import {patientInfo} from '../../store/patient.js';
import {getInspection, deleteRequestForm, withdrawRequestForm} from './api'; import {getInspection, deleteRequestForm, withdrawRequestForm} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js'; import {getDepartmentList} from '@/api/public.js';
import LaboratoryTests from '../order/applicationForm/laboratoryTests.vue';
import {saveInspection} from '../order/applicationForm/api.js';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const tableData = ref([]); const tableData = ref([]);
const loading = ref(false); const loading = ref(false);
const detailDialogVisible = ref(false); const detailDialogVisible = ref(false);
const editDialogVisible = ref(false);
const editRowData = ref(null);
const editFormRef = ref(null);
const currentDetail = ref(null); const currentDetail = ref(null);
const descJsonData = ref(null); const descJsonData = ref(null);
const orgOptions = ref([]); const orgOptions = ref([]);
@@ -285,6 +314,9 @@ const labelMap = {
otherDiagnosis: '其他诊断', otherDiagnosis: '其他诊断',
relatedResult: '相关结果', relatedResult: '相关结果',
attention: '注意事项', attention: '注意事项',
applicationType: '申请类型',
specimenName: '标本类型',
executeTime: '执行时间',
}; };
/** /**
@@ -311,8 +343,8 @@ const parsePriorityCode = (descJson) => {
if (!descJson) return '-'; if (!descJson) return '-';
try { try {
const obj = JSON.parse(descJson); const obj = JSON.parse(descJson);
// priorityCode: 0-普通, 1-急 // applicationType: 0-普通, 1-急
return obj.priorityCode === 1 ? '急' : '普通'; return obj.applicationType === 1 ? '急' : '普通';
} catch (e) { } catch (e) {
console.error('解析 descJson 失败:', e); console.error('解析 descJson 失败:', e);
return '-'; return '-';
@@ -336,6 +368,24 @@ const parseSpecimenType = (descJson) => {
} }
}; };
/**
* 根据申请单详情构建申请单名称
* 单一项目:显示项目名称+数量
* 多个项目:显示首个项目名称+数量+"等X项"
*/
const buildApplicationName = (row) => {
const details = row.requestFormDetailList;
if (!details || details.length === 0) {
return row.name || '-';
}
if (details.length === 1) {
const item = details[0];
return `${item.adviceName}${item.quantity || ''}`;
}
const first = details[0];
return `${first.adviceName}${first.quantity || ''}${details.length}`;
};
const isFieldMatched = (key) => { const isFieldMatched = (key) => {
return key in labelMap; return key in labelMap;
}; };
@@ -391,6 +441,9 @@ const handleViewDetail = async (row) => {
try { try {
const obj = JSON.parse(row.descJson); const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment); obj.targetDepartment = recursionFun(obj.targetDepartment);
// 转换申请类型编码为可读文本
if (obj.applicationType === 0) obj.applicationType = '普通';
else if (obj.applicationType === 1) obj.applicationType = '急诊';
descJsonData.value = obj; descJsonData.value = obj;
} catch (e) { } catch (e) {
console.error('解析 descJson 失败:', e); console.error('解析 descJson 失败:', e);
@@ -405,10 +458,32 @@ const handleViewDetail = async (row) => {
/** /**
* 修改检验申请单(待签发状态) * 修改检验申请单(待签发状态)
*/ */
const handleEdit = (row) => { const handleEdit = async (row) => {
// 复用详情查看逻辑,后续可扩展为打开编辑弹窗 // 确保科室数据已加载
handleViewDetail(row); if (!orgOptions.value || orgOptions.value.length === 0) {
proxy.$modal?.msgInfo?.('修改功能待接入,请通过详情弹窗查看后重新开立'); await getLocationInfo();
}
editRowData.value = row;
editDialogVisible.value = true;
};
/**
* 编辑弹窗提交成功回调
*/
const handleEditSubmitOk = async () => {
editDialogVisible.value = false;
editRowData.value = null;
proxy.$modal?.msgSuccess?.('修改成功');
await fetchData();
};
/**
* 编辑弹窗提交按钮
*/
const submitEditForm = () => {
if (editFormRef.value?.submit) {
editFormRef.value.submit();
}
}; };
/** /**
@@ -422,7 +497,7 @@ const handleDelete = async (row) => {
} }
try { try {
const res = await deleteRequestForm({ prescriptionNo: row.prescriptionNo }); const res = await deleteRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) { if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('删除成功'); proxy.$modal?.msgSuccess?.('删除成功');
await fetchData(); await fetchData();
@@ -445,7 +520,7 @@ const handleWithdraw = async (row) => {
} }
try { try {
const res = await withdrawRequestForm({ prescriptionNo: row.prescriptionNo }); const res = await withdrawRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) { if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('撤回成功'); proxy.$modal?.msgSuccess?.('撤回成功');
await fetchData(); await fetchData();

View File

@@ -289,19 +289,19 @@ function getList() {
return obj; return obj;
}); });
form.value.diagnosisList = datas; form.value.diagnosisList = datas;
// form.value.diagnosisList = res.data; // 去重:按 conditionId 去重,防止后端重复插入导致重复记录
deduplicateDiagnosisList();
emits('diagnosisSave', false); emits('diagnosisSave', false);
} }
}); });
getTcmDiagnosis({ encounterId: props.patientInfo.encounterId }).then((res) => { getTcmDiagnosis({ encounterId: props.patientInfo.encounterId }).then((res) => {
console.log('getTcmDiagnosis=======>', JSON.stringify(res.data.illness)); console.log('getTcmDiagnosis=======>', JSON.stringify(res.data?.illness));
if (res.code == 200) { if (res.code == 200 && res.data?.illness?.length > 0) {
if (res.data.illness.length > 0) {
diagnosisNetDatas.value = res.data.illness; diagnosisNetDatas.value = res.data.illness;
res.data.illness.forEach((item, index) => { res.data.illness.forEach((item, index) => {
newList.push({ newList.push({
name: item.name + '-' + (res.data.symptom[index]?.name || ''), name: item.name + '-' + (res.data.symptom?.[index]?.name || ''),
ybNo: item.ybNo, ybNo: item.ybNo,
medTypeCode: item.medTypeCode, medTypeCode: item.medTypeCode,
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name, diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
@@ -318,14 +318,33 @@ function getList() {
const bNo = typeof b.diagSrtNo === 'number' ? b.diagSrtNo : 9999; const bNo = typeof b.diagSrtNo === 'number' ? b.diagSrtNo : 9999;
return aNo - bNo; return aNo - bNo;
}); });
// TCM 数据添加后也去重
deduplicateDiagnosisList();
} }
emits('diagnosisSave', false); emits('diagnosisSave', false);
}
}); });
getTree(); getTree();
} }
/**
* 诊断列表去重:按 ybNo + name 组合去重,保留第一条记录
* 防止后端 saveOrUpdate 在删除后误 INSERT 导致重复
*/
function deduplicateDiagnosisList() {
const seen = new Set();
const dedupedList = [];
for (const item of form.value.diagnosisList) {
// 使用 ybNo 和 name 组合作为唯一标识(中医诊断没有 ybNo用 name 去重)
const key = item.ybNo ? `${item.ybNo}` : `name_${item.name}`;
if (!seen.has(key)) {
seen.add(key);
dedupedList.push(item);
}
}
form.value.diagnosisList = dedupedList;
}
init(); init();
function init() { function init() {
diagnosisInit().then((res) => { diagnosisInit().then((res) => {
@@ -568,6 +587,11 @@ function handleMaindise(value, index) {
* 保存诊断 * 保存诊断
*/ */
function handleSaveDiagnosis() { function handleSaveDiagnosis() {
// 防止重复点击保存
if (isSaving.value) {
return;
}
for (let index = 0; index < (form.value.diagnosisList || []).length; index++) { for (let index = 0; index < (form.value.diagnosisList || []).length; index++) {
const item = form.value.diagnosisList[index]; const item = form.value.diagnosisList[index];
if (!item.diagSrtNo) { if (!item.diagSrtNo) {
@@ -598,9 +622,21 @@ function handleSaveDiagnosis() {
return aNo - bNo; return aNo - bNo;
}); });
// 步骤1.5:确保每条诊断都有诊断医生和诊断时间(元数据补全)
const doctorName = props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name;
const now = new Date().toLocaleString('zh-CN');
sortedList.forEach((item) => {
if (!item.diagnosisDoctor) {
item.diagnosisDoctor = doctorName;
}
if (!item.diagnosisTime) {
item.diagnosisTime = now;
}
});
// 步骤2重新分配连续的序号从1开始 // 步骤2重新分配连续的序号从1开始
sortedList.forEach((item, index) => { sortedList.forEach((item, index) => {
item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序 item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序
}); });
// 步骤3提交排序后的数据 // 步骤3提交排序后的数据
@@ -610,12 +646,12 @@ function handleSaveDiagnosis() {
diagnosisChildList: sortedList, diagnosisChildList: sortedList,
}).then((res) => { }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
// 步骤4更新本地数据使用全新对象防止响应式问题
form.value.diagnosisList = sortedList.map(item => ({ ...item }));
emits('diagnosisSave', false); emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存'); proxy.$modal.msgSuccess('诊断已保存');
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 食源性疾病逻辑 // 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => { isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) { if (res2.code === 20 && res2.data) {

View File

@@ -249,7 +249,7 @@ const submit = () => {
requestFormId: '', // 申请单ID requestFormId: '', // 申请单ID
name: '输血申请单', name: '输血申请单',
descJson: JSON.stringify(form), descJson: JSON.stringify(form),
categoryEnum: '3', // 1 检验 2 检查 3 输血 4 手术 categoryEnum: '23', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
}).then((res) => { }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$message.success(res.msg); proxy.$message.success(res.msg);

View File

@@ -6,9 +6,26 @@
<template> <template>
<div class="LaboratoryTests-container"> <div class="LaboratoryTests-container">
<div v-loading="loading" class="transfer-wrapper"> <div v-loading="loading" class="transfer-wrapper">
<!-- 远程搜索框 -->
<div class="search-bar">
<el-input
v-model="searchKey"
placeholder="输入项目代码/名称搜索"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
style="width: 300px; margin-bottom: 10px"
>
<template #append>
<el-button @click="handleSearch">搜索</el-button>
</template>
</el-input>
<span v-if="!searchKey" class="total-count"> {{ totalCount }} </span>
<span v-else class="total-count">搜索到 {{ filteredCount }} / {{ totalCount }} </span>
</div>
<el-transfer <el-transfer
v-model="transferValue" v-model="transferValue"
:data="applicationList" :data="transferData"
filter-placeholder="项目代码/名称" filter-placeholder="项目代码/名称"
filterable filterable
:titles="['未选择', '已选择']" :titles="['未选择', '已选择']"
@@ -117,7 +134,7 @@
</div> </div>
</template> </template>
<script setup name="LaboratoryTests"> <script setup name="LaboratoryTests">
import {getCurrentInstance, onBeforeMount, onMounted, reactive, watch} from 'vue'; import {getCurrentInstance, onMounted, reactive, ref, watch, computed} from 'vue';
import {patientInfo} from '../../../store/patient.js'; import {patientInfo} from '../../../store/patient.js';
import {getApplicationList, saveInspection} from './api'; import {getApplicationList, saveInspection} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js'; import {getOrgList} from '@/views/doctorstation/components/api.js';
@@ -138,29 +155,23 @@ const findTreeItem = (list, id) => {
return null; return null;
}; };
const emits = defineEmits(['submitOk']); const emits = defineEmits(['submitOk']);
const props = defineProps({}); const props = defineProps({
editData: {
type: Object,
default: null,
},
});
const isEditMode = computed(() => !!props.editData?.requestFormId);
const state = reactive({}); const state = reactive({});
const applicationListAll = ref(); const applicationListAll = ref([]);
const applicationList = ref();
const loading = ref(false); const loading = ref(false);
const orgOptions = ref([]); // 科室选项 const orgOptions = ref([]);
const getList = () => { const searchKey = ref('');
if (!patientInfo.value?.inHospitalOrgId) { const totalCount = ref(0);
applicationList.value = [];
return; // 将已加载的全部数据转为 transfer 组件所需的格式
} const buildTransferData = (records) => {
loading.value = true; return records.map((item) => {
getApplicationList({
pageSize: 9999,
pageNum: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], //1 药品 2耗材 3诊疗
})
.then((res) => {
if (res.code === 200) {
applicationListAll.value = res.data.records;
applicationList.value = res.data.records.map((item) => {
const priceInfo = item.priceList?.[0] || {}; const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00'; const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || ''; const unit = item.unitCode_dictText || item.unitCode || '';
@@ -171,15 +182,65 @@ const getList = () => {
key: item.adviceDefinitionId, key: item.adviceDefinitionId,
}; };
}); });
console.log('applicationList========>', JSON.stringify(res.data.records)); };
} else {
proxy.$message.error(res.message); // 加载全部数据(不分页,一次性拉取)
applicationList.value = []; const loadAllData = async () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationListAll.value = [];
return;
} }
}) loading.value = true;
.finally(() => { try {
loading.value = false; // 使用大 pageSize 一次性拉取所有启用状态的检验类诊疗项目
const res = await getApplicationList({
pageSize: 9999,
pageNo: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
}); });
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationListAll.value = [];
return;
}
applicationListAll.value = res.data?.records || [];
totalCount.value = res.data?.total || 0;
} catch (e) {
proxy.$message.error('获取检验项目列表失败');
applicationListAll.value = [];
} finally {
loading.value = false;
}
};
// 根据搜索关键词过滤数据
const filterData = (key) => {
if (!key || key.trim() === '') {
return applicationListAll.value;
}
const lowerKey = key.toLowerCase().trim();
return applicationListAll.value.filter((item) => {
return (
item.adviceName?.toLowerCase().includes(lowerKey) ||
item.pyStr?.toLowerCase().includes(lowerKey) ||
item.adviceBusNo?.toLowerCase().includes(lowerKey)
);
});
};
// transfer 组件实际显示的数据(受搜索词影响)
const transferData = computed(() => buildTransferData(filterData(searchKey.value)));
// 当前显示的条数
const filteredCount = computed(() => filterData(searchKey.value).length);
const getList = async () => {
await loadAllData();
};
const handleSearch = () => {
// 搜索时保持已选中的项目不受影响
}; };
const transferValue = ref([]); const transferValue = ref([]);
const form = reactive({ const form = reactive({
@@ -198,7 +259,6 @@ const form = reactive({
otherDiagnosisList: [], //其他断目录 otherDiagnosisList: [], //其他断目录
}); });
const rules = reactive({}); const rules = reactive({});
onBeforeMount(() => {});
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
@@ -211,12 +271,22 @@ const projectWithDepartment = (selectProjectIds, type) => {
let isRelease = true; let isRelease = true;
// 选中项目的数组 // 选中项目的数组
const arr = []; const arr = [];
// 根据选中的项目id查找对应的项目 // 根据选中的项目id查找对应的项目(从全部原始数据中查找)
selectProjectIds.forEach((element) => { selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => { const searchData = applicationListAll.value.find((item) => {
return element == item.adviceDefinitionId; return element == item.adviceDefinitionId;
}); });
arr.push(searchData); if (searchData) {
const priceInfo = searchData.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = searchData.unitCode_dictText || searchData.unitCode || '';
arr.push({
adviceDefinitionId: searchData.adviceDefinitionId,
orgId: searchData.orgId,
label: searchData.adviceName + ' (¥' + price + '/' + unit + ')',
key: searchData.adviceDefinitionId,
});
}
}); });
// 保存用户手动选择的发往科室(提交时需要保留) // 保存用户手动选择的发往科室(提交时需要保留)
const manualDept = type === 2 ? form.targetDepartment : ''; const manualDept = type === 2 ? form.targetDepartment : '';
@@ -242,18 +312,27 @@ const projectWithDepartment = (selectProjectIds, type) => {
if (type === 2 && manualDept) { if (type === 2 && manualDept) {
form.targetDepartment = manualDept; form.targetDepartment = manualDept;
isRelease = true; isRelease = true;
} else { } else if (type === 2 && !manualDept) {
// 提交时用户未手动选择科室,才提示错误
isRelease = false; isRelease = false;
ElMessage({ ElMessage({
type: 'error', type: 'error',
message: '未找到项目执行的科室', message: '未找到项目执行的科室',
}); });
} else {
// type=1(选择项目变化)时,不弹窗,仅清空科室让用户自行选择
isRelease = false;
} }
} }
if (findItem && isRelease) { if (findItem && isRelease) {
// 提交时若用户已选「发往科室」,不得用项目默认执行科室覆盖
if (type === 2 && manualDept) {
form.targetDepartment = manualDept;
} else {
form.targetDepartment = findItem.id; form.targetDepartment = findItem.id;
} }
} }
}
return isRelease; return isRelease;
}; };
// 监听选择项目变化 // 监听选择项目变化
@@ -263,6 +342,44 @@ watch(
projectWithDepartment(newValue, 1); projectWithDepartment(newValue, 1);
} }
); );
// 编辑模式下,回显已有数据
watch(
() => props.editData,
(newData) => {
if (!newData || !newData.requestFormId) return;
// 解析 descJson 回填表单
if (newData.descJson) {
try {
const obj = JSON.parse(newData.descJson);
Object.keys(form).forEach((key) => {
if (obj[key] !== undefined) {
form[key] = obj[key];
}
});
} catch (e) {
console.error('解析 descJson 失败:', e);
}
}
// 回填已选项目
if (newData.requestFormDetailList && newData.requestFormDetailList.length > 0) {
// 从全部数据中匹配已选项目
const selectedIds = [];
newData.requestFormDetailList.forEach((detail) => {
const matched = applicationListAll.value.find(
(item) => item.adviceName === detail.adviceName
);
if (matched) {
selectedIds.push(matched.adviceDefinitionId);
}
});
transferValue.value = selectedIds;
}
},
{ immediate: true }
);
const submit = () => { const submit = () => {
if (transferValue.value.length == 0) { if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单'); return proxy.$message.error('请选择申请单');
@@ -280,7 +397,7 @@ const submit = () => {
unitCode: item.priceList[0].unitCode /** 请求单位编码 */, unitCode: item.priceList[0].unitCode /** 请求单位编码 */,
unitPrice: item.priceList[0].price /** 单价 */, unitPrice: item.priceList[0].price /** 单价 */,
totalPrice: item.priceList[0].price /** 总价 */, totalPrice: item.priceList[0].price /** 总价 */,
positionId: item.positionId || form.targetDepartment, //执行科室id未配置时使用用户手动选择的科室 positionId: form.targetDepartment || item.positionId, // 用户指定发往科室优先于项目默认执行科室
ybClassEnum: item.ybClassEnum, //类别医保编码 ybClassEnum: item.ybClassEnum, //类别医保编码
conditionId: item.conditionId, //诊断ID conditionId: item.conditionId, //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id
@@ -295,15 +412,15 @@ const submit = () => {
patientId: patientInfo.value.patientId, //患者ID patientId: patientInfo.value.patientId, //患者ID
encounterId: patientInfo.value.encounterId, // 就诊ID encounterId: patientInfo.value.encounterId, // 就诊ID
organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID
requestFormId: '', // 申请单ID requestFormId: isEditMode.value ? props.editData.requestFormId : '', // 申请单ID(编辑模式传入,新增为空)
name: '检验申请单', name: '检验申请单',
descJson: JSON.stringify(form), descJson: JSON.stringify(form),
categoryEnum: '1', // 1 检验 2 检查 3 输血 4 手术 categoryEnum: '21', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
}; };
saveInspection(params).then((res) => { saveInspection(params).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$message.success(res.msg); proxy.$message.success(isEditMode.value ? '修改成功' : res.msg);
applicationList.value = []; transferValue.value = [];
emits('submitOk'); emits('submitOk');
} else { } else {
proxy.$message.error(res.message); proxy.$message.error(res.message);
@@ -360,6 +477,19 @@ defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList });
.transfer-wrapper { .transfer-wrapper {
position: relative; position: relative;
min-height: 300px; min-height: 300px;
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.total-count {
font-size: 13px;
color: #909399;
white-space: nowrap;
}
}
} }
.el-transfer { .el-transfer {

View File

@@ -5,36 +5,25 @@
--> -->
<template> <template>
<div class="medicalExaminations-container"> <div class="medicalExaminations-container">
<!-- 顶部标题栏 --> <!-- 主体内容 -->
<div class="form-header"> <div class="form-body">
<div class="header-left"> <!-- 右上角紧急程度 -->
<el-icon class="header-icon"><Files /></el-icon> <div class="urgency-bar">
<span class="header-title">检查申请单</span> <span class="urgency-bar-label">紧急程度</span>
</div> <el-radio-group v-model="form.urgencyLevel" @change="handleUrgencyChange" size="small">
<div class="header-right">
<span class="urgency-label">紧急程度</span>
<el-radio-group v-model="form.urgencyLevel" @change="handleUrgencyChange" class="urgency-radio-group">
<el-radio-button label="routine">普通</el-radio-button> <el-radio-button label="routine">普通</el-radio-button>
<el-radio-button label="emergency">急诊</el-radio-button> <el-radio-button label="emergency">急诊</el-radio-button>
</el-radio-group> </el-radio-group>
<transition name="el-fade-in-linear"> <transition name="el-fade-in-linear">
<span v-if="form.urgencyLevel === 'emergency'" class="emergency-tip"> <span v-if="form.urgencyLevel === 'emergency'" class="emergency-tip-inline">
<el-icon><WarningFilled /></el-icon> <el-icon><WarningFilled /></el-icon>
急诊单将进入绿色通道 绿色通道
</span> </span>
</transition> </transition>
</div> </div>
</div>
<!-- 主体内容区 -->
<div class="form-body">
<!-- 选择检查项目 --> <!-- 选择检查项目 -->
<div class="section-card"> <div class="section-card">
<div class="section-header"> <div class="transfer-wrapper">
<el-icon><Document /></el-icon>
<span>选择检查项目</span>
</div>
<div v-loading="loading" class="transfer-wrapper">
<el-transfer <el-transfer
v-model="transferValue" v-model="transferValue"
:data="applicationList" :data="applicationList"
@@ -45,17 +34,10 @@
</div> </div>
</div> </div>
<!-- 申请信息 -->
<div class="section-card">
<div class="section-header">
<el-icon><EditPen /></el-icon>
<span>申请信息</span>
</div>
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="info-form"> <el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="info-form">
<!-- 第一行发往科室 + 期望检查时间 --> <!-- 第一行发往科室 + 紧急程度 + 期望检查时间 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="8">
<el-form-item label="发往科室" prop="targetDepartment"> <el-form-item label="发往科室" prop="targetDepartment">
<el-tree-select <el-tree-select
clearable clearable
@@ -127,45 +109,11 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form>
<!-- 过敏史卡片 --> <!-- 第五行检查目的 + 病史摘要 -->
<div class="section-card allergy-card"> <el-row :gutter="16">
<div class="section-header"> <el-col :span="12">
<el-icon><Warning /></el-icon> <el-form-item label="检查目的" prop="examinationPurpose">
<span>过敏史</span>
<span v-if="form.allergyHistory" class="header-count">{{ form.allergyHistory.length }}</span>
</div>
<div class="allergy-content">
<div class="allergy-input-row">
<el-input
v-model="form.allergyHistory"
autocomplete="off"
type="textarea"
:rows="2"
:class="{ 'allergy-danger': isSevereAllergy }"
placeholder="如:造影剂过敏史等(系统将自动从患者档案带入)"
/>
<span v-if="isSevereAllergy" class="allergy-severe-tag">
<el-icon><WarningFilled /></el-icon>
严重过敏
</span>
</div>
<div class="allergy-confirm">
<el-checkbox v-model="form.allergyConfirmed" size="small">
已通过口头询问确认无过敏史
</el-checkbox>
</div>
</div>
</div>
<!-- 检查目的卡片 -->
<div class="section-card purpose-card">
<div class="section-header">
<el-icon><Aim /></el-icon>
<span>检查目的</span>
<span class="required-mark">必填</span>
</div>
<el-input <el-input
v-model="form.examinationPurpose" v-model="form.examinationPurpose"
autocomplete="off" autocomplete="off"
@@ -175,35 +123,61 @@
show-word-limit show-word-limit
placeholder="请输入检查目的,如:明确诊断、术后复查、疗效评估等" placeholder="请输入检查目的,如:明确诊断、术后复查、疗效评估等"
/> />
</div> </el-form-item>
</el-col>
<!-- 病史摘要卡片 --> <el-col :span="12">
<div class="section-card history-card"> <el-form-item label="病史摘要" prop="medicalHistorySummary">
<div class="section-header"> <div class="history-field-wrapper">
<el-icon><DocumentCopy /></el-icon>
<span>病史摘要</span>
<span class="required-mark">必填</span>
<el-button
type="primary"
plain
size="small"
class="sync-btn"
@click="handleSyncHistory"
:loading="syncingHistory"
>
<el-icon><Refresh /></el-icon>
同步现病史/体征
</el-button>
</div>
<el-input <el-input
v-model="form.medicalHistorySummary" v-model="form.medicalHistorySummary"
autocomplete="off" autocomplete="off"
type="textarea" type="textarea"
:rows="3" :rows="2"
placeholder="请简要描述患者病史摘要" placeholder="请简要描述患者病史摘要"
/> />
<el-button
type="primary"
plain
size="small"
class="history-sync-btn"
@click="handleSyncHistory"
:loading="syncingHistory"
>
<el-icon><Refresh /></el-icon>
同步
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 第六行过敏史 -->
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="过敏史">
<div class="allergy-wrapper">
<el-input
v-model="form.allergyHistory"
autocomplete="off"
type="textarea"
:rows="1"
:class="{ 'allergy-danger': isSevereAllergy }"
placeholder="如:造影剂过敏史等(系统将自动从患者档案带入)"
/>
<div class="allergy-actions">
<span v-if="isSevereAllergy" class="allergy-severe-tag-inline">
<el-icon><WarningFilled /></el-icon>
严重过敏
</span>
<el-checkbox v-model="form.allergyConfirmed" size="small">
已通过口头询问确认无过敏史
</el-checkbox>
</div> </div>
</div> </div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div> </div>
<!-- 急诊确认弹窗 --> <!-- 急诊确认弹窗 -->
@@ -227,7 +201,8 @@
</template> </template>
<script setup name="MedicalExaminations"> <script setup name="MedicalExaminations">
import {getCurrentInstance, onMounted, reactive, ref, watch, computed} from 'vue'; import {getCurrentInstance, onMounted, reactive, ref, watch, computed, nextTick} from 'vue';
import dayjs from 'dayjs';
import {patientInfo} from '../../../store/patient.js'; import {patientInfo} from '../../../store/patient.js';
import {getDepartmentList} from '@/api/public.js'; import {getDepartmentList} from '@/api/public.js';
import {getEncounterDiagnosis} from '../../api.js'; import {getEncounterDiagnosis} from '../../api.js';
@@ -251,7 +226,26 @@ const findTreeItem = (list, id) => {
}; };
const emits = defineEmits(['submitOk']); const emits = defineEmits(['submitOk']);
const props = defineProps({}); const props = defineProps({
isEditMode: {
type: Boolean,
default: false,
},
editData: {
type: Object,
default: () => ({}),
},
// 支持通过props传入patientInfo
externalPatientInfo: {
type: Object,
default: null,
},
});
// 优先使用外部传入的patientInfo否则使用store中的
const effectivePatientInfo = computed(() => {
return props.externalPatientInfo || patientInfo.value;
});
const orgOptions = ref([]); const orgOptions = ref([]);
const state = reactive({}); const state = reactive({});
const applicationListAll = ref(); const applicationListAll = ref();
@@ -272,7 +266,9 @@ const isSevereAllergy = computed(() => {
}); });
const getList = () => { const getList = () => {
if (!patientInfo.value?.inHospitalOrgId) { console.log('getList called, effectivePatientInfo:', effectivePatientInfo.value);
if (!effectivePatientInfo.value?.inHospitalOrgId) {
console.log('inHospitalOrgId is missing, setting empty list');
applicationList.value = []; applicationList.value = [];
return; return;
} }
@@ -281,7 +277,7 @@ const getList = () => {
pageSize: 500, pageSize: 500,
pageNum: 1, pageNum: 1,
categoryCode: '23', categoryCode: '23',
organizationId: patientInfo.value.inHospitalOrgId, organizationId: effectivePatientInfo.value.inHospitalOrgId,
adviceTypes: [3], adviceTypes: [3],
}) })
.then((res) => { .then((res) => {
@@ -298,6 +294,24 @@ const getList = () => {
key: item.adviceDefinitionId, key: item.adviceDefinitionId,
}; };
}); });
// 编辑模式下,加载完数据后设置已选项目
if (props.isEditMode && props.editData?.requestFormDetailList?.length) {
nextTick(() => {
// 使用 adviceName 匹配
const selectedNames = props.editData.requestFormDetailList.map(item => item.adviceName);
console.log('getList completed, selectedNames:', selectedNames);
const selectedIds = [];
applicationList.value?.forEach(app => {
// 匹配时去掉价格部分,只比较名称
const appName = app.label?.split(' (')[0];
if (selectedNames.includes(appName)) {
selectedIds.push(app.key);
}
});
console.log('getList completed, matched selectedIds:', selectedIds);
transferValue.value = selectedIds;
});
}
} else { } else {
proxy.$message.error(res.message); proxy.$message.error(res.message);
applicationList.value = []; applicationList.value = [];
@@ -316,7 +330,7 @@ const form = reactive({
allergyHistory: '', allergyHistory: '',
examinationPurpose: '', examinationPurpose: '',
medicalHistorySummary: '', medicalHistorySummary: '',
expectedExaminationTime: '', expectedExaminationTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
symptom: '', symptom: '',
sign: '', sign: '',
clinicalDiagnosis: '', clinicalDiagnosis: '',
@@ -387,12 +401,65 @@ const handleSyncHistory = async () => {
// 自动带入患者过敏史 // 自动带入患者过敏史
const loadPatientAllergyHistory = () => { const loadPatientAllergyHistory = () => {
if (!patientInfo.value?.patientId) return; if (!effectivePatientInfo.value?.patientId) return;
};
// 加载编辑数据
const loadEditData = () => {
if (!props.isEditMode || !props.editData?.requestFormId) return;
console.log('loadEditData called, editData:', props.editData);
// 解析已有的descJson填充表单
if (props.editData.descJson) {
try {
const obj = JSON.parse(props.editData.descJson);
form.targetDepartment = obj.targetDepartment || '';
form.urgencyLevel = obj.urgencyLevel || 'routine';
form.allergyHistory = obj.allergyHistory || '';
form.examinationPurpose = obj.examinationPurpose || '';
form.medicalHistorySummary = obj.medicalHistorySummary || '';
form.expectedExaminationTime = obj.expectedExaminationTime || '';
form.symptom = obj.symptom || '';
form.sign = obj.sign || '';
form.clinicalDiagnosis = obj.clinicalDiagnosis || '';
form.otherDiagnosis = obj.otherDiagnosis || '';
form.relatedResult = obj.relatedResult || '';
form.attention = obj.attention || '';
form.allergyConfirmed = obj.allergyConfirmed || false;
} catch (e) {
console.error('解析descJson失败:', e);
}
}
// 设置已选项目从requestFormDetailList获取
// 注意:后端返回的字段是 adviceName不是 adviceDefinitionId
console.log('requestFormDetailList:', props.editData.requestFormDetailList);
if (props.editData.requestFormDetailList && props.editData.requestFormDetailList.length) {
// 使用 adviceName 匹配
const selectedNames = props.editData.requestFormDetailList.map(item => item.adviceName);
console.log('setting transferValue by adviceName to:', selectedNames);
// 通过名称匹配找到对应的 key
const selectedIds = [];
applicationList.value?.forEach(app => {
if (selectedNames.includes(app.label?.split(' (')[0])) {
selectedIds.push(app.key);
}
});
console.log('matched selectedIds:', selectedIds);
transferValue.value = selectedIds;
} else {
console.log('requestFormDetailList is empty or undefined');
}
}; };
const projectWithDepartment = (selectProjectIds, type) => { const projectWithDepartment = (selectProjectIds, type) => {
let isRelease = true; let isRelease = true;
const arr = []; const arr = [];
// 确保 applicationList 存在
if (!applicationList.value || !Array.isArray(applicationList.value)) {
return true;
}
selectProjectIds.forEach((element) => { selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => { const searchData = applicationList.value.find((item) => {
return element == item.adviceDefinitionId; return element == item.adviceDefinitionId;
@@ -429,9 +496,28 @@ const projectWithDepartment = (selectProjectIds, type) => {
}; };
watch(() => transferValue.value, (newValue) => { watch(() => transferValue.value, (newValue) => {
console.log('transferValue changed:', newValue);
console.log('applicationList length:', applicationList.value?.length);
projectWithDepartment(newValue, 1); projectWithDepartment(newValue, 1);
}); });
// 监听 applicationList 加载完成,重新设置已选项目
watch(() => applicationList.value, (newList) => {
if (newList && newList.length > 0 && props.isEditMode && props.editData?.requestFormDetailList?.length) {
console.log('applicationList loaded, re-setting transferValue');
// 使用 adviceName 匹配
const selectedNames = props.editData.requestFormDetailList.map(item => item.adviceName);
const selectedIds = [];
newList.forEach(app => {
const appName = app.label?.split(' (')[0];
if (selectedNames.includes(appName)) {
selectedIds.push(app.key);
}
});
transferValue.value = selectedIds;
}
});
const getPriorityCode = () => { const getPriorityCode = () => {
return form.urgencyLevel === 'emergency' ? 1 : 0; return form.urgencyLevel === 'emergency' ? 1 : 0;
}; };
@@ -459,6 +545,7 @@ const submit = () => {
applicationListAllFilter = applicationListAllFilter.map((item) => { applicationListAllFilter = applicationListAllFilter.map((item) => {
return { return {
adviceDefinitionId: item.adviceDefinitionId, adviceDefinitionId: item.adviceDefinitionId,
adviceName: item.adviceName,
quantity: 1, quantity: 1,
unitCode: item.priceList[0].unitCode, unitCode: item.priceList[0].unitCode,
unitPrice: item.priceList[0].price, unitPrice: item.priceList[0].price,
@@ -470,25 +557,32 @@ const submit = () => {
adviceType: item.adviceType, adviceType: item.adviceType,
definitionId: item.priceList[0].definitionId, definitionId: item.priceList[0].definitionId,
definitionDetailId: item.priceList[0].definitionDetailId, definitionDetailId: item.priceList[0].definitionDetailId,
accountId: patientInfo.value.accountId, accountId: effectivePatientInfo.value.accountId,
}; };
}); });
const submitForm = { ...form, priorityCode: getPriorityCode() }; const submitForm = { ...form, priorityCode: getPriorityCode() };
console.log('提交 descJson:', JSON.stringify(submitForm)); console.log('提交 descJson:', JSON.stringify(submitForm));
// 如果是编辑模式带上requestFormId
const requestFormId = props.isEditMode ? props.editData?.requestFormId : '';
saveCheckd({ saveCheckd({
activityList: applicationListAllFilter, activityList: applicationListAllFilter,
patientId: patientInfo.value.patientId, patientId: effectivePatientInfo.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: effectivePatientInfo.value.encounterId,
organizationId: patientInfo.value.inHospitalOrgId, organizationId: effectivePatientInfo.value.inHospitalOrgId,
requestFormId: '', requestFormId: requestFormId,
name: applicationListAllFilter.map(item => item.adviceName).join('、'), name: applicationListAllFilter.map(item => item.adviceName).join('、'),
descJson: JSON.stringify(submitForm), descJson: JSON.stringify(submitForm),
categoryEnum: '2', categoryEnum: '22',
}).then((res) => { }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
if (props.isEditMode) {
proxy.$message.success(res.msg || '修改成功');
} else {
proxy.$message.success(res.msg); proxy.$message.success(res.msg);
}
applicationList.value = []; applicationList.value = [];
resetForm(); resetForm();
emits('submitOk'); emits('submitOk');
@@ -504,7 +598,7 @@ const resetForm = () => {
form.allergyHistory = ''; form.allergyHistory = '';
form.examinationPurpose = ''; form.examinationPurpose = '';
form.medicalHistorySummary = ''; form.medicalHistorySummary = '';
form.expectedExaminationTime = ''; form.expectedExaminationTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss');
form.symptom = ''; form.symptom = '';
form.sign = ''; form.sign = '';
form.clinicalDiagnosis = ''; form.clinicalDiagnosis = '';
@@ -524,7 +618,7 @@ const getLocationInfo = () => {
}; };
function getDiagnosisList() { function getDiagnosisList() {
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => { getEncounterDiagnosis(effectivePatientInfo.value.encounterId).then((res) => {
if (res.code == 200) { if (res.code == 200) {
const datas = (res.data || []).map((item) => { const datas = (res.data || []).map((item) => {
let obj = { ...item }; let obj = { ...item };
@@ -545,9 +639,24 @@ onMounted(() => {
getList(); getList();
getLocationInfo(); getLocationInfo();
loadPatientAllergyHistory(); loadPatientAllergyHistory();
loadEditData();
}); });
defineExpose({ state, submit, getLocationInfo, getDiagnosisList, resetForm }); // 监听编辑模式变化,重新加载数据
watch(
() => props.isEditMode,
(newVal) => {
if (newVal) {
nextTick(() => {
getList();
getLocationInfo();
loadEditData();
});
}
}
);
defineExpose({ state, submit, getLocationInfo, getDiagnosisList, resetForm, getList });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -572,81 +681,13 @@ $bg-color: #f5f7fa;
background: $bg-color; background: $bg-color;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
// 顶部标题栏 // 主体内容区 - 紧凑布局
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 20px;
background: linear-gradient(135deg, #fff 0%, #f0f7ff 100%);
border-bottom: 1px solid $border-color;
.header-left {
display: flex;
align-items: center;
gap: 10px;
.header-icon {
font-size: 24px;
color: $primary-color;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: $text-primary;
letter-spacing: 1px;
}
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
.urgency-label {
font-size: 13px;
color: $text-secondary;
font-weight: 500;
}
.urgency-radio-group {
:deep(.el-radio-button__inner) {
border-radius: 4px;
margin: 0;
}
:deep(.el-radio-button:first-child .el-radio-button__inner) {
border-radius: 4px;
}
:deep(.el-radio-button:last-child .el-radio-button__inner) {
border-radius: 4px;
}
}
.emergency-tip {
display: flex;
align-items: center;
gap: 4px;
color: $danger-color;
font-size: 13px;
font-weight: 500;
background: #fef0f0;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid #fde2e2;
}
}
}
// 主体内容区
.form-body { .form-body {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 8px;
padding: 16px; padding: 8px 12px;
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -663,47 +704,30 @@ $bg-color: #f5f7fa;
} }
} }
// 卡片通用样式 // 紧急程度栏 - 右上角
.section-card { .urgency-bar {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.04);
.section-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end;
gap: 8px; gap: 8px;
padding-bottom: 12px; padding: 4px 0;
margin-bottom: 12px; margin-bottom: 4px;
border-bottom: 1px dashed $border-color;
font-size: 14px;
font-weight: 600;
color: $text-primary;
> i {
font-size: 16px;
color: $primary-color;
} }
.header-count { .urgency-bar-label {
margin-left: auto; font-size: 13px;
font-size: 12px;
font-weight: 400;
color: $text-secondary;
}
.required-mark {
font-size: 12px;
font-weight: 500; font-weight: 500;
color: #fff; color: $text-regular;
background: $danger-color; white-space: nowrap;
padding: 2px 8px;
border-radius: 10px;
margin-left: 4px;
}
} }
// 卡片通用样式 - 紧凑
.section-card {
background: #fff;
border-radius: 6px;
padding: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 4px;
} }
.transfer-wrapper { .transfer-wrapper {
@@ -717,10 +741,23 @@ $bg-color: #f5f7fa;
display: flex !important; display: flex !important;
flex-direction: row !important; flex-direction: row !important;
} }
// 信息表单
// 穿梭框按钮垂直居中
:deep(.el-transfer__buttons) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 4px;
}
:deep(.el-transfer__button) {
margin: 4px 0;
}
// 信息表单 - 紧凑
.info-form { .info-form {
:deep(.el-form-item) { :deep(.el-form-item) {
margin-bottom: 14px; margin-bottom: 6px;
.el-form-item__label { .el-form-item__label {
font-size: 13px; font-size: 13px;
@@ -750,54 +787,11 @@ $bg-color: #f5f7fa;
} }
} }
// 过敏史卡片 // 过敏史危险输入样式
.allergy-card { :deep(.el-textarea__inner.allergy-danger) {
.allergy-content {
.allergy-input-row {
position: relative;
:deep(.el-textarea) {
.el-textarea__inner.allergy-danger {
border-color: $danger-color !important; border-color: $danger-color !important;
background-color: #fef0f0; background-color: #fef0f0;
} }
}
}
.allergy-severe-tag {
position: absolute;
right: 12px;
top: 8px;
display: flex;
align-items: center;
gap: 4px;
color: $danger-color;
font-size: 13px;
font-weight: 600;
background: #fef0f0;
padding: 3px 10px;
border-radius: 12px;
border: 1px solid #fde2e2;
}
.allergy-confirm {
margin-top: 10px;
padding-left: 4px;
}
}
}
// 病史摘要卡片
.history-card {
.section-header {
.sync-btn {
margin-left: auto;
font-size: 12px;
padding: 6px 12px;
border-radius: 16px;
}
}
}
// 急诊确认弹窗 // 急诊确认弹窗
.emergency-dialog-content { .emergency-dialog-content {
@@ -835,4 +829,64 @@ $bg-color: #f5f7fa;
.fade-in-linear-leave-to { .fade-in-linear-leave-to {
opacity: 0; opacity: 0;
} }
/* 紧急程度行内布局 */
.urgency-inline {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.emergency-tip-inline {
display: inline-flex;
align-items: center;
gap: 2px;
color: $danger-color;
font-size: 11px;
font-weight: 500;
background: #fef0f0;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
}
/* 过敏史包装 */
.allergy-wrapper {
width: 100%;
}
.allergy-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 4px;
}
.allergy-severe-tag-inline {
display: inline-flex;
align-items: center;
gap: 2px;
color: $danger-color;
font-size: 11px;
font-weight: 600;
background: #fef0f0;
padding: 2px 8px;
border-radius: 3px;
}
/* 病史摘要同步按钮 */
.history-field-wrapper {
position: relative;
width: 100%;
}
.history-sync-btn {
position: absolute;
right: 4px;
top: -28px;
font-size: 11px;
padding: 2px 8px;
height: 24px;
}
</style> </style>

View File

@@ -86,6 +86,9 @@ import {getApplicationList, saveSurgery} from './api';
import {ElMessage} from 'element-plus'; import {ElMessage} from 'element-plus';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
// 模块级缓存:避免每次打开弹窗都重新请求手术项目列表
let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
// 递归查找树形科室节点 // 递归查找树形科室节点
const findTreeItem = (list, id) => { const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null; if (!list || list.length === 0) return null;
@@ -110,6 +113,12 @@ const getList = () => {
applicationList.value = []; applicationList.value = [];
return; return;
} }
// 命中缓存时直接使用,避免重复请求导致加载缓慢
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
return;
}
loading.value = true; loading.value = true;
getApplicationList({ getApplicationList({
pageSize: 500, pageSize: 500,
@@ -132,6 +141,9 @@ const getList = () => {
key: item.adviceDefinitionId, key: item.adviceDefinitionId,
}; };
}); });
// 写入模块缓存,后续打开弹窗直接复用
surgeryRecordsCache = res.data.records;
surgeryMappedCache = applicationList.value;
} else { } else {
console.warn('获取手术项目列表失败:', res.message); console.warn('获取手术项目列表失败:', res.message);
applicationList.value = []; applicationList.value = [];
@@ -255,7 +267,7 @@ const submit = () => {
requestFormId: '', // 申请单ID requestFormId: '', // 申请单ID
name: '手术申请单', name: '手术申请单',
descJson: JSON.stringify(form), descJson: JSON.stringify(form),
categoryEnum: '4', // 1 检验 2 检查 3 输血 4 手术 categoryEnum: '24', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
}).then((res) => { }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$message.success(res.msg); proxy.$message.success(res.msg);

View File

@@ -142,6 +142,11 @@
<el-table-column label="医嘱" align="center" prop="productName" width="300"> <el-table-column label="医嘱" align="center" prop="productName" width="300">
<template #default="scope"> <template #default="scope">
<template v-if="getRowDisabled(scope.row)"> <template v-if="getRowDisabled(scope.row)">
<!-- 当行的 adviceType 不在当前选项列表中时显示标签而非下拉框 -->
<template v-if="!hasAdviceTypeOption(scope.row)">
<el-tag size="small" type="primary">{{ getAdviceTypeLabel(scope.row.adviceType) }}</el-tag>
</template>
<template v-else>
<el-select <el-select
style="width: 35%; margin-right: 8px" style="width: 35%; margin-right: 8px"
:model-value="getRowSelectValue(scope.row)" :model-value="getRowSelectValue(scope.row)"
@@ -197,6 +202,7 @@
style="width: 62%" style="width: 62%"
v-model="scope.row.adviceName" v-model="scope.row.adviceName"
placeholder="请选择项目" placeholder="请选择项目"
@input="handleChange"
@click="handleFocus(scope.row, scope.$index)" @click="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)" @keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown=" @keydown="
@@ -216,6 +222,7 @@
</template> </template>
</el-popover> </el-popover>
</template> </template>
</template>
<span v-else>{{ scope.row.adviceName }}</span> <span v-else>{{ scope.row.adviceName }}</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -532,6 +539,7 @@ const statusOption = [
let loadingInstance = undefined; let loadingInstance = undefined;
onMounted(() => { onMounted(() => {
document.addEventListener('keydown', escKeyListener); document.addEventListener('keydown', escKeyListener);
getList();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -572,7 +580,6 @@ function handleTotalAmount() {
} }
}, new Decimal(0)); }, new Decimal(0));
} }
getList();
function getList() { function getList() {
getDiagnosisDefinitionList(queryParams.value).then((res) => { getDiagnosisDefinitionList(queryParams.value).then((res) => {
// prescriptionList.value = res.data.records; // prescriptionList.value = res.data.records;
@@ -584,6 +591,11 @@ function refresh() {
} }
// 获取列表信息 // 获取列表信息
function getListInfo(addNewRow) { function getListInfo(addNewRow) {
// 守护:未选择患者时不发起 API 请求,避免页面加载时循环报错
if (!patientInfo.value || !patientInfo.value.encounterId) {
console.warn('⚠️ getListInfo 跳过:未选择患者');
return;
}
loadingInstance = ElLoading.service({ fullscreen: true }); loadingInstance = ElLoading.service({ fullscreen: true });
setTimeout(() => { setTimeout(() => {
loadingInstance.close(); loadingInstance.close();
@@ -709,9 +721,17 @@ function loadConfiguredCategories() {
// 数据过滤 // 数据过滤
const filterPrescriptionList = computed(() => { const filterPrescriptionList = computed(() => {
const pList = prescriptionList.value.filter((item) => { const pList = prescriptionList.value.filter((item) => {
// 修复 Bug #488orderClassCode 可能是复合值 '1-2',需提取 adviceType 部分进行比较
let matchAdviceType = true;
if (orderClassCode.value) {
const filterAdviceType = String(orderClassCode.value).includes('-')
? parseInt(String(orderClassCode.value).split('-')[0])
: orderClassCode.value;
matchAdviceType = filterAdviceType == item.adviceType;
}
return ( return (
(!therapyEnum.value || therapyEnum.value == item.therapyEnum) && (!therapyEnum.value || therapyEnum.value == item.therapyEnum) &&
(!orderClassCode.value || orderClassCode.value == item.adviceType) && matchAdviceType &&
(!orderStatus.value || (orderStatus.value == item.statusEnum && item.requestId)) (!orderStatus.value || (orderStatus.value == item.statusEnum && item.requestId))
); );
}); });
@@ -736,16 +756,57 @@ function getRowDisabled(row) {
return row.isEdit; return row.isEdit;
} }
/**
* 判断行的 adviceType 是否在当前 adviceTypeList 选项中有匹配项
* 修复 Bug #488避免 el-select 因无匹配选项而回显为纯数字
*/
function hasAdviceTypeOption(row) {
if (!row.adviceType && row.adviceType !== 0) return false;
if (row.adviceType == 1 && row.categoryCode) {
const compositeValue = '1-' + row.categoryCode;
return adviceTypeList.value.some(item => item.value === compositeValue);
}
return adviceTypeList.value.some(item => item.value === row.adviceType);
}
/**
* 将原始 adviceType 数字映射为人类可读标签
* 修复 Bug #488当 adviceType 不在选项列表中时,显示标签而非数字
*/
function getAdviceTypeLabel(type) {
if (!type && type !== 0) return '';
// 优先使用字典文本(如果后端返回了 adviceType_dictText
// 但由于当前行数据可能没有 dictText提供兜底映射
const fallbackMap = { 1: '西药', 2: '中成药', 3: '诊疗', 4: '耗材', 5: '会诊', 6: '手术', 23: '检查' };
return fallbackMap[type] || String(type);
}
/** /**
* 将行的 adviceType + categoryCode 映射为 el-select 的选中值 * 将行的 adviceType + categoryCode 映射为 el-select 的选中值
* 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值 * 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值
* 修复 Bug #488当行的 adviceType 在当前配置中找不到匹配选项时,返回最接近的可用值,避免回显为纯数字
*/ */
function getRowSelectValue(row) { function getRowSelectValue(row) {
if (row.adviceType == 1 && row.categoryCode) { if (row.adviceType == 1 && row.categoryCode) {
return '1-' + row.categoryCode; const compositeValue = '1-' + row.categoryCode;
// 检查复合值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === compositeValue)) {
return compositeValue;
}
// 配置的 categoryCode 已变更,回退到第一个药品选项
const firstPharmacy = adviceTypeList.value.find(item => String(item.value).startsWith('1-'));
if (firstPharmacy) {
return firstPharmacy.value;
} }
return row.adviceType; return row.adviceType;
} }
// 诊疗/手术等非药品类型,检查其值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === row.adviceType)) {
return row.adviceType;
}
// 不在选项中的值(如已废弃的 adviceType返回 undefined 让 el-select 显示为空
return undefined;
}
// 新增医嘱 // 新增医嘱
function handleAddPrescription() { function handleAddPrescription() {
@@ -802,13 +863,22 @@ function clickRowDb(row, column, event) {
} }
row.showPopover = false; row.showPopover = false;
// 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑 // 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑
if (row.statusEnum == 1) { // 使用 Number() 做类型转换,确保后端返回的数值能正确比较
if (Number(row.statusEnum) === 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1') // 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1'); row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true; row.isEdit = true;
const index = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey); const index = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey);
prescriptionList.value[index] = row; if (index !== -1) {
prescriptionList.value.splice(index, 1, row);
// 使用 nextTick 确保数据更新后再设置展开状态,保证 el-table 能正确识别 row-key
nextTick(() => {
expandOrder.value = [row.uniqueKey]; expandOrder.value = [row.uniqueKey];
});
} else {
console.warn('⚠️ clickRowDb 未找到匹配行: uniqueKey=', row.uniqueKey, ', row=', row);
proxy.$modal.msgWarning('无法进入编辑模式,请刷新列表后重试');
}
} }
} }
@@ -880,9 +950,8 @@ function handleFocus(row, index) {
// 用 adviceType + categoryCode 组合查找匹配的选项 // 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 修复Bug #486当行没有显式选择医嘱类型时row.adviceType为undefined // 当行没有显式选择医嘱类型时使用已配置的默认categoryCode确保后端能返回结果
// 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤 const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : (adviceQueryParams.value.categoryCode || '');
const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
const searchKey = row.adviceName || ''; const searchKey = row.adviceName || '';
nextTick(() => { nextTick(() => {
@@ -919,10 +988,11 @@ function handleChange(value) {
// 用 adviceType + categoryCode 组合查找匹配的选项 // 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 修复Bug #486当行没有显式选择医嘱类型时row?.adviceType为undefined // 当行没有显式选择医嘱类型时使用已配置的默认categoryCode确保后端能返回结果
// 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤 const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : (adviceQueryParams.value.categoryCode || '');
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : ''; // 修复Bug #453当adviceType为空字符串或NaN时不传具体类型让refresh函数根据searchKey决定搜索范围
tableRef.refresh(adviceType, categoryCode, value); const effectiveAdviceType = (adviceType && !isNaN(Number(adviceType))) ? adviceType : '';
tableRef.refresh(effectiveAdviceType, categoryCode, value);
} }
} }
} }
@@ -1193,7 +1263,7 @@ function handleSave() {
groupId: item.groupId, groupId: item.groupId,
uniqueKey: undefined, uniqueKey: undefined,
// 确保 therapyEnum 被正确传递 // 确保 therapyEnum 被正确传递
therapyEnum: parsedContent.therapyEnum || item.therapyEnum || '1', therapyEnum: parsedContent?.therapyEnum || item.therapyEnum || '1',
}; };
}); });
} catch (error) { } catch (error) {
@@ -1216,6 +1286,13 @@ function handleSave() {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess('签发成功'); proxy.$modal.msgSuccess('签发成功');
isSaving.value = false; isSaving.value = false;
// 乐观更新:立即将已签发医嘱的状态设为"已签发",确保列表实时刷新
saveList.forEach((item) => {
const row = prescriptionList.value.find((r) => r.requestId && r.requestId === item.requestId);
if (row) {
row.statusEnum = 2;
}
});
getListInfo(false); getListInfo(false);
bindMethod.value = {}; bindMethod.value = {};
nextId.value = 1; nextId.value = 1;
@@ -1319,11 +1396,12 @@ function handleCancelEdit(row, index) {
function handleSaveSign(row, index) { function handleSaveSign(row, index) {
if (row.adviceType != 2) { if (row.adviceType != 2) {
// 修复 Bug #488严格校验 itemNo确保非空且为有效字符串才发起请求
let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId; let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId;
if (!itemNo) { if (!itemNo || String(itemNo).trim() === '') {
console.warn('绑定设备检查跳过itemNo为空adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + ''); console.warn('绑定设备检查跳过itemNo为空adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + '');
} else { } else {
getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => { getBindDevice({ typeCode: row.adviceType, itemNo: String(itemNo) }).then((res) => {
if (res.data.length == 0) { if (res.data.length == 0) {
return; return;
} }
@@ -1338,6 +1416,9 @@ function handleSaveSign(row, index) {
bindMethod.value[itemNo] = true; bindMethod.value[itemNo] = true;
} }
} }
}).catch(() => {
// 绑定设备接口失败不影响主流程保存,静默降级
console.warn('绑定设备检查接口调用失败adviceType=' + row.adviceType + ', itemNo=' + itemNo + '');
}); });
} }
} }
@@ -1439,11 +1520,18 @@ function handleSaveBatch() {
.then((res) => { .then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功'); proxy.$modal.msgSuccess('保存成功');
// 修复#405:保存成功后重置所有待保存行的 isEdit 为 false锁定医嘱不再编辑 // 修复 Bug #405保存成功后锁定所有待保存行,避免医嘱条目仍处于可编辑状态
// saveList 中的 item 与 prescriptionList 是同一对象引用,直接修改即可
saveList.forEach(item => { saveList.forEach(item => {
const row = prescriptionList.value.find(r => r.uniqueKey === item.uniqueKey); item.isEdit = false;
if (row) row.isEdit = false;
}); });
// 兜底:锁定所有 statusEnum == 1 的行,确保没有遗漏
prescriptionList.value.forEach(row => {
if (row.statusEnum == 1) {
row.isEdit = false;
}
});
expandOrder.value = [];
getListInfo(false); getListInfo(false);
nextId.value = 1; nextId.value = 1;
isSaving.value = false; isSaving.value = false;
@@ -1456,16 +1544,16 @@ function handleSaveBatch() {
} }
function setValue(row) { function setValue(row) {
// 构造单位列表 // 构造单位列表,确保 value 始终为 String 类型,避免 el-select 值类型不匹配
unitCodeList.value = [ unitCodeList.value = [
{ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' }, { value: String(row.unitCode ?? ''), label: row.unitCode_dictText, type: 'unit' },
{ {
value: row.doseUnitCode, value: String(row.doseUnitCode ?? ''),
label: row.doseUnitCode_dictText, label: row.doseUnitCode_dictText,
type: 'dose', type: 'dose',
}, },
{ {
value: row.minUnitCode, value: String(row.minUnitCode ?? ''),
label: row.minUnitCode_dictText, label: row.minUnitCode_dictText,
type: 'minUnit', type: 'minUnit',
}, },
@@ -1530,9 +1618,9 @@ function setValue(row) {
orgName: row.adviceType != 3 ? undefined : (findOrgName(row.orgId || row.positionId || patientInfo.value?.inHospitalOrgId) || row.orgName || patientInfo.value?.inHospitalOrgName || ''), orgName: row.adviceType != 3 ? undefined : (findOrgName(row.orgId || row.positionId || patientInfo.value?.inHospitalOrgId) || row.orgName || patientInfo.value?.inHospitalOrgName || ''),
// dose: undefined, Removed to preserve dose value from group package // dose: undefined, Removed to preserve dose value from group package
unitCodeList: unitCodeList.value, unitCodeList: unitCodeList.value,
doseUnitCode: row.doseUnitCode, doseUnitCode: String(row.doseUnitCode ?? ''),
minUnitCode: row.minUnitCode, minUnitCode: String(row.minUnitCode ?? ''),
unitCode: row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode, unitCode: row.partAttributeEnum == 1 ? String(row.minUnitCode ?? '') : String(row.unitCode ?? ''),
categoryEnum: row.categoryCode, categoryEnum: row.categoryCode,
definitionId: row.chargeItemDefinitionId, definitionId: row.chargeItemDefinitionId,
executeNum: 1, executeNum: 1,
@@ -1548,6 +1636,10 @@ function setValue(row) {
? new Decimal(selectedStock.price).div(row.partPercent).toFixed(6) ? new Decimal(selectedStock.price).div(row.partPercent).toFixed(6)
: prevRow.minUnitPrice, : prevRow.minUnitPrice,
positionName: selectedStock?.locationName, positionName: selectedStock?.locationName,
// 🔧 Bug #523 修复:初始化 totalPrice 为 0避免总金额列显示为横杠
totalPrice: row.quantity
? new Decimal(row.quantity).mul(selectedStock?.price ?? 0).toFixed(6)
: '0',
} }
: { : {
quantity: 1, quantity: 1,
@@ -1613,20 +1705,14 @@ function handleSaveGroup(orderGroupList) {
// 创建新的处方项目 // 创建新的处方项目
// 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail已由 setValue 填充完整数据) // 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail已由 setValue 填充完整数据)
// 先取 setValue 填充的行数据作为基础
const baseRow = prescriptionList.value[rowIndex.value];
const newRow = { const newRow = {
...prescriptionList.value[rowIndex.value], ...baseRow,
patientId: patientInfo.value.patientId, patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId, encounterId: patientInfo.value.encounterId,
accountId: accountId.value, accountId: accountId.value,
quantity: item.quantity ?? mergedDetail.quantity,
methodCode: item.methodCode ?? mergedDetail.methodCode,
rateCode: item.rateCode ?? mergedDetail.rateCode,
dispensePerDuration: item.dispensePerDuration ?? mergedDetail.dispensePerDuration,
dose: item.dose ?? mergedDetail.dose,
doseQuantity: item.doseQuantity ?? mergedDetail.doseQuantity,
executeNum: 1, executeNum: 1,
unitCode: item.unitCode ?? mergedDetail.unitCode,
unitCode_dictText: item.unitCodeName || mergedDetail.unitCodeName || '',
statusEnum: 1, statusEnum: 1,
orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '', orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时保存 orgName确保树匹配不到时仍有中文名称可显示 // 🔧 修复:同时保存 orgName确保树匹配不到时仍有中文名称可显示
@@ -1635,19 +1721,33 @@ function handleSaveGroup(orderGroupList) {
conditionId: conditionId.value, conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value, conditionDefinitionId: conditionDefinitionId.value,
encounterDiagnosisId: encounterDiagnosisId.value, encounterDiagnosisId: encounterDiagnosisId.value,
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || '1', therapyEnum: baseRow?.therapyEnum || '1',
}; };
// 覆盖关键字段:优先使用 item 的值,其次 mergedDetail已由 setValue 填充),最后 baseRow
newRow.quantity = item.quantity ?? mergedDetail.quantity ?? baseRow.quantity;
newRow.methodCode = item.methodCode ?? mergedDetail.methodCode ?? baseRow.methodCode;
newRow.rateCode = item.rateCode ?? mergedDetail.rateCode ?? baseRow.rateCode;
newRow.dispensePerDuration = item.dispensePerDuration ?? mergedDetail.dispensePerDuration ?? baseRow.dispensePerDuration;
newRow.dose = item.dose ?? mergedDetail.dose ?? baseRow.dose;
newRow.doseQuantity = item.doseQuantity ?? mergedDetail.doseQuantity ?? baseRow.doseQuantity;
newRow.unitCode = item.unitCode ?? mergedDetail.unitCode ?? baseRow.unitCode;
newRow.unitCode_dictText = item.unitCodeName || mergedDetail.unitCodeName || baseRow.unitCode_dictText || '';
// 计算价格和总量 // 计算价格和总量
const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode); // 🔧 Bug #403 修复:使用 newRow.unitCode已由 setValue 填充)而非 item.unitCode
// 使用 ?? 替代 || 计算 partPercent确保值为 0 时不会被错误替换
const finalUnitCode = newRow.unitCode;
const unitInfo = unitCodeList.value.find((k) => k.value == finalUnitCode);
const finalQuantity = newRow.quantity;
const partPercent = item.orderDetailInfos?.partPercent ?? mergedDetail.partPercent ?? baseRow.partPercent ?? 1;
if (unitInfo && unitInfo.type == 'minUnit') { if (unitInfo && unitInfo.type == 'minUnit') {
newRow.price = newRow.minUnitPrice; newRow.price = newRow.minUnitPrice;
newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6); newRow.totalPrice = ((finalQuantity || 0) * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity; newRow.minUnitQuantity = finalQuantity || 0;
} else { } else {
newRow.price = newRow.unitPrice; newRow.price = newRow.unitPrice;
newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6); newRow.totalPrice = ((finalQuantity || 0) * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity * (item.orderDetailInfos?.partPercent || mergedDetail.partPercent || 1); newRow.minUnitQuantity = (finalQuantity || 0) * partPercent;
} }
newRow.contentJson = JSON.stringify(newRow); newRow.contentJson = JSON.stringify(newRow);

View File

@@ -19,11 +19,9 @@
<el-tab-pane label="检验申请" name="test"> <el-tab-pane label="检验申请" name="test">
<TestApplication ref="testApplicationRef" :show-status-column="true" /> <TestApplication ref="testApplicationRef" :show-status-column="true" />
</el-tab-pane> </el-tab-pane>
```vue
<el-tab-pane label="检查申请" name="examine"> <el-tab-pane label="检查申请" name="examine">
<ExamineApplication ref="examineApplicationRef" /> <ExamineApplication ref="examineApplicationRef" />
</el-tab-pane> </el-tab-pane>
```
<el-tab-pane label="汇总发药申请" name="summaryDrug"> <el-tab-pane label="汇总发药申请" name="summaryDrug">
<SummaryDrugApplication ref="summaryDrugApplicationRef" /> <SummaryDrugApplication ref="summaryDrugApplicationRef" />
</el-tab-pane> </el-tab-pane>
@@ -49,6 +47,7 @@
import {computed, onBeforeMount, onMounted, provide, reactive, ref, watch,} from 'vue'; import {computed, onBeforeMount, onMounted, provide, reactive, ref, watch,} from 'vue';
import Emr from './emr/index.vue'; import Emr from './emr/index.vue';
import SummaryDrugApplication from './components/applicationShow/summaryDrugApplication.vue';
import inPatientBarDoctorFold from '@/components/patientBar/inPatientBarDoctorFold.vue'; import inPatientBarDoctorFold from '@/components/patientBar/inPatientBarDoctorFold.vue';
import PatientList from '@/components/PatientList/patient-list.vue'; import PatientList from '@/components/PatientList/patient-list.vue';
import {localPatientInfo, updateLocalPatientInfo} from './store/localPatient'; import {localPatientInfo, updateLocalPatientInfo} from './store/localPatient';
@@ -84,6 +83,7 @@ const currentPatientInfo = ref({});
const testApplicationRef = ref(); const testApplicationRef = ref();
const examineApplicationRef = ref(); const examineApplicationRef = ref();
const surgeryApplicationRef = ref(); const surgeryApplicationRef = ref();
const summaryDrugApplicationRef = ref();
const bloodTtransfusionAapplicationRef = ref(); const bloodTtransfusionAapplicationRef = ref();
// 患者列表相关逻辑 // 患者列表相关逻辑

View File

@@ -1,4 +1,5 @@
<template> <template>
<div class="fee-dialog-wrapper">
<el-dialog v-model="dialogVisible" title="补费" width="80%" :close-on-click-modal="false"> <el-dialog v-model="dialogVisible" title="补费" width="80%" :close-on-click-modal="false">
<div style="font-size: 16px; font-weight: bold; margin-bottom: 10px"> <div style="font-size: 16px; font-weight: bold; margin-bottom: 10px">
患者信息{{ 患者信息{{
@@ -251,7 +252,7 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 划价组套选择对话框 --> <!-- 划价组套选择对话框 -->
<el-dialog v-model="groupSetDialogVisible" title="划价组套选择" width="600px" :close-on-click-modal="false" append-to-body> <el-dialog v-model="groupSetDialogVisible" title="划价组套选择" width="600px" :close-on-click-modal="false" append-to-body :z-index="3000" destroy-on-close>
<div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px"> <div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px">
<el-input <el-input
v-model="groupSetSearchText" v-model="groupSetSearchText"
@@ -296,11 +297,13 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-empty v-if="!groupSetLoading && groupSetList.length === 0" description="暂无划价组套数据" :image-size="80" />
<div style="margin-top: 15px; text-align: right"> <div style="margin-top: 15px; text-align: right">
<el-button @click="groupSetDialogVisible = false">取消</el-button> <el-button @click="groupSetDialogVisible = false">取消</el-button>
<el-button type="primary" @click="applyGroupSet" :disabled="!selectedGroupSet">应用</el-button> <el-button type="primary" @click="applyGroupSet" :disabled="!selectedGroupSet">应用</el-button>
</div> </div>
</el-dialog> </el-dialog>
</div>
</template> </template>
<script setup> <script setup>
@@ -481,19 +484,65 @@ watch(
(visible) => { (visible) => {
if (visible) { if (visible) {
executeTime.value = formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss'); executeTime.value = formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss');
// 弹窗打开时重新加载科室和位置选项,确保数据最新
loadDepartmentOptions();
getDiseaseInitLoc(16);
} else { } else {
resetData(); resetData();
} }
} }
); );
// 加载科室选项 // 监听科室选项加载完成,为已添加的诊疗项目设置默认执行科室
watch(
() => departmentOptions.value,
(depts) => {
if (!depts || depts.length === 0) return;
feeItemsList.value.forEach(item => {
if (item.adviceType === 3 && !item.positionId) {
const patientOrgId = props.patientInfo.organizationId;
const matched = depts.find(d => String(d.id) === String(patientOrgId));
item.positionId = matched ? String(matched.id) : String(depts[0].id);
}
});
}
);
// 监听位置选项加载完成,为已添加的耗材项目设置默认位置
watch(
() => locationOptions.value,
(locs) => {
if (!locs || locs.length === 0) return;
feeItemsList.value.forEach(item => {
if (item.adviceType === 2 && !item.positionId) {
item.positionId = String(locs[0].value);
}
});
}
);
// 加载科室选项(递归扁平化树形结构)
function loadDepartmentOptions() { function loadDepartmentOptions() {
getOrgList() getOrgList()
.then((res) => { .then((res) => {
if (res.data && res.data.records && res.data.records.length > 0) { if (!res?.data?.records?.length) {
departmentOptions.value = res.data.records[0].children || []; departmentOptions.value = [];
return;
} }
// 递归扁平化树形结构,提取所有科室节点
const flattenTree = (nodes) => {
const result = [];
for (const node of nodes) {
if (node?.id && node?.name) {
result.push(node);
}
if (node?.children?.length) {
result.push(...flattenTree(node.children));
}
}
return result;
};
departmentOptions.value = flattenTree(res.data.records);
}) })
.catch(() => { .catch(() => {
console.warn('科室列表加载失败(可能无权限)'); console.warn('科室列表加载失败(可能无权限)');
@@ -527,23 +576,27 @@ function getDiseaseInitLoc() {
locationOptions.value = []; locationOptions.value = [];
}); });
} }
// 下拉框模糊搜索过滤 // 下拉框模糊搜索过滤自定义filter-method配合element-plus filterable使用
function filterOptions(val, row, optionsKey) { function filterOptions(val, row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey; const key = row.adviceDefinitionId + '_' + optionsKey;
filterKeywords.value[key] = val; if (!val || val.trim() === '') {
delete filterKeywords.value[key];
} else {
filterKeywords.value[key] = val.toLowerCase();
}
} }
function getFilteredOptions(row, optionsKey) { function getFilteredOptions(row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey; const key = row.adviceDefinitionId + '_' + optionsKey;
const keyword = filterKeywords.value[key]; const keyword = filterKeywords.value[key];
const options = optionsKey === 'departmentOptions' ? departmentOptions.value : locationOptions.value; const options = optionsKey === 'departmentOptions' ? departmentOptions.value : locationOptions.value;
if (!keyword || keyword.trim() === '') { if (!keyword) {
return options; return options;
} }
const lower = keyword.toLowerCase();
return options.filter(item => { return options.filter(item => {
const name = (item.name || item.label || '').toLowerCase(); const name = (item.name || item.label || '').toLowerCase();
const id = String(item.id || item.value || '').toLowerCase(); const id = String(item.id || item.value || '').toLowerCase();
return name.includes(lower) || id.includes(lower); const py = (item.pyStr || '').toLowerCase();
return name.includes(keyword) || id.includes(keyword) || py.includes(keyword);
}); });
} }
// 获取组套类型文本 // 获取组套类型文本
@@ -552,19 +605,26 @@ function getItemType_Text(type) {
return map[type] || '其他'; return map[type] || '其他';
} }
function getUnitCodeOptions(row) { function getUnitCodeOptions(row) {
const unitCodes = [ const unitCodes = [];
{ code: row.unitCode, codeText: row.unitCode_dictText }, // 大单位:优先用 codecode 缺失时用字典文本兜底
{ code: row.minUnitCode, codeText: row.minUnitCode_dictText }, if (row.unitCode != null && String(row.unitCode) !== '') {
]; unitCodes.push({ code: String(row.unitCode), codeText: row.unitCode_dictText || String(row.unitCode) });
// 使用 Set 来跟踪已经存在的 code } else if (row.unitCode_dictText) {
unitCodes.push({ code: row.unitCode_dictText, codeText: row.unitCode_dictText });
}
// 小单位:同上
if (row.minUnitCode != null && String(row.minUnitCode) !== '') {
unitCodes.push({ code: String(row.minUnitCode), codeText: row.minUnitCode_dictText || String(row.minUnitCode) });
} else if (row.minUnitCode_dictText) {
unitCodes.push({ code: row.minUnitCode_dictText, codeText: row.minUnitCode_dictText });
}
// 去重
const seenCodes = new Set(); const seenCodes = new Set();
const uniqueUnitCodes = unitCodes.filter((item) => { const uniqueUnitCodes = unitCodes.filter((item) => {
// 如果 Set 中没有这个 code就保留它并把它加入 Set
if (!seenCodes.has(item.code)) { if (!seenCodes.has(item.code)) {
seenCodes.add(item.code); seenCodes.add(item.code);
return true; return true;
} }
// 如果已经存在,就过滤掉
return false; return false;
}); });
return uniqueUnitCodes; return uniqueUnitCodes;
@@ -575,11 +635,11 @@ function unitCodeChange(row) {
// 获取价格 // 获取价格
const price = row.priceList?.[0]?.price || 0; const price = row.priceList?.[0]?.price || 0;
// 根据选择的单位调整单价 // 根据选择的单位调整单价(统一用字符串比较)
if (row.selectUnitCode === row.unitCode) { if (String(row.selectUnitCode) === String(row.unitCode)) {
// 如果选择的是大单位 (如 "") // 如果选择的是大单位 (如 "")
row.unitPrice = price.toFixed(6); // 单价就是原价 row.unitPrice = price.toFixed(6); // 单价就是原价
} else if (row.selectUnitCode === row.minUnitCode) { } else if (String(row.selectUnitCode) === String(row.minUnitCode)) {
// 如果选择的是小单位 (如 "") // 如果选择的是小单位 (如 "")
row.unitPrice = (price / (row.partPercent || 1)).toFixed(6); // 单价 = 原价 / 拆零比 row.unitPrice = (price / (row.partPercent || 1)).toFixed(6); // 单价 = 原价 / 拆零比
} }
@@ -627,14 +687,36 @@ function selectChange(row) {
defaultUnitCode = String(row.minUnitCode); defaultUnitCode = String(row.minUnitCode);
} else if (row.unitCode) { } else if (row.unitCode) {
defaultUnitCode = String(row.unitCode); defaultUnitCode = String(row.unitCode);
} else if (row.minUnitCode_dictText) {
// 兜底:如果 minUnitCode 为空但字典文本存在,使用文本作为选项值
defaultUnitCode = row.minUnitCode_dictText;
} else if (row.unitCode_dictText) {
defaultUnitCode = row.unitCode_dictText;
}
// 如果默认单位不在 uniqueUnitCodes 中,添加兜底选项
if (defaultUnitCode && !uniqueUnitCodes.some(u => u.code === defaultUnitCode)) {
uniqueUnitCodes.push({ code: defaultUnitCode, codeText: defaultUnitCode });
} }
// 设置默认执行科室/位置(统一转为字符串,避免 el-select 类型不匹配) // 设置默认执行科室/位置(统一转为字符串,避免 el-select 类型不匹配)
let defaultPositionId = undefined; let defaultPositionId = undefined;
if (row.adviceType === 3 && departmentOptions.value.length > 0) { if (row.adviceType === 3 && departmentOptions.value.length > 0) {
// 诊疗:优先使用患者所在科室,否则取第一个科室 // 诊疗:
const patientOrgId = props.patientInfo.organizationId; // 1. 优先使用诊疗目录项目的"所属科室"row.orgId
const matched = departmentOptions.value.find(d => String(d.id) === String(patientOrgId)); // 2. 其次使用患者当前病房科室patientInfo.organizationId
defaultPositionId = matched ? String(matched.id) : String(departmentOptions.value[0].id); // 3. 最后取第一个科室
const orgIdPriority = [row.orgId, props.patientInfo.organizationId];
for (const id of orgIdPriority) {
if (id) {
const matched = departmentOptions.value.find(d => String(d.id) === String(id));
if (matched) {
defaultPositionId = String(matched.id);
break;
}
}
}
if (!defaultPositionId) {
defaultPositionId = String(departmentOptions.value[0].id);
}
} else if (row.adviceType === 2 && locationOptions.value.length > 0) { } else if (row.adviceType === 2 && locationOptions.value.length > 0) {
// 耗材:默认取第一个药房/耗材房 // 耗材:默认取第一个药房/耗材房
defaultPositionId = String(locationOptions.value[0].value); defaultPositionId = String(locationOptions.value[0].value);
@@ -763,6 +845,7 @@ function resetData() {
// 划价组套相关功能 // 划价组套相关功能
function openGroupSetDialog() { function openGroupSetDialog() {
console.log('openGroupSetDialog called');
groupSetDialogVisible.value = true; groupSetDialogVisible.value = true;
groupSetSearchText.value = ''; groupSetSearchText.value = '';
selectedGroupSet.value = null; selectedGroupSet.value = null;
@@ -771,19 +854,37 @@ function openGroupSetDialog() {
function loadGroupSets() { function loadGroupSets() {
groupSetLoading.value = true; groupSetLoading.value = true;
getOrderGroup({ organizationId: orgId.value }) const params = { organizationId: orgId.value };
// 传递搜索关键字,后端 /group-package-for-order 虽不直接支持 searchKey
// 但保持参数传递以便后续扩展
if (groupSetSearchText.value && groupSetSearchText.value.trim()) {
params.searchKey = groupSetSearchText.value.trim();
}
getOrderGroup(params)
.then((res) => { .then((res) => {
const data = res?.data || {}; const data = res?.data || {};
let rawList = [];
if (groupSetRange.value === 1) { if (groupSetRange.value === 1) {
groupSetList.value = data.personalList || []; rawList = data.personalList || [];
} else if (groupSetRange.value === 2) { } else if (groupSetRange.value === 2) {
groupSetList.value = data.organizationList || []; rawList = data.organizationList || [];
} else { } else {
groupSetList.value = data.hospitalList || []; rawList = data.hospitalList || [];
}
// 客户端过滤:根据搜索关键字过滤组套名称
const keyword = groupSetSearchText.value?.trim()?.toLowerCase();
if (keyword) {
groupSetList.value = rawList.filter(item => {
const name = (item.name || item.Name || '').toLowerCase();
return name.includes(keyword);
});
} else {
groupSetList.value = rawList;
} }
}) })
.catch(() => { .catch((err) => {
console.warn('组套列表加载失败(可能无权限)'); console.warn('组套列表加载失败(可能无权限):', err);
ElMessage.warning('组套列表加载失败,当前暂无可用组套');
groupSetList.value = []; groupSetList.value = [];
}) })
.finally(() => { .finally(() => {

View File

@@ -28,6 +28,7 @@ export function getOrgList() {
return request({ return request({
url: '/base-data-manage/organization/organization', url: '/base-data-manage/organization/organization',
method: 'get', method: 'get',
params: { pageSize: 100, pageNum: 1 },
}); });
} }
/** /**

View File

@@ -463,20 +463,45 @@ function watchPatientSelection() {
}, 300); }, 300);
} }
/** 查询科室 */ /** 查询科室(支持树形/扁平多种响应结构) */
const getLocationInfo = () => { const getLocationInfo = () => {
getOrgList().then((res) => { getOrgList().then((res) => {
orgOptions.value = res.data?.records[0]?.children; if (!res.data) {
orgOptions.value = [];
return;
}
// 尝试从树形结构取records[0].children
if (res.data.records && res.data.records.length > 0) {
if (res.data.records[0].children && res.data.records[0].children.length > 0) {
orgOptions.value = res.data.records[0].children;
return;
}
// 如果 records[0] 有 id 和 name非树根节点直接用所有 records
if (res.data.records[0].id) {
orgOptions.value = res.data.records;
return;
}
}
// 兜底:如果 data 本身是数组
if (Array.isArray(res.data)) {
orgOptions.value = res.data;
return;
}
orgOptions.value = [];
}).catch(() => {
console.warn('科室列表加载失败(可能无权限)');
orgOptions.value = [];
}); });
}; };
getLocationInfo(); getLocationInfo();
// 映射 // 映射(查找失败时返回 '-' 而非显示内码)
const selectOrg = (itemid) => { const selectOrg = (itemid) => {
if (!itemid) return '-';
const item = orgOptions.value.find((item) => { const item = orgOptions.value.find((item) => {
return item.id == itemid; return item.id == itemid;
}); });
return item?.name; return item?.name || '-';
}; };
// 重置 // 重置
const onReset = () => { const onReset = () => {

View File

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

View File

@@ -85,7 +85,7 @@
:deadline="deadline" :deadline="deadline"
:therapyEnum="therapyEnum" :therapyEnum="therapyEnum"
/> />
<SummaryMedicineList v-else /> <SummaryMedicineList v-else ref="summaryMedicineRefs" :therapyEnum="therapyEnum" />
<!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick"> <!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick">
<el-tab-pane <el-tab-pane
v-for="tab in prescriptionTabs" v-for="tab in prescriptionTabs"
@@ -129,6 +129,7 @@ const therapyEnum = ref(undefined);
// 存储子组件引用的对象 // 存储子组件引用的对象
const prescriptionRefs = ref(); const prescriptionRefs = ref();
const summaryMedicineRefs = ref();
const navigationButtons = inpatientNurseNavs; const navigationButtons = inpatientNurseNavs;
@@ -165,7 +166,11 @@ function handleClick(tabName) {
function handleGetPrescription() { function handleGetPrescription() {
chooseAll.value = false; chooseAll.value = false;
if (isDetails.value == '1') {
prescriptionRefs.value?.handleGetPrescription(); prescriptionRefs.value?.handleGetPrescription();
} else {
summaryMedicineRefs.value?.handleGetPrescription();
}
} }
function handelSwicthChange(value) { function handelSwicthChange(value) {

View File

@@ -458,7 +458,9 @@ const loadPatientInfo = () => {
'YYYY-MM-DD HH:mm:ss' 'YYYY-MM-DD HH:mm:ss'
); );
} else { } else {
interventionForm.value.startTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'); // 已有患者entranceType == 1不自动填充当前时间避免覆盖历史数据
// 新入科患者由后端默认返回当前时间,或由用户手动选择
interventionForm.value.startTime = '';
} }
interventionForm.value.height = res.data.height; interventionForm.value.height = res.data.height;
interventionForm.value.weight = res.data.weight; interventionForm.value.weight = res.data.weight;

View File

@@ -241,6 +241,8 @@ const loading = ref(false);
const chooseAll = ref(false); const chooseAll = ref(false);
// 独立维护选中行ID集合避免el-table内部selection状态异常导致联动全选 // 独立维护选中行ID集合避免el-table内部selection状态异常导致联动全选
const selectedRowIds = ref(new Set()); const selectedRowIds = ref(new Set());
// 跳过选中事件级联:程序化调用 toggleRowSelection 时阻止 handleRowSelect 触发 selectAllCheckboxesInRow
const skipSelectCascade = ref(false);
const props = defineProps({ const props = defineProps({
exeStatus: { exeStatus: {
type: Number, type: Number,
@@ -459,10 +461,15 @@ function handleGetPrescription(skipAutoSelectAll = false) {
// 执行 // 执行
function handleExecute() { function handleExecute() {
let list = getSelectRows(); let list = getSelectRows();
if (list.length === 0) {
proxy.$modal.msgWarning('请选择需要执行的医嘱');
return;
}
let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(','); let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(',');
list = list.map((item) => { list = list.map((item) => {
return { return {
requestId: item.requestId, requestId: item.requestId,
encounterId: item.encounterId,
accountId: item.accountId, accountId: item.accountId,
adviceTable: item.adviceTable, adviceTable: item.adviceTable,
executeTimes: item.executeTimes, executeTimes: item.executeTimes,
@@ -471,21 +478,40 @@ function handleExecute() {
console.log(list, 'list'); console.log(list, 'list');
adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => { adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
// 仅当选中医嘱中包含耗材类医嘱时,才调用耗材批号匹配(排除纯药品医嘱场景) // 仅当选中医嘱中包含诊疗类医嘱(可能绑定耗材)时,才调用耗材批号匹配
const hasDevice = list.some((item) => // adviceTable 取值为 med_medication_request药品或 wor_service_request诊疗/耗材)
String(item.adviceTable || '').includes('device'), const hasServiceRequest = list.some((item) =>
String(item.adviceTable || '') === 'wor_service_request',
); );
if (hasDevice) { if (hasServiceRequest) {
lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => { // 仅传入选中医嘱对应的 encounterId避免其他患者的耗材记录干扰
const selectedEncounterIds = [...new Set(list.map((item) => item.encounterId).filter(Boolean))];
// 仅传入诊疗类医嘱的 requestId让后端仅校验与本次执行相关的耗材避免其他未执行医嘱的耗材记录干扰
const selectedRequestIds = list
.filter((item) => String(item.adviceTable || '') === 'wor_service_request')
.map((item) => item.requestId)
.filter(Boolean);
if (selectedEncounterIds.length > 0) {
lotNumberMatch({ encounterIdList: selectedEncounterIds, requestIdList: selectedRequestIds }, { skipErrorMsg: true })
.then((matchRes) => {
if (matchRes && matchRes.code !== 200) {
console.warn('lotNumberMatch returned error:', matchRes.msg);
}
})
.catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error); console.warn('lotNumberMatch failed after adviceExecute:', error);
}); });
} }
}
// 刷新列表(不自动全选,保持用户操作前的选择状态) // 刷新列表(不自动全选,保持用户操作前的选择状态)
handleGetPrescription(true); handleGetPrescription(true);
proxy.$modal.msgSuccess(res.msg || '医嘱执行成功'); proxy.$modal.msgSuccess(res.msg || '医嘱执行成功');
} else { } else {
proxy.$modal.msgError(res.msg || '医嘱执行失败'); proxy.$modal.msgError(res.msg || '医嘱执行失败');
} }
}).catch((error) => {
console.error('医嘱执行接口调用失败:', error);
proxy.$modal.msgError('医嘱执行失败,请稍后重试');
}); });
} }
@@ -631,6 +657,7 @@ function handleRateChange(value, date, time, row, rateItem) {
} }
function handelSwicthChange(value) { function handelSwicthChange(value) {
skipSelectCascade.value = true;
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item, index) => {
const tableRef = proxy.$refs['tableRef' + index]; const tableRef = proxy.$refs['tableRef' + index];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
@@ -651,12 +678,15 @@ function handelSwicthChange(value) {
} }
} }
}); });
skipSelectCascade.value = false;
} }
// 默认选中全部行 // 默认选中全部行
function defaultSelectAllRows() { function defaultSelectAllRows() {
// 清空并重建选中集合 // 清空并重建选中集合
selectedRowIds.value.clear(); selectedRowIds.value.clear();
// 阻止 toggleRowSelection 触发 handleRowSelect 中的 selectAllCheckboxesInRow 级联
skipSelectCascade.value = true;
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item, index) => {
const tableRef = proxy.$refs['tableRef' + index]; const tableRef = proxy.$refs['tableRef' + index];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
@@ -669,6 +699,7 @@ function defaultSelectAllRows() {
}); });
} }
}); });
skipSelectCascade.value = false;
// 更新全选开关状态 // 更新全选开关状态
chooseAll.value = true; chooseAll.value = true;
} }
@@ -745,6 +776,7 @@ function checkAndToggleRowSelection(row) {
const isCurrentlySelected = selectedRowIds.value.has(row.requestId); const isCurrentlySelected = selectedRowIds.value.has(row.requestId);
// 根据checkbox状态更新表格行选中状态 // 根据checkbox状态更新表格行选中状态
skipSelectCascade.value = true;
if (isAllSelected && !isCurrentlySelected) { if (isAllSelected && !isCurrentlySelected) {
selectedRowIds.value.add(row.requestId); selectedRowIds.value.add(row.requestId);
tableRef[0].toggleRowSelection(row, true); tableRef[0].toggleRowSelection(row, true);
@@ -752,6 +784,7 @@ function checkAndToggleRowSelection(row) {
selectedRowIds.value.delete(row.requestId); selectedRowIds.value.delete(row.requestId);
tableRef[0].toggleRowSelection(row, false); tableRef[0].toggleRowSelection(row, false);
} }
skipSelectCascade.value = false;
} }
} }
}); });
@@ -763,8 +796,10 @@ function handleRowSelect(selection, row, tableIndex) {
if (isSelected) { if (isSelected) {
selectedRowIds.value.add(row.requestId); selectedRowIds.value.add(row.requestId);
// 选中选中该行内部的所有checkbox // 仅在非程序化选中时,联动选中该行内部的所有checkbox
if (!skipSelectCascade.value) {
selectAllCheckboxesInRow(row); selectAllCheckboxesInRow(row);
}
} else { } else {
selectedRowIds.value.delete(row.requestId); selectedRowIds.value.delete(row.requestId);
// 取消选中行时取消选中该行内部的所有checkbox // 取消选中行时取消选中该行内部的所有checkbox

View File

@@ -1057,8 +1057,8 @@ function confirmCharge() {
params.recordingDate = formData.value.recordingDate || moment(new Date()).format('YYYY-MM-DD'); params.recordingDate = formData.value.recordingDate || moment(new Date()).format('YYYY-MM-DD');
addVitalSigns(params).then(res => { addVitalSigns(params).then(res => {
console.log('保存成功:', res);
if (res.code === 200) { if (res.code === 200) {
proxy.msgSuccess('保存成功');
// 保存成功后刷新列表 // 保存成功后刷新列表
getPatientList(); getPatientList();
// 清空表单 // 清空表单
@@ -1087,8 +1087,6 @@ function confirmCharge() {
urineVolume: '', urineVolume: '',
stoolVolume: '', stoolVolume: '',
}; };
// 保存成功后关闭弹窗
closeDialog();
} }
}); });
} }

View File

@@ -61,7 +61,11 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
/** 表头所选出库仓库:传入后药品列表只含该仓有库存的行,避免选到别仓批号导致 inventory-item-info 一直为 0 */
orgLocationId: {
type: [String, Number],
default: undefined,
},
}); });
const emit = defineEmits(['selectRow']); const emit = defineEmits(['selectRow']);
const queryParams = ref({ const queryParams = ref({
@@ -89,11 +93,14 @@ watch(
queryParams.value.searchKey = newValue.searchKey; queryParams.value.searchKey = newValue.searchKey;
queryParams.value.itemType = newValue.itemType; queryParams.value.itemType = newValue.itemType;
queryParams.value.purchaseFlag = 0; queryParams.value.purchaseFlag = 0;
// queryParams.value.sourceLocationId = newValue.sourceLocationId; if (newValue.orgLocationId != null && newValue.orgLocationId !== '') {
// queryParams.value.purposeLocationId = newValue.purposeLocationId; queryParams.value.orgLocationId = newValue.orgLocationId;
} else {
delete queryParams.value.orgLocationId;
}
throttledGetList(); throttledGetList();
}, },
{ immdiate: true, deep: true } { immediate: true, deep: true }
); );
getList(); getList();

View File

@@ -133,6 +133,7 @@
filterable filterable
style="width: 200px" style="width: 200px"
:disabled="data.isEdit" :disabled="data.isEdit"
@change="onHeaderWarehouseChange"
> >
<el-option <el-option
v-for="item in purposeTypeListOptions" v-for="item in purposeTypeListOptions"
@@ -222,6 +223,7 @@
@selectRow="(row) => selectRow(row, scope.$index)" @selectRow="(row) => selectRow(row, scope.$index)"
:searchKey="medicineSearchKey" :searchKey="medicineSearchKey"
:itemType="itemType" :itemType="itemType"
:orgLocationId="receiptHeaderForm.headerLocationId"
/> />
</template> </template>
</PopoverList> </PopoverList>
@@ -483,6 +485,46 @@ import {useStore} from '@/store/store';
import useTagsViewStore from '@/store/modules/tagsView'; import useTagsViewStore from '@/store/modules/tagsView';
import TraceNoDialog from '@/components/OpenHis/TraceNoDialog/index.vue' import TraceNoDialog from '@/components/OpenHis/TraceNoDialog/index.vue'
/** 领用保存 IssueDto后端 Jackson 只认 yyyy-MM-dd HH:mm:ss库存接口可能回传 2025/4/2 00:00:00 等 */
function toIssueDateTimeStr(val) {
if (val == null || val === '') return undefined;
if (typeof val === 'string') {
const s = val.trim();
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(s)) return s;
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return `${s} 00:00:00`;
}
const d = val instanceof Date ? val : new Date(val);
if (Number.isNaN(d.getTime())) return undefined;
return formatDate(d);
}
/** 总库存 totalQuantity 为最小单位;领用数量按当前计量单位折算成最小单位后再比较 */
function getRequisitionQtyInMinUnit(r) {
const q = Number(r.itemQuantity ?? 0);
if (!Number.isFinite(q) || q < 0) return NaN;
const minCode = r.unitList?.minUnitCode;
if (!minCode || r.unitCode === minCode) return q;
const part = Number(r.partPercent ?? 1);
return q * part;
}
function validateRequisitionQtyVsStock(r, lineNo) {
const cap = Number(r.totalQuantity ?? 0);
const reqMin = getRequisitionQtyInMinUnit(r);
if (!Number.isFinite(reqMin)) {
return `${lineNo}行:领用数量请输入有效数字`;
}
if (cap > 0 && reqMin > cap + 1e-9) {
const name = r.name || `${lineNo}`;
return `${name}:领用数量(折合最小单位)不能超过当前仓库可领库存 ${cap},请修改后再保存。`;
}
if (cap <= 0 && reqMin > 0) {
const name = r.name || `${lineNo}`;
return `${name}:当前仓库可领库存为 0不能填写正数领用数量。`;
}
return null;
}
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const store = useStore(); const store = useStore();
@@ -492,6 +534,7 @@ const userStore = useUserStore();
const openTraceNoDialog = ref(false) const openTraceNoDialog = ref(false)
const rowData = ref({}) const rowData = ref({})
const ypName = ref('') const ypName = ref('')
const currentIndex = ref(-1)
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const { warehous_type, category_code, service_type_code, specialty_code, purchase_type } = const { warehous_type, category_code, service_type_code, specialty_code, purchase_type } =
@@ -568,6 +611,8 @@ const data = reactive({
medicationType: [{ required: true, message: '请选择药品类型', trigger: 'change' }], medicationType: [{ required: true, message: '请选择药品类型', trigger: 'change' }],
locationId: [{ required: true, message: '请选择领用部门', trigger: 'change' }], locationId: [{ required: true, message: '请选择领用部门', trigger: 'change' }],
practitionerId: [{ required: true, message: '请选择部门经手人', trigger: 'change' }], practitionerId: [{ required: true, message: '请选择部门经手人', trigger: 'change' }],
// 领用出库按「表头仓库」查 /app-common/inventory-item-info未选仓库会查不到库存并误报「仓库数量为0」
headerLocationId: [{ required: true, message: '请先选择仓库(按仓库查询可领用库存)', trigger: 'change' }],
}, },
tableRules: { tableRules: {
name: [{ required: true, message: '项目不能为空', trigger: 'change' }], name: [{ required: true, message: '项目不能为空', trigger: 'change' }],
@@ -988,10 +1033,9 @@ function selectRow(rowValue, index) {
form.purchaseinventoryList[index].unitList = rowValue.unitList[0]; form.purchaseinventoryList[index].unitList = rowValue.unitList[0];
form.purchaseinventoryList[index].lotNumber = rowValue.lotNumber; form.purchaseinventoryList[index].lotNumber = rowValue.lotNumber;
form.purchaseinventoryList[index].ybNo = rowValue.ybNo; form.purchaseinventoryList[index].ybNo = rowValue.ybNo;
// #439 fix: 不清空sourceLocationId保留handleAddRow设置的仓库ID // 出库仓库:优先表头当前所选仓库(避免先选药后选仓时行上一直为空)
if (!form.purchaseinventoryList[index].sourceLocationId) { form.purchaseinventoryList[index].sourceLocationId =
form.purchaseinventoryList[index].sourceLocationId = receiptHeaderForm.headerLocationId || ''; receiptHeaderForm.headerLocationId || form.purchaseinventoryList[index].sourceLocationId || '';
}
getPharmacyCabinetList().then((res) => { getPharmacyCabinetList().then((res) => {
purposeTypeListOptions.value = res.data; purposeTypeListOptions.value = res.data;
handleLocationClick(1, rowValue, index) handleLocationClick(1, rowValue, index)
@@ -1004,78 +1048,127 @@ function selectRow(rowValue, index) {
}); });
} }
// 选择仓库 /** 多条库存记录时取可领数量最大的一条(避免仅取 res.data[0] 恰好为 0 */
function handleLocationClick(item, row, index) { function pickBestOrgQuantityRow(list) {
getCount({ if (!Array.isArray(list) || list.length === 0) return null;
itemId: form.purchaseinventoryList[index].itemId, return list.reduce((best, cur) => {
orgLocationId: form.purchaseinventoryList[index].sourceLocationId, const cq = Number(cur?.orgQuantity ?? 0);
// objLocationId:purposeLocationId, const bq = Number(best?.orgQuantity ?? 0);
lotNumber: form.purchaseinventoryList[index].lotNumber, return cq > bq ? cur : best;
}).then((res) => { });
if (res.data && res.data.length > 0) { }
form.purchaseinventoryList[index].itemTable = res.data[0].itemTable || '';
form.purchaseinventoryList[index].totalQuantity = res.data[0].orgQuantity || 0;
if (res.data[0].price) { /** 表头「仓库」变化:同步每行 sourceLocationId 并重新拉库存(修复先选药品后选仓库行上仍无仓库 ID */
form.purchaseinventoryList[index].price = res.data[0].price.toFixed(4); function onHeaderWarehouseChange() {
} else { const hid = receiptHeaderForm.headerLocationId;
form.purchaseinventoryList[index].price = 0; form.purchaseinventoryList.forEach((r) => {
r.sourceLocationId = hid || '';
});
form.purchaseinventoryList.forEach((r, idx) => {
if (hid && r.itemId) {
handleLocationClick(1, {}, idx);
} }
// 获取供应商id });
form.purchaseinventoryList[index].supplierId = res.data[0].supplierId || '';
// 生产日期
form.purchaseinventoryList[index].startTime = res.data[0].productionDate;
// 有效期
form.purchaseinventoryList[index].endTime = res.data[0].expirationDate;
form.purchaseinventoryList[index].unitCode =
form.purchaseinventoryList[index].unitList.minUnitCode;
form.purchaseinventoryList[index].unitCode_dictText =
form.purchaseinventoryList[index].unitList.minUnitCode_dictText;
// 单价 大单位单价
console.log(
form.purchaseinventoryList[index].unitCode ==
form.purchaseinventoryList[index].unitList.minUnitCode,
1212121
);
if (
form.purchaseinventoryList[index].unitCode ==
form.purchaseinventoryList[index].unitList.minUnitCode
) {
form.purchaseinventoryList[index].price =
res.data[0].price / form.purchaseinventoryList[index].partPercent || '';
form.purchaseinventoryList[index].price =
form.purchaseinventoryList[index].price.toFixed(4);
// parseFloat(form.purchaseinventoryList[index].price.toFixed(4))
} else {
console.log(
form.purchaseinventoryList[index].price > 1,
1212,
form.purchaseinventoryList[index].price
);
if (form.purchaseinventoryList[index].price > 1) {
form.purchaseinventoryList[index].price =
form.purchaseinventoryList[index].price.toFixed(4);
} }
}
if (form.purchaseinventoryList[index].totalQuantity == 0) { // 选择仓库 / 选药品后拉取该仓库存
proxy.$message.warning('仓库数量为0无法调用'); function handleLocationClick(item, row, index) {
const r = form.purchaseinventoryList[index];
let orgLocationId = r.sourceLocationId || receiptHeaderForm.headerLocationId || '';
if (!orgLocationId) {
proxy.$message.warning('请先在表头选择「仓库」。库存按仓库维度查询,未选仓库无法匹配您看到的总库存。');
r.totalQuantity = 0;
r.price = 0;
return; return;
} }
} else { if (!r.sourceLocationId) {
form.purchaseinventoryList[index].totalQuantity = 0; r.sourceLocationId = orgLocationId;
form.purchaseinventoryList[index].price = 0;
// if(form.purchaseinventoryList[index].totalQuantity==0){
proxy.$message.warning('仓库数量为0无法调用');
// }
} }
}).catch(() => {
form.purchaseinventoryList[index].totalQuantity = 0; const lotTrimmed =
form.purchaseinventoryList[index].price = 0; r.lotNumber != null && String(r.lotNumber).trim() !== '' ? String(r.lotNumber).trim() : null;
});
const runGet = (withLot) => {
const params = { itemId: r.itemId, orgLocationId };
if (withLot && lotTrimmed) {
params.lotNumber = lotTrimmed;
}
return getCount(params);
};
const applyFromDto = (d, syncLotFromPick) => {
if (syncLotFromPick && d.lotNumber != null && d.lotNumber !== '') {
r.lotNumber = d.lotNumber;
}
r.itemTable = d.itemTable || '';
r.totalQuantity = d.orgQuantity || 0;
if (d.price) {
r.price = d.price.toFixed(4);
} else {
r.price = 0;
}
r.supplierId = d.supplierId || '';
r.startTime = toIssueDateTimeStr(d.productionDate) || '';
r.endTime = toIssueDateTimeStr(d.expirationDate) || '';
r.unitCode = r.unitList.minUnitCode;
r.unitCode_dictText = r.unitList.minUnitCode_dictText;
if (r.unitCode == r.unitList.minUnitCode) {
r.price = d.price / r.partPercent || '';
r.price = r.price.toFixed(4);
} else if (r.price > 1) {
r.price = r.price.toFixed(4);
}
};
const persistStore = () => {
store.setCurrentDataLYCK({ store.setCurrentDataLYCK({
purchaseinventoryList: form.purchaseinventoryList, purchaseinventoryList: form.purchaseinventoryList,
receiptHeaderForm: receiptHeaderForm, receiptHeaderForm: receiptHeaderForm,
}); });
};
runGet(true)
.then((res) => {
const list = res.data || [];
const d = pickBestOrgQuantityRow(list);
// 严格批号查询有库存orgQuantity > 0
if (d && Number(d.orgQuantity ?? 0) > 0) {
applyFromDto(d, false);
persistStore();
return;
}
// 严格查询无库存或数量为0 → 回退到非严格查询(查同仓库其他批号)
if (lotTrimmed) {
return runGet(false).then((res2) => {
const list2 = res2.data || [];
const d2 = pickBestOrgQuantityRow(list2);
if (d2 && Number(d2.orgQuantity ?? 0) > 0) {
applyFromDto(d2, true);
proxy.$message.info(
'所选批号在本仓库无对应库存或批号不一致,已按仓库实物回显批号与可领数量,请核对。'
);
} else {
r.totalQuantity = 0;
r.price = 0;
proxy.$message.warning('仓库数量为0无法调用');
}
persistStore();
});
}
// 没有指定批号,直接提示
r.totalQuantity = 0;
r.price = 0;
proxy.$message.warning('仓库数量为0无法调用');
persistStore();
})
.catch(() => {
r.totalQuantity = 0;
r.price = 0;
persistStore();
});
} }
// 切换仓库类型获取药房/药库列表 // 切换仓库类型获取药房/药库列表
// function handleChangeLocationType(value) { // function handleChangeLocationType(value) {
@@ -1232,20 +1325,19 @@ function getMaxCounts(row, index, counts) {
} }
// 计算总价 // 计算总价
function handleTotalPrice(index) { function handleTotalPrice(index) {
form.purchaseinventoryList[index].olditemQuantity = const r = form.purchaseinventoryList[index];
form.purchaseinventoryList[index].itemQuantity * row.partPercent; r.olditemQuantity = r.itemQuantity * (r.partPercent ?? 1);
form.purchaseinventoryList[index].itemMaxQuantity = r.itemMaxQuantity = r.itemQuantity;
form.purchaseinventoryList[index].itemQuantity; let purchaseItem = r;
let purchaseItem = form.purchaseinventoryList[index];
if (purchaseItem.price > 0 && purchaseItem.itemQuantity > 0) { if (purchaseItem.price > 0 && purchaseItem.itemQuantity > 0) {
form.purchaseinventoryList[index].totalPrice = purchaseItem.price * purchaseItem.itemQuantity; r.totalPrice = purchaseItem.price * purchaseItem.itemQuantity;
form.purchaseinventoryList[index].totalPrice = r.totalPrice = r.totalPrice.toFixed(4);
form.purchaseinventoryList[index].totalPrice.toFixed(4);
// parseFloat(form.purchaseinventoryList[index].totalPrice.toFixed(4))
} }
if (form.purchaseinventoryList[index].itemQuantity == 0) { if (r.itemQuantity == 0) {
form.purchaseinventoryList[index].totalPrice = 0; r.totalPrice = 0;
} }
const qtyErr = validateRequisitionQtyVsStock(r, index + 1);
r.error = !!qtyErr;
store.setCurrentDataLYCK({ store.setCurrentDataLYCK({
purchaseinventoryList: form.purchaseinventoryList, purchaseinventoryList: form.purchaseinventoryList,
receiptHeaderForm: receiptHeaderForm, receiptHeaderForm: receiptHeaderForm,
@@ -1254,6 +1346,15 @@ function handleTotalPrice(index) {
// 保存 // 保存
function handleSave(row, index) { function handleSave(row, index) {
rowList.value = []; rowList.value = [];
for (let i = 0; i < form.purchaseinventoryList.length; i++) {
const line = form.purchaseinventoryList[i];
if (!line) continue;
const err = validateRequisitionQtyVsStock(line, i + 1);
if (err) {
proxy.$message.warning(err);
return;
}
}
form.purchaseinventoryList.map((row, index) => { form.purchaseinventoryList.map((row, index) => {
if (row) { if (row) {
// 触发校验 // 触发校验
@@ -1301,7 +1402,13 @@ function handleSave(row, index) {
}); });
} }
function addTransferProducts(rowList) { function addTransferProducts(rowList) {
addTransferProduct(JSON.parse(JSON.stringify(rowList))).then((res) => { const payload = (Array.isArray(rowList) ? rowList : []).map((item) => ({
...item,
startTime: toIssueDateTimeStr(item.startTime),
endTime: toIssueDateTimeStr(item.endTime),
occurrenceTime: toIssueDateTimeStr(item.occurrenceTime) ?? item.occurrenceTime,
}));
addTransferProduct(JSON.parse(JSON.stringify(payload))).then((res) => {
// 当前行没有id视为首次新增 // 当前行没有id视为首次新增
// if (!row.id) { // if (!row.id) {
// data.isAdding = false; // 允许新增下一行 // data.isAdding = false; // 允许新增下一行
@@ -1593,6 +1700,17 @@ const exportRequiredParams = ref({
pageSize: 10, pageSize: 10,
busNo: route.query.supplyBusNo busNo: route.query.supplyBusNo
}); });
// 追溯码对话框提交处理
function submit(traceNoData) {
if (currentIndex.value >= 0 && form.purchaseinventoryList[currentIndex.value]) {
form.purchaseinventoryList[currentIndex.value].traceNo = traceNoData.traceNo;
form.purchaseinventoryList[currentIndex.value].ybNo = traceNoData.ybNo;
proxy.$message.success('追溯码保存成功');
}
openTraceNoDialog.value = false;
}
function handleExport() { function handleExport() {
proxy.downloadGet( proxy.downloadGet(
'/issue-manage/requisition/excel-out', '/issue-manage/requisition/excel-out',

View File

@@ -76,7 +76,7 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
<el-table v-loading="loading" :data="surgeryList" row-key="id" :row-class-name="getRowClassName"> <el-table v-loading="loading" :data="surgeryList" row-key="id" :row-class-name="getRowClassName" highlight-current-row @current-change="handleCurrentChange">
<!-- 申请日期datetime - 2025-09-19 14:15:00 - 不可操作 --> <!-- 申请日期datetime - 2025-09-19 14:15:00 - 不可操作 -->
<el-table-column label="申请日期" align="center" prop="createTime" width="160"> <el-table-column label="申请日期" align="center" prop="createTime" width="160">
<template #default="scope"> <template #default="scope">
@@ -1405,6 +1405,12 @@ function getRowClassName({ row }) {
return '' return ''
} }
// 当前选中行(高亮)
const currentRow = ref(null)
function handleCurrentChange(row) {
currentRow.value = row
}
// 时间格式化函数 // 时间格式化函数
function parseTime(time, pattern) { function parseTime(time, pattern) {
if (!time) return '' if (!time) return ''

View File

@@ -803,7 +803,7 @@
</el-dialog> </el-dialog>
<!-- 手术计费弹窗 --> <!-- 手术计费弹窗 -->
<el-dialog :title="chargeDialogTitle" v-model="showChargeDialog" width="1400px" @close="closeChargeDialog" append-to-body> <el-dialog :title="chargeDialogTitle" v-model="showChargeDialog" width="1400px" @close="closeChargeDialog" append-to-body destroy-on-close>
<div style="display: flex; justify-content: space-between; height: 80vh"> <div style="display: flex; justify-content: space-between; height: 80vh">
<div style="width: 100%; border: 1px solid #eee; position: relative"> <div style="width: 100%; border: 1px solid #eee; position: relative">
<div style="padding: 10px; border: 1px solid #eee; height: 50px; border-left: 0"> <div style="padding: 10px; border: 1px solid #eee; height: 50px; border-left: 0">
@@ -829,7 +829,7 @@
</el-descriptions> </el-descriptions>
</div> </div>
<div style="padding: 10px"> <div style="padding: 10px">
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef" <prescriptionlist :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1" :generateSourceEnum="1"
:sourceBillNo="chargePatientInfo.sourceBillNo" /> :sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div> <div class="overlay" v-if="disabled"></div>
@@ -881,16 +881,13 @@ import {
deleteSurgerySchedule, deleteSurgerySchedule,
getSurgeryScheduleDetail getSurgeryScheduleDetail
} from '@/api/surgicalschedule' } from '@/api/surgicalschedule'
import { listUser } from '@/api/system/user'
import { deptTreeSelect } from '@/api/system/user'
import { listOperatingRoom } from '@/api/operatingroom'
import { getSurgeryPage} from '@/views/inpatientDoctor/home/components/applicationShow/api.js' import { getSurgeryPage} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getContract } from '@/views/inpatientDoctor/home/components/api.js' import { getContract } from '@/views/inpatientDoctor/home/components/api.js'
import request from '@/utils/request' import request from '@/utils/request'
import SurgeryCharge from '../charge/surgerycharge/index.vue' import SurgeryCharge from '../charge/surgerycharge/index.vue'
import TemporaryMedical from './temporaryMedical.vue' import TemporaryMedical from './temporaryMedical.vue'
// 静默获取卫生机构列表(跳过拦截器错误提示,手术室护士等角色可能无此权限) // 静默获取字典列表(跳过拦截器错误提示,手术室护士等角色可能无此权限)
function getTenantPageSilent(query) { function getTenantPageSilent(query) {
return request({ return request({
url: '/system/tenant/page', url: '/system/tenant/page',
@@ -900,6 +897,36 @@ function getTenantPageSilent(query) {
}) })
} }
// 静默获取科室树(跳过拦截器错误提示)
function deptTreeSelectSilent(params = {}) {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: { typeEnum: 2, ...params },
skipErrorMsg: true
})
}
// 静默获取用户列表(跳过拦截器错误提示)
function listUserSilent(query) {
return request({
url: '/base-data-manage/practitioner/user-practitioner-page',
method: 'get',
params: query,
skipErrorMsg: true
})
}
// 静默获取手术室列表(跳过拦截器错误提示)
function listOperatingRoomSilent(query) {
return request({
url: '/base-data-manage/operating-room/list',
method: 'get',
params: query,
skipErrorMsg: true
})
}
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(true) const loading = ref(true)
@@ -939,7 +966,7 @@ const form = reactive({
allergyRemark: undefined, allergyRemark: undefined,
surgeryNature: undefined, surgeryNature: undefined,
surgerySite: undefined, surgerySite: undefined,
incisionLevel: undefined, incisionType: undefined,
surgeryLevel: undefined, surgeryLevel: undefined,
admissionTime: undefined, admissionTime: undefined,
@@ -1148,7 +1175,7 @@ function loadOrgList() {
// 加载科室列表 // 加载科室列表
function loadDeptList() { function loadDeptList() {
deptTreeSelect() deptTreeSelectSilent()
.then(res => { .then(res => {
if (res.code === 200) { if (res.code === 200) {
const tree = res.data?.records || res.data || [] const tree = res.data?.records || res.data || []
@@ -1168,7 +1195,7 @@ function loadDeptList() {
// 加载医生列表 // 加载医生列表
function loadDoctorList() { function loadDoctorList() {
listUser({ pageNo: 1, pageSize: 1000 }) listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => { .then(res => {
if (res.code === 200) { if (res.code === 200) {
const records = res.data?.records || [] const records = res.data?.records || []
@@ -1188,7 +1215,7 @@ function loadDoctorList() {
// 加载护士列表 // 加载护士列表
function loadNurseList() { function loadNurseList() {
listUser({ pageNo: 1, pageSize: 1000 }) listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => { .then(res => {
if (res.code === 200) { if (res.code === 200) {
const records = res.data?.records || [] const records = res.data?.records || []
@@ -1208,7 +1235,7 @@ function loadNurseList() {
// 加载手术室列表 // 加载手术室列表
function loadOperatingRoomList() { function loadOperatingRoomList() {
listOperatingRoom({ pageNo: 1, pageSize: 1000, statusEnum: 1 }) listOperatingRoomSilent({ pageNo: 1, pageSize: 1000, statusEnum: 1 })
.then(res => { .then(res => {
if (res.code === 200) { if (res.code === 200) {
const records = res.data?.records || [] const records = res.data?.records || []
@@ -1429,17 +1456,18 @@ async function handleChargeCharge(row) {
} }
// 关闭计费弹窗 // 关闭计费弹窗
function closeChargeDialog() { async function closeChargeDialog() {
// 先关闭 prescriptionlist 内所有已打开的项目字典 popover // 先关闭 prescriptionlist 内所有已打开的项目字典 popover
if (prescriptionRef.value && prescriptionRef.value.closeAllPopovers) { if (prescriptionRef.value && prescriptionRef.value.closeAllPopovers) {
prescriptionRef.value.closeAllPopovers() prescriptionRef.value.closeAllPopovers()
} }
// 等 Vue 完成 DOM 更新后再关闭弹窗,确保 popover 先消失 // 等 Vue 完成 popover 可见性更新的 DOM 操作,
nextTick(() => { // 因为 el-popover 通过 teleport 渲染在 body 上,需要在 dialog 卸载前完成清理
await nextTick()
// 清空数据,避免下次打开时使用缓存
showChargeDialog.value = false showChargeDialog.value = false
chargePatientInfo.value = {} chargePatientInfo.value = {}
chargeSurgeryInfo.value = {} chargeSurgeryInfo.value = {}
})
} }
// 🔧 新增:标志位,用于区分是"打开"还是"刷新" // 🔧 新增:标志位,用于区分是"打开"还是"刷新"
@@ -1500,11 +1528,15 @@ function handleMedicalAdvice(row) {
// 先清空旧数据 // 先清空旧数据
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
temporaryAdvices.value = [] temporaryAdvices.value = []
temporarySigned.value = false // 🔧 重置签名状态 // 🔧 修复 Bug #446: 如果是同一 encounter 且已有提交的医嘱(有 requestId保留签名状态
const hasSubmittedAdvices = temporaryAdvices.value.length > 0 &&
temporaryAdvices.value[0]?.originalMedicine?.encounterId === row.visitId &&
temporaryAdvices.value.some(a => a.originalMedicine?.requestId);
temporarySigned.value = hasSubmittedAdvices; // 修复:根据已有数据状态设置,而非盲目重置
temporaryMedicalLoading.value = true // 🔧 新增:开始加载 temporaryMedicalLoading.value = true // 🔧 新增:开始加载
// 调用计费接口获取数据 // 调用计费接口获取数据(使用手术计费来源参数,匹配 surgery billing 创建的记录)
getPrescriptionList(row.visitId).then((res) => { getPrescriptionList(row.visitId, 6, row.operCode).then((res) => {
console.log('=== 拉取计费数据返回结果 ===', res) console.log('=== 拉取计费数据返回结果 ===', res)
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目 // 🔧 修复:显示所有药品请求数据,不管有没有计费项目
@@ -1515,9 +1547,13 @@ function handleMedicalAdvice(row) {
const filteredItems = res.data.filter(item => { const filteredItems = res.data.filter(item => {
// 匹配 encounterId // 匹配 encounterId
if (item.encounterId !== row.visitId) return false; if (item.encounterId !== row.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目 // 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name; const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false; if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表)
if (item.requestId) return false;
// 根据药品请求ID去重避免重复显示 // 根据药品请求ID去重避免重复显示
const itemId = item.requestId || item.id; const itemId = item.requestId || item.id;
if (itemId && seenIds.has(itemId)) return false; if (itemId && seenIds.has(itemId)) return false;
@@ -1705,15 +1741,39 @@ function handleTemporaryMedicalSubmit(data) {
} }
}) })
// 同步更新计费药品列表:移除已生成医嘱的项目,避免数据重复显示 // 🔧 修复 Bug #445: 使用稳定可靠的字段组合匹配已提交项目,从已生成列表中剔除待生成项
const submittedIds = new Set( // 匹配键:优先使用 chargeItemId后端费用项目ID最可靠其次使用 名称+规格+数量 组合
(data.temporaryAdvices || []).map(a => a.originalMedicine?.requestId || a.originalMedicine?.chargeItemId).filter(Boolean) const submittedKeys = new Set()
) const submittedChargeIds = new Set()
if (submittedIds.size > 0) {
temporaryBillingMedicines.value = (data.billingMedicines || []).filter( ;(data.temporaryAdvices || []).forEach(a => {
m => !submittedIds.has(m.requestId || m.chargeItemId) const om = a.originalMedicine || {}
) // 收集 chargeItemId最可靠的匹配标识
if (om.chargeItemId) {
submittedChargeIds.add(om.chargeItemId)
}
// 构建名称+规格+数量的匹配键(用于无 chargeItemId 的兜底匹配)
// 注意originalMedicine 中的名称字段是 adviceName来自 billingMedicines.map 时的字段)
const name = om.medicineName || om.adviceName || om.advice_name || a.adviceName || ''
const spec = om.specification || om.volume || ''
const qty = om.quantity || 0
if (name) {
submittedKeys.add(`${name}|||${spec}|||${qty}`)
}
})
if (submittedChargeIds.size > 0 || submittedKeys.size > 0) {
temporaryBillingMedicines.value = (temporaryBillingMedicines.value || []).filter(m => {
// 优先用 chargeItemId 匹配
if (m.chargeItemId && submittedChargeIds.has(m.chargeItemId)) {
return false
}
// 兜底用 名称+规格+数量 匹配
const key = `${m.medicineName || m.adviceName || ''}|||${m.specification || m.volume || ''}|||${m.quantity || 0}`
return !submittedKeys.has(key)
})
} else { } else {
// 如果没有任何匹配标识,清空待生成列表(保守策略:认为所有项目都已提交)
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
} }
@@ -1773,13 +1833,19 @@ function handleQuoteBilling() {
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
temporaryAdvices.value = [] temporaryAdvices.value = []
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目 // 🔧 修复 Bug #445: 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中)
const filteredItems = res.data.filter(item => { const filteredItems = res.data.filter(item => {
// 匹配 encounterId // 匹配 encounterId
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false; if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目 // 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name; const medicineName = item.adviceName || item.advice_name;
return medicineName && medicineName.trim() !== ''; if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId
if (item.requestId) return false;
return true;
}) })
// 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死 // 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死
const maxItems = 100 const maxItems = 100
@@ -1996,7 +2062,12 @@ function resetForm() {
function submitForm() { function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => { proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) { if (valid) {
const submitData = { ...form, orgId: userStore.orgId } const submitData = {
...form,
orgId: userStore.orgId,
incisionLevel: form.incisionType
}
delete submitData.incisionType
if (!form.scheduleId) { if (!form.scheduleId) {
// 新增手术安排 // 新增手术安排
addSurgerySchedule(submitData).then((res) => { addSurgerySchedule(submitData).then((res) => {

View File

@@ -147,9 +147,10 @@
<el-button class="cancel-btn" @click="handleCancel">取消</el-button> <el-button class="cancel-btn" @click="handleCancel">取消</el-button>
<el-button <el-button
class="sign-btn" class="sign-btn"
:disabled="allItemsSubmitted"
@click="handleSignAndSubmit" @click="handleSignAndSubmit"
> >
{{ isSigned ? '提交医嘱' : '一键签名并生成医嘱' }} {{ allItemsSubmitted ? '已签发' : (isSigned ? '提交医嘱' : '一键签名并生成医嘱') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -310,8 +311,21 @@ const getMethodCodeDict = computed(() => {
return dict return dict
}) })
// 🔧 修复 Bug #446: 检查计费药品是否已全部提交(有 requestId用于区分"首次签名"和"已提交重开"
const allItemsSubmitted = computed(() => {
const meds = props.billingMedicines || []
return meds.length > 0 && meds.every(m => m.requestId)
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化 // 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp) const isSigned = ref(props.isSignedProp)
// 🔧 修复 Bug #446: 同步父组件 isSignedProp 的变化到本地 isSigned
// ref(props.isSignedProp) 只在初始化时读取一次,父组件后续更新不会自动同步
watch(() => props.isSignedProp, (newVal) => {
isSigned.value = newVal
})
const signatureTime = ref('') const signatureTime = ref('')
const showSignDialog = ref(false) const showSignDialog = ref(false)
const signPassword = ref('') const signPassword = ref('')