138 Commits

Author SHA1 Message Date
赵云
ae8f0bc06b Fix Bug #518: [门诊医生工作站-诊断-传染病报卡] 报卡页面缺失"性别、出生日期、实足年龄"核心字段
根因:infectiousDiseaseReportDialog.vue 读取患者性别时使用了错误的字段名
patientInfo.sex || patientInfo.genderName,但门诊医生站API返回的字段是
genderEnum(数字:1=男,2=女)和genderEnum_enumText(文本:男/女)。
新增 normalizeSexFromPatientInfo 函数,兼容HIS系统所有可能的性别字段命名。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:02:12 +08:00
76 changed files with 4272 additions and 1083 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ import com.openhis.medication.service.IMedicationDispenseService;
import com.openhis.medication.service.IMedicationRequestService;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
import com.openhis.web.doctorstation.appservice.IDoctorStationInspectionLabApplyService;
import com.openhis.web.doctorstation.dto.*;
import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper;
import com.openhis.web.doctorstation.utils.AdviceUtils;
@@ -47,12 +48,15 @@ import com.openhis.workflow.domain.InventoryItem;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -62,6 +66,9 @@ import java.util.stream.Collectors;
@Service
public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAppService {
private static final Pattern INSPECTION_APPLY_NO_JSON =
Pattern.compile("\"applyNo\"\\s*:\\s*\"([^\"]+)\"");
@Resource
AssignSeqUtil assignSeqUtil;
@@ -118,6 +125,13 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
@Resource
IInventoryItemService inventoryItemService;
/**
* 与检验申请实现存在循环依赖,需延迟注入;删除诊疗医嘱时按 contentJson 级联作废检验申请单。
*/
@Resource
@Lazy
private IDoctorStationInspectionLabApplyService iDoctorStationInspectionLabApplyService;
// 缓存 key 前缀
private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:";
// 缓存过期时间(小时)
@@ -228,8 +242,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 医嘱定义ID集合
List<Long> adviceDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getAdviceDefinitionId)
.collect(Collectors.toList());
// 费用定价主表ID集合
List<Long> chargeItemDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getChargeItemDefinitionId)
// 费用定价主表ID集合过滤null值手术项目无定价定义
List<Long> chargeItemDefinitionIdList = adviceBaseDtoList.stream()
.map(AdviceBaseDto::getChargeItemDefinitionId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 判断是否包含药品或耗材类型(只有这些类型才需要库存相关查询)
@@ -275,9 +291,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
medLocationConfig = Collections.emptyList();
allowedLocByCategory = Collections.emptyMap();
}
// 费用定价子表信息 - 使用分批处理避免大量参数问题
// 费用定价子表信息 - 仅药品/耗材需要批次定价查询,手术/诊疗无库存概念不需要
List<AdvicePriceDto> childCharge = new ArrayList<>();
if (chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
if (hasMedOrDevice && chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) {
// 分批处理每批最多1000个ID增加批次大小以减少查询次数
int batchSize = 1000;
for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) {
@@ -957,11 +973,16 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
for (AdviceSaveDto adviceSaveDto : deleteList) {
iMedicationRequestService.removeById(adviceSaveDto.getRequestId());
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的药品请求
if (requestId == null) {
log.warn("BugFix#442: handMedication - 跳过 requestId 为 null 的删除请求");
continue;
}
iMedicationRequestService.removeById(requestId);
// 删除已经产生的药品发放信息
iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId());
// 🔧 Bug Fix #219: 删除费用项
Long requestId = adviceSaveDto.getRequestId();
String serviceTable = CommonConstants.TableName.MED_MEDICATION_REQUEST;
// 直接删除费用项
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
@@ -1417,6 +1438,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix#219: handDevice - 开始删除循环, deleteList.size={}", deleteList.size());
for (AdviceSaveDto adviceSaveDto : deleteList) {
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的耗材请求
if (requestId == null) {
log.warn("BugFix#442: handDevice - 跳过 requestId 为 null 的删除请求");
continue;
}
log.info("BugFix#219: handDevice - 删除开始: requestId={}", requestId);
// 1. 删除耗材请求
@@ -1517,21 +1543,44 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
}
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号
deviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型
deviceRequest.setDeviceDefId(adviceSaveDto.getAdviceDefinitionId());// 耗材定义id
// 🔧 BugFix #498: categoryEnum=22(检查) 走 ServiceRequest不走 DeviceRequest
// 检查申请单的诊疗定义ID存在 activityId不在 adviceDefinitionId
// deviceDefId 对应耗材定义ID不能用诊疗定义ID填充
if (adviceSaveDto.getCategoryEnum() == 22) {
log.info("handDevice skip - 检查申请单(categoryEnum=22) 走 ServiceRequest 路径,跳过 DeviceRequest 保存");
continue; // 跳过本次循环,不走耗材请求路径
} else if (adviceSaveDto.getAdviceDefinitionId() != null) {
deviceRequest.setDeviceDefId(adviceSaveDto.getAdviceDefinitionId());// 耗材定义id
} else {
log.warn("handDevice - deviceDefId 为空adviceDefinitionId=null, categoryEnum={}", adviceSaveDto.getCategoryEnum());
}
deviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId());// 开方人科室
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间
// 发放耗材房若前端未传locationId使用登录用户科室作为默认值
// 发放耗材房若前端未传locationId优先沿用已有DeviceRequest的performLocation否则使用登录用户科室)
Long locId = adviceSaveDto.getLocationId();
if (locId == null) {
locId = SecurityUtils.getLoginUser().getOrgId();
log.info("耗材locationId为空使用登录用户科室作为默认值: locationId={}", locId);
// 尝试从已有DeviceRequest获取原始的performLocation
if (adviceSaveDto.getRequestId() != null) {
DeviceRequest existingDevice = iDeviceRequestService.getById(adviceSaveDto.getRequestId());
if (existingDevice != null && existingDevice.getPerformLocation() != null) {
locId = existingDevice.getPerformLocation();
log.info("耗材locationId为空使用已有DeviceRequest的performLocation: locationId={}", locId);
}
}
// 如果已有记录也没有performLocation则使用登录用户科室作为兜底
if (locId == null) {
locId = SecurityUtils.getLoginUser().getOrgId();
log.info("耗材locationId为空且无已有记录使用登录用户科室作为默认值: locationId={}", locId);
}
}
deviceRequest.setPerformLocation(locId);
deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id
@@ -1672,6 +1721,21 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
/**
* 从诊疗医嘱 contentJson 中解析检验申请单号(检验保存时写入形如 {"applyNo":"..."})。
*/
private String extractInspectionApplyNoFromContentJson(String contentJson) {
if (StringUtils.isBlank(contentJson) || !contentJson.contains("applyNo")) {
return null;
}
Matcher m = INSPECTION_APPLY_NO_JSON.matcher(contentJson);
if (!m.find()) {
return null;
}
String applyNo = m.group(1).trim();
return StringUtils.isBlank(applyNo) ? null : applyNo;
}
/**
* 处理诊疗
*/
@@ -1720,13 +1784,49 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
}
// 检验申请单在医嘱 contentJson 中写入 applyNo从医嘱删除时需先级联作废检验单避免检验页签仍显示孤儿申请
Map<String, List<Long>> labApplyNoToRequestIds = new LinkedHashMap<>();
for (AdviceSaveDto adviceSaveDto : deleteList) {
iServiceRequestService.removeById(adviceSaveDto.getRequestId());// 删除诊疗
Long requestId = adviceSaveDto.getRequestId();
// 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的诊疗请求
if (requestId == null) {
log.warn("BugFix#442: handService - 跳过 requestId 为 null 的删除请求");
continue;
}
iServiceRequestService.removeById(requestId);// 删除诊疗
ServiceRequest existing = iServiceRequestService.getById(adviceSaveDto.getRequestId());
if (existing == null) {
continue;
}
String applyNo = extractInspectionApplyNoFromContentJson(existing.getContentJson());
if (StringUtils.isNotBlank(applyNo)) {
labApplyNoToRequestIds.computeIfAbsent(applyNo, k -> new ArrayList<>())
.add(adviceSaveDto.getRequestId());
}
}
Set<Long> labCascadeSkippedRequestIds = new HashSet<>();
for (Map.Entry<String, List<Long>> e : labApplyNoToRequestIds.entrySet()) {
R<?> delLab = iDoctorStationInspectionLabApplyService.deleteInspectionLabApply(e.getKey());
if (delLab != null && R.isSuccess(delLab)) {
labCascadeSkippedRequestIds.addAll(e.getValue());
log.info("handService - 级联作废检验申请单 applyNo={},已跳过重复删除的医嘱 requestIds={}",
e.getKey(), e.getValue());
} else {
String msg = delLab != null && StringUtils.isNotEmpty(delLab.getMsg()) ? delLab.getMsg() : "删除检验申请单失败";
log.warn("handService - 级联作废检验申请单未成功 applyNo={} msg={},将回退为仅删除当前医嘱记录",
e.getKey(), msg);
}
}
for (AdviceSaveDto adviceSaveDto : deleteList) {
if (labCascadeSkippedRequestIds.contains(adviceSaveDto.getRequestId())) {
continue;
}
Long requestId = adviceSaveDto.getRequestId();
iServiceRequestService.removeById(requestId);// 删除诊疗
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getParentId,
adviceSaveDto.getRequestId()));// 删除诊疗套餐对应的子项
requestId));// 删除诊疗套餐对应的子项
// 🔧 Bug Fix #219: 删除费用项
Long requestId = adviceSaveDto.getRequestId();
String serviceTable = CommonConstants.TableName.WOR_SERVICE_REQUEST;
// 直接删除费用项
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
@@ -1783,8 +1883,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("handService - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
}
// 🔧 Bug Fix #238: 诊疗项目执行科室非空校验
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3) {
// 🔧 Bug Fix #238/#454: 诊疗项目执行科室非空校验(删除操作跳过校验)
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3
&& !DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) {
Long effectiveOrgId = adviceSaveDto.getEffectiveOrgId();
if (effectiveOrgId == null) {
throw new ServiceException("诊疗项目必须选择执行科室");
@@ -1806,6 +1907,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
}
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
serviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
@@ -1987,13 +2089,25 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
*/
@Override
public R<?> getRequestBaseInfo(Long encounterId) {
return this.getRequestBaseInfo(encounterId, null, null);
}
@Override
public R<?> getRequestBaseInfo(Long encounterId, Integer generateSourceEnum, String sourceBillNo) {
// 当前账号的参与者id
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
// 未指定generateSourceEnum时默认只查询医生开立的医嘱
int sourceEnum = (generateSourceEnum != null) ? generateSourceEnum : GenerateSource.DOCTOR_PRESCRIPTION.getValue();
// 医嘱请求数据
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, null,
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue());
sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时过滤掉药品1保留耗材2和诊疗3/6
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& dto.getAdviceType() == 1);
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态
requestBaseDto
@@ -2114,7 +2228,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
List<RequestBaseDto> requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, patientId,
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.YES.getCode(),
GenerateSource.DOCTOR_PRESCRIPTION.getValue());
GenerateSource.DOCTOR_PRESCRIPTION.getValue(), null);
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态
requestBaseDto

View File

@@ -261,8 +261,10 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
// 设置创建时间,避免数据库约束错误
encounterDiagnosis.setCreateTime(new Date());
iEncounterDiagnosisService.saveOrUpdate(encounterDiagnosis);
// 回写就诊诊断ID供前端后续更新使用
saveDiagnosisChildParam.setEncounterDiagnosisId(encounterDiagnosis.getId());
}
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
return R.ok(saveDiagnosisParam, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] {"诊断"}));
}
@@ -580,7 +582,11 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
@Override
public R<?> saveInfectiousDiseaseReport(InfectiousDiseaseReportDto infectiousDiseaseReportDto) {
// 检查卡片编号唯一性(新增时检查,编辑时排除当前记录)
String cardNo = infectiousDiseaseReportDto.getCardNo().trim();
String cardNo = infectiousDiseaseReportDto.getCardNo();
if (cardNo == null || cardNo.trim().isEmpty()) {
return R.fail("卡片编号不能为空");
}
cardNo = cardNo.trim();
LambdaQueryWrapper<InfectiousDiseaseReport> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(InfectiousDiseaseReport::getCardNo, cardNo);
long count = iInfectiousDiseaseReportService.count(queryWrapper);

View File

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

View File

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

View File

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

View File

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

View File

@@ -178,6 +178,8 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
inpatientAdviceParam.setEncounterIds(null);
Integer exeStatus = inpatientAdviceParam.getExeStatus();
inpatientAdviceParam.setExeStatus(null);
// requestStatus由前端tab控制后端SQL已通过CASE条件处理校对状态过滤无需再作为SQL条件
inpatientAdviceParam.setRequestStatus(null);
// 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);

View File

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

View File

@@ -43,6 +43,19 @@ public interface IRequestFormManageAppService {
*/
List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate);
/**
* 查询申请单(支持筛选+状态+关键字)
*
* @param encounterId 就诊id
* @param typeCode 申请单类型
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/项目名称模糊匹配)
* @return 申请单列表
*/
List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate, String status, String keyword);
/**
* 分页查询申请单
*
@@ -50,4 +63,20 @@ public interface IRequestFormManageAppService {
* @return 申请单
*/
IPage<RequestFormPageDto> getRequestFormPage(RequestFormDto requestFormDto);
/**
* 删除申请单(仅待签发状态可删除)
*
* @param requestFormId 申请单ID
* @return 结果
*/
R<?> deleteRequestForm(Long requestFormId);
/**
* 撤回申请单(已签发状态撤回至待签发)
*
* @param requestFormId 申请单ID
* @return 结果
*/
R<?> withdrawRequestForm(Long requestFormId);
}

View File

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

View File

@@ -1,6 +1,7 @@
package com.openhis.web.regdoctorstation.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
@@ -31,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -75,12 +77,38 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 申请单ID前端空字符串可能反序列化为0L需同时判0
Long requestFormId = requestFormSaveDto.getRequestFormId();
boolean isEdit = requestFormId != null && requestFormId != 0L;
// 诊疗执行科室配置校验(必须在任何数据库操作之前)
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室");
}
// 逐个校验activityList中的项目是否都配置了执行科室并收集positionId供后续使用
// 必须在任何数据库操作之前完成全部校验,避免部分保存后异常导致脏数据
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
// 缓存校验结果,避免主循环中重复查询和可能出现的数据不一致
java.util.Map<Long, Long> activityIdToPositionIdMap = new java.util.HashMap<>();
if (activityList != null && !activityList.isEmpty()) {
for (ActivitySaveDto activitySaveDto : activityList) {
Long positionId = activityOrganizationConfig.stream()
.filter(dto -> activitySaveDto.getAdviceDefinitionId().equals(dto.getActivityDefinitionId()))
.map(ActivityOrganizationConfigDto::getOrganizationId).findFirst().orElse(null);
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
activityIdToPositionIdMap.put(activitySaveDto.getAdviceDefinitionId(), positionId);
}
}
// 诊疗处方号
String prescriptionNo;
// 申请单ID
Long requestFormId = requestFormSaveDto.getRequestFormId();
// 编辑场景
if (requestFormId != null) {
if (isEdit) {
RequestForm requestFormInfo = iRequestFormService.getById(requestFormId);
prescriptionNo = requestFormInfo.getPrescriptionNo();
// 该申请单存在的待发送医嘱个数
@@ -91,8 +119,10 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.fail("无待签发的医嘱,该申请单不可编辑");
}
} else {
// 诊疗处方
prescriptionNo = assignSeqUtil.getSeq(AssignSeqEnum.ACTIVITY_PSYCHOTROPIC_NO.getPrefix(), 8);
// 检查申请单号JC检查+ Z住院标识+ yyMMdd日期+ 5位顺序
String dateStr = new java.text.SimpleDateFormat("yyMMdd").format(new Date());
int seq = assignSeqUtil.getSeqNoByDay(AssignSeqEnum.CHECK_APPLY_NO.getPrefix());
prescriptionNo = "JCZ" + dateStr + String.format("%05d", seq);
}
// 当前时间
@@ -122,7 +152,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
iRequestFormService.saveOrUpdate(requestForm);
// 编辑场景时,先删除掉原有诊疗项目及账单再新增
if (requestFormId != null) {
if (isEdit) {
List<Long> serviceRequestIds = iServiceRequestService
.list(new LambdaQueryWrapper<ServiceRequest>().eq(ServiceRequest::getPrescriptionNo, prescriptionNo))
.stream().map(ServiceRequest::getId).collect(Collectors.toList());
@@ -136,15 +166,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
ServiceRequest serviceRequest;
ChargeItem chargeItem;
// 诊疗集合
List<ActivitySaveDto> activityList = requestFormSaveDto.getActivityList();
log.info("保存申请单typeCode={}, activityListSize={}, encounterId={}", typeCode, activityList != null ? activityList.size() : 0, encounterId);
// 诊疗执行科室配置
List<ActivityOrganizationConfigDto> activityOrganizationConfig =
requestFormManageAppMapper.getActivityOrganizationConfig(typeCode);
if (activityOrganizationConfig.isEmpty()) {
throw new ServiceException("请先配置当前时间段的执行科室");
}
for (ActivitySaveDto activitySaveDto : activityList) {
serviceRequest = new ServiceRequest();
@@ -162,9 +184,7 @@ 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);
Long positionId = activityIdToPositionIdMap.get(activitySaveDto.getAdviceDefinitionId());
if (positionId == null) {
throw new ServiceException(activitySaveDto.getAdviceDefinitionName() + "未配置当前时间段的执行科室");
}
@@ -275,7 +295,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
surgeryServiceRequest.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
surgeryServiceRequest.setQuantity(BigDecimal.valueOf(1));
surgeryServiceRequest.setUnitCode("");
surgeryServiceRequest.setCategoryEnum(4); // 4-手术
surgeryServiceRequest.setCategoryEnum(24); // 24-手术(新值域,避开 adviceType 碰撞)
// 优先从 activityList 获取手术 ID
if (activityList != null && !activityList.isEmpty()) {
Long activityId = activityList.get(0).getAdviceDefinitionId();
@@ -428,12 +448,28 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
*/
@Override
public List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate) {
return getRequestForm(encounterId, typeCode, startDate, endDate, null, null);
}
/**
* 查询申请单(支持筛选+状态+关键字)
*
* @param encounterId 就诊id
* @param typeCode 申请单类型
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/项目名称模糊匹配)
* @return 申请单列表
*/
@Override
public List<RequestFormQueryDto> getRequestForm(Long encounterId, String typeCode, String startDate, String endDate, String status, String keyword) {
// 检查参数
if (encounterId == null) {
return new java.util.ArrayList<>();
}
List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate,null,null);
List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate, status, keyword);
for (RequestFormQueryDto requestFormQueryDto : requestFormList) {
// 查询处方详情
List<RequestFormDetailQueryDto> requestFormDetail =
@@ -455,4 +491,90 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return requestFormManageAppMapper.getRequestFormPage(requestFormDto, page);
}
@Override
public R<?> deleteRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest含子项
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
// 校验:只有待签发(status=0)的申请单可删除
boolean allDraft = serviceRequests.stream()
.allMatch(sr -> RequestStatus.DRAFT.getValue().equals(sr.getStatusEnum()));
if (!allDraft) {
return R.fail("只有待签发状态的申请单可删除");
}
List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList());
// 1. 删除关联的费用项
for (Long srId : serviceRequestIds) {
iChargeItemService.deleteByServiceTableAndId(
CommonConstants.TableName.WOR_SERVICE_REQUEST, srId);
}
// 2. 删除子项 ServiceRequestparentId 非空)
iServiceRequestService.remove(
new LambdaQueryWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds)
.isNotNull(ServiceRequest::getParentId));
// 3. 删除主项 ServiceRequest
iServiceRequestService.removeByIds(serviceRequestIds);
// 4. 删除申请单
iRequestFormService.removeById(requestFormId);
log.info("检查申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("删除成功");
}
@Override
public R<?> withdrawRequestForm(Long requestFormId) {
if (requestFormId == null) {
return R.fail("申请单ID不能为空");
}
RequestForm requestForm = iRequestFormService.getById(requestFormId);
if (requestForm == null) {
return R.fail("申请单不存在");
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
// 校验:只有已签发(status=2)的申请单可撤回
boolean allActive = serviceRequests.stream()
.allMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!allActive) {
return R.fail("只有已签发状态的申请单可撤回");
}
// 将所有 ServiceRequest 状态改回待签发(DRAFT=0)
List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList());
iServiceRequestService.update(
new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()),
new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds));
log.info("检查申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("撤回成功");
}
}

View File

@@ -16,7 +16,9 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 申请单管理 controller
@@ -81,14 +83,23 @@ public class RequestFormManageController {
* 查询检查申请单
*
* @param encounterId 就诊id
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检查项目名称模糊匹配)
* @return 检查申请单
*/
@GetMapping(value = "/get-check")
public R<?> getCheckRequestForm(@RequestParam(required = false) Long encounterId) {
public R<?> getCheckRequestForm(
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) {
return R.fail("就诊ID不能为空");
}
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode()));
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.TEST.getCode(), startDate, endDate, status, keyword));
}
/**
@@ -98,6 +109,7 @@ public class RequestFormManageController {
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检验项目模糊匹配)
* @return 检验申请单
*/
@GetMapping(value = "/get-inspection")
@@ -105,11 +117,12 @@ public class RequestFormManageController {
@RequestParam(required = false) Long encounterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status) {
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
if (encounterId == null) {
return R.fail("就诊ID不能为空");
}
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROOF.getCode(), startDate, endDate));
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROOF.getCode(), startDate, endDate, status, keyword));
}
/**
@@ -140,6 +153,32 @@ public class RequestFormManageController {
return R.ok(iRequestFormManageAppService.getRequestForm(encounterId, ActivityDefCategory.PROCEDURE.getCode()));
}
/**
* 分页查询手术申请单全局不需要encounterId用于门诊手术安排查找弹窗
*
* @param surgeryNo 手术单号(模糊查询,可选)
* @param applyTimeStart 申请时间起始(可选)
* @param applyTimeEnd 申请时间截止(可选)
* @param mainDoctorId 主刀医生ID可选
* @param applyDeptId 申请科室ID可选
* @param pageNo 页码默认1
* @param pageSize 每页数量默认10
* @return 分页手术申请单列表
*/
@GetMapping(value = "/get-surgery-page")
public R<IPage<RequestFormPageDto>> getSurgeryRequestFormPage(
@RequestParam(required = false) String surgeryNo,
@RequestParam(required = false) LocalDate applyTimeStart,
@RequestParam(required = false) LocalDate applyTimeEnd,
@RequestParam(required = false) Long mainDoctorId,
@RequestParam(required = false) Long applyDeptId,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
RequestFormDto dto = new RequestFormDto(surgeryNo, ActivityDefCategory.PROCEDURE.getCode(), applyTimeStart, applyTimeEnd,
mainDoctorId, applyDeptId, pageNo, pageSize);
return R.ok(iRequestFormManageAppService.getRequestFormPage(dto));
}
/**
* 分页查询申请单
* @return 申请单
*/
@@ -147,4 +186,26 @@ public class RequestFormManageController {
public R<IPage<RequestFormPageDto>> getRequestFormPage(@RequestBody RequestFormDto requestFormDto) {
return R.ok(iRequestFormManageAppService.getRequestFormPage(requestFormDto));
}
/**
* 删除申请单(仅待签发状态可删除)
*
* @param data 包含 requestFormId 的请求体
* @return 结果
*/
@PostMapping(value = "/delete")
public R<?> deleteRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.deleteRequestForm(data.get("requestFormId"));
}
/**
* 撤回申请单(已签发状态撤回至待签发)
*
* @param data 包含 requestFormId 的请求体
* @return 结果
*/
@PostMapping(value = "/withdraw")
public R<?> withdrawRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.withdrawRequestForm(data.get("requestFormId"));
}
}

View File

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

View File

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

View File

@@ -97,9 +97,14 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(
wsr.content_json::json->>'surgeryName',
wsr.content_json::json->>'adviceName',
T9sr.surgery_name)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = 6 THEN T2."name"
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} AND T1.service_table = 'wor_service_request' THEN COALESCE(T9.surgery_name, wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN COALESCE(wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"
@@ -107,6 +112,7 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 THEN T2.yb_no
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
@@ -117,6 +123,7 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(T9sr.id, wsr.activity_id)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = 6 THEN T2.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
@@ -158,6 +165,11 @@
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
LEFT JOIN cli_surgery AS T9sr ON T1.context_enum = 6
AND T1.service_table = 'wor_service_request'
AND wsr.activity_id IS NOT NULL
AND wsr.activity_id = T9sr.id
AND T9sr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsrp ON wsrp.id = wsr.parent_id AND wsrp.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (0
@@ -222,9 +234,14 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(
wsr.content_json::json->>'surgeryName',
wsr.content_json::json->>'adviceName',
T9sr.surgery_name)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = 6 THEN T2."name"
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} AND T1.service_table = 'wor_service_request' THEN COALESCE(T9.surgery_name, wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN COALESCE(wsr.content_json::json->>'surgeryName', wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"
@@ -232,6 +249,7 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = 6 THEN T2.yb_no
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
@@ -242,6 +260,7 @@
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = 6 AND T1.service_table = 'wor_service_request' THEN COALESCE(T9sr.id, wsr.activity_id)
WHEN T1.context_enum = 6 AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = 6 THEN T2.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
@@ -284,6 +303,11 @@
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
LEFT JOIN cli_surgery AS T9sr ON T1.context_enum = 6
AND T1.service_table = 'wor_service_request'
AND wsr.activity_id IS NOT NULL
AND wsr.activity_id = T9sr.id
AND T9sr.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (0
, #{planned}

View File

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

View File

@@ -239,7 +239,7 @@
NULL AS activity_type_dictText,
-- 前端"包装单位"列显示使用单位permitted_unit_code
T1.permitted_unit_code AS unit_code,
'' AS min_unit_code,
T1.permitted_unit_code AS min_unit_code,
'' AS volume,
'' AS method_code,
'' AS rate_code,
@@ -527,6 +527,9 @@
LEFT JOIN cli_condition AS cc ON cc.id = T1.condition_id AND cc.delete_flag = '0'
LEFT JOIN cli_condition_definition AS ccd ON ccd.id = cc.definition_id
WHERE T1.delete_flag = '0' AND T1.tcm_flag = 0
<if test="generateSourceEnum != null">
AND T1.generate_source_enum = #{generateSourceEnum}
</if>
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>
@@ -536,14 +539,15 @@
AND T1.refund_medicine_id IS NULL
ORDER BY T1.status_enum,T1.sort_number)
UNION ALL
-- 🔧 新增:查询门诊术中计费生成的耗材数据(这些数据存在于 adm_charge_item 和 wor_device_request
-- 🔧 查询仅存在于 adm_charge_item 的"孤儿"耗材数据DeviceRequest 缺失或 generate_source_enum 未设置
-- 正常 DeviceRequestgenerate_source_enum 已赋值)由下方 Part 3 统一负责,此处不做重复覆盖避免 UNION ALL 重复行
(SELECT 2 AS advice_type,
CI.service_id AS request_id,
CI.service_id || '-ci-dev' AS unique_key,
'' AS prescription_no,
CI.enterer_id AS requester_id,
COALESCE(DR.requester_id, CI.enterer_id) AS requester_id,
CI.entered_date AS request_time,
CASE WHEN CI.enterer_id = #{practitionerId} THEN '1' ELSE '0' END AS biz_request_flag,
CASE WHEN COALESCE(DR.requester_id, CI.enterer_id) = #{practitionerId} THEN '1' ELSE '0' END AS biz_request_flag,
DR.content_json AS content_json,
NULL AS skin_test_flag,
NULL AS inject_flag,
@@ -580,6 +584,9 @@
LEFT JOIN adm_location AS AL ON AL.id = DR.perform_location AND AL.delete_flag = '0'
WHERE CI.delete_flag = '0'
AND CI.service_table = 'wor_device_request'
<if test="generateSourceEnum != null">
AND DR.generate_source_enum IS NULL <!-- 仅匹配孤儿记录normal DeviceRequest 由 Part 3 负责,避免 UNION ALL 重复 -->
</if>
<if test="historyFlag == '0'.toString()">
AND CI.encounter_id = #{encounterId}
</if>
@@ -695,6 +702,9 @@
-- based_on_table='med_medication_request' → 输液/皮试执行记录,应排除
-- based_on_table='exam_apply'/'lab_apply' → 申请单原始医嘱,应保留
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL OR T1.based_on_table NOT IN ('med_medication_request', 'med_medication_dispense'))
<if test="sourceBillNo != null and sourceBillNo != ''">
AND T1.prescription_no = #{sourceBillNo}
</if>
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>

View File

@@ -231,7 +231,7 @@
ae.priority_enum,
ae.organization_id,
ae.start_time AS in_hos_time,
bed.start_time,
COALESCE(bed.start_time, ae.start_time) AS start_time,
bed.location_id AS bed_id,
bed.location_name AS bed_name,
house.location_id AS house_id,
@@ -264,7 +264,6 @@
WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0'
AND ael.form_enum = #{bed}
LIMIT 1
) AS bed ON bed.encounter_id = ae.id
LEFT JOIN (
SELECT ael.encounter_id,
@@ -275,7 +274,6 @@
WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0'
AND ael.form_enum = #{house}
LIMIT 1
) AS house ON house.encounter_id = ae.id
LEFT JOIN (
SELECT ael.encounter_id,
@@ -286,7 +284,6 @@
WHERE ael.status_enum = #{active}
AND ael.delete_flag = '0'
AND ael.form_enum = #{ward}
LIMIT 1
) AS ward ON ward.encounter_id = ae.id
LEFT JOIN (
SELECT aep.encounter_id,

View File

@@ -155,7 +155,7 @@
ii.performer_check_id,
ii.category_code,
ii.dispense_status
FROM (( SELECT T1.encounter_id,
FROM (( SELECT DISTINCT T1.encounter_id,
T1.tenant_id,
#{medMedicationRequest} AS advice_table,
T1.id AS request_id,
@@ -280,9 +280,13 @@
aa.balance_amount
) AS personal_account
ON personal_account.encounter_id = ae.id
LEFT JOIN med_medication_dispense mmd
ON mmd.med_req_id = T1.id
AND mmd.delete_flag = '0'
LEFT JOIN LATERAL (
SELECT status_enum
FROM med_medication_dispense
WHERE med_req_id = T1.id AND delete_flag = '0'
ORDER BY create_time DESC
LIMIT 1
) mmd ON true
WHERE T1.delete_flag = '0'
AND T1.refund_medicine_id IS NULL
AND T1.generate_source_enum = #{doctorPrescription}
@@ -293,7 +297,7 @@
T1.sort_number,
T1.group_id )
UNION
( SELECT T1.encounter_id,
( SELECT DISTINCT T1.encounter_id,
T1.tenant_id,
#{worServiceRequest} AS advice_table,
T1.id AS request_id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -372,7 +372,6 @@ import {
} from './diagnosistreatment';
import PopoverList from '@/components/OpenHis/popoverList/index.vue';
import medicineList from './medicineList.vue';
import MedicineList from '../components/medicineList.vue';
import {getCurrentInstance, nextTick, watch} from 'vue';
const { proxy } = getCurrentInstance();
@@ -467,16 +466,21 @@ function calculateTotalPrice() {
try {
let sum = 0;
treatmentItems.value.forEach((item) => {
if (item.adviceDefinitionId && item.retailPrice && item.childrenRequestNum) {
const price = parseFloat(item.retailPrice) || 0;
const count = parseInt(item.childrenRequestNum) || 0;
if (item.adviceDefinitionId && item.adviceDefinitionId !== '') {
const price = Number(item.retailPrice) || 0;
const count = Number(item.childrenRequestNum) || 0;
sum += price * count;
}
});
totalPrice.value = sum.toFixed(2);
// Bug #464: 零售价与诊疗子项合计总价实时同步
if (treatmentItems.value.length > 0 && treatmentItems.value[0].adviceDefinitionId !== '') {
form.value.retailPrice = parseFloat(totalPrice.value);
// Bug #464: 零售价与诊疗子项合计总价实时同步直接赋值不使用nextTick避免多调用方竞争
const hasValidItem = treatmentItems.value.some(
(item) => item.adviceDefinitionId && item.adviceDefinitionId !== ''
);
if (hasValidItem) {
form.value.retailPrice = parseFloat(totalPrice.value) || 0;
} else {
form.value.retailPrice = undefined;
}
} catch (error) {
totalPrice.value = '0.00';
@@ -486,7 +490,7 @@ function calculateTotalPrice() {
// 添加表单项
function addItem() {
treatmentItems.value.push({ adviceDefinitionId: '', childrenRequestNum: 1, retailPrice: 0 });
treatmentItems.value.push({ adviceDefinitionId: '', childrenRequestNum: 1, name: '', retailPrice: 0 });
// 使用nextTick确保DOM更新后再计算
nextTick(() => {
calculateTotalPrice();
@@ -560,15 +564,16 @@ function edit() {
form.value.pricingFlag = 1;
}
// 处理子项数据确保包含retailPrice字段
// 处理子项数据确保包含retailPrice和name字段
if (props.item.childrenJson) {
const parsedItems = JSON.parse(props.item.childrenJson);
treatmentItems.value = parsedItems.map((item) => ({
...item,
name: item.name || '',
retailPrice: item.retailPrice || 0,
}));
} else {
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, retailPrice: 0 }];
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, name: '', retailPrice: 0 }];
}
form.value.permittedUnitCode = form.value.permittedUnitCode
? form.value.permittedUnitCode.toString()
@@ -613,7 +618,7 @@ function reset() {
chrgitmLv: undefined, //医保等级
pricingFlag: 1, // 划价标记,默认允许划价
};
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, retailPrice: 0 }];
treatmentItems.value = [{ adviceDefinitionId: '', childrenRequestNum: 1, name: '', retailPrice: 0 }];
totalPrice.value = '0.00';
proxy.resetForm('diagnosisTreatmentRef');
}
@@ -647,12 +652,15 @@ async function submitForm() {
form.value.ybMatchFlag ? (form.value.ybMatchFlag = 1) : (form.value.ybMatchFlag = 0);
form.value.ruleId ? (form.value.ruleId = 1) : (form.value.ruleId = 0);
form.value.childrenJson =
treatmentItems.value.length > 0 && treatmentItems.value[0].adviceDefinitionId != ''
treatmentItems.value.some((item) => item.adviceDefinitionId != '' && item.adviceDefinitionId)
? JSON.stringify(treatmentItems.value)
: undefined;
// Bug #464 修复:零售价自动与诊疗子项合计总价同步
// 当有子项时,零售价自动设置为子项合计总价
if (treatmentItems.value.length > 0 && treatmentItems.value[0].adviceDefinitionId != '') {
const hasValidItem = treatmentItems.value.some(
(item) => item.adviceDefinitionId && item.adviceDefinitionId !== ''
);
if (hasValidItem) {
form.value.retailPrice = parseFloat(totalPrice.value) || 0;
}
proxy.$refs['diagnosisTreatmentRef'].validate(async (valid) => {

View File

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

View File

@@ -69,13 +69,14 @@ const throttledGetList = throttle(
watch(
() => props.adviceQueryParams,
(newValue) => {
// 只有在弹窗打开时才响应 adviceQueryParams 的变化,避免选择项目后弹窗关闭时触发不必要的请求
if (!props.popoverVisible) {
return;
}
// 始终同步参数到 queryParams避免弹窗打开时使用旧参数
queryParams.value.searchKey = newValue?.searchKey;
queryParams.value.adviceType = newValue?.adviceType;
queryParams.value.categoryCode = newValue?.categoryCode;
// 只有在弹窗打开时才触发 API 请求
if (!props.popoverVisible) {
return;
}
throttledGetList();
},
{ deep: true }
@@ -114,22 +115,44 @@ function getList() {
console.log('[adviceBaseList] getList() 跳过:未选择患者');
return; // 不执行API调用
}
// 只有在弹窗打开时才执行查询
if (!props.popoverVisible) {
console.log('[adviceBaseList] getList() 跳过:弹窗未打开');
return;
}
queryParams.value.organizationId = props.patientInfo.orgId;
console.log('[adviceBaseList] getList() 请求参数:', JSON.stringify(queryParams.value));
getAdviceBaseInfo(queryParams.value).then((res) => {
// 🔧 Bug #448 修复:显式构建请求参数,确保 adviceType 正确传递
// 不直接使用 queryParams.value避免 undefined 值被发送到后端导致过滤失效
const requestParams = {
pageSize: queryParams.value.pageSize,
pageNum: queryParams.value.pageNum,
organizationId: props.patientInfo.orgId,
};
// 只在 adviceType 有值时添加0 是无效值undefined/null 会导致后端查询所有类型)
if (queryParams.value.adviceType != null && queryParams.value.adviceType !== 0) {
requestParams.adviceType = queryParams.value.adviceType;
}
// 只在 categoryCode 有值时添加
if (queryParams.value.categoryCode) {
requestParams.categoryCode = queryParams.value.categoryCode;
}
// 只在 searchKey 有值时添加
if (queryParams.value.searchKey) {
requestParams.searchKey = queryParams.value.searchKey;
}
console.log('[adviceBaseList] getList() 请求参数:', JSON.stringify(requestParams));
getAdviceBaseInfo(requestParams).then((res) => {
console.log('[adviceBaseList] getList() 响应数据:', {
total: res.data?.total,
recordsCount: res.data?.records?.length || 0,
firstRecord: res.data?.records?.[0]?.adviceName || '无数据',
adviceType: queryParams.value.adviceType
adviceType: requestParams.adviceType
});
adviceBaseList.value = res.data.records || [];
total.value = res.data.total || 0;

View File

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

View File

@@ -381,6 +381,14 @@ const props = defineProps({
activeTab: {
type: String,
},
generateSourceEnum: {
type: Number,
default: null,
},
sourceBillNo: {
type: String,
default: null,
},
});
const isAdding = ref(false);
const isSaving = ref(false); // #437 防重复提交锁
@@ -453,7 +461,7 @@ watch(
console.log(prescriptionList.value,"prescriptionList.value")
if(newValue&&newValue.length>0){
let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
})
console.log(saveList,"prescriptionList.value")
if (saveList.length == 0) {
@@ -468,7 +476,11 @@ watch(
function getListInfo(addNewRow) {
isAdding.value = false;
getPrescriptionList(props.patientInfo.encounterId).then((res) => {
getPrescriptionList(
props.patientInfo.encounterId,
props.generateSourceEnum ?? undefined,
props.sourceBillNo ?? undefined,
).then((res) => {
// 为每行数据添加 adviceTypeValue 字段,用于类型下拉框显示
prescriptionList.value = (res.data || []).map(item => {
// 根据 adviceType 和 categoryCode 找到对应的 adviceTypeValue
@@ -868,19 +880,27 @@ function handleDelete() {
proxy.$modal.msgWarning('请选择要删除的项目');
return;
}
let deleteList = groupIndexList.value.map((index) => {
const item = prescriptionList.value[index];
// 只删除待签发且未收费的项目
if (item.statusEnum != 1 || item.chargeStatus == 5) {
return null;
}
// 🔧 Bug #442: 非本人创建的医嘱不允许删除(与签发/签退逻辑保持一致)
if (Number(item.bizRequestFlag) !== 1 && item.bizRequestFlag) {
return null;
}
// 🔧 Bug #442: 已保存的行必须有有效的 requestId否则跳过避免后端删除不存在的记录
if (item.requestId == null || item.requestId === undefined || item.requestId === '') {
return null;
}
return {
requestId: item.requestId,
dbOpType: '3',
adviceType: item.adviceType,
};
}).filter(item => item !== null); // 过滤掉已签发已收费的项目
}).filter(item => item !== null); // 过滤掉已签发已收费、非本人创建或无 requestId 的项目
if (deleteList.length == 0) {
proxy.$modal.msgWarning('只能删除待签发且未收费的项目');
@@ -995,7 +1015,7 @@ function handleSave() {
return;
}
let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
});
// let saveList = prescriptionList.value
// .filter((item) => {
@@ -1016,6 +1036,14 @@ function handleSave() {
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
// 🔧 Bug #443: 补充顶层关键字段(这些不在 contentJson 中,需从 API 响应顶层提取)
encounterId: item.encounterId,
patientId: item.patientId,
locationId: item.positionId,
adviceType: item.adviceType,
adviceTableName: item.adviceTableName,
adviceDefinitionId: item.adviceDefinitionId,
chargeItemId: item.chargeItemId,
};
});
savePrescriptionSign({
@@ -1031,7 +1059,12 @@ function handleSave() {
groupIndexList.value = []
groupList.value = []
nextId.value = 1;
} else {
proxy.$modal.msgError(res?.msg || '签发失败,请重试');
}
}).catch((error) => {
console.error('签发失败:', error);
proxy.$modal.msgError(error?.response?.data?.msg || error?.message || '签发失败,请重试');
});
}
@@ -1047,42 +1080,44 @@ function handleSaveSign(row, index) {
proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
return;
}
isSaving.value = true; // #437 立即加锁,消除 TOCTOU 竞态
proxy.$refs['formRef' + index].validate((valid) => {
if (valid) {
isSaving.value = true; // #437 加
row.isEdit = false;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
if (!valid) {
isSaving.value = false; // 验证失败释放
return;
}
});
row.isEdit = false;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }, '1').then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
})
}
// 签退

View File

@@ -49,6 +49,40 @@
</el-col>
</el-row>
<!-- 性别出生日期或实足年龄 -->
<el-row :gutter="16" class="form-row">
<el-col :span="7" class="form-item">
<span class="form-label required">性别</span>
<el-radio-group v-model="form.sex" class="gender-radio-group">
<el-radio value="男"></el-radio>
<el-radio value="女"></el-radio>
<el-radio value="未知">未知</el-radio>
</el-radio-group>
</el-col>
<el-col :span="10" class="form-item">
<span class="form-label required">出生日期</span>
<div class="birth-input-group">
<el-input v-model="form.birthYear" class="birth-input year" placeholder="年" maxlength="4" />
<span class="birth-separator"></span>
<el-input v-model="form.birthMonth" class="birth-input month" placeholder="月" maxlength="2" />
<span class="birth-separator"></span>
<el-input v-model="form.birthDay" class="birth-input day" placeholder="日" maxlength="2" />
<span class="birth-separator"></span>
</div>
</el-col>
<el-col :span="7" class="form-item">
<span class="form-label"> 实足年龄</span>
<div class="age-input-group">
<el-input v-model="form.age" class="age-input" placeholder="年龄" />
<el-select v-model="form.ageUnit" class="age-unit-select">
<el-option label="岁" value="岁" />
<el-option label="月" value="月" />
<el-option label="天" value="天" />
</el-select>
</div>
</el-col>
</el-row>
<!-- 联系电话紧急联系人电话 -->
<el-row :gutter="16" class="form-row">
<el-col :span="12" class="form-item">
@@ -1000,6 +1034,17 @@ function normalizeSex(value) {
return '未知';
}
function normalizeSexFromPatientInfo(patientInfo) {
// 优先使用文本字段
if (patientInfo.genderEnum_enumText) return patientInfo.genderEnum_enumText;
if (patientInfo.genderName) return patientInfo.genderName;
if (patientInfo.sex) return normalizeSex(patientInfo.sex);
// 使用数字枚举字段
if (patientInfo.genderEnum === 1) return '男';
if (patientInfo.genderEnum === 2) return '女';
return '未知';
}
function normalizeAgeUnit(value) {
const ageUnitMap = {
1: '岁',
@@ -1243,11 +1288,14 @@ async function show(diagnosisData) {
const res = await getNextCardNo(orgCode);
if (res.code === 200 && res.data && res.data.length >= 12) {
cardNo = res.data;
} else {
// API返回失败或不合规时生成临时卡号避免保存时 cardNo 为空导致后端校验失败
cardNo = 'TEMP_' + Date.now();
}
// API失败或返回不合规时保持为空字符串由用户手动填写或后端自动生成
} catch (err) {
console.error('获取卡片编号失败:', err);
// 保持为空,不使用不合规的临时值
// API调用异常时生成临时卡号
cardNo = 'TEMP_' + Date.now();
}
form.value = {
@@ -1258,7 +1306,7 @@ async function show(diagnosisData) {
patName: patientInfo.patientName || patientInfo.name || '', // 患者姓名
parentName: '', // 家长姓名14岁以下患者必填
idNo: patientInfo.idCard, // 身份证号
sex: patientInfo.sex || patientInfo.genderName || '男', // 性别
sex: normalizeSexFromPatientInfo(patientInfo), // 性别
// 出生日期信息
birthYear: birthInfo.year, // 出生年份
@@ -1318,7 +1366,11 @@ async function show(diagnosisData) {
// 系统关联信息
encounterId: patientInfo.encounterId || '', // 就诊ID
patientId: patientInfo.patientId || '', // 患者ID
diagnosisId: diagnosisData?.conditionId || diagnosisData?.definitionId || '', // 诊断ID
diagnosisId: (diagnosisData?.conditionId != null && diagnosisData?.conditionId !== '')
? diagnosisData.conditionId
: (diagnosisData?.definitionId != null && diagnosisData?.definitionId !== '')
? diagnosisData.definitionId
: '', // 诊断ID
};
// 更新selectedDiseases数组
@@ -1359,6 +1411,9 @@ async function buildSubmitData() {
} else if (formData.otherDisease) {
// 其他传染病使用自定义编码
diseaseCode = 'OTHER';
} else if (formData.selectedDiseases && formData.selectedDiseases.length > 0) {
// 兜底:如果 ClassA/B/C 都为空但 selectedDiseases 有值,取第一个作为 diseaseCode
diseaseCode = formData.selectedDiseases[0];
}
// 转换年龄单位:岁=1, 月=2, 天=3
@@ -1373,7 +1428,7 @@ async function buildSubmitData() {
const submitData = {
cardNo: formData.cardNo,
visitId: props.patientInfo?.encounterId || formData.encounterId || null,
diagId: formData.diagnosisId || null,
diagId: formData.diagnosisId ? Number(formData.diagnosisId) : null,
patId: formData.patientId || null,
idType: 1, // 默认身份证
idNo: formData.idNo,
@@ -1426,8 +1481,8 @@ async function buildSubmitData() {
function validateFormManually() {
const errors = [];
// 卡片编号验证至少12位后端自动生成16位编号
if (form.value.cardNo && form.value.cardNo.length < 12) {
// 卡片编号验证至少12位后端自动生成16位编号;临时卡号 TEMP_ 开头允许通过
if (form.value.cardNo && !form.value.cardNo.startsWith('TEMP_') && form.value.cardNo.length < 12) {
errors.push('卡片编号至少12位');
}
@@ -1539,6 +1594,12 @@ async function handleSubmit() {
return;
}
// 检查诊断ID是否有效后端 @NotNull 校验要求)
if (!form.value.diagnosisId) {
proxy.$modal.msgError('诊断信息不完整,请重新选择诊断后重试');
return;
}
// 开始加载状态,防止重复提交
submitLoading.value = true;
@@ -1762,6 +1823,33 @@ defineExpose({ show, showReport, close: handleClose });
color: #999;
}
/* 输入框下划线样式(与 underline-select 保持一致) */
.underline-input :deep(.el-input__wrapper) {
border: none;
border-bottom: 1px solid #dcdfe6;
border-radius: 0;
box-shadow: none;
background: transparent;
}
.underline-input :deep(.el-input__wrapper:hover) {
border-bottom-color: #c0c4cc;
}
.underline-input :deep(.el-input__wrapper.is-focus) {
border-bottom-color: #409eff;
}
.underline-input :deep(.el-input__inner) {
font-size: 12px;
color: #666;
}
.underline-input :deep(.el-input__inner::placeholder) {
font-size: 12px;
color: #999;
}
/* 街道下拉框下划线样式 */
.underline-select {
width: 100%;
@@ -1949,4 +2037,53 @@ defineExpose({ show, showReport, close: handleClose });
display: flex !important;
justify-content: center !important;
}
/* 性别单选按钮组 */
.gender-radio-group {
display: flex;
gap: 12px;
padding-top: 4px;
}
/* 出生日期输入组 */
.birth-input-group {
display: flex;
align-items: center;
gap: 2px;
}
.birth-input {
text-align: center;
}
.birth-input.year {
width: 70px;
}
.birth-input.month,
.birth-input.day {
width: 50px;
}
.birth-separator {
color: #606266;
font-size: 13px;
margin: 0 2px;
}
/* 年龄输入组 */
.age-input-group {
display: flex;
align-items: center;
gap: 6px;
}
.age-input {
width: 70px;
text-align: center;
}
.age-unit-select {
width: 65px;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -626,14 +626,19 @@ function getList() {
loading.value = false
return
}
loading.value = true
getSurgeryPage({
pageNo: 1,
pageSize: 100,
encounterId: props.patientInfo.encounterId
}).then((res) => {
surgeryList.value = res.data.records || []
if (res.code === 200) {
surgeryList.value = res.data?.records || []
} else {
proxy.$modal.msgError(res.msg || '数据加载失败,请稍后重试')
surgeryList.value = []
}
}).catch(error => {
console.error('获取手术列表失败:', error)
proxy.$modal.msgError('数据加载失败,请稍后重试')
@@ -1133,13 +1138,12 @@ function submitForm() {
// 新增手术
addSurgery(form.value).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请提交成功!')
proxy.$modal.msgSuccess('手术申请提交成功!')
// 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false
getList() // 提交成功后直接刷新列表
emit('saved') // 通知父组件刷新医嘱列表
// 刷新手术申请列表
getList()
} else {
proxy.$modal.msgError(res.msg || '新增手术失败,请检查表单信息')
}
@@ -1151,13 +1155,12 @@ function submitForm() {
// 修改手术
updateSurgery(form.value).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess(res.msg || '手术申请修改成功!')
proxy.$modal.msgSuccess('手术申请修改成功!')
// 保存麻醉方式
sessionStorage.setItem('anesthesiaType', form.value.anesthesiaTypeEnum)
open.value = false
getList() // 修改成功后直接刷新列表
emit('saved') // 通知父组件刷新医嘱列表
// 刷新手术申请列表
getList()
} else {
proxy.$modal.msgError(res.msg || '更新手术失败,请检查表单信息')
}

View File

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

View File

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

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

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

View File

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

View File

@@ -568,6 +568,11 @@ function handleMaindise(value, index) {
* 保存诊断
*/
function handleSaveDiagnosis() {
// 防止重复点击保存
if (isSaving.value) {
return;
}
for (let index = 0; index < (form.value.diagnosisList || []).length; index++) {
const item = form.value.diagnosisList[index];
if (!item.diagSrtNo) {
@@ -600,7 +605,7 @@ function handleSaveDiagnosis() {
// 步骤2重新分配连续的序号从1开始
sortedList.forEach((item, index) => {
item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序
item.diagSrtNo = index + 1; // 这里是关键!把诊断排序”改成新顺序
});
// 步骤3提交排序后的数据
@@ -610,12 +615,12 @@ function handleSaveDiagnosis() {
diagnosisChildList: sortedList,
}).then((res) => {
if (res.code === 200) {
// 步骤4更新本地数据使用全新对象防止响应式问题
form.value.diagnosisList = sortedList.map(item => ({ ...item }));
emits('diagnosisSave', false);
proxy.$modal.msgSuccess('诊断已保存');
// 保存成功后从服务器重新加载数据,确保前后端数据一致
getList();
// 食源性疾病逻辑
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
if (res2.code === 20 && res2.data) {

View File

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

View File

@@ -6,9 +6,26 @@
<template>
<div class="LaboratoryTests-container">
<div v-loading="loading" class="transfer-wrapper">
<!-- 远程搜索框 -->
<div class="search-bar">
<el-input
v-model="searchKey"
placeholder="输入项目代码/名称搜索"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
style="width: 300px; margin-bottom: 10px"
>
<template #append>
<el-button @click="handleSearch">搜索</el-button>
</template>
</el-input>
<span v-if="!searchKey" class="total-count"> {{ totalCount }} </span>
<span v-else class="total-count">搜索到 {{ filteredCount }} / {{ totalCount }} </span>
</div>
<el-transfer
v-model="transferValue"
:data="applicationList"
:data="transferData"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
@@ -72,13 +89,52 @@
<el-input v-model="form.attention" autocomplete="off" type="textarea" />
</el-form-item>
</el-col>
<!-- 申请类型 -->
<el-col :span="12">
<el-form-item label="申请类型" prop="applicationType" style="width: 100%">
<el-radio-group v-model="form.applicationType">
<el-radio :value="0">普通</el-radio>
<el-radio :value="1">急诊</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- 标本类型 -->
<el-col :span="12">
<el-form-item label="标本类型" prop="specimenName" style="width: 100%">
<el-select v-model="form.specimenName" placeholder="请选择标本类型" style="width: 100%">
<el-option label="血液" value="血液" />
<el-option label="尿液" value="尿液" />
<el-option label="粪便" value="粪便" />
<el-option label="痰液" value="痰液" />
<el-option label="咽拭子" value="咽拭子" />
<el-option label="脑脊液" value="脑脊液" />
<el-option label="胸腹水" value="胸腹水" />
<el-option label="关节液" value="关节液" />
<el-option label="分泌物" value="分泌物" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<!-- 执行时间 -->
<el-col :span="12">
<el-form-item label="执行时间" prop="executeTime" style="width: 100%">
<el-date-picker
v-model="form.executeTime"
type="datetime"
placeholder="选择执行时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
</template>
<script setup name="LaboratoryTests">
import {getCurrentInstance, onBeforeMount, onMounted, reactive, watch} from 'vue';
import {getCurrentInstance, onMounted, reactive, ref, watch, computed} from 'vue';
import {patientInfo} from '../../../store/patient.js';
import {getApplicationList, saveInspection} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js';
@@ -99,48 +155,92 @@ const findTreeItem = (list, id) => {
return null;
};
const emits = defineEmits(['submitOk']);
const props = defineProps({});
const props = defineProps({
editData: {
type: Object,
default: null,
},
});
const isEditMode = computed(() => !!props.editData?.requestFormId);
const state = reactive({});
const applicationListAll = ref();
const applicationList = ref();
const applicationListAll = ref([]);
const loading = ref(false);
const orgOptions = ref([]); // 科室选项
const getList = () => {
const orgOptions = ref([]);
const searchKey = ref('');
const totalCount = ref(0);
// 将已加载的全部数据转为 transfer 组件所需的格式
const buildTransferData = (records) => {
return records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
};
// 加载全部数据(不分页,一次性拉取)
const loadAllData = async () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationList.value = [];
applicationListAll.value = [];
return;
}
loading.value = true;
getApplicationList({
pageSize: 9999,
pageNum: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], //1 药品 2耗材 3诊疗
})
.then((res) => {
if (res.code === 200) {
applicationListAll.value = res.data.records;
applicationList.value = res.data.records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
};
});
console.log('applicationList========>', JSON.stringify(res.data.records));
} else {
proxy.$message.error(res.message);
applicationList.value = [];
}
})
.finally(() => {
loading.value = false;
try {
// 使用大 pageSize 一次性拉取所有启用状态的检验类诊疗项目
const res = await getApplicationList({
pageSize: 9999,
pageNo: 1,
categoryCode: '22',
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
});
if (res.code !== 200) {
proxy.$message.error(res.message);
applicationListAll.value = [];
return;
}
applicationListAll.value = res.data?.records || [];
totalCount.value = res.data?.total || 0;
} catch (e) {
proxy.$message.error('获取检验项目列表失败');
applicationListAll.value = [];
} finally {
loading.value = false;
}
};
// 根据搜索关键词过滤数据
const filterData = (key) => {
if (!key || key.trim() === '') {
return applicationListAll.value;
}
const lowerKey = key.toLowerCase().trim();
return applicationListAll.value.filter((item) => {
return (
item.adviceName?.toLowerCase().includes(lowerKey) ||
item.pyStr?.toLowerCase().includes(lowerKey) ||
item.adviceBusNo?.toLowerCase().includes(lowerKey)
);
});
};
// transfer 组件实际显示的数据(受搜索词影响)
const transferData = computed(() => buildTransferData(filterData(searchKey.value)));
// 当前显示的条数
const filteredCount = computed(() => filterData(searchKey.value).length);
const getList = async () => {
await loadAllData();
};
const handleSearch = () => {
// 搜索时保持已选中的项目不受影响
};
const transferValue = ref([]);
const form = reactive({
@@ -152,11 +252,13 @@ const form = reactive({
otherDiagnosis: '', // 其他诊断
relatedResult: '', // 相关结果
attention: '', // 注意事项
applicationType: 0, // 申请类型 0-普通 1-急诊
specimenName: '血液', // 标本类型
executeTime: null, // 执行时间
primaryDiagnosisList: [], //主诊断目录
otherDiagnosisList: [], //其他断目录
});
const rules = reactive({});
onBeforeMount(() => {});
onMounted(() => {
getList();
});
@@ -164,17 +266,27 @@ onMounted(() => {
* type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组)
* */
const projectWithDepartment = (selectProjectIds) => {
const projectWithDepartment = (selectProjectIds, type) => {
//1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置
let isRelease = true;
// 选中项目的数组
const arr = [];
// 根据选中的项目id查找对应的项目
// 根据选中的项目id查找对应的项目(从全部原始数据中查找)
selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => {
const searchData = applicationListAll.value.find((item) => {
return element == item.adviceDefinitionId;
});
arr.push(searchData);
if (searchData) {
const priceInfo = searchData.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = searchData.unitCode_dictText || searchData.unitCode || '';
arr.push({
adviceDefinitionId: searchData.adviceDefinitionId,
orgId: searchData.orgId,
label: searchData.adviceName + ' (¥' + price + '/' + unit + ')',
key: searchData.adviceDefinitionId,
});
}
});
// 保存用户手动选择的发往科室(提交时需要保留)
const manualDept = type === 2 ? form.targetDepartment : '';
@@ -200,12 +312,16 @@ const projectWithDepartment = (selectProjectIds) => {
if (type === 2 && manualDept) {
form.targetDepartment = manualDept;
isRelease = true;
} else {
} else if (type === 2 && !manualDept) {
// 提交时用户未手动选择科室,才提示错误
isRelease = false;
ElMessage({
type: 'error',
message: '未找到项目执行的科室',
});
} else {
// type=1(选择项目变化)时,不弹窗,仅清空科室让用户自行选择
isRelease = false;
}
}
if (findItem && isRelease) {
@@ -221,6 +337,44 @@ watch(
projectWithDepartment(newValue, 1);
}
);
// 编辑模式下,回显已有数据
watch(
() => props.editData,
(newData) => {
if (!newData || !newData.requestFormId) return;
// 解析 descJson 回填表单
if (newData.descJson) {
try {
const obj = JSON.parse(newData.descJson);
Object.keys(form).forEach((key) => {
if (obj[key] !== undefined) {
form[key] = obj[key];
}
});
} catch (e) {
console.error('解析 descJson 失败:', e);
}
}
// 回填已选项目
if (newData.requestFormDetailList && newData.requestFormDetailList.length > 0) {
// 从全部数据中匹配已选项目
const selectedIds = [];
newData.requestFormDetailList.forEach((detail) => {
const matched = applicationListAll.value.find(
(item) => item.adviceName === detail.adviceName
);
if (matched) {
selectedIds.push(matched.adviceDefinitionId);
}
});
transferValue.value = selectedIds;
}
},
{ immediate: true }
);
const submit = () => {
if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单');
@@ -253,15 +407,15 @@ const submit = () => {
patientId: patientInfo.value.patientId, //患者ID
encounterId: patientInfo.value.encounterId, // 就诊ID
organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID
requestFormId: '', // 申请单ID
requestFormId: isEditMode.value ? props.editData.requestFormId : '', // 申请单ID(编辑模式传入,新增为空)
name: '检验申请单',
descJson: JSON.stringify(form),
categoryEnum: '1', // 1 检验 2 检查 3 输血 4 手术
categoryEnum: '21', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
};
saveInspection(params).then((res) => {
if (res.code === 200) {
proxy.$message.success(res.msg);
applicationList.value = [];
proxy.$message.success(isEditMode.value ? '修改成功' : res.msg);
transferValue.value = [];
emits('submitOk');
} else {
proxy.$message.error(res.message);
@@ -318,6 +472,19 @@ defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList });
.transfer-wrapper {
position: relative;
min-height: 300px;
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.total-count {
font-size: 13px;
color: #909399;
white-space: nowrap;
}
}
}
.el-transfer {

View File

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

View File

@@ -5,16 +5,14 @@
-->
<template>
<div class="surgery-container">
<div class="transfer-wrapper">
<div v-loading="loading" style="min-height: 300px;">
<el-transfer
v-model="transferValue"
:data="applicationList"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
/>
</div>
<div v-loading="loading" class="transfer-wrapper" style="min-height: 300px;">
<el-transfer
v-model="transferValue"
:data="applicationList"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
/>
</div>
<div class="bloodTransfusion-form">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="demo-ruleForm">
@@ -88,6 +86,9 @@ import {getApplicationList, saveSurgery} from './api';
import {ElMessage} from 'element-plus';
const { proxy } = getCurrentInstance();
// 模块级缓存:避免每次打开弹窗都重新请求手术项目列表
let surgeryRecordsCache = null; // 原始 API 记录
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
@@ -103,8 +104,8 @@ const findTreeItem = (list, id) => {
const emits = defineEmits(['submitOk']);
const props = defineProps({});
const state = reactive({});
const applicationListAll = ref([]);
const applicationList = ref([]);
const applicationListAll = ref();
const applicationList = ref();
const orgOptions = ref([]); // 科室选项
const loading = ref(false); // 加载状态
const getList = () => {
@@ -112,6 +113,12 @@ const getList = () => {
applicationList.value = [];
return;
}
// 命中缓存时直接使用,避免重复请求导致加载缓慢
if (surgeryMappedCache && surgeryMappedCache.length > 0) {
applicationList.value = surgeryMappedCache;
applicationListAll.value = surgeryRecordsCache;
return;
}
loading.value = true;
getApplicationList({
pageSize: 500,
@@ -134,6 +141,9 @@ const getList = () => {
key: item.adviceDefinitionId,
};
});
// 写入模块缓存,后续打开弹窗直接复用
surgeryRecordsCache = res.data.records;
surgeryMappedCache = applicationList.value;
} else {
console.warn('获取手术项目列表失败:', res.message);
applicationList.value = [];
@@ -257,7 +267,7 @@ const submit = () => {
requestFormId: '', // 申请单ID
name: '手术申请单',
descJson: JSON.stringify(form),
categoryEnum: '4', // 1 检验 2 检查 3 输血 4 手术
categoryEnum: '24', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
}).then((res) => {
if (res.code === 200) {
proxy.$message.success(res.msg);

View File

@@ -197,6 +197,7 @@
style="width: 62%"
v-model="scope.row.adviceName"
placeholder="请选择项目"
@input="handleChange"
@click="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown="
@@ -532,6 +533,7 @@ const statusOption = [
let loadingInstance = undefined;
onMounted(() => {
document.addEventListener('keydown', escKeyListener);
getList();
});
onBeforeUnmount(() => {
@@ -572,7 +574,6 @@ function handleTotalAmount() {
}
}, new Decimal(0));
}
getList();
function getList() {
getDiagnosisDefinitionList(queryParams.value).then((res) => {
// prescriptionList.value = res.data.records;
@@ -584,6 +585,11 @@ function refresh() {
}
// 获取列表信息
function getListInfo(addNewRow) {
// 守护:未选择患者时不发起 API 请求,避免页面加载时循环报错
if (!patientInfo.value || !patientInfo.value.encounterId) {
console.warn('⚠️ getListInfo 跳过:未选择患者');
return;
}
loadingInstance = ElLoading.service({ fullscreen: true });
setTimeout(() => {
loadingInstance.close();
@@ -709,9 +715,17 @@ function loadConfiguredCategories() {
// 数据过滤
const filterPrescriptionList = computed(() => {
const pList = prescriptionList.value.filter((item) => {
// 修复 Bug #488orderClassCode 可能是复合值 '1-2',需提取 adviceType 部分进行比较
let matchAdviceType = true;
if (orderClassCode.value) {
const filterAdviceType = String(orderClassCode.value).includes('-')
? parseInt(String(orderClassCode.value).split('-')[0])
: orderClassCode.value;
matchAdviceType = filterAdviceType == item.adviceType;
}
return (
(!therapyEnum.value || therapyEnum.value == item.therapyEnum) &&
(!orderClassCode.value || orderClassCode.value == item.adviceType) &&
matchAdviceType &&
(!orderStatus.value || (orderStatus.value == item.statusEnum && item.requestId))
);
});
@@ -739,12 +753,28 @@ function getRowDisabled(row) {
/**
* 将行的 adviceType + categoryCode 映射为 el-select 的选中值
* 药品子分类使用复合值如 '1-2'adviceType-categoryCode诊疗/手术/全部使用原始值
* 修复 Bug #488当行的 adviceType 在当前配置中找不到匹配选项时,返回最接近的可用值,避免回显为纯数字
*/
function getRowSelectValue(row) {
if (row.adviceType == 1 && row.categoryCode) {
return '1-' + row.categoryCode;
const compositeValue = '1-' + row.categoryCode;
// 检查复合值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === compositeValue)) {
return compositeValue;
}
// 配置的 categoryCode 已变更,回退到第一个药品选项
const firstPharmacy = adviceTypeList.value.find(item => String(item.value).startsWith('1-'));
if (firstPharmacy) {
return firstPharmacy.value;
}
return row.adviceType;
}
return row.adviceType;
// 诊疗/手术等非药品类型,检查其值是否在选项列表中
if (adviceTypeList.value.some(item => item.value === row.adviceType)) {
return row.adviceType;
}
// 不在选项中的值(如已废弃的 adviceType返回 undefined 让 el-select 显示为空
return undefined;
}
// 新增医嘱
@@ -801,8 +831,8 @@ function clickRowDb(row, column, event) {
return;
}
row.showPopover = false;
// 待签发(已保存 requestId存在)”允许编辑;仅“待保存(无requestId)”允许编辑
if (row.statusEnum == 1 && !row.requestId) {
// 仅”待签发(statusEnum==1)”允许编辑;”已签发(statusEnum==2)”及之后状态不允许编辑
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true;
@@ -922,7 +952,9 @@ function handleChange(value) {
// 修复Bug #486当行没有显式选择医嘱类型时row?.adviceType为undefined
// 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
tableRef.refresh(adviceType, categoryCode, value);
// 修复Bug #453当adviceType为空字符串或NaN时不传具体类型让refresh函数根据searchKey决定搜索范围
const effectiveAdviceType = (adviceType && !isNaN(Number(adviceType))) ? adviceType : '';
tableRef.refresh(effectiveAdviceType, categoryCode, value);
}
}
}
@@ -1181,19 +1213,27 @@ function handleSave() {
});
// 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可
loading.value = true;
let list = saveList.map((item) => {
const parsedContent = JSON.parse(item.contentJson);
return {
...parsedContent,
adviceType: item.adviceType,
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
uniqueKey: undefined,
// 确保 therapyEnum 被正确传递
therapyEnum: parsedContent.therapyEnum || item.therapyEnum || '1',
};
});
let list = [];
try {
list = saveList.map((item) => {
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) || {} : {};
return {
...parsedContent,
adviceType: item.adviceType,
requestId: item.requestId,
dbOpType: '1',
groupId: item.groupId,
uniqueKey: undefined,
// 确保 therapyEnum 被正确传递
therapyEnum: parsedContent?.therapyEnum || item.therapyEnum || '1',
};
});
} catch (error) {
loading.value = false;
isSaving.value = false;
proxy.$modal.msgError('医嘱内容解析失败,请检查待签发医嘱');
return;
}
// 保存签发按钮
isSaving.value = true;
console.log('签发处方参数:', {
@@ -1208,9 +1248,16 @@ function handleSave() {
if (res.code === 200) {
proxy.$modal.msgSuccess('签发成功');
isSaving.value = false;
// 乐观更新:立即将已签发医嘱的状态设为"已签发",确保列表实时刷新
saveList.forEach((item) => {
const row = prescriptionList.value.find((r) => r.requestId && r.requestId === item.requestId);
if (row) {
row.statusEnum = 2;
}
});
getListInfo(false);
bindMethod.value = {};
nextId.value == 1;
nextId.value = 1;
} else {
proxy.$modal.msgError(res.message);
isSaving.value = false;
@@ -1311,11 +1358,12 @@ function handleCancelEdit(row, index) {
function handleSaveSign(row, index) {
if (row.adviceType != 2) {
// 修复 Bug #488严格校验 itemNo确保非空且为有效字符串才发起请求
let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId;
if (!itemNo) {
if (!itemNo || String(itemNo).trim() === '') {
console.warn('绑定设备检查跳过itemNo为空adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + '');
} else {
getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => {
getBindDevice({ typeCode: row.adviceType, itemNo: String(itemNo) }).then((res) => {
if (res.data.length == 0) {
return;
}
@@ -1376,13 +1424,21 @@ function handleSaveSign(row, index) {
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value == 1;
nextId.value = 1;
}
});
} else {
if (prescriptionList.value[0].adviceName) {
handleAddPrescription();
}
// 新增行:调用保存接口将数据持久化到后端
row.dbOpType = '1';
savePrescription({ regAdviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
nextId.value = 1;
// 保存成功后刷新列表,确保后端返回的数据带 requestId
getListInfo(false);
}
});
// 不需要再添加空行,保存成功后由 getListInfo 处理
}
adviceQueryParams.value.adviceType = undefined;
}
@@ -1423,18 +1479,26 @@ function handleSaveBatch() {
.then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
// 修复#405:保存成功后重置所有待保存行的 isEdit 为 false锁定医嘱不再编辑
// 修复 Bug #405保存成功后锁定所有待保存行,避免医嘱条目仍处于可编辑状态
// saveList 中的 item 与 prescriptionList 是同一对象引用,直接修改即可
saveList.forEach(item => {
const row = prescriptionList.value.find(r => r.uniqueKey === item.uniqueKey);
if (row) row.isEdit = false;
item.isEdit = false;
});
// 兜底:锁定所有 statusEnum == 1 的行,确保没有遗漏
prescriptionList.value.forEach(row => {
if (row.statusEnum == 1) {
row.isEdit = false;
}
});
expandOrder.value = [];
getListInfo(false);
nextId.value == 1;
nextId.value = 1;
isSaving.value = false;
}
})
.catch((error) => {
isSaving.value = false;
proxy.$modal.msgError(error?.msg || '保存失败,请重试');
});
}
@@ -1562,17 +1626,21 @@ function handleSaveGroup(orderGroupList) {
// 🔥 新版组件已经预处理了数据,优先使用 mergedDetail
const mergedDetail = item.mergedDetail || {
...(item.orderDetailInfos || {}),
adviceName: item.orderDetailInfos?.adviceName || item.orderDefinitionName || '未知项目',
adviceName: item.orderDefinitionName || item.orderDetailInfos?.adviceName || '未知项目',
adviceType: item.orderDetailInfos?.adviceType,
adviceDefinitionId: item.orderDefinitionId || item.orderDetailInfos?.adviceDefinitionId,
quantity: item.quantity,
unitCode: item.unitCode || item.orderDetailInfos?.unitCode,
unitCodeName: item.unitCodeName,
dose: item.dose || item.orderDetailInfos?.dose,
// 🔧 Bug #403 修复dose/doseQuantity/dispensePerDuration 需用 null 检查,
// 避免组套中值为 null 时回退到医嘱库的 orderDetailInfos
dose: item.dose !== undefined && item.dose !== null ? item.dose : item.orderDetailInfos?.dose,
rateCode: item.rateCode || item.orderDetailInfos?.rateCode,
methodCode: item.methodCode || item.orderDetailInfos?.methodCode,
dispensePerDuration: item.dispensePerDuration || item.orderDetailInfos?.dispensePerDuration,
doseQuantity: item.doseQuantity,
dispensePerDuration: item.dispensePerDuration !== undefined && item.dispensePerDuration !== null
? item.dispensePerDuration : item.orderDetailInfos?.dispensePerDuration,
doseQuantity: item.doseQuantity !== undefined && item.doseQuantity !== null
? item.doseQuantity : item.orderDetailInfos?.doseQuantity,
inventoryList: item.orderDetailInfos?.inventoryList || [],
priceList: item.orderDetailInfos?.priceList || [],
partPercent: item.orderDetailInfos?.partPercent || 1,
@@ -1591,20 +1659,15 @@ function handleSaveGroup(orderGroupList) {
setValue(mergedDetail);
// 创建新的处方项目
// 🔧 Bug #403 修复:关键字段使用 null-safe 回退到 mergedDetail已由 setValue 填充完整数据)
// 先取 setValue 填充的行数据作为基础
const baseRow = prescriptionList.value[rowIndex.value];
const newRow = {
...prescriptionList.value[rowIndex.value],
...baseRow,
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
quantity: item.quantity,
methodCode: item.methodCode,
rateCode: item.rateCode,
dispensePerDuration: item.dispensePerDuration,
dose: item.dose,
doseQuantity: item.doseQuantity,
executeNum: 1,
unitCode: item.unitCode,
unitCode_dictText: item.unitCodeName || '',
statusEnum: 1,
orgId: resolveOrgId(item.orderDetailInfos?.orgId || mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时保存 orgName确保树匹配不到时仍有中文名称可显示
@@ -1613,19 +1676,33 @@ function handleSaveGroup(orderGroupList) {
conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value,
encounterDiagnosisId: encounterDiagnosisId.value,
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || '1',
therapyEnum: baseRow?.therapyEnum || '1',
};
// 覆盖关键字段:优先使用 item 的值,其次 mergedDetail已由 setValue 填充),最后 baseRow
newRow.quantity = item.quantity ?? mergedDetail.quantity ?? baseRow.quantity;
newRow.methodCode = item.methodCode ?? mergedDetail.methodCode ?? baseRow.methodCode;
newRow.rateCode = item.rateCode ?? mergedDetail.rateCode ?? baseRow.rateCode;
newRow.dispensePerDuration = item.dispensePerDuration ?? mergedDetail.dispensePerDuration ?? baseRow.dispensePerDuration;
newRow.dose = item.dose ?? mergedDetail.dose ?? baseRow.dose;
newRow.doseQuantity = item.doseQuantity ?? mergedDetail.doseQuantity ?? baseRow.doseQuantity;
newRow.unitCode = item.unitCode ?? mergedDetail.unitCode ?? baseRow.unitCode;
newRow.unitCode_dictText = item.unitCodeName || mergedDetail.unitCodeName || baseRow.unitCode_dictText || '';
// 计算价格和总量
const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode);
// 🔧 Bug #403 修复:使用 newRow.unitCode已由 setValue 填充)而非 item.unitCode
// 使用 ?? 替代 || 计算 partPercent确保值为 0 时不会被错误替换
const finalUnitCode = newRow.unitCode;
const unitInfo = unitCodeList.value.find((k) => k.value == finalUnitCode);
const finalQuantity = newRow.quantity;
const partPercent = item.orderDetailInfos?.partPercent ?? mergedDetail.partPercent ?? baseRow.partPercent ?? 1;
if (unitInfo && unitInfo.type == 'minUnit') {
newRow.price = newRow.minUnitPrice;
newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity;
newRow.totalPrice = ((finalQuantity || 0) * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = finalQuantity || 0;
} else {
newRow.price = newRow.unitPrice;
newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity * (item.orderDetailInfos?.partPercent || mergedDetail.partPercent || 1);
newRow.totalPrice = ((finalQuantity || 0) * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = (finalQuantity || 0) * partPercent;
}
newRow.contentJson = JSON.stringify(newRow);

View File

@@ -19,11 +19,9 @@
<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>
@@ -49,6 +47,7 @@
import {computed, onBeforeMount, onMounted, provide, reactive, ref, watch,} from 'vue';
import Emr from './emr/index.vue';
import SummaryDrugApplication from './components/applicationShow/summaryDrugApplication.vue';
import inPatientBarDoctorFold from '@/components/patientBar/inPatientBarDoctorFold.vue';
import PatientList from '@/components/PatientList/patient-list.vue';
import {localPatientInfo, updateLocalPatientInfo} from './store/localPatient';
@@ -84,6 +83,7 @@ const currentPatientInfo = ref({});
const testApplicationRef = ref();
const examineApplicationRef = ref();
const surgeryApplicationRef = ref();
const summaryDrugApplicationRef = ref();
const bloodTtransfusionAapplicationRef = ref();
// 患者列表相关逻辑

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,11 @@
</div>
<div>
<span class="descriptions-item-label">全选</span>
<el-switch v-model="chooseAll" @change="handelSwicthChange" />
<el-switch
v-model="chooseAll"
:disabled="isDetails != '1'"
@change="handelSwicthChange"
/>
<el-button class="ml20 mr20" type="primary" @click="handleExecute"> 汇总领药 </el-button>
</div>
</div>
@@ -81,7 +85,7 @@
:deadline="deadline"
:therapyEnum="therapyEnum"
/>
<SummaryMedicineList v-else />
<SummaryMedicineList v-else ref="summaryMedicineRefs" :therapyEnum="therapyEnum" />
<!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick">
<el-tab-pane
v-for="tab in prescriptionTabs"
@@ -125,6 +129,7 @@ const therapyEnum = ref(undefined);
// 存储子组件引用的对象
const prescriptionRefs = ref();
const summaryMedicineRefs = ref();
const navigationButtons = inpatientNurseNavs;
@@ -160,24 +165,35 @@ function handleClick(tabName) {
}
function handleGetPrescription() {
prescriptionRefs.value.handleGetPrescription();
chooseAll.value = false;
if (isDetails.value == '1') {
prescriptionRefs.value?.handleGetPrescription();
} else {
summaryMedicineRefs.value?.handleGetPrescription();
}
}
function handelSwicthChange() {
if (chooseAll.value) {
proxy.$refs['prescriptionRefs'].selectAllRows();
function handelSwicthChange(value) {
if (!prescriptionRefs.value) {
chooseAll.value = false;
return;
}
if (value) {
prescriptionRefs.value.selectAllRows();
} else {
proxy.$refs['prescriptionRefs'].clearSelection();
prescriptionRefs.value.clearSelection();
}
}
function handleRadioChange(value) {
chooseAll.value = false;
if (value == '1') {
handleGetPrescription();
}
}
function handleTherapyChange() {
chooseAll.value = false;
handleGetPrescription();
}
@@ -216,4 +232,4 @@ provide('handleGetPrescription', (value) => {
:deep(.el-tabs__header) {
margin: 0;
}
</style>
</style>

View File

@@ -453,8 +453,14 @@ 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');
// 已有患者entranceType == 1不自动填充当前时间避免覆盖历史数据
// 新入科患者由后端默认返回当前时间,或由用户手动选择
interventionForm.value.startTime = '';
}
interventionForm.value.height = res.data.height;
interventionForm.value.weight = res.data.weight;

View File

@@ -459,10 +459,15 @@ function handleGetPrescription(skipAutoSelectAll = false) {
// 执行
function handleExecute() {
let list = getSelectRows();
if (list.length === 0) {
proxy.$modal.msgWarning('请选择需要执行的医嘱');
return;
}
let encounterIds = patientInfoList.value.map((i) => i.encounterId).join(',');
list = list.map((item) => {
return {
requestId: item.requestId,
encounterId: item.encounterId,
accountId: item.accountId,
adviceTable: item.adviceTable,
executeTimes: item.executeTimes,
@@ -471,14 +476,25 @@ function handleExecute() {
console.log(list, 'list');
adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => {
if (res.code == 200) {
// 仅当选中医嘱中包含耗材类医嘱时,才调用耗材批号匹配(排除纯药品医嘱场景)
const hasDevice = list.some((item) =>
String(item.adviceTable || '').includes('device'),
// 仅当选中医嘱中包含诊疗类医嘱(可能绑定耗材)时,才调用耗材批号匹配
// adviceTable 取值为 med_medication_request药品或 wor_service_request诊疗/耗材)
const hasServiceRequest = list.some((item) =>
String(item.adviceTable || '') === 'wor_service_request',
);
if (hasDevice) {
lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error);
});
if (hasServiceRequest) {
// 仅传入选中医嘱对应的 encounterId避免其他患者的耗材记录干扰
const selectedEncounterIds = [...new Set(list.map((item) => item.encounterId).filter(Boolean))];
if (selectedEncounterIds.length > 0) {
lotNumberMatch({ encounterIdList: selectedEncounterIds }, { skipErrorMsg: true })
.then((matchRes) => {
if (matchRes && matchRes.code !== 200) {
console.warn('lotNumberMatch returned error:', matchRes.msg);
}
})
.catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error);
});
}
}
// 刷新列表(不自动全选,保持用户操作前的选择状态)
handleGetPrescription(true);
@@ -486,6 +502,9 @@ function handleExecute() {
} else {
proxy.$modal.msgError(res.msg || '医嘱执行失败');
}
}).catch((error) => {
console.error('医嘱执行接口调用失败:', error);
proxy.$modal.msgError('医嘱执行失败,请稍后重试');
});
}

View File

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

View File

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

View File

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

View File

@@ -803,7 +803,7 @@
</el-dialog>
<!-- 手术计费弹窗 -->
<el-dialog :title="chargeDialogTitle" v-model="showChargeDialog" width="1400px" @close="closeChargeDialog" append-to-body>
<el-dialog :title="chargeDialogTitle" v-model="showChargeDialog" width="1400px" @close="closeChargeDialog" append-to-body destroy-on-close>
<div style="display: flex; justify-content: space-between; height: 80vh">
<div style="width: 100%; border: 1px solid #eee; position: relative">
<div style="padding: 10px; border: 1px solid #eee; height: 50px; border-left: 0">
@@ -829,7 +829,9 @@
</el-descriptions>
</div>
<div style="padding: 10px">
<prescriptionlist v-if="showChargeDialog" :patientInfo="chargePatientInfo" ref="prescriptionRef" />
<prescriptionlist :patientInfo="chargePatientInfo" ref="prescriptionRef"
:generateSourceEnum="1"
:sourceBillNo="chargePatientInfo.sourceBillNo" />
<div class="overlay" v-if="disabled"></div>
</div>
</div>
@@ -872,16 +874,59 @@ 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 { listUser } from '@/api/system/user'
import { deptTreeSelect } from '@/api/system/user'
import { listOperatingRoom } from '@/api/operatingroom'
import { getSurgery} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getTenantPage } from '@/api/system/tenant'
import {
getSurgerySchedulePage,
addSurgerySchedule,
updateSurgerySchedule,
deleteSurgerySchedule,
getSurgeryScheduleDetail
} from '@/api/surgicalschedule'
import { getSurgeryPage} from '@/views/inpatientDoctor/home/components/applicationShow/api.js'
import { getContract } from '@/views/inpatientDoctor/home/components/api.js'
import request from '@/utils/request'
import SurgeryCharge from '../charge/surgerycharge/index.vue'
import TemporaryMedical from './temporaryMedical.vue'
// 静默获取字典列表(跳过拦截器错误提示,手术室护士等角色可能无此权限)
function getTenantPageSilent(query) {
return request({
url: '/system/tenant/page',
method: 'get',
params: query,
skipErrorMsg: true
})
}
// 静默获取科室树(跳过拦截器错误提示)
function deptTreeSelectSilent(params = {}) {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: { typeEnum: 2, ...params },
skipErrorMsg: true
})
}
// 静默获取用户列表(跳过拦截器错误提示)
function listUserSilent(query) {
return request({
url: '/base-data-manage/practitioner/user-practitioner-page',
method: 'get',
params: query,
skipErrorMsg: true
})
}
// 静默获取手术室列表(跳过拦截器错误提示)
function listOperatingRoomSilent(query) {
return request({
url: '/base-data-manage/operating-room/list',
method: 'get',
params: query,
skipErrorMsg: true
})
}
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(true)
@@ -921,7 +966,7 @@ const form = reactive({
allergyRemark: undefined,
surgeryNature: undefined,
surgerySite: undefined,
incisionLevel: undefined,
incisionType: undefined,
surgeryLevel: undefined,
admissionTime: undefined,
@@ -1110,7 +1155,7 @@ onMounted(() => {
// 加载卫生机构列表
function loadOrgList() {
getTenantPage({ pageNo: 1, pageSize: 1000 })
getTenantPageSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || res.data || []
@@ -1130,7 +1175,7 @@ function loadOrgList() {
// 加载科室列表
function loadDeptList() {
deptTreeSelect()
deptTreeSelectSilent()
.then(res => {
if (res.code === 200) {
const tree = res.data?.records || res.data || []
@@ -1150,7 +1195,7 @@ function loadDeptList() {
// 加载医生列表
function loadDoctorList() {
listUser({ pageNo: 1, pageSize: 1000 })
listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1170,7 +1215,7 @@ function loadDoctorList() {
// 加载护士列表
function loadNurseList() {
listUser({ pageNo: 1, pageSize: 1000 })
listUserSilent({ pageNo: 1, pageSize: 1000 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1190,7 +1235,7 @@ function loadNurseList() {
// 加载手术室列表
function loadOperatingRoomList() {
listOperatingRoom({ pageNo: 1, pageSize: 1000, statusEnum: 1 })
listOperatingRoomSilent({ pageNo: 1, pageSize: 1000, statusEnum: 1 })
.then(res => {
if (res.code === 200) {
const records = res.data?.records || []
@@ -1388,8 +1433,8 @@ async function handleChargeCharge(row) {
orgId: userStore.organizationId || userStore.orgId || userStore.tenantId || 1,
// 添加账户ID
accountId: accountId,
// 添加手术申请单号用于追溯
sourceBillNo: row.applyId,
// 添加手术单号用于关联对应的手术医嘱
sourceBillNo: row.operCode,
//添加计费标志手术计费
generateSourceEnum: 6
}
@@ -1411,11 +1456,15 @@ async function handleChargeCharge(row) {
}
// 关闭计费弹窗
function closeChargeDialog() {
async function closeChargeDialog() {
// 先关闭 prescriptionlist 内所有已打开的项目字典 popover
if (prescriptionRef.value && prescriptionRef.value.closeAllPopovers) {
prescriptionRef.value.closeAllPopovers()
}
// 等待 Vue 完成 popover 可见性更新的 DOM 操作,
// 因为 el-popover 通过 teleport 渲染在 body 上,需要在 dialog 卸载前完成清理
await nextTick()
// 清空数据,避免下次打开时使用缓存
showChargeDialog.value = false
chargePatientInfo.value = {}
chargeSurgeryInfo.value = {}
@@ -1447,7 +1496,8 @@ function handleMedicalAdvice(row) {
role: userStore.roles[0],
effectiveOrgId : row.effectiveOrgId,
orgId: userStore.orgId,
positionId: userStore.orgId
positionId: userStore.orgId,
applyId: row.applyId // 手术申请单ID用于过滤关联医嘱
}
// 🔧 关键修复:如果已有提交的医嘱数据,并且是同一个患者的就诊,则使用保存的数据
@@ -1478,7 +1528,11 @@ function handleMedicalAdvice(row) {
// 先清空旧数据
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
temporarySigned.value = false // 🔧 重置签名状态
// 🔧 修复 Bug #446: 如果是同一 encounter 且已有提交的医嘱(有 requestId保留签名状态
const hasSubmittedAdvices = temporaryAdvices.value.length > 0 &&
temporaryAdvices.value[0]?.originalMedicine?.encounterId === row.visitId &&
temporaryAdvices.value.some(a => a.originalMedicine?.requestId);
temporarySigned.value = hasSubmittedAdvices; // 修复:根据已有数据状态设置,而非盲目重置
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
// 调用计费接口获取数据
@@ -1493,9 +1547,13 @@ function handleMedicalAdvice(row) {
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== row.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表)
if (item.requestId) return false;
// 根据药品请求ID去重避免重复显示
const itemId = item.requestId || item.id;
if (itemId && seenIds.has(itemId)) return false;
@@ -1683,15 +1741,27 @@ function handleTemporaryMedicalSubmit(data) {
}
})
// 同步更新计费药品列表:移除已生成医嘱的项目,避免数据重复显示
const submittedIds = new Set(
(data.temporaryAdvices || []).map(a => a.originalMedicine?.requestId || a.originalMedicine?.chargeItemId).filter(Boolean)
// 🔧 修复 Bug #445: 使用稳定的字段组合匹配已提交项目,而不是依赖可能为空的 requestId/chargeItemId
// 构建已提交项目的匹配键集合(药品名称 + 规格 + 数量)
const submittedKeys = new Set(
(data.temporaryAdvices || [])
.map(a => {
const om = a.originalMedicine || {}
const name = om.medicineName || om.adviceName || om.advice_name || a.adviceName || ''
const spec = om.specification || om.volume || ''
const qty = om.quantity || 0
return `${name}|||${spec}|||${qty}`
})
.filter(k => k !== '|||0') // 过滤掉空项
)
if (submittedIds.size > 0) {
temporaryBillingMedicines.value = (data.billingMedicines || []).filter(
m => !submittedIds.has(m.requestId || m.chargeItemId)
)
if (submittedKeys.size > 0) {
temporaryBillingMedicines.value = (temporaryBillingMedicines.value || []).filter(m => {
const key = `${m.medicineName || ''}|||${m.specification || ''}|||${m.quantity || 0}`
return !submittedKeys.has(key)
})
} else {
// 如果没有任何匹配键,清空待生成列表(所有项目都已提交)
temporaryBillingMedicines.value = []
}
@@ -1745,19 +1815,25 @@ function handleQuoteBilling() {
// 重新拉取计费药品数据
if (temporaryPatientInfo.value.visitId) {
temporaryMedicalLoading.value = true // 🔧 新增:开始加载
getPrescriptionList(temporaryPatientInfo.value.visitId).then((res) => {
getPrescriptionList(temporaryPatientInfo.value.visitId, 6, temporaryPatientInfo.value.operCode).then((res) => {
if (res.code === 200 && res.data) {
// 🔧 修复:先清空旧数据,避免数据累积
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目
// 🔧 修复 Bug #445: 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中)
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3)
if (item.adviceType !== 1) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
return medicineName && medicineName.trim() !== '';
if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId
if (item.requestId) return false;
return true;
})
// 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死
const maxItems = 100
@@ -1974,7 +2050,12 @@ function resetForm() {
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
const submitData = { ...form, orgId: userStore.orgId }
const submitData = {
...form,
orgId: userStore.orgId,
incisionLevel: form.incisionType
}
delete submitData.incisionType
if (!form.scheduleId) {
// 新增手术安排
addSurgerySchedule(submitData).then((res) => {
@@ -2020,7 +2101,7 @@ function handleFindApply() {
getSurgicalScheduleList()
}
// 获取手术申请列表(用于查找”弹窗)
// 获取手术申请列表(用于查找”弹窗)
function getSurgicalScheduleList() {
applyLoading.value = true
const params = { ...applyQueryParams }
@@ -2029,8 +2110,7 @@ function getSurgicalScheduleList() {
params.applyTimeEnd = params.applyTimeRange[1]
delete params.applyTimeRange
}
getSurgery(params).then((res) => {
// Check if data is nested under data.data or directly under data
getSurgeryPage(params).then((res) => {
const responseData = res.data.data || res.data
applyList.value = responseData.records || []
applyTotal.value = responseData.total || 0
@@ -2279,4 +2359,4 @@ function getRowClassName({ row, rowIndex }) {
border-bottom: 1px solid #d9ecff !important;
}
</style>
</style>

View File

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

View File

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