Compare commits

..

82 Commits

Author SHA1 Message Date
232577caaa fix: #579 (codex) 2026-05-24 15:07:56 +08:00
926c1f68e3 fix: #568 (codex) 2026-05-24 15:03:14 +08:00
f11fa023c4 fix: #579 (codex) 2026-05-24 14:57:10 +08:00
1ac2252c34 fix: #568 (codex) 2026-05-24 14:55:18 +08:00
2b2fcc0f20 fix: #568 (codex) 2026-05-24 14:53:07 +08:00
72b0040921 fix: #568 (codex) 2026-05-24 14:45:15 +08:00
e439cf46cf update his-repo submodule 2026-05-24 14:40:20 +08:00
24ad69dfed fix: #568 门诊日结排版 #571 撤回流程 #579 报表格式 2026-05-24 14:37:31 +08:00
310847eae4 Fix Bug #571: 修复检验申请撤回时双重错误提示
根因:响应拦截器已对非200响应(code=500等)显示ElMessage错误提示,
但handleWithdraw的catch块再次调用proxy.$modal.msgError显示相同错误,
导致用户看到两个红色错误弹窗。

修复:将handleWithdraw和handleDelete的catch块改为静默处理,
与examineApplication.vue的handleRecall模式一致——响应拦截器已统一处理错误提示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:37:27 +08:00
fd0fe29e54 Fix Bug #568: 根因+修复方案摘要 2026-05-22 12:36:30 +08:00
1406bbfcee Fix Bug #568: 修复门诊日结页面排版混乱 - 优化CSS Grid布局间距和字体
- 增大 grid gap 从 10px 16px 到 12px 24px,改善行列间距
- 为 .report-item 添加 gap: 8px,标签与数值间留白
- 统一 .label 字体大小为 14px,保持视觉一致
- 移除 .value 的 overflow:hidden,避免内容截断
- 调整费用性质下拉框宽度为 130px
2026-05-22 12:35:35 +08:00
f08e047a66 Fix Bug #571: 根因+修复方案摘要 2026-05-22 12:31:23 +08:00
6e15c334ec Fix Bug #571: 根因+修复方案摘要 2026-05-22 12:30:03 +08:00
b4bcb0898f Fix Bug #571: 根因+修复方案摘要 2026-05-22 12:15:00 +08:00
ef81dff673 Fix Bug #571: 根因+修复方案摘要 2026-05-22 12:12:40 +08:00
8d0b158b01 Fix Bug #568: 根因+修复方案摘要 2026-05-22 12:00:05 +08:00
f458a75324 Fix Bug #568: 修复门诊日结页面排版混乱 - 居中报告容器并优化间距
根因:
1. .report-container缺少margin:0 auto,宽屏下报告内容左对齐,右侧大量留白
2. Grid行间距gap:8px偏小,数据项之间视觉层次不够分明
3. 分隔线margin:12px偏小,各区块之间区分不够清晰

修复:
1. 添加margin:0 auto居中报告容器,在宽屏下对称显示
2. Grid行间距从8px增至10px,改善数据项之间的视觉间距
3. 分隔线margin从12px增至16px,增强区块之间的视觉分隔

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:57:12 +08:00
6db7659990 Fix Bug #568: 修复门诊日结页面排版混乱 - 使用固定宽度替代最小宽度确保标签对齐
将 .label 的 min-width: 140px 改为 width: 140px,确保所有标签宽度一致,
避免短标签(如"现金:")和长标签(如"实际现金收入:")宽度不同导致的排版混乱。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:55:02 +08:00
67a7f17abd Fix Bug #571: 根因+修复方案摘要 2026-05-22 11:54:34 +08:00
6d6a17615c Fix Bug #568: 修复门诊日结页面排版混乱 - 增强网格对齐和标签宽度
根因:原始布局使用混合的cols-3/cols-4网格类,缺少统一的对齐方式,
标签宽度不足导致较长标签(如"实际现金收入:")显示空间不够。

修复:
1. 统一使用CSS Grid 4列布局,配合span-2处理跨列项
2. 添加align-items:baseline确保网格项文本基线对齐
3. 将.label最小宽度从120px增加到140px适配长标签
4. 添加flex:1让.value占据剩余空间
5. 添加响应式断点支持移动端/平板显示
6. 移除文本溢出截断,确保内容完整显示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:52:28 +08:00
3913f70351 Fix Bug #568: 修复门诊日结页面排版混乱 - 移除overflow裁剪并改用min-width自适应标签
根因:
1. .report-item的overflow:hidden导致内容被裁剪显示不全
2. .label使用固定width:120px,较长标签(如"实际现金收入:")空间不足导致排版错乱

修复:
1. 移除.report-item的overflow:hidden,让内容自然显示
2. 将.label从width:120px改为min-width:120px,允许标签按内容自适应扩展

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:48:08 +08:00
bb43c6f3cb Fix Bug #568: 修复门诊日结页面排版混乱 - 移除overflow裁剪并调整标签宽度
根因:
1. .report-item的overflow:hidden导致内容被裁剪,显示不全
2. .label宽度120px对于较长标签(如"实际现金收入:")显示空间不足
3. 响应式断点中.span-2在2列布局下错误地设为span 1

修复:
1. 移除.report-item的overflow:hidden,让内容自然显示
2. 将.label宽度从120px增加到140px
3. 修正1200px断点下.span-2为span 2(保持跨2列)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:46:58 +08:00
1f653ed729 Fix Bug #571: 修复检验申请撤回操作状态校验逻辑不一致
根因:SQL查询使用EXISTS判断(任一ServiceRequest为ACTIVE即显示已签发),
但后端撤回校验使用allMatch(要求所有ServiceRequest均为ACTIVE)。
当多项申请单中部分为待签发时,前端显示已签发但后端拒绝撤回,导致报错。

修复:
1. 将allMatch改为anyMatch,与SQL的EXISTS逻辑保持一致
2. 仅更新ACTIVE状态的ServiceRequest为DRAFT,避免影响其他状态
3. 增加update返回值校验,处理并发场景下的状态变更

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:40:55 +08:00
385c3e0990 Fix Bug #568: 修复门诊日结页面排版混乱 - 补充容器宽度和移除内容截断
根因:.report-container缺少width:100%导致容器未填满可用空间,
网格列过窄造成内容溢出和排版混乱。.value的overflow:hidden和
text-overflow:ellipsis截断了显示内容。
修复:添加width:100%和box-sizing:border-box,移除.value的溢出截断。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:40:35 +08:00
e5c13f6e30 Fix Bug #571: 修复检验申请撤回操作状态校验逻辑不一致
根因:SQL查询使用EXISTS判断(任一ServiceRequest为ACTIVE即显示已签发),
但后端撤回校验使用allMatch(要求所有ServiceRequest均为ACTIVE)。
当多项申请单中部分为待签发时,前端显示已签发但后端拒绝撤回,导致报错。

修复:将allMatch改为anyMatch,与SQL的EXISTS逻辑保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:32:56 +08:00
00d7d2ce0b Fix Bug #568: 修复门诊日结页面排版混乱 - 使用CSS Grid替代el-row布局
根因:之前使用el-row/el-col配合float:right和margin-right:50px导致列对齐混乱。
修复:改用CSS Grid布局(repeat(4,1fr))确保列均匀对齐,添加响应式断点。
2026-05-22 11:14:31 +08:00
cab2a92e9a Fix Bug #568: 根因+修复方案摘要 2026-05-22 11:12:13 +08:00
23158ecc82 Fix Bug #568: 修复门诊日结页面排版混乱 - 使用CSS Grid替代el-row布局
根因:原版使用el-row/el-col的:span="5"布局(5×4=20/24),列间距不均匀,
3项行与4项行不对齐,且固定1200px宽度无响应式。

修复方案:
- 使用CSS Grid 4列等宽布局(repeat(4, 1fr))替代el-row/el-col
- 3项行的最后一项使用span-2横跨2列,与4项行对齐
- 添加响应式断点:<=1200px降为2列,<=768px降为1列
- 为label固定120px宽度+右对齐,value使用ellipsis截断

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:10:39 +08:00
2eba125351 Fix Bug #568: 根因+修复方案摘要 2026-05-22 11:07:35 +08:00
03e47be0d8 Fix Bug #571: 修复检验申请撤回操作状态校验逻辑不一致
根因:SQL查询使用EXISTS判断(任一ServiceRequest为ACTIVE即显示已签发),
但后端撤回校验使用allMatch(要求所有ServiceRequest均为ACTIVE)。
当多项申请单中部分为待签发时,前端显示已签发但后端拒绝撤回,导致报错。

修复:将allMatch改为anyMatch,与SQL的EXISTS逻辑保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:00:13 +08:00
4c462e00db Fix Bug #568: 修复门诊日结页面排版混乱问题
根因:费用明细最后一行使用cols-3导致与上面cols-4行不对齐
修复:统一使用cols-4网格布局,对需要占两列的项使用span-2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:56:36 +08:00
fce3c9ab01 Fix Bug #571: 根因+修复方案摘要 2026-05-22 10:53:18 +08:00
50f1013391 Fix Bug #571: 修复检验申请撤回操作模板逻辑错误
问题:已签发状态的检验申请点击撤回时触发错误提示
根因:模板中 v-if/v-else-if 链结构错误,isReportStatus 作为 canManageRow 的
else-if 分支,导致权限校验和状态判断互相干扰,撤回按钮显示逻辑异常
修复:将嵌套的 v-if/v-else-if 改为独立的 v-if 块,每个按钮的显示条件独立判断

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:50:24 +08:00
e81a6a9e37 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 修正最后一行费用明细的列数
将费用明细最后一行的 cols-4 改为 cols-3,因为该行只有3个项目(诊疗费、挂号费、其他费用),
使用4列网格会导致布局错位。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:40:39 +08:00
fc05eef2b3 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 添加缺失的section-title和report-section样式定义
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:39:13 +08:00
3d998e3987 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 添加缺失的cols-3和cols-4 CSS类定义
根因:模板中使用了 cols-3 和 cols-4 类名区分3列和4列布局,但CSS中只定义了
report-row 的固定4列网格,导致所有行都以4列显示,3列行的布局错乱。

修复:将 report-row 的 grid-template-columns 移到 cols-3 和 cols-4 类中,
使3列行正确显示为3列,4列行显示为4列。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:36:48 +08:00
64cfc20bd4 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 使用CSS Grid确保列对齐
根因:页面使用CSS Grid 4列布局,但部分行只有3个数据项,导致列不对齐。
修复:为3个数据项的行添加空占位div,确保所有行都有4个元素与grid列数匹配。
2026-05-22 10:35:39 +08:00
80cc0e4fa2 Fix Bug #571: 修复检验申请撤回操作权限问题 - 移除非权限用户的撤回按钮
问题:非申请者本人点击撤回按钮时,后端权限校验失败导致报错
原因:模板中 isIssuedStatus 分支对所有用户显示撤回按钮,但后端会校验权限
修复:移除非权限用户(canManageRow为false)的撤回按钮,只保留详情按钮

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:34:41 +08:00
c11fe3f0af Fix Bug #568: 修复门诊日结页面排版混乱问题 - 使用CSS Grid确保列对齐
- 使用 CSS Grid (grid-template-columns: repeat(4, 1fr)) 替代 flexbox
- 确保所有行的列宽一致,解决3项行与4项行不对齐问题
- 固定 label 宽度为 120px,保持标签对齐
- 移除 flex-wrap,使用网格布局自动换行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:32:29 +08:00
0a4b901300 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 使用div+CSS flexbox替代el-row布局
- 使用 div + CSS flexbox 替代 el-row/el-col 布局
- 添加 report-container/report-row/report-item 语义化类名
- 移除 float: right 导致的对齐错乱
- 使用 min-width 替代固定 width,自适应标签长度
- 添加分隔线区分不同费用类别

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:29:10 +08:00
4f6df9017a Fix Bug #571: 添加缺失的 isIssuedStatus 函数定义,修复检验申请撤回操作报错
模板中使用了 isIssuedStatus() 但脚本中未定义该函数,导致已签发状态的检验申请
在非申请者本人账号下查看时触发 ReferenceError,撤回按钮无法正常显示和操作。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:19:03 +08:00
7115563ff9 Fix Bug #568: 修复门诊日结页面排版混乱问题 - 添加固定label宽度、分隔线和统一布局 2026-05-22 10:02:04 +08:00
Ranyunqiao
175a863aa0 497 【住院医生工作站-检查申请】检查申请列表缺失“申请单状态”列及全流程闭环状态流转逻辑 2026-05-22 10:00:07 +08:00
69e048e21e Fix Bug #568: 修复门诊日结页面排版混乱问题
- el-col span从3改为6,增加列宽避免内容挤压
- 移除.label的固定宽度120px和.value的float:right
- 每列使用inline-flex布局,标签和数值自然对齐
- 增加gutter间距和flex-wrap响应式换行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:42:50 +08:00
bcc2f490a0 bug550\556569 2026-05-21 17:40:26 +08:00
Ranyunqiao
966e4f6544 497【住院医生工作站-检查申请】检查申请列表缺失“申请单状态”列及全流程闭环状态流转逻辑
523 [住院医生站-临床医嘱] 待保存医嘱总金额显示缺失且编辑态单位选择框变为数字控件
560 [住院医生站-检验申请] “已签发”状态的申请单在操作列缺失“详情”查看按钮
563 [住院医生站-临床医嘱-手术] 打开手术申请单弹窗时出现异常,功能无法使用
2026-05-21 17:06:09 +08:00
wangjian963
8c81c52f4e Merge remote-tracking branch 'origin/develop' into develop 2026-05-20 18:13:22 +08:00
wangjian963
b97a3ad598 562 [门诊医生工作站-待写病历]数据加载时间超过2秒一直加载
561 [门诊医生站-医嘱] 医嘱录入后,总量单位显示异常,显示为“null”而非诊疗目录配置值
544 【智能分诊】排队队列列表无法显示“完诊”状态患者且缺失历史队列查询功能
505 【业务逻辑缺陷】药品医嘱已由药房发药,护士仍能在“医嘱校对”模块执行“退回”操作
2026-05-20 18:12:58 +08:00
474aa894fd bug519 [门诊医生站-诊断-报卡] 已完成传染病报卡的诊断在再次点保存时重复弹出报卡界面
Number()导致conditionId精度丢失,conditionId现在会在所有传染病诊断中选择
2026-05-20 17:35:26 +08:00
ed7e4bbeb3 bug469 2026-05-20 13:47:36 +08:00
1e77c0756b Fix Bug #559: 根因+修复方案摘要 2026-05-20 11:08:03 +08:00
Ranyunqiao
3e89cb7977 Merge remote-tracking branch 'origin/develop' into develop 2026-05-20 11:05:03 +08:00
Ranyunqiao
62c5674233 bug 555 558 2026-05-20 11:04:33 +08:00
41948c0bcd Fix Bug #559: 根因+修复方案摘要 2026-05-20 11:02:41 +08:00
31d9098b37 Fix Bug #547: 执行科室配置保存时时间冲突检测范围错误 — 根因:addOrEditOrgLoc 方法使用 getOrgLocListByActivityDefinitionId 跨科室查询同一诊疗的所有配置,导致不同科室间的正常时间重叠被误判为冲突;修复:改为 getOrgLocListByOrgIdAndActivityDefinitionId(orgId, activityDefId) 限定同科室范围;同时优化软删除科室处理,当冲突记录关联的科室已被删除时,使用"科室[ID]已删除"替代静默跳过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:17:06 +08:00
2db79e3ac9 Fix Bug #559: 住院医生站-临床医嘱 组套功能添加医嘱后新增医嘱置顶显示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:15:39 +08:00
c7da7440f6 Fix Bug #556: 就诊卡号改用busNo映射、执行时间默认当前时间、套餐标识增加packageName联合判断
根因:
1. medicalrecordNumber 绑定到 identifierNo(身份证号)而非 busNo(就诊卡号),导致字段为空
2. executeTime 初始化为 null 且未在 initData/resetForm 中设置默认值
3. loadApplicationToForm 中 isPackage 判断仅用 feePackageId != null,缺少 packageName 联合判断,
   导致 feePackageId 非空但非套餐的项目被误标为"套餐"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:11:16 +08:00
wangjian963
232a0db810 Merge remote-tracking branch 'origin/develop' into develop 2026-05-20 09:46:29 +08:00
wangjian963
3394aa54d7 549【住院医生站-临床医嘱-检验】打开“检验申请单”弹窗获取项目列表响应极其缓慢
546 【患者管理】-【患者列表】-【新增患者】,新增患者,保存成功,但没有数据
536 [门诊手术安排]“手术申请查询”弹窗底部,分页组件与界底部元素重叠,影响操作。
2026-05-20 09:45:33 +08:00
dc94978187 Fix Bug #557: ApplicationConfig 全局 Jackson LocalDateTime 反序列化器缺失 — 根因:JavaTimeModule 仅注册了 LocalDateTimeSerializer,未注册 LocalDateTimeDeserializer,导致默认反序列化器期望 ISO-8601 格式(T 分隔符),与前端 el-date-picker 空格分隔格式(YYYY-MM-DD HH:mm:ss)不匹配;修复:新增 LocalDateTimeDeserializer(pattern="yyyy-MM-dd HH:mm:ss")
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:42:20 +08:00
b925d6ba17 Fix Bug #557: 编辑手术安排时间字段保存报日期格式解析错误 — 根因:OpSchedule 实体中 admissionTime/entryTime/startTime/endTime/anesStart/anesEnd 六个时间字段的 @JsonFormat 使用 yyyy-MM-dd'T'HH:mm:ss(ISO T分隔符),而前端 el-date-picker 以 value-format="YYYY-MM-DD HH:mm:ss" 发送空格分隔格式,Jackson 反序列化失败;修复:统一改为 @JsonFormat + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:34:10 +08:00
72b9639ec0 Fix Bug #556: 根因+修复方案摘要 2026-05-19 19:12:56 +08:00
0e8fb32108 Fix Bug #556: 根因+修复方案摘要 2026-05-19 19:07:29 +08:00
955c72af41 Fix Bug #556: 根因+修复方案摘要 2026-05-19 19:05:33 +08:00
Ranyunqiao
be57c026ec 553 【住院护士站-医嘱校对】医嘱列表缺少“医嘱状态”显示列 2026-05-19 17:41:08 +08:00
3bf7e04a04 Fix Bug #469: 根因+修复方案摘要 2026-05-19 16:10:13 +08:00
7743bb5df4 Fix Bug #547: 执行科室配置保存时,冲突检测应跳过已被软删除科室的孤脏记录 — 根因:时间冲突校验未排除科室已删除的 OrganizationLocation 记录,导致已不存在的科室仍会阻断新配置保存
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:02:58 +08:00
f274ebaf5c Fix Bug #478: 住院医生工作站检验申请详情「发往科室」显示为- — 根因:getLocationInfo 未对科室ID做类型归一化,recursionFun 中 item.id == targetDepartment 在类型不一致时匹配失败;修复:新增 normalizeOrgTreeIds 统一转 String,recursionFun 改用 String(item.id) === String(targetDepartment)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:09:23 +08:00
9826df98e3 Fix Bug #552: 根因+修复方案摘要 2026-05-19 15:04:12 +08:00
Ranyunqiao
fbe434f01f Merge remote-tracking branch 'origin/develop' into develop 2026-05-19 14:23:21 +08:00
Ranyunqiao
c28b322e91 bug 443 444 445 478 494 521 2026-05-19 14:22:40 +08:00
7eeaafef59 bug550 2026-05-19 14:13:57 +08:00
05e7d54d87 Fix Bug #552: 双击待保存医嘱编辑保存后不应自动添加空医嘱 — 根因:handleSaveSign 中自动添加空行的条件缺少 isAdding.value 判断,导致双击编辑已有待保存医嘱也会触发 handleAddPrescription()
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:06:33 +08:00
c75b8038ec Fix Bug #547: 根因+修复方案摘要 2026-05-19 14:02:45 +08:00
af17d1f460 Fix Bug #469: 根因+修复方案摘要
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:04:48 +08:00
efc1c100aa Fix Bug #547: 根因+修复方案摘要 2026-05-19 13:01:36 +08:00
wangjian963
d9c975a950 Merge remote-tracking branch 'origin/develop' into develop 2026-05-19 12:16:46 +08:00
0874012dae Fix Bug #547: 根因+修复方案摘要 2026-05-19 12:12:38 +08:00
wangjian963
cbad13bddc Fix: 门诊预约挂号→签到→退号 slot/pool 状态流转对齐需求
- 枚举重排: SlotStatus LOCKED=4→2, CANCELLED=2→4,匹配需求编号
  - 预约: lockSlotForBooking 写入 LOCKED(2) 替代 BOOKED(1),pool locked_num+1 原子递增
  - 签到: LOCKED(2)→BOOKED(1) 替代 CHECKED_IN(3),加前置状态校验
  - 退号: 加 BOOKED(1) 前置校验
  - 池计数: refreshPoolStats booked_num=COUNT(1), locked_num=COUNT(2)
  - SQL 状态值全部由 SlotStatus 枚举传入,消除硬编码
  - 查询/显示: 加 locked 筛选分支,BOOKED→已取号, LOCKED→已锁定
  - 前端常量同步,签到列表查询 book→locked
2026-05-19 12:12:16 +08:00
a91ee66368 bug446,468,541,548 2026-05-19 11:59:55 +08:00
871e2de574 fix: same idCard substring fix for top-level copy 2026-05-19 11:40:20 +08:00
3d279548f0 chore: update his-repo submodule (patient save fix) 2026-05-19 11:39:13 +08:00
93 changed files with 5775 additions and 1753 deletions

View File

@@ -0,0 +1,27 @@
# Bug #556 Analysis
## Title
【门诊医生站-检验】新增检验申请单时就诊卡号/执行时间未自动回显,且项目列表冗余显示"套餐"文字
## Root Cause Analysis
### Issue 1: 就诊卡号未自动回显
- **Code**: `inspectionApplication.vue:886` - `formData.medicalrecordNumber = props.patientInfo.identifierNo || ''`
- **Root Cause**: Logic is correct but depends on `props.patientInfo.identifierNo` being populated. The watch on `props.patientInfo` (line 2074) triggers `initData()`. The card number field itself is correctly bound. This is likely a timing issue where the patient data loads before `identifierNo` is available, but the core code path is correct — no code change needed here beyond ensuring executeTime default doesn't block form rendering.
### Issue 2: 执行时间未默认填充当前系统时间
- **Code**: `inspectionApplication.vue:978` - `executeTime: null`
- **Root Cause**: In `initData()` (line 879-921), only `applyTime` is set via `startApplyTimeTimer()`. `formData.executeTime` is never assigned a default value. Similarly in `resetForm()` (line 1550), `executeTime` remains `null`.
- **Fix**: Add `formData.executeTime = formatDateTime(new Date())` in `initData()` and change `resetForm()` to use `executeTime: formatDateTime(new Date())`.
### Issue 3: 项目列表冗余显示"套餐"文字
- **Code**: `inspectionApplication.vue:1190` - Already fixed with `packageName` check. But `inspectionApplication.vue:2000` in `loadApplicationToForm()` still uses loose check: `item.feePackageId != null || item.itemName?.includes('套餐')`.
- **Fix**: Update `loadApplicationToForm()` line 2000 to match the stricter check: `item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName`.
## Files to Modify
- `openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue`
## Changes
1. `initData()`: Add `formData.executeTime = formatDateTime(new Date())` after line 899
2. `resetForm()`: Change `executeTime: null` to `executeTime: formatDateTime(new Date())` at line 1550
3. `loadApplicationToForm()`: Fix `isPackage` logic at line 2000

View File

@@ -0,0 +1,53 @@
# Bug #556 分析报告
## 问题描述
【门诊医生站-检验】新增检验申请单时:
1. 就诊卡号字段为空,未自动带出患者就诊卡号
2. 执行时间字段未自动填充,仅显示占位提示
3. 检验项目列表每条记录前均带"套餐"文字标签(冗余显示)
## 根因分析
### 问题1就诊卡号未自动回显
- 代码路径:`initData()``formData.medicalrecordNumber = props.patientInfo.identifierNo || ''`
- 数据绑定:`v-model="formData.medicalrecordNumber"`
- `props.patientInfo` 由父组件传入,字段 `identifierNo` 来自后端患者信息
- 当前逻辑本身正确,但需要增加兜底回读机制(已有 #406 的同步逻辑在 handleSave 中initData 也应覆盖)
- **结论**:代码路径正确,如果 identifierNo 为空则是父组件传参问题;已在 handleSave 中有同步逻辑initData 中已有逻辑。无需额外修复。
### 问题2执行时间未自动填充
- 根因:`formData.executeTime``formData` 初始化时line 978设为 `null`
- `initData()` 函数没有为 executeTime 设置默认值
- `resetForm()` 函数line 1550也将 executeTime 重置为 `null`
- 前端 datetime picker 在 `v-model``null` 时显示占位符 "选择执行时间"
- **修复方案**:在 `initData()` 中设置 `formData.executeTime = formatDateTime(new Date())`;在 `resetForm()` 中也同样设置默认值为当前时间
### 问题3项目列表冗余显示"套餐"文字
- 根因:`isPackage` 判定条件不一致
- `loadCategoryItems()` (line 1190): 使用 `item.feePackageId != null && ... && item.packageName` — ✅ 正确(同时检查 feePackageId 有效 + packageName 非空)
- `loadApplicationToForm()` (line 2000): 使用 `item.feePackageId != null || item.itemName?.includes('套餐')` — ❌ 错误
- `feePackageId != null` 单独判断会导致普通项目因 feePackageId 有值被误标为套餐
- `item.itemName?.includes('套餐')` 更是直接按名称文字判断,极不准确
- 影响位置:
- 检验项目选择区line 566`<el-tag v-if="item.isPackage">套餐</el-tag>`
- 已选项目列表line 617`<el-tag v-if="item.isPackage">套餐</el-tag>`
- 检验信息详情表格line 448`<el-tag v-if="scope.row.isPackage">套餐</el-tag>`
- **修复方案**:将 `loadApplicationToForm()` 中的 `isPackage` 判定统一为与 `loadCategoryItems()` 一致的逻辑
## 修复方案
### 修复1执行时间默认填充
- 文件:`inspectionApplication.vue`
- 位置:`initData()` 函数,在已有患者信息赋值后添加 `formData.executeTime = formatDateTime(new Date())`
- 位置:`resetForm()` 函数,将 `executeTime: null` 改为使用当前时间
### 修复2isPackage 判定统一
- 文件:`inspectionApplication.vue`
- 位置:`loadApplicationToForm()` 函数 line 2000
- 旧代码:`const isPackage = item.feePackageId != null || item.itemName?.includes('套餐')`
- 新代码:`const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName`
## 验收标准
1. 新增检验申请单时执行时间字段自动填充当前系统时间YYYY-MM-DD HH:mm:ss 格式)
2. 检验项目列表中,只有真正的套餐项目前显示"套餐"标签,普通项目不显示
3. 就诊卡号在有患者信息时正常显示

View File

@@ -20,3 +20,23 @@
2. **前端 API**: 新增撤回接口 `withdrawInspectionApplication(applyNo)`
3. **后端 Controller**: 新增 `POST /withdraw/{applyNo}` 端点
4. **后端 Service**: 新增 `withdrawInspectionLabApply` 方法,将 applyStatus 置回 0needRefund/needExecute 置回 false
## 修复结果
✅ 成功共14行改动2个commit完成
### 修复详情
1. **commit c643a78b** - 初始修复:将操作列从静态"打印/删除"改为基于状态的动态按钮(修改/删除/撤回/详情10行改动
2. **commit f369ea41** - 跟进修复:将"详情"按钮包裹在 `<template v-else>`避免对所有状态始终渲染4行改动
### 状态机实现
| 状态 | 条件 | 显示按钮 |
|------|------|---------|
| 待签发 | billStatus == '0' | 修改 + 删除 |
| 已签发 | billStatus == '1' | 撤回 |
| 其他状态 | 已采证/已送检/报告已出/已作废 | 详情 |
### 涉及文件
- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/testApplication.vue` - 前端操作列动态按钮
- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/applicationShow/api.js` - 前端APIdeleteRequestForm, withdrawRequestForm
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/regdoctorstation/controller/RequestFormManageController.java` - 后端Controller/delete, /withdraw 端点)
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/regdoctorstation/appservice/impl/RequestFormManageAppServiceImpl.java` - 后端Service实现

41
bug547_analysis.md Normal file
View File

@@ -0,0 +1,41 @@
# Bug #547 分析报告
## Bug 描述
在"系统管理-执行科室配置"页面,选择科室(如检验科)后添加新项目并保存,显示"与未知科室时间冲突"错误。
## 根因定位
**核心问题在 `OrganizationLocationAppServiceImpl.java:161-174`**
时间冲突检测的查询逻辑存在两个缺陷:
### 缺陷1查询范围过窄
```java
// 只查同一科室 + 同一诊疗的记录
getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId());
```
只查询**同一科室**的记录。如果同一诊疗项目在其他科室已有配置且时间重叠,不会被当前查询检测到。但系统本应阻止同一诊疗在多个科室同时段执行。
### 缺陷2"未知科室"错误提示
当冲突记录关联的科室被软删除(`delete_flag='1'`)时,`organizationService.getById()``@TableLogic` 注解影响查不到该科室,返回 null错误提示变成"与未知科室时间冲突"。
数据库验证发现确实存在软删除科室的组织位置记录内科门诊、上海学校医院、信息科等共9条
### 数据流
1. 前端选择科室 → 点击"添加新项目" → 填写诊疗和时间 → 点击"保存"
2. 后端 `addOrEditOrgLoc()` 接收请求
3. 查询现有冲突记录(**当前只查同科室**
4. 对冲突记录检查时间重叠
5. 查找冲突科室名称 → 若科室被软删除则返回 null → "未知科室"
## 修复方案
1. **修改冲突检测范围**:查询同一 `activityDefinitionId` 的所有记录(跨科室检测),而非仅限当前科室
2. **优雅处理"未知科室"**:当 `getById` 返回 null 时,使用 "已删除科室( ID )" 替代 "未知科室",提供更有用的信息
3. **新增 Service 方法**`getOrgLocListByActivityDefinitionId(Long activityDefinitionId)` 用于按诊疗定义查询所有记录
## 涉及文件
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/basedatamanage/appservice/impl/OrganizationLocationAppServiceImpl.java`
- `openhis-server-new/openhis-domain/src/main/java/com/openhis/administration/service/IOrganizationLocationService.java`
- `openhis-server-new/openhis-domain/src/main/java/com/openhis/administration/service/impl/OrganizationLocationServiceImpl.java`

Submodule his-repo updated: 414c204578...ea1271db8a

View File

@@ -1,6 +1,7 @@
package com.core.framework.config;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
@@ -34,7 +35,9 @@ public class ApplicationConfig {
// 设置日期格式为 yyyy/M/d HH:mm:ss支持多种格式反序列化
builder.simpleDateFormat("yyyy/M/d HH:mm:ss");
// 添加JavaTimeModule支持用于LocalDateTime
builder.modules(new JavaTimeModule());
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
builder.modules(javaTimeModule);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss")));
};
}

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.common.constant.CommonConstants;
import com.openhis.common.enums.SlotStatus;
import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
import com.openhis.appointmentmanage.domain.SchedulePool;
@@ -502,8 +502,8 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
// 该排班下存在有效患者预约(号源槽:已预约/已锁定/已取号)则禁止删除;已退号、仅可用/已取消槽位不计入
long appointmentCount = scheduleSlotService.count(new QueryWrapper<ScheduleSlot>()
.in("pool_id", poolIds)
.in("status", CommonConstants.SlotStatus.BOOKED, CommonConstants.SlotStatus.LOCKED,
CommonConstants.SlotStatus.CHECKED_IN));
.in("status", SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue(),
SlotStatus.CHECKED_IN.getValue()));
if (appointmentCount > 0) {
return R.fail("该排班已有患者预约,禁止删除!如需取消请先处理患者退预约或使用'停诊'功能。");
}

View File

@@ -9,7 +9,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service;
@@ -193,25 +193,24 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊");
} else {
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus());
if (status != null) {
if (status == SlotStatus.LOCKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("预约");
dto.setStatus("锁定");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
} else if (status == SlotStatus.BOOKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("锁定");
} else if (status == SlotStatus.RETURNED) {
dto.setStatus("退号");
} else {
dto.setStatus("未预约");
}
@@ -237,6 +236,10 @@ public class TicketAppServiceImpl implements ITicketAppService {
/**
* 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据
*/
/**
* 规范前端传入的状态查询参数,映射到 SQL 的 slotStatusNormExpr 值。
* 数值映射: 0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
*/
private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
String rawStatus = query.getStatus();
if (rawStatus == null) {
@@ -263,28 +266,31 @@ public class TicketAppServiceImpl implements ITicketAppService {
case "已预约":
query.setStatus("booked");
break;
case "locked":
case "2":
case "已锁定":
query.setStatus("locked");
break;
case "checked":
case "checkin":
case "checkedin":
case "2":
case "3":
case "已取号":
query.setStatus("checked");
break;
case "cancelled":
case "canceled":
case "3":
case "4":
case "已停诊":
case "已取消":
query.setStatus("cancelled");
break;
case "returned":
case "4":
case "5":
case "已退号":
query.setStatus("returned");
break;
default:
// 设置为 impossible 值,配合 mapper 的 otherwise 分支直接返回空
query.setStatus("__invalid__");
break;
}
@@ -367,26 +373,25 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊");
} else {
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...)
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已锁定...)
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus());
if (status != null) {
if (status == SlotStatus.LOCKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("预约");
dto.setStatus("锁定");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
} else if (status == SlotStatus.BOOKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("锁定");
} else if (status == SlotStatus.RETURNED) {
dto.setStatus("退号");
} else {
dto.setStatus("未预约");
}

View File

@@ -159,7 +159,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
String activityName = activityDef != null ? activityDef.getName() : "";
List<OrganizationLocation> organizationLocationList =
organizationLocationService.getOrgLocListByOrgIdAndActivityDefinitionId(orgLoc.getOrganizationId(), orgLoc.getActivityDefinitionId());
organizationLocationService.getOrgLocListByActivityDefinitionId(orgLoc.getActivityDefinitionId());
organizationLocationList = (orgLoc.getId() != null)
? organizationLocationList.stream().filter(item -> !orgLoc.getId().equals(item.getId())).toList()
: organizationLocationList;
@@ -169,7 +169,7 @@ public class OrganizationLocationAppServiceImpl implements IOrganizationLocation
if (DateTimeUtils.isOverlap(organizationLocation.getStartTime(), organizationLocation.getEndTime(),
orgLoc.getStartTime(), orgLoc.getEndTime())) {
Organization org = organizationService.getById(organizationLocation.getOrganizationId());
String organizationName = org != null ? org.getName() : "未知科室";
String organizationName = org != null ? org.getName() : ("科室[" + organizationLocation.getOrganizationId() + "]已删除");
return R.fail("当前诊疗:" + activityName + CommonConstants.Common.DASH + orgLoc.getStartTime()
+ CommonConstants.Common.DASH + orgLoc.getEndTime() + "" + organizationName + "时间冲突");
}

View File

@@ -31,4 +31,9 @@ public class OrgLocQueryParam implements Serializable {
/** 发放类别 */
private String distributionCategoryCode;
/**
* 项目编码 | 药品:1 耗材:2
*/
private String itemCode;
}

View File

@@ -18,6 +18,7 @@ import com.openhis.administration.mapper.PatientMapper;
import com.openhis.administration.service.*;
import com.openhis.common.constant.CommonConstants;
import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.*;
import com.openhis.common.enums.ybenums.YbPayment;
import com.openhis.common.utils.EnumUtils;
@@ -643,8 +644,7 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
.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::getCancelReason, "诊前退号")
.set(Order::getUpdateTime, new Date())
.setSql("version = version + 1")
.eq(Order::getId, appointmentOrder.getId())
@@ -660,17 +660,27 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.AVAILABLE);
if (slotRows > 0) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
// 只有已预约(1)的号源才能退号,对应签到后的 BOOKED 状态
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.BOOKED.getValue().equals(slot.getStatus())) {
log.warn("退号跳过:槽位非已预约状态, slotId={}, status={}", slotId,
slot != null ? slot.getStatus() : null);
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue());
if (slotRows == 0) {
log.warn("退号时更新槽位状态未影响任何行, slotId={}", slotId);
return appointmentOrder.getId();
}
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("booked_num = booked_num - 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return appointmentOrder.getId();
} catch (Exception e) {

View File

@@ -215,7 +215,10 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
if (surgery != null) {
surgery.setStatusEnum(1); // 1 = 已排期
surgery.setUpdateTime(new Date());
// Bug #558: 手术安排时同步写入手术室确认时间和确认人
surgery.setOperatingRoomConfirmTime(new Date());
surgery.setOperatingRoomConfirmUser(loginUser.getUsername());
// 填充缺失的申请科室和主刀医生名称
fillSurgeryMissingNames(surgery);

View File

@@ -147,6 +147,6 @@ public interface IDoctorStationAdviceAppService {
*/
IPage<SurgeryItemDto> getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey);
IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey);
IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey, String categoryCode);
}

View File

@@ -63,17 +63,21 @@ public interface IDoctorStationEmrAppService {
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
* @param pageNo 当前页码
* @param pageSize 每页条数
* @param patientName 患者姓名(可选)
* @return 待写病历分页数据
*/
R<?> getPendingEmrList(Long doctorId);
R<?> getPendingEmrList(Long doctorId, Integer pageNo, Integer pageSize, String patientName);
/**
* 获取待写病历数量
*
* @param doctorId 医生ID
* @param patientName 患者姓名(可选)
* @return 待写病历数量
*/
R<?> getPendingEmrCount(Long doctorId);
R<?> getPendingEmrCount(Long doctorId, String patientName);
/**
* 检查患者是否需要写病历

View File

@@ -2192,11 +2192,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时过滤掉药品1保留耗材2和诊疗3/6
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& dto.getAdviceType() == 1);
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态
requestBaseDto
@@ -2210,6 +2205,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 收费状态
requestBaseDto.setChargeStatus_enumText(
EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus()));
// 单位字典翻译失败时回退使用原始值(如手术申请硬编码了中文单位名)
if (StringUtils.isNotBlank(requestBaseDto.getUnitCode()) && StringUtils.isBlank(requestBaseDto.getUnitCode_dictText())) {
requestBaseDto.setUnitCode_dictText(requestBaseDto.getUnitCode());
}
}
return R.ok(requestBaseInfo);
}
@@ -2571,12 +2570,13 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
@Override
public IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey) {
public IPage<SurgeryItemDto> getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey, String categoryCode) {
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getExaminationPage(
new Page<>(pageNo, pageSize),
PublicationStatus.ACTIVE.getValue(),
organizationId,
searchKey);
searchKey,
categoryCode);
return result;
}

View File

@@ -29,6 +29,7 @@ import com.openhis.document.service.IEmrTemplateService;
import com.openhis.web.doctorstation.appservice.IDoctorStationEmrAppService;
import com.openhis.web.doctorstation.dto.EmrTemplateDto;
import com.openhis.web.doctorstation.dto.PatientEmrDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@@ -41,6 +42,7 @@ import java.util.stream.Collectors;
/**
* 医生站-电子病历 应用实现类
*/
@Slf4j
@Service
public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppService {
@@ -60,13 +62,7 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
IDocRecordService docRecordService;
@Resource
private EncounterMapper encounterMapper;
@Resource
private PatientMapper patientMapper;
@Resource
private com.openhis.administration.mapper.EncounterParticipantMapper encounterParticipantMapper;
private com.openhis.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper;
/**
* 添加病人病历信息
@@ -223,52 +219,35 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
* @return 待写病历列表
*/
@Override
public R<?> getPendingEmrList(Long doctorId) {
// 由于Encounter实体中没有jzPractitionerUserId字段我们需要通过关联查询来获取相关信息
// 使用医生工作站的mapper来查询相关数据
// 这里我们直接使用医生工作站的查询逻辑
public R<?> getPendingEmrList(Long doctorId, Integer pageNo, Integer pageSize, String patientName) {
List<Map<String, Object>> allRows = doctorStationEmrAppMapper.getPendingEmrList(doctorId, patientName);
int total = allRows.size();
// 查询当前医生负责的、状态为"就诊中"但还没有写病历的患者
// 需要通过EncounterParticipant表来关联医生信息
List<Encounter> encounters = encounterMapper.selectList(
new LambdaQueryWrapper<Encounter>()
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
);
// 过滤出由指定医生负责且还没有写病历的就诊记录
List<Map<String, Object>> pendingEmrs = new ArrayList<>();
for (Encounter encounter : encounters) {
// 检查该就诊记录是否已经有病历
Emr existingEmr = emrService.getOne(
new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, encounter.getId())
);
// 检查该就诊是否由指定医生负责
boolean isAssignedToDoctor = isEncounterAssignedToDoctor(encounter.getId(), doctorId);
if (existingEmr == null && isAssignedToDoctor) {
// 如果没有病历且由该医生负责,则添加到待写病历列表
Map<String, Object> pendingEmr = new java.util.HashMap<>();
// 获取患者信息
Patient patient = patientMapper.selectById(encounter.getPatientId());
pendingEmr.put("encounterId", encounter.getId());
pendingEmr.put("patientId", encounter.getPatientId());
pendingEmr.put("patientName", patient != null ? patient.getName() : "未知");
pendingEmr.put("gender", patient != null ? patient.getGenderEnum() : null);
// 使用出生日期计算年龄
pendingEmr.put("age", patient != null && patient.getBirthDate() != null ?
calculateAge(patient.getBirthDate()) : null);
// 使用创建时间作为挂号时间
pendingEmr.put("registerTime", encounter.getCreateTime());
pendingEmr.put("busNo", encounter.getBusNo()); // 病历号
pendingEmrs.add(pendingEmr);
}
// 分页截取
int fromIndex = (pageNo - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, total);
List<Map<String, Object>> pageRows;
if (fromIndex >= total) {
pageRows = new ArrayList<>();
} else {
pageRows = allRows.subList(fromIndex, toIndex);
}
return R.ok(pendingEmrs);
// 计算年龄列
for (Map<String, Object> row : pageRows) {
Object birthDate = row.get("birthDate");
if (birthDate instanceof Date) {
row.put("age", calculateAge((Date) birthDate));
} else {
row.put("age", null);
}
row.remove("birthDate");
}
Map<String, Object> result = new java.util.HashMap<>();
result.put("rows", pageRows);
result.put("total", total);
return R.ok(result);
}
/**
@@ -278,14 +257,9 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
* @return 待写病历数量
*/
@Override
public R<?> getPendingEmrCount(Long doctorId) {
// 获取待写病历列表,然后返回数量
R<?> result = getPendingEmrList(doctorId);
if (result.getCode() == 200) {
List<?> pendingEmrs = (List<?>) result.getData();
return R.ok(pendingEmrs.size());
}
return R.ok(0);
public R<?> getPendingEmrCount(Long doctorId, String patientName) {
Long count = doctorStationEmrAppMapper.getPendingEmrCount(doctorId, patientName);
return R.ok(count != null ? count.intValue() : 0);
}
/**
@@ -306,24 +280,6 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
return R.ok(needWrite);
}
/**
* 检查就诊是否分配给指定医生
*
* @param encounterId 就诊ID
* @param doctorId 医生ID
* @return 是否分配给指定医生
*/
private boolean isEncounterAssignedToDoctor(Long encounterId, Long doctorId) {
// 查询就诊参与者表,检查是否有指定医生的接诊记录
com.openhis.administration.domain.EncounterParticipant participant =
encounterParticipantMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.openhis.administration.domain.EncounterParticipant>()
.eq(com.openhis.administration.domain.EncounterParticipant::getEncounterId, encounterId)
.eq(com.openhis.administration.domain.EncounterParticipant::getPractitionerId, doctorId)
);
return participant != null;
}
/**
* 根据出生日期计算年龄

View File

@@ -226,8 +226,9 @@ public class DoctorStationAdviceController {
@RequestParam(value = "organizationId", required = false) Long organizationId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "500") Integer pageSize,
@RequestParam(value = "searchKey", defaultValue = "") String searchKey) {
return R.ok(iDoctorStationAdviceAppService.getExaminationPage(organizationId, pageNo, pageSize, searchKey));
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
@RequestParam(value = "categoryCode", defaultValue = "23") String categoryCode) {
return R.ok(iDoctorStationAdviceAppService.getExaminationPage(organizationId, pageNo, pageSize, searchKey, categoryCode));
}
}

View File

@@ -26,34 +26,36 @@ public class PendingEmrController {
* 获取待写病历列表
*
* @param doctorId 医生ID
* @return 待写病历列表
* @param pageNo 当前页码
* @param pageSize 每页条数
* @param patientName 患者姓名(可选)
* @return 待写病历分页数据
*/
@GetMapping("/pending-list")
public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId) {
// 如果没有传递医生ID则使用当前登录用户ID
public R<?> getPendingEmrList(@RequestParam(required = false) Long doctorId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String patientName) {
if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getPractitionerId();
}
// 调用服务获取待写病历列表
return iDoctorStationEmrAppService.getPendingEmrList(doctorId);
return iDoctorStationEmrAppService.getPendingEmrList(doctorId, pageNum, pageSize, patientName);
}
/**
* 获取待写病历数量
*
*
* @param doctorId 医生ID
* @param patientName 患者姓名(可选)
* @return 待写病历数量
*/
@GetMapping("/pending-count")
public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId) {
// 如果没有传递医生ID则使用当前登录用户ID
public R<?> getPendingEmrCount(@RequestParam(required = false) Long doctorId,
@RequestParam(required = false) String patientName) {
if (doctorId == null) {
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getUserId();
doctorId = com.core.common.utils.SecurityUtils.getLoginUser().getPractitionerId();
}
// 调用服务获取待写病历数量
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId);
return iDoctorStationEmrAppService.getPendingEmrCount(doctorId, patientName);
}
/**

View File

@@ -198,8 +198,10 @@ public class AdviceBaseDto {
/**
* 所属科室
*/
@Dict(dictTable = "adm_organization", dictCode = "id", dictText = "name")
@JsonSerialize(using = ToStringSerializer.class)
private Long orgId;
private String orgId_dictText;
/**
* 所在位置

View File

@@ -23,6 +23,9 @@ public class SurgeryItemDto {
@JsonSerialize(using = ToStringSerializer.class)
private Long orgId;
/** 所属科室名称 */
private String orgName;
/** 执行科室ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long positionId;

View File

@@ -203,6 +203,7 @@ public interface DoctorStationAdviceAppMapper {
IPage<SurgeryItemDto> getExaminationPage(@Param("page") Page<SurgeryItemDto> page,
@Param("statusEnum") Integer statusEnum,
@Param("organizationId") Long organizationId,
@Param("searchKey") String searchKey);
@Param("searchKey") String searchKey,
@Param("categoryCode") String categoryCode);
}

View File

@@ -1,11 +1,20 @@
package com.openhis.web.doctorstation.mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
/**
* 医生站-电子病历 应用Mapper
*/
@Repository
public interface DoctorStationEmrAppMapper {
List<Map<String, Object>> getPendingEmrList(@Param("doctorId") Long doctorId,
@Param("patientName") String patientName);
Long getPendingEmrCount(@Param("doctorId") Long doctorId,
@Param("patientName") String patientName);
}

View File

@@ -359,6 +359,24 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
medRequestList.add(item);
}
}
// 校验医嘱是否已执行,已执行的医嘱需要先取消执行后才能退回
List<Long> allRequestIds = performInfoList.stream().map(PerformInfoDto::getRequestId).toList();
List<Procedure> allProcedures = procedureService.list(
new LambdaQueryWrapper<Procedure>()
.in(Procedure::getRequestId, allRequestIds)
.eq(Procedure::getDeleteFlag, "0"));
Set<Long> executedIds = allProcedures.stream()
.filter(p -> EventStatus.COMPLETED.getValue().equals(p.getStatusEnum()))
.map(Procedure::getId)
.collect(Collectors.toSet());
Set<Long> cancelledRefundIds = allProcedures.stream()
.filter(p -> EventStatus.CANCEL.getValue().equals(p.getStatusEnum()) && p.getRefundId() != null)
.map(Procedure::getRefundId)
.collect(Collectors.toSet());
executedIds.removeAll(cancelledRefundIds);
if (!executedIds.isEmpty()) {
return R.fail("该医嘱已执行,请先取消执行后再退回");
}
// 校验药品医嘱是否已发药,已发药的医嘱不允许退回
if (!medRequestList.isEmpty()) {
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();

View File

@@ -78,12 +78,10 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
.map(notPerformedReason -> new DispenseInitDto.NotPerformedReasonOption(notPerformedReason.getValue(),
notPerformedReason.getInfo()))
.collect(Collectors.toList());
// 发药状态
// 发药状态(汇总单:待配药→已提交,已发放→已发药)
List<DispenseStatusOption> dispenseStatusOptions = new ArrayList<>();
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(),
DispenseStatus.PREPARATION.getInfo()));
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(),
DispenseStatus.COMPLETED.getInfo()));
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(), "已提交"));
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(), "已发药"));
initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions);
return R.ok(initDto);
@@ -161,8 +159,8 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
new Page<>(pageNo, pageSize), queryWrapper, DispenseStatus.COMPLETED.getValue(),
DispenseStatus.PREPARATION.getValue(), SupplyType.SUMMARY_DISPENSE.getValue());
medicineSummaryFormPage.getRecords().forEach(e -> {
// 发药状态
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(DispenseStatus.class, e.getStatusEnum()));
// 发药状态(汇总单展示文案)
e.setStatusEnum_enumText(getSummaryFormStatusText(e.getStatusEnum()));
});
return R.ok(medicineSummaryFormPage);
}
@@ -292,4 +290,17 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
}
return R.ok(MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[]{"取消"}));
}
/**
* 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→已提交,发药→已发药)
*/
private String getSummaryFormStatusText(Integer statusEnum) {
if (DispenseStatus.PREPARATION.getValue().equals(statusEnum)) {
return "已提交";
}
if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) {
return "已发药";
}
return EnumUtils.getInfoByValue(DispenseStatus.class, statusEnum);
}
}

View File

@@ -133,47 +133,13 @@ public class PatientInformationServiceImpl implements IPatientInformationService
@Override
public IPage<PatientBaseInfoDto> getPatientInfo(PatientBaseInfoDto patientBaseInfoDto, String searchKey,
Integer pageNo, Integer pageSize, HttpServletRequest request) {
// 获取登录者信息
// 构建基础查询条件
LoginUser loginUser = SecurityUtils.getLoginUser();
Long userId = loginUser.getUserId();
Integer tenantId = loginUser.getTenantId().intValue();
// 先构建基础查询条件
QueryWrapper<PatientBaseInfoDto> queryWrapper = HisQueryUtils.buildQueryWrapper(
patientBaseInfoDto, searchKey, new HashSet<>(Arrays.asList(CommonConstants.FieldName.Name,
CommonConstants.FieldName.BusNo, CommonConstants.FieldName.PyStr, CommonConstants.FieldName.WbStr)),
request);
// 检查是否是精确ID查询从门诊挂号页面跳转时使用
boolean hasExactIdQuery = (patientBaseInfoDto.getId() != null);
// 只有非精确ID查询时才添加医生患者过滤条件
if (!hasExactIdQuery) {
// 查询当前用户对应的医生信息
LambdaQueryWrapper<com.openhis.administration.domain.Practitioner> practitionerQuery = new LambdaQueryWrapper<>();
practitionerQuery.eq(com.openhis.administration.domain.Practitioner::getUserId, userId);
// 使用list()避免TooManyResultsException异常然后取第一个记录
List<com.openhis.administration.domain.Practitioner> practitionerList = practitionerService.list(practitionerQuery);
com.openhis.administration.domain.Practitioner practitioner = practitionerList != null && !practitionerList.isEmpty() ? practitionerList.get(0) : null;
// 如果当前用户是医生,添加医生患者过滤条件
if (practitioner != null) {
// 查询该医生作为接诊医生ADMITTER, code="1"和挂号医生REGISTRATION_DOCTOR, code="12"的所有就诊记录的患者ID
List<Long> doctorPatientIds = patientManageMapper.getPatientIdsByPractitionerId(
practitioner.getId(),
Arrays.asList(ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode()),
tenantId);
if (doctorPatientIds != null && !doctorPatientIds.isEmpty()) {
// 添加患者ID过滤条件 - 注意:这里使用列名而不是表别名
queryWrapper.in("id", doctorPatientIds);
} else {
// 如果没有相关患者,返回空结果
queryWrapper.eq("id", -1); // 设置一个不存在的ID
}
}
// 如果不是医生,查询所有患者
}
IPage<PatientBaseInfoDto> patientInformationPage
= patientManageMapper.getPatientPage(new Page<>(pageNo, pageSize), queryWrapper);
@@ -269,7 +235,7 @@ public class PatientInformationServiceImpl implements IPatientInformationService
// log.debug("添加病人信息,patientInfoDto:{}", patientBaseInfoDto);
// 如果患者没有输入身份证号则根据年龄自动生成
String idCard = patientBaseInfoDto.getIdCard();
if (idCard == null || CommonConstants.Common.AREA_CODE.equals(idCard.substring(0, 6))) {
if (idCard == null || idCard.length() < 6 || CommonConstants.Common.AREA_CODE.equals(idCard.substring(0, 6))) {
if (patientBaseInfoDto.getAge() != null) {
idCard = IdCardUtil.generateIdByAge(patientBaseInfoDto.getAge());
patientBaseInfoDto.setIdCard(idCard);

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.enums.DelFlag;
import com.core.common.exception.ServiceException;
import com.core.common.utils.AssignSeqUtil;
import com.core.common.utils.MessageUtils;
@@ -17,6 +18,8 @@ import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.*;
import com.openhis.document.domain.RequestForm;
import com.openhis.document.service.IRequestFormService;
import com.openhis.lab.domain.Specimen;
import com.openhis.lab.service.ISpecimenService;
import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams;
import com.openhis.web.doctorstation.utils.AdviceUtils;
import com.openhis.web.regdoctorstation.appservice.IRequestFormManageAppService;
@@ -67,6 +70,39 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
@Resource
IActivityDefinitionService iActivityDefinitionService;
@Resource
ISpecimenService iSpecimenService;
/**
* 校验当前用户是否有权操作该申请单(申请者本人或管理员)
*/
private R<?> validateRequestFormPermission(RequestForm requestForm) {
if (SecurityUtils.isAdmin(SecurityUtils.getUserId())) {
return null;
}
Long currentPractitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Long requesterId = requestForm.getRequesterId();
if (currentPractitionerId == null || requesterId == null
|| !currentPractitionerId.equals(requesterId)) {
return R.fail("无操作权限,仅申请开立者或管理员可操作");
}
return null;
}
/**
* 校验关联医嘱是否已采证(存在已采集/已接收标本则不可撤回)
*/
private boolean hasCollectedSpecimen(List<Long> serviceRequestIds) {
if (serviceRequestIds == null || serviceRequestIds.isEmpty()) {
return false;
}
long count = iSpecimenService.count(
new LambdaQueryWrapper<Specimen>()
.in(Specimen::getServiceId, serviceRequestIds)
.ge(Specimen::getCollectionStatusEnum, SpecCollectStatus.COLLECTED.getValue()));
return count > 0;
}
/**
* 保存申请单
*
@@ -528,12 +564,17 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
if (requestForm == null) {
return R.fail("申请单不存在");
}
R<?> permissionResult = validateRequestFormPermission(requestForm);
if (permissionResult != null) {
return permissionResult;
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest含子项
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
@@ -563,7 +604,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
// 4. 删除申请单
iRequestFormService.removeById(requestFormId);
log.info("申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
log.info("申请单删除成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("删除成功");
}
@@ -576,32 +617,47 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
if (requestForm == null) {
return R.fail("申请单不存在");
}
R<?> permissionResult = validateRequestFormPermission(requestForm);
if (permissionResult != null) {
return permissionResult;
}
String prescriptionNo = requestForm.getPrescriptionNo();
// 查询该申请单下所有 ServiceRequest
List<ServiceRequest> serviceRequests = iServiceRequestService.list(
new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo));
.eq(ServiceRequest::getPrescriptionNo, prescriptionNo)
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
if (serviceRequests == null || serviceRequests.isEmpty()) {
return R.fail("未找到关联的诊疗医嘱");
}
// 校验:只有已签发(status=2)的申请单可撤回
boolean allActive = serviceRequests.stream()
.allMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!allActive) {
return R.fail("只有已签发状态的申请单可撤回");
}
// 将所有 ServiceRequest 状态改回待签发(DRAFT=0)
List<Long> serviceRequestIds = serviceRequests.stream()
.map(ServiceRequest::getId).collect(Collectors.toList());
iServiceRequestService.update(
// 校验:标本已采集则不可撤回
if (hasCollectedSpecimen(serviceRequestIds)) {
return R.fail("标本已采集,无法撤回");
}
// 校验任一ServiceRequest为ACTIVE(status=2)即可撤回与SQL的EXISTS逻辑一致
boolean hasActive = serviceRequests.stream()
.anyMatch(sr -> RequestStatus.ACTIVE.getValue().equals(sr.getStatusEnum()));
if (!hasActive) {
return R.fail("只有已签发且未采证的申请单可撤回");
}
// 将所有已签发的 ServiceRequest 状态改回待签发,与申请单展示状态同步
boolean updated = iServiceRequestService.update(
new ServiceRequest().setStatusEnum(RequestStatus.DRAFT.getValue()),
new LambdaUpdateWrapper<ServiceRequest>()
.in(ServiceRequest::getId, serviceRequestIds));
.in(ServiceRequest::getId, serviceRequestIds)
.eq(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue()));
if (!updated) {
return R.fail("撤回失败,医嘱状态已变更,请刷新后重试");
}
log.info("申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
log.info("申请单撤回成功requestFormId={}, prescriptionNo={}", requestFormId, prescriptionNo);
return R.ok("撤回成功");
}

View File

@@ -194,8 +194,8 @@ public class RequestFormManageController {
* @return 结果
*/
@PostMapping(value = "/delete")
public R<?> deleteRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.deleteRequestForm(data.get("requestFormId"));
public R<?> deleteRequestForm(@RequestBody Map<String, Object> data) {
return iRequestFormManageAppService.deleteRequestForm(parseLong(data.get("requestFormId")));
}
/**
@@ -205,7 +205,24 @@ public class RequestFormManageController {
* @return 结果
*/
@PostMapping(value = "/withdraw")
public R<?> withdrawRequestForm(@RequestBody Map<String, Long> data) {
return iRequestFormManageAppService.withdrawRequestForm(data.get("requestFormId"));
public R<?> withdrawRequestForm(@RequestBody Map<String, Object> data) {
return iRequestFormManageAppService.withdrawRequestForm(parseLong(data.get("requestFormId")));
}
private Long parseLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -31,8 +31,8 @@ public class HomeController {
HomeStatisticsDto statisticsDto = homeStatisticsService.getHomeStatistics();
// 获取待写病历数量
Long userId = SecurityUtils.getLoginUser().getUserId();
R<?> pendingEmrCount = doctorStationEmrAppService.getPendingEmrCount(userId);
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
R<?> pendingEmrCount = doctorStationEmrAppService.getPendingEmrCount(practitionerId, null);
// 将待写病历数量添加到统计数据中
statisticsDto.setPendingEmr((Integer) pendingEmrCount.getData());

View File

@@ -74,7 +74,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getTenantId, tenantId)
.eq(TriageQueueItem::getQueueDate, qd)
.eq(TriageQueueItem::getDeleteFlag, "0")
.ne(TriageQueueItem::getStatus, TriageQueueStatus.COMPLETED.getValue())
.orderByAsc(TriageQueueItem::getQueueOrder);
// 如果指定了科室,按科室过滤;否则查询所有科室(全科模式)
@@ -92,14 +91,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
});
}
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
if (list != null && !list.isEmpty()) {
int beforeSize = list.size();
list = list.stream()
.filter(item -> !TriageQueueStatus.COMPLETED.getValue().equals(item.getStatus()))
.collect(java.util.stream.Collectors.toList());
}
return R.ok(list);
}

View File

@@ -871,6 +871,7 @@
</select>
<!-- 手术项目专用分页查询:仅查手术 + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
<!-- 使用 LIMIT/OFFSET 直接查询,避免 MyBatis Plus 分页插件的 COUNT 开销 -->
<select id="getSurgeryPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
SELECT DISTINCT ON (t1.ID)
t1.ID AS advice_definition_id,
@@ -895,12 +896,13 @@
ORDER BY t1.ID, t1.name ASC, t2.ID ASC
</select>
<!-- 检查项目专用分页查询:仅查检查(23) + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
<!-- 检查/检验项目专用分页查询:仅查指定 category_code + 定价,无库存/草稿库存/取药科室等无关逻辑 -->
<select id="getExaminationPage" resultType="com.openhis.web.doctorstation.dto.SurgeryItemDto">
SELECT DISTINCT ON (t1.ID)
t1.ID AS advice_definition_id,
t1.NAME AS advice_name,
t1.org_id AS org_id,
t3.name AS org_name,
t1.org_id AS position_id,
t2.ID AS charge_item_definition_id,
t2.price AS price,
@@ -912,8 +914,11 @@
AND t2.delete_flag = '0'
AND t2.status_enum = #{statusEnum}
AND t2.instance_table = 'wor_activity_definition'
LEFT JOIN adm_organization t3
ON t3.id = t1.org_id
AND t3.delete_flag = '0'
WHERE t1.delete_flag = '0'
AND t1.category_code = '23'
AND t1.category_code = #{categoryCode}
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if>

View File

@@ -4,4 +4,38 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationEmrAppMapper">
</mapper>
<select id="getPendingEmrList" resultType="java.util.HashMap">
SELECT e.id AS "encounterId",
e.patient_id AS "patientId",
p.name AS "patientName",
p.gender_enum AS "gender",
p.birth_date AS "birthDate",
e.create_time AS "registerTime",
e.bus_no AS "busNo"
FROM adm_encounter e
INNER JOIN adm_encounter_participant ep ON e.id = ep.encounter_id AND ep.practitioner_id = #{doctorId}
LEFT JOIN adm_patient p ON e.patient_id = p.id
LEFT JOIN doc_emr emr ON e.id = emr.encounter_id
WHERE e.status_enum = 2
AND emr.id IS NULL
<if test="patientName != null and patientName != ''">
AND p.name LIKE CONCAT('%', #{patientName}, '%')
</if>
ORDER BY e.create_time DESC
</select>
<select id="getPendingEmrCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM adm_encounter e
INNER JOIN adm_encounter_participant ep ON e.id = ep.encounter_id AND ep.practitioner_id = #{doctorId}
LEFT JOIN doc_emr emr ON e.id = emr.encounter_id
WHERE e.status_enum = 2
AND emr.id IS NULL
<if test="patientName != null and patientName != ''">
AND e.patient_id IN (
SELECT id FROM adm_patient WHERE name LIKE CONCAT('%', #{patientName}, '%')
)
</if>
</select>
</mapper>

View File

@@ -35,21 +35,27 @@
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 8
) THEN 6
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 5
) THEN 7
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 3
) THEN 5
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
INNER JOIN lab_specimen ls ON ls.service_id = ws.id
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ls.collection_status_enum >= 1
) THEN 4
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 2
) THEN 1
WHEN EXISTS (
SELECT 1 FROM wor_service_request ws
WHERE ws.prescription_no = drf.prescription_no AND ws.delete_flag = '0'
AND ws.status_enum = 5
) THEN 7
ELSE 0
END AS computed_status
FROM doc_request_form AS drf
@@ -57,8 +63,6 @@
AND ae.delete_flag = '0'
LEFT JOIN adm_patient AS ap ON ap.ID = ae.patient_id
AND ap.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.prescription_no = drf.prescription_no
AND wsr.delete_flag = '0'
WHERE drf.delete_flag = '0'
AND drf.encounter_id = #{encounterId}
AND drf.type_code = #{typeCode}

View File

@@ -768,36 +768,4 @@ public class CommonConstants {
Integer ACCOUNT_DEVICE_TYPE = 6;
}
/**
* 号源槽位状态 (adm_schedule_slot.status)
*/
public interface SlotStatus {
/** 可用 / 待预约 */
Integer AVAILABLE = 0;
/** 已预约 */
Integer BOOKED = 1;
/** 已取消 / 已停诊 */
Integer CANCELLED = 2;
/** 已签到 / 已取号 */
Integer CHECKED_IN = 3;
/** 已锁定 */
Integer LOCKED = 4;
/** 已退号 */
Integer RETURNED = 5;
}
/**
* 预约订单状态 (order_main.status)
*/
public interface AppointmentOrderStatus {
/** 已预约 (待就诊) */
Integer BOOKED = 1;
/** 已取号 (已就诊) */
Integer CHECKED_IN = 2;
/** 已取消 */
Integer CANCELLED = 3;
/** 已退号 */
Integer RETURNED = 4;
}
}

View File

@@ -0,0 +1,57 @@
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 号源槽位状态 (adm_schedule_slot.status)
*
* <pre>
* 状态流转:
* 预约 → 0→2 (锁定), locked_num+1
* 取消预约 → 2→0 (释放), locked_num-1
* 签到 → 2→1 (已约), locked_num-1, booked_num+1
* 退号 → 1→0 (释放), booked_num-1
* 停诊 → 任意→4 (已取消)
* </pre>
*
* @author system
*/
@Getter
@AllArgsConstructor
public enum SlotStatus implements HisEnumInterface {
/** 可用 / 待预约 */
AVAILABLE(0, "available", "可用"),
/** 已预约 */
BOOKED(1, "booked", "已预约"),
/** 已锁定 (约而不付:预约后锁定号源) */
LOCKED(2, "locked", "已锁定"),
/** 已签到 / 已取号 */
CHECKED_IN(3, "checked_in", "已签到"),
/** 已取消 / 已停诊 */
CANCELLED(4, "cancelled", "已取消"),
/** 已退号 */
RETURNED(5, "returned", "已退号");
private final Integer value;
private final String code;
private final String info;
public static SlotStatus getByValue(Integer value) {
if (value == null) {
return null;
}
for (SlotStatus val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -38,4 +38,12 @@ public interface IOrganizationLocationService extends IService<OrganizationLocat
*/
List<OrganizationLocation> getOrgLocListByOrgIdAndActivityDefinitionId(Long organizationId, Long activityDefinitionId);
/**
* 根据诊疗定义id查询所有执行科室列表跨科室
*
* @param activityDefinitionId 诊疗定义id
* @return 执行科室列表
*/
List<OrganizationLocation> getOrgLocListByActivityDefinitionId(Long activityDefinitionId);
}

View File

@@ -64,4 +64,16 @@ public class OrganizationLocationServiceImpl extends ServiceImpl<OrganizationLoc
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
}
/**
* 根据诊疗定义id查询所有执行科室列表跨科室
*
* @param activityDefinitionId 诊疗定义id
* @return 执行科室列表
*/
@Override
public List<OrganizationLocation> getOrgLocListByActivityDefinitionId(Long activityDefinitionId) {
return baseMapper.selectList(new LambdaQueryWrapper<OrganizationLocation>()
.eq(OrganizationLocation::getActivityDefinitionId, activityDefinitionId));
}
}

View File

@@ -10,10 +10,11 @@ import org.springframework.stereotype.Repository;
public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
/**
* 按号源池实时重算统计值,避免并发场景下计数漂移
* 按号源池实时重算统计值。
*
* 说明available_num 在当前项目中可能为数据库生成列,因此这里仅维护
* booked_num / locked_num剩余号由数据库或查询逻辑计算。
* @param poolId 号源池ID
* @param bookedStatus 已约状态值,由 SlotStatus.BOOKED.getValue() 传入
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/
@Update("""
UPDATE adm_schedule_pool p
@@ -23,20 +24,22 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND s.status = 1
AND s.status = #{bookedStatus}
), 0),
locked_num = COALESCE((
SELECT COUNT(1)
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND s.status = 3
AND s.status = #{lockedStatus}
), 0),
update_time = now()
WHERE p.id = #{poolId}
AND p.delete_flag = '0'
""")
int refreshPoolStats(@Param("poolId") Long poolId);
int refreshPoolStats(@Param("poolId") Long poolId,
@Param("bookedStatus") Integer bookedStatus,
@Param("lockedStatus") Integer lockedStatus);
/**
* 签到时更新号源池统计:锁定数-1已预约数+1

View File

@@ -22,9 +22,12 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
TicketSlotDTO selectTicketSlotById(@Param("id") Long id);
/**
* 原子抢占槽位:仅当当前状态=0(可用)时,更新为1(已预约)
* 原子抢占槽位:仅当当前状态=0(待约)时,更新为目标锁定状态
*
* @param slotId 槽位ID
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/
int lockSlotForBooking(@Param("slotId") Long slotId);
int lockSlotForBooking(@Param("slotId") Long slotId, @Param("lockedStatus") Integer lockedStatus);
/**
* 按主键更新槽位状态。
@@ -34,12 +37,16 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
/**
* 更新槽位状态并记录签到时间
*
* @param slotId 槽位ID
* @param status 状态
* @param checkInTime 签到时间
* @param slotId 槽位ID
* @param status 目标状态,由 SlotStatus.BOOKED.getValue() 传入
* @param checkInTime 签到时间
* @param requiredStatus 前置状态,由 SlotStatus.LOCKED.getValue() 传入
* @return 结果
*/
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, @Param("status") Integer status, @Param("checkInTime") Date checkInTime);
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId,
@Param("status") Integer status,
@Param("checkInTime") Date checkInTime,
@Param("requiredStatus") Integer requiredStatus);
/**
* 根据槽位ID查询所属号源池ID。

View File

@@ -1,10 +1,12 @@
package com.openhis.clinical.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.appointmentmanage.domain.AppointmentConfig;
import com.openhis.appointmentmanage.service.IAppointmentConfigService;
import com.openhis.appointmentmanage.domain.TicketSlotDTO;
import com.openhis.appointmentmanage.domain.SchedulePool;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
@@ -13,7 +15,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.TicketMapper;
import com.openhis.clinical.service.IOrderService;
import com.openhis.clinical.service.ITicketService;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -177,7 +179,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
logger.error("安全拦截号源底库核对失败slotId: {}", slotId);
throw new RuntimeException("号源数据不存在");
}
if (slot.getSlotStatus() != null && !SlotStatus.AVAILABLE.equals(slot.getSlotStatus())) {
if (slot.getSlotStatus() != null && SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.AVAILABLE) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
}
if (Boolean.TRUE.equals(slot.getIsStopped())) {
@@ -205,7 +207,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
}
// 原子抢占:避免并发下同一槽位被重复预约
int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId);
int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId, SlotStatus.LOCKED.getValue());
if (lockRows <= 0) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
}
@@ -260,7 +262,15 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
throw new RuntimeException("预约成功但号源回填订单失败,请重试");
}
refreshPoolStatsBySlotId(slotId);
// 6. 预约成功后 locked_num+1原子递增替代全量 recount避免并发计数漂移
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("locked_num = locked_num + 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return 1;
}
@@ -277,7 +287,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
if (slot == null) {
throw new RuntimeException("号源槽位不存在");
}
if (slot.getSlotStatus() == null || !SlotStatus.BOOKED.equals(slot.getSlotStatus())) {
// 只有锁定态(2)的号源可以取消预约
if (slot.getSlotStatus() == null || SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.LOCKED) {
throw new RuntimeException("号源不可取消预约");
}
@@ -292,7 +303,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "患者取消预约");
}
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE);
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue());
if (updated > 0) {
refreshPoolStatsBySlotId(slotId);
}
@@ -318,11 +329,14 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 查询号源槽位信息
// 2. 只有锁定态(2)的号源才能签到,签到时 2→1(LOCKED→BOOKED)
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
throw new RuntimeException("号源状态异常,无法签到");
}
// 3. 更新号源槽位状态为已签到,记录签到时间
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN, new Date());
// 3. 更新号源槽位状态 2→1LOCKED→BOOKED已预约=已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 4. 更新号源池统计:锁定数-1已预约数+1
if (slot != null && slot.getPoolId() != null) {
@@ -351,7 +365,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "医生停诊");
}
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED);
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED.getValue());
if (updated > 0) {
refreshPoolStatsBySlotId(slotId);
}
@@ -364,7 +378,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
private void refreshPoolStatsBySlotId(Long slotId) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.refreshPoolStats(poolId, SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue());
}
}

View File

@@ -79,11 +79,13 @@ public class OpSchedule extends HisBaseEntity {
private String surgerySite;
/** 入院时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime admissionTime;
/** 入手术室时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime entryTime;
/** 手术室编码 */
@@ -142,19 +144,23 @@ public class OpSchedule extends HisBaseEntity {
private String assistant3Code;
/** 手术开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
/** 手术结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
/** 麻醉开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime anesStart;
/** 麻醉结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime anesEnd;
/** 手术状态 */

View File

@@ -4,14 +4,17 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.appointmentmanage.mapper.ScheduleSlotMapper">
<!-- 统一状态值(兼容数字/英文字符串存储),输出 Integer避免 resultType 映射 NumberFormatException -->
<!--
统一状态值映射: DB 数值 → 规范化输出
0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
-->
<sql id="slotStatusNormExpr">
CASE
WHEN LOWER(CONCAT('', s.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', s.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'locked') THEN 2
WHEN LOWER(CONCAT('', s.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'locked') THEN 4
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4
WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5
ELSE NULL
END
@@ -31,9 +34,9 @@
CASE
WHEN LOWER(CONCAT('', p.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', p.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'locked') THEN 2
WHEN LOWER(CONCAT('', p.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'locked') THEN 4
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4
WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5
ELSE NULL
END
@@ -149,10 +152,11 @@
s.id = #{id}
</select>
<!-- 预约锁定: 0→#{lockedStatus} (AVAILABLE→LOCKED),由枚举传入 -->
<update id="lockSlotForBooking">
UPDATE adm_schedule_slot
SET
status = 1,
status = #{lockedStatus},
update_time = now()
WHERE
id = #{slotId}
@@ -174,6 +178,7 @@
AND delete_flag = '0'
</update>
<!-- 签到: #{requiredStatus}→#{status} (LOCKED→BOOKED),前置条件由枚举传入 -->
<update id="updateSlotStatusAndCheckInTime">
UPDATE adm_schedule_slot
SET
@@ -182,6 +187,7 @@
update_time = NOW()
WHERE
id = #{slotId}
AND status = #{requiredStatus}
AND delete_flag = '0'
</update>
@@ -202,7 +208,7 @@
update_time = now()
WHERE
id = #{slotId}
AND status = 1
AND status = 2
AND delete_flag = '0'
</update>
@@ -299,15 +305,16 @@
<if test="query.phone != null and query.phone != ''">
AND o.phone LIKE CONCAT('%', #{query.phone}, '%')
</if>
<!-- 5. 按系统时间过滤Bug #398 #399 修复:仅未预约受时间过滤,已预约/已取号/已退号不受影响 -->
<!-- 5. 时间过滤: 仅待约(0)受时间限制,已锁定(2)/已约(1)/已签到(3)/已退号(5)不受影响 -->
AND (
(<include refid="slotStatusNormExpr" /> = 0 AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW())))
OR <include refid="slotStatusNormExpr" /> = 1
OR <include refid="slotStatusNormExpr" /> = 2
OR <include refid="slotStatusNormExpr" /> = 3
OR <include refid="slotStatusNormExpr" /> = 5
OR <include refid="orderStatusNormExpr" /> = 4
)
<!-- 6. 状态过滤 -->
<!-- 6. 状态筛选: unbooked(0) locked(2) booked(2) checked(1) cancelled(4) returned(5) -->
<if test="query.status != null and query.status != '' and query.status != 'all'">
<choose>
<when test="'unbooked'.equals(query.status) or '未预约'.equals(query.status)">
@@ -318,7 +325,15 @@
)
</when>
<when test="'booked'.equals(query.status) or '已预约'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 1
AND <include refid="slotStatusNormExpr" /> = 2
AND <include refid="orderStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
)
</when>
<when test="'locked'.equals(query.status) or '已锁定'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 2
AND <include refid="orderStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
@@ -326,13 +341,7 @@
)
</when>
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status)">
AND (
<include refid="slotStatusNormExpr" /> = 3
OR (
<include refid="slotStatusNormExpr" /> = 1
AND <include refid="orderStatusNormExpr" /> = 2
)
)
AND <include refid="slotStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
@@ -340,7 +349,7 @@
</when>
<when test="'cancelled'.equals(query.status) or '已停诊'.equals(query.status) or '已取消'.equals(query.status)">
AND (
<include refid="slotStatusNormExpr" /> = 2
<include refid="slotStatusNormExpr" /> = 4
OR d.is_stopped = TRUE
)
</when>

View File

@@ -172,12 +172,12 @@ export const SlotStatus = {
AVAILABLE: 0,
/** 已预约 */
BOOKED: 1,
/** 已取消 / 已停诊 */
CANCELLED: 2,
/** 已锁定 */
LOCKED: 2,
/** 已签到 / 已取号 */
CHECKED_IN: 3,
/** 已锁定 */
LOCKED: 4,
/** 已取消 / 已停诊 */
CANCELLED: 4,
};
/**
@@ -185,10 +185,10 @@ export const SlotStatus = {
*/
export const SlotStatusDescriptions = {
0: '未预约',
1: '已预约',
2: '已停诊',
1: '已取号',
2: '已锁定',
3: '已取号',
4: '已锁定',
4: '已停诊',
};
/**
@@ -220,3 +220,18 @@ export function getSlotStatusDescription(value) {
export function getSlotStatusClass(status) {
return SlotStatusClassMap[status] || 'status-unbooked';
}
/**
* 诊疗项目分类代码(对应后端 ActivityDefCategory 枚举)
* wor_activity_definition.category_code 字段
*/
export const ActivityCategory = {
/** 治疗 */
TREATMENT: '21',
/** 检验 */
PROOF: '22',
/** 检查 */
TEST: '23',
/** 手术 */
PROCEDURE: '24',
};

View File

@@ -34,6 +34,7 @@
<select id="status-select" class="search-select" v-model="selectedStatus" @change="onSearch">
<option value="all">全部</option>
<option value="unbooked">未预约</option>
<option value="locked">已锁定</option>
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
@@ -253,6 +254,7 @@ import useUserStore from '@/store/modules/user';
const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已锁定': 'status-locked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已退号': 'status-returned',
@@ -774,6 +776,7 @@ export default {
// 🔧 BugFix#399: 确保已取号状态正确匹配
const statusMap = {
unbooked: ['未预约'],
locked: ['已锁定'],
booked: ['已预约'],
checked: ['已取号', '已签到'],
cancelled: ['已停诊', '已取消'],

View File

@@ -1685,7 +1685,7 @@ function loadCheckInPatientList() {
const today = formatDateStr(new Date(), 'YYYY-MM-DD');
listTicket({
date: today,
status: 'booked',
status: 'locked',
name: checkInSearchKey.value, // 支持姓名等模糊查询,后端需适配
page: checkInPage.value,
limit: checkInLimit.value

View File

@@ -1173,8 +1173,9 @@ function handleSaveSign(row, index) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }, '1').then((res) => {
// 🔧 门诊计费场景:保存为草稿,让药品出现在临时医嘱弹窗"已引用计费药品(待生成医嘱)"中
const adviceOpType = props.patientInfo.sourceBillNo ? '0' : '1'
savePrescription({ adviceSaveList: [cleanRow] }, adviceOpType).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);

View File

@@ -1,30 +1,30 @@
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
ref="queryRef"
:model="queryParams"
:inline="true"
label-width="90px"
>
<el-form-item label="查询日期">
<el-form-item label="查询日期">
<el-date-picker
v-model="queryTime"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 300px; margin-right: 20px"
@change="getValue"
style="width: 300px"
value-format="YYYY-MM-DD"
@change="getValue"
/>
</el-form-item>
<el-form-item label="费用性质">
<el-form-item label="费用性质">
<el-select
v-model="contractNo"
placeholder="费用性质"
clearable
style="width: 160px"
@change="getValue"
style="width: 150px; margin-right: 30px"
>
<el-option
v-for="item in contractList"
@@ -33,228 +33,241 @@
:value="item.busNo"
/>
</el-select>
<el-button type="primary" plain icon="Search" @click="getValue">查询</el-button>
<el-button type="primary" plain icon="Printer" @click="print">打印</el-button>
</el-form-item>
<!-- <el-form-item label="科室:" prop="sourceLocationId">
<el-select
v-model="queryParams.sourceLocationId"
placeholder=""
clearable
style="width: 150px"
<el-form-item class="search-buttons">
<el-button
type="primary"
plain
icon="Search"
@click="getValue"
>
<el-option
v-for="issueDepartment in issueDepartmentDto"
:key="issueDepartment.id"
:label="issueDepartment.name"
:value="issueDepartment.id"
/>
</el-select>
</el-form-item> -->
查询
</el-button>
<el-button
type="primary"
plain
icon="Printer"
@click="print"
>
打印
</el-button>
</el-form-item>
</el-form>
<!-- <el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Search" @click="getValue">查询</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="CircleClose" @click="handleClear">重置</el-button>
</el-col>
</el-row> -->
<div v-loading="loading">
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<!-- <el-col :span="3">
<span>经办人编号</span>
</el-col> -->
<el-col :span="3">
<span class="label">经办人姓名</span>
<span class="value"> {{ userStore.nickName }}</span>
<div
v-loading="loading"
class="report-container"
>
<div class="report-title">
门诊收费日结单
</div>
<el-row :gutter="20" class="info-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="info-cell">
<span class="info-label">经办人姓名</span>
<span class="info-value">{{ userStore.nickName || '全部' }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">科室</span>
<span class="value">{{ userStore.orgName }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-cell">
<span class="info-label">科室</span>
<span class="info-value">{{ userStore.orgName || '-' }}</span>
</div>
</el-col>
<el-col :span="4.5">
<span class="label">时间</span>
<span class="value"> {{ queryTime[0] + '~' + queryTime[1] }} </span>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-cell">
<span class="info-label">机构</span>
<span class="info-value">{{ userStore.hospitalName || '-' }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-cell">
<span class="info-label">时间</span>
<span class="info-value">{{ queryTime && queryTime.length === 2 ? queryTime[0] + ' ~ ' + queryTime[1] : '-' }}</span>
</div>
</el-col>
</el-row>
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<el-col :span="3">
<span class="label">实际现金收入</span>
<span class="value"> {{ formatValue(reportValue.cashSum) }}</span>
<el-divider />
<div class="section-title">收入汇总</div>
<el-row :gutter="16" class="data-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">总收入</span>
<span class="data-value">{{ formatValue(reportValue.cashSum) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">现金</span>
<span class="value">{{ formatValue(reportValue.rmbCashSum) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">现金</span>
<span class="data-value">{{ formatValue(reportValue.rmbCashSum) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">微信</span>
<span class="value">{{ formatValue(reportValue.vxCashSum) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">微信</span>
<span class="data-value">{{ formatValue(reportValue.vxCashSum) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">支付宝</span>
<span class="value">{{ formatValue(reportValue.aliCashSum) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">支付宝</span>
<span class="data-value">{{ formatValue(reportValue.aliCashSum) }}</span>
</div>
</el-col>
</el-row>
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<el-col :span="3">
<span class="label">统筹支付</span>
<span class="value">{{ formatValue(reportValue.tcSum) }}</span>
<el-divider />
<div class="section-title">医保支付</div>
<el-row :gutter="16" class="data-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">统筹支付</span>
<span class="data-value">{{ formatValue(reportValue.tcSum) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">账户支付</span>
<span class="value">{{ formatValue(reportValue.zhSum) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">账户支付</span>
<span class="data-value">{{ formatValue(reportValue.zhSum) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">基金支付总额</span>
<span class="value">{{ formatValue(reportValue.fundSum) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">基金支付总额</span>
<span class="data-value">{{ formatValue(reportValue.fundSum) }}</span>
</div>
</el-col>
<!-- <el-col :span="3">
<span>医保人次{{ reportValue.aliCashSum }}</span>
</el-col> -->
</el-row>
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<el-col :span="3">
<span class="label">诊查费</span>
<span class="value">{{ formatValue(reportValue.DIAGNOSTIC_FEE) }}</span>
</el-col>
<el-col :span="3">
<span class="label">检查费</span>
<span class="value">{{ formatValue(reportValue.CHECK_FEE) }}</span>
</el-col>
<el-col :span="3">
<span class="label">化验费</span>
<span class="value">{{ formatValue(reportValue.DIAGNOSTIC_TEST_FEE) }}</span>
</el-col>
<el-col :span="3">
<span class="label">治疗费</span>
<span class="value">{{ formatValue(reportValue.MEDICAL_EXPENSE_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">医保统筹+账户</span>
<span class="data-value">{{ formatValue(Number(reportValue.zhSum || 0) + Number(reportValue.fundSum || 0)) }}</span>
</div>
</el-col>
</el-row>
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<el-col :span="3">
<span class="label">西药费</span>
<span class="value">{{ formatValue(reportValue.WEST_MEDICINE) }}</span>
<el-divider />
<div class="section-title">费用明细</div>
<el-row :gutter="16" class="data-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">诊查费</span>
<span class="data-value">{{ formatValue(reportValue.DIAGNOSTIC_FEE) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">中药饮片费</span>
<span class="value">{{ formatValue(reportValue.CHINESE_MEDICINE_SLICES_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">检查费</span>
<span class="data-value">{{ formatValue(reportValue.CHECK_FEE) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">中成药费</span>
<span class="value">{{ formatValue(reportValue.CHINESE_MEDICINE_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">化验费</span>
<span class="data-value">{{ formatValue(reportValue.DIAGNOSTIC_TEST_FEE) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">卫生材料费</span>
<span class="value">{{ formatValue(reportValue.SANITARY_MATERIALS_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">治疗费</span>
<span class="data-value">{{ formatValue(reportValue.MEDICAL_EXPENSE_FEE) }}</span>
</div>
</el-col>
</el-row>
<el-row
:gutter="10"
outpatientNo="mb8"
style="
margin: 20px 0;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 20px;
"
>
<el-col :span="3">
<span class="label">诊疗费</span>
<span class="value">{{ formatValue(reportValue.GENERAL_CONSULTATION_FEE) }}</span>
<el-row :gutter="16" class="data-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">西药费</span>
<span class="data-value">{{ formatValue(reportValue.WEST_MEDICINE) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">挂号费</span>
<span class="value">{{ formatValue(reportValue.REGISTRATION_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">中药饮片费</span>
<span class="data-value">{{ formatValue(reportValue.CHINESE_MEDICINE_SLICES_FEE) }}</span>
</div>
</el-col>
<el-col :span="3">
<span class="label">其他费用</span>
<span class="value">{{ formatValue(reportValue.OTHER_FEE) }}</span>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">中成药费</span>
<span class="data-value">{{ formatValue(reportValue.CHINESE_MEDICINE_FEE) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">卫生材料费</span>
<span class="data-value">{{ formatValue(reportValue.SANITARY_MATERIALS_FEE) }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="data-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">普通挂号费</span>
<span class="data-value">{{ formatValue(reportValue.GENERAL_CONSULTATION_FEE) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">挂号费</span>
<span class="data-value">{{ formatValue(reportValue.REGISTRATION_FEE) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">其他费用</span>
<span class="data-value">{{ formatValue(reportValue.OTHER_FEE) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell">
<span class="data-label">退费金额</span>
<span class="data-value">{{ formatValue(reportValue.returnFee) }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="data-row summary-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell summary-cell">
<span class="data-label summary-label">费用总额</span>
<span class="data-value value-highlight">{{ totalFeeAmount }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="data-cell summary-cell">
<span class="data-label summary-label">医保报销</span>
<span class="data-value value-highlight">{{ insuranceReimbursement }}</span>
</div>
</el-col>
<!-- <el-col :span="3">
<span>现金</span>
</el-col> -->
</el-row>
</div>
</div>
</template>
<script setup name="dayEnd">
import {getContractList, getRreportReturnIssue, getTotal} from './component/api';
import useUserStore from '@/store/modules/user';
import {formatDateStr} from '@/utils/index';
<script setup name="DayEnd">
import { ref, reactive, toRefs, getCurrentInstance, computed } from 'vue';
import Decimal from 'decimal.js';
import { getTotal, getContractList, getRreportReturnIssue } from './component/api';
import useUserStore from '@/store/modules/user';
import { formatDateStr } from '@/utils/index';
const userStore = useUserStore();
// import Dialog from "./components/Dialog";
const router = useRouter();
const { proxy } = getCurrentInstance();
const purchaseinventoryRef = ref(null); // 初始化 ref
const purchaseinventoryList = ref([]);
const open = ref(false);
const loading = ref(true);
const reportValue = ref({});
const total = ref(0);
const loading = ref(false);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref('');
const contractList = ref(undefined);
const reportValue = ref({});
const occurrenceTime = ref([]);
const contractList = ref([]);
const queryTime = ref([
formatDateStr(new Date(), 'YYYY-MM-DD'),
formatDateStr(new Date(), 'YYYY-MM-DD'),
@@ -300,10 +313,6 @@ function getContract() {
}
function getPharmacyCabinetLists() {
// occurrenceTime.value =
// getDepartmentList().then((response) => {
// issueDepartmentDto.value = response.data
// })
}
/** 查询调拨管理项目列表 */
function getList() {
@@ -331,7 +340,6 @@ function handleQuery() {
/** 清空条件按钮操作 */
function handleClear() {
// 清空查询条件
queryParams.value.approvalTimeSTime = '';
queryParams.value.approvalTimeETime = '';
occurrenceTime.value = '';
@@ -348,12 +356,11 @@ function handleSelectionChange(selection) {
/** 打印门诊日结 */
async function print() {
// const selectedRows = proxy.$refs['tableRef'].getSelectionRows();
console.log(reportValue.value, '==reportValue.value==');
const result = {
data: [
{
...reportValue.value, // 将 reportValue.value 中的所有属性展开到 result 中
...reportValue.value,
nickName: userStore.nickName,
orgName: userStore.orgName,
fixmedinsName: userStore.hospitalName,
@@ -374,14 +381,12 @@ async function print() {
],
};
console.log(result, '==result.data==');
// 将对象转换为 JSON 字符串
let jsonString = JSON.stringify(result, null, 2);
console.log(jsonString, 'jsonstring');
await CefSharp.BindObjectAsync('boundAsync');
await boundAsync
.printReport(getPrintFileName(contractNo.value), jsonString)
.then((response) => {
//返回结果是jsonString可判断其调用是否成功
console.log(response, 'response');
var res = JSON.parse(response);
if (!res.IsSuccess) {
@@ -397,9 +402,9 @@ function getPrintFileName(value) {
switch (value) {
case '0000':
return '门诊日结单(按登录角色查询)自费.grf';
case '229900': // 省医保
case '229900':
return '门诊日结单(按登录角色查询)省医保.grf';
case '220100': // 市医保
case '220100':
return '门诊日结单(按登录角色查询)市医保.grf';
}
}
@@ -408,28 +413,173 @@ function formatValue(value) {
return value == null || value == undefined ? '0.00 元' : value.toFixed(2) + ' 元';
}
// 计算属性:费用总额
const totalFeeAmount = computed(() => {
const v = reportValue.value;
const sum =
Number(v.DIAGNOSTIC_FEE || 0) +
Number(v.CHECK_FEE || 0) +
Number(v.DIAGNOSTIC_TEST_FEE || 0) +
Number(v.MEDICAL_EXPENSE_FEE || 0) +
Number(v.WEST_MEDICINE || 0) +
Number(v.CHINESE_MEDICINE_SLICES_FEE || 0) +
Number(v.CHINESE_MEDICINE_FEE || 0) +
Number(v.GENERAL_CONSULTATION_FEE || 0) +
Number(v.REGISTRATION_FEE || 0) +
Number(v.OTHER_FEE || 0) +
Number(v.SANITARY_MATERIALS_FEE || 0);
return formatValue(sum);
});
// 计算属性:医保报销(统筹+账户)
const insuranceReimbursement = computed(() => {
const v = reportValue.value;
const sum = Number(v.tcSum || 0) + Number(v.zhSum || 0);
return formatValue(sum);
});
getList();
getPharmacyCabinetLists();
</script>
<style scoped>
.custom-tree-node {
display: flex;
align-items: center;
.app-container {
padding: 16px;
}
.title {
font-weight: bold;
font-size: large;
margin-bottom: 10px;
.report-container {
max-width: 1200px;
margin: 16px auto;
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.label {
display: inline-block;
width: 120px !important;
.report-title {
text-align: center;
font-size: 20px;
font-weight: 600;
margin: 0 0 20px;
color: #303133;
}
.value {
float: right;
.info-row {
padding: 12px 0;
}
.el-col {
margin-right: 50px;
.info-cell {
display: flex;
align-items: center;
padding: 6px 0;
min-height: 32px;
}
.info-label {
color: #909399;
font-size: 13px;
white-space: nowrap;
min-width: 80px;
}
.info-value {
color: #303133;
font-size: 14px;
font-weight: 500;
flex: 1;
}
.data-row {
padding: 4px 0;
}
.data-cell {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 4px;
background: #fafafa;
border-radius: 4px;
min-height: 40px;
}
.data-label {
color: #606266;
font-size: 13px;
white-space: nowrap;
min-width: 100px;
text-align: right;
padding-right: 8px;
}
.data-value {
color: #303133;
font-size: 14px;
font-weight: 500;
flex: 1;
text-align: right;
}
.value-highlight {
color: #409eff;
font-weight: 700;
font-size: 15px;
}
.summary-row {
margin-top: 8px;
}
.summary-cell {
background: #ecf5ff;
border: 1px solid #d9ecff;
}
.summary-label {
font-weight: 600;
color: #409eff;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #409eff;
padding: 8px 0 8px 12px;
margin: 8px 0 4px;
border-left: 3px solid #409eff;
background: linear-gradient(90deg, rgba(64, 158, 255, 0.05) 0%, transparent 100%);
}
.search-buttons {
margin-bottom: 0;
}
.search-buttons .el-form-item__content {
justify-content: flex-start;
}
:deep(.el-divider--horizontal) {
margin: 12px 0;
}
.el-form--inline .el-form-item {
margin-bottom: 12px;
margin-right: 16px;
}
@media (max-width: 768px) {
.data-label {
min-width: 80px;
text-align: left;
padding-right: 4px;
}
.data-value {
text-align: left;
}
.info-label {
min-width: 70px;
}
}
</style>

View File

@@ -0,0 +1,352 @@
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
label-width="90px"
>
<el-form-item label="查询日期:">
<el-date-picker
v-model="queryTime"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 300px; margin-right: 20px"
@change="getValue"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="费用性质:">
<el-select
v-model="contractNo"
placeholder="费用性质"
clearable
@change="getValue"
style="width: 150px; margin-right: 30px"
>
<el-option
v-for="item in contractList"
:key="item.busNo"
:label="item.contractName"
:value="item.busNo"
/>
</el-select>
<el-button type="primary" plain icon="Search" @click="getValue">查询</el-button>
<el-button type="primary" plain icon="Printer" @click="print">打印</el-button>
</el-form-item>
</el-form>
<div v-loading="loading" style="width: 1300px">
<div style="text-align: center">
<h2>门诊收费日结单</h2>
</div>
<el-row
:gutter="5"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="4">
<span class="label">经办人姓名</span>
<span class="value">{{ userStore.nickName }}</span>
</el-col>
<el-col :span="4">
<span class="label">科室</span>
<span class="value">{{ userStore.orgName }}</span>
</el-col>
<el-col :span="7">
<span class="label">时间</span>
<span class="value">{{ queryTime[0] + '~' + queryTime[1] }}</span>
</el-col>
</el-row>
<div class="divider"></div>
<el-row
:gutter="10"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="5">
<span class="label">总收入</span>
<span class="value">{{ formatValue(reportValue.cashSum) }}</span>
</el-col>
<el-col :span="5">
<span class="label">现金</span>
<span class="value">{{ formatValue(reportValue.rmbCashSum) }}</span>
</el-col>
<el-col :span="5">
<span class="label">微信</span>
<span class="value">{{ formatValue(reportValue.vxCashSum) }}</span>
</el-col>
<el-col :span="5">
<span class="label">支付宝</span>
<span class="value">{{ formatValue(reportValue.aliCashSum) }}</span>
</el-col>
</el-row>
<div class="divider"></div>
<el-row
:gutter="10"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="5">
<span class="label">统筹支付</span>
<span class="value">{{ formatValue(reportValue.tcSum) }}</span>
</el-col>
<el-col :span="5">
<span class="label">账户支付</span>
<span class="value">{{ formatValue(reportValue.zhSum) }}</span>
</el-col>
<el-col :span="5">
<span class="label">基金支付总额</span>
<span class="value">{{ formatValue(reportValue.fundSum) }}</span>
</el-col>
</el-row>
<div class="divider"></div>
<el-row
:gutter="10"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="5">
<span class="label">诊查费</span>
<span class="value">{{ formatValue(reportValue.DIAGNOSTIC_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">检查费</span>
<span class="value">{{ formatValue(reportValue.CHECK_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">化验费</span>
<span class="value">{{ formatValue(reportValue.DIAGNOSTIC_TEST_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">治疗费</span>
<span class="value">{{ formatValue(reportValue.MEDICAL_EXPENSE_FEE) }}</span>
</el-col>
</el-row>
<el-row
:gutter="10"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="5">
<span class="label">西药费</span>
<span class="value">{{ formatValue(reportValue.WEST_MEDICINE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">中药饮片费</span>
<span class="value">{{ formatValue(reportValue.CHINESE_MEDICINE_SLICES_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">中成药费</span>
<span class="value">{{ formatValue(reportValue.CHINESE_MEDICINE_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">卫生材料费</span>
<span class="value">{{ formatValue(reportValue.SANITARY_MATERIALS_FEE) }}</span>
</el-col>
</el-row>
<el-row
:gutter="10"
style="margin: 20px 0; display: flex; align-items: center; justify-content: flex-start; padding: 0 20px"
>
<el-col :span="5">
<span class="label">诊疗费</span>
<span class="value">{{ formatValue(reportValue.GENERAL_CONSULTATION_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">挂号费</span>
<span class="value">{{ formatValue(reportValue.REGISTRATION_FEE) }}</span>
</el-col>
<el-col :span="5">
<span class="label">其他费用</span>
<span class="value">{{ formatValue(reportValue.OTHER_FEE) }}</span>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup name="dayEnd">
import {getContractList, getRreportReturnIssue, getTotal} from './component/api';
import useUserStore from '@/store/modules/user';
import {formatDateStr} from '@/utils/index';
import Decimal from 'decimal.js';
const userStore = useUserStore();
const router = useRouter();
const { proxy } = getCurrentInstance();
const purchaseinventoryRef = ref(null);
const purchaseinventoryList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref('');
const contractList = ref(undefined);
const reportValue = ref({});
const queryTime = ref([
formatDateStr(new Date(), 'YYYY-MM-DD'),
formatDateStr(new Date(), 'YYYY-MM-DD'),
]);
const contractNo = ref('0000');
const data = reactive({
queryParams: {
form: {},
pageNo: 1,
pageSize: 10,
searchKey: undefined,
purposeLocationId: undefined,
sourceLocationId: undefined,
supplierId: undefined,
approvalTimeSTime: undefined,
approvalTimeETime: undefined,
},
rules: {},
});
const { queryParams, form, rules } = toRefs(data);
getValue();
function getValue() {
loading.value = true;
getTotal({
contractNo: contractNo.value,
startTime: queryTime.value[0] + ' 00:00:00',
endTime: queryTime.value[1] + ' 23:59:59',
entererId: userStore.practitionerId,
}).then((res) => {
loading.value = false;
reportValue.value = res.data;
});
}
getContract();
function getContract() {
getContractList().then((response) => {
contractList.value = response.data;
});
}
function getPharmacyCabinetLists() {
}
/** 查询调拨管理项目列表 */
function getList() {
loading.value = true;
getRreportReturnIssue(queryParams.value).then((res) => {
loading.value = false;
purchaseinventoryList.value = res.data.records;
total.value = res.data.total;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.approvalTimeSTime =
occurrenceTime.value && occurrenceTime.value.length == 2
? occurrenceTime.value[0] + ' 00:00:00'
: '';
queryParams.value.approvalTimeETime =
occurrenceTime.value && occurrenceTime.value.length == 2
? occurrenceTime.value[1] + ' 23:59:59'
: '';
queryParams.value.pageNo = 1;
getList();
}
/** 清空条件按钮操作 */
function handleClear() {
queryParams.value.approvalTimeSTime = '';
queryParams.value.approvalTimeETime = '';
occurrenceTime.value = '';
proxy.resetForm('queryRef');
getList();
}
/** 选择条数 */
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 打印门诊日结 */
async function print() {
console.log(reportValue.value, '==reportValue.value==');
const result = {
data: [
{
...reportValue.value,
nickName: userStore.nickName,
orgName: userStore.orgName,
fixmedinsName: userStore.hospitalName,
queryTime: queryTime.value[0] + '~' + queryTime.value[1],
zfAmount: new Decimal(reportValue.value.zhSum || 0).add(reportValue.value.fundSum || 0),
feeAmount: new Decimal(reportValue.value.DIAGNOSTIC_FEE || 0)
.add(reportValue.value.CHECK_FEE || 0)
.add(reportValue.value.DIAGNOSTIC_TEST_FEE || 0)
.add(reportValue.value.MEDICAL_EXPENSE_FEE || 0)
.add(reportValue.value.WEST_MEDICINE || 0)
.add(reportValue.value.CHINESE_MEDICINE_SLICES_FEE || 0)
.add(reportValue.value.CHINESE_MEDICINE_FEE || 0)
.add(reportValue.value.GENERAL_CONSULTATION_FEE || 0)
.add(reportValue.value.REGISTRATION_FEE || 0)
.add(reportValue.value.OTHER_FEE || 0)
.add(reportValue.value.SANITARY_MATERIALS_FEE || 0),
},
],
};
console.log(result, '==result.data==');
let jsonString = JSON.stringify(result, null, 2);
console.log(jsonString, 'jsonstring');
await CefSharp.BindObjectAsync('boundAsync');
await boundAsync
.printReport(getPrintFileName(contractNo.value), jsonString)
.then((response) => {
console.log(response, 'response');
var res = JSON.parse(response);
if (!res.IsSuccess) {
proxy.$modal.msgError('调用打印插件失败:' + res.ErrorMessage);
}
})
.catch((error) => {
proxy.$modal.msgError('调用打印插件失败:' + error);
});
}
function getPrintFileName(value) {
switch (value) {
case '0000':
return '门诊日结单(按登录角色查询)自费.grf';
case '229900':
return '门诊日结单(按登录角色查询)省医保.grf';
case '220100':
return '门诊日结单(按登录角色查询)市医保.grf';
}
}
function formatValue(value) {
return value == null || value == undefined ? '0.00 元' : value.toFixed(2) + ' 元';
}
getList();
getPharmacyCabinetLists();
</script>
<style scoped>
.label {
display: inline-block;
width: 120px !important;
}
.value {
float: right;
}
.el-col {
margin-right: 50px;
}
.divider {
height: 3px;
background-color: #000;
margin: 20px 0;
}
</style>

View File

@@ -749,22 +749,26 @@ function handleInfectiousDiseaseReport() {
'手足口病': '0311',
};
// 获取所有诊断名称对应的报卡编码,但跳过已有已提交报卡的诊断
const allSelectedDiseases = form.value.diagnosisList
.filter(d => d.name && d.hasInfectiousReport !== 1)
.map(d => diseaseNameToCode[d.name] || null)
.filter(code => code);
// 获取所有命中传染病映射的诊断,但跳过已有已提交报卡的诊断
const infectiousDiagnoses = form.value.diagnosisList
.map(d => ({
diagnosis: d,
diseaseCode: d.name && d.hasInfectiousReport !== 1 ? diseaseNameToCode[d.name] : null
}))
.filter(item => item.diseaseCode);
const allSelectedDiseases = infectiousDiagnoses.map(item => item.diseaseCode);
if (allSelectedDiseases.length === 0) {
return;
}
// 优先使用主诊断(同样跳过已有报卡的)
const mainDiagnosis = form.value.diagnosisList.find(d => d.maindiseFlag === 1 && d.hasInfectiousReport !== 1);
const firstDiagnosis = form.value.diagnosisList.find(d => d.hasInfectiousReport !== 1) || form.value.diagnosisList[0];
// 优先使用命中传染病映射的主诊断,否则使用第一条命中的传染病诊断
const mainInfectiousDiagnosis = infectiousDiagnoses.find(item => item.diagnosis.maindiseFlag === 1)?.diagnosis;
const firstInfectiousDiagnosis = infectiousDiagnoses[0].diagnosis;
const diagnosisToShow = {
...(mainDiagnosis || firstDiagnosis),
...(mainInfectiousDiagnosis || firstInfectiousDiagnosis),
selectedDiseases: allSelectedDiseases
};

View File

@@ -1442,7 +1442,7 @@ async function buildSubmitData() {
const submitData = {
cardNo: formData.cardNo,
visitId: props.patientInfo?.encounterId || formData.encounterId || null,
diagId: formData.diagnosisId ? Number(formData.diagnosisId) : null,
diagId: formData.diagnosisId || null,
patId: formData.patientId || null,
idType: 1, // 默认身份证
idNo: formData.idNo,

View File

@@ -179,8 +179,8 @@
type="datetime"
placeholder="选择执行时间"
size="small"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 100%"
/>
</el-form-item>
@@ -445,7 +445,6 @@
>
<el-table-column label="项目名称" prop="itemName" min-width="180">
<template #default="scope">
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span :style="{ fontWeight: scope.row.isPackage ? 'bold' : 'normal' }">
{{ scope.row.itemName }}
</span>
@@ -563,7 +562,6 @@
@change="toggleInspectionItem(item)"
@click.stop
/>
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
</div>
@@ -614,7 +612,6 @@
<template v-if="item.isPackage">{{ item.expanded ? '' : '' }}</template>
<template v-else></template>
</span>
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
<span class="item-itemName">{{ item.itemName }}</span>
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
<el-button
@@ -875,6 +872,30 @@ let applyTimeTimer = null
const userStore = useUserStore()
const { id: userId, name: userName, nickName: userNickName } = storeToRefs(userStore)
/** 执行时间默认值:当前系统时间,精确到分钟 */
const getDefaultExecuteTime = () => {
const d = new Date()
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
/** 将后端时间规范为 YYYY-MM-DD HH:mm */
const normalizeExecuteTime = (value) => {
if (!value) return getDefaultExecuteTime()
const d = new Date(String(value).replace(/-/g, '/'))
if (Number.isNaN(d.getTime())) return getDefaultExecuteTime()
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 修改 initData 函数
const initData = async () => {
// 先初始化患者信息(如果有)
@@ -897,6 +918,13 @@ const initData = async () => {
formData.applyNo = '自动生成'
// 申请日期实时更新(启动定时器)
startApplyTimeTimer()
// 执行时间默认当前系统时间(精确到分钟)
if (!formData.executeTime) {
formData.executeTime = getDefaultExecuteTime()
}
// 执行时间默认填充当前系统时间
formData.executeTime = formatDateTime(new Date())
// 获取主诊断信息
try {
@@ -975,7 +1003,7 @@ const formData = reactive({
applyDeptCode: '',
specimenName: '血液',
encounterId: '',
executeTime: null,
executeTime: getDefaultExecuteTime(),
applicationType: 0
})
@@ -1185,9 +1213,9 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
// 映射数据格式(从检验项目维护页面的数据结构映射)
const mappedItems = records.map(item => {
// 套餐项目处理:套餐项目使用套餐金额,普通项目使用零售价
// BugFix#404: 增加对空字符串的判断避免空字符串被误认为有效套餐ID
const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null'
// 套餐项目处理:需同时满足 feePackageId 有效且 packageName 非空
// BugFix#556: 增加 packageName 联合判断,避免普通项目因 feePackageId 有值被误标为套餐
const isPackage = item.feePackageId != null && item.feePackageId !== '' && item.feePackageId !== 'null' && item.packageName
const itemPrice = isPackage
? (parseFloat(item.packageAmount || 0) || parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
: (parseFloat(item.retailPrice || 0) || parseFloat(item.price || 0))
@@ -1547,7 +1575,7 @@ const resetForm = async () => {
visitNo: '',
specimenName: '血液',
encounterId: props.patientInfo.encounterId || '',
executeTime: null,
executeTime: getDefaultExecuteTime(),
applicationType: 0,
})
selectedInspectionItems.value = []
@@ -1985,7 +2013,7 @@ const loadApplicationToForm = async (row) => {
visitNo: detail.visitNo,
specimenName: detail.specimenName,
encounterId: detail.encounterId,
executeTime: detail.executeTime || null,
executeTime: normalizeExecuteTime(detail.executeTime),
applicationType: detail.applicationType ?? 0
})

View File

@@ -89,8 +89,14 @@ const getList = async () => {
const response = await listPendingEmr(queryParams)
// 根据后端返回的数据结构调整
if (response.code === 200) {
emrList.value = response.data || []
total.value = Array.isArray(response.data) ? response.data.length : 0
const data = response.data
if (data && data.rows !== undefined) {
emrList.value = data.rows || []
total.value = data.total || 0
} else {
emrList.value = Array.isArray(data) ? data : []
total.value = emrList.value.length
}
} else {
ElMessage.error(response.msg || '获取待写病历列表失败')
emrList.value = []

View File

@@ -18,16 +18,12 @@
<!-- <el-table-column label="组套类型" align="center" prop="typeEnum_enumText" /> -->
<el-table-column label="单次剂量" align="center" prop="rangeCode_dictText">
<template #default="scope">
{{
scope.row.dose
? formatNumber(scope.row.dose) + ' ' + scope.row.doseUnitCode_dictText
: ''
}}
{{ formatHistoryDose(scope.row) }}
</template>
</el-table-column>
<el-table-column label="总量" align="center" prop="rangeCode_dictText">
<template #default="scope">
{{ scope.row.quantity ? scope.row.quantity + ' ' + scope.row.unitCode_dictText : '' }}
{{ formatHistoryTotalQuantity(scope.row) }}
</template>
</el-table-column>
<el-table-column label="频次/用法" align="center" prop="rangeCode_dictText" width="200">
@@ -90,6 +86,31 @@ const queryParams = ref({
typeEnum: 1,
});
function formatHistoryTotalQuantity(row) {
if (!row || row.quantity == null || row.quantity === '') return '';
const fromDict = row.unitCode_dictText ?? row.minUnitCode_dictText ?? row.unitCodeName;
let u =
fromDict != null && String(fromDict).trim() !== ''
&& String(fromDict).toLowerCase() !== 'null'
? String(fromDict).trim()
: '';
if (!u) {
const t = Number(row.adviceType);
if (t === 3 || t === 6 || t === 23 || t === 5) u = '次';
else if (t === 4) u = '个';
}
return u ? `${row.quantity} ${u}` : String(row.quantity);
}
function formatHistoryDose(row) {
if (!row?.dose) return '';
const du = row.doseUnitCode_dictText;
if (du != null && String(du).trim() !== '' && String(du).toLowerCase() !== 'null') {
return formatNumber(row.dose) + ' ' + du;
}
return formatNumber(row.dose);
}
function handleOpen() {
drawer.value = true;
getList();

View File

@@ -82,7 +82,7 @@
<span>{{ index + 1 + '. ' }}</span>
<span>{{ medItem.adviceName }}</span>
<span>{{ '(' + medItem.volume + ')' }}</span>
<span>{{ medItem.quantity + ' ' + medItem.unitCode_dictText }}</span>
<span>{{ formatPrintLineQuantity(medItem) }}</span>
<span>{{ '批次号:' + medItem.lotNumber }}</span>
<div>
<span>用法用量</span>
@@ -161,6 +161,22 @@ const props = defineProps({
const emit = defineEmits(['close']);
//合计
function formatPrintLineQuantity(row) {
if (row == null || row.quantity == null || row.quantity === '') return '';
const fromDict = row.unitCode_dictText ?? row.minUnitCode_dictText ?? row.unitCodeName;
let u =
fromDict != null && String(fromDict).trim() !== ''
&& String(fromDict).toLowerCase() !== 'null'
? String(fromDict).trim()
: '';
if (!u) {
const t = Number(row.adviceType);
if (t === 3 || t === 6 || t === 23 || t === 5) u = '次';
else if (t === 4) u = '个';
}
return u ? `${row.quantity} ${u}` : String(row.quantity);
}
function getTotalPrice(item) {
let totalPrice = new Decimal(0);
item.prescriptionInfoDetailList.forEach((medItem) => {

View File

@@ -686,7 +686,15 @@
<span style="margin-left: 4px">{{ scope.row.doseUnitCode_dictText }}</span>
</template>
<span v-else>
{{ scope.row.dose ? scope.row.dose + ' ' + scope.row.doseUnitCode_dictText : '' }}
{{
scope.row.dose
? scope.row.dose +
(scope.row.doseUnitCode_dictText &&
String(scope.row.doseUnitCode_dictText).toLowerCase() !== 'null'
? ' ' + scope.row.doseUnitCode_dictText
: '')
: ''
}}
</span>
</template>
</el-table-column>
@@ -703,10 +711,10 @@
@change="calculateTotalPrice(scope.row, scope.$index)"
@input="calculateTotalPrice(scope.row, scope.$index)"
/>
<span style="margin-left: 4px">{{ scope.row.unitCode_dictText }}</span>
<span style="margin-left: 4px">{{ resolveTotalQuantityUnit(scope.row) }}</span>
</template>
<span v-else>
{{ scope.row.quantity ? scope.row.quantity + ' ' + scope.row.unitCode_dictText : '' }}
{{ formatTotalQuantityWithUnit(scope.row) }}
</span>
</template>
</el-table-column>
@@ -917,6 +925,39 @@ const unitMap = ref({
minUnit: 'minUnit',
unit: 'unit',
});
/** 解析总量单位文案(无字典时:诊疗/手术/检查默认「次」,耗材默认「个」) */
const resolveTotalQuantityUnit = (row) => {
if (row == null) return '';
const fromDict =
row.unitCode_dictText ?? row.minUnitCode_dictText ?? row.unitCodeName;
let unitStr =
fromDict != null && String(fromDict).trim() !== ''
&& String(fromDict).toLowerCase() !== 'null'
? String(fromDict).trim()
: '';
if (!unitStr) {
const t = Number(row.adviceType);
// drord_doctor_type: 3=诊疗 4=耗材 5=会诊 6=手术23=检查(特殊)
// 注意2=中成药(药品),不可用「次」作为默认单位
if (t === 3 || t === 6 || t === 23 || t === 5) {
unitStr = '次';
} else if (t === 4) {
unitStr = '个';
}
}
return unitStr;
};
/** 总量列展示:避免 unitCode_dictText 为空时显示「1 null」 */
const formatTotalQuantityWithUnit = (row) => {
if (row == null) return '';
const q = row.quantity;
if (q === undefined || q === null || q === '') return '';
const unitStr = resolveTotalQuantityUnit(row);
return unitStr ? `${q} ${unitStr}` : String(q);
};
const buttonDisabled = computed(() => {
return !props.patientInfo;
});
@@ -2714,7 +2755,8 @@ function handleEmrTreatment() {
treatment += '诊疗[' + (index + 1) + ']' + ' ';
treatment += item.adviceName + ' ';
if (item.quantity) {
treatment += '数量:' + item.quantity + item.unitCode_dictText + ' ';
const u = resolveTotalQuantityUnit(item);
treatment += '数量:' + item.quantity + (u ? ' ' + u : '') + ' ';
}
treatment += '频次:' + item.rateCode_dictText + ' ';
if (item.methodCode_dictText) {

View File

@@ -113,10 +113,17 @@ const getList = async () => {
loading.value = true
try {
const response = await listPendingEmr(queryParams)
// 根据后端返回的数据结构调整
if (response.code === 200) {
emrList.value = response.data || []
total.value = Array.isArray(response.data) ? response.data.length : 0
const data = response.data
if (data && data.rows !== undefined) {
// 新分页格式 {rows, total}
emrList.value = data.rows || []
total.value = data.total || 0
} else {
// 兼容旧格式(数组)
emrList.value = Array.isArray(data) ? data : []
total.value = emrList.value.length
}
} else {
ElMessage.error(response.msg || '获取待写病历列表失败')
emrList.value = []

View File

@@ -58,7 +58,11 @@
<el-table-column prop="busNo" label="单据号" align="center" width="150" />
<el-table-column prop="applicantName" label="申请人" align="center" width="100" />
<el-table-column prop="locationName" label="发药药房" align="center" />
<el-table-column prop="statusEnum_enumText" label="状态" align="center" />
<el-table-column prop="statusEnum_enumText" label="状态" align="center">
<template #default="scope">
{{ formatSummaryStatusText(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="applyTime" label="汇总日期" align="center" width="140">
<template #default="scope">
{{ scope.row.applyTime ? parseTime(scope.row.applyTime, '{y}-{m}-{d}') : '-' }}
@@ -139,6 +143,32 @@ import {getCurrentInstance, ref} from 'vue';
import {getFromSummaryDetails, getFromSummaryInit, getFromSummaryList, totalSendDrug,} from './api.js';
const { proxy } = getCurrentInstance();
/** 发药汇总单状态展示(汇总申请→已提交,发药→已发药) */
const SUMMARY_STATUS_DISPLAY = {
2: '已提交',
4: '已发药',
};
const LEGACY_SUMMARY_STATUS_TEXT = {
待配药: '已提交',
已发放: '已发药',
};
function formatSummaryStatusText(row) {
const code = Number(row?.statusEnum);
if (SUMMARY_STATUS_DISPLAY[code]) {
return SUMMARY_STATUS_DISPLAY[code];
}
return LEGACY_SUMMARY_STATUS_TEXT[row?.statusEnum_enumText] || row?.statusEnum_enumText || '-';
}
function mapSummaryStatusOptions(options = []) {
return options.map((item) => ({
...item,
label: SUMMARY_STATUS_DISPLAY[item.value] ?? LEGACY_SUMMARY_STATUS_TEXT[item.label] ?? item.label,
}));
}
const statusEnumOptions = ref([]);
const summaryList = ref([]);
const queryParams = ref({
@@ -222,7 +252,7 @@ function handleSend(row) {
const getStatusOption = async () => {
try {
const res = await getFromSummaryInit();
statusEnumOptions.value = res.data.dispenseStatusOptions;
statusEnumOptions.value = mapSummaryStatusOptions(res.data.dispenseStatusOptions);
} catch (error) {}
};
getStatusOption();

View File

@@ -23,7 +23,7 @@
</template>
<script setup lang="ts">
import {computed, nextTick, onMounted, ref} from 'vue';
import {computed, nextTick, ref} from 'vue';
import {throttle} from 'lodash-es';
import Table from '@/components/TableLayout/Table.vue';
import {getAdviceBaseInfo} from './api';
@@ -204,11 +204,6 @@ defineExpose({
handleKeyDown,
refresh,
});
// 组件挂载时自动加载数据el-popover 懒渲染,父组件 refresh 可能因时序问题未生效onMounted 最可靠)
onMounted(() => {
getList();
});
</script>
<style scoped lang="scss">

View File

@@ -250,10 +250,11 @@ export function getContract(params) {
/**
* 获取科室列表
*/
export function getOrgTree() {
export function getOrgTree(params = {}) {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: { pageNo: 1, pageSize: 5000, ...params },
});
}

View File

@@ -86,7 +86,7 @@
</template>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column label="申请单名称" width="140">
<el-table-column label="申请单名称" min-width="140">
<template #default="scope">
<span>{{ buildApplicationName(scope.row) }}</span>
</template>
@@ -444,11 +444,9 @@ const buildApplicationName = (row) => {
if (!details || details.length === 0) {
return row.name || '-';
}
if (details.length === 1) {
return details[0].adviceName || row.name || '-';
}
const first = details[0];
return `${first.adviceName || ''}${details.length}`;
const names = details.map(d => d.adviceName).filter(Boolean);
if (names.length === 0) return row.name || '-';
return names.join(' + ');
};
/**

View File

@@ -41,7 +41,9 @@
<el-option label="全部" value="" />
<el-option label="待签发" value="0" />
<el-option label="已签发" value="1" />
<el-option label="已出报告" value="6" />
<el-option label="已采证" value="4" />
<el-option label="已送检" value="5" />
<el-option label="报告已出" value="6" />
<el-option label="已作废" value="7" />
</el-select>
</el-form-item>
@@ -91,7 +93,15 @@
<el-table-column prop="prescriptionNo" label="申请单号" width="140" />
<el-table-column label="单据状态" width="100" align="center">
<template #default="scope">
<span>{{ parseBillStatus(scope.row.billStatus ?? scope.row.status) }}</span>
<el-tag
:type="getBillStatusTagType(scope.row)"
effect="plain"
round
:class="{ 'report-status-tag': isReportStatus(scope.row) }"
@click="handleStatusClick(scope.row)"
>
{{ parseBillStatus(getBillStatus(scope.row)) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="申请类型" width="100" align="center">
@@ -105,20 +115,18 @@
</template>
</el-table-column>
<el-table-column prop="requesterId_dictText" label="申请者" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="220">
<el-table-column label="操作" align="center" fixed="right" width="280">
<template #default="scope">
<!-- 待签发status=0或null/undefined可修改删除 -->
<template v-if="!scope.row.status || scope.row.status == 0">
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
<template v-if="canManageRow(scope.row) && isPendingStatus(scope.row)">
<el-button link type="primary" @click="handleEdit(scope.row)">修改</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
<!-- 已签发status=1可撤回 -->
<template v-else-if="scope.row.status == 1">
<template v-if="canManageRow(scope.row) && isWithdrawableStatus(scope.row)">
<el-button link type="warning" @click="handleWithdraw(scope.row)">撤回</el-button>
</template>
<!-- 已校对(2)待接收(3)已收样(4)已出报告(6)已作废(7)仅查看详情 -->
<template v-else>
<el-button link type="primary" @click="handleViewDetail(scope.row)">详情</el-button>
<template v-if="isReportStatus(scope.row)">
<el-button link type="success" @click="handleViewReport(scope.row)">查看报告</el-button>
</template>
</template>
</el-table-column>
@@ -212,13 +220,16 @@
</template>
<script setup>
import {computed, getCurrentInstance, ref, watch} from 'vue';
import {computed, getCurrentInstance, nextTick, ref, watch} from 'vue';
import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js';
import {getInspection, deleteRequestForm, withdrawRequestForm} from './api';
import {getInspection, deleteRequestForm, withdrawRequestForm, getProofResult} from './api';
import {getDepartmentList} from '@/api/public.js';
import LaboratoryTests from '../order/applicationForm/laboratoryTests.vue';
import {saveInspection} from '../order/applicationForm/api.js';
import useUserStore from '@/store/modules/user';
import auth from '@/plugins/auth';
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
@@ -270,7 +281,7 @@ const fetchData = async () => {
if (res.code === 200 && res.data) {
const raw = res.data?.records || res.data;
const list = Array.isArray(raw) ? raw : [raw];
tableData.value = list.filter(Boolean);
tableData.value = list.filter(Boolean).sort(sortByCreateTimeDesc);
} else {
tableData.value = [];
}
@@ -329,19 +340,110 @@ const labelMap = {
* @param {string|number} status - 状态码
* @returns {string} 状态文本
*/
const getBillStatus = (row) => {
return row?.billStatus ?? row?.status ?? row?.statusEnum ?? row?.applyStatus;
};
const parseBillStatus = (status) => {
const statusMap = {
'0': '待签发',
'1': '已签发',
'2': '已校对',
'3': '待接收',
'4': '已收样',
'6': '已出报告',
'2': '已采证',
'3': '已送检',
'4': '已采证',
'5': '已送检',
'6': '报告已出',
'8': '报告已出',
'7': '已作废',
};
return statusMap[String(status)] || '-';
};
const getBillStatusTagType = (row) => {
const typeMap = {
'0': 'info',
'1': 'primary',
'2': 'primary',
'3': 'warning',
'4': 'primary',
'5': 'warning',
'6': 'success',
'7': 'danger',
'8': 'success',
};
return typeMap[String(getBillStatus(row))] || 'info';
};
const isPendingStatus = (row) => {
const status = getBillStatus(row);
return status === undefined || status === null || status === '' || String(status) === '0';
};
const isWithdrawableStatus = (row) => String(getBillStatus(row)) === '1';
const isReportStatus = (row) => ['6', '8'].includes(String(getBillStatus(row)));
/**
* 是否可管理该申请单:申请者本人或管理员
*/
const canManageRow = (row) => {
if (auth.hasRole('admin')) {
return true;
}
const currentPractitionerId = userStore.practitionerId;
const requesterId = row?.requesterId;
if (!currentPractitionerId || !requesterId) {
return false;
}
return String(currentPractitionerId) === String(requesterId);
};
const sortByCreateTimeDesc = (a, b) => {
const aTime = a?.createTime ? new Date(a.createTime).getTime() : 0;
const bTime = b?.createTime ? new Date(b.createTime).getTime() : 0;
return bTime - aTime;
};
const handleStatusClick = (row) => {
if (isReportStatus(row)) {
handleViewReport(row);
}
};
const pickReportUrl = (data, row) => {
if (!data) return '';
if (typeof data === 'string') return data;
const raw = data.records || data;
const list = Array.isArray(raw) ? raw : [raw];
const matched =
list.find((item) => {
const reportNo = item.busNo || item.reportNo || item.applyNo || item.prescriptionNo;
return reportNo && row.prescriptionNo && String(reportNo) === String(row.prescriptionNo);
}) || list[0];
return matched?.requestUrl || matched?.pdfUrl || matched?.reportUrl || matched?.url || '';
};
const handleViewReport = async (row) => {
try {
const res = await getProofResult({
encounterId: row.encounterId || patientInfo.value?.encounterId,
prescriptionNo: row.prescriptionNo,
});
if (res?.code === 200) {
const url = pickReportUrl(res.data, row);
if (url) {
window.open(url, '_blank');
return;
}
}
proxy.$modal?.msgWarning?.('暂未获取到检验报告链接');
} catch (e) {
proxy.$modal?.msgError?.(e.message || '获取检验报告失败');
}
};
/**
* 解析申请类型(优先级代码)
* @param {string} descJson - JSON字符串
@@ -443,7 +545,13 @@ const handleViewDetail = async (row) => {
if (row.descJson) {
try {
const obj = JSON.parse(row.descJson);
obj.targetDepartment = recursionFun(obj.targetDepartment);
// 将发往科室 ID 转换为名称
if (obj.targetDepartment) {
const deptName = recursionFun(obj.targetDepartment);
if (deptName) {
obj.targetDepartment = deptName;
}
}
// 转换申请类型编码为可读文本
if (obj.applicationType === 0) obj.applicationType = '普通';
else if (obj.applicationType === 1) obj.applicationType = '急诊';
@@ -462,12 +570,12 @@ const handleViewDetail = async (row) => {
* 修改检验申请单(待签发状态)
*/
const handleEdit = async (row) => {
// 确保科室数据已加载
if (!orgOptions.value || orgOptions.value.length === 0) {
await getLocationInfo();
}
editRowData.value = row;
editDialogVisible.value = true;
await nextTick();
editFormRef.value?.getList?.();
editFormRef.value?.getLocationInfo?.();
editFormRef.value?.getDiagnosisList?.();
};
/**
@@ -494,9 +602,9 @@ const submitEditForm = () => {
*/
const handleDelete = async (row) => {
try {
await proxy.$modal?.confirm?.(`确定要删除申请单 "${row.prescriptionNo}" 吗?此操作不可恢复。`);
await proxy.$modal?.confirm?.('确认作废该申请单吗?作废后不可撤销');
} catch {
return; // 用户取消
return;
}
try {
@@ -507,19 +615,21 @@ const handleDelete = async (row) => {
} else {
proxy.$modal?.msgError?.(res?.msg || '删除失败');
}
} catch (e) {
proxy.$modal?.msgError?.(e.message || '删除异常');
} catch {
// 响应拦截器已处理错误提示,此处静默
}
};
/**
* 撤回检验申请单(已签发状态撤回至待签发
* 撤回检验申请单(已签发且未采证状态撤回)
*/
const handleWithdraw = async (row) => {
try {
await proxy.$modal?.confirm?.(`确定要撤回申请单 "${row.prescriptionNo}" 吗?撤回后将恢复为待签发状态。`);
await proxy.$modal?.confirm?.(
'确认撤回该申请单吗?撤回后申请单及关联医嘱将恢复为待签发状态,护士站将同步更新。'
);
} catch {
return; // 用户取消
return;
}
try {
@@ -530,8 +640,8 @@ const handleWithdraw = async (row) => {
} else {
proxy.$modal?.msgError?.(res?.msg || '撤回失败');
}
} catch (e) {
proxy.$modal?.msgError?.(e.message || '撤回异常');
} catch {
// 响应拦截器已处理错误提示,此处静默
}
};
@@ -646,6 +756,14 @@ defineExpose({
animation: rotating 2s linear infinite;
}
.report-status-tag {
cursor: pointer;
background-color: #f0f9eb !important;
border-color: #67c23a !important;
color: #529b2e !important;
font-weight: 600;
}
@keyframes rotating {
0% {
transform: rotate(0deg);

View File

@@ -633,11 +633,11 @@ const calculateTotalAmount = () => {
nextTick(() => {
const row = props.row;
const qty = new Decimal(row.doseQuantity || 0);
const isMinUnit = row.unitCode == row.minUnitCode;
const price = isMinUnit ? row.minUnitPrice : row.unitPrice;
// 四舍五入到2位再算与页面显示的单价一致
// 根据首次用量单位类型决定使用哪个单价
const unitType = row.unitCodeList?.find((k) => k.value == row.doseUnitCode)?.type;
const price = unitType == 'unit' ? row.unitPrice : row.minUnitPrice;
const roundedPrice = new Decimal(price || 0).toDecimalPlaces(2, Decimal.ROUND_HALF_UP);
row.totalPrice = qty.mul(roundedPrice).toFixed(6);
row.totalPrice = qty.mul(roundedPrice).toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toString();
});
};
const setInputRef = props.handlers.setInputRef;

View File

@@ -24,22 +24,20 @@
</el-col> -->
<el-col :span="12">
<el-form-item label="发往科室" prop="targetDepartment" style="width: 100%">
<!-- <el-input v-model="form.targetDepartment" autocomplete="off" /> -->
<el-tree-select
clearable
style="width: 100%"
<el-select
v-model="form.targetDepartment"
filterable
:data="orgOptions"
:props="{
value: 'id',
label: 'name',
children: 'children',
}"
value-key="id"
check-strictly
clearable
placeholder="请选择科室"
/>
style="width: 100%"
>
<el-option
v-for="opt in flatOrgOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -78,18 +76,33 @@
</div>
</template>
<script setup name="BloodTransfusion">
import {getCurrentInstance, onBeforeMount, onMounted, reactive, ref} from 'vue';
import {computed, getCurrentInstance, nextTick, onBeforeMount, onMounted, reactive, ref, watch} from 'vue';
import {ElMessage} from 'element-plus';
import {patientInfo} from '../../../store/patient.js';
import {getDepartmentList} from '@/api/public.js';
import request from '@/utils/request';
import {getDiagnosisTreatmentOne} from '@/views/catalog/diagnosistreatment/components/diagnosistreatment';
import {getEncounterDiagnosis} from '../../api.js';
import {getApplicationList, saveBloodTransfusio} from './api';
const { proxy } = getCurrentInstance();
/** 科室树节点 id 统一为字符串,避免大整数精度丢失导致 tree-select 无法匹配 */
const normalizeOrgTreeIds = (nodes) => {
if (!Array.isArray(nodes)) return [];
return nodes.map((node) => ({
...node,
id: node.id != null ? String(node.id) : node.id,
children: node.children?.length ? normalizeOrgTreeIds(node.children) : undefined,
}));
};
// 递归查找树形科室节点
const findTreeItem = (list, id) => {
if (!list || list.length === 0) return null;
if (!list || list.length === 0 || id == null || id === '') return null;
const strId = String(id);
for (const item of list) {
if (item.id == id) return item;
if (String(item.id) === strId) return item;
if (item.children && item.children.length > 0) {
const found = findTreeItem(item.children, id);
if (found) return found;
@@ -97,11 +110,149 @@ const findTreeItem = (list, id) => {
}
return null;
};
/** 在科室树中解析 orgId兼容 Long 转 Number 后的精度丢失) */
const resolveOrgIdInTree = (rawOrgId) => {
if (rawOrgId == null || rawOrgId === '') return '';
const strOrgId = String(rawOrgId);
const findInTree = (nodes) => {
if (!nodes?.length) return null;
for (const node of nodes) {
if (String(node.id) === strOrgId) return String(node.id);
if (
typeof node.id === 'string' &&
node.id.length >= 16 &&
strOrgId.length >= 16 &&
node.id.substring(0, 15) === strOrgId.substring(0, 15)
) {
return String(node.id);
}
if (node.children?.length) {
const found = findInTree(node.children);
if (found) return found;
}
}
return null;
};
return findInTree(orgOptions.value) || strOrgId;
};
const resolveTargetDepartmentId = (rawId) => {
if (rawId == null || rawId === '') return '';
const resolved = resolveOrgIdInTree(rawId);
const node = findTreeItem(orgOptions.value, resolved);
return node ? String(node.id) : resolved;
};
/** 诊疗目录「所属科室」→ AdviceBaseDto.orgId */
const getBelongingOrgId = (item) => {
if (!item) return null;
return item.orgId ?? item.org_id ?? null;
};
/** 诊疗目录所属科室名称(字典翻译字段) */
const getBelongingOrgName = (item) => {
if (!item) return '';
return item.orgId_dictText || item.orgName || item.org_name || '';
};
/** 按机构 ID 拉取科室名称(树中无节点时兜底) */
const fetchOrgNameById = async (orgId) => {
if (orgId == null || orgId === '') return '';
const fromTree = findOrgName(orgId);
if (fromTree) return fromTree;
try {
const res = await request({
url: '/base-data-manage/organization/organization-getById',
method: 'get',
params: { orgId },
});
if (res.code === 200 && res.data?.name) {
return res.data.name;
}
} catch (e) {
console.error('查询科室名称失败', e);
}
return '';
};
/** 从机构树解析科室名称 */
const findOrgName = (orgId) => {
if (orgId == null || orgId === '') return '';
const node = findTreeItem(orgOptions.value, orgId);
if (node?.name) return node.name;
const resolved = resolveOrgIdInTree(orgId);
const resolvedNode = findTreeItem(orgOptions.value, resolved);
return resolvedNode?.name || '';
};
/** 自动填充时缓存的科室名称 */
const targetDepartmentName = ref('');
/** 扁平化科室选项,保证 el-select 能稳定显示 label */
const flatOrgOptions = computed(() => {
const options = [];
const seen = new Set();
const walk = (nodes) => {
for (const node of nodes || []) {
if (node?.id == null) continue;
const value = String(node.id);
if (seen.has(value)) continue;
seen.add(value);
options.push({ value, label: node.name || value });
if (node.children?.length) walk(node.children);
}
};
walk(orgOptions.value);
const curId = form.targetDepartment;
const curName = targetDepartmentName.value || findOrgName(curId);
if (curId && curName && !seen.has(String(curId))) {
options.unshift({ value: String(curId), label: curName });
}
return options;
});
/** 从诊疗目录详情补全所属科室(医嘱下拉接口不带 orgId_dictText */
const resolveProjectOrgInfo = async (item) => {
if (!item) return { orgId: null, orgName: '' };
let orgId = getBelongingOrgId(item);
let orgName = getBelongingOrgName(item);
if ((!orgId || !orgName) && item.adviceDefinitionId) {
try {
const res = await getDiagnosisTreatmentOne(item.adviceDefinitionId);
const detail = res?.data;
if (detail) {
orgId = orgId ?? detail.orgId ?? detail.org_id ?? null;
orgName = orgName || detail.orgId_dictText || detail.orgName || '';
}
} catch (e) {
console.error('查询诊疗目录所属科室失败', e);
}
}
if (orgId && !orgName) {
orgName = await fetchOrgNameById(orgId);
}
return { orgId, orgName };
};
/** 写入发往科室 */
const applyTargetDepartment = async (belongOrgId, nameHint = '') => {
if (belongOrgId == null || belongOrgId === '') {
form.targetDepartment = '';
targetDepartmentName.value = '';
return;
}
const resolvedDeptId = resolveTargetDepartmentId(belongOrgId);
const deptName =
nameHint || findOrgName(belongOrgId) || findOrgName(resolvedDeptId) || (await fetchOrgNameById(belongOrgId));
targetDepartmentName.value = deptName;
form.targetDepartment = resolvedDeptId;
};
const emits = defineEmits(['submitOk']);
const props = defineProps({});
const state = reactive({});
const applicationListAll = ref();
const applicationList = ref();
const applicationListAll = ref([]);
const applicationList = ref([]);
const loading = ref(false);
const orgOptions = ref([]); // 科室选项
const getList = () => {
@@ -118,29 +269,48 @@ const getList = () => {
adviceTypes: [3], //1 药品 2耗材 3诊疗
})
.then((res) => {
if (res.code === 200) {
applicationListAll.value = res.data.records;
applicationList.value = res.data.records.map((item) => {
if (res.code === 200 && Array.isArray(res.data?.records)) {
const records = res.data.records.filter((item) => item.adviceDefinitionId != null);
applicationListAll.value = records;
applicationList.value = records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
const id = item.adviceDefinitionId;
return {
adviceDefinitionId: item.adviceDefinitionId,
adviceDefinitionId: id,
orgId: item.orgId,
label: item.adviceName + ' (¥' + price + '/' + unit + ')',
key: item.adviceDefinitionId,
key: id,
};
});
} else {
proxy.$message.error(res.message);
proxy.$message.error(res.message || '加载输血项目失败');
applicationListAll.value = [];
applicationList.value = [];
}
})
.finally(() => {
loading.value = false;
if (transferValue.value.length > 0) {
nextTick(async () => {
const valid = await validateTransferOrgConsistency(transferValue.value);
if (valid) {
lastValidTransferValue.value = [...transferValue.value];
fillTargetDepartmentFromSelection(transferValue.value, 1);
} else {
transferValue.value = [];
lastValidTransferValue.value = [];
}
});
}
});
};
const transferValue = ref([]);
/** 上一次通过校验的已选项目(科室不一致时回滚到此状态) */
const lastValidTransferValue = ref([]);
const isRevertingTransfer = ref(false);
let transferValidateSeq = 0;
const form = reactive({
// categoryType: '', // 项目类别
targetDepartment: '', // 发往科室
@@ -157,86 +327,140 @@ const rules = reactive({});
onBeforeMount(() => {});
onMounted(() => {
getList();
getLocationInfo();
});
const collectSelectedProjects = (selectProjectIds) => {
return (selectProjectIds || [])
.map((element) =>
applicationListAll.value.find((item) => String(item.adviceDefinitionId) === String(element))
)
.filter(Boolean);
};
/** 校验已选项目的所属科室是否一致(超过 1 项时才校验) */
const validateTransferOrgConsistency = async (selectProjectIds) => {
const arr = collectSelectedProjects(selectProjectIds);
if (arr.length <= 1) {
return true;
}
const orgInfoList = await Promise.all(arr.map((item) => resolveProjectOrgInfo(item)));
const firstOrgId = orgInfoList[0]?.orgId;
return orgInfoList.every((info) => String(info?.orgId ?? '') === String(firstOrgId ?? ''));
};
/**
* type(1watch监听类型 2:点击保存类型)
* selectProjectIds(选中项目的id数组)
* */
const projectWithDepartment = (selectProjectIds, type) => {
//1.获取选中的项目 2.判断项目的执行科室是否相同 3.判断执行科室是否配置 4.将项目的执行科室复值到执行科室下拉选位置
let isRelease = true;
// 选中项目的数组
const arr = [];
// 根据选中的项目id查找对应的项目
selectProjectIds.forEach((element) => {
const searchData = applicationList.value.find((item) => {
return element == item.adviceDefinitionId;
});
arr.push(searchData);
});
// 清空科室
form.targetDepartment = '';
if (arr.length > 0) {
const obj = arr[0];
// 判断科室是否相同
const isCompare = arr.every((item) => {
return item.orgId == obj.orgId;
});
if (!isCompare) {
ElMessage({
type: 'error',
message: '执行科室不同',
});
isRelease = false;
}
// 选中项目中的执行科室id与全部科室数据做匹配
const findItem = findTreeItem(orgOptions.value, obj.orgId);
*/
const fillTargetDepartmentFromSelection = async (selectProjectIds, type) => {
const manualDept = type === 2 && form.targetDepartment ? form.targetDepartment : '';
const arr = collectSelectedProjects(selectProjectIds);
if (!findItem) {
isRelease = false;
ElMessage({
type: 'error',
message: '未找到项目执行的科室',
});
}
if (type == 1) {
if (isRelease) {
form.targetDepartment = findItem.id;
}
if (arr.length === 0) {
// 项目列表尚未加载完时,已选 ID 存在则先不清空(避免误清发往科室)
if ((selectProjectIds || []).length > 0 && applicationListAll.value.length === 0) {
return type === 2 ? !!manualDept : true;
}
form.targetDepartment = '';
targetDepartmentName.value = '';
return type === 2 ? !!manualDept : true;
}
return isRelease;
const orgInfoList = await Promise.all(arr.map((item) => resolveProjectOrgInfo(item)));
const firstOrg = orgInfoList[0];
const belongOrgId = firstOrg?.orgId;
const allSameOrg = orgInfoList.every((info) => String(info?.orgId ?? '') === String(belongOrgId ?? ''));
if (!allSameOrg) {
if (type === 2) {
ElMessage.error('所选项目的所属科室不一致,请分开申请');
}
return false;
}
if (belongOrgId == null || belongOrgId === '') {
if (type === 2 && manualDept) {
await applyTargetDepartment(manualDept, findOrgName(manualDept));
return true;
}
if (type === 2) {
ElMessage.warning('所选项目未在诊疗目录配置所属科室,请手动选择发往科室');
return false;
}
form.targetDepartment = '';
targetDepartmentName.value = '';
return true;
}
if (type === 2 && manualDept) {
await applyTargetDepartment(manualDept, findOrgName(manualDept));
return true;
}
await applyTargetDepartment(belongOrgId, firstOrg?.orgName || '');
return true;
};
// 监听选择项目变化
// 选中项目:先校验所属科室一致,不通过则回滚穿梭框,不允许进入「已选择」
watch(
() => transferValue.value,
(newValue) => {
projectWithDepartment(newValue, 1);
async (newValue) => {
if (isRevertingTransfer.value) return;
const seq = ++transferValidateSeq;
const valid = await validateTransferOrgConsistency(newValue);
if (seq !== transferValidateSeq) return;
if (!valid) {
ElMessage.error('所选项目的所属科室不一致,请分开申请');
isRevertingTransfer.value = true;
transferValue.value = [...lastValidTransferValue.value];
await nextTick();
isRevertingTransfer.value = false;
return;
}
lastValidTransferValue.value = [...newValue];
await fillTargetDepartmentFromSelection(newValue, 1);
}
);
const submit = () => {
watch(
() => orgOptions.value,
() => {
if (transferValue.value.length > 0) {
nextTick(() => {
fillTargetDepartmentFromSelection(transferValue.value, 1);
});
}
},
{ deep: true }
);
const submit = async () => {
if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单');
}
if (!projectWithDepartment(transferValue.value, 2)) {
if (!(await fillTargetDepartmentFromSelection(transferValue.value, 2))) {
return;
}
if (!form.targetDepartment) {
return proxy.$message.error('请选择发往科室');
}
let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId);
return transferValue.value.some((id) => String(id) === String(item.adviceDefinitionId));
});
applicationListAllFilter = applicationListAllFilter.map((item) => {
const priceInfo = item.priceList?.[0] || {};
return {
adviceDefinitionId: item.adviceDefinitionId /** 诊疗定义id */,
quantity: 1, // /** 请求数量 */
unitCode: item.priceList[0].unitCode /** 请求单位编码 */,
unitPrice: item.priceList[0].price /** 单价 */,
totalPrice: item.priceList[0].price /** 总价 */,
positionId: item.positionId, //执行科室id
unitCode: priceInfo.unitCode /** 请求单位编码 */,
unitPrice: priceInfo.price /** 单价 */,
totalPrice: priceInfo.price /** 总价 */,
positionId: form.targetDepartment || item.positionId, //执行科室id
ybClassEnum: item.ybClassEnum, //类别医保编码
conditionId: item.conditionId, //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id
adviceType: item.adviceType, ///** 医嘱类型 */
definitionId: item.priceList[0].definitionId, //费用定价主表ID */
definitionId: priceInfo.definitionId, //费用定价主表ID */
definitionDetailId: item.definitionDetailId, //费用定价子表ID */
accountId: patientInfo.value.accountId, // // 账户id
};
@@ -254,16 +478,22 @@ const submit = () => {
if (res.code === 200) {
proxy.$message.success(res.msg);
applicationList.value = [];
applicationListAll.value = [];
transferValue.value = [];
lastValidTransferValue.value = [];
emits('submitOk');
} else {
proxy.$message.error(res.message);
}
});
};
/** 查询科室 */
/** 查询科室(与检验申请单一致) */
const getLocationInfo = () => {
getDepartmentList().then((res) => {
orgOptions.value = res.data || [];
return getDepartmentList().then((res) => {
orgOptions.value = normalizeOrgTreeIds(res?.data || []);
if (transferValue.value.length > 0) {
nextTick(() => fillTargetDepartmentFromSelection(transferValue.value, 1));
}
});
};
// 获取诊断目录
@@ -300,7 +530,7 @@ function getDiagnosisList() {
}
});
}
defineExpose({ state, submit, getLocationInfo, getDiagnosisList });
defineExpose({ state, submit, getLocationInfo, getDiagnosisList, getList });
</script>
<style lang="scss" scoped>
.bloodTransfusion-container {
@@ -312,8 +542,22 @@ defineExpose({ state, submit, getLocationInfo, getDiagnosisList });
min-height: 300px;
}
.el-transfer {
:deep(.el-transfer) {
--el-transfer-panel-width: 480px !important;
display: flex !important;
flex-direction: row !important;
}
:deep(.el-transfer__buttons) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 4px;
}
:deep(.el-transfer__button) {
margin: 4px 0;
}
.bloodTransfusion-form {

View File

@@ -17,17 +17,14 @@
style="width: 300px; margin-bottom: 10px"
>
<template #append>
<el-button @click="handleSearch">搜索</el-button>
<el-button @click="handleSearch" :loading="loading">搜索</el-button>
</template>
</el-input>
<span v-if="!searchKey" class="total-count"> {{ totalCount }} </span>
<span v-else class="total-count">搜索到 {{ filteredCount }} / {{ totalCount }} </span>
<span class="total-count"> {{ totalCount }} </span>
</div>
<el-transfer
v-model="transferValue"
:data="transferData"
filter-placeholder="项目代码/名称"
filterable
:titles="['未选择', '已选择']"
/>
</div>
@@ -134,10 +131,11 @@
</div>
</template>
<script setup name="LaboratoryTests">
import {getCurrentInstance, onMounted, reactive, ref, watch, computed} from 'vue';
import {getCurrentInstance, nextTick, onMounted, reactive, ref, watch, computed} from 'vue';
import {patientInfo} from '../../../store/patient.js';
import {getApplicationList, saveInspection} from './api';
import {getOrgList} from '@/views/doctorstation/components/api.js';
import {getExaminationPage, saveInspection} from './api';
import {ActivityCategory} from '@/utils/medicalConstants';
import {getDepartmentList} from '@/api/public.js';
import {getEncounterDiagnosis} from '../../api.js';
import {ElMessage} from 'element-plus';
@@ -168,13 +166,13 @@ const loading = ref(false);
const orgOptions = ref([]);
const searchKey = ref('');
const totalCount = ref(0);
const skipDeptAutoFill = ref(false);
// 将已加载的全部数据转为 transfer 组件所需的格式
const buildTransferData = (records) => {
return records.map((item) => {
const priceInfo = item.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = item.unitCode_dictText || item.unitCode || '';
const price = item.price != null ? Number(item.price).toFixed(2) : '0.00';
const unit = item.unitCodeDictText || item.unitCode || '';
return {
adviceDefinitionId: item.adviceDefinitionId,
orgId: item.orgId,
@@ -184,7 +182,8 @@ const buildTransferData = (records) => {
});
};
// 加载全部数据(不分页,一次性拉取)
const selectedItemsCache = ref(new Map());
const loadAllData = async () => {
if (!patientInfo.value?.inHospitalOrgId) {
applicationListAll.value = [];
@@ -192,13 +191,12 @@ const loadAllData = async () => {
}
loading.value = true;
try {
// 使用大 pageSize 一次性拉取所有启用状态的检验类诊疗项目
const res = await getApplicationList({
pageSize: 9999,
const res = await getExaminationPage({
pageSize: 100,
pageNo: 1,
categoryCode: '22',
categoryCode: ActivityCategory.PROOF,
organizationId: patientInfo.value.inHospitalOrgId,
adviceTypes: [3], // 1 药品 2 耗材 3 诊疗
searchKey: searchKey.value,
});
if (res.code !== 200) {
proxy.$message.error(res.message);
@@ -207,8 +205,9 @@ const loadAllData = async () => {
}
applicationListAll.value = res.data?.records || [];
totalCount.value = res.data?.total || 0;
// 检验项目列表为异步加载,编辑回显必须在数据就绪后执行,否则已选区一直为空
applyEditTransferSelection()
if (!searchKey.value) {
applyEditTransferSelection();
}
} catch (e) {
proxy.$message.error('获取检验项目列表失败');
applicationListAll.value = [];
@@ -217,32 +216,18 @@ const loadAllData = async () => {
}
};
// 根据搜索关键词过滤数据
const filterData = (key) => {
if (!key || key.trim() === '') {
return applicationListAll.value;
}
const lowerKey = key.toLowerCase().trim();
return applicationListAll.value.filter((item) => {
return (
item.adviceName?.toLowerCase().includes(lowerKey) ||
item.pyStr?.toLowerCase().includes(lowerKey) ||
item.adviceBusNo?.toLowerCase().includes(lowerKey)
);
});
};
// transfer 组件实际显示的数据(受搜索词影响)
const transferData = computed(() => buildTransferData(filterData(searchKey.value)));
// 当前显示的条数
const filteredCount = computed(() => filterData(searchKey.value).length);
const transferData = computed(() => buildTransferData(applicationListAll.value));
const getList = async () => {
await loadAllData();
};
let searchTimer = null;
const handleSearch = () => {
// 搜索时保持已选中的项目不受影响
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
loadAllData();
}, 300);
};
// 编辑初始化标志:避免 applyEditTransferSelection 设置 transferValue 时触发 projectWithDepartment 覆盖 descJson 中的科室值
const isInitializing = ref(false);
@@ -263,7 +248,31 @@ const form = reactive({
otherDiagnosisList: [], //其他断目录
});
const rules = reactive({});
const normalizeOrgTreeIds = (nodes) => {
if (!Array.isArray(nodes)) return [];
return nodes.map((node) => ({
...node,
id: node.id != null ? String(node.id) : node.id,
children: node.children?.length ? normalizeOrgTreeIds(node.children) : undefined,
}));
};
const resolveTargetDepartmentId = (rawId) => {
if (rawId == null || rawId === '') return '';
const node = findTreeItem(orgOptions.value, rawId);
return node ? String(node.id) : String(rawId);
};
const applyTargetDepartmentEcho = () => {
if (form.targetDepartment) {
form.targetDepartment = resolveTargetDepartmentId(form.targetDepartment);
}
};
onMounted(() => {
getLocationInfo();
getDiagnosisList();
getList();
});
/**
@@ -277,13 +286,17 @@ const projectWithDepartment = (selectProjectIds, type) => {
const arr = [];
// 根据选中的项目id查找对应的项目从全部原始数据中查找
selectProjectIds.forEach((element) => {
const searchData = applicationListAll.value.find((item) => {
let searchData = applicationListAll.value.find((item) => {
return element == item.adviceDefinitionId;
});
if (!searchData) {
searchData = selectedItemsCache.value.get(element);
}
if (searchData) {
const priceInfo = searchData.priceList?.[0] || {};
const price = priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = searchData.unitCode_dictText || searchData.unitCode || '';
const price = searchData.price != null ? Number(searchData.price).toFixed(2)
: priceInfo.price != null ? Number(priceInfo.price).toFixed(2) : '0.00';
const unit = searchData.unitCodeDictText || searchData.unitCode_dictText || searchData.unitCode || '';
arr.push({
adviceDefinitionId: searchData.adviceDefinitionId,
orgId: searchData.orgId,
@@ -292,8 +305,9 @@ const projectWithDepartment = (selectProjectIds, type) => {
});
}
});
// 保存用户手动选择的发往科室(提交时需要保留)
const manualDept = type === 2 ? form.targetDepartment : '';
// 保存用户手动选择/回显的发往科室(提交、编辑回显时需要保留)
const manualDept =
type === 2 || (isEditMode.value && form.targetDepartment) ? form.targetDepartment : '';
// 清空科室
form.targetDepartment = '';
if (arr.length > 0) {
@@ -313,8 +327,8 @@ const projectWithDepartment = (selectProjectIds, type) => {
const findItem = findTreeItem(orgOptions.value, obj.orgId);
if (!findItem) {
// type=2(提交)时,若用户已手动选择发往科室,则允许提交
if (type === 2 && manualDept) {
form.targetDepartment = manualDept;
if ((type === 2 || isEditMode.value) && manualDept) {
form.targetDepartment = resolveTargetDepartmentId(manualDept);
isRelease = true;
} else if (type === 2 && !manualDept) {
// 提交时用户未手动选择科室,才提示错误
@@ -330,10 +344,10 @@ const projectWithDepartment = (selectProjectIds, type) => {
}
if (findItem && isRelease) {
// 提交时若用户已选「发往科室」,不得用项目默认执行科室覆盖
if (type === 2 && manualDept) {
form.targetDepartment = manualDept;
if ((type === 2 || isEditMode.value) && manualDept) {
form.targetDepartment = resolveTargetDepartmentId(manualDept);
} else {
form.targetDepartment = findItem.id;
form.targetDepartment = String(findItem.id);
}
}
}
@@ -343,7 +357,14 @@ const projectWithDepartment = (selectProjectIds, type) => {
watch(
() => transferValue.value,
(newValue) => {
if (skipDeptAutoFill.value) return;
if (isInitializing.value) return;
newValue.forEach((id) => {
if (!selectedItemsCache.value.has(id)) {
const item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
if (item) selectedItemsCache.value.set(id, item);
}
});
projectWithDepartment(newValue, 1);
}
);
@@ -382,7 +403,11 @@ const applyEditTransferSelection = () => {
const uniq = [...new Set(selectedIds)]
// 设置初始化标志,防止 transferValue 变化触发 projectWithDepartment 覆盖 descJson 中的科室值
isInitializing.value = true
skipDeptAutoFill.value = true
transferValue.value = uniq
nextTick(() => {
skipDeptAutoFill.value = false
})
isInitializing.value = false
if (newData.requestFormDetailList.length && uniq.length === 0) {
console.warn(
@@ -406,6 +431,7 @@ watch(
form[key] = obj[key]
}
})
applyTargetDepartmentEcho()
} catch (e) {
console.error('解析 descJson 失败:', e)
}
@@ -416,13 +442,21 @@ watch(
{ immediate: true, deep: true }
)
// 编辑模式下applicationListAll 加载完成后重新回显已选项目
watch(
() => orgOptions.value,
() => {
applyTargetDepartmentEcho()
}
)
// 编辑模式下,项目字典首次加载完成后回显已选项目(搜索刷新不重置)
watch(
() => applicationListAll.value,
() => {
if (!props.editData?.requestFormId) return;
if (!props.editData.requestFormDetailList?.length) return;
if (!applicationListAll.value.length) return;
if (searchKey.value) return;
const selectedIds = [];
props.editData.requestFormDetailList.forEach((detail) => {
@@ -436,6 +470,7 @@ watch(
isInitializing.value = true;
transferValue.value = selectedIds;
isInitializing.value = false;
applyEditTransferSelection();
}
);
@@ -446,26 +481,29 @@ const submit = () => {
if (!projectWithDepartment(transferValue.value, 2)) {
return;
}
let applicationListAllFilter = applicationListAll.value.filter((item) => {
return transferValue.value.includes(item.adviceDefinitionId);
});
applicationListAllFilter = applicationListAllFilter.map((item) => {
let applicationListAllFilter = transferValue.value.map((id) => {
let item = applicationListAll.value.find((i) => i.adviceDefinitionId == id);
if (!item) {
item = selectedItemsCache.value.get(id);
}
if (!item) return null;
const priceInfo = item.priceList?.[0] || {};
return {
adviceDefinitionId: item.adviceDefinitionId /** 诊疗定义id */,
quantity: 1, // /** 请求数量 */
unitCode: item.priceList[0].unitCode /** 请求单位编码 */,
unitPrice: item.priceList[0].price /** 单价 */,
totalPrice: item.priceList[0].price /** 总价 */,
unitCode: item.unitCode || priceInfo.unitCode || '' /** 请求单位编码 */,
unitPrice: item.price ?? priceInfo.price ?? 0 /** 单价 */,
totalPrice: item.price ?? priceInfo.price ?? 0 /** 总价 */,
positionId: form.targetDepartment || item.positionId, // 用户指定发往科室优先于项目默认执行科室
ybClassEnum: item.ybClassEnum, //类别医保编码
conditionId: item.conditionId, //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId, //就诊诊断id
adviceType: item.adviceType, ///** 医嘱类型 */
definitionId: item.priceList[0].definitionId, //费用定价主表ID */
definitionDetailId: item.definitionDetailId, //费用定价子表ID */
ybClassEnum: item.ybClassEnum || '', //类别医保编码
conditionId: item.conditionId || '', //诊断ID
encounterDiagnosisId: item.encounterDiagnosisId || '', //就诊诊断id
adviceType: item.adviceType || 3, ///** 医嘱类型 */
definitionId: item.chargeItemDefinitionId || priceInfo.definitionId || '', //费用定价主表ID */
definitionDetailId: item.definitionDetailId || priceInfo.definitionDetailId || '', //费用定价子表ID */
accountId: patientInfo.value.accountId, // // 账户id
};
});
}).filter(Boolean);
const params = {
activityList: applicationListAllFilter,
patientId: patientInfo.value.patientId, //患者ID
@@ -480,6 +518,7 @@ const submit = () => {
if (res.code === 200) {
proxy.$message.success(isEditMode.value ? '修改成功' : res.msg);
transferValue.value = [];
selectedItemsCache.value.clear();
emits('submitOk');
} else {
proxy.$message.error(res.message);
@@ -488,9 +527,9 @@ const submit = () => {
};
/** 查询科室 */
const getLocationInfo = () => {
getOrgList().then((res) => {
orgOptions.value = res.data.records;
console.log('科室========>', JSON.stringify(orgOptions.value));
return getDepartmentList().then((res) => {
orgOptions.value = normalizeOrgTreeIds(res.data || []);
applyTargetDepartmentEcho();
});
};
// 获取诊断目录

View File

@@ -207,6 +207,7 @@ import {patientInfo} from '../../../store/patient.js';
import {getDepartmentList} from '@/api/public.js';
import {getEncounterDiagnosis} from '../../api.js';
import {getExaminationPage, saveCheckd} from './api';
import {ActivityCategory} from '@/utils/medicalConstants';
import {ElMessage, ElMessageBox} from 'element-plus';
import {WarningFilled, Warning, Refresh, Files, Document, EditPen, Aim, DocumentCopy} from '@element-plus/icons-vue';
@@ -276,6 +277,7 @@ const getList = () => {
pageNo: 1,
pageSize: 5000,
searchKey: '',
categoryCode: ActivityCategory.TEST,
})
.then((res) => {
if (res.code === 200 && res.data?.records) {
@@ -428,11 +430,52 @@ const loadEditData = () => {
const projectWithDepartment = (selectProjectIds) => {
if (!selectProjectIds || selectProjectIds.length === 0) {
form.targetDepartment = '';
return;
}
// 获取第一个选中项目的发往科室orgId
// 优先使用配置的发往科室,如果没有则保留手动选择
const selectedProject = applicationListAll.value?.find(
item => selectProjectIds.includes(item.adviceDefinitionId)
);
if (selectedProject && selectedProject.orgId) {
// 项目配置了发往科室,自动填充
const orgId = selectedProject.orgId;
const orgName = selectedProject.orgName;
// 查找树中对应的节点,获取正确的 id 类型
const findNode = (nodes, targetId) => {
if (!nodes) return null;
for (const node of nodes) {
if (String(node.id) === String(targetId)) {
return node;
}
if (node.children && node.children.length > 0) {
const found = findNode(node.children, targetId);
if (found) return found;
}
}
return null;
};
const treeNode = findNode(orgOptions.value, orgId);
if (treeNode) {
// 使用树节点的原始 id 值(确保类型匹配)
form.targetDepartment = treeNode.id;
} else {
// 科室不在列表中(可能已删除),留空让用户手动选择
form.targetDepartment = '';
}
}
// 如果没有配置发往科室,保留手动选择(不修改 form.targetDepartment
};
watch(() => transferValue.value, (newValue) => {
projectWithDepartment(newValue);
// 使用 nextTick 确保 DOM 更新完成后再设置值
nextTick(() => {
projectWithDepartment(newValue);
});
});
const getPriorityCode = () => {

View File

@@ -198,7 +198,7 @@
v-model="scope.row.adviceName"
placeholder="请选择项目"
@input="handleChange"
@click="handleFocus(scope.row, scope.$index)"
@focus="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown="
(e) => {
@@ -429,6 +429,8 @@ const props = defineProps({
});
const isAdding = ref(false);
const isSaving = ref(false);
// 标记双击编辑的是否为已有数据的行(用于保存后是否自动添加下一行)
const wasDoubleClickEdit = ref(false);
const prescriptionRef = ref();
const expandOrder = ref([]); //目前的展开行
const stockList = ref([]);
@@ -638,6 +640,10 @@ function getListInfo(addNewRow) {
};
})
.sort((a, b) => {
// 没有 requestTime 的项(新增/组套添加)排在最前面
if (!a.requestTime && !b.requestTime) return 0;
if (!a.requestTime) return -1;
if (!b.requestTime) return 1;
return new Date(b.requestTime) - new Date(a.requestTime);
});
getGroupMarkers(); // 更新标记
@@ -804,7 +810,7 @@ function checkUnit(item, row) {
}
}
// 行双击打开编辑块"待保存"和"待签发"均可编辑
// 行双击打开编辑块待保存待签发医嘱均可编辑;已签发/已完成/停止不允许编辑
function clickRowDb(row, column, event) {
// 检查点击的是否是复选框
if (event && event.target.closest('.el-checkbox')) {
@@ -815,14 +821,18 @@ function clickRowDb(row, column, event) {
return;
}
row.showPopover = false;
// statusEnum == 1 包含"待保存(无requestId)"和"待签发(有requestId)",均允许编辑
if (row.statusEnum == 1) {
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
row.therapyEnum = String(row.therapyEnum ?? '1');
row.isEdit = true;
const index = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey);
prescriptionList.value[index] = row;
rowIndex.value = index;
if (index !== -1) {
prescriptionList.value[index] = row;
}
expandOrder.value = [row.uniqueKey];
} else {
proxy.$modal.msgWarning('仅待保存或待签发医嘱允许编辑');
}
}
@@ -890,31 +900,21 @@ function handleDiagnosisChange(item) {
function handleFocus(row, index) {
rowIndex.value = index;
row.showPopover = true;
// Bug #555: handleFocus 只负责开 popover 和初始化查询参数,搜索由 handleChange 统一处理
// 避免异步 refresh 用旧闭包 searchKey 覆盖 handleChange 的搜索结果
const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
// 用 adviceType + categoryCode 组合查找匹配的选项
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);
// If the row has an explicit adviceType (saved/existing row), use its own categoryCode.
// If no type is selected (new row), use empty string for global search across all categories.
const categoryCode = selectedItem ? selectedItem.categoryCode : (row.adviceType != null ? (row.categoryCode || '') : '');
const searchKey = row.adviceName || '';
nextTick(() => {
nextTick(() => {
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef && tableRef.refresh) {
tableRef.refresh(adviceType, categoryCode, searchKey);
} else {
// fallback: 如果双重 nextTick 仍未挂载,延迟 100ms 再试
setTimeout(() => {
const tableRef2 = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef2 && tableRef2.refresh) {
tableRef2.refresh(adviceType, categoryCode, searchKey);
}
}, 100);
}
});
});
let categoryCode = '';
if (row.adviceType !== undefined) {
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);
categoryCode = selectedItem ? selectedItem.categoryCode : (row.categoryCode || '');
}
adviceQueryParams.value = { adviceType, categoryCode, searchKey: '' };
// handleFocus 打开 popover 时也要加载数据
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef && tableRef.refresh) {
tableRef.refresh(adviceType, categoryCode, '');
}
}
function handleBlur(row) {
@@ -923,20 +923,24 @@ function handleBlur(row) {
function handleChange(value) {
adviceQueryParams.value.searchKey = value;
// 搜索词变化时,调用当前行子组件的 refresh 方法
const index = rowIndex.value;
if (index >= 0) {
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
if (tableRef && tableRef.refresh) {
const row = filterPrescriptionList.value[index];
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
// 用 adviceType + categoryCode 组合查找匹配的选项
// @focus 已先于 @input 执行rowIndex 必定有效
const currentIndex = rowIndex.value;
if (currentIndex < 0) return;
const row = filterPrescriptionList.value[currentIndex];
// popover 被 blur 关闭后,用户继续输入时自行打开
if (!row.showPopover) {
row.showPopover = true;
}
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[currentIndex] : adviceTableRef.value;
if (tableRef && tableRef.refresh) {
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
let categoryCode = '';
if (row?.adviceType !== undefined) {
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);
// 修复Bug #486当行没有显式选择医嘱类型时不传categoryCode让搜索在全药库中进行
const categoryCode = selectedItem ? selectedItem.categoryCode : (row?.adviceType !== undefined ? (adviceQueryParams.value.categoryCode || '') : '');
tableRef.refresh(adviceType, categoryCode, value);
categoryCode = selectedItem ? selectedItem.categoryCode : (adviceQueryParams.value.categoryCode || '');
}
tableRef.refresh(adviceType, categoryCode, value);
}
}
@@ -1393,7 +1397,9 @@ function handleSaveSign(row, index) {
}
});
} else {
if (prescriptionList.value[0].adviceName) {
// 仅通过【新增】按钮创建的医嘱保存后才自动添加下一行空医嘱
// 双击编辑已有"待保存"医嘱保存时,不应自动添加空行
if (isAdding.value && prescriptionList.value[0].adviceName) {
handleAddPrescription();
}
}
@@ -1571,11 +1577,24 @@ function handleSaveGroup(orderGroupList) {
let successCount = 0;
// 收集所有要添加的新行,最后统一 unshift 到数组开头(置顶显示)
const newRows = [];
// 记录循环前的数组长度,用于清理循环中创建的临时行
const originalLength = prescriptionList.value.length;
orderGroupList.forEach((item) => {
rowIndex.value = prescriptionList.value.length;
// 使用临时索引,先追加到末尾用于 setValue 填充
const tempIndex = prescriptionList.value.length;
prescriptionList.value[tempIndex] = {
uniqueKey: nextId.value++,
isEdit: false,
statusEnum: 1,
};
if (!item) {
console.warn('组套中的项目为空');
prescriptionList.value.splice(tempIndex, 1);
return;
}
@@ -1601,18 +1620,12 @@ function handleSaveGroup(orderGroupList) {
therapyEnum: item.orderDetailInfos?.therapyEnum || '1',
};
// 预初始化空行(组套项带预填值,设为 false 让明细字段在表格中直接展示)
prescriptionList.value[rowIndex.value] = {
uniqueKey: nextId.value++,
isEdit: false,
statusEnum: 1,
};
rowIndex.value = tempIndex;
setValue(mergedDetail);
// 创建新的处方项目
const newRow = {
...prescriptionList.value[rowIndex.value],
...prescriptionList.value[tempIndex],
patientId: patientInfo.value.patientId,
encounterId: patientInfo.value.encounterId,
accountId: accountId.value,
@@ -1631,12 +1644,12 @@ function handleSaveGroup(orderGroupList) {
orgId: resolveOrgId(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || '',
// 🔧 修复:同时存储 orgName确保树匹配不到时仍有中文名称可显示
orgName: findOrgName(mergedDetail.orgId || patientInfo.value?.inHospitalOrgId) || mergedDetail.orgName || patientInfo.value?.inHospitalOrgName || '',
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1',
dbOpType: prescriptionList.value[tempIndex].requestId ? '2' : '1',
conditionId: conditionId.value,
conditionDefinitionId: conditionDefinitionId.value,
encounterDiagnosisId: encounterDiagnosisId.value,
diagnosisName: diagnosisName.value,
therapyEnum: prescriptionList.value[rowIndex.value]?.therapyEnum || mergedDetail.therapyEnum || '1',
therapyEnum: prescriptionList.value[tempIndex]?.therapyEnum || mergedDetail.therapyEnum || '1',
// 🔧 修复:确保组套医嘱的 categoryEnum 被正确映射,防止后端 NPE
categoryEnum: mergedDetail?.categoryEnum || mergedDetail?.categoryCode || item?.categoryCode,
};
@@ -1655,11 +1668,14 @@ function handleSaveGroup(orderGroupList) {
}
newRow.contentJson = JSON.stringify(newRow);
prescriptionList.value[rowIndex.value] = newRow;
newRows.push(newRow);
successCount++;
});
if (successCount > 0) {
// 清理循环中创建的临时行,统一添加到数组开头(置顶显示)
if (newRows.length > 0) {
prescriptionList.value.splice(originalLength); // 移除循环中追加到末尾的临时行
prescriptionList.value.unshift(...newRows);
proxy.$modal.msgSuccess(`成功添加 ${successCount} 个医嘱项`);
}
}

View File

@@ -46,7 +46,8 @@
<div style="display: flex; gap: 20px; height: 70vh">
<div
style="
width: 250px;
width: 350px;
min-width: 350px;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
@@ -70,21 +71,35 @@
<span class="status-dot"></span>
{{ getItemType_Text(item.adviceType) }}
</div>
<div class="item-name">{{ item.adviceName }}</div>
<div class="item-name">
{{
<el-tooltip :content="item.adviceName" placement="top" :show-after="500">
<div class="item-name">{{ item.adviceName }}</div>
</el-tooltip>
<el-tooltip
:content="
item.priceList && item.priceList.length > 0
? (item.priceList[0].price / item.partPercent).toFixed(2) +
'元' +
'/' +
item.minUnitCode_dictText
? (item.priceList[0].price / item.partPercent).toFixed(2) + '元/' + item.minUnitCode_dictText
: ''
}}
</div>
<div class="item-name" v-if="item.adviceType === 2">
库存数量
{{ handleQuantity(item) }}
</div>
"
placement="top"
:show-after="500"
>
<div class="item-name">
{{
item.priceList && item.priceList.length > 0
? (item.priceList[0].price / item.partPercent).toFixed(2) +
'元' +
'/' +
item.minUnitCode_dictText
: ''
}}
</div>
</el-tooltip>
<el-tooltip v-if="item.adviceType === 2" :content="'库存数量:' + handleQuantity(item)" placement="top" :show-after="500">
<div class="item-name">
库存数量
{{ handleQuantity(item) }}
</div>
</el-tooltip>
</div>
<!-- 只显示暂无数据文本 -->
<div
@@ -308,7 +323,7 @@
import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from 'vue';
import {ElMessage} from 'element-plus';
import {formatDateStr} from '@/utils/index';
import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList} from './api.js';
import {getAdviceBaseInfo, getDiseaseTreatmentInitLoc, getOrgList, getOrgLocConfig} from './api.js';
import {getOrderGroup} from '@/views/doctorstation/components/api.js';
import useUserStore from '@/store/modules/user';
@@ -366,6 +381,7 @@ const executeTime = ref('');
const departmentOptions = ref([]);
const AdviceBaseInfoList = ref([]);
const locationOptions = ref([]);
const consumableDefaultLocId = ref(null); // 患者科室耗材默认库房ID来自取药科室配置
const searchText = ref('');
const userId = ref('');
const orgId = ref('');
@@ -471,11 +487,8 @@ onMounted(() => {
const userStore = useUserStore();
userId.value = userStore.id;
orgId.value = userStore.orgId;
console.log(props.patientInfo, 'patientInfo in FeeDialog');
console.log('initialData in FeeDialog');
loadDepartmentOptions();
getAdviceBaseInfos();
getDiseaseInitLoc();
// 数据加载由 watch(visible) 统一触发,避免 patientInfo 未就绪时调用报错
});
// 监听弹窗显示状态
@@ -484,10 +497,12 @@ watch(
(visible) => {
if (visible) {
executeTime.value = formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss');
consumableDefaultLocId.value = null; // 重置耗材默认库房,避免复用上次患者配置
// 弹窗打开时按当前患者科室重新加载,避免复用上一次患者/登录科室的结果
loadDepartmentOptions();
getAdviceBaseInfos();
getDiseaseInitLoc(16);
loadConsumableDefaultLoc();
} else {
resetData();
}
@@ -516,7 +531,16 @@ watch(
if (!locs || locs.length === 0) return;
feeItemsList.value.forEach(item => {
if (item.adviceType === 2 && !item.positionId) {
item.positionId = String(locs[0].value);
if (consumableDefaultLocId.value) {
const matched = locs.find(d => String(d.value) === consumableDefaultLocId.value);
if (matched) {
item.positionId = String(matched.value);
} else {
ElMessage.warning(`"${item.adviceName}" 未找到匹配的执行科室,请手动选择`);
}
} else {
ElMessage.warning(`"${item.adviceName}" 所在科室未配置耗材执行科室,请手动选择`);
}
}
});
}
@@ -586,14 +610,33 @@ function getAdviceBaseInfos() {
});
}
function getDiseaseInitLoc() {
getDiseaseTreatmentInitLoc(16)
.then((response) => {
console.log('Disease Treatment Init Loc:', response);
locationOptions.value = response.data.locationOptions;
// 16=药房17=耗材库,合并后作为耗材执行科室下拉选项
Promise.all([
getDiseaseTreatmentInitLoc(16).catch(() => ({ data: { locationOptions: [] } })),
getDiseaseTreatmentInitLoc(17).catch(() => ({ data: { locationOptions: [] } })),
]).then(([pharmacyRes, warehouseRes]) => {
const pharmacies = pharmacyRes.data?.locationOptions || [];
const warehouses = warehouseRes.data?.locationOptions || [];
locationOptions.value = [...pharmacies, ...warehouses];
});
}
/**
* 查询患者科室的耗材默认库房(取药科室配置 itemCode=2
*/
function loadConsumableDefaultLoc() {
const deptId = props.patientInfo?.organizationId;
if (!deptId) {
consumableDefaultLocId.value = null;
return;
}
getOrgLocConfig({ organizationId: deptId, itemCode: '2', pageNo: 1, pageSize: 100 })
.then((res) => {
const records = res.data?.records || [];
consumableDefaultLocId.value = records.length > 0 ? String(records[0].defLocationId) : null;
})
.catch(() => {
console.warn('位置列表加载失败(可能无权限)');
locationOptions.value = [];
consumableDefaultLocId.value = null;
});
}
// 下拉框模糊搜索过滤自定义filter-method配合element-plus filterable使用
@@ -744,8 +787,19 @@ function selectChange(row) {
defaultPositionId = String(departmentOptions.value[0].id);
}
} else if (row.adviceType === 2 && locationOptions.value.length > 0) {
// 耗材:默认取第一个药房/耗材房
defaultPositionId = String(locationOptions.value[0].value);
// 耗材:必须从取药科室配置中匹配默认库房,未配置则提示用户
if (consumableDefaultLocId.value) {
const matched = locationOptions.value.find(
d => String(d.value) === consumableDefaultLocId.value
);
if (matched) {
defaultPositionId = String(matched.value);
} else {
ElMessage.warning(`"${row.adviceName}" 未找到匹配的执行科室,请手动选择`);
}
} else {
ElMessage.warning(`"${row.adviceName}" 所在科室未配置耗材执行科室请手动选择`);
}
}
//插入费用列表
feeItemsList.value.push({
@@ -1023,6 +1077,8 @@ function applyGroupSet() {
font-weight: 600;
color: #303133;
margin-bottom: 8px;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -111,6 +111,16 @@ export function getDiseaseTreatmentInitLoc(id) {
method: 'get',
});
}
/**
* 查询科室取药配置(耗材默认库房)
*/
export function getOrgLocConfig(params) {
return request({
url: '/base-data-manage/org-loc/org-loc',
method: 'get',
params: params,
});
}
// 住院护士站费用明细
export function getCostDetail(queryParams) {
return request({

View File

@@ -262,7 +262,7 @@
</template>
<script setup>
import {computed, nextTick, onMounted, ref, watch} from 'vue';
import {nextTick, onMounted, ref} from 'vue';
import {ElMessage, ElMessageBox} from 'element-plus';
// Element Plus 图标导入
import {User} from '@element-plus/icons-vue';
@@ -366,9 +366,9 @@ const rawPrescriptionList = ref([]); // 原始未分组数据
const groupedPrescriptionList = ref([]); // 按encounterId分组后的数据
const activeCollapseNames = ref([]); // Collapse激活状态
const selectedRows = ref({}); // 选中的行数据
const totalItemsCount = ref(0); // 总医嘱项数
const totalAmount = ref(0); // 总金额保留4位小数
const dialogVisible = ref(false);
/** Tab 切换同步日期时跳过 date-picker change避免与 v-model 循环触发 */
const syncingDateFromTab = ref(false);
const selectedFeeItems = ref([]);
const currentPatientInfo = ref(null);
const queryParams = ref({
@@ -381,24 +381,6 @@ const userStore = useUserStore();
const userId = ref(safeGet(userStore, 'id', ''));
const orgId = ref(safeGet(userStore, 'orgId', ''));
// ========== 计算属性 ==========
// 计算总统计信息(总项数、总金额)
const calculateTotalStats = computed(() => {
let itemsCount = 0;
let amount = 0;
safeArray(groupedPrescriptionList.value).forEach((patientGroup) => {
safeArray(patientGroup).forEach((item) => {
itemsCount++;
// 累加单价保留4位小数精度
amount = Math.round((amount + Number(safeGet(item, 'unitPrice', 0))) * 10000) / 10000;
});
});
totalItemsCount.value = itemsCount;
totalAmount.value = amount;
});
// ========== 方法 ==========
/**
* 计算单个患者的总金额保留4位小数
@@ -447,16 +429,19 @@ const handleTableSelectionChange = (index, val) => {
};
/**
* 日期Tab切换
* @param {Object} tab - 标签页
* 按 Tab 同步日期范围(避免 date-picker @change 与 Tab v-model 互相覆盖)
* @param {string} rangeType - today | yesterday | custom
*/
const handleDateTabClick = (tab) => {
const applyDateRangeByTab = (rangeType) => {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const format = (date) => formatDateStr(date, 'YYYY-MM-DD');
switch (safeGet(tab, 'paneName')) {
syncingDateFromTab.value = true;
dateRange.value = rangeType;
switch (rangeType) {
case 'today':
dateRangeValue.value = [format(today), format(today)];
break;
@@ -464,27 +449,54 @@ const handleDateTabClick = (tab) => {
dateRangeValue.value = [format(yesterday), format(yesterday)];
break;
case 'custom':
if (!dateRangeValue.value.length) {
if (safeArray(dateRangeValue.value).length < 2) {
dateRangeValue.value = [format(today), format(today)];
}
break;
default:
break;
}
nextTick(() => {
syncingDateFromTab.value = false;
});
};
/**
* 日期选择器变化
* 日期Tab切换
* @param {Object} tab - 标签页
*/
const handleDateTabClick = (tab) => {
const rangeType = tab?.paneName ?? tab?.props?.name;
if (!rangeType) return;
applyDateRangeByTab(rangeType);
handleQuery();
};
/**
* 日期选择器变化(仅用户手动改日期时切到「自定义」)
* @param {Array} val - 选中日期
*/
const handleDatePickerChange = (val) => {
if (syncingDateFromTab.value) return;
const dateVal = safeArray(val);
if (dateVal.length === 2) {
if (dateVal.length !== 2) return;
const start = new Date(dateVal[0]);
const end = new Date(dateVal[1]);
if (start > end) {
ElMessage.warning('开始日期不能晚于结束日期');
syncingDateFromTab.value = true;
dateRangeValue.value = [dateVal[1], dateVal[0]];
nextTick(() => {
syncingDateFromTab.value = false;
});
return;
}
if (dateRange.value !== 'custom') {
dateRange.value = 'custom';
const start = new Date(dateVal[0]);
const end = new Date(dateVal[1]);
if (start > end) {
ElMessage.warning('开始日期不能晚于结束日期');
dateRangeValue.value = [dateVal[1], dateVal[0]];
}
}
};
@@ -714,24 +726,7 @@ const handleSingleDelete = (row) => {
};
// ========== 初始化 ==========
onMounted(() => {
// 设置默认日期
const today = new Date();
const defaultDate = formatDateStr(today, 'YYYY-MM-DD');
dateRangeValue.value = [defaultDate, defaultDate];
// 监听日期变化自动查询
watch(
[dateRange, dateRangeValue],
([newRange, newVal], [oldRange, oldVal]) => {
if (oldRange !== undefined && safeArray(newVal).length === 2) {
handleQuery();
}
},
{ deep: true }
);
// 初始化统计信息
calculateTotalStats.value;
applyDateRangeByTab('today');
});
</script>

View File

@@ -90,6 +90,13 @@
</span>
</template>
</el-table-column>
<el-table-column label="医嘱状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row)" size="small">
{{ getStatusDisplayText(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="医嘱内容" prop="adviceName">
<template #default="scope">
<span>
@@ -169,6 +176,43 @@ import {formatDateStr} from '@/utils/index';
import {getCurrentInstance, ref} from 'vue';
import useUserStore from '@/store/modules/user';
/** 发药状态 → 医嘱状态(药品医嘱状态映射表) */
const DISPENSE_STATUS_TO_ADVICE_TEXT = {
2: '已执行',
8: '已提交',
4: '已发药',
};
function getStatusDisplayText(row) {
const params = row?.medicineSummaryParamList || [];
const pending = params.filter((p) => Number(p.dispenseStatus) !== 8);
if (pending.length === 0) {
return params.length ? '已提交' : '已执行';
}
const texts = [
...new Set(
pending
.map((p) => DISPENSE_STATUS_TO_ADVICE_TEXT[Number(p.dispenseStatus)])
.filter(Boolean),
),
];
if (texts.length === 1) {
return texts[0];
}
return '已执行';
}
function getStatusType(row) {
const text = getStatusDisplayText(row);
if (text === '已发药') {
return 'success';
}
if (text === '已提交') {
return 'warning';
}
return 'primary';
}
const activeNames = ref([]);
const userStore = useUserStore();
@@ -290,6 +334,7 @@ function handleMedicineSummary() {
medicineSummary(ids).then((res) => {
if (res.code == 200) {
proxy.$message.success('操作成功');
handleGetPrescription();
}
});
}

View File

@@ -16,7 +16,13 @@
<el-table-column label="药房" align="center" prop="locationName" />
<el-table-column label="申请人" align="center" prop="applicantName" />
<el-table-column label="领药人" align="center" prop="receiverName" />
<el-table-column label="发药状态" align="center" prop="statusEnum_enumText" />
<el-table-column label="医嘱状态" align="center" prop="statusEnum_enumText">
<template #default="scope">
<el-tag :type="getSummaryStatusType(scope.row)" size="small">
{{ formatSummaryStatusText(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button link type="primary" @click="getDetail(scope.row)">详情</el-button>
@@ -52,6 +58,35 @@ import {getMedicineSummary, getMedicineSummaryDetail} from './api';
import {patientInfoList} from '../../components/store/patient.js';
import {getCurrentInstance, ref} from 'vue';
const SUMMARY_STATUS_DISPLAY = {
2: '已提交',
4: '已发药',
};
const LEGACY_SUMMARY_STATUS_TEXT = {
待配药: '已提交',
已发放: '已发药',
};
function formatSummaryStatusText(row) {
const code = Number(row?.statusEnum);
if (SUMMARY_STATUS_DISPLAY[code]) {
return SUMMARY_STATUS_DISPLAY[code];
}
return LEGACY_SUMMARY_STATUS_TEXT[row?.statusEnum_enumText] || row?.statusEnum_enumText || '-';
}
function getSummaryStatusType(row) {
const text = formatSummaryStatusText(row);
if (text === '已发药') {
return 'success';
}
if (text === '已提交') {
return 'warning';
}
return 'info';
}
const medicineSummaryFormList = ref([]);
const medicineSummaryFormDetails = ref([]);
const dialogVisible = ref(false);

View File

@@ -184,6 +184,11 @@
</template>
</el-table-column>
<el-table-column label="开始/终止" prop="requestTime" width="200" />
<el-table-column label="医嘱状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="props.exeStatus === 6" type="success" size="small">已执行</el-tag>
</template>
</el-table-column>
<el-table-column :label="props.exeStatus == 1 ? '预计执行' : '执行时间'" prop="times">
<template #default="scope">
<div
@@ -229,8 +234,80 @@ import {adviceCancel, adviceExecute, adviceNoExecute, getPrescriptionList} from
import {patientInfoList} from '../../components/store/patient.js';
import {lotNumberMatch} from '@/api/public';
import {formatDateStr} from '@/utils/index';
import {RequestStatus} from '@/utils/medicalConstants';
import {getCurrentInstance, nextTick, ref, provide} from 'vue';
/** 请求状态 → 医嘱状态映射表(开具/签发/校对) */
const REQUEST_STATUS_DISPLAY = {
[RequestStatus.DRAFT]: '待签发',
[RequestStatus.ACTIVE]: '已签发',
[RequestStatus.COMPLETED]: '已校对',
};
/** 发药状态 → 医嘱状态映射表(汇总申请/发药) */
const DISPENSE_STATUS_DISPLAY = {
8: '已提交',
4: '已发药',
};
/** 执行页签对应的医嘱状态展示 */
const STATUS_DISPLAY_BY_EXE_TAB = {
1: { text: '已校对', type: 'success' },
6: { text: '已执行', type: 'success' },
5: { text: '不执行', type: 'warning' },
9: { text: '取消执行', type: 'info' },
};
const LEGACY_STATUS_TEXT = {
待发送: '待签发',
已发送: '已签发',
'已发送/待执行': '已签发',
已完成: '已校对',
待配药: '已提交',
已汇总: '已提交',
已发放: '已发药',
};
function getStatusDisplayText(row) {
const dispenseCode = Number(row?.dispenseStatus);
if (DISPENSE_STATUS_DISPLAY[dispenseCode]) {
return DISPENSE_STATUS_DISPLAY[dispenseCode];
}
const tabText = STATUS_DISPLAY_BY_EXE_TAB[props.exeStatus]?.text;
if (tabText) {
return tabText;
}
const requestCode = Number(row?.requestStatus);
if (REQUEST_STATUS_DISPLAY[requestCode]) {
return REQUEST_STATUS_DISPLAY[requestCode];
}
return (
LEGACY_STATUS_TEXT[row?.requestStatus_enumText] ||
LEGACY_STATUS_TEXT[row?.dispenseStatus_enumText] ||
row?.requestStatus_enumText ||
row?.dispenseStatus_enumText ||
'-'
);
}
function getStatusType(row) {
const tabType = STATUS_DISPLAY_BY_EXE_TAB[props.exeStatus]?.type;
if (tabType) {
return tabType;
}
const status = row?.requestStatus;
const map = {
1: 'info',
2: 'primary',
3: 'success',
4: 'warning',
5: 'danger',
6: 'danger',
7: 'warning',
};
return map[status] || 'info';
}
const activeNames = ref([]);
const prescriptionList = ref([]);
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');

View File

@@ -89,7 +89,7 @@ function handleClick(tabName) {
// 执行状态待执行
exeStatus.value = 1;
// 请求状态已校对
requestStatus.value = 3;
requestStatus.value = RequestStatus.COMPLETED;
break;
case 'completed':
exeStatus.value = 6;

View File

@@ -142,6 +142,13 @@
</template>
</template>
</el-table-column>
<el-table-column label="医嘱状态" prop="requestStatus_enumText" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.requestStatus)" size="small">
{{ getStatusDisplayText(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="执行科室" prop="positionName" width="230" />
<el-table-column label="签发时间" prop="requestTime" width="230" />
</el-table>
@@ -152,10 +159,11 @@
</div>
</template>
<script setup>
import {ref, computed} from 'vue';
import {ref, computed, getCurrentInstance} from 'vue';
import {adviceVerify, cancel, getPrescriptionList} from './api';
import {patientInfoList} from '../../components/store/patient.js';
import {formatDateStr} from '@/utils/index';
import {RequestStatus} from '@/utils/medicalConstants';
const activeNames = ref([]);
const prescriptionList = ref([]);
@@ -165,6 +173,35 @@ const { proxy } = getCurrentInstance();
const loading = ref(false);
const chooseAll = ref(false);
const selectionTrigger = ref(0);
/** 各页签对应的医嘱状态展示文案(与后端枚举值解耦,贴合校对业务语义) */
const STATUS_DISPLAY_BY_TAB = {
[RequestStatus.ACTIVE]: { text: '已签发', type: 'primary' },
[RequestStatus.COMPLETED]: { text: '已校对', type: 'success' },
[RequestStatus.DRAFT]: { text: '待签发', type: 'info' },
[RequestStatus.STOPPED]: { text: '已停止', type: 'danger' },
};
const getStatusType = (status) => {
const tabType = STATUS_DISPLAY_BY_TAB[props.requestStatus]?.type;
if (tabType) return tabType;
const map = {
1: 'info',
2: 'primary',
3: 'success',
4: 'warning',
5: 'danger',
6: 'danger',
7: 'info',
};
return map[status] || 'info';
};
const getStatusDisplayText = (row) => {
const tabText = STATUS_DISPLAY_BY_TAB[props.requestStatus]?.text;
if (tabText) return tabText;
return row.requestStatus_enumText || '';
};
const hasDispensedSelected = computed(() => {
selectionTrigger.value;
return getSelectRows().some(item => item.dispenseStatus === 4);
@@ -174,6 +211,10 @@ const props = defineProps({
type: Number,
default: 2,
},
activeTab: {
type: String,
default: 'unverified',
},
});
function handleRadioChange() {

View File

@@ -41,6 +41,7 @@
<!-- 使用模板引用 -->
<PrescriptionList
:requestStatus="requestStatus"
:activeTab="activeName"
:ref="(el) => setPrescriptionRef(el, tab.name)"
/>
</el-tab-pane>
@@ -82,16 +83,16 @@ function handleTabClick(tabName) {
switch (activeTabName) {
case 'unverified':
requestStatus.value = 2;
requestStatus.value = RequestStatus.ACTIVE;
break;
case 'verified':
requestStatus.value = 3;
requestStatus.value = RequestStatus.COMPLETED;
break;
case 'stopped':
requestStatus.value = 6;
requestStatus.value = RequestStatus.STOPPED;
break;
case 'cancelled':
requestStatus.value = 1;
requestStatus.value = RequestStatus.DRAFT;
break;
}
// 调用子组件方法

View File

@@ -1,5 +1,5 @@
<template>
<div busNo="app-container">
<div class="app-container">
<el-form
style="margin-top: 20px; margin-left: 20px"
:model="queryParams"
@@ -97,7 +97,7 @@
<el-row
:gutter="10"
busNo="mb8"
class="mb8"
style="margin-left: 20px; margin-right: 0px; margin-bottom: 5px"
>
<el-col :span="1.5">
@@ -268,7 +268,7 @@
/>
<el-row
:gutter="10"
busNo="mb8"
class="mb8"
style="
margin-top: 10px;
display: flex;
@@ -298,6 +298,7 @@ const { proxy } = getCurrentInstance();
const totalAmount = ref(0);
const purchaseinventoryListAll = ref([]);
const xiaojiTotal = ref([]);
const rowSpanMap = ref({});
const rowSpan = ref(1);
const issuerOptions = ref([]);
const payeeOptions = ref([]);
@@ -548,93 +549,92 @@ function handleTotalAmount() {
}, 0);
}
// 门诊号合并行处理
function getTotals(row, i) {
let totalPriceSums = Number(purchaseinventoryList.value[i].totalPrice);
for (let j = 1; i - j >= 0; j++) {
if (purchaseinventoryList.value[i].busNo == purchaseinventoryList.value[i - j].busNo) {
totalPriceSums += Number(purchaseinventoryList.value[i - j].totalPrice);
}
}
xiaojiTotal.value.push({
inde: i + 1,
busNo: row.busNo,
genderEnum_enumText: row.genderEnum_enumText,
totalPrice: totalPriceSums.toFixed(4) || 0.0,
});
purchaseinventoryList.value.splice(i + 1, 0, {
busNo: row.busNo,
genderEnum_enumText: row.genderEnum_enumText,
departmentName: '小计',
totalPrice: totalPriceSums.toFixed(4) || 0.0,
});
}
// 表格合并行方法
// 表格合并行方法(纯函数,不修改数据)
const arraySpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (columnIndex === 1 && purchaseinventoryList.value.length > 0) {
if (
rowIndex === 0 ||
(rowIndex > 0 && row.busNo !== purchaseinventoryList.value[rowIndex - 1]?.busNo)
) {
let rowspan = 1;
let totalPriceSum = 0;
for (let i = rowIndex + 1; i < purchaseinventoryList.value.length + 1; i++) {
if (purchaseinventoryList.value[i - 1].departmentName != '合计') {
if (
purchaseinventoryList.value[i] &&
purchaseinventoryList.value[i].busNo === row.busNo
) {
rowspan++;
totalPriceSum += Number(purchaseinventoryList.value[i].totalPrice);
if (i == purchaseinventoryList.value.length - 1) {
let findIndexTotal = xiaojiTotal.value.findIndex((k) => k.busNo == row.busNo);
if (findIndexTotal < 0) {
getTotals(row, i);
}
}
} else {
totalPriceSum += Number(row.totalPrice);
let findIndexTotal = xiaojiTotal.value.findIndex((k) => k.busNo == row.busNo);
if (findIndexTotal < 0) {
xiaojiTotal.value.push({
inde: i,
genderEnum_enumText: row.genderEnum_enumText,
busNo: row.busNo,
totalPrice: totalPriceSum,
});
purchaseinventoryList.value.splice(i, 0, {
busNo: row.busNo,
genderEnum_enumText: row.genderEnum_enumText,
departmentName: '小计',
totalPrice: totalPriceSum.toFixed(4) || 0.0,
});
}
break;
}
}
rowspan++;
}
return { rowspan, colspan: 1 };
// 合并门诊号列columnIndex === 1
if (columnIndex === 1) {
const spanInfo = rowSpanMap.value[rowIndex];
if (spanInfo) {
return { rowspan: spanInfo.rowspan, colspan: spanInfo.colspan || 1 };
} else {
return { rowspan: 0, colspan: 0 };
}
}
// 其他列不合并
return { rowspan: 1, colspan: 1 };
};
// 预处理列表数据:插入小计行、计算合并行信息
// 此函数替代了原来在 arraySpanMethod 中 splice 修改数据的做法
function processListWithSubtotals(list) {
rowSpanMap.value = {};
xiaojiTotal.value = [];
const result = [];
let i = 0;
while (i < list.length) {
const row = list[i];
// 跳过已有的合计行
if (row.departmentName === '合计') {
result.push(row);
rowSpanMap.value[result.length - 1] = { rowspan: 1, colspan: 1 };
i++;
continue;
}
const currentBusNo = row.busNo;
let rowspan = 0;
let totalPriceSum = 0;
let j = i;
// 计算相同门诊号的行数
while (j < list.length && list[j].busNo === currentBusNo && list[j].departmentName !== '合计') {
rowspan++;
totalPriceSum += Number(list[j].totalPrice) || 0;
j++;
}
// 设置第一行的 rowspan
const startRow = result.length;
rowSpanMap.value[startRow] = { rowspan, colspan: 1 };
// 添加数据行
for (let k = i; k < j; k++) {
result.push(list[k]);
}
// 添加小计行多于1行时才添加
if (rowspan > 1) {
const subtotalRow = {
...list[i],
departmentName: '小计',
totalPrice: totalPriceSum.toFixed(4),
};
// 小计行不合并
rowSpanMap.value[result.length] = { rowspan: 1, colspan: 1 };
result.push(subtotalRow);
xiaojiTotal.value.push({
inde: result.length,
busNo: currentBusNo,
genderEnum_enumText: list[i].genderEnum_enumText,
totalPrice: totalPriceSum.toFixed(4),
});
}
i = j;
}
return result;
}
// 统计类型变化处理
function inventoryChange(val) {
queryParams.value.statisticsType = val;
xiaojiTotal.value = [];
purchaseinventoryList.value = [];
rowSpanMap.value = {};
getList();
}
@@ -694,8 +694,11 @@ function getList(type) {
: '0.0000' + (k.quantityUnit_dictText ? k.quantityUnit_dictText : '');
});
// 处理搜索关键词时的合计
if (queryParams.value.searchKey) {
// 处理搜索关键词或单页数据
if (queryParams.value.searchKey || (total.value && total.value <= queryParams.value.pageSize)) {
// 先处理小计行和合并信息
purchaseinventoryList.value = processListWithSubtotals(purchaseinventoryList.value);
purchaseinventoryList.value.forEach((k) => {
if (k.departmentName !== '小计' && k.departmentName !== '合计') {
totalPrice2 += Number(k.totalPrice) || 0;
@@ -703,39 +706,24 @@ function getList(type) {
});
totalPrice2 = totalPrice2 ? totalPrice2.toFixed(4) : totalPrice2;
purchaseinventoryList.value.push({ departmentName: '合计', totalPrice: totalPrice2 });
loading.value = false;
return;
}
// 处理分页数据
purchaseinventoryList.value.forEach((k) => {
if (total.value && total.value <= queryParams.value.pageSize) {
totalPrice2 += Number(k.totalPrice);
}
});
if (total.value <= res.data.size) {
loading.value = false;
}
// 单页数据合计
if (total.value && total.value <= queryParams.value.pageSize) {
totalPrice2 = totalPrice2 ? totalPrice2.toFixed(4) : totalPrice2;
let pageNoAll = total.value / queryParams.value.pageSize;
let pageNoAll = total.value / queryParams.value.pageSize;
if (Math.ceil(pageNoAll) == queryParams.value.pageNo) {
purchaseinventoryList.value.push({ departmentName: '合计', totalPrice: totalPrice2 });
rowSpanMap.value[purchaseinventoryList.value.length - 1] = { rowspan: 1, colspan: 1 };
}
}
// 多页数据处理
if (total.value && total.value > queryParams.value.pageSize && !queryParams.value.searchKey) {
loading.value = false;
} else if (total.value && total.value > queryParams.value.pageSize && !queryParams.value.searchKey) {
// 多页数据先处理当前页数据确保rowSpanMap正确初始化避免表格格式错乱
purchaseinventoryList.value = processListWithSubtotals(purchaseinventoryList.value);
loading.value = false;
// 然后获取全部数据进行完整处理
let queryParamsValue = { ...queryParams.value };
queryParamsValue.pageSize = total.value;
queryParamsValue.pageNo = 1;
// 移除空值参数
Object.keys(queryParamsValue).forEach(key => {
if (queryParamsValue[key] === undefined || queryParamsValue[key] === null || queryParamsValue[key] === '') {
delete queryParamsValue[key];
@@ -746,7 +734,7 @@ function getList(type) {
purchaseinventoryListAll.value = res.data.records || [];
if (purchaseinventoryListAll.value.length > 0) {
purchaseinventoryListAll.value.map((k, index) => {
purchaseinventoryListAll.value.map((k) => {
k.totalPrice = k.totalPrice ? k.totalPrice.toFixed(4) : '0.0000';
k.price = k.price ? k.price.toFixed(4) : '0.0000';
k.number = k.number
@@ -754,45 +742,16 @@ function getList(type) {
: '0.0000' + (k.quantityUnit_dictText ? k.quantityUnit_dictText : '');
totalPrice2 += Number(k.totalPrice);
// 处理跨页门诊号小计
for (let m = 1; m < index; m++) {
if (
queryParams.value.pageNo > 1 &&
index == queryParams.value.pageSize * (queryParams.value.pageNo - 1) &&
k.busNo == purchaseinventoryListAll.value[index - m]?.busNo
) {
let dispenseNoIndex1 = purchaseinventoryList.value.findIndex(
(o) => o.departmentName == '小计' && o.busNo == purchaseinventoryListAll.value[index - m].busNo
);
if (dispenseNoIndex1 > 0) {
purchaseinventoryList.value[dispenseNoIndex1].totalPrice =
(Number(purchaseinventoryList.value[dispenseNoIndex1].totalPrice) +
Number(purchaseinventoryListAll.value[index - m].totalPrice)).toFixed(4) || '0.0000';
}
}
if (
index + m == queryParams.value.pageSize * queryParams.value.pageNo &&
k.busNo == purchaseinventoryListAll.value[index + m]?.busNo
) {
if (
purchaseinventoryList.value[purchaseinventoryList.value.length - 1]
.departmentName == '小计'
) {
purchaseinventoryList.value.splice(purchaseinventoryList.value.length - 1, 1);
}
}
}
});
purchaseinventoryList.value = processListWithSubtotals(purchaseinventoryListAll.value);
totalPrice2 = totalPrice2 ? totalPrice2.toFixed(4) : totalPrice2;
loading.value = false;
let pageNoAll = total.value / queryParams.value.pageSize;
if (Math.ceil(pageNoAll) == queryParams.value.pageNo) {
purchaseinventoryList.value.push({ departmentName: '合计', totalPrice: totalPrice2 });
rowSpanMap.value[purchaseinventoryList.value.length - 1] = { rowspan: 1, colspan: 1 };
}
}
});

View File

@@ -740,58 +740,59 @@
</el-form-item>
</el-form>
<!-- 结果表格 -->
<el-table
ref="applyTableRef"
v-loading="applyLoading"
:data="applyList"
row-key="surgeryNo"
@row-click="handleApplyRowClick"
:row-class-name="tableRowClassName"
style="width: 100%"
max-height="340"
:scroll="{ y: 340 }"
>
<el-table-column type="selection" width="55" :selectable="handleSelectable" />
<el-table-column label="ID" align="center" width="80" fixed>
<template #default="{ $index }">
{{ (applyQueryParams.pageNo - 1) * applyQueryParams.pageSize + $index + 1 }}
</template>
</el-table-column>
<el-table-column label="姓名" align="center" prop="name" width="100" />
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="120" />
<el-table-column label="手术名称" align="center" prop="descJson.surgeryName" min-width="140" show-overflow-tooltip />
<el-table-column label="申请科室" align="center" width="100" prop="applyDeptName" />
<el-table-column label="手术类型" align="center" width="90">
<template #default="scope">
{{ getSurgeryTypeName(scope.row.surgeryType) }}
</template>
</el-table-column>
<el-table-column label="手术等级" align="center" width="90">
<template #default="scope">
{{ getSurgeryLevelName(scope.row.surgeryLevel || scope.row.descJson?.surgeryLevel) }}
</template>
</el-table-column>
<el-table-column label="麻醉方式" align="center" width="90">
<template #default="scope">
{{ getAnesthesiaName(scope.row.anesthesiaTypeEnum) }}
</template>
</el-table-column>
<el-table-column label="主刀医生" align="center" width="100" prop="mainSurgeonName" />
</el-table>
<!-- 底部分页区 -->
<div class="pagination-container apply-pagination">
<pagination
v-show="applyTotal > 0"
:total="applyTotal"
:page="applyQueryParams.pageNo"
:limit="applyQueryParams.pageSize"
@update:page="val => applyQueryParams.pageNo = val"
@update:limit="val => applyQueryParams.pageSize = val"
@pagination="getSurgicalScheduleList"
/>
</div>
<!-- 结果表格卡片 -->
<el-card shadow="never" class="apply-card">
<el-table
ref="applyTableRef"
v-loading="applyLoading"
:data="applyList"
row-key="surgeryNo"
@row-click="handleApplyRowClick"
:row-class-name="tableRowClassName"
style="width: 100%"
max-height="320"
>
<el-table-column type="selection" width="55" :selectable="handleSelectable" />
<el-table-column label="ID" align="center" width="80" fixed>
<template #default="{ $index }">
{{ (applyQueryParams.pageNo - 1) * applyQueryParams.pageSize + $index + 1 }}
</template>
</el-table-column>
<el-table-column label="姓名" align="center" prop="name" width="100" />
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="120" />
<el-table-column label="手术名称" align="center" prop="descJson.surgeryName" min-width="140" show-overflow-tooltip />
<el-table-column label="申请科室" align="center" width="100" prop="applyDeptName" />
<el-table-column label="手术类型" align="center" width="90">
<template #default="scope">
{{ getSurgeryTypeName(scope.row.surgeryType) }}
</template>
</el-table-column>
<el-table-column label="手术等级" align="center" width="90">
<template #default="scope">
{{ getSurgeryLevelName(scope.row.surgeryLevel || scope.row.descJson?.surgeryLevel) }}
</template>
</el-table-column>
<el-table-column label="麻醉方式" align="center" width="90">
<template #default="scope">
{{ getAnesthesiaName(scope.row.anesthesiaTypeEnum) }}
</template>
</el-table-column>
<el-table-column label="主刀医生" align="center" width="100" prop="mainSurgeonName" />
</el-table>
<!-- 分页在卡片内部 -->
<div class="apply-pagination">
<pagination
v-show="applyTotal > 0"
:total="applyTotal"
:page="applyQueryParams.pageNo"
:limit="applyQueryParams.pageSize"
layout="total, sizes, prev, pager, next"
@update:page="val => applyQueryParams.pageNo = val"
@update:limit="val => applyQueryParams.pageSize = val"
@pagination="getSurgicalScheduleList"
/>
</div>
</el-card>
<!-- 底部操作区 -->
<template #footer>
<div class="dialog-footer" style="padding-top: 12px; border-top: 1px solid #ebeef5">
@@ -1067,15 +1068,6 @@ const temporaryPatientInfo = ref({})
const temporaryBillingMedicines = ref([])
const temporaryAdvices = ref([])
// 🔧 新增:监听 temporaryAdvices 的变化,用于调试
watch(temporaryAdvices, (newVal, oldVal) => {
console.log('=== temporaryAdvices 变化 ===')
console.log('=== 新值 ===', newVal)
console.log('=== 新值[1]?.dosage ===', newVal[1]?.dosage)
console.log('=== 旧值 ===', oldVal)
console.log('=== 旧值[1]?.dosage ===', oldVal[1]?.dosage)
}, { deep: true })
const temporaryMedicalLoading = ref(false) // 🔧 新增:临时医嘱加载状态
const temporarySigned = ref(false) // 🔧 新增:签名状态,用于保持按钮名称一致性
@@ -1499,9 +1491,6 @@ async function closeChargeDialog() {
chargeSurgeryInfo.value = {}
}
// 🔧 新增:标志位,用于区分是"打开"还是"刷新"
const isRefreshAction = ref(false)
// 处理医嘱按钮点击事件
function handleMedicalAdvice(row) {
// 如果没有传入行数据,使用选中的行
@@ -1529,31 +1518,7 @@ function handleMedicalAdvice(row) {
applyId: row.applyId // 手术申请单ID用于过滤关联医嘱
}
// 🔧 关键修复:如果已有提交的医嘱数据,并且是同一个患者的就诊,则使用保存的数据
// 这样可以保留 requestId避免重复创建医嘱记录
console.log('=== 检查是否使用已保存的医嘱数据 ===')
console.log('=== temporaryAdvices.value.length ===', temporaryAdvices.value.length)
console.log('=== temporaryAdvices.value[0]?.originalMedicine?.encounterId ===', temporaryAdvices.value[0]?.originalMedicine?.encounterId)
console.log('=== row.visitId ===', row.visitId)
console.log('=== isRefreshAction.value ===', isRefreshAction.value)
const isSameEncounter = temporaryAdvices.value.length > 0 &&
temporaryAdvices.value[0]?.originalMedicine?.encounterId === row.visitId &&
!isRefreshAction.value
console.log('=== isSameEncounter ===', isSameEncounter)
if (isSameEncounter) {
console.log('=== 使用已保存的医嘱数据,避免重复创建 ===')
console.log('=== temporaryAdvices.value[0]?.originalMedicine?.requestId ===', temporaryAdvices.value[0]?.originalMedicine?.requestId)
// 直接打开弹窗,使用已保存的数据
showTemporaryMedical.value = true
temporaryMedicalLoading.value = false
isRefreshAction.value = false // 重置标志位
return
}
// 🔧 修复:每次打开临时医嘱时都重新加载数据,避免使用缓存数据导致数据重复
// 🔧 每次打开临时医嘱都重新拉取最新数据,确保计费弹窗签发后数据自动更新
// 先清空旧数据
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
@@ -1566,46 +1531,39 @@ function handleMedicalAdvice(row) {
// 调用计费接口获取数据
getPrescriptionList(row.visitId, 6, row.operCode).then((res) => {
console.log('=== 拉取计费数据返回结果 ===', res)
if (res.code === 200 && res.data) {
// 🔧 修复:显示所有药品请求数据,不管有没有计费项目
// 根据用户需求:已引用计费药品(待生成医嘱)和临时医嘱预览(已生成)显示的数据应该相同
// 在提交医嘱之前状态应该是"待签发",提交之后变为"已签发"
// 再次打开医嘱界面的时候能看到这两个状态的药品
const seenIds = new Set();
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
if (item.encounterId !== row.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6)
// 🔧 修复 Bug #444: 使用 Number() 显式转换,避免字符串 "1" 被 !== 1 放行
// 只保留药品(1)和耗材(2),屏蔽诊疗(3)和手术(6)
const at = Number(item.adviceType ?? item.advice_type);
if (at !== 1) return false;
if (at !== 1 && at !== 2) return false;
// 过滤掉名称为空的项目
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #444: 二次过滤,排除名称中包含手术/检查/诊疗关键词的非药品项目
// 某些计费项目可能在 adm_charge_item 中被错误标注为 adviceType=1
// 排除名称中包含手术/检查/诊疗关键词的非药品项目
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId 的不应出现在"待生成"列表中)
if (item.requestId) return false;
// 根据药品请求ID去重避免重复显示
const itemId = item.requestId || item.id;
if (itemId && seenIds.has(itemId)) return false;
if (itemId) seenIds.add(itemId);
return true;
})
// 按 statusEnum 区分1=草稿(待生成)2=已签发(已生成)
const draftItems = filteredItems.filter(item => item.statusEnum === 1)
const activeItems = filteredItems.filter(item => item.statusEnum === 2)
// 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死
const maxItems = 100
if (filteredItems.length > maxItems) {
ElMessage.warning(`待签发医嘱数量过多(${filteredItems.length}条),仅显示前${maxItems}`)
filteredItems.length = maxItems
if (draftItems.length > maxItems) {
ElMessage.warning(`待签发医嘱数量过多(${draftItems.length}条),仅显示前${maxItems}`)
draftItems.length = maxItems
}
// 将过滤后的数据转换为临时医嘱需要的格式 - 兼容驼峰和下划线命名
// 对于从 adm_charge_item计费项目表查询来的项目特殊处理
temporaryBillingMedicines.value = filteredItems.map(item => {
// === 待生成列表statusEnum=1 草稿状态的项目 ===
temporaryBillingMedicines.value = draftItems.map(item => {
try {
// 从 contentJson 或 content_json 中解析详细数据 - 兼容下划线和驼峰命名
const jsonContent = item.contentJson || item.content_json;
@@ -1652,74 +1610,65 @@ function handleMedicalAdvice(row) {
};
}
});
} else {
// 如果没有数据或接口调用失败,初始化空列表
temporaryBillingMedicines.value = []
}
// 将计费药品转换为临时医嘱数据
temporaryAdvices.value = temporaryBillingMedicines.value.map((medicine, index) => {
// 解析规格中的数值和单位
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
// 计算剂量 = 规格数值 × 数量
const dosage = specValue * (medicine.quantity || 1)
// === 已生成列表statusEnum=2 已签发状态的项目,直接转为医嘱格式 ===
temporaryAdvices.value = activeItems.map((item, index) => {
try {
const jsonContent = item.contentJson || item.content_json;
const contentData = jsonContent ? JSON.parse(jsonContent) : {};
const medicineName = contentData.adviceName || contentData.advice_name || item.adviceName || item.advice_name || '';
const spec = contentData.volume || contentData.specification || item.volume || item.specification || '';
const specMatch = spec.match(/(\d+)(\D+)/)
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
const dosage = specValue * (contentData.quantity || item.quantity || 1)
// 🔧 修复:优先从 contentJson 中读取已有的用法,如果没有则根据药品名称判断
let usageCode = 'iv' // 默认静脉注射编码
let usageLabel = '静脉注射' // 默认显示名称
let usageCode = contentData.methodCode || 'iv'
let usageLabel = getUsageLabel(usageCode)
if (usageCode === 'iv') {
if (medicineName.includes('注射液')) { usageCode = 'iv'; usageLabel = '静脉注射' }
} else if (usageCode === 'po') {
if (medicineName.includes('片') || medicineName.includes('胶囊')) { usageCode = 'po'; usageLabel = '口服' }
}
// 尝试从 contentJson 中读取用法
try {
const jsonContent = medicine.contentJson || medicine.content_json;
if (jsonContent) {
const contentData = JSON.parse(jsonContent);
if (contentData.methodCode) {
usageCode = contentData.methodCode;
usageLabel = getUsageLabel(contentData.methodCode);
return {
id: index + 1,
adviceName: medicineName,
dosage,
unit: specUnit,
usage: usageCode,
usageLabel,
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: medicineName,
specification: spec,
quantity: contentData.quantity || item.quantity || 1,
encounterId: row.visitId
}
}
} catch (e) {
return {
id: index + 1,
adviceName: item.adviceName || item.advice_name || '',
dosage: 1, unit: 'ml', usage: 'iv', usageLabel: '静脉注射',
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: item.adviceName || item.advice_name || '',
specification: item.volume || item.specification || '',
quantity: item.quantity || 1,
encounterId: row.visitId
}
}
}
} catch (e) {
// 解析失败,继续使用默认值
}
// 如果没有从 contentJson 中读取到用法,根据药品名称判断
if (!usageCode || usageCode === 'iv') {
if (medicine.medicineName && medicine.medicineName.includes('注射液')) {
usageCode = 'iv'
usageLabel = '静脉注射'
} else if (medicine.medicineName && medicine.medicineName.includes('片')) {
usageCode = 'po'
usageLabel = '口服'
} else if (medicine.medicineName && medicine.medicineName.includes('胶囊')) {
usageCode = 'po'
usageLabel = '口服'
}
}
return {
id: index + 1,
adviceName: medicine.medicineName || '',
dosage: dosage,
unit: specUnit,
usage: usageCode, // 🔧 修复:保存的是编码
usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
// 🔧 关键修复:确保 originalMedicine 中包含 encounterId 和匹配字段
// medicineName/specification/quantity 用于 handleTemporaryMedicalSubmit 中的
// 已提交项目匹配过滤Bug #445缺少这些字段会导致过滤失效
originalMedicine: {
...medicine,
medicineName: medicine.medicineName,
specification: medicine.specification,
quantity: medicine.quantity,
encounterId: row.visitId // 添加 encounterId 字段
}
}
})
})
} else {
temporaryBillingMedicines.value = []
temporaryAdvices.value = []
}
// 打开临时医嘱弹窗
showTemporaryMedical.value = true
@@ -1745,11 +1694,6 @@ function closeTemporaryMedical() {
// 处理临时医嘱提交
// 🔧 修复:提交成功后,更新 temporaryAdvices 中的 requestId以便下次提交时执行更新操作
function handleTemporaryMedicalSubmit(data) {
console.log('=== handleTemporaryMedicalSubmit 被调用 ===')
console.log('=== data ===', data)
console.log('=== data.temporaryAdvices ===', data.temporaryAdvices)
console.log('=== data.temporaryAdvices[1]?.dosage ===', data.temporaryAdvices[1]?.dosage)
// 🔧 修复:使用用户修改后的数据,而不是重新加载数据
// 这样可以确保用户修改的内容(如剂量)在保存后仍然正确显示
if (data.temporaryAdvices && data.temporaryAdvices.length > 0) {
@@ -1804,9 +1748,7 @@ function handleTemporaryMedicalSubmit(data) {
// 如果没有任何匹配键,清空待生成列表(所有项目都已提交)
temporaryBillingMedicines.value = []
}
console.log('=== 使用用户修改后的临时医嘱数据 ===', temporaryAdvices.value)
console.log('=== temporaryAdvices.value[1]?.dosage ===', temporaryAdvices.value[1]?.dosage)
} else {
// 如果没有传递数据,则清空
temporaryAdvices.value = []
@@ -1878,21 +1820,70 @@ function handleQuoteBilling() {
// 🔧 修复 Bug #445: 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6)
// 同时过滤掉已有 requestId 的项目(已生成医嘱的不需要再次显示在"待生成"列表中)
const filteredItems = res.data.filter(item => {
// 匹配 encounterId
// 先提取已签发项目(statusEnum=2)填充已生成列表
const activeItems = res.data.filter(item => {
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
// 只保留药品类型adviceType=1过滤掉耗材(2)和诊疗项目(3/6)
// 🔧 修复 Bug #444: 使用 Number() 显式转换 + snake_case 回退,避免字符串 "1" 匹配失败
const at = Number(item.adviceType ?? item.advice_type);
if (at !== 1) return false;
// 过滤掉名称为空的项目
if (at !== 1 && at !== 2) return false;
if (item.statusEnum !== 2) return false;
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
return true;
})
temporaryAdvices.value = activeItems.map((item, index) => {
try {
const jsonContent = item.contentJson || item.content_json;
const contentData = jsonContent ? JSON.parse(jsonContent) : {};
const medicineName = contentData.adviceName || contentData.advice_name || item.adviceName || item.advice_name || '';
const spec = contentData.volume || contentData.specification || item.volume || item.specification || '';
const specMatch = spec.match(/(\d+)(\D+)/)
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
const dosage = specValue * (contentData.quantity || item.quantity || 1)
let usageCode = contentData.methodCode || 'iv'
let usageLabel = getUsageLabel(usageCode)
if (usageCode === 'iv' && medicineName.includes('注射液')) { usageLabel = '静脉注射' }
else if (usageCode === 'po' && (medicineName.includes('片') || medicineName.includes('胶囊'))) { usageLabel = '口服' }
return {
id: index + 1, adviceName: medicineName, dosage, unit: specUnit,
usage: usageCode, usageLabel, frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: medicineName,
specification: spec,
quantity: contentData.quantity || item.quantity || 1,
encounterId: temporaryPatientInfo.value.visitId
}
}
} catch (e) {
return {
id: index + 1, adviceName: item.adviceName || item.advice_name || '',
dosage: 1, unit: 'ml', usage: 'iv', usageLabel: '静脉注射',
frequency: '临时', executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...item,
medicineName: item.adviceName || item.advice_name || '',
specification: item.volume || item.specification || '',
quantity: item.quantity || 1,
encounterId: temporaryPatientInfo.value.visitId
}
}
}
})
// 再提取草稿项目(statusEnum=1)填充待生成列表
const filteredItems = res.data.filter(item => {
if (item.encounterId !== temporaryPatientInfo.value.visitId) return false;
const at = Number(item.adviceType ?? item.advice_type);
if (at !== 1 && at !== 2) return false;
if (item.statusEnum !== 1) return false;
const medicineName = item.adviceName || item.advice_name;
if (!medicineName || medicineName.trim() === '') return false;
// 🔧 修复 Bug #444: 二次过滤,排除名称中包含手术/检查/诊疗关键词的非药品项目
const excludedKeywords = ['术', '超声', '多普勒', '检查', '检验', '彩超', 'X线', 'CT', 'MRI', '扫描', '造影'];
if (excludedKeywords.some(kw => medicineName.includes(kw))) return false;
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目(已有 requestId
if (item.requestId) return false;
return true;
})
// 🔧 修复限制返回数量最多显示前100条避免数据过多导致页面卡死
@@ -1905,7 +1896,6 @@ function handleQuoteBilling() {
// 将过滤后的数据转换为临时医嘱需要的格式
temporaryBillingMedicines.value = filteredItems.map(item => {
try {
// 从 contentJson 或 content_json 中解析详细数据 - 兼容下划线和驼峰命名
const jsonContent = item.contentJson || item.content_json;
const contentData = jsonContent ? JSON.parse(jsonContent) : {};
return {
@@ -1924,10 +1914,9 @@ function handleQuoteBilling() {
definitionDetailId: contentData.definitionDetailId || item.definitionDetailId
}
} catch (e) {
// 如果解析失败,使用顶层数据 - 兼容 snake_case 和 camelCase
return {
medicineName: item.adviceName || item.advice_name || '',
specification: item.specification || item.specification || item.volume || '',
specification: item.specification || item.volume || '',
quantity: item.quantity || item.quantity_value || 0,
batchNumber: item.lotNumber || item.lot_number || '',
unitPrice: item.unitPrice || item.unit_price || 0,
@@ -1943,123 +1932,6 @@ function handleQuoteBilling() {
}
})
// 将计费药品转换为临时医嘱数据
temporaryAdvices.value = temporaryBillingMedicines.value.map((medicine, index) => {
// 解析规格中的数值和单位
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
// 计算剂量 = 规格数值 × 数量
const dosage = specValue * (medicine.quantity || 1)
// 🔧 修复:优先从 contentJson 中读取已有的用法,如果没有则根据药品名称判断
let usageCode = 'iv' // 默认静脉注射编码
let usageLabel = '静脉注射' // 默认显示名称
// 尝试从 contentJson 中读取用法
try {
const jsonContent = medicine.contentJson || medicine.content_json;
if (jsonContent) {
const contentData = JSON.parse(jsonContent);
if (contentData.methodCode) {
usageCode = contentData.methodCode;
usageLabel = getUsageLabel(contentData.methodCode);
}
}
} catch (e) {
// 解析失败,继续使用默认值
}
// 如果没有从 contentJson 中读取到用法,根据药品名称判断
if (!usageCode || usageCode === 'iv') {
if (medicine.medicineName && medicine.medicineName.includes('注射液')) {
usageCode = 'iv'
usageLabel = '静脉注射'
} else if (medicine.medicineName && medicine.medicineName.includes('片')) {
usageCode = 'po'
usageLabel = '口服'
} else if (medicine.medicineName && medicine.medicineName.includes('胶囊')) {
usageCode = 'po'
usageLabel = '口服'
}
}
return {
id: index + 1,
adviceName: medicine.medicineName || '',
dosage: dosage,
unit: specUnit,
usage: usageCode, // 🔧 修复:保存的是编码
usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
// 🔧 关键修复:确保 originalMedicine 中包含 encounterId 和匹配字段
// medicineName/specification/quantity 用于已提交项目匹配过滤Bug #445
originalMedicine: {
...medicine,
medicineName: medicine.medicineName,
specification: medicine.specification,
quantity: medicine.quantity,
encounterId: temporaryPatientInfo.value.visitId // 添加 encounterId 字段
}
}
})
// 🔧 修复 Bug #445: 过滤掉已生成医嘱的项目,避免"引用计费"后已提交项目重新出现在"待生成"列表
// 使用清空前提取的 submittedKeys名称|||规格|||数量复合键)进行匹配
if (submittedKeys.size > 0) {
temporaryBillingMedicines.value = temporaryBillingMedicines.value.filter(m => {
const key = `${m.medicineName || ''}|||${m.specification || ''}|||${m.quantity ?? 0}`
return !submittedKeys.has(key)
})
// 同步更新 temporaryAdvices保持两份数据一致
temporaryAdvices.value = temporaryBillingMedicines.value.map((medicine, index) => {
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
const dosage = specValue * (medicine.quantity || 1)
let usageCode = 'iv'
let usageLabel = '静脉注射'
try {
const jsonContent = medicine.contentJson || medicine.content_json;
if (jsonContent) {
const contentData = JSON.parse(jsonContent);
if (contentData.methodCode) {
usageCode = contentData.methodCode;
usageLabel = getUsageLabel(contentData.methodCode);
}
}
} catch (e) {}
if (!usageCode || usageCode === 'iv') {
if (medicine.medicineName && medicine.medicineName.includes('注射液')) {
usageCode = 'iv'; usageLabel = '静脉注射';
} else if (medicine.medicineName && medicine.medicineName.includes('片')) {
usageCode = 'po'; usageLabel = '口服';
} else if (medicine.medicineName && medicine.medicineName.includes('胶囊')) {
usageCode = 'po'; usageLabel = '口服';
}
}
return {
id: index + 1,
adviceName: medicine.medicineName || '',
dosage,
unit: specUnit,
usage: usageCode,
usageLabel,
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: {
...medicine,
medicineName: medicine.medicineName,
specification: medicine.specification,
quantity: medicine.quantity,
encounterId: temporaryPatientInfo.value.visitId
}
}
})
}
temporaryMedicalLoading.value = false // 🔧 新增:加载完成
ElMessage.success('已成功引用最新计费药品信息!')
} else {
@@ -2468,19 +2340,35 @@ function getRowClassName({ row, rowIndex }) {
margin-left: 10px;
}
/* 手术申请查询弹窗 — 分页与footer间距 */
/* 手术申请查询弹窗 — flex 布局确保分页不溢出 */
.surgery-apply-dialog :deep(.el-dialog__body) {
display: flex;
flex-direction: column;
padding-bottom: 16px;
overflow: hidden;
}
.surgery-apply-dialog :deep(.el-dialog__footer) {
padding-top: 8px;
padding-top: 0;
}
.surgery-apply-dialog :deep(.apply-card) {
flex: 1;
overflow: hidden;
min-height: 0;
}
.surgery-apply-dialog :deep(.apply-card .el-card__body) {
overflow-y: auto;
}
.surgery-apply-dialog :deep(.apply-pagination) {
padding-top: 12px;
padding-bottom: 16px;
display: flex;
justify-content: flex-end;
padding-top: 8px;
border-top: 1px solid #ebeef5;
}
.surgery-apply-dialog :deep(.apply-pagination .pagination-container) {
margin-top: 0;
}
.surgery-apply-dialog :deep(.apply-pagination .el-pagination) {
margin-right: 80px;
position: static;
}
/* 选中行样式 */
@@ -2496,17 +2384,33 @@ function getRowClassName({ row, rowIndex }) {
<style>
/* 手术申请查询弹窗 — 非 scoped 确保穿透 teleport */
.surgery-apply-dialog .apply-pagination {
padding-top: 12px !important;
padding-bottom: 16px !important;
}
.surgery-apply-dialog .apply-pagination .el-pagination {
margin-right: 80px !important;
}
.surgery-apply-dialog .el-dialog__body {
display: flex !important;
flex-direction: column !important;
padding-bottom: 16px !important;
overflow: hidden !important;
}
.surgery-apply-dialog .el-dialog__footer {
padding-top: 0 !important;
}
.surgery-apply-dialog .apply-card {
flex: 1 !important;
overflow: hidden !important;
min-height: 0 !important;
}
.surgery-apply-dialog .apply-card .el-card__body {
overflow-y: auto !important;
}
.surgery-apply-dialog .apply-pagination {
display: flex !important;
justify-content: flex-end !important;
padding-top: 8px !important;
border-top: 1px solid #ebeef5 !important;
}
.surgery-apply-dialog .apply-pagination .pagination-container {
margin-top: 0 !important;
}
.surgery-apply-dialog .apply-pagination .el-pagination {
position: static !important;
}
</style>

View File

@@ -48,9 +48,12 @@
<div class="medicine-section">
<div class="section-title">
已引用计费药品待生成医嘱
<span v-if="(billingMedicines || []).length >= PAGE_SIZE" style="margin-left:auto;font-size:14px;color:#4a8bc9;cursor:pointer;white-space:nowrap;" @click="billingExpanded = !billingExpanded">
{{ billingExpanded ? '收起' : `展开全部(${(billingMedicines || []).length})` }}
</span>
</div>
<el-table
:data="billingMedicines"
:data="displayBillingMedicines"
stripe
border
style="width: 100%;"
@@ -98,9 +101,12 @@
<div class="advice-section">
<div class="section-title">
临时医嘱预览已生成
<span v-if="(displayAdvices || []).length >= PAGE_SIZE" style="margin-left:auto;font-size:14px;color:#4a8bc9;cursor:pointer;white-space:nowrap;" @click="advicesExpanded = !advicesExpanded">
{{ advicesExpanded ? '收起' : `展开全部(${(displayAdvices || []).length})` }}
</span>
</div>
<el-table
:data="displayAdvices"
:data="displayAdvicesList"
stripe
border
style="width: 100%;"
@@ -150,7 +156,7 @@
:disabled="allItemsSubmitted"
@click="handleSignAndSubmit"
>
{{ allItemsSubmitted ? '已签发' : (isSigned ? '提交医嘱' : '一键签名并生成医嘱') }}
{{ allItemsSubmitted ? '已签发' : '一键签名并生成医嘱' }}
</el-button>
</div>
</div>
@@ -317,6 +323,19 @@ const allItemsSubmitted = computed(() => {
return meds.length > 0 && meds.every(m => m.requestId)
})
// 展开/收起控制
const PAGE_SIZE = 3
const billingExpanded = ref(false)
const advicesExpanded = ref(false)
const displayBillingMedicines = computed(() => {
const all = props.billingMedicines || []
return billingExpanded.value ? all : all.slice(0, PAGE_SIZE)
})
const displayAdvicesList = computed(() => {
const all = displayAdvices.value || []
return advicesExpanded.value ? all : all.slice(0, PAGE_SIZE)
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)
@@ -1045,6 +1064,21 @@ const editFormUsageLabel = computed(() => {
padding-bottom: 12px;
margin-bottom: 16px;
border-bottom: 2px solid #e4e7ed;
display: flex;
align-items: center;
gap: 12px;
}
.expand-btn {
font-size: 0.85rem;
color: #4a8bc9;
cursor: pointer;
font-weight: 400;
margin-left: auto;
}
.expand-btn:hover {
color: #2a6ba9;
text-decoration: underline;
}
.medicine-summary {

View File

@@ -88,16 +88,16 @@
</el-table>
</div>
<div class="candidate-actions">
<el-button
type="primary"
:disabled="selectedCandidates.length === 0"
<el-button
type="primary"
:disabled="selectedCandidates.length === 0 || isQueryingHistory"
@click="handleAddToQueue"
>
加入队列 >>
</el-button>
<el-button
type="primary"
:disabled="filteredCandidatePoolList.length === 0"
<el-button
type="primary"
:disabled="filteredCandidatePoolList.length === 0 || isQueryingHistory"
@click="handleAddAllToQueue"
>
一键加入队列
@@ -109,6 +109,19 @@
<div class="right-panel">
<div class="panel-header">
<span class="panel-title"> 智能队列 (全科)</span>
<div class="history-query">
<el-date-picker
v-model="queryDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
size="small"
style="width: 150px"
/>
<el-button type="primary" size="small" @click="handleHistoryQuery">查询</el-button>
<el-button size="small" @click="handleTodayQuery">今天</el-button>
</div>
</div>
<div class="table-container">
<el-table
@@ -173,25 +186,25 @@
</div>
<div class="display-options">
<div class="queue-actions-left">
<el-button
type="danger"
:disabled="!selectedQueueRow"
<el-button
type="danger"
:disabled="!selectedQueueRow || isQueryingHistory"
size="small"
@click="handleRemoveFromQueue"
>
&lt;&lt; 移出队列
</el-button>
<el-button
type="info"
:disabled="!selectedQueueRow || !canMoveUp"
<el-button
type="info"
:disabled="!selectedQueueRow || !canMoveUp || isQueryingHistory"
size="small"
@click="handleMoveUp"
>
</el-button>
<el-button
type="info"
:disabled="!selectedQueueRow || !canMoveDown"
<el-button
type="info"
:disabled="!selectedQueueRow || !canMoveDown || isQueryingHistory"
size="small"
@click="handleMoveDown"
>
@@ -259,30 +272,35 @@
<div class="control-buttons">
<el-button
type="primary"
:disabled="isQueryingHistory"
@click="handleSelectCall"
>
选呼
</el-button>
<el-button
type="success"
:disabled="isQueryingHistory"
@click="handleNextPatient"
>
下一患者
</el-button>
<el-button
type="warning"
:disabled="isQueryingHistory"
@click="handleSkip"
>
跳过
</el-button>
<el-button
type="primary"
:disabled="isQueryingHistory"
@click="handleComplete"
>
完成
</el-button>
<el-button
type="info"
:disabled="isQueryingHistory"
@click="handleRequeue"
>
过号重排
@@ -682,6 +700,14 @@ const showOnlyWaiting = ref(false)
// Bug #411诊室过滤替代原来的科室下拉框selectedDept/departmentList 已移除)
const selectedRoom = ref('all')
// 历史队列查询日期 (默认当天)
const getTodayStr = () => {
const now = new Date()
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
}
const queryDate = ref(getTodayStr())
const isQueryingHistory = computed(() => queryDate.value !== getTodayStr())
// 修复【#397】动态获取当前科室名称
const currentDeptName = computed(() => {
return userStore.deptName || userStore.orgName || '心内科'
@@ -901,14 +927,12 @@ const mapFrontendStatusToBackend = (status) => {
}
// 从数据库加载队列
const loadQueueFromDb = async () => {
const loadQueueFromDb = async (dateStr) => {
try {
// Bug #411不再按科室选筛加载后端默认按当前登录人科室查询
const organizationId = undefined
// 只查询今天的患者
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
const res = await getTriageQueueList({ organizationId, date: todayStr }).catch((err) => {
const queryDateStr = dateStr || queryDate.value
const res = await getTriageQueueList({ organizationId, date: queryDateStr }).catch((err) => {
console.error('【心内科】loadQueueFromDb 请求异常:', err)
return { code: 500, msg: err?.message || '请求失败', data: null }
})
@@ -931,10 +955,6 @@ const loadQueueFromDb = async () => {
originalQueueList.value = list
.map((it) => {
const frontendStatus = mapBackendStatusToFrontend(it.status)
// 调试日志:检查状态映射
if (list.length <= 5) {
console.log('【心内科】状态映射:后端状态=', it.status, '-> 前端状态=', frontendStatus, '患者=', it.patientName)
}
// 计算等待时间基于创建时间createTime
let waitingTime = '00:00'
if (it.createTime) {
@@ -972,15 +992,7 @@ const loadQueueFromDb = async () => {
organizationId: it.organizationId
}
})
.filter((item) => {
// 过滤掉"已完成"状态的患者,不显示在队列中
if (item.status === '已完成') {
console.log('【心内科】过滤掉已完成状态的患者:', item.patientName)
return false
}
return true
})
// 调试日志:检查查找结果
const callingCount = originalQueueList.value.filter(i => i.status === '叫号中').length
const waitingCount = originalQueueList.value.filter(i => i.status === '等待').length
@@ -1196,9 +1208,6 @@ const formatSecondsToMmSs = (totalSeconds) => {
const filteredQueueList = computed(() => {
let filtered = originalQueueList.value
// 先过滤掉"已完成"状态的患者(无论什么情况都不显示)
filtered = filtered.filter(item => item.status !== '已完成')
// 再按诊室过滤
if (selectedRoom.value !== 'all') {
filtered = filtered.filter(item => item.room === selectedRoom.value)
@@ -1627,6 +1636,26 @@ const handleRefresh = async () => {
ElMessage.success('已刷新(已从数据库恢复队列)')
}
// 历史队列查询
const handleHistoryQuery = async () => {
if (!queryDate.value) {
ElMessage.warning('请选择查询日期')
return
}
console.log('【心内科】历史队列查询:', queryDate.value)
await loadQueueFromDb(queryDate.value)
if (isQueryingHistory.value) {
ElMessage.success(`已加载 ${queryDate.value} 的队列数据`)
}
}
// 回到今天
const handleTodayQuery = async () => {
queryDate.value = getTodayStr()
await loadQueueFromDb(getTodayStr())
ElMessage.success('已切换到今天队列')
}
// 退出
const handleExit = () => {
ElMessage.info('退出功能待实现')
@@ -2165,12 +2194,21 @@ onUnmounted(() => {
padding: 15px 20px;
border-bottom: 2px solid #409eff;
background-color: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
.panel-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.history-query {
display: flex;
gap: 8px;
align-items: center;
}
}
.table-container {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long