Compare commits

...

57 Commits

Author SHA1 Message Date
关羽
6e0e78cd8d Fix Bug #405: 住院医生工作站:临床医嘱保存成功后,医嘱条目仍处于可编辑状态(未锁定展示)
- handleSaveSign: 新增行点击确定后调用savePrescription持久化到后端,而非仅设置isEdit=false后调用handleAddPrescription
- handleSaveBatch: 修复nextId.value == 1 为赋值操作(=)
- handleSaveBatch: catch块增加错误提示,保存失败时用户可感知

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:42:33 +08:00
关羽
41562c5bbd Fix Bug #402: 住院医生站诊断录入:点击保存诊断后,列表出现重复记录且部分条目元数据缺失
根因分析:
1. 后端 deleteTcmByEncounterId SQL 过滤条件错误(tcm_flag=0 应为 tcm_flag=1),导致中医诊断记录无法被正确清理
2. 前端 getList() 从服务器加载数据后,未补充缺失的 diagnosisDoctor 和 diagnosisTime 字段
3. 前端 handleSaveDiagnosis() 保存成功后直接更新本地数据而非从服务器刷新,导致数据不一致和重复记录
4. 前端新增诊断时缺少 classification、onsetDate、longTermFlag 等必要字段

修复内容:
- 后端:修复 EncounterDiagnosisMapper.xml 中 deleteTcmByEncounterId 的 tcm_flag 过滤条件
- 前端:getList() 增加缺失字段默认值填充逻辑
- 前端:handleSaveDiagnosis() 改为 async/await,保存成功后调用 getList() 从服务器刷新
- 前端:addDiagnosisItem()/handleImport()/handleNodeClick() 补充完整字段

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:35:55 +08:00
关羽
00ff4158a1 Fix Bug #402: 住院医生站诊断录入:点击保存诊断后,列表出现重复记录且部分条目元数据缺失
根因分析:
1. 前端"保存诊断"按钮无防重复点击保护,快速点击两次导致并发请求产生重复记录
2. 前端日期使用 toLocaleString('zh-CN') 格式与后端 @JsonFormat(pattern="yyyy/M/d HH:mm:ss") 不匹配,导致诊断时间解析失败为null
3. EncounterDiagnosisMapper.xml 中 deleteTcmByEncounterId 使用 tcm_flag=0(应为1),导致中医诊断删除逻辑错误

修复内容:
- 前端:添加 saveLoading 状态防止重复点击
- 前端:统一使用 formatDateStr(new Date(), 'YYYY/M/D HH:mm:ss') 确保与后端日期格式一致
- 后端:修正 deleteTcmByEncounterId 的 tcm_flag 条件为 1

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:25:48 +08:00
关羽
f7a3f658fb Merge remote-tracking branch 'origin/关羽' - resolve conflicts 2026-05-13 09:39:03 +08:00
关羽
dd36dc49ea Fix Bug #400: 门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
根因:完诊时使用 triageQueueItemService.updateById(entity) 更新队列状态,
实体序列化过程中 status 字段为 Integer 类型但数据库列为 VARCHAR(50),
可能导致 MyBatis-Plus 的 UPDATE SQL 未正确包含 status 字段。

修复方案:改用 LambdaUpdateWrapper 直接构造 UPDATE SQL,确保 status 字段被正确更新为 30。
同时增加成功/失败日志,便于后续排查。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:15:52 +08:00
华佗
21e68da15e Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
根据单据状态动态显示操作按钮:
- 待签发(状态0):显示修改、删除、详情
- 已签发(状态1):显示撤回、详情
- 其他状态:仅显示详情

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:18:58 +08:00
331fc532fc bug 486 [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选“医嘱类型”时检索结果为空 删去了搜索条件的默认值 2026-05-12 12:18:58 +08:00
关羽
dafa5961c4 Fix Bug #489: 护士校对界面重复待校对记录 - SQL JOIN 乘法修复
根因: AdviceProcessAppMapper.xml 中 LEFT JOIN med_medication_dispense
直接关联时,一条药品医嘱若有多条发药记录会产生多行结果(SQL 笛卡尔积),
导致护士校对界面显示重复的待校对记录。

修复: 将直接 JOIN 改为 ROW_NUMBER() 子查询,每个 med_req_id 只取最新
一条发药记录的状态,避免行倍增。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:30:17 +08:00
赵云
f223192ec5 Fix Bug #502: 【住院护士站-汇总发药申请】顶部医嘱类型(长期/临时)过滤按钮点击无响应
根因:父组件 index.vue 中 therapyEnum 变量未声明为 ref,且未通过 props 传递给子组件 prescriptionList.vue,
导致点击"长期/临时"按钮时数据流断裂,子组件 API 调用始终使用本地未变化的 therapyEnum 值。

修复:
1. index.vue 新增 const therapyEnum = ref(undefined)
2. index.vue 新增 handleTherapyChange() 调用 handleGetPrescription() 刷新列表
3. index.vue 将 therapyEnum 作为 prop 传入 PrescriptionList
4. prescriptionList.vue 将本地 therapyEnum ref 改为 props 接收
2026-05-11 15:30:17 +08:00
赵云
c7956a116b fix: 恢复 Bug #497 的后端修改 + 数据库字段同步 (ALTER TABLE doc_request_form ADD COLUMN status) 2026-05-11 15:30:17 +08:00
关羽
845354e863 fix: 还原 Bug #443/#475/#477/#486/#497 引入的 getRequestForm 编译错误 2026-05-11 15:30:17 +08:00
赵云
56ea4b6af1 fix: 完整回退 Bug #497 的后端修改(SQL/Java接口/Impl/Dto) 2026-05-11 15:30:17 +08:00
赵云
703e9ead43 Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. 非耗材类医嘱执行报"耗材库存"错误: handleExecute 中无条件调用 lotNumberMatch,
   后端会校验该就诊下所有待发放耗材库存,即使当前执行的是口服药等非耗材类医嘱
2. 全选联动异常: msgSuccess 在 handleGetPrescription 之前执行,数据刷新后
   defaultSelectAllRows 重新选中所有行,用户关闭弹窗后看到全选效果

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:30:17 +08:00
赵云
a43f98cc5a fix: 完全回退 Bug #497 引入的 drf.status 字段(数据库不存在) 2026-05-11 15:30:17 +08:00
wangjian963
9215c288d3 506 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
CommonConstants.AppointmentOrderStatus 常量 → OrderStatus 枚举重构
   新增枚举:0=患者取消 / 1=有效 / 2=系统取消 / 3=已完成
   退号流程加乐观锁防并发,slot 状态改回待约,退号日志独立事务 修复 XML 中 Integer 比较用字符串的问题
Bug #411 — 诊室过滤栏从科室下拉框改为诊室按钮组
2026-05-11 15:30:17 +08:00
关羽
777ba71c7d Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:handleFocus/handleChange 中 categoryCode 的计算逻辑错误。当新增行未选择
医嘱类型时(row.adviceType 为 undefined),代码回退到 adviceQueryParams 的默认值并
匹配到具体药品分类(如西药 categoryCode='2'),导致搜索被限制在单一分类而非全局药库。

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:07:50 +08:00
关羽
c3dfd3eb21 Fix Bug #499: 【住院医生工作站-检查申请】检查申请列表缺失查询过滤功能,不符合临床高效检索要求
补充修复:
1. 后端 get-inspection 接口新增 keyword 关键字参数(检验申请关键字搜索)
2. 新增 deleteRequestForm 接口(仅待签发状态可删除)
3. 新增 withdrawRequestForm 接口(已签发状态撤回至待签发)
4. RequestForm 实体新增 status 字段(用于撤回方法状态校验)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:07:50 +08:00
关羽
8093f8acda Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因:handleViewDetail 为同步方法,点击详情时 getLocationInfo 尚未返回,
orgOptions 为空导致 recursionFun 无法将 targetDepartment ID 解析为科室名称。

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:01:15 +08:00
关羽
5af86494dd Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
根因分析:
1. el-collapse accordion 模式下快速切换分类时,连续的折叠/展开动画重叠,
   Element Plus 在动画过程中重新计算面板高度,导致高度跳变和白屏闪烁
2. 折叠容器缺少 overflow:hidden,动画过渡期间内容溢出造成闪烁

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:49:33 +08:00
28 changed files with 484 additions and 140 deletions

View File

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

View File

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

View File

@@ -1987,13 +1987,25 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
*/ */
@Override @Override
public R<?> getRequestBaseInfo(Long encounterId) { public R<?> getRequestBaseInfo(Long encounterId) {
return this.getRequestBaseInfo(encounterId, null, null);
}
@Override
public R<?> getRequestBaseInfo(Long encounterId, Integer generateSourceEnum, String sourceBillNo) {
// 当前账号的参与者id // 当前账号的参与者id
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
// 未指定generateSourceEnum时默认只查询医生开立的医嘱
int sourceEnum = (generateSourceEnum != null) ? generateSourceEnum : GenerateSource.DOCTOR_PRESCRIPTION.getValue();
// 医嘱请求数据 // 医嘱请求数据
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, null, List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, null,
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(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue()); sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时只保留诊疗请求3/6过滤掉药品1和耗材2
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& (dto.getAdviceType() == 1 || dto.getAdviceType() == 2));
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) { for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态 // 请求状态
requestBaseDto requestBaseDto
@@ -2114,7 +2126,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, patientId, List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, patientId,
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.YES.getCode(), CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.YES.getCode(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue()); GenerateSource.DOCTOR_PRESCRIPTION.getValue(), null);
for (RequestBaseDto requestBaseDto : requestBaseInfo) { for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态 // 请求状态
requestBaseDto requestBaseDto

View File

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

View File

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

View File

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

View File

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

@@ -31,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -91,8 +92,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.fail("无待签发的医嘱,该申请单不可编辑"); return R.fail("无待签发的医嘱,该申请单不可编辑");
} }
} else { } else {
// 诊疗处方 // 检查申请单号JC检查+ Z住院标识+ yyMMdd日期+ 5位顺序
prescriptionNo = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_PSYCHOTROPIC_NO.getPrefix(), 8); String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
int seq = assignSeqUtil.getSeqNoByDay(AssignSeqEnum.CHECK_APPLY_NO.getPrefix());
prescriptionNo = "JCZ" + dateStr + String.format("%05d", seq);
} }
// 当前时间 // 当前时间
@@ -471,4 +474,68 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return requestFormManageAppMapper.getRequestFormPage(requestFormDto, page); return requestFormManageAppMapper.getRequestFormPage(requestFormDto, page);
} }
/**
* 删除申请单(仅待签发状态可删除)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> deleteRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
if (!Integer.valueOf(0).equals(requestForm.getStatus())) {
return R.fail("仅待签发状态的申请单可删除");
}
// 删除申请单
iRequestFormService.removeById(requestFormId);
// 删除关联的诊疗项目及账单
String prescriptionNo = requestForm.getPrescriptionNo();
List<Long> serviceRequestIds = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo))
.stream().map(ServiceRequest::getId).collect(Collectors.toList());
for (Long serviceRequestId : serviceRequestIds) {
iServiceRequestService.removeById(serviceRequestId);
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_SERVICE_REQUEST, serviceRequestId);
}
return R.ok(null, "删除成功");
}
/**
* 撤回申请单(已签发状态撤回至待签发)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> withdrawRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
if (!Integer.valueOf(1).equals(requestForm.getStatus())) {
return R.fail("仅已签发状态的申请单可撤回");
}
// 将申请单状态回滚至待签发
RequestForm updateForm = new RequestForm();
updateForm.setId(requestFormId);
updateForm.setStatus(0);
iRequestFormService.updateById(updateForm);
// 将关联的诊疗项目状态回滚至DRAFT
String prescriptionNo = requestForm.getPrescriptionNo();
List<ServiceRequest> serviceRequests = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
for (ServiceRequest serviceRequest : serviceRequests) {
ServiceRequest updateService = new ServiceRequest();
updateService.setId(serviceRequest.getId());
updateService.setStatusEnum(RequestStatus.DRAFT.getValue());
iServiceRequestService.updateById(updateService);
}
return R.ok(null, "撤回成功");
}
} }

View File

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

View File

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

View File

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

View File

@@ -280,9 +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 (
SELECT med_req_id, status_enum
FROM (
SELECT med_req_id, status_enum,
ROW_NUMBER() OVER (PARTITION BY med_req_id ORDER BY id DESC) AS rn
FROM med_medication_dispense
WHERE delete_flag = '0'
) t
WHERE rn = 1
) mmd
ON mmd.med_req_id = T1.id ON mmd.med_req_id = T1.id
AND mmd.delete_flag = '0'
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}

View File

@@ -161,6 +161,9 @@
<if test="requestFormDto.surgeryNo != null and requestFormDto.surgeryNo != ''"> <if test="requestFormDto.surgeryNo != null and requestFormDto.surgeryNo != ''">
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 != ''">
AND drf.type_code = #{requestFormDto.typeCode}
</if>
<if test="requestFormDto.applyTimeStart != null"> <if test="requestFormDto.applyTimeStart != null">
AND drf.create_time >= #{requestFormDto.applyTimeStart} AND drf.create_time >= #{requestFormDto.applyTimeStart}
</if> </if>

View File

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

View File

@@ -59,4 +59,9 @@ public class RequestForm extends HisBaseEntity {
*/ */
private String typeCode; private String typeCode;
/**
* 单据状态 0=待签发 1=已签发 2=已校对 3=待接收 4=已接收 5=已检查 6=已出报告 7=已作废
*/
private Integer status;
} }

View File

@@ -15,7 +15,7 @@
DELETE DELETE
FROM adm_encounter_diagnosis FROM adm_encounter_diagnosis
WHERE encounter_id = #{encounterId} WHERE encounter_id = #{encounterId}
AND tcm_flag = 0 AND tcm_flag = 1
</delete> </delete>
<select id="getEncounterDiagnosisByEncounterConDefId" <select id="getEncounterDiagnosisByEncounterConDefId"
resultType="com.openhis.administration.domain.EncounterDiagnosis"> resultType="com.openhis.administration.domain.EncounterDiagnosis">

View File

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

View File

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

View File

@@ -381,6 +381,14 @@ const props = defineProps({
activeTab: { activeTab: {
type: String, type: String,
}, },
generateSourceEnum: {
type: Number,
default: null,
},
sourceBillNo: {
type: String,
default: null,
},
}); });
const isAdding = ref(false); const isAdding = ref(false);
const isSaving = ref(false); // #437 防重复提交锁 const isSaving = ref(false); // #437 防重复提交锁
@@ -468,7 +476,11 @@ watch(
function getListInfo(addNewRow) { function getListInfo(addNewRow) {
isAdding.value = false; isAdding.value = false;
getPrescriptionList(props.patientInfo.encounterId).then((res) => { getPrescriptionList(
props.patientInfo.encounterId,
props.generateSourceEnum ?? undefined,
props.sourceBillNo ?? undefined,
).then((res) => {
// 为每行数据添加 adviceTypeValue 字段,用于类型下拉框显示 // 为每行数据添加 adviceTypeValue 字段,用于类型下拉框显示
prescriptionList.value = (res.data || []).map(item => { prescriptionList.value = (res.data || []).map(item => {
// 根据 adviceType 和 categoryCode 找到对应的 adviceTypeValue // 根据 adviceType 和 categoryCode 找到对应的 adviceTypeValue

View File

@@ -402,6 +402,21 @@
<span class="method-price">¥{{ method.packagePrice || item.price }}</span> <span class="method-price">¥{{ method.packagePrice || item.price }}</span>
</el-checkbox> </el-checkbox>
</div> </div>
<!-- 选中方法后显示对应的套餐明细 -->
<div v-if="item.selectedMethod && item.methodPackageDetails && item.methodPackageDetails.length > 0" class="method-package-details">
<div class="method-package-header">
<span class="method-package-title">套餐明细 - {{ item.selectedMethod.name }}</span>
</div>
<div v-for="detail in item.methodPackageDetails" :key="detail.id" class="method-option">
<el-checkbox v-model="detail.checked">
<span class="method-name">{{ detail.name }}</span>
<span class="method-price">数量: {{ detail.quantity }} ¥{{ detail.price }}</span>
</el-checkbox>
</div>
</div>
<div v-if="item.selectedMethod && item.methodPackageLoading" class="method-package-loading">
加载套餐明细中...
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -419,7 +434,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue'; import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import request from '@/utils/request'; import request from '@/utils/request';
import { listCheckMethod, searchCheckMethod } from '@/api/system/checkType'; import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType';
import { getEncounterDiagnosis } from '../api.js'; import { getEncounterDiagnosis } from '../api.js';
const props = defineProps({ const props = defineProps({
@@ -445,7 +460,7 @@ async function loadPackageDetails(row, treeNode, resolve) {
} }
try { try {
const res = await request({ const res = await request({
url: `/exam/package/${row.packageId}/details`, url: `/system/package/${row.packageId}/details`,
method: 'get' method: 'get'
}); });
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
@@ -474,7 +489,7 @@ async function loadPackageDetailsForItem(item) {
} }
try { try {
const res = await request({ const res = await request({
url: `/exam/package/${item.packageId}/details`, url: `/system/package/${item.packageId}/details`,
method: 'get' method: 'get'
}); });
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
@@ -682,7 +697,6 @@ async function handleCategoryExpand(cat) {
code: m.code, code: m.code,
price: m.price || 0, price: m.price || 0,
packageName: m.packageName || '', packageName: m.packageName || '',
packageId: m.packageId || null,
packagePrice: m.packagePrice || null, packagePrice: m.packagePrice || null,
serviceFee: m.serviceFee || null serviceFee: m.serviceFee || null
})); }));
@@ -993,12 +1007,15 @@ function handleRowClick(row) {
selectedItems.value = []; selectedItems.value = [];
activeDetailTab.value = 'applyForm'; activeDetailTab.value = 'applyForm';
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => { request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
const d = res.data || res; const resp = res.data || res;
if (d.data) Object.assign(form, d.data); // 保存 items 在顶层响应中,避免后面 d.data 赋值后丢失
if (d.items && Array.isArray(d.items)) { const rawItems = resp.items;
const d = resp.data || resp;
if (d) Object.assign(form, d);
if (rawItems && Array.isArray(rawItems)) {
try { try {
// 为每个项目加载检查方法 // 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(d.items.map(async m => { const itemsWithMethods = await Promise.all(rawItems.map(async m => {
const item = { const item = {
id: m.itemCode, name: m.itemName, id: m.itemCode, name: m.itemName,
price: m.itemFee || 0, quantity: 1, price: m.itemFee || 0, quantity: 1,
@@ -1026,18 +1043,12 @@ function handleRowClick(row) {
code: md.code, code: md.code,
price: m.itemFee || 0, // fallback 到已保存的价格 price: m.itemFee || 0, // fallback 到已保存的价格
packageName: md.packageName || '', packageName: md.packageName || '',
packageId: md.packageId || null,
packagePrice: md.packagePrice || null, // Bug #384修复: 套餐价格 packagePrice: md.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: md.serviceFee || null serviceFee: md.serviceFee || null
})); }));
// 如果有已保存的检查方法信息,尝试匹配 // 如果有已保存的检查方法信息,尝试匹配
if (m.checkMethodId) { if (m.checkMethodId) {
item.selectedMethod = item.methods.find(md => md.id === m.checkMethodId) || null; item.selectedMethod = item.methods.find(md => md.id === m.checkMethodId) || null;
// 从已保存的方法中获取套餐信息
if (item.selectedMethod?.packageId) {
item.isPackage = true;
item.packageId = item.selectedMethod.packageId;
}
} }
} }
} catch (err) { } catch (err) {
@@ -1098,13 +1109,6 @@ async function handleMethodSelect(checked, method, cat) {
const existingItem = selectedItems.value.find(s => s.id === targetItem.id); const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
if (existingItem) { if (existingItem) {
existingItem.selectedMethod = method; existingItem.selectedMethod = method;
// 从方法中获取套餐信息
if (method.packageId) {
existingItem.isPackage = true;
existingItem.packageId = method.packageId;
// 预加载套餐明细
loadPackageDetailsForItem(existingItem);
}
updateMethodDisplay(); updateMethodDisplay();
return; return;
} }
@@ -1119,7 +1123,7 @@ async function handleMethodSelect(checked, method, cat) {
} }
} }
const newItem = { selectedItems.value.push({
id: targetItem.id, name: targetItem.name, id: targetItem.id, name: targetItem.name,
price: targetItem.price, quantity: 1, price: targetItem.price, quantity: 1,
serviceFee: targetItem.serviceFee || 0, serviceFee: targetItem.serviceFee || 0,
@@ -1131,16 +1135,9 @@ async function handleMethodSelect(checked, method, cat) {
methods: [method], methods: [method],
selectedMethod: method, selectedMethod: method,
expanded: false, expanded: false,
// 从方法中获取套餐信息(优先级高于项目本身的 packageName isPackage: !!targetItem.packageName,
isPackage: !!method.packageId || !!targetItem.packageName, packageId: targetItem.packageId || null
packageId: method.packageId || targetItem.packageId || null });
};
selectedItems.value.push(newItem);
// 如果是套餐,预加载套餐明细
if (newItem.isPackage && newItem.packageId) {
loadPackageDetailsForItem(newItem);
}
// 自动回填执行科室 // 自动回填执行科室
if (selectedItems.value.length === 1 && cat?.performDeptName) { if (selectedItems.value.length === 1 && cat?.performDeptName) {
@@ -1185,7 +1182,6 @@ async function handleItemSelect(checked, item, cat) {
code: m.code, code: m.code,
price: m.price || item.price, // fallback 到项目价格 price: m.price || item.price, // fallback 到项目价格
packageName: m.packageName || '', packageName: m.packageName || '',
packageId: m.packageId || null,
packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格 packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格
serviceFee: m.serviceFee || null // Bug #384修复: 服务费 serviceFee: m.serviceFee || null // Bug #384修复: 服务费
})); }));
@@ -1250,11 +1246,14 @@ async function toggleItemExpand(item) {
} }
// Bug #384修复: 勾选框选择检查方法(单选逻辑) // Bug #384修复: 勾选框选择检查方法(单选逻辑)
function selectMethodCheckbox(checked, item, method) { async function selectMethodCheckbox(checked, item, method) {
if (checked) { if (checked) {
item.selectedMethod = method; item.selectedMethod = method;
// 动态加载该方法对应的套餐明细
await loadMethodPackageDetails(item, method);
} else { } else {
item.selectedMethod = null; item.selectedMethod = null;
item.methodPackageDetails = [];
} }
// 联动更新表单检查方法显示字段 // 联动更新表单检查方法显示字段
updateMethodDisplay(); updateMethodDisplay();
@@ -1265,6 +1264,50 @@ function selectMethodCheckbox(checked, item, method) {
}); });
} }
// 根据检查方法的packageName加载对应的套餐明细
async function loadMethodPackageDetails(item, method) {
item.methodPackageLoading = true;
item.methodPackageDetails = [];
try {
if (!method.packageName) {
item.methodPackageLoading = false;
return;
}
// 通过packageName查询套餐获取packageId
const pkgRes = await listCheckPackage({ packageName: method.packageName });
let packages = pkgRes?.data || [];
if (!Array.isArray(packages)) {
packages = packages.records || packages.data || [];
}
if (packages.length === 0) {
item.methodPackageLoading = false;
return;
}
const packageId = packages[0].id;
// 查询套餐明细
const detailRes = await request({
url: `/system/package/${packageId}/details`,
method: 'get'
});
if (detailRes.code === 200 && detailRes.data) {
item.methodPackageDetails = detailRes.data.map(d => ({
id: d.id,
name: d.itemName || d.name,
quantity: d.quantity || 1,
unit: d.unit || '次',
price: d.unitPrice || d.price || 0,
amount: d.amount || d.total || 0,
checked: true // 默认勾选
}));
}
} catch (err) {
console.error('加载方法套餐明细失败:', err);
item.methodPackageDetails = [];
} finally {
item.methodPackageLoading = false;
}
}
// Bug #384修复: 更新检查方法显示字段(联动) // Bug #384修复: 更新检查方法显示字段(联动)
function updateMethodDisplay() { function updateMethodDisplay() {
// 找到第一个有选中检查方法的项目 // 找到第一个有选中检查方法的项目
@@ -1670,6 +1713,29 @@ defineExpose({ getList });
margin-left: 8px; margin-left: 8px;
} }
/* 选中方法后显示的套餐明细 */
.method-package-details {
margin-top: 4px;
padding: 4px 0;
border-top: 1px dashed #dcdfe6;
}
.method-package-header {
padding: 2px 0 4px 24px;
}
.method-package-title {
font-size: 10px;
color: #909399;
font-weight: 500;
}
.method-package-loading {
padding: 4px 0 4px 24px;
font-size: 10px;
color: #c0c4cc;
}
/* 折叠组件细节 */ /* 折叠组件细节 */
:deep(.el-collapse) { :deep(.el-collapse) {
border: none; border: none;

View File

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

View File

@@ -79,7 +79,7 @@
<el-table-column prop="patientName" label="患者姓名" width="120" /> <el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" /> <el-table-column prop="name" label="申请单名称" width="140" />
<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 prop="requesterId_dictText" label="申请者" width="120" /> <el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="申请单状态" width="120" align="center"> <el-table-column label="申请单状态" width="120" align="center">
<template #default="scope"> <template #default="scope">
@@ -118,7 +118,7 @@
<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="申请者">{{

View File

@@ -169,6 +169,7 @@
<script setup> <script setup>
import {getCurrentInstance} from 'vue'; // 添加 nextTick 导入 import {getCurrentInstance} from 'vue'; // 添加 nextTick 导入
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import { formatDateStr } from '@/utils';
import { import {
delEncounterDiagnosis, delEncounterDiagnosis,
deleteDiagnosisBind, deleteDiagnosisBind,
@@ -286,10 +287,22 @@ function getList() {
if (obj.diagSrtNo == null) { if (obj.diagSrtNo == null) {
obj.diagSrtNo = '1'; obj.diagSrtNo = '1';
} }
// 补充缺失的元数据字段
if (!obj.diagnosisDoctor) {
obj.diagnosisDoctor = props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name;
}
if (!obj.diagnosisTime) {
obj.diagnosisTime = item.diagnosisTime || getCurrentDate();
}
if (!obj.classification) {
obj.classification = item.typeName === '中医诊断' ? '中医' : '西医';
}
if (obj.longTermFlag == null) {
obj.longTermFlag = 0;
}
return obj; return obj;
}); });
form.value.diagnosisList = datas; form.value.diagnosisList = datas;
// form.value.diagnosisList = res.data;
emits('diagnosisSave', false); emits('diagnosisSave', false);
} }
}); });
@@ -305,7 +318,10 @@ function getList() {
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,
diagnosisTime: new Date().toLocaleString('zh-CN') diagnosisTime: item.diagnosisTime || getCurrentDate(),
diagSrtNo: item.diagSrtNo,
classification: '中医',
longTermFlag: item.longTermFlag || 0
}); });
}); });
@@ -344,6 +360,7 @@ function handleImport() {
// 获取患者慢性病信息 // 获取患者慢性病信息
getChronicDisease({ encounterId: props.patientInfo.encounterId }).then((res) => { getChronicDisease({ encounterId: props.patientInfo.encounterId }).then((res) => {
if (res.data && res.data.length > 0) { if (res.data && res.data.length > 0) {
const currentDate = getCurrentDate();
res.data.forEach((item, index) => { res.data.forEach((item, index) => {
form.value.diagnosisList.push({ form.value.diagnosisList.push({
...item, ...item,
@@ -355,7 +372,10 @@ function handleImport() {
iptDiseTypeCode: 2, iptDiseTypeCode: 2,
diagnosisDesc: '', diagnosisDesc: '',
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name, diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: new Date().toLocaleString('zh-CN'), diagnosisTime: currentDate,
onsetDate: currentDate,
classification: '西医',
longTermFlag: 0,
//添加 patientId //添加 patientId
patientId: props.patientInfo.patientId patientId: props.patientInfo.patientId
}, },
@@ -470,6 +490,7 @@ function handleAddDiagnosis() {
* 添加诊断项 * 添加诊断项
*/ */
function addDiagnosisItem() { function addDiagnosisItem() {
const currentDate = getCurrentDate();
form.value.diagnosisList.push({ form.value.diagnosisList.push({
showPopover: false, showPopover: false,
name: undefined, name: undefined,
@@ -479,9 +500,10 @@ function addDiagnosisItem() {
iptDiseTypeCode: 2, iptDiseTypeCode: 2,
diagnosisDesc: '', diagnosisDesc: '',
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name, diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: new Date().toLocaleString('zh-CN'), diagnosisTime: currentDate,
classification: '西医',
// 新增这一行:为每个诊断项添加 patientId onsetDate: currentDate,
longTermFlag: 0,
patientId: props.patientInfo.patientId patientId: props.patientInfo.patientId
}); });
@@ -558,16 +580,7 @@ function handleMaindise(value, index) {
/** /**
* 保存诊断 * 保存诊断
*/ */
/** async function handleSaveDiagnosis() {
* 保存诊断
*/
/**
* 保存诊断
*/
/**
* 保存诊断
*/
function handleSaveDiagnosis() {
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) {
@@ -578,7 +591,7 @@ function handleSaveDiagnosis() {
break; break;
} }
} }
proxy.$refs.formRef.validate((valid) => { proxy.$refs.formRef.validate(async (valid) => {
if (valid) { if (valid) {
if (form.value.diagnosisList.length === 0) { if (form.value.diagnosisList.length === 0) {
proxy.$modal.msgWarning('诊断不能为空'); proxy.$modal.msgWarning('诊断不能为空');
@@ -591,6 +604,7 @@ function handleSaveDiagnosis() {
// 设置保存标志避免触发watch监听器 // 设置保存标志避免触发watch监听器
isSaving.value = true; isSaving.value = true;
try {
// 步骤1深拷贝并按 diagSrtNo 排序 // 步骤1深拷贝并按 diagSrtNo 排序
const sortedList = [...form.value.diagnosisList].sort((a, b) => { const sortedList = [...form.value.diagnosisList].sort((a, b) => {
const aNo = typeof a.diagSrtNo === 'number' ? a.diagSrtNo : 9999; const aNo = typeof a.diagSrtNo === 'number' ? a.diagSrtNo : 9999;
@@ -598,36 +612,43 @@ function handleSaveDiagnosis() {
return aNo - bNo; return aNo - bNo;
}); });
// 步骤2重新分配连续的序号从1开始 // 步骤2转换日期格式为后端期望的格式
sortedList.forEach((item, index) => { const diagnosisChildList = sortedList.map(item => ({
item.diagSrtNo = index + 1; // 这里是关键!把“诊断排序”改成新顺序 ...item,
}); onsetDate: item.onsetDate ? formatDateStr(item.onsetDate, 'YYYY/M/D HH:mm:ss') : null,
diagnosisTime: item.diagnosisTime ? formatDateStr(item.diagnosisTime, 'YYYY/M/D HH:mm:ss') : null
}));
// 步骤3提交排序后的数据 // 步骤3提交排序后的数据
saveDiagnosis({ const res = await saveDiagnosis({
patientId: props.patientInfo.patientId, patientId: props.patientInfo.patientId,
encounterId: props.patientInfo.encounterId, encounterId: props.patientInfo.encounterId,
diagnosisChildList: sortedList, diagnosisChildList: diagnosisChildList,
}).then((res) => { });
if (res.code === 200) {
// 步骤4更新本地数据使用全新对象防止响应式问题
form.value.diagnosisList = sortedList.map(item => ({ ...item }));
if (res.code === 200) {
// 步骤4从服务器刷新数据确保获取最新的诊断信息避免重复记录
await getList();
getTree();
emits('diagnosisSave', false); emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存'); proxy.$modal.msgSuccess('诊断已保存');
// 食源性疾病逻辑 // 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => { isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) { if (res2.code === 200 && res2.data) {
window.open(res2.data, '_blank'); window.open(res2.data, '_blank');
} }
}); });
} }
}).finally(() => { } catch (error) {
console.error('保存诊断失败:', error);
const errorMsg = error?.response?.data?.msg || error?.message || '保存诊断失败,请稍后重试';
proxy.$modal.msgError(errorMsg);
} finally {
setTimeout(() => { setTimeout(() => {
isSaving.value = false; isSaving.value = false;
}, 100); }, 100);
}); }
} }
}); });
} }
@@ -687,6 +708,7 @@ function handleNodeClick(data) {
proxy.$modal.msgWarning('该诊断项已存在'); proxy.$modal.msgWarning('该诊断项已存在');
return; return;
} }
const currentDate = getCurrentDate();
form.value.diagnosisList.push({ form.value.diagnosisList.push({
ybNo: data.ybNo, ybNo: data.ybNo,
name: data.name, name: data.name,
@@ -694,9 +716,12 @@ function handleNodeClick(data) {
medTypeCode: undefined, medTypeCode: undefined,
diagSrtNo: form.value.diagnosisList.length + 1, diagSrtNo: form.value.diagnosisList.length + 1,
definitionId: data.definitionId, definitionId: data.definitionId,
classification: '西医',
onsetDate: currentDate,
diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name, diagnosisDoctor: props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
diagnosisTime: new Date().toLocaleString('zh-CN'), diagnosisTime: currentDate,
// 添加 patientId longTermFlag: 0,
selectedDiseases: data.ybNo ? [data.ybNo] : [],
patientId: props.patientInfo.patientId patientId: props.patientInfo.patientId
}); });
if (form.value.diagnosisList.length == 1) { if (form.value.diagnosisList.length == 1) {

View File

@@ -164,7 +164,7 @@ onMounted(() => {
* type(1watch监听类型 2:点击保存类型) * type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组) * selectProjectIds(选中项目的id数组)
* */ * */
const projectWithDepartment = (selectProjectIds, type) => { const projectWithDepartment = (selectProjectIds) => {
//1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置 //1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置
let isRelease = true; let isRelease = true;
// 选中项目的数组 // 选中项目的数组

View File

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

View File

@@ -1210,7 +1210,7 @@ function handleSave() {
isSaving.value = false; isSaving.value = false;
getListInfo(false); getListInfo(false);
bindMethod.value = {}; bindMethod.value = {};
nextId.value == 1; nextId.value = 1;
} else { } else {
proxy.$modal.msgError(res.message); proxy.$modal.msgError(res.message);
isSaving.value = false; isSaving.value = false;
@@ -1376,13 +1376,21 @@ function handleSaveSign(row, index) {
savePrescription({ regAdviceSaveList: [row] }).then((res) => { savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功'); proxy.$modal.msgSuccess('保存成功');
nextId.value == 1; nextId.value = 1;
} }
}); });
} else { } else {
if (prescriptionList.value[0].adviceName) { // 新增行:调用保存接口将数据持久化到后端
handleAddPrescription(); row.dbOpType = '1';
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value = 1;
// 保存成功后刷新列表,确保后端返回的数据带 requestId
getListInfo(false);
} }
});
// 不需要再添加空行,保存成功后由 getListInfo 处理
} }
adviceQueryParams.value.adviceType = undefined; adviceQueryParams.value.adviceType = undefined;
} }
@@ -1429,12 +1437,13 @@ function handleSaveBatch() {
if (row) row.isEdit = false; if (row) row.isEdit = false;
}); });
getListInfo(false); getListInfo(false);
nextId.value == 1; nextId.value = 1;
isSaving.value = false; isSaving.value = false;
} }
}) })
.catch((error) => { .catch((error) => {
isSaving.value = false; isSaving.value = false;
proxy.$modal.msgError(error?.msg || '保存失败,请重试');
}); });
} }

View File

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

View File

@@ -829,7 +829,9 @@
</el-descriptions> </el-descriptions>
</div> </div>
<div style="padding: 10px"> <div style="padding: 10px">
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef" /> <prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1"
:sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div> <div class="overlay" v-if="disabled"></div>
</div> </div>
</div> </div>
@@ -872,11 +874,17 @@ import { Loading } from '@element-plus/icons-vue' // 🔧 新增:导入 Loadin
import { getPrescriptionList } from '@/views/clinicmanagement/bargain/component/api' import { getPrescriptionList } from '@/views/clinicmanagement/bargain/component/api'
// API 导入 // API 导入
import { getSurgerySchedulePage, addSurgerySchedule, updateSurgerySchedule, deleteSurgerySchedule, getSurgeryScheduleDetail } from '@/api/surgicalschedule' import {
getSurgerySchedulePage,
addSurgerySchedule,
updateSurgerySchedule,
deleteSurgerySchedule,
getSurgeryScheduleDetail
} from '@/api/surgicalschedule'
import { listUser } from '@/api/system/user' import { listUser } from '@/api/system/user'
import { deptTreeSelect } from '@/api/system/user' import { deptTreeSelect } from '@/api/system/user'
import { listOperatingRoom } from '@/api/operatingroom' import { listOperatingRoom } from '@/api/operatingroom'
import { getSurgery} from '@/views/inpatientDoctor/home/components/applicationShow/api.js' import { getSurgeryPage} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getTenantPage } from '@/api/system/tenant' import { getTenantPage } from '@/api/system/tenant'
import { getContract } from '@/views/inpatientDoctor/home/components/api.js' import { getContract } from '@/views/inpatientDoctor/home/components/api.js'
import SurgeryCharge from '../charge/surgerycharge/index.vue' import SurgeryCharge from '../charge/surgerycharge/index.vue'
@@ -1388,8 +1396,8 @@ async function handleChargeCharge(row) {
orgId: userStore.organizationId || userStore.orgId || userStore.tenantId || 1, orgId: userStore.organizationId || userStore.orgId || userStore.tenantId || 1,
// 添加账户ID // 添加账户ID
accountId: accountId, accountId: accountId,
// 添加手术申请单号用于追溯 // 添加手术单号用于关联对应的手术医嘱
sourceBillNo: row.applyId, sourceBillNo: row.operCode,
//添加计费标志手术计费 //添加计费标志手术计费
generateSourceEnum: 6 generateSourceEnum: 6
} }
@@ -1450,7 +1458,8 @@ function handleMedicalAdvice(row) {
role: userStore.roles[0], role: userStore.roles[0],
effectiveOrgId : row.effectiveOrgId, effectiveOrgId : row.effectiveOrgId,
orgId: userStore.orgId, orgId: userStore.orgId,
positionId: userStore.orgId positionId: userStore.orgId,
applyId: row.applyId // 手术申请单ID用于过滤关联医嘱
} }
// 🔧 关键修复:如果已有提交的医嘱数据,并且是同一个患者的就诊,则使用保存的数据 // 🔧 关键修复:如果已有提交的医嘱数据,并且是同一个患者的就诊,则使用保存的数据
@@ -1748,7 +1757,7 @@ function handleQuoteBilling() {
// 重新拉取计费药品数据 // 重新拉取计费药品数据
if (temporaryPatientInfo.value.visitId) { if (temporaryPatientInfo.value.visitId) {
temporaryMedicalLoading.value = true // 🔧 新增:开始加载 temporaryMedicalLoading.value = true // 🔧 新增:开始加载
getPrescriptionList(temporaryPatientInfo.value.visitId).then((res) => { getPrescriptionList(temporaryPatientInfo.value.visitId, 6, temporaryPatientInfo.value.operCode).then((res) => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 🔧 修复:先清空旧数据,避免数据累积 // 🔧 修复:先清空旧数据,避免数据累积
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
@@ -2023,7 +2032,7 @@ function handleFindApply() {
getSurgicalScheduleList() getSurgicalScheduleList()
} }
// 获取手术申请列表(用于查找”弹窗) // 获取手术申请列表(用于查找”弹窗)
function getSurgicalScheduleList() { function getSurgicalScheduleList() {
applyLoading.value = true applyLoading.value = true
const params = { ...applyQueryParams } const params = { ...applyQueryParams }
@@ -2032,8 +2041,7 @@ function getSurgicalScheduleList() {
params.applyTimeEnd = params.applyTimeRange[1] params.applyTimeEnd = params.applyTimeRange[1]
delete params.applyTimeRange delete params.applyTimeRange
} }
getSurgery(params).then((res) => { getSurgeryPage(params).then((res) => {
// Check if data is nested under data.data or directly under data
const responseData = res.data.data || res.data const responseData = res.data.data || res.data
applyList.value = responseData.records || [] applyList.value = responseData.records || []
applyTotal.value = responseData.total || 0 applyTotal.value = responseData.total || 0