62 Commits

Author SHA1 Message Date
华佗
5a4c2b6354 Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
策略A:修改 clickRowDb 双击处理函数的编辑准入条件
- 原条件:row.statusEnum == 1 && !row.requestId(只允许从未保存的新医嘱编辑)
- 新条件:row.statusEnum == 1(允许所有待签发状态的医嘱编辑,包括护士退回的)
- 原因:护士退回医嘱时后端将状态重置为 DRAFT(1),但 requestId 仍存在,
  原条件因此拦截了退回医嘱,导致双击无响应
- 保存逻辑已支持有 requestId 的场景(dbOpType='2' 更新模式),无需额外修改

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

修复:
1. index.vue 新增 const therapyEnum = ref(undefined)
2. index.vue 新增 handleTherapyChange() 调用 handleGetPrescription() 刷新列表
3. index.vue 将 therapyEnum 作为 prop 传入 PrescriptionList
4. prescriptionList.vue 将本地 therapyEnum ref 改为 props 接收
2026-05-11 17:08:56 +08:00
赵云
e2f4996f47 Merge remote-tracking branch 'origin/华佗' into 华佗
# Conflicts:
#	openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/index.vue
2026-05-11 15:27:40 +08:00
赵云
37b57e8b12 Fix Bug #511: [住院医生工作站-临床医嘱] 护士退回的医嘱在医生站双击无法进入编辑模式,导致无法修改重发
根因: clickRowDb 双击编辑条件为 row.statusEnum == 1 && !row.requestId,
护士退回的医嘱 statusEnum 被重置为 1(DRAFT),但 requestId 仍存在(之前已保存过),
导致不满足 !row.requestId 条件,无法进入编辑模式。

修复: 移除 !row.requestId 限制,仅保留 statusEnum == 1 条件。
保存流程已支持两种场景:
- 新建医嘱(无requestId): dbOpType = '1' 创建
- 退回医嘱(有requestId): dbOpType = '2' 更新
2026-05-11 15:26:39 +08:00
wangjian963
9bd39c06e7 Merge remote-tracking branch 'origin/develop' into develop 2026-05-11 15:25:17 +08:00
赵云
bde42d6b14 fix: 恢复 Bug #497 的后端修改 + 数据库字段同步 (ALTER TABLE doc_request_form ADD COLUMN status) 2026-05-11 14:57:06 +08:00
关羽
01bf3177c9 fix: 还原 Bug #443/#475/#477/#486/#497 引入的 getRequestForm 编译错误 2026-05-11 14:40:07 +08:00
赵云
2a9f8376e6 fix: 完整回退 Bug #497 的后端修改(SQL/Java接口/Impl/Dto) 2026-05-11 14:13:49 +08:00
关羽
0774d9f877 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
根因:handleFocus/handleChange 中 categoryCode 的计算逻辑错误。当新增行未选择
医嘱类型时(row.adviceType 为 undefined),代码回退到 adviceQueryParams 的默认值并
匹配到具体药品分类(如西药 categoryCode='2'),导致搜索被限制在单一分类而非全局药库。

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:08:13 +08:00
赵云
cee38eceae Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
根因分析:
1. 非耗材类医嘱执行报"耗材库存"错误: handleExecute 中无条件调用 lotNumberMatch,
   后端会校验该就诊下所有待发放耗材库存,即使当前执行的是口服药等非耗材类医嘱
2. 全选联动异常: msgSuccess 在 handleGetPrescription 之前执行,数据刷新后
   defaultSelectAllRows 重新选中所有行,用户关闭弹窗后看到全选效果

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:07:49 +08:00
赵云
bbc740b6ce fix: 完全回退 Bug #497 引入的 drf.status 字段(数据库不存在) 2026-05-11 14:06:53 +08:00
关羽
256b986c0e Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
根因:handleViewDetail 为同步方法,点击详情时 getLocationInfo 尚未返回,
orgOptions 为空导致 recursionFun 无法将 targetDepartment ID 解析为科室名称。

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:01:23 +08:00
wangjian963
df6c5f3824 506 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
CommonConstants.AppointmentOrderStatus 常量 → OrderStatus 枚举重构
   新增枚举:0=患者取消 / 1=有效 / 2=系统取消 / 3=已完成
   退号流程加乐观锁防并发,slot 状态改回待约,退号日志独立事务 修复 XML 中 Integer 比较用字符串的问题
Bug #411 — 诊室过滤栏从科室下拉框改为诊室按钮组
2026-05-11 13:51:47 +08:00
关羽
08075c90e2 Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
根因分析:
1. el-collapse accordion 模式下快速切换分类时,连续的折叠/展开动画重叠,
   Element Plus 在动画过程中重新计算面板高度,导致高度跳变和白屏闪烁
2. 折叠容器缺少 overflow:hidden,动画过渡期间内容溢出造成闪烁

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:50:04 +08:00
赵云
c5820fcec2 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因:get-check 接口只接收 encounterId 参数,忽略前端传递的 startDate/endDate/status 筛选参数,
导致日期筛选和状态筛选全部失效。同类型的 get-inspection 接口已正确支持这些参数。
修复:在 controller 的 get-check 方法增加 startDate、endDate、status 三个 @RequestParam,
调用 5 参数重载的 service 方法,使筛选参数正确传递到 SQL 层。
前端 examineApplication.vue 已包含状态列、parseStatus 映射、状态筛选下拉框,无需修改。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:45 +08:00
赵云
fbe7f4f41f Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空
根因:projectWithDepartment() 在提交时会清空用户手动选择的发往科室,
且项目未配置执行科室时 findTreeItem 返回 null 导致校验失败。
同时 submit() 使用 item.positionId(可能为 undefined)作为执行科室。

修复:
1. 清空科室前保存用户手动选择的值(manualDept)
2. type=2(提交)且 findItem 不存在时,若用户已手动选择科室则恢复并允许通过
3. positionId 兜底使用 form.targetDepartment

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:48:22 +08:00
赵云
f9ab4c5688 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
修复三个问题:
1. 后端DTO字段名为Name(大写)而前端模板引用name(小写),导致组套名称显示为空,使用name || Name兼容
2. 后端未返回rangeCode_dictText字段,新增getRangeText()函数根据当前选中范围标签动态显示
3. 前端向/group-package-for-order接口发送searchKey参数但后端不支持,移除该多余参数
2026-05-11 12:06:55 +08:00
赵云
861129c9d4 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
1. 修复单位字段默认为空:selectChange中增加defaultUnitCode逻辑,优先取minUnitCode,回退到unitCode
2. 修复执行科室显示内码:统一positionId和el-option value为字符串类型,避免类型不匹配导致el-select无法匹配选项
3. 科室匹配增加String类型转换:find时用String(d.id)===String(patientOrgId)避免因类型不同找不到匹配科室
4. loadDepartmentOptions和getDiseaseInitLoc增加.catch优雅降级,避免API失败时页面阻断

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 12:04:36 +08:00
赵云
78a2dfa3fe Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击"取消执行"报错
根因分析:handleCancel 函数从 exePerformRecordList 提取 procedureId 时,
未过滤 null/空值,导致无效 procedureId 被发送到后端,引发 SQL 异常。
同时 therapyEnum 可能存在类型不一致问题。

修复内容:
1. 提取 procedureId 后增加 filter 过滤空值
2. 构建请求参数时再次过滤,确保不发无效 procedureId
3. therapyEnum 显式转为 Number 类型确保后端正确匹配过滤
4. producerIds 为空时增加用户提示
5. 增加 .catch 错误处理避免未捕获的 Promise rejection
2026-05-11 11:35:19 +08:00
赵云
98e5a0b984 Fix Bug #509: [门诊医生站-手术申请] 提交申请后列表未实时刷新展示数据,且提示语需优化
1. 修复列表不显示问题:SurgeryMapper.xml 中 sr.id IS NULL 条件导致已生成医嘱的手术被过滤,
   提交手术时 addSurgery() 会创建 category_enum=4 的 ServiceRequest,导致查询立即被排除。
   移除 sr.id IS NULL 过滤条件,使手术申请列表正常展示所有手术记录。

2. 修复提示语问题:将后端 addSurgery 返回消息从"手术信息添加成功"改为"手术申请提交成功!",
   updateSurgery 改为"手术申请修改成功!",前端直接使用后端返回消息。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:24:06 +08:00
e5c944069b bug 273 门诊医生站-》医嘱TAB页面:修改用药天数字段的值,总量字段的值未自动通过公式换算 补充:修改单词用量和用药频次时也自动换算总量字段 2026-05-11 11:11:30 +08:00
a68ffbfec4 去除多余的字符,修复语法问题 2026-05-11 10:00:15 +08:00
14f8a8b0a3 import 去掉多余逗号并略作格式整理 2026-05-11 09:53:02 +08:00
0f1e57227b 修复标签语法错误 2026-05-11 09:44:08 +08:00
赵云
98a370f3a2 fix: 修复 surgeryApplication.vue 多余的 </template> 标签 2026-05-11 09:31:02 +08:00
b2ce368749 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	openhis-ui-vue3/src/views/doctorstation/components/diagnosis/infectiousDiseaseReportDialog.vue
2026-05-11 09:25:32 +08:00
c691f82958 feat(diagnosis): 添加传染病报告对话框中的工作单位字段
- 在传染病报告对话框中新增工作单位输入框
- 添加了相应的工作单位表单项布局结构
- 集成了工作单位数据绑定功能
2026-05-11 09:24:59 +08:00
赵云
d2bfde7230 fix: 修复 Bug #412 引入的 Vue template 语法错误(缺少 el-row/el-col 标签) 2026-05-11 09:18:13 +08:00
赵云
62a09a8b94 Fix Bug #412: 门诊医生站:传染病报告卡保存失败,提示报错
根因:infectiousDiseaseReportDialog.vue 的 show() 函数将 cardNo 初始化为空字符串,
而后端 DTO 的 cardNo 字段有 @NotBlank 校验,导致保存时后端拒绝请求。
同仓库的 infectiousReport/index.vue 已有此修复(调用 getNextCardNo API),
但诊断流程使用的 infectiousDiseaseReportDialog.vue 漏掉了此修复。

修复:在 show() 函数中调用 getNextCardNo API 获取卡片编号,
API 失败时降级为 TEMP_ 前缀的临时卡号,与 infectiousReport/index.vue 保持一致。
2026-05-11 09:03:38 +08:00
关羽
10b2c58a3e Fix Bug #476: 住院医生工作-检查申请单界面缺失核心临床字段(紧急程度、过敏史、检查目的等)
在检查申请单开立界面新增5个核心临床字段(依次放在发往科室之后):
1. 紧急程度:el-select下拉选择(普通/急诊),区分急诊和普通检查
2. 过敏史:textarea输入,提示造影剂过敏风险
3. 检查目的:textarea输入,让执行科室医生了解临床背景
4. 病史摘要:textarea输入,补充患者病史信息
5. 期望检查时间:datetime选择器,实现精准调度

同步更新:
- medicalExaminations.vue: 表单模板 + form reactive对象新增字段
- examineApplication.vue: labelMap映射 + 详情弹窗紧急程度转换 + 修改弹窗新增字段 + 编辑/保存逻辑覆盖新字段
2026-05-11 08:45:13 +08:00
赵云
abc995523b Fix Bug #492: 【门诊手术安排】关闭"手术计费"主弹窗后,项目字典选择列表依然残留悬浮在界面上
在 prescriptionlist 组件中新增 closeAllPopovers 方法,关闭手术计费弹窗时
先关闭所有行悬浮的项目字典下拉弹窗,避免主弹窗关闭后残留

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 08:45:05 +08:00
赵云
c0438c0288 Fix Bug #496: 【住院医生工作站-检查申请】检查申请列表字段命名不规范及单号生成规则不符合医疗行业标准
将检查申请列表及详情中的"处方号"统一修改为"申请单号",涉及列表表头、详情弹窗和打印内容三处

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 08:44:25 +08:00
关羽
13731c2373 Fix Bug #475: 【住院医生工作站】开立检查申请单报错"请先配置当前时间段的执行科室"后,系统仍生成申请记录
将执行科室配置校验提前到数据库写入操作之前,避免校验失败时已写入RequestForm记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:24:08 +08:00
关羽
d63b00fd33 Fix Bug #481: [住院护士站-医嘱执行] 药品"注射用头孢哌酮钠舒巴坦钠"库存充足,但执行医嘱时提示库存不足
在 checkExeMedInventory 方法中,原代码使用 findFirst() 只取第一个批次的库存
进行校验,导致同一库房多个批次的库存总量未被聚合计算。改为 collect(Collectors.toList())
收集所有匹配批次,然后用 Stream reduce 聚合总可用库存后再与需求量比较。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:22:42 +08:00
赵云
c60cd6878e Fix Bug #413: 医生个人报卡管理核心缺陷:医生个人报卡编辑/查看界面与门诊医生站登记报卡界面设计不统一 2026-05-11 00:21:29 +08:00
赵云
cf50f8968a Fix Bug #478: 【住院医生工作站-检验申请】点击"详情"查看检验单时,"发往科室"字段回显异常(显示为"-")
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:09:19 +08:00
赵云
94fac8e257 Fix Bug #495: 【医嘱闭环】已校对医嘱无法流转至"医嘱执行"界面,导致费用无法提交执行
医嘱执行模块 prescriptionList.vue 中 try-catch 被注释掉,导致数据处理
异常时静默失败且 loading 状态无法重置,页面显示空数据无报错。
- 恢复 try-catch 错误处理,捕获 res.data.records 空值及数据处理异常
- 添加 .catch() 处理 API 接口级别失败,重置 loading 并清空列表
- 修复无患者时 loading 状态未重置的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:02:18 +08:00
赵云
f162134156 Fix Bug #411: 智能分诊排队:底部操作控制区"过滤栏"功能实现与PRD需求不符(误设为科室过滤)
将底部过滤栏从"就诊科室快速过滤栏"改为"诊室快速过滤栏":
- UI文案:过滤栏标题、下拉框placeholder均改为诊室相关
- 数据源:移除 getLocationTree() 科室树API调用,改为从队列/候选池数据中动态提取诊室列表
- 过滤逻辑:改为按诊室名称(room字段)过滤,支持本科室下不同诊室快速切换
- 后端API调用不再依赖过滤栏选择,改用队列数据自身的organizationId

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:00:24 +08:00
关羽
d866d898df Fix Bug #506: 门诊挂号:门诊诊前退号后,数据库多表状态值变更与 PRD 定义不符
在 syncAppointmentReturnStatus 方法中:
1. 退号时同步将 order_main.pay_status 设为 0(未支付),修复退费后 pay_status 仍为 1 的问题
2. cancel_reason 固定使用标准化值"门诊退号",确保与 PRD 定义一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 23:58:40 +08:00
关羽
6725cef643 Fix Bug #477: 住院医生工作站-住院检查申请详情弹窗中"发往科室"字段显示为短横线(-),未正常获取数据
修复 examineApplication.vue 中 recursionFun 函数的空指针异常:
1. 增加 orgOptions.value 数组有效性校验,防止接口未返回数据时崩溃
2. 增加 obj.children 的 Array.isArray 检查,原代码直接访问 children.length 在 children 为 undefined 时抛 TypeError
3. 匹配成功后增加 break 提前退出循环
4. handleViewDetail 增加 targetDepartment 存在性检查,递归查找失败时回退显示原始 ID 值
2026-05-10 23:56:39 +08:00
赵云
7790a64eab Fix Bug #479: [住院护士站-三测单] 体征录入模块缺少"录入日期"字段,导致无法补录历史体征数据
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 23:53:18 +08:00
赵云
49fa1c9b90 Fix Bug #493: 【住院医生工作站-临床医嘱-检验申请】项目未维护执行科室时,医生手动选择发往科室后仍报错且数据被清空 2026-05-10 23:50:43 +08:00
关羽
e0a035204e Fix Bug #487: 【临床医嘱】诊疗类医嘱签发后,列表状态未实时刷新为"已签发"
诊疗类医嘱(handService)签发时仅依赖saveOrUpdate更新statusEnum,
但该方式对已有记录可能未正确将statusEnum更新为ACTIVE(2)。
修复:在handService方法末尾使用LambdaUpdateWrapper批量显式更新
所有已处理ServiceRequest的statusEnum为ACTIVE(签发)/DRAFT(保存),
与ServiceRequestServiceImpl中activeStatusEnum/updateDraftStatusBatch
等方法的实现模式保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:20:43 +08:00
关羽
349b0453c8 Fix Bug #480: [住院护士站-医嘱执行] 非耗材类医嘱执行报"耗材库存"错误且全选逻辑联动异常
1. 修复模板结构错误:删除premature的</template>和多余的</div>标签,确保el-table正确渲染
2. 新增selectedRowIds独立维护选中行ID集合,不再依赖el-table内部selection状态,避免执行选中时联动触发全选
3. 更新所有选择事件处理器同步维护selectedRowIds
4. 补充index.vue缺失的ref/nextTick/provide导入

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:14:00 +08:00
关羽
3ddd74d679 Fix Bug #494: 住院医生工作站-检查申请:"申请单名称"字段显示为通用名称,未展示具体检查项目名称
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:06:16 +08:00
关羽
9829843b3e Fix Bug #273: 门诊医生站-》医嘱TAB页面:修改用药天数字段的值,总量字段的值未自动通过公式换算
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:05:37 +08:00
关羽
0d95cc1341 Fix Bug #389: 住院护士站-》医嘱校对:界面筛选条件失效:勾选"临时"医嘱仍显示"长期"医嘱数据
前端therapyEnum参数在type.value为undefined时会被序列化为字符串"undefined"传递给后端,
导致后端无法正确解析而跳过过滤条件。修复为条件展开语法:仅在type.value有值时才传递therapyEnum参数,
确保"全部"筛选时不传该字段以获取全部医嘱。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:07:17 +08:00
关羽
1dfceeaf46 Fix Bug #389: 住院护士站-》医嘱校对:界面筛选条件失效:勾选"临时"医嘱仍显示"长期"医嘱数据
根因:therapyEnum 参数映射逻辑完全颠倒。
原代码:type.value === 1 ? undefined : type.value === 2 ? 1 : 2
- 选择"长期"(1)时传 undefined(不传,无过滤)
- 选择"临时"(2)时传 1(长期值)
- 选择"全部"时传 2(临时值)

修复:直接传 type.value,与后端 therapyEnum 枚举一致:
- undefined → 全部 / 1 → 长期 / 2 → 临时

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:06:21 +08:00
关羽
1b79df4f93 Fix Bug #488: 【临床医嘱】双击编辑待签发医嘱,医嘱类型回显为数字且点击确认报接口错误
- 修复 handleSaveSign 中 getBindDevice 调用时 itemNo 可能为 undefined 导致的后端报错 "Required request parameter 'itemNo' for method parameter type String is not present":增加 itemNo 空值检查,为空时 console.warn 跳过调用而非发送无效请求
- 移除模板中两处调试残留:console.log 表达式渲染到页面(类型列和频次/用法列)
- 修复签发失败处理中截断的 conso; 语法错误
2026-05-10 16:05:42 +08:00
关羽
f62a280dfc Fix Bug #390: 住院护士站-医嘱执行:通过住院号检索无法定位/筛选患者
原 handleSearch 调用 reloadAllPatients 仅尝试刷新已展开的病区节点,
对懒加载树不可靠。改为递增 treeKey 强制树组件完全重新渲染,
触发 loadNode/loadPatientList 重新从后端拉取数据并传入 searchKey 过滤。
2026-05-10 16:05:17 +08:00
关羽
e5d949a740 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:02:35 +08:00
关羽
4bd0d2dcc8 Fix Bug #486: [住院医生工作站-临床医嘱] 医嘱检索框不支持全局模糊搜索,未选"医嘱类型"时检索结果为空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:02:20 +08:00
赵云
6451c308c2 Fix Bug #498: 【住院医生工作站-检查申请】检查申请列表操作项过于单一,缺失修改/作废/打印/看报告等核心临床操作
- 根据申请单状态动态展示操作按钮(详情/修改/删除/撤回/打印/看报告)
- 待签发状态:显示修改、删除按钮
- 已签发状态:显示撤回按钮
- 已校对/待接收状态:显示打印按钮
- 已接收/已检查/已出报告状态:显示打印、看报告按钮
- 新增修改申请单弹窗和报告查看弹窗
- 新增删除、撤回申请单 API 调用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:23:32 +08:00
关羽
772ec5537c Fix Bug #504: 【住院医生工作站-临床医嘱】护士退回药品医嘱后,医生修改并保存时提示"未匹配到库存信息"
根因分析:
1. SQL查询 getRegRequestBaseInfo 未返回 medication_id/adviceDefinitionId 字段,
   退回医嘱的 adviceDefinitionId 为 null,导致库存校验查询无法匹配到库存记录
2. 退回医嘱可能缺少 locationId,严格的 locationId 匹配导致校验失败

修复方案:
1. AdviceManageAppMapper.xml:在三个UNION查询中分别添加 medication_id/device_def_id/activity_id AS advice_definition_id
2. AdviceUtils.checkInventory():
   - 过滤 null adviceDefinitionId,避免SQL查询异常
   - 所有adviceDefinitionId为null时跳过库存校验
   - 退回医嘱单个adviceDefinitionId为null时跳过该校验项
   - 添加 locationId 容错匹配(为null时跳过location匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 12:28:19 +08:00
赵云
af6494c806 Fix Bug #508: [住院护士站-住院记账-补费] 点击"划价组套"按钮无任何响应,无法选择组套项目
划价组套选择对话框嵌套在补费弹窗内部,缺少 append-to-body 属性导致
Dialog 被渲染在外层弹窗 DOM 内,z-index 层级被遮挡而不可见。
添加 append-to-body 使其挂载到 body 下,确保正常显示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:48:31 +08:00
荀彧
2901dafe10 Fix Bug #507: [住院护士站-住院记账-补费] 项目单位未获取、执行科室显示内码且缺乏默认/模糊搜索逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:22:27 +08:00
华佗
ffc2562aea Fix Bug #505: 【业务逻辑缺陷】药品医嘱已由药房发药,护士仍能在"医嘱校对"模块执行"退回"操作
前后端双重校验防止已发药医嘱被退回:
1. 后端 InpatientAdviceDto 新增 dispenseStatus 字段,Mapper SQL LEFT JOIN med_medication_dispense 获取发药状态
2. 后端 adviceReject 方法增加前置校验,已发药(COMPLETED)的医嘱直接拒绝退回
3. 前端 prescriptionList.vue handleCancel 方法增加 dispenseStatus 校验,已发药医嘱点击退回时弹窗提示
2026-05-10 11:17:35 +08:00
赵云
5da537f863 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击“取消执行”报错 2026-05-10 11:13:25 +08:00
赵云
facbe7cd44 Fix Bug #501: 【住院护士站-医嘱执行】医嘱执行页面点击"取消执行"报错
取消执行时 procedureIds 数组可能为空导致后端 SQL 异常,改为从
exePerformRecordList 直接提取 procedureId,并增加空数据校验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:11:45 +08:00
赵云
283d25642a Fix Bug #500: 【门诊医生站】检查申请右侧"检查项目分类"切换时,界面出现明显抖动/闪烁
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:11:45 +08:00
关羽
7b17a66214 Fix Bug #503: 【住院发退药】发药明细与发药汇总单数据触发时机不一致,存在业务脱节风险
在 selectEncounterInfoListPage 和 selectMedicineDispenseOrderPage 两个查询中增加
summary_no IS NOT NULL 过滤条件,使发药明细单仅在护士执行"汇总发药申请"后才显示记录,
与发药汇总单保持一致的触发时机。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:07:21 +08:00
44 changed files with 958 additions and 557 deletions

View File

@@ -10,7 +10,7 @@ import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService; import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto; import com.openhis.web.appointmentmanage.dto.TicketDto;
import com.openhis.common.constant.CommonConstants.SlotStatus; import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus; import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -198,10 +198,11 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) { if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号"); dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) { } else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) { // order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
dto.setStatus("已取号"); if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else { } else {
dto.setStatus("已预约"); dto.setStatus("已预约");
} }
@@ -372,10 +373,11 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) { if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号"); dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) { } else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) { // order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
dto.setStatus("已取号"); if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
dto.setStatus("已退号"); dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else { } else {
dto.setStatus("已预约"); dto.setStatus("已预约");
} }

View File

@@ -135,7 +135,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
BeanUtils.copyProperties(orgLocQueryDto, orgLoc); BeanUtils.copyProperties(orgLocQueryDto, orgLoc);
Long activityDefinitionId = orgLoc.getActivityDefinitionId(); Long activityDefinitionId = orgLoc.getActivityDefinitionId();
String activityName = activityDefinitionId != null ? activityDefinitionMapper.selectById(activityDefinitionId).getName() : ""; String activityName = activityDefinitionId != null
? activityDefinitionMapper.selectById(activityDefinitionId).getName() : "";
List<OrganizationLocation> organizationLocationList = List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getActivityDefinitionId()); organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getActivityDefinitionId());
@@ -147,8 +148,8 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
for (OrganizationLocation organizationLocation : organizationLocationList) for (OrganizationLocation organizationLocation : organizationLocationList)
if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(), if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(),
orgLoc.getStartTime(), orgLoc.getEndTime())) { orgLoc.getStartTime(), orgLoc.getEndTime())) {
String organizationName = Organization org = organizationService.getById(organizationLocation.getOrganizationId());
organizationService.getById(organizationLocation.getOrganizationId()).getName(); String organizationName = org != null ? org.getName() : "未知科室";
return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime() return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime()
+ CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突"); + CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突");
} }

View File

@@ -2,6 +2,7 @@ package com.openhis.web.chargemanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
@@ -329,16 +330,14 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
} }
} }
// 如果本次门诊挂号来自预约签到,同步预约订单与号源槽位状态改为已退号 // 退费成功后,同步回滚预约订单状态及号源;同时移除分诊队列
Long refundOrderMainId = null;
if (result != null && result.getCode() == 200) { if (result != null && result.getCode() == 200) {
syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason()); refundOrderMainId = syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason());
// 同步移除分诊队列中的记录
removeTriageQueueItem(byId.getId()); removeTriageQueueItem(byId.getId());
} }
// 退号日志独立事务写入,无论退费成功与否均记录
// 记录退号日志 recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon, refundOrderMainId);
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
// 2025/05/05 该处保存费用项后,会通过统一收费处理进行收费 // 2025/05/05 该处保存费用项后,会通过统一收费处理进行收费
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"})); return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
@@ -435,8 +434,6 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
// 通过患者、科室、日期查找关联的预约订单 // 通过患者、科室、日期查找关联的预约订单
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>() LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId()) .eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.orderByDesc(Order::getUpdateTime) .orderByDesc(Order::getUpdateTime)
.orderByDesc(Order::getCreateTime) .orderByDesc(Order::getCreateTime)
.last("LIMIT 1"); .last("LIMIT 1");
@@ -590,20 +587,25 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
} }
/** /**
* 同步预约号源状态为已退号 * 诊前退号:回滚预约订单、号源槽位、号源池统计
* 说明: *
* 1) 门诊退号主流程不依赖该步骤成功与否,因此此方法内部异常仅记录日志,不向上抛出。 * <p>处理四件事:
* 2) 通过患者、科室、日期以及状态筛选最近一条预约订单,尽量避免误匹配。 * <ol>
* <li>order_main → status=0(患者取消), pay_status=3(已退费), 写入取消时间和原因</li>
* <li>adm_schedule_slot → status=0(待约), order_id=NULL(释放号源)</li>
* <li>adm_schedule_pool → 重算统计值 + version+1</li>
* <li>返回 order_main.id 供 refund_log 关联</li>
* </ol>
*
* <p>异常仅记录日志不向上抛,不影响主流程返回成功。
*/ */
private void syncAppointmentReturnStatus(Encounter encounter, String reason) { private Long syncAppointmentReturnStatus(Encounter encounter, String reason) {
if (encounter == null || encounter.getPatientId() == null) { if (encounter == null || encounter.getPatientId() == null) {
return; return null;
} }
try { try {
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>() LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId()) .eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.orderByDesc(Order::getUpdateTime) .orderByDesc(Order::getUpdateTime)
.orderByDesc(Order::getCreateTime) .orderByDesc(Order::getCreateTime)
.last("LIMIT 1"); .last("LIMIT 1");
@@ -625,35 +627,55 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
Order appointmentOrder = orderService.getOne(queryWrapper, false); Order appointmentOrder = orderService.getOne(queryWrapper, false);
if (appointmentOrder == null) { if (appointmentOrder == null) {
return; return null;
} }
Date now = new Date(); // 只有有效订单(1)才能退号
if (!CommonConstants.AppointmentOrderStatus.RETURNED.equals(appointmentOrder.getStatus())) { if (!OrderStatus.ACTIVE.getValue().equals(appointmentOrder.getStatus())) {
Order updateOrder = new Order(); log.warn("退号跳过:订单状态非有效, orderId={}, status={}",
updateOrder.setId(appointmentOrder.getId()); appointmentOrder.getId(), appointmentOrder.getStatus());
updateOrder.setStatus(CommonConstants.AppointmentOrderStatus.RETURNED); return null;
updateOrder.setCancelTime(now); }
updateOrder.setCancelReason(
StringUtils.isNotEmpty(reason) ? reason : "门诊退号"); // 乐观锁更新WHERE version = 旧值,防并发重复退号
updateOrder.setUpdateTime(now); boolean updated = orderService.update(
orderService.updateById(updateOrder); new LambdaUpdateWrapper<Order>()
.set(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue())
.set(Order::getPayStatus, PaymentStatus.REFUND_ALL.getValue())
.set(Order::getCancelTime, new Date())
.set(Order::getCancelReason,
StringUtils.isNotEmpty(reason) ? reason : "诊前退号")
.set(Order::getUpdateTime, new Date())
.setSql("version = version + 1")
.eq(Order::getId, appointmentOrder.getId())
.eq(Order::getVersion, appointmentOrder.getVersion())
);
if (!updated) {
log.warn("退号乐观锁冲突,订单已被其他操作修改, orderId={}", appointmentOrder.getId());
return null;
} }
Long slotId = appointmentOrder.getSlotId(); Long slotId = appointmentOrder.getSlotId();
if (slotId == null) { if (slotId == null) {
return; return appointmentOrder.getId();
} }
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.RETURNED); int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.AVAILABLE);
if (slotRows > 0) { if (slotRows > 0) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId); Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) { if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId); schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
} }
} }
return appointmentOrder.getId();
} catch (Exception e) { } catch (Exception e) {
log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e); log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e);
return null;
} }
} }
@@ -672,22 +694,29 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
} }
/** /**
* 记录退号日志 * 记录退号日志(独立事务)。
*
* <p>REQUIRES_NEW 确保即使主事务回滚,退号审计日志也不丢失。
* orderMainId 优先使用 order_main.id若退费失败则 fallback 到 encounterId。
* *
* @param cancelRegPaymentDto 退号请求对象 * @param cancelRegPaymentDto 退号请求对象
* @param encounter 就诊信息 * @param encounter 就诊信息
* @param result 退号结果 * @param result 退号结果
* @param paymentRecon 支付对账信息 * @param paymentRecon 支付对账信息
* @param orderMainId 预约订单主键order_main.id用于关联业务数据
*/ */
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordRefundLog(CancelRegPaymentDto cancelRegPaymentDto, public void recordRefundLog(CancelRegPaymentDto cancelRegPaymentDto,
Encounter encounter, Encounter encounter,
R<?> result, R<?> result,
PaymentReconciliation paymentRecon) { PaymentReconciliation paymentRecon,
Long orderMainId) {
RefundLog refundLog = new RefundLog(); RefundLog refundLog = new RefundLog();
try { try {
// 1. 订单ID唯一 // 1. 订单ID关联 order_main.id
String orderId = String.valueOf(cancelRegPaymentDto.getEncounterId()); String orderId = orderMainId != null
? String.valueOf(orderMainId)
: String.valueOf(cancelRegPaymentDto.getEncounterId());
refundLog.setOrderId(orderId); refundLog.setOrderId(orderId);
// 已存在则不重复插入(防止唯一约束异常) // 已存在则不重复插入(防止唯一约束异常)

View File

@@ -418,7 +418,7 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
// 清除相关缓存 // 清除相关缓存
clearSurgeryAppCache(surgery); clearSurgeryAppCache(surgery);
return R.ok(surgeryId, MessageUtils.createMessage(PromptMsgConstant.Common.M00001, new Object[]{"手术信息"})); return R.ok(surgeryId, "手术申请提交成功!");
} }
/** /**
@@ -497,7 +497,7 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
// 清除相关缓存 // 清除相关缓存
clearSurgeryAppCache(surgery); clearSurgeryAppCache(surgery);
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"手术信息"})); return R.ok(null, "手术申请修改成功!");
} }
/** /**

View File

@@ -83,9 +83,14 @@ public class AdviceUtils {
* @return 提示信息 * @return 提示信息
*/ */
public String checkInventory(List<AdviceSaveDto> adviceSaveList) { public String checkInventory(List<AdviceSaveDto> adviceSaveList) {
// 医嘱定义ID集合 // 医嘱定义ID集合过滤null值避免SQL查询异常
List<Long> adviceDefinitionIdList List<Long> adviceDefinitionIdList
= adviceSaveList.stream().map(AdviceSaveDto::getAdviceDefinitionId).collect(Collectors.toList()); = adviceSaveList.stream().map(AdviceSaveDto::getAdviceDefinitionId)
.filter(id -> id != null).collect(Collectors.toList());
// 🔧 Bug #504 修复如果所有adviceDefinitionId都为null跳过库存校验
if (adviceDefinitionIdList.isEmpty()) {
return null;
}
// 医嘱库存集合 // 医嘱库存集合
List<AdviceInventoryDto> adviceInventoryList List<AdviceInventoryDto> adviceInventoryList
= doctorStationAdviceAppMapper.getAdviceInventory(null, adviceDefinitionIdList, = doctorStationAdviceAppMapper.getAdviceInventory(null, adviceDefinitionIdList,
@@ -99,6 +104,10 @@ public class AdviceUtils {
= this.subtractInventory(adviceInventoryList, adviceDraftInventoryList); = this.subtractInventory(adviceInventoryList, adviceDraftInventoryList);
// 检查库存 // 检查库存
for (AdviceSaveDto saveDto : adviceSaveList) { for (AdviceSaveDto saveDto : adviceSaveList) {
// 🔧 Bug #504 修复退回医嘱可能adviceDefinitionId为空跳过校验
if (saveDto.getAdviceDefinitionId() == null) {
continue;
}
boolean matched = false; boolean matched = false;
for (AdviceInventoryDto inventoryDto : adviceInventory) { for (AdviceInventoryDto inventoryDto : adviceInventory) {
// 匹配条件adviceDefinitionId, adviceTableName, locationId, lotNumber 同时相等 // 匹配条件adviceDefinitionId, adviceTableName, locationId, lotNumber 同时相等
@@ -108,10 +117,12 @@ public class AdviceUtils {
|| saveDto.getLotNumber().equals(inventoryDto.getLotNumber()); || saveDto.getLotNumber().equals(inventoryDto.getLotNumber());
boolean tableNameMatch = StringUtils.isEmpty(saveDto.getAdviceTableName()) boolean tableNameMatch = StringUtils.isEmpty(saveDto.getAdviceTableName())
|| inventoryDto.getItemTable().equals(saveDto.getAdviceTableName()); || inventoryDto.getItemTable().equals(saveDto.getAdviceTableName());
// if (saveDto.) // 🔧 Bug #504 修复退回医嘱可能locationId为空跳过location匹配
boolean locationMatch = saveDto.getLocationId() == null
|| inventoryDto.getLocationId().equals(saveDto.getLocationId());
if (inventoryDto.getItemId().equals(saveDto.getAdviceDefinitionId()) if (inventoryDto.getItemId().equals(saveDto.getAdviceDefinitionId())
&& tableNameMatch && tableNameMatch
&& inventoryDto.getLocationId().equals(saveDto.getLocationId()) && lotNumberMatch) { && locationMatch && lotNumberMatch) {
matched = true; matched = true;
// 检查库存是否充足 // 检查库存是否充足
BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity(); BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity();
@@ -167,22 +178,26 @@ public class AdviceUtils {
// 生命提示信息集合 // 生命提示信息集合
List<String> tipsList = new ArrayList<>(); List<String> tipsList = new ArrayList<>();
for (MedicationRequestUseExe medicationRequestUseExe : medUseExeList) { for (MedicationRequestUseExe medicationRequestUseExe : medUseExeList) {
Optional<AdviceInventoryDto> matchedInventory = adviceInventory.stream() // 聚合同一位置所有批次的库存总量
List<AdviceInventoryDto> matchedInventories = adviceInventory.stream()
.filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId()) .filter(inventoryDto -> medicationRequestUseExe.getMedicationId().equals(inventoryDto.getItemId())
&& CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable()) && CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(inventoryDto.getItemTable())
&& medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId()) && medicationRequestUseExe.getPerformLocation().equals(inventoryDto.getLocationId())
// 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件 // 如果选择了具体的批次号,校验库存时需要加上批次号的匹配条件
&& (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber()) && (StringUtils.isEmpty(medicationRequestUseExe.getLotNumber())
|| medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber()))) || medicationRequestUseExe.getLotNumber().equals(inventoryDto.getLotNumber())))
.findFirst(); .collect(Collectors.toList());
// 匹配到库存信息 // 匹配到库存信息
if (matchedInventory.isPresent()) { if (!matchedInventories.isEmpty()) {
AdviceInventoryDto inventoryDto = matchedInventory.get(); // 聚合所有批次的可用库存
if ((medicationRequestUseExe.getExecuteTimesNum() BigDecimal totalQuantity = matchedInventories.stream()
.multiply(medicationRequestUseExe.getMinUnitQuantity())) .map(dto -> dto.getQuantity() != null ? dto.getQuantity() : BigDecimal.ZERO)
.compareTo(inventoryDto.getQuantity()) > 0) { .reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal requestQuantity = medicationRequestUseExe.getExecuteTimesNum()
.multiply(medicationRequestUseExe.getMinUnitQuantity());
if (requestQuantity.compareTo(totalQuantity) > 0) {
tipsList tipsList
.add("" + medicationRequestUseExe.getBusNo() + "】在" + inventoryDto.getLocationName() + "库存不足"); .add("" + medicationRequestUseExe.getBusNo() + "】在" + matchedInventories.get(0).getLocationName() + "库存不足");
} }
} else { } else {
tipsList.add("" + medicationRequestUseExe.getBusNo() + "】未匹配到库存信息"); tipsList.add("" + medicationRequestUseExe.getBusNo() + "】未匹配到库存信息");

View File

@@ -206,6 +206,8 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum())); e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum()));
// 请求状态 // 请求状态
e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus())); e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus()));
// 发药状态
e.setDispenseStatus_enumText(EnumUtils.getInfoByValue(DispenseStatus.class, e.getDispenseStatus()));
// 性别枚举 // 性别枚举
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum())); e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
// 计算年龄 // 计算年龄
@@ -356,6 +358,17 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
medRequestList.add(item); medRequestList.add(item);
} }
} }
// 校验药品医嘱是否已发药,已发药的医嘱不允许退回
if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
List<MedicationDispense> dispenseList = medicationDispenseService.list(
new LambdaQueryWrapper<MedicationDispense>()
.in(MedicationDispense::getMedReqId, medReqIds)
.eq(MedicationDispense::getStatusEnum, DispenseStatus.COMPLETED.getValue()));
if (!dispenseList.isEmpty()) {
return R.fail("该医嘱已发药,无法退回");
}
}
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date(); Date checkDate = new Date();
if (!serviceRequestList.isEmpty()) { if (!serviceRequestList.isEmpty()) {

View File

@@ -255,4 +255,8 @@ public class InpatientAdviceDto {
/** 总价 */ /** 总价 */
private BigDecimal totalPrice; private BigDecimal totalPrice;
/** 发药状态 */
private Integer dispenseStatus;
private String dispenseStatus_enumText;
} }

View File

@@ -1991,7 +1991,7 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
Order appointmentOrder = iOrderService.getOne( Order appointmentOrder = iOrderService.getOne(
new LambdaQueryWrapper<Order>() new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounterFormData.getPatientId()) .eq(Order::getPatientId, encounterFormData.getPatientId())
.eq(Order::getStatus, CommonConstants.AppointmentOrderStatus.CHECKED_IN) .eq(Order::getStatus, OrderStatus.ACTIVE.getValue()) // 有效订单(1)
.eq(Order::getDeleteFlag, "0") .eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime) .orderByDesc(Order::getCreateTime)
.last("LIMIT 1") .last("LIMIT 1")
@@ -2114,11 +2114,11 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
Long queuePoolId = null; Long queuePoolId = null;
Long queueSlotId = null; Long queueSlotId = null;
try { try {
// 查询患者当天的待签到预约订单status = 1 或 2 表示已预约或已取号) // 查询患者当天有效订单(1);已取消(0/2)和已完成(3)的不参与排队
Order order = iOrderService.getOne( Order order = iOrderService.getOne(
new LambdaQueryWrapper<Order>() new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId()) .eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, 1, 2) // 1=BOOKED 已预约, 2=CHECKED_IN 已取号 .eq(Order::getStatus, OrderStatus.ACTIVE.getValue()) // 有效(1)
.eq(Order::getDeleteFlag, "0") .eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime) .orderByDesc(Order::getCreateTime)
.last("LIMIT 1") .last("LIMIT 1")

View File

@@ -511,6 +511,9 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
// 签发操作 // 签发操作
boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType); boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType);
// 收集已处理的requestId用于批量更新状态
List<Long> processedRequestIds = new ArrayList<>();
// 声明长期医嘱诊疗请求 // 声明长期医嘱诊疗请求
ServiceRequest longServiceRequest; ServiceRequest longServiceRequest;
// 新增 + 修改 (长期医嘱) // 新增 + 修改 (长期医嘱)
@@ -555,6 +558,9 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
} }
iServiceRequestService.saveOrUpdate(longServiceRequest); iServiceRequestService.saveOrUpdate(longServiceRequest);
if (longServiceRequest.getId() != null) {
processedRequestIds.add(longServiceRequest.getId());
}
} }
// 声明临时医嘱诊疗请求 // 声明临时医嘱诊疗请求
@@ -603,6 +609,9 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
} }
} }
iServiceRequestService.saveOrUpdate(tempServiceRequest); iServiceRequestService.saveOrUpdate(tempServiceRequest);
if (tempServiceRequest.getId() != null) {
processedRequestIds.add(tempServiceRequest.getId());
}
// 保存时,保存诊疗费用项 // 保存时,保存诊疗费用项
if (is_save) { if (is_save) {
@@ -654,6 +663,14 @@ 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));
}
} }
/** /**

View File

@@ -434,7 +434,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return new java.util.ArrayList<>(); // 返回空列表而不是查询数据库 return new java.util.ArrayList<>(); // 返回空列表而不是查询数据库
} }
List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate, status); List<RequestFormQueryDto> requestFormList = requestFormManageAppMapper.getRequestForm(encounterId, typeCode, startDate, endDate, status, null);
for (RequestFormQueryDto requestFormQueryDto : requestFormList) { for (RequestFormQueryDto requestFormQueryDto : requestFormList) {
// 查询处方详情 // 查询处方详情
List<RequestFormDetailQueryDto> requestFormDetail = List<RequestFormDetailQueryDto> requestFormDetail =

View File

@@ -37,13 +37,15 @@ public interface RequestFormManageAppMapper {
* @param startDate 开始日期可选格式yyyy-MM-dd * @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd * @param endDate 结束日期可选格式yyyy-MM-dd
* @param status 单据状态(可选) * @param status 单据状态(可选)
* @param keyword 关键字(可选,申请单号/检查项目名称模糊匹配)
* @return 申请单列表 * @return 申请单列表
*/ */
List<RequestFormQueryDto> getRequestForm(@Param("encounterId") Long encounterId, List<RequestFormQueryDto> getRequestForm(@Param("encounterId") Long encounterId,
@Param("typeCode") String typeCode, @Param("typeCode") String typeCode,
@Param("startDate") String startDate, @Param("startDate") String startDate,
@Param("endDate") String endDate, @Param("endDate") String endDate,
@Param("status") String status); @Param("status") String status,
@Param("keyword") String keyword);
/** /**
* 查询申请单详情 * 查询申请单详情

View File

@@ -331,12 +331,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
) t ) t
WHERE rn = 1 WHERE rn = 1
) pi ON s.patient_id = pi.patient_id ) pi ON s.patient_id = pi.patient_id
<!-- 排除已生成医嘱的手术 --> <!-- 关联服务请求表(仅用于数据关联,不再过滤) -->
LEFT JOIN wor_service_request sr ON sr.activity_id = s.id AND sr.delete_flag = '0' AND sr.category_enum = 4 LEFT JOIN wor_service_request sr ON sr.activity_id = s.id AND sr.delete_flag = '0' AND sr.category_enum = 4
<where> <where>
s.delete_flag = '0' s.delete_flag = '0'
<!-- 只显示未生成医嘱的手术 -->
AND sr.id IS NULL
<if test="ew.sqlSegment != null and ew.sqlSegment != ''"> <if test="ew.sqlSegment != null and ew.sqlSegment != ''">
<!-- 补充 encounter_id 替换,修复多表关联时字段歧义。注释不能放进 OGNL 表达式内部。 --> <!-- 补充 encounter_id 替换,修复多表关联时字段歧义。注释不能放进 OGNL 表达式内部。 -->
<![CDATA[ <![CDATA[

View File

@@ -153,7 +153,8 @@
ii.balance_amount AS balance_amount, ii.balance_amount AS balance_amount,
ii.account_id AS account_id, ii.account_id AS account_id,
ii.performer_check_id, ii.performer_check_id,
ii.category_code ii.category_code,
ii.dispense_status
FROM (( SELECT T1.encounter_id, FROM (( SELECT T1.encounter_id,
T1.tenant_id, T1.tenant_id,
#{medMedicationRequest} AS advice_table, #{medMedicationRequest} AS advice_table,
@@ -197,7 +198,8 @@
pra."name" AS admitting_doctor_name, pra."name" AS admitting_doctor_name,
personal_account.balance_amount, personal_account.balance_amount,
personal_account.id AS account_id, personal_account.id AS account_id,
T2.category_code T2.category_code,
mmd.status_enum AS dispense_status
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 LEFT JOIN med_medication_definition AS T2
ON T2.id = T1.medication_id ON T2.id = T1.medication_id
@@ -278,6 +280,9 @@
aa.balance_amount aa.balance_amount
) AS personal_account ) AS personal_account
ON personal_account.encounter_id = ae.id ON personal_account.encounter_id = ae.id
LEFT JOIN med_medication_dispense mmd
ON mmd.med_req_id = T1.id
AND mmd.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
AND T1.generate_source_enum = #{doctorPrescription} AND T1.generate_source_enum = #{doctorPrescription}
@@ -331,7 +336,8 @@
pra."name" AS admitting_doctor_name, pra."name" AS admitting_doctor_name,
personal_account.balance_amount, personal_account.balance_amount,
personal_account.id AS account_id, personal_account.id AS account_id,
T2.category_code T2.category_code,
NULL AS dispense_status
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.id = T1.activity_id ON T2.id = T1.activity_id

View File

@@ -105,6 +105,8 @@
<if test="statusEnum == 4"> <if test="statusEnum == 4">
T4.status_enum = #{completed} T4.status_enum = #{completed}
</if> </if>
AND T4.summary_no IS NOT NULL
AND T4.summary_no != ''
) AS ii ) AS ii
${ew.customSqlSegment} ${ew.customSqlSegment}
GROUP BY ii.encounter_id, GROUP BY ii.encounter_id,
@@ -263,6 +265,8 @@
AND T15.delete_flag = '0' AND T15.delete_flag = '0'
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
-- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查 -- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查
AND T1.summary_no IS NOT NULL
AND T1.summary_no != ''
AND AND
<if test="dispenseStatus == null"> <if test="dispenseStatus == null">
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared}) T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared})

View File

@@ -216,7 +216,8 @@
ccd.name AS condition_definition_name, ccd.name AS condition_definition_name,
T1.therapy_enum AS therapyEnum, T1.therapy_enum AS therapyEnum,
T1.sort_number AS sort_number, T1.sort_number AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.medication_id AS advice_definition_id
FROM med_medication_request AS T1 FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -268,7 +269,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
2 AS therapyEnum, 2 AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.device_def_id AS advice_definition_id
FROM wor_device_request AS T1 FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0' AND T2.delete_flag = '0'
@@ -317,7 +319,8 @@
'' AS condition_definition_name, '' AS condition_definition_name,
COALESCE(T1.therapy_enum, 2) AS therapyEnum, COALESCE(T1.therapy_enum, 2) AS therapyEnum,
99 AS sort_number, 99 AS sort_number,
T1.based_on_id AS based_on_id T1.based_on_id AS based_on_id,
T1.activity_id AS advice_definition_id
FROM wor_service_request AS T1 FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2 LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id ON T2.ID = T1.activity_id

View File

@@ -0,0 +1,63 @@
/*
* Copyright ©2023 CJB-CNIT Team. All rights reserved
*/
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态 (order_main.status)
*
* <pre>
* 状态流转:
* 创建订单 → ACTIVE(1)
* 签到 → ACTIVE(1) 不变
* 患者退号 → PATIENT_CANCELLED(0)
* 系统取消 → SYSTEM_CANCELLED(2)
* 就诊完成 → COMPLETED(3)
* </pre>
*
* @author wangjian963
* @date 2026-05-09
*/
@Getter
@AllArgsConstructor
public enum OrderStatus implements HisEnumInterface {
/**
* 患者取消
*/
PATIENT_CANCELLED(0, "0", "患者取消"),
/**
* 有效
*/
ACTIVE(1, "1", "有效"),
/**
* 系统取消
*/
SYSTEM_CANCELLED(2, "2", "系统取消"),
/**
* 已完成
*/
COMPLETED(3, "3", "已完成");
private Integer value;
private String code;
private String info;
public static OrderStatus getByValue(Integer value) {
if (value == null) {
return null;
}
for (OrderStatus val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -6,8 +6,8 @@ import com.core.common.utils.AssignSeqUtil;
import com.openhis.clinical.domain.Order; import com.openhis.clinical.domain.Order;
import com.openhis.clinical.mapper.OrderMapper; import com.openhis.clinical.mapper.OrderMapper;
import com.openhis.clinical.service.IOrderService; import com.openhis.clinical.service.IOrderService;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
import com.openhis.common.enums.AssignSeqEnum; import com.openhis.common.enums.AssignSeqEnum;
import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -124,7 +124,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
: new Date(); // 兜底:正常业务不应走到这里 : new Date(); // 兜底:正常业务不应走到这里
order.setAppointmentDate(appointmentDateTime); order.setAppointmentDate(appointmentDateTime);
order.setAppointmentTime(appointmentDateTime); order.setAppointmentTime(appointmentDateTime);
order.setStatus(AppointmentOrderStatus.BOOKED); // 订单状态: 0=患者取消 1=有效 2=系统取消 3=已完成
order.setStatus(OrderStatus.ACTIVE.getValue());
order.setPayStatus(0); order.setPayStatus(0);
order.setVersion(0); order.setVersion(0);
@@ -169,10 +170,13 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
if (order == null) { if (order == null) {
throw new RuntimeException("订单不存在"); throw new RuntimeException("订单不存在");
} }
if (AppointmentOrderStatus.CANCELLED.equals(order.getStatus())) { // 已取消患者取消0 或 系统取消2不可再次取消
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(order.getStatus())
|| OrderStatus.SYSTEM_CANCELLED.getValue().equals(order.getStatus())) {
throw new RuntimeException("订单已取消"); throw new RuntimeException("订单已取消");
} }
if (AppointmentOrderStatus.CHECKED_IN.equals(order.getStatus())) { // 已完成(3)的订单不可取消
if (OrderStatus.COMPLETED.getValue().equals(order.getStatus())) {
throw new RuntimeException("订单已完成,无法取消"); throw new RuntimeException("订单已完成,无法取消");
} }
@@ -189,6 +193,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
.eq(Order::getPatientId, patientId) .eq(Order::getPatientId, patientId)
.eq(Order::getTenantId, tenantId) .eq(Order::getTenantId, tenantId)
.ge(Order::getCancelTime, startTime) .ge(Order::getCancelTime, startTime)
.eq(Order::getStatus, AppointmentOrderStatus.CANCELLED)); // 只统计患者主动取消(0),不含系统取消(2)
.eq(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue()));
} }
} }

View File

@@ -13,8 +13,8 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.TicketMapper; import com.openhis.clinical.mapper.TicketMapper;
import com.openhis.clinical.service.IOrderService; import com.openhis.clinical.service.IOrderService;
import com.openhis.clinical.service.ITicketService; import com.openhis.clinical.service.ITicketService;
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
import com.openhis.common.constant.CommonConstants.SlotStatus; import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -195,8 +195,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
Date startTime = Date.from(periodStart.atZone(ZoneId.systemDefault()).toInstant()); Date startTime = Date.from(periodStart.atZone(ZoneId.systemDefault()).toInstant());
Date endTime = Date.from(periodEnd.atZone(ZoneId.systemDefault()).toInstant()); Date endTime = Date.from(periodEnd.atZone(ZoneId.systemDefault()).toInstant());
// 预约去重以订单为准order_main因为预约成功会先落订单clinical_ticket 不一定在此链路写入 // 预约去重以订单为准order_main有效订单(1)才参与去重
List<Integer> effectiveOrderStatuses = Arrays.asList(AppointmentOrderStatus.BOOKED, AppointmentOrderStatus.CHECKED_IN); List<Integer> effectiveOrderStatuses = Arrays.asList(OrderStatus.ACTIVE.getValue());
int exists = orderMapper.countPatientDeptOrdersInPeriod(dto.getPatientId(), slot.getDepartmentId(), slot.getDepartmentName(), int exists = orderMapper.countPatientDeptOrdersInPeriod(dto.getPatientId(), slot.getDepartmentId(), slot.getDepartmentName(),
startTime, endTime, effectiveOrderStatuses); startTime, endTime, effectiveOrderStatuses);
if (exists > 0) { if (exists > 0) {
@@ -314,9 +314,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
} }
Order latestOrder = orders.get(0); Order latestOrder = orders.get(0);
// 1. 更新订单状态为已取号,并更新支付状态和支付时间 // 1. 签到不改变订单状态(仍为有效1),更新支付状态为已支付并记录支付时间
orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN); orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
// 更新支付状态为已支付,记录支付时间
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date()); orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 查询号源槽位信息 // 2. 查询号源槽位信息

View File

@@ -160,11 +160,12 @@
AND delete_flag = '0' AND delete_flag = '0'
</update> </update>
<!-- status=0(待约)时清空order_id释放号源使退号后号源可再被预约 -->
<update id="updateSlotStatus"> <update id="updateSlotStatus">
UPDATE adm_schedule_slot UPDATE adm_schedule_slot
SET SET
status = #{status}, status = #{status},
<if test="status != null and '0'.equals(status.toString())"> <if test="status != null and status == 0">
order_id = NULL, order_id = NULL,
</if> </if>
update_time = now() update_time = now()

View File

@@ -117,12 +117,14 @@
</where> </where>
</select> </select>
<!-- status=1: 只查有效订单(0=患者取消 1=有效 2=系统取消 3=已完成) -->
<select id="selectOrderById" resultMap="OrderResult"> <select id="selectOrderById" resultMap="OrderResult">
select * from order_main where id = #{id} select * from order_main where id = #{id}
and status = 1 and status = 1
order by create_time desc order by create_time desc
</select> </select>
<!-- status=1: 只查有效订单 -->
<select id="selectOrderBySlotId" resultMap="OrderResult"> <select id="selectOrderBySlotId" resultMap="OrderResult">
select * from order_main where slot_id = #{slotId} and status = 1 select * from order_main where slot_id = #{slotId} and status = 1
</select> </select>
@@ -248,8 +250,9 @@
update order_main set status = #{status} where id = #{id} update order_main set status = #{status} where id = #{id}
</update> </update>
<!-- status=0: 患者取消 (OrderStatus.PATIENT_CANCELLED) -->
<update id="updateOrderCancelInfoById"> <update id="updateOrderCancelInfoById">
update order_main set status = 3, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id} update order_main set status = 0, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id}
</update> </update>
<update id="updatePayStatus"> <update id="updatePayStatus">

View File

@@ -38,7 +38,6 @@ export function listAdviceTypes(query) {
params: query params: query
}) })
} }
```
/** /**
* 查询手术室详细 * 查询手术室详细

View File

@@ -1146,7 +1146,13 @@ function calculateTotalPrice(row, index) {
}); });
} }
defineExpose({ getListInfo }); function closeAllPopovers() {
prescriptionList.value.forEach(row => {
row.showPopover = false;
});
}
defineExpose({ getListInfo, closeAllPopovers });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -41,43 +41,9 @@
</el-col> </el-col>
</el-row> </el-row>
<!-- 性别出生日期实足年龄 --> <!-- 工作单位学校 -->
<el-row :gutter="16" class="form-row align-bottom">
<el-col :span="6" class="form-item">
<span class="form-label required">性别</span>
<el-radio-group v-model="form.sex">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
<el-radio label="未知">未知</el-radio>
</el-radio-group>
</el-col>
<el-col :span="10" class="form-item birth-date">
<span class="form-label required">出生日期</span>
<div class="date-inputs">
<el-input v-model="form.birthYear" class="underline-input small" maxlength="4" @change="calculateAge" />
<span></span>
<el-input v-model="form.birthMonth" class="underline-input small" maxlength="2" @change="calculateAge" />
<span></span>
<el-input v-model="form.birthDay" class="underline-input small" maxlength="2" @change="calculateAge" />
<span></span>
</div>
</el-col>
<el-col :span="8" class="form-item age-item">
<span class="form-label"> 实足年龄</span>
<div class="age-inputs">
<el-input v-model="form.age" class="underline-input small"/>
<el-select v-model="form.ageUnit" class="age-unit-select">
<el-option label="岁" value="岁" />
<el-option label="月" value="月" />
<el-option label="天" value="天" />
</el-select>
</div>
</el-col>
</el-row>
<!-- 工作单位 -->
<el-row :gutter="16" class="form-row"> <el-row :gutter="16" class="form-row">
<el-col :span="24" class="form-item full-width"> <el-col :span="12" class="form-item">
<span class="form-label">工作单位学校</span> <span class="form-label">工作单位学校</span>
<el-input v-model="form.workplace" class="underline-input" /> <el-input v-model="form.workplace" class="underline-input" />
</el-col> </el-col>
@@ -522,7 +488,7 @@
<script setup> <script setup>
import { ref, computed, getCurrentInstance, watch } from 'vue'; import { ref, computed, getCurrentInstance, watch } from 'vue';
import pcas from 'china-division/dist/pcas-code.json'; import pcas from 'china-division/dist/pcas-code.json';
import { saveInfectiousDiseaseReport } from '../api'; import { saveInfectiousDiseaseReport, getNextCardNo } from '../api';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import { useDict } from '@/utils/dict'; import { useDict } from '@/utils/dict';
@@ -1237,7 +1203,7 @@ function calculateAge() {
* 显示对话框并初始化表单数据 * 显示对话框并初始化表单数据
* @param {Object} diagnosisData - 诊断数据,包含已选疾病和地址信息 * @param {Object} diagnosisData - 诊断数据,包含已选疾病和地址信息
*/ */
function show(diagnosisData) { async function show(diagnosisData) {
dialogVisible.value = true; dialogVisible.value = true;
// 重置地址选择器状态 // 重置地址选择器状态
@@ -1267,12 +1233,24 @@ function show(diagnosisData) {
}); });
} }
// 生成默认卡片编号:医疗机构编码+当前年月日+4位流水 // 生成默认卡片编号:调用后端接口获取
const orgCode = userStore.fixmedinsCode || '0000'; const orgCode = userStore.fixmedinsCode || '0000';
let cardNo = '';
try {
const res = await getNextCardNo(orgCode);
if (res.code === 200 && res.data) {
cardNo = res.data;
} else {
cardNo = 'TEMP_' + Date.now();
}
} catch (err) {
console.error('获取卡片编号失败:', err);
cardNo = 'TEMP_' + Date.now();
}
form.value = { form.value = {
// 卡片编号:单位自编,与网络直报一致 // 卡片编号:单位自编,与网络直报一致
cardNo: '', cardNo: cardNo,
// 患者基本信息 // 患者基本信息
patName: patientInfo.patientName || patientInfo.name || '', // 患者姓名 patName: patientInfo.patientName || patientInfo.name || '', // 患者姓名

View File

@@ -314,6 +314,7 @@
> >
<template #title> <template #title>
<span class="cat-title">{{ cat.categoryName }}</span> <span class="cat-title">{{ cat.categoryName }}</span>
<span v-if="categoryLoadingSet.has(cat.typeId)" class="loading-dot"></span>
</template> </template>
<div <div
v-for="item in cat.items" v-for="item in cat.items"
@@ -329,6 +330,9 @@
</el-checkbox> </el-checkbox>
<span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span> <span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span>
</div> </div>
<div v-if="categoryLoadingSet.has(cat.typeId)" class="category-loading-hint">
加载中...
</div>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
@@ -517,6 +521,8 @@ const rules = {
const categoryList = ref([]); // 原始分类+项目数据 const categoryList = ref([]); // 原始分类+项目数据
const dictSearchKey = ref(''); const dictSearchKey = ref('');
const activeNames = ref(''); // 当前展开的折叠项 const activeNames = ref(''); // 当前展开的折叠项
const categoryLoadingSet = ref(new Set()); // Bug #500: 正在加载方法的分类集合
const isAnimating = ref(false); // Bug #500: 防止快速切换时折叠动画重叠导致抖动
const allMethods = ref([]); const allMethods = ref([]);
@@ -632,9 +638,14 @@ const availableMethods = computed(() => {
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空 // 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
// #428: 分类展开时联动加载检查方法 // #428: 分类展开时联动加载检查方法
// Bug #500: 使用 categoryLoadingSet 替代 dictLoading避免切换分类时整个区域闪烁
async function handleCategoryExpand(cat) { async function handleCategoryExpand(cat) {
if (!cat || !cat.typeName) return; if (!cat || !cat.typeName) return;
// 如果已加载过或正在加载中,跳过
if ((cat.methods && cat.methods.length > 0) || categoryLoadingSet.value.has(cat.typeId)) return;
categoryLoadingSet.value.add(cat.typeId);
try { try {
const res = await searchCheckMethod({ checkType: cat.typeName }); const res = await searchCheckMethod({ checkType: cat.typeName });
let data = res?.data?.data || res?.data || res?.rows || res; let data = res?.data?.data || res?.data || res?.rows || res;
@@ -655,14 +666,21 @@ async function handleCategoryExpand(cat) {
} }
} catch (err) { } catch (err) {
console.error('加载分类检查方法失败', err); console.error('加载分类检查方法失败', err);
} finally {
categoryLoadingSet.value.delete(cat.typeId);
} }
} }
async function handleCollapseChange(activeName) { // Bug #500: 添加防抖逻辑,快速切换时跳过中间状态的动画,避免高度跳变和白屏闪烁
// 当折叠面板展开时,加载对应分类的检查方法 function handleCollapseChange(activeName) {
if (isAnimating.value) return; // 动画进行中,忽略后续点击
isAnimating.value = true;
setTimeout(() => { isAnimating.value = false; }, 300); // 与 CSS 过渡时长一致
if (activeName) { if (activeName) {
const cat = filteredCategoryList.value.find(c => c.typeId == activeName); const cat = filteredCategoryList.value.find(c => c.typeId == activeName);
if (cat && (!cat.methods || cat.methods.length === 0)) { if (cat && (!cat.methods || cat.methods.length === 0)) {
await handleCategoryExpand(cat); handleCategoryExpand(cat); // 异步加载,不 await
} }
} }
} }
@@ -1295,6 +1313,7 @@ defineExpose({ getList });
.collapse-scroll { .collapse-scroll {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; /* Bug #500: 防止切换时水平方向溢出导致抖动 */
} }
.empty-hint { .empty-hint {
color: #909399; color: #909399;
@@ -1309,6 +1328,20 @@ defineExpose({ getList });
font-weight: 500; font-weight: 500;
color: #303133; color: #303133;
} }
/* Bug #500: 分类加载中的小圆点动画 */
.loading-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #409eff;
margin-left: 6px;
animation: dotPulse 1s ease-in-out infinite;
}
@keyframes dotPulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.item-row { .item-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1448,10 +1481,24 @@ defineExpose({ getList });
height: auto; height: auto;
line-height: 1.5; line-height: 1.5;
} }
/* Bug #500: 折叠内容添加平滑过渡动画,避免切换时高度跳变 */
:deep(.el-collapse-item__content) { :deep(.el-collapse-item__content) {
padding-bottom: 4px; padding-bottom: 4px;
transition: all 0.3s ease;
} }
/* Bug #500: 折叠面板动画容器,添加 overflow:hidden 防止展开时内容溢出导致闪烁 */
:deep(.el-collapse-item__wrap) { :deep(.el-collapse-item__wrap) {
border: none; border: none;
overflow: hidden;
}
:deep(.el-collapse-item) {
transition: margin 0.2s ease;
}
/* Bug #500: 分类加载中提示样式 */
.category-loading-hint {
color: #909399;
font-size: 12px;
text-align: center;
padding: 8px 0;
} }
</style> </style>

View File

@@ -246,7 +246,8 @@
<el-input-number :min="0" v-model="scope.row.doseQuantity" controls-position="right" <el-input-number :min="0" v-model="scope.row.doseQuantity" controls-position="right"
:controls="false" style="width: 70px; margin-right: 20px" :controls="false" style="width: 70px; margin-right: 20px"
:ref="(el) => (inputRefs.doseQuantity = el)" @input="convertValues(scope.row, scope.$index)" :ref="(el) => (inputRefs.doseQuantity = el)" @input="convertValues(scope.row, scope.$index)"
@keyup.enter.prevent="handleEnter('doseQuantity', scope.row, scope.$index)" /> @keyup.enter.prevent="handleEnter('doseQuantity', scope.row, scope.$index)"
@change="calculateTotalAmount(scope.row, scope.$index)"/>
</el-form-item> </el-form-item>
<!-- 剂量单位 --> <!-- 剂量单位 -->
<el-select v-model="scope.row.minUnitCode" style="width: 70px; margin-right: 20px" placeholder=" "> <el-select v-model="scope.row.minUnitCode" style="width: 70px; margin-right: 20px" placeholder=" ">
@@ -294,7 +295,8 @@
} }
// inputRefs.rateCode.blur(); // inputRefs.rateCode.blur();
} }
" :ref="(el) => (inputRefs.rateCode = el)"> " :ref="(el) => (inputRefs.rateCode = el)"
@change="calculateTotalAmount(scope.row, scope.$index)">
<el-option v-for="dict in rate_code" @click="() => (scope.row.rateCode_dictText = dict.label)" <el-option v-for="dict in rate_code" @click="() => (scope.row.rateCode_dictText = dict.label)"
:key="dict.value" :label="dict.label" :value="dict.value" /> :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select> </el-select>
@@ -313,6 +315,7 @@
data-prop="dispensePerDuration"> data-prop="dispensePerDuration">
<el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1" <el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1"
controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)" controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)"
@change="calculateTotalAmount(scope.row, scope.$index)"
@keyup.enter.prevent=" @keyup.enter.prevent="
handleEnter('dispensePerDuration', scope.row, scope.$index) handleEnter('dispensePerDuration', scope.row, scope.$index)
"> ">

View File

@@ -56,137 +56,6 @@
<!-- 手术等级 --> <!-- 手术等级 -->
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" /> <el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
<!-- 手术室确认时间 -->
<el-table-column label="手术室确认时间" align="center" prop="confirmTime" width="180">
<template #default="scope">
{{ scope.row.confirmTime ? parseTime(scope.row.confirmTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="handleView(scope.row)"
>
查看
</el-button>
<el-button
type="primary"
link
size="small"
@click="handleEdit(scope.row)"
:disabled="scope.row.status !== '0'"
>
修改
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getSurgeryApplicationList, addSurgeryApplication, updateSurgeryApplication } from '@/api/doctorstation/surgery'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(true)
const surgeryList = ref([])
// 查询手术申请列表
const getList = async () => {
try {
loading.value = true
const response = await getSurgeryApplicationList()
surgeryList.value = response.data || []
} catch (error) {
console.warn('获取手术申请列表失败', error)
surgeryList.value = []
} finally {
loading.value = false
}
}
// 新增手术申请
const handleAdd = () => {
// 跳转到新增页面或打开新增弹窗
// 这里需要根据实际路由配置调整
openSurgeryDialog()
}
// 编辑手术申请
const handleEdit = (row) => {
if(row.status !== '0') {
ElMessage.warning('该申请已提交,无法修改')
return
}
openSurgeryDialog(row)
}
// 查看手术申请详情
const handleView = (row) => {
openSurgeryDialog(row, true)
}
// 刷新列表
const handleRefresh = () => {
getList()
}
// 打开手术申请对话框
const openSurgeryDialog = (row, isView = false) => {
// 这里需要引入具体的对话框组件或跳转到详情页
// 示例代码,具体实现需要根据项目实际情况调整
console.log('打开手术申请对话框', row, isView)
}
// 获取行样式
const getRowClassName = ({ row }) => {
if (row.status === '1') {
return 'success-row'
} else if (row.status === '2') {
return 'warning-row'
}
return ''
}
onMounted(() => {
getList()
})
// 暴露方法给父组件调用,用于提交后刷新列表
defineExpose({
refreshList: getList
})
</script>
<style scoped>
.surgery-application-container {
padding: 20px;
}
.top-operation-bar {
margin-bottom: 16px;
}
.add-button,
.refresh-button {
margin-bottom: 10px;
}
/* 表格行状态样式 */
::v-deep(.success-row) {
background-color: #f0f9ff;
}
::v-deep(.warning-row) {
background-color: #fff7e6;
}
</style>
<el-table-column label="手术室确认时间" align="center" prop="operatingRoomConfirmTime" width="180"> <el-table-column label="手术室确认时间" align="center" prop="operatingRoomConfirmTime" width="180">
<template #default="scope"> <template #default="scope">
{{ scope.row.operatingRoomConfirmTime ? parseTime(scope.row.operatingRoomConfirmTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }} {{ scope.row.operatingRoomConfirmTime ? parseTime(scope.row.operatingRoomConfirmTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}

View File

@@ -121,3 +121,25 @@ export function getTestResultPage(queryParams) {
data: queryParams, data: queryParams,
}); });
} }
/**
* 删除申请单(仅待签发状态可删除)
*/
export function deleteRequestForm(data) {
return request({
url: '/reg-doctorstation/request-form/delete',
method: 'post',
data: data,
});
}
/**
* 撤回申请单(已签发状态撤回至待签发)
*/
export function withdrawRequestForm(data) {
return request({
url: '/reg-doctorstation/request-form/withdraw',
method: 'post',
data: data,
});
}

View File

@@ -175,10 +175,9 @@ const hasMatchedFields = computed(() => {
}); });
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = () => { const getLocationInfo = async () => {
getOrgList().then((res) => { const res = await getOrgList();
orgOptions.value = res.data.records; orgOptions.value = res.data.records;
});
}; };
const recursionFun = (targetDepartment) => { const recursionFun = (targetDepartment) => {
@@ -199,7 +198,12 @@ const recursionFun = (targetDepartment) => {
return name; return name;
}; };
const handleViewDetail = (row) => { const handleViewDetail = async (row) => {
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row; currentDetail.value = row;
// 解析 descJson // 解析 descJson
if (row.descJson) { if (row.descJson) {
@@ -220,10 +224,9 @@ const handleViewDetail = (row) => {
watch( watch(
() => patientInfo.value?.encounterId, () => patientInfo.value?.encounterId,
(val) => { async (val) => {
if (val) { if (val) {
fetchData(); await Promise.all([fetchData(), getLocationInfo()]);
getLocationInfo();
} else { } else {
tableData.value = []; tableData.value = [];
} }

View File

@@ -49,15 +49,6 @@
<el-option label="已作废" value="7" /> <el-option label="已作废" value="7" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键字">
<el-input
v-model="filterForm.keyword"
placeholder="申请单号/检查项目"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading"> <el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon> <el-icon><Search /></el-icon>
@@ -189,22 +180,9 @@ const descJsonData = ref(null);
const orgOptions = ref([]); const orgOptions = ref([]);
// 筛选表单数据 // 筛选表单数据
const getDefaultDateRange = () => {
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const formatDate = (d) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
return [formatDate(weekAgo), formatDate(now)];
};
const filterForm = ref({ const filterForm = ref({
dateRange: getDefaultDateRange(), // 默认近一周 dateRange: [], // [startDate, endDate]
status: '', // 申请单状态 status: '', // 申请单状态
keyword: '', // 关键字搜索
}); });
const fetchData = async () => { const fetchData = async () => {
@@ -229,11 +207,6 @@ const fetchData = async () => {
params.status = filterForm.value.status; params.status = filterForm.value.status;
} }
// 添加关键字搜索
if (filterForm.value.keyword && filterForm.value.keyword.trim()) {
params.keyword = filterForm.value.keyword.trim();
}
const res = await getCheck(params); const res = await getCheck(params);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const raw = res.data?.records || res.data; const raw = res.data?.records || res.data;
@@ -270,9 +243,8 @@ const handleSearch = async () => {
* 重置按钮处理 * 重置按钮处理
*/ */
const handleReset = () => { const handleReset = () => {
filterForm.value.dateRange = getDefaultDateRange(); filterForm.value.dateRange = [];
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
fetchData(); fetchData();
}; };
@@ -372,9 +344,8 @@ watch(
getLocationInfo(); getLocationInfo();
} else { } else {
tableData.value = []; tableData.value = [];
filterForm.value.dateRange = getDefaultDateRange(); filterForm.value.dateRange = [];
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
} }
}, },
{ immediate: true } { immediate: true }

View File

@@ -0,0 +1,20 @@
<!--
* 住院医生站 汇总发药申请复用药房发药汇总单能力
-->
<template>
<div class="summary-drug-application">
<MedicationSummary />
</div>
</template>
<script setup>
import MedicationSummary from '@/views/drug/inpatientMedicationDispensing/components/MedicationSummary.vue';
</script>
<style scoped>
.summary-drug-application {
height: 100%;
min-height: 400px;
overflow: auto;
}
</style>

View File

@@ -181,10 +181,9 @@ const hasMatchedFields = computed(() => {
}); });
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = () => { const getLocationInfo = async () => {
getOrgList().then((res) => { const res = await getOrgList();
orgOptions.value = res.data.records; orgOptions.value = res.data.records;
});
}; };
const recursionFun = (targetDepartment) => { const recursionFun = (targetDepartment) => {
@@ -205,7 +204,12 @@ const recursionFun = (targetDepartment) => {
return name; return name;
}; };
const handleViewDetail = (row) => { const handleViewDetail = async (row) => {
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row; currentDetail.value = row;
// 解析 descJson // 解析 descJson
if (row.descJson) { if (row.descJson) {
@@ -226,10 +230,9 @@ const handleViewDetail = (row) => {
watch( watch(
() => patientInfo.value?.encounterId, () => patientInfo.value?.encounterId,
(val) => { async (val) => {
if (val) { if (val) {
fetchData(); await Promise.all([fetchData(), getLocationInfo()]);
getLocationInfo();
} else { } else {
tableData.value = []; tableData.value = [];
} }

View File

@@ -47,6 +47,15 @@
<el-option label="已作废" value="5" /> <el-option label="已作废" value="5" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键字">
<el-input
v-model="filterForm.keyword"
placeholder="申请单号/检验项目"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading"> <el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon> <el-icon><Search /></el-icon>
@@ -173,7 +182,7 @@ import {computed, getCurrentInstance, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue'; import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js'; import {patientInfo} from '../../store/patient.js';
import {getInspection} from './api'; import {getInspection} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js'; import {getDepartmentList} from '@/api/public.js';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -188,6 +197,7 @@ const orgOptions = ref([]);
const filterForm = ref({ const filterForm = ref({
dateRange: [], // [startDate, endDate] dateRange: [], // [startDate, endDate]
status: '', // 单据状态 status: '', // 单据状态
keyword: '', // 关键字搜索
}); });
const fetchData = async () => { const fetchData = async () => {
@@ -212,6 +222,11 @@ const fetchData = async () => {
params.status = filterForm.value.status; params.status = filterForm.value.status;
} }
// 添加关键字搜索
if (filterForm.value.keyword && filterForm.value.keyword.trim()) {
params.keyword = filterForm.value.keyword.trim();
}
const res = await getInspection(params); const res = await getInspection(params);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const raw = res.data?.records || res.data; const raw = res.data?.records || res.data;
@@ -251,6 +266,7 @@ const handleReset = () => {
// 重置筛选条件为默认值 // 重置筛选条件为默认值
filterForm.value.dateRange = []; filterForm.value.dateRange = [];
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
// 重新加载数据 // 重新加载数据
fetchData(); fetchData();
}; };
@@ -331,10 +347,9 @@ const hasMatchedFields = computed(() => {
}); });
/** 查询科室 */ /** 查询科室 */
const getLocationInfo = () => { const getLocationInfo = async () => {
getOrgList().then((res) => { const res = await getDepartmentList();
orgOptions.value = res.data.records; orgOptions.value = res.data || [];
});
}; };
const recursionFun = (targetDepartment) => { const recursionFun = (targetDepartment) => {
@@ -345,17 +360,24 @@ const recursionFun = (targetDepartment) => {
name = obj.name; name = obj.name;
} }
const subObjArray = obj['children']; const subObjArray = obj['children'];
for (let index = 0; index < subObjArray.length; index++) { if (subObjArray && subObjArray.length > 0) {
const item = subObjArray[index]; for (let i = 0; i < subObjArray.length; i++) {
const item = subObjArray[i];
if (item.id == targetDepartment) { if (item.id == targetDepartment) {
name = item.name; name = item.name;
} }
} }
} }
}
return name; return name;
}; };
const handleViewDetail = (row) => { const handleViewDetail = async (row) => {
// 确保科室数据已加载,以便将 ID 解析为名称
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
currentDetail.value = row; currentDetail.value = row;
// 解析 descJson // 解析 descJson
if (row.descJson) { if (row.descJson) {
@@ -376,7 +398,7 @@ const handleViewDetail = (row) => {
watch( watch(
() => patientInfo.value?.encounterId, () => patientInfo.value?.encounterId,
(val) => { async (val) => {
if (val) { if (val) {
// 设置默认日期范围为近7天 // 设置默认日期范围为近7天
const today = new Date(); const today = new Date();
@@ -396,13 +418,13 @@ watch(
formatDate(today) formatDate(today)
]; ];
fetchData(); await Promise.all([fetchData(), getLocationInfo()]);
getLocationInfo();
} else { } else {
tableData.value = []; tableData.value = [];
// 重置筛选条件 // 重置筛选条件
filterForm.value.dateRange = []; filterForm.value.dateRange = [];
filterForm.value.status = ''; filterForm.value.status = '';
filterForm.value.keyword = '';
} }
}, },
{ immediate: true } { immediate: true }

View File

@@ -176,6 +176,8 @@ const projectWithDepartment = (selectProjectIds, type) => {
}); });
arr.push(searchData); arr.push(searchData);
}); });
// 保存用户手动选择的发往科室(提交时需要保留)
const manualDept = type === 2 ? form.targetDepartment : '';
// 清空科室 // 清空科室
form.targetDepartment = ''; form.targetDepartment = '';
if (arr.length > 0) { if (arr.length > 0) {
@@ -194,12 +196,18 @@ const projectWithDepartment = (selectProjectIds, type) => {
// 选中项目中的执行科室id与全部科室数据做匹配 // 选中项目中的执行科室id与全部科室数据做匹配
const findItem = findTreeItem(orgOptions.value, obj.orgId); const findItem = findTreeItem(orgOptions.value, obj.orgId);
if (!findItem) { if (!findItem) {
// type=2(提交)时,若用户已手动选择发往科室,则允许提交
if (type === 2 && manualDept) {
form.targetDepartment = manualDept;
isRelease = true;
} else {
isRelease = false; isRelease = false;
ElMessage({ ElMessage({
type: 'error', type: 'error',
message: '未找到项目执行的科室', message: '未找到项目执行的科室',
}); });
} }
}
if (type == 1) { if (type == 1) {
if (isRelease) { if (isRelease) {
form.targetDepartment = findItem.id; form.targetDepartment = findItem.id;
@@ -232,7 +240,7 @@ const submit = () => {
unitCode: item.priceList[0].unitCode /** 请求单位编码 */, unitCode: item.priceList[0].unitCode /** 请求单位编码 */,
unitPrice: item.priceList[0].price /** 单价 */, unitPrice: item.priceList[0].price /** 单价 */,
totalPrice: item.priceList[0].price /** 总价 */, totalPrice: item.priceList[0].price /** 总价 */,
positionId: item.positionId, //执行科室id positionId: item.positionId || form.targetDepartment, //执行科室id未配置时使用用户手动选择的科室
ybClassEnum: item.ybClassEnum, //类别医保编码 ybClassEnum: item.ybClassEnum, //类别医保编码
conditionId: item.conditionId, //诊断ID conditionId: item.conditionId, //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id

View File

@@ -42,6 +42,40 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="紧急程度" prop="urgencyLevel" style="width: 100%">
<el-select v-model="form.urgencyLevel" placeholder="请选择紧急程度" clearable style="width: 100%">
<el-option label="普通" value="routine" />
<el-option label="急诊" value="emergency" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="过敏史" prop="allergyHistory" style="width: 100%">
<el-input v-model="form.allergyHistory" autocomplete="off" type="textarea" placeholder="如:造影剂过敏史等" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="检查目的" prop="examinationPurpose" style="width: 100%">
<el-input v-model="form.examinationPurpose" autocomplete="off" type="textarea" placeholder="请说明检查目的" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病史摘要" prop="medicalHistorySummary" style="width: 100%">
<el-input v-model="form.medicalHistorySummary" autocomplete="off" type="textarea" placeholder="请简要描述患者病史" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="期望检查时间" prop="expectedExaminationTime" style="width: 100%">
<el-date-picker
v-model="form.expectedExaminationTime"
type="datetime"
placeholder="请选择期望检查时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="症状" prop="symptom" style="width: 100%"> <el-form-item label="症状" prop="symptom" style="width: 100%">
<el-input v-model="form.symptom" autocomplete="off" type="textarea" /> <el-input v-model="form.symptom" autocomplete="off" type="textarea" />
@@ -145,6 +179,11 @@ const transferValue = ref([]);
const form = reactive({ const form = reactive({
// categoryType: '', // 项目类别 // categoryType: '', // 项目类别
targetDepartment: '', // 发往科室 targetDepartment: '', // 发往科室
urgencyLevel: '', // 紧急程度
allergyHistory: '', // 过敏史
examinationPurpose: '', // 检查目的
medicalHistorySummary: '', // 病史摘要
expectedExaminationTime: '', // 期望检查时间
symptom: '', // 症状 symptom: '', // 症状
sign: '', // 体征 sign: '', // 体征
clinicalDiagnosis: '', // 临床诊断 clinicalDiagnosis: '', // 临床诊断

View File

@@ -122,7 +122,6 @@
<el-table-column label="类型" align="center" prop="" width="120"> <el-table-column label="类型" align="center" prop="" width="120">
<template #default="scope"> <template #default="scope">
{{ console.log(scope.row, 1111) }}
<el-radio-group <el-radio-group
v-model="scope.row.therapyEnum" v-model="scope.row.therapyEnum"
size="small" size="small"
@@ -270,7 +269,6 @@
</el-table-column> </el-table-column>
<el-table-column label="频次/用法" align="center" prop="" width="180"> <el-table-column label="频次/用法" align="center" prop="" width="180">
<template #default="scope"> <template #default="scope">
{{ console.log(scope.row) }}
<span v-if="!scope.row.isEdit && scope.row.adviceType == 1" style="text-align: right"> <span v-if="!scope.row.isEdit && scope.row.adviceType == 1" style="text-align: right">
{{ {{
[ [
@@ -804,8 +802,10 @@ function clickRowDb(row, column, event) {
return; return;
} }
row.showPopover = false; row.showPopover = false;
// “待签发(已保存 requestId存在)”不允许再编辑;仅“待保存(无requestId)”允许编辑 // 允许所有 statusEnum==1 的医嘱进入编辑
if (row.statusEnum == 1 && !row.requestId) { // 1. 新医嘱(无 requestId):待保存
// 2. 护士退回医嘱(有 requestId退回后状态重置为 DRAFT(1),需允许医生编辑修改后重新签发
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1') // 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1'); row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true; row.isEdit = true;
@@ -879,14 +879,13 @@ function handleDiagnosisChange(item) {
function handleFocus(row, index) { function handleFocus(row, index) {
rowIndex.value = index; rowIndex.value = index;
row.showPopover = true; row.showPopover = true;
// el-popover 懒渲染,需要等两帧组件才会挂载
const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType; const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
// 用 adviceType + categoryCode 组合查找匹配的选项 // 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 诊疗(3)和手术(6)没有categoryCode传空字符串给refresh由子组件转为undefined不发送 // 修复Bug #486当行没有显式选择医嘱类型时row.adviceType为undefined
// 药品(1)才有categoryCode如'1'=中成药,'2'=西药,'4'=中草药) // 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤
const categoryCode = selectedItem ? selectedItem.categoryCode : (adviceQueryParams.value.categoryCode || ''); const categoryCode = row.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
const searchKey = row.adviceName || ''; const searchKey = row.adviceName || '';
nextTick(() => { nextTick(() => {
@@ -923,8 +922,9 @@ function handleChange(value) {
// 用 adviceType + categoryCode 组合查找匹配的选项 // 用 adviceType + categoryCode 组合查找匹配的选项
const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType; const selectValue = (adviceType == 1 && row?.categoryCode) ? '1-' + row.categoryCode : adviceType;
const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType); const selectedItem = adviceTypeList.value.find(item => item.value === selectValue) || adviceTypeList.value.find(item => item.adviceType === adviceType);
// 诊疗/手术的categoryCode为空字符串由子组件转为undefined不发送 // 修复Bug #486当行没有显式选择医嘱类型时row?.adviceType为undefined
const categoryCode = selectedItem ? selectedItem.categoryCode : (adviceQueryParams.value.categoryCode || ''); // 不传categoryCode让搜索在全药库中进行只有行已选择类型时才用对应categoryCode过滤
const categoryCode = row?.adviceType !== undefined ? (selectedItem ? selectedItem.categoryCode : '') : '';
tableRef.refresh(adviceType, categoryCode, value); tableRef.refresh(adviceType, categoryCode, value);
} }
} }
@@ -1214,12 +1214,8 @@ function handleSave() {
getListInfo(false); getListInfo(false);
bindMethod.value = {}; bindMethod.value = {};
nextId.value == 1; nextId.value == 1;
// 处方保存成功后,自动将医嘱信息同步至患者处置记录
// handleEmrTreatment();
} else { } else {
proxy.$modal.msgError(res.message); proxy.$modal.msgError(res.message);
conso;
isSaving.value = false; isSaving.value = false;
} }
}) })
@@ -1319,6 +1315,9 @@ function handleCancelEdit(row, index) {
function handleSaveSign(row, index) { function handleSaveSign(row, index) {
if (row.adviceType != 2) { if (row.adviceType != 2) {
let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId; let itemNo = row.adviceType == 1 ? row.methodCode : row.adviceDefinitionId;
if (!itemNo) {
console.warn('绑定设备检查跳过itemNo为空adviceType=' + row.adviceType + ', adviceName=' + row.adviceName + '');
} else {
getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => { getBindDevice({ typeCode: row.adviceType, itemNo: itemNo }).then((res) => {
if (res.data.length == 0) { if (res.data.length == 0) {
return; return;
@@ -1336,6 +1335,7 @@ function handleSaveSign(row, index) {
} }
}); });
} }
}
// 更新UI状态 // 更新UI状态
row.isEdit = false; row.isEdit = false;

View File

@@ -40,7 +40,7 @@
</template> </template>
</el-input> </el-input>
</div> </div>
<el-button type="primary">划价组套</el-button> <el-button type="primary" @click="openGroupSetDialog">划价组套</el-button>
</div> </div>
<!-- 弹窗内容 - 左右布局 --> <!-- 弹窗内容 - 左右布局 -->
<div style="display: flex; gap: 20px; height: 70vh"> <div style="display: flex; gap: 20px; height: 70vh">
@@ -147,6 +147,7 @@
<el-select <el-select
v-model="scope.row.selectUnitCode" v-model="scope.row.selectUnitCode"
placeholder="单位" placeholder="单位"
filterable
style="width: 100px" style="width: 100px"
@change="unitCodeChange(scope.row)" @change="unitCodeChange(scope.row)"
> >
@@ -171,30 +172,32 @@
v-if="scope.row.adviceType == 3" v-if="scope.row.adviceType == 3"
clearable clearable
filterable filterable
:filter-method="(val) => filterOptions(val, scope.row, 'departmentOptions')"
v-model="scope.row.positionId" v-model="scope.row.positionId"
placeholder="选择科室" placeholder="选择科室"
style="width: 220px" style="width: 220px"
> >
<el-option <el-option
v-for="dept in departmentOptions" v-for="dept in getFilteredOptions(scope.row, 'departmentOptions')"
:key="dept.id" :key="dept.id"
:label="dept.name" :label="dept.name"
:value="dept.id" :value="String(dept.id)"
/> />
</el-select> </el-select>
<el-select <el-select
v-if="scope.row.adviceType == 2" v-if="scope.row.adviceType == 2"
clearable clearable
filterable filterable
:filter-method="(val) => filterOptions(val, scope.row, 'locationOptions')"
v-model="scope.row.positionId" v-model="scope.row.positionId"
placeholder="选择药房/耗材房" placeholder="选择药房/耗材房"
style="width: 220px" style="width: 220px"
> >
<el-option <el-option
v-for="dept in locationOptions" v-for="dept in getFilteredOptions(scope.row, 'locationOptions')"
:key="dept.value" :key="dept.value"
:label="dept.label" :label="dept.label"
:value="dept.value" :value="String(dept.value)"
/> />
</el-select> </el-select>
</template> </template>
@@ -247,6 +250,57 @@
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
<!-- 划价组套选择对话框 -->
<el-dialog v-model="groupSetDialogVisible" title="划价组套选择" width="600px" :close-on-click-modal="false" append-to-body>
<div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px">
<el-input
v-model="groupSetSearchText"
placeholder="请输入组套名称搜索"
clearable
style="width: 250px"
@keyup.enter="loadGroupSets"
>
<template #append>
<el-button icon="Search" @click="loadGroupSets" />
</template>
</el-input>
<el-radio-group v-model="groupSetRange" @change="loadGroupSets">
<el-radio-button :label="1">个人</el-radio-button>
<el-radio-button :label="2">科室</el-radio-button>
<el-radio-button :label="3">全院</el-radio-button>
</el-radio-group>
</div>
<el-table
:data="groupSetList"
v-loading="groupSetLoading"
highlight-current-row
border
stripe
@current-change="handleGroupSetSelect"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="组套名称" min-width="180" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.name || scope.row.Name || '' }}
</template>
</el-table-column>
<el-table-column label="使用范围" width="100" align="center">
<template #default="scope">
{{ getRangeText(scope.row) }}
</template>
</el-table-column>
<el-table-column label="明细数量" width="100" align="center">
<template #default="scope">
{{ scope.row.detailList?.length || 0 }} 项
</template>
</el-table-column>
</el-table>
<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>
</div>
</el-dialog>
</template> </template>
<script setup> <script setup>
@@ -254,6 +308,7 @@ import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from 'vue
import {ElMessage} from 'element-plus'; import {ElMessage} from 'element-plus';
import {formatDateStr} from '@/utils/index'; import {formatDateStr} from '@/utils/index';
import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList} from './api.js'; import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList} from './api.js';
import {getOrderGroup} from '@/views/doctorstation/components/api.js';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -312,6 +367,8 @@ const locationOptions = ref([]);
const searchText = ref(''); const searchText = ref('');
const userId = ref(''); const userId = ref('');
const orgId = ref(''); const orgId = ref('');
// 下拉框模糊搜索关键字(按行存储)
const filterKeywords = ref({});
const queryParams = ref({ const queryParams = ref({
pageSize: 100, pageSize: 100,
pageNum: 1, pageNum: 1,
@@ -389,6 +446,14 @@ const submitData = reactive({
}); });
const adviceLoading = ref(false); const adviceLoading = ref(false);
// 划价组套相关
const groupSetDialogVisible = ref(false);
const groupSetList = ref([]);
const groupSetLoading = ref(false);
const groupSetSearchText = ref('');
const groupSetRange = ref(2);
const selectedGroupSet = ref(null);
// 【优化核心】计算总金额 - 使用计算属性实现实时更新 // 【优化核心】计算总金额 - 使用计算属性实现实时更新
const totalAmount = computed(() => { const totalAmount = computed(() => {
return feeItemsList.value.reduce((sum, item) => { return feeItemsList.value.reduce((sum, item) => {
@@ -424,10 +489,15 @@ watch(
// 加载科室选项 // 加载科室选项
function loadDepartmentOptions() { function loadDepartmentOptions() {
getOrgList().then((res) => { getOrgList()
.then((res) => {
if (res.data && res.data.records && res.data.records.length > 0) { if (res.data && res.data.records && res.data.records.length > 0) {
departmentOptions.value = res.data.records[0].children || []; departmentOptions.value = res.data.records[0].children || [];
} }
})
.catch(() => {
console.warn('科室列表加载失败(可能无权限)');
departmentOptions.value = [];
}); });
} }
@@ -447,9 +517,33 @@ function getAdviceBaseInfos() {
}); });
} }
function getDiseaseInitLoc() { function getDiseaseInitLoc() {
getDiseaseTreatmentInitLoc(16).then((response) => { getDiseaseTreatmentInitLoc(16)
.then((response) => {
console.log('Disease Treatment Init Loc:', response); console.log('Disease Treatment Init Loc:', response);
locationOptions.value = response.data.locationOptions; locationOptions.value = response.data.locationOptions;
})
.catch(() => {
console.warn('位置列表加载失败(可能无权限)');
locationOptions.value = [];
});
}
// 下拉框模糊搜索过滤
function filterOptions(val, row, optionsKey) {
const key = row.adviceDefinitionId + '_' + optionsKey;
filterKeywords.value[key] = val;
}
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() === '') {
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);
}); });
} }
// 获取组套类型文本 // 获取组套类型文本
@@ -527,14 +621,32 @@ function selectChange(row) {
const price = row.priceList?.[0]?.price || 0; const price = row.priceList?.[0]?.price || 0;
//获取大小单位 //获取大小单位
const uniqueUnitCodes = getUnitCodeOptions(row); const uniqueUnitCodes = getUnitCodeOptions(row);
// 设置默认单位:优先使用 minUnitCode小单位回退到 unitCode大单位
let defaultUnitCode = '';
if (row.minUnitCode) {
defaultUnitCode = String(row.minUnitCode);
} else if (row.unitCode) {
defaultUnitCode = String(row.unitCode);
}
// 设置默认执行科室/位置(统一转为字符串,避免 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);
} else if (row.adviceType === 2 && locationOptions.value.length > 0) {
// 耗材:默认取第一个药房/耗材房
defaultPositionId = String(locationOptions.value[0].value);
}
//插入费用列表 //插入费用列表
feeItemsList.value.push({ feeItemsList.value.push({
...row, ...row,
uniqueUnitCodes: uniqueUnitCodes, uniqueUnitCodes: uniqueUnitCodes,
unitPrice: (price / (row.partPercent || 1)).toFixed(6), // 根据拆零比计算单价 unitPrice: (price / (row.partPercent || 1)).toFixed(6), // 根据拆零比计算单价
quantity: 1, quantity: 1,
// positionId: row.positionId === null || row.positionId === undefined ? orgId : row.positionId, // 默认执行科室 positionId: defaultPositionId, // 默认执行科室/位置(字符串类型)
selectUnitCode: row.minUnitCode, // 默认选择小单位 selectUnitCode: defaultUnitCode, // 默认选择小单位
}); });
} }
@@ -648,6 +760,101 @@ function resetData() {
searchText.value = ''; searchText.value = '';
executeTime.value = ''; executeTime.value = '';
} }
// 划价组套相关功能
function openGroupSetDialog() {
groupSetDialogVisible.value = true;
groupSetSearchText.value = '';
selectedGroupSet.value = null;
loadGroupSets();
}
function loadGroupSets() {
groupSetLoading.value = true;
getOrderGroup({ organizationId: orgId.value })
.then((res) => {
const data = res?.data || {};
if (groupSetRange.value === 1) {
groupSetList.value = data.personalList || [];
} else if (groupSetRange.value === 2) {
groupSetList.value = data.organizationList || [];
} else {
groupSetList.value = data.hospitalList || [];
}
})
.catch(() => {
console.warn('组套列表加载失败(可能无权限)');
groupSetList.value = [];
})
.finally(() => {
groupSetLoading.value = false;
});
}
function getRangeText(row) {
// 后端未返回rangeCode根据当前选中的范围标签显示
const rangeMap = { 1: '个人', 2: '科室', 3: '全院' };
if (row.rangeCode_dictText) return row.rangeCode_dictText;
return rangeMap[groupSetRange.value] || '';
}
function handleGroupSetSelect(row) {
selectedGroupSet.value = row;
}
function applyGroupSet() {
if (!selectedGroupSet.value) {
ElMessage.warning('请先选择一个组套');
return;
}
const detailList = selectedGroupSet.value.detailList;
if (!detailList || detailList.length === 0) {
ElMessage.warning('该组套没有明细项');
return;
}
let successCount = 0;
detailList.forEach((item) => {
const orderDetail = item.orderDetailInfos || {};
const feeItem = {
adviceDefinitionId: item.orderDefinitionId || orderDetail.adviceDefinitionId,
adviceName: orderDetail.adviceName || item.orderDefinitionName || '未知项目',
adviceType: orderDetail.adviceType,
unitPrice: orderDetail.unitPrice,
minUnitPrice: orderDetail.minUnitPrice,
inventoryList: orderDetail.inventoryList || [],
priceList: orderDetail.priceList || [],
partPercent: orderDetail.partPercent || 1,
positionId: item.positionId || orderDetail.positionId,
defaultLotNumber: orderDetail.defaultLotNumber,
unitCode: item.unitCode || orderDetail.unitCode,
unitCode_dictText: item.unitCodeName || orderDetail.unitCode_dictText,
minUnitCode: orderDetail.minUnitCode,
doseUnitCode: orderDetail.doseUnitCode,
categoryCode: orderDetail.categoryCode,
pharmacologyCategoryCode: orderDetail.pharmacologyCategoryCode,
partAttributeEnum: orderDetail.partAttributeEnum,
chargeItemDefinitionId: orderDetail.chargeItemDefinitionId,
adviceTableName: orderDetail.adviceTableName,
methodCode: item.methodCode || orderDetail.methodCode,
rateCode: item.rateCode || orderDetail.rateCode,
dose: item.dose || orderDetail.dose,
doseQuantity: item.doseQuantity,
dispensePerDuration: item.dispensePerDuration || orderDetail.dispensePerDuration,
dosageInstruction: orderDetail.dosageInstruction,
skinTestFlag: orderDetail.skinTestFlag,
injectFlag: orderDetail.injectFlag,
quantity: item.quantity || 1,
chrgitmLv_dictText: orderDetail.chrgitmLv_dictText,
volume: orderDetail.volume,
};
selectChange(feeItem);
successCount++;
});
if (successCount > 0) {
ElMessage.success(`已添加 ${successCount} 项组套费用`);
}
groupSetDialogVisible.value = false;
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -273,8 +273,9 @@ function handleSearch() {
// 清除缓存(搜索时需要重新加载) // 清除缓存(搜索时需要重新加载)
patientDataCache.value.clear(); patientDataCache.value.clear();
// 重新加载所有已展开病区患者列表 // 通过递增 key 强制重新渲染树组件,触发重新加载所有病区患者列表
reloadAllPatients(); // 此时 searchKey 已有值,loadPatientList 会将 searchKey 传给后端进行过滤
treeKey.value += 1;
} }
// 暴露方法供外部调用 // 暴露方法供外部调用

View File

@@ -174,7 +174,6 @@ const activeNames = ref([]);
const userStore = useUserStore(); const userStore = useUserStore();
const prescriptionList = ref([]); const prescriptionList = ref([]);
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59'); const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
const therapyEnum = ref(undefined);
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const loading = ref(false); const loading = ref(false);
const chooseAll = ref(false); const chooseAll = ref(false);
@@ -190,6 +189,10 @@ const props = defineProps({
deadline: { deadline: {
type: String, type: String,
}, },
therapyEnum: {
type: Number,
default: undefined,
},
}); });
function handleGetPrescription() { function handleGetPrescription() {
@@ -200,7 +203,7 @@ function handleGetPrescription() {
encounterIds: encounterIds, encounterIds: encounterIds,
pageSize: 10000, pageSize: 10000,
pageNo: 1, pageNo: 1,
therapyEnum: therapyEnum.value, therapyEnum: props.therapyEnum,
exeStatus: props.exeStatus, exeStatus: props.exeStatus,
requestStatus: props.requestStatus, requestStatus: props.requestStatus,
}) })

View File

@@ -58,7 +58,7 @@
:clearable="false" :clearable="false"
@change="handleGetPrescription" @change="handleGetPrescription"
/> />
<el-radio-group v-model="therapyEnum" class="ml20" @change="handleRadioChange"> <el-radio-group v-model="therapyEnum" class="ml20" @change="handleTherapyChange">
<el-radio :value="undefined">全部</el-radio> <el-radio :value="undefined">全部</el-radio>
<el-radio :value="1">长期</el-radio> <el-radio :value="1">长期</el-radio>
<el-radio :value="2">临时</el-radio> <el-radio :value="2">临时</el-radio>
@@ -79,6 +79,7 @@
:exeStatus="exeStatus" :exeStatus="exeStatus"
:requestStatus="requestStatus" :requestStatus="requestStatus"
:deadline="deadline" :deadline="deadline"
:therapyEnum="therapyEnum"
/> />
<SummaryMedicineList v-else /> <SummaryMedicineList v-else />
<!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick"> <!-- <el-tabs v-model="activeName" class="demo-tabs centered-tabs" @tab-change="handleClick">
@@ -120,6 +121,7 @@ const requestStatus = ref(RequestStatus.COMPLETED);
const chooseAll = ref(false); const chooseAll = ref(false);
const drugType = ref('1'); const drugType = ref('1');
const isDetails = ref('1'); const isDetails = ref('1');
const therapyEnum = ref(undefined);
// 存储子组件引用的对象 // 存储子组件引用的对象
const prescriptionRefs = ref(); const prescriptionRefs = ref();
@@ -175,6 +177,10 @@ function handleRadioChange(value) {
} }
} }
function handleTherapyChange() {
handleGetPrescription();
}
function handleExecute() { function handleExecute() {
proxy.$refs['prescriptionRefs'].handleMedicineSummary(); proxy.$refs['prescriptionRefs'].handleMedicineSummary();
} }

View File

@@ -229,7 +229,7 @@ import {adviceCancel, adviceExecute, adviceNoExecute, getPrescriptionList} from
import {patientInfoList} from '../../components/store/patient.js'; import {patientInfoList} from '../../components/store/patient.js';
import {lotNumberMatch} from '@/api/public'; import {lotNumberMatch} from '@/api/public';
import {formatDateStr} from '@/utils/index'; import {formatDateStr} from '@/utils/index';
import {getCurrentInstance, nextTick, ref} from 'vue'; import {getCurrentInstance, nextTick, ref, provide} from 'vue';
const activeNames = ref([]); const activeNames = ref([]);
const prescriptionList = ref([]); const prescriptionList = ref([]);
@@ -239,6 +239,8 @@ const therapyEnum = ref(undefined);
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const loading = ref(false); const loading = ref(false);
const chooseAll = ref(false); const chooseAll = ref(false);
// 独立维护选中行ID集合避免el-table内部selection状态异常导致联动全选
const selectedRowIds = ref(new Set());
const props = defineProps({ const props = defineProps({
exeStatus: { exeStatus: {
type: Number, type: Number,
@@ -280,9 +282,10 @@ function handleGetPrescription() {
exeStatus: props.exeStatus, exeStatus: props.exeStatus,
requestStatus: props.requestStatus, requestStatus: props.requestStatus,
}).then((res) => { }).then((res) => {
// try { try {
const records = res?.data?.records || [];
// 根据encounterId分组 // 根据encounterId分组
const groupedPrescriptions = res.data.records.reduce((groups, prescription) => { const groupedPrescriptions = records.reduce((groups, prescription) => {
// 获取当前医嘱全部执行频次 // 获取当前医嘱全部执行频次
let rate; let rate;
let times; let times;
@@ -427,18 +430,26 @@ function handleGetPrescription() {
// 将分组结果转换为数组形式 // 将分组结果转换为数组形式
prescriptionList.value = Object.values(groupedPrescriptions); prescriptionList.value = Object.values(groupedPrescriptions);
loading.value = false;
// 默认选中全部行 // 默认选中全部行
nextTick(() => { nextTick(() => {
defaultSelectAllRows(); defaultSelectAllRows();
}); });
// } catch { } catch (error) {
// loading.value = false; console.error('医嘱执行-获取处方列表数据处理失败:', error);
// } prescriptionList.value = [];
} finally {
loading.value = false;
}
}).catch((error) => {
console.error('医嘱执行-获取处方列表接口失败:', error);
prescriptionList.value = [];
loading.value = false;
}); });
chooseAll.value = false; chooseAll.value = false;
} else { } else {
prescriptionList.value = []; prescriptionList.value = [];
selectedRowIds.value.clear();
loading.value = false;
// proxy.$message.warning('请选择患者'); // proxy.$message.warning('请选择患者');
} }
} }
@@ -458,11 +469,19 @@ function handleExecute() {
console.log(list, 'list'); console.log(list, 'list');
adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => { adviceExecute({ exeDate: exeDate.value, adviceExecuteDetailList: list }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess(res.msg || '医嘱执行成功');
handleGetPrescription(); handleGetPrescription();
// 仅当选中医嘱中包含药品/耗材类医嘱时,才调用耗材批号匹配
const hasMedOrDevice = list.some(
(item) =>
item.adviceTable === 'med_medication_request' ||
String(item.adviceTable || '').includes('device'),
);
if (hasMedOrDevice) {
lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => { lotNumberMatch({ encounterIdList: encounterIds }, { skipErrorMsg: true }).catch((error) => {
console.warn('lotNumberMatch failed after adviceExecute:', error); console.warn('lotNumberMatch failed after adviceExecute:', error);
}); });
}
proxy.$modal.msgSuccess(res.msg || '医嘱执行成功');
} else { } else {
proxy.$modal.msgError(res.msg || '医嘱执行失败'); proxy.$modal.msgError(res.msg || '医嘱执行失败');
} }
@@ -497,15 +516,30 @@ function handleCancel() {
let list = getSelectRows(); let list = getSelectRows();
let producerIds = []; let producerIds = [];
list.forEach((item) => { list.forEach((item) => {
// 从 exePerformRecordList 直接提取 procedureId过滤空值避免后端SQL异常
const procedureIds = (item.exePerformRecordList || [])
.map((record) => record.procedureId)
.filter((id) => id != null && id !== '');
if (procedureIds.length === 0 && (!item.procedureIds || item.procedureIds.length === 0)) {
proxy.$modal.msgError('请选择已执行的医嘱记录');
return;
}
const ids = procedureIds.length > 0 ? procedureIds : item.procedureIds;
producerIds.push( producerIds.push(
...item.procedureIds.map((value) => { ...ids
.filter((value) => value != null && value !== '')
.map((value) => {
return { return {
procedureId: value, procedureId: value,
therapyEnum: item.therapyEnum, therapyEnum: Number(item.therapyEnum),
}; };
}) })
); );
}); });
if (producerIds.length === 0) {
proxy.$modal.msgError('未找到有效的执行记录,无法取消执行');
return;
}
adviceCancel({ adviceExecuteDetailList: producerIds }).then((res) => { adviceCancel({ adviceExecuteDetailList: producerIds }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess(res.msg || '取消执行成功'); proxy.$modal.msgSuccess(res.msg || '取消执行成功');
@@ -513,14 +547,20 @@ function handleCancel() {
} else { } else {
proxy.$modal.msgError(res.msg || '取消执行失败'); proxy.$modal.msgError(res.msg || '取消执行失败');
} }
}).catch(() => {
proxy.$modal.msgError('取消执行失败,请稍后重试');
}); });
} }
function getSelectRows() { function getSelectRows() {
// 获取选中的医嘱信息 // 优先从独立维护的selectedRowIds集合中获取选中行避免el-table内部selection状态异常
let list = []; const list = [];
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item) => {
list = [...list, ...proxy.$refs['tableRef' + index][0].getSelectionRows()]; item.forEach((row) => {
if (selectedRowIds.value.has(row.requestId)) {
list.push(row);
}
});
}); });
return list; return list;
} }
@@ -596,12 +636,14 @@ function handelSwicthChange(value) {
if (value) { if (value) {
// 全选选中所有行并联动checkbox // 全选选中所有行并联动checkbox
item.forEach((row) => { item.forEach((row) => {
selectedRowIds.value.add(row.requestId);
tableRef[0].toggleRowSelection(row, true); tableRef[0].toggleRowSelection(row, true);
selectAllCheckboxesInRow(row); selectAllCheckboxesInRow(row);
}); });
} else { } else {
// 取消全选取消选中所有行并联动checkbox // 取消全选取消选中所有行并联动checkbox
item.forEach((row) => { item.forEach((row) => {
selectedRowIds.value.delete(row.requestId);
tableRef[0].toggleRowSelection(row, false); tableRef[0].toggleRowSelection(row, false);
unselectAllCheckboxesInRow(row); unselectAllCheckboxesInRow(row);
}); });
@@ -612,11 +654,14 @@ function handelSwicthChange(value) {
// 默认选中全部行 // 默认选中全部行
function defaultSelectAllRows() { function defaultSelectAllRows() {
// 清空并重建选中集合
selectedRowIds.value.clear();
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item, index) => {
const tableRef = proxy.$refs['tableRef' + index]; const tableRef = proxy.$refs['tableRef' + index];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
// 选中该表格的所有行 // 选中该表格的所有行
item.forEach((row) => { item.forEach((row) => {
selectedRowIds.value.add(row.requestId);
tableRef[0].toggleRowSelection(row, true); tableRef[0].toggleRowSelection(row, true);
// 同时选中该行内部的所有checkbox // 同时选中该行内部的所有checkbox
selectAllCheckboxesInRow(row); selectAllCheckboxesInRow(row);
@@ -696,13 +741,14 @@ function checkAndToggleRowSelection(row) {
const tableRef = proxy.$refs['tableRef' + tableIndex]; const tableRef = proxy.$refs['tableRef' + tableIndex];
if (tableRef && tableRef[0]) { if (tableRef && tableRef[0]) {
const isAllSelected = isAllCheckboxesSelected(row); const isAllSelected = isAllCheckboxesSelected(row);
const selectedRows = tableRef[0].getSelectionRows(); const isCurrentlySelected = selectedRowIds.value.has(row.requestId);
const isCurrentlySelected = selectedRows.some((r) => r.requestId === row.requestId);
// 根据checkbox状态更新表格行选中状态 // 根据checkbox状态更新表格行选中状态
if (isAllSelected && !isCurrentlySelected) { if (isAllSelected && !isCurrentlySelected) {
selectedRowIds.value.add(row.requestId);
tableRef[0].toggleRowSelection(row, true); tableRef[0].toggleRowSelection(row, true);
} else if (!isAllSelected && isCurrentlySelected) { } else if (!isAllSelected && isCurrentlySelected) {
selectedRowIds.value.delete(row.requestId);
tableRef[0].toggleRowSelection(row, false); tableRef[0].toggleRowSelection(row, false);
} }
} }
@@ -715,9 +761,11 @@ function handleRowSelect(selection, row, tableIndex) {
const isSelected = selection.some((item) => item.requestId === row.requestId); const isSelected = selection.some((item) => item.requestId === row.requestId);
if (isSelected) { if (isSelected) {
selectedRowIds.value.add(row.requestId);
// 选中行时选中该行内部的所有checkbox // 选中行时选中该行内部的所有checkbox
selectAllCheckboxesInRow(row); selectAllCheckboxesInRow(row);
} else { } else {
selectedRowIds.value.delete(row.requestId);
// 取消选中行时取消选中该行内部的所有checkbox // 取消选中行时取消选中该行内部的所有checkbox
unselectAllCheckboxesInRow(row); unselectAllCheckboxesInRow(row);
} }
@@ -734,11 +782,13 @@ function handleSelectAll(selection, tableIndex) {
if (selection.length > 0) { if (selection.length > 0) {
// 全选时选中所有行内部的所有checkbox // 全选时选中所有行内部的所有checkbox
tableData.forEach((row) => { tableData.forEach((row) => {
selectedRowIds.value.add(row.requestId);
selectAllCheckboxesInRow(row); selectAllCheckboxesInRow(row);
}); });
} else { } else {
// 取消全选时取消选中所有行内部的所有checkbox // 取消全选时取消选中所有行内部的所有checkbox
tableData.forEach((row) => { tableData.forEach((row) => {
selectedRowIds.value.delete(row.requestId);
unselectAllCheckboxesInRow(row); unselectAllCheckboxesInRow(row);
}); });
} }
@@ -750,17 +800,13 @@ function handleSelectAll(selection, tableIndex) {
// 更新全选开关状态 // 更新全选开关状态
function updateChooseAllStatus() { function updateChooseAllStatus() {
let allSelected = true; let allSelected = true;
prescriptionList.value.forEach((item, index) => { prescriptionList.value.forEach((item) => {
const tableRef = proxy.$refs['tableRef' + index]; item.forEach((row) => {
if (tableRef && tableRef[0]) { if (!selectedRowIds.value.has(row.requestId)) {
const selectedRows = tableRef[0].getSelectionRows();
if (selectedRows.length !== item.length) {
allSelected = false;
}
} else {
allSelected = false; allSelected = false;
} }
}); });
});
chooseAll.value = allSelected; chooseAll.value = allSelected;
} }

View File

@@ -51,7 +51,7 @@
</template> </template>
<script setup> <script setup>
import {getCurrentInstance} from 'vue'; import {getCurrentInstance, ref, nextTick, provide} from 'vue';
import PatientList from '../components/patientList.vue'; import PatientList from '../components/patientList.vue';
import PrescriptionList from './components/prescriptionList.vue'; import PrescriptionList from './components/prescriptionList.vue';
import { RequestStatus } from '@/utils/medicalConstants'; import { RequestStatus } from '@/utils/medicalConstants';

View File

@@ -181,7 +181,7 @@ function handleGetPrescription() {
getPrescriptionList({ getPrescriptionList({
encounterIds: encounterIds, encounterIds: encounterIds,
requestStatus: props.requestStatus, requestStatus: props.requestStatus,
therapyEnum: type.value === 1 ? undefined : type.value === 2 ? 1 : 2, // 1=全部(不传), 2=长期(1), 3=临时(2) ...(type.value !== undefined ? { therapyEnum: type.value } : {}),
pageSize: 10000, pageSize: 10000,
pageNo: 1, pageNo: 1,
}).then((res) => { }).then((res) => {
@@ -288,6 +288,12 @@ function handleCheck() {
function handleCancel() { function handleCancel() {
let list = getSelectRows(); let list = getSelectRows();
if (list.length > 0) { if (list.length > 0) {
// 校验已发药的医嘱不允许退回
let dispensedItems = list.filter(item => item.dispenseStatus === 4);
if (dispensedItems.length > 0) {
proxy.$message.error('该医嘱已发药,无法退回');
return;
}
cancel(list).then((res) => { cancel(list).then((res) => {
if (res.code == 200) { if (res.code == 200) {
proxy.$modal.msgSuccess(res.msg); proxy.$modal.msgSuccess(res.msg);

View File

@@ -112,6 +112,19 @@
<el-form ref="dynamicForm" :model="formData" label-width="100px" :rules="formRules"> <el-form ref="dynamicForm" :model="formData" label-width="100px" :rules="formRules">
<div class="page-bottom"> <div class="page-bottom">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="8">
<el-form-item style="margin-top: 15px" label="录入日期">
<el-date-picker
v-model="formData.recordingDate"
type="date"
placeholder="请选择日期"
size="small"
format="YYYY/MM/DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item style="margin-top: 15px" label="录入时间"> <el-form-item style="margin-top: 15px" label="录入时间">
<div class="input-time-inline"> <div class="input-time-inline">
@@ -766,6 +779,7 @@ const receptionTime = ref(null);
// 表单数据 - 体征录入 // 表单数据 - 体征录入
const formData = ref({ const formData = ref({
recordingDate: '',
timePoint: '', timePoint: '',
temperature: '', temperature: '',
systolicPressure: '', systolicPressure: '',
@@ -872,6 +886,7 @@ function getPatientDetial() {
// 默认查询今天的数据 // 默认查询今天的数据
const today = moment().format('YYYY-MM-DD'); const today = moment().format('YYYY-MM-DD');
receptionTime.value = [today, today]; receptionTime.value = [today, today];
formData.value.recordingDate = today;
// 自动加载数据 // 自动加载数据
getPatientList(); getPatientList();
listPatient(queryParams.value).then((res) => { listPatient(queryParams.value).then((res) => {
@@ -920,6 +935,7 @@ function handleRowClick(row) {
formData.value = { formData.value = {
...formData.value, ...formData.value,
id: row.id, id: row.id,
recordingDate: row.recordingDate || '',
timePoint: convertTimePoint(row.timePoint) || '', timePoint: convertTimePoint(row.timePoint) || '',
temperature: row.temperature || '', temperature: row.temperature || '',
systolicPressure: row.systolicPressure || '', systolicPressure: row.systolicPressure || '',
@@ -1038,7 +1054,7 @@ function confirmCharge() {
params.vitalSignsCode = vitalSignsCode; params.vitalSignsCode = vitalSignsCode;
params.vitalSignsValues = vitalSignsValues; params.vitalSignsValues = vitalSignsValues;
params.recordingDate = moment(new Date()).format('YYYY-MM-DD'); params.recordingDate = formData.value.recordingDate || moment(new Date()).format('YYYY-MM-DD');
addVitalSigns(params).then(res => { addVitalSigns(params).then(res => {
console.log('保存成功:', res); console.log('保存成功:', res);
@@ -1047,6 +1063,7 @@ function confirmCharge() {
getPatientList(); getPatientList();
// 清空表单 // 清空表单
formData.value = { formData.value = {
recordingDate: '',
timePoint: '', timePoint: '',
temperature: '', temperature: '',
systolicPressure: '', systolicPressure: '',

View File

@@ -198,7 +198,38 @@
</el-button> </el-button>
</div> </div>
<div class="queue-actions-right"> </div>
</div>
</div>
<!-- 底部控制面板 -->
<div class="footer-section">
<!-- Bug #411诊室快速过滤栏筛选维度从科室改为诊室 -->
<div class="filter-section">
<div class="filter-left">
<div class="filter-label">
诊室快速过滤栏
</div>
<div class="filter-button-wrapper">
<el-button
:type="selectedRoom === 'all' ? 'primary' : ''"
size="small"
@click="selectedRoom = 'all'"
>
全部
</el-button>
<el-button
v-for="room in uniqueRooms"
:key="room"
:type="selectedRoom === room ? 'primary' : ''"
size="small"
@click="selectedRoom = room"
>
{{ room }}
</el-button>
</div>
</div>
<div class="filter-right">
<el-button <el-button
:type="showOnlyWaiting ? 'primary' : ''" :type="showOnlyWaiting ? 'primary' : ''"
size="small" size="small"
@@ -215,38 +246,6 @@
</el-button> </el-button>
</div> </div>
</div> </div>
</div>
</div>
<!-- 底部控制面板 -->
<div class="footer-section">
<!-- 就诊科室快速过滤栏 -->
<div class="filter-section">
<div class="filter-label">
就诊科室快速过滤栏
</div>
<div class="filter-select-wrapper">
<el-select
v-model="selectedDept"
placeholder="请选择就诊科室"
clearable
filterable
style="width: 100%"
size="default"
>
<el-option
label="全部"
value="all"
/>
<el-option
v-for="dept in departmentList"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</div>
</div>
<!-- 叫号控制板 --> <!-- 叫号控制板 -->
<div class="call-control-section"> <div class="call-control-section">
@@ -648,7 +647,6 @@ import { Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { import {
getCandidatePool, getCandidatePool,
getLocationTree,
getTriageQueueList, getTriageQueueList,
addToQueue, addToQueue,
removeFromQueue, removeFromQueue,
@@ -681,10 +679,8 @@ const selectedCandidates = ref([])
// 显示选项 // 显示选项
const showOnlyWaiting = ref(false) const showOnlyWaiting = ref(false)
// 科室过滤(改为使用就诊科室 // Bug #411诊室过滤替代原来的科室下拉框selectedDept/departmentList 已移除
const selectedDept = ref('all') const selectedRoom = ref('all')
// 就诊科室列表
const departmentList = ref([])
// 修复【#397】动态获取当前科室名称 // 修复【#397】动态获取当前科室名称
const currentDeptName = computed(() => { const currentDeptName = computed(() => {
@@ -907,12 +903,11 @@ const mapFrontendStatusToBackend = (status) => {
// 从数据库加载队列 // 从数据库加载队列
const loadQueueFromDb = async () => { const loadQueueFromDb = async () => {
try { try {
// 如果选择了具体科室,就按科室加载;否则加载当前登录人科室(后端默认) // Bug #411不再按科室选筛加载后端默认按当前登录人科室查询
const organizationId = selectedDept.value !== 'all' ? selectedDept.value : undefined const organizationId = undefined
// 只查询今天的患者 // 只查询今天的患者
const today = new Date() const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}` const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
console.log('【心内科】loadQueueFromDb 开始organizationId=', organizationId, 'date=', todayStr, 'selectedDept=', selectedDept.value)
const res = await getTriageQueueList({ organizationId, date: todayStr }).catch((err) => { const res = await getTriageQueueList({ organizationId, date: todayStr }).catch((err) => {
console.error('【心内科】loadQueueFromDb 请求异常:', err) console.error('【心内科】loadQueueFromDb 请求异常:', err)
return { code: 500, msg: err?.message || '请求失败', data: null } return { code: 500, msg: err?.message || '请求失败', data: null }
@@ -1158,55 +1153,29 @@ const loadDataFromApi = async () => {
// 原始数据存储(用于过滤) // 原始数据存储(用于过滤)
const originalCandidatePoolList = ref(getInitialCandidatePoolList()) const originalCandidatePoolList = ref(getInitialCandidatePoolList())
// 辅助函数:扁平化科室树形结构 // 过滤后的智能候选池数据(按诊室过滤)
const flattenDepartmentTree = (tree, result = []) => {
if (!Array.isArray(tree)) return result
tree.forEach(node => {
if (node.id && node.name) {
result.push({ id: node.id, name: node.name })
}
if (node.children && Array.isArray(node.children)) {
flattenDepartmentTree(node.children, result)
}
})
return result
}
// 加载就诊科室列表
const loadDepartmentList = async () => {
try {
const response = await getLocationTree()
if (response && response.data) {
// 扁平化树形结构
departmentList.value = flattenDepartmentTree(response.data)
console.log('【心内科】已加载就诊科室列表:', departmentList.value.length, '个科室')
}
} catch (error) {
console.error('【心内科】加载就诊科室列表失败:', error)
ElMessage.warning('加载就诊科室列表失败,使用默认数据')
}
}
// 获取选中科室的名称
const getSelectedDeptName = () => {
if (selectedDept.value === 'all') return null
const dept = departmentList.value.find(d => d.id === selectedDept.value)
return dept ? dept.name : null
}
// 过滤后的智能候选池数据
const filteredCandidatePoolList = computed(() => { const filteredCandidatePoolList = computed(() => {
if (selectedDept.value === 'all') { if (selectedRoom.value === 'all') {
return originalCandidatePoolList.value return originalCandidatePoolList.value
} }
const deptName = getSelectedDeptName() return originalCandidatePoolList.value.filter(item => item.room === selectedRoom.value)
if (!deptName) return originalCandidatePoolList.value
return originalCandidatePoolList.value.filter(item => item.room === deptName)
}) })
// 原始队列数据存储(用于过滤) // 原始队列数据存储(用于过滤)
const originalQueueList = ref(getInitialQueueList()) const originalQueueList = ref(getInitialQueueList())
// 动态计算已加载数据中的唯一诊室列表(依赖上方两个 ref确保声明顺序正确
const uniqueRooms = computed(() => {
const rooms = new Set()
originalCandidatePoolList.value.forEach(item => {
if (item.room && item.room !== '-') rooms.add(item.room)
})
originalQueueList.value.forEach(item => {
if (item.room && item.room !== '-') rooms.add(item.room)
})
return Array.from(rooms).sort()
})
const parseMmSsToSeconds = (mmss) => { const parseMmSsToSeconds = (mmss) => {
if (!mmss || typeof mmss !== 'string') return 0 if (!mmss || typeof mmss !== 'string') return 0
const [mm, ss] = mmss.split(':') const [mm, ss] = mmss.split(':')
@@ -1223,19 +1192,16 @@ const formatSecondsToMmSs = (totalSeconds) => {
return `${mm}:${ss}` return `${mm}:${ss}`
} }
// 过滤后的智能队列数据(同时考虑科室过滤和状态过滤) // 过滤后的智能队列数据(Bug #411诊室过滤 + 状态过滤)
const filteredQueueList = computed(() => { const filteredQueueList = computed(() => {
let filtered = originalQueueList.value let filtered = originalQueueList.value
// 先过滤掉"已完成"状态的患者(无论什么情况都不显示) // 先过滤掉"已完成"状态的患者(无论什么情况都不显示)
filtered = filtered.filter(item => item.status !== '已完成') filtered = filtered.filter(item => item.status !== '已完成')
// 再按室过滤 // 再按室过滤
if (selectedDept.value !== 'all') { if (selectedRoom.value !== 'all') {
const deptName = getSelectedDeptName() filtered = filtered.filter(item => item.room === selectedRoom.value)
if (deptName) {
filtered = filtered.filter(item => item.room === deptName)
}
} }
// 再按状态过滤(只显示等待) // 再按状态过滤(只显示等待)
@@ -1746,16 +1712,12 @@ const handleNextPatient = async () => {
reqData.id = selectedQueueRow.value.id reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId reqData.organizationId = selectedQueueRow.value.organizationId
} else { } else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑) // Bug #411已移除 selectedDept改为从队列数据中动态获取科室
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
// "全科"模式:优先用"当前叫号中/第一个等待"所在科室
if (orgId == null) {
const calling = originalQueueList.value.find((i) => i.status === '叫号中') const calling = originalQueueList.value.find((i) => i.status === '叫号中')
const waiting = originalQueueList.value.find((i) => i.status === '等待') const waiting = originalQueueList.value.find((i) => i.status === '等待')
console.log('【心内科】handleNextPatient 查找:叫号中=', calling?.patientName, '等待=', waiting?.patientName) console.log('【心内科】handleNextPatient 查找:叫号中=', calling?.patientName, '等待=', waiting?.patientName)
orgId = calling?.organizationId ?? waiting?.organizationId const orgId = calling?.organizationId ?? waiting?.organizationId
console.log('【心内科】handleNextPatient 确定的 orgId=', orgId) console.log('【心内科】handleNextPatient 确定的 orgId=', orgId)
}
if (orgId != null) { if (orgId != null) {
reqData.organizationId = orgId reqData.organizationId = orgId
} }
@@ -1784,13 +1746,9 @@ const handleSkip = async () => {
reqData.id = selectedQueueRow.value.id reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId reqData.organizationId = selectedQueueRow.value.organizationId
} else { } else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑) // 如果没有选中患者,使用当前叫号中的科室
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
// “全科”模式:优先用“当前叫号中”所在科室
if (orgId == null) {
const calling = originalQueueList.value.find((i) => i.status === '叫号中') const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId const orgId = calling?.organizationId
}
if (orgId != null) { if (orgId != null) {
reqData.organizationId = orgId reqData.organizationId = orgId
} }
@@ -1818,13 +1776,9 @@ const handleComplete = async () => {
reqData.id = selectedQueueRow.value.id reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId reqData.organizationId = selectedQueueRow.value.organizationId
} else { } else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑) // 如果没有选中患者,使用当前叫号中的科室
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
// “全科”模式:优先用“当前叫号中”所在科室
if (orgId == null) {
const calling = originalQueueList.value.find((i) => i.status === '叫号中') const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId const orgId = calling?.organizationId
}
if (orgId != null) { if (orgId != null) {
reqData.organizationId = orgId reqData.organizationId = orgId
} }
@@ -1852,13 +1806,9 @@ const handleRequeue = async () => {
reqData.id = selectedQueueRow.value.id reqData.id = selectedQueueRow.value.id
reqData.organizationId = selectedQueueRow.value.organizationId reqData.organizationId = selectedQueueRow.value.organizationId
} else { } else {
// 如果没有选中患者,使用查询条件(兼容旧逻辑) // 如果没有选中患者,使用当前叫号中的科室
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
// “全科”模式:优先用“当前叫号中”所在科室
if (orgId == null) {
const calling = originalQueueList.value.find((i) => i.status === '叫号中') const calling = originalQueueList.value.find((i) => i.status === '叫号中')
orgId = calling?.organizationId const orgId = calling?.organizationId
}
if (orgId != null) { if (orgId != null) {
reqData.organizationId = orgId reqData.organizationId = orgId
} }
@@ -2121,8 +2071,6 @@ const handleTestRule = () => {
// 组件挂载 // 组件挂载
onMounted(() => { onMounted(() => {
// 加载就诊科室列表
loadDepartmentList()
// 初始化:优先从后端加载,失败则回退本地假数据 // 初始化:优先从后端加载,失败则回退本地假数据
loadDataFromApi() loadDataFromApi()
startWaitingTimer() startWaitingTimer()
@@ -2244,10 +2192,6 @@ onUnmounted(() => {
gap: 10px; gap: 10px;
} }
.queue-actions-right {
display: flex;
gap: 10px;
}
} }
.candidate-actions { .candidate-actions {
@@ -2269,6 +2213,12 @@ onUnmounted(() => {
.filter-section { .filter-section {
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
align-items: flex-start;
justify-content: space-between;
.filter-left {
flex: 1;
.filter-label { .filter-label {
font-size: 14px; font-size: 14px;
@@ -2277,8 +2227,18 @@ onUnmounted(() => {
margin-bottom: 10px; margin-bottom: 10px;
} }
.filter-select-wrapper { .filter-button-wrapper {
width: 100%; display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.filter-right {
display: flex;
gap: 8px;
flex-shrink: 0;
align-self: flex-end;
} }
} }