Compare commits

..

42 Commits

Author SHA1 Message Date
华佗
838f717df4 Fix Bug #516: [住院医生站-临床医嘱-检验申请] 检验申请单手动填写的"发往科室"与生成的医嘱执行科室不一致
根因: 前端 projectWithDepartment 函数缺少 type 参数声明,导致用户手动选择的发往科室被清空;后端完全忽略前端传来的 positionId,始终使用配置表中的默认科室。
修复: 1) 前端添加 type 参数 + 提交前保存用户选择并在校验后恢复;2) 后端优先使用前端 positionId,配置表仅作为兜底。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:15:08 +08:00
华佗
25477dc6f0 Fix Bug #537: [住院医生工作站] 屏蔽汇总发药申请标签页——移除医生站中护士职能的汇总发药申请Tab(同时清理远程分支合并遗留的markdown代码块污染)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:12:05 +08:00
赵云
086467c4da Merge remote-tracking branch 'origin/华佗' - resolve conflicts 2026-05-13 01:05:18 +08:00
赵云
87155a1091 Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,"检查明细"标签页显示"暂无数据"
根因:Axios拦截器已返回 res.data(AjaxResult体),handleRowClick 中再次执行 res.data
导致 d 被赋为 ExamApply 实体对象(不含 items),明细列表永远无法加载。

修复:通过 res.code 判断 res 是否已是 AjaxResult 体,避免二次解包。
2026-05-13 01:04:27 +08:00
张飞
3590a18adc Fix Bug #362: 住院护士站:入出转管理双击查看详情时,"入科时间"字段显示当前系统时间而非实际入科时间
在 selectAdmissionPatientInfo SQL 中,startTime 原取自 bed.start_time(床位级别的位置记录),
当该 LEFT JOIN 无匹配记录时返回 NULL,前端 fallback 到当前系统时间。
改为 COALESCE(bed.start_time, ae.start_time),无床位记录时回退到 encounter 的入院时间。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:27:05 +08:00
赵云
4263cbbd9d Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:27:04 +08:00
关羽
6100d585c0 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 17:27:04 +08:00
赵云
7df3999917 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 17:27:04 +08:00
Ranyunqiao
b21d499f66 429 433 438 476 477 478 2026-05-12 17:27:04 +08:00
赵云
1476c406cf 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 17:27:04 +08:00
赵云
9843ff9c05 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 17:27:04 +08:00
a620f8da22 调优:测试环境数据库改为 test1 2026-05-12 17:27:04 +08:00
赵云
374658923d 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 17:27:04 +08:00
赵云
a81e7ec980 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 17:27:04 +08:00
39fefe9e80 bug 486 [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选“医嘱类型”时检索结果为空 删去了搜索条件的默认值 2026-05-12 17:27:04 +08:00
华佗
5a4c2b6354 Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
策略A:修改 clickRowDb 双击处理函数的编辑准入条件
- 原条件:row.statusEnum == 1 && !row.requestId(只允许从未保存的新医嘱编辑)
- 新条件:row.statusEnum == 1(允许所有待签发状态的医嘱编辑,包括护士退回的)
- 原因:护士退回医嘱时后端将状态重置为 DRAFT(1),但 requestId 仍存在,
  原条件因此拦截了退回医嘱,导致双击无响应
- 保存逻辑已支持有 requestId 的场景(dbOpType='2' 更新模式),无需额外修改

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:09:54 +08:00
赵云
6626c8f1cd 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 17:08:56 +08:00
赵云
e2f4996f47 Merge remote-tracking branch 'origin/华佗' into 华佗
# Conflicts:
#	openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue
2026-05-11 15:27:40 +08:00
赵云
37b57e8b12 Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
根因: clickRowDb 双击编辑条件为 row.statusEnum == 1 && !row.requestId,
护士退回的医嘱 statusEnum 被重置为 1(DRAFT),但 requestId 仍存在(之前已保存过),
导致不满足 !row.requestId 条件,无法进入编辑模式。

修复: 移除 !row.requestId 限制,仅保留 statusEnum == 1 条件。
保存流程已支持两种场景:
- 新建医嘱(无requestId): dbOpType = '1' 创建
- 退回医嘱(有requestId): dbOpType = '2' 更新
2026-05-11 15:26:39 +08:00
关羽
4bd0d2dcc8 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:02:20 +08:00
27 changed files with 93 additions and 398 deletions

View File

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

View File

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

View File

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

View File

@@ -274,8 +274,27 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
return R.fail("非就诊中患者不能完诊");
}
// 2. 查找队列项(限定当天,避免复诊患者匹配到历史队列记录)
// 2. 获取 pool_id 和 slot_id从 encounter → order_main → adm_schedule_slot 链路获取
// 确保 div_log 中的值与排班主表一致,不依赖 triage_queue_item队列项可能不存在或值错误
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
Long divPoolId = null;
Long divSlotId = null;
if (encounter.getOrderId() != null) {
try {
Order order = iOrderService.getById(encounter.getOrderId());
if (order != null && order.getSlotId() != null) {
divSlotId = order.getSlotId();
ScheduleSlot slot = scheduleSlotMapper.selectById(divSlotId);
if (slot != null) {
divPoolId = slot.getPoolId();
}
}
} catch (Exception e) {
log.warn("获取完诊div_log的pool_id/slot_id失败encounterId={}", encounterId, e);
}
}
// 3. 查找队列项(限定当天,避免复诊患者匹配到历史队列记录)
TriageQueueItem queueItem = triageQueueItemService.getOne(
new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
@@ -300,41 +319,14 @@ 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 &&
!TriageQueueStatus.COMPLETED.getValue().equals(queueItem.getStatus())) {
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
// 使用 LambdaUpdateWrapper 直接更新,确保 status 字段必定写入数据库
boolean queueUpdate = triageQueueItemService.update(new LambdaUpdateWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getId, queueItem.getId())
.set(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue())
.set(TriageQueueItem::getUpdateTime, nowLocal));
if (!queueUpdate) {
log.error("完诊triage_queue_item 状态更新失败queueItemId={}", queueItem.getId());
}
queueItem.setStatus(TriageQueueStatus.COMPLETED.getValue());
queueItem.setUpdateTime(nowLocal);
triageQueueItemService.updateById(queueItem);
} else if (queueItem == null) {
log.error("完诊:未找到任何 triage_queue_item 记录encounterId={}, tenantId={}",
encounterId, tenantId);

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -92,10 +91,8 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.fail("无待签发的医嘱,该申请单不可编辑");
}
} else {
// 检查申请单号JC检查+ Z住院标识+ yyMMdd日期+ 5位顺序
String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
int seq = assignSeqUtil.getSeqNoByDay(AssignSeqEnum.CHECK_APPLY_NO.getPrefix());
prescriptionNo = "JCZ" + dateStr + String.format("%05d", seq);
// 诊疗处方
prescriptionNo = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_PSYCHOTROPIC_NO.getPrefix(), 8);
}
// 当前时间
@@ -165,11 +162,15 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
serviceRequest.setEncounterId(encounterId); // 就诊id
serviceRequest.setAuthoredTime(curDate); // 请求签发时间
Long positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
// 优先使用前端传来的 positionId用户手动选择的发往科室未传时从配置表查找
Long positionId = activitySaveDto.getPositionId();
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
}
serviceRequest.setOrgId(positionId); // 执行科室

View File

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

View File

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

View File

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

View File

@@ -161,9 +161,6 @@
<if test="requestFormDto.surgeryNo != null and requestFormDto.surgeryNo != ''">
AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%')
</if>
<if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''">
AND drf.type_code = #{requestFormDto.typeCode}
</if>
<if test="requestFormDto.applyTimeStart != null">
AND drf.create_time >= #{requestFormDto.applyTimeStart}
</if>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1318,11 +1318,7 @@ async function show(diagnosisData) {
// 系统关联信息
encounterId: patientInfo.encounterId || '', // 就诊ID
patientId: patientInfo.patientId || '', // 患者ID
diagnosisId: (diagnosisData?.conditionId != null && diagnosisData?.conditionId !== '')
? diagnosisData.conditionId
: (diagnosisData?.definitionId != null && diagnosisData?.definitionId !== '')
? diagnosisData.definitionId
: '', // 诊断ID
diagnosisId: diagnosisData?.conditionId || diagnosisData?.definitionId || '', // 诊断ID
};
// 更新selectedDiseases数组
@@ -1377,7 +1373,7 @@ async function buildSubmitData() {
const submitData = {
cardNo: formData.cardNo,
visitId: props.patientInfo?.encounterId || formData.encounterId || null,
diagId: formData.diagnosisId ? Number(formData.diagnosisId) : null,
diagId: formData.diagnosisId || null,
patId: formData.patientId || null,
idType: 1, // 默认身份证
idNo: formData.idNo,
@@ -1543,12 +1539,6 @@ async function handleSubmit() {
return;
}
// 检查诊断ID是否有效后端 @NotNull 校验要求)
if (!form.value.diagnosisId) {
proxy.$modal.msgError('诊断信息不完整,请重新选择诊断后重试');
return;
}
// 开始加载状态,防止重复提交
submitLoading.value = true;

View File

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

View File

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

View File

@@ -79,7 +79,7 @@
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="name" label="申请单名称" width="140" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column prop="prescriptionNo" label="处方号" width="140" />
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="申请单状态" width="120" align="center">
<template #default="scope">
@@ -118,7 +118,7 @@
<el-descriptions-item label="创建时间">{{
currentDetail.createTime || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请单号">{{
<el-descriptions-item label="处方号">{{
currentDetail.prescriptionNo || '-'
}}</el-descriptions-item>
<el-descriptions-item label="申请者">{{

View File

@@ -225,9 +225,13 @@ const submit = () => {
if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单');
}
// 🔧 Bug #516: 捕获用户手动选择的发往科室,避免 projectWithDepartment 将其覆盖为默认科室
const savedTargetDept = form.targetDepartment;
if (!projectWithDepartment(transferValue.value, 2)) {
return;
}
// 用户手动选择的发往科室优先于项目默认执行科室
const targetDept = savedTargetDept || form.targetDepartment;
let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId);
});
@@ -238,7 +242,7 @@ const submit = () => {
unitCode: item.priceList[0].unitCode /** 请求单位编码 */,
unitPrice: item.priceList[0].price /** 单价 */,
totalPrice: item.priceList[0].price /** 总价 */,
positionId: item.positionId || form.targetDepartment, //执行科室id未配置时使用用户手动选择的科室
positionId: targetDept || item.positionId, // 用户指定发往科室优先于项目默认执行科室
ybClassEnum: item.ybClassEnum, //类别医保编码
conditionId: item.conditionId, //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id

View File

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

View File

@@ -801,7 +801,9 @@ function clickRowDb(row, column, event) {
return;
}
row.showPopover = false;
// 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑
// 允许所有 statusEnum==1 的医嘱进入编辑
// 1. 新医嘱(无 requestId待保存
// 2. 护士退回医嘱(有 requestId退回后状态重置为 DRAFT(1),需允许医生编辑修改后重新签发
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
@@ -1210,7 +1212,7 @@ function handleSave() {
isSaving.value = false;
getListInfo(false);
bindMethod.value = {};
nextId.value = 1;
nextId.value == 1;
} else {
proxy.$modal.msgError(res.message);
isSaving.value = false;
@@ -1376,21 +1378,13 @@ function handleSaveSign(row, index) {
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value = 1;
nextId.value == 1;
}
});
} else {
// 新增行:调用保存接口将数据持久化到后端
row.dbOpType = '1';
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value = 1;
// 保存成功后刷新列表,确保后端返回的数据带 requestId
getListInfo(false);
}
});
// 不需要再添加空行,保存成功后由 getListInfo 处理
if (prescriptionList.value[0].adviceName) {
handleAddPrescription();
}
}
adviceQueryParams.value.adviceType = undefined;
}
@@ -1437,13 +1431,12 @@ function handleSaveBatch() {
if (row) row.isEdit = false;
});
getListInfo(false);
nextId.value = 1;
nextId.value == 1;
isSaving.value = false;
}
})
.catch((error) => {
isSaving.value = false;
proxy.$modal.msgError(error?.msg || '保存失败,请重试');
});
}

View File

@@ -19,14 +19,10 @@
<el-tab-pane label="检验申请" name="test">
<TestApplication ref="testApplicationRef" :show-status-column="true" />
</el-tab-pane>
```vue
<el-tab-pane label="检查申请" name="examine">
<ExamineApplication ref="examineApplicationRef" />
</el-tab-pane>
```
<el-tab-pane label="汇总发药申请" name="summaryDrug">
<SummaryDrugApplication ref="summaryDrugApplicationRef" />
</el-tab-pane>
<!-- 汇总发药申请属于护士站职能医生站不显示 -->
<el-tab-pane label="手术申请" name="surgery">
<SurgeryApplication ref="surgeryApplicationRef" />
</el-tab-pane>

View File

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

View File

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