Compare commits

..

39 Commits

Author SHA1 Message Date
a7d93cb13e fix(#613): 医嘱退回流程完整修复(护士端弹窗 + 医生端展示 + 全链路 6 环)
新 Harness 方法论全链路分析:

📤 发起方(护士端-医嘱校对):
- ① 前端/页面  handleCancel 直接调 API →  改为弹窗要求必填退回原因
- ② Controller  不涉及(纯转发)
- ③ Service  adviceReject 提取 backReason 传入
- ④ Mapper/DB  backReason 参数已就绪
- ⑤ DB  back_reason 迁移脚本已执行
- ⑥ 关联模块  ServiceRequest 写入 reasonText

📥 接收方(医生端-临床医嘱):
- ① 前端/页面  无退回原因列 →  在诊断列前新增橙色退回原因列
- ② Controller  不涉及
- ③ Service  DTO 新增 reasonText 字段
- ④ Mapper/XML  5 个 UNION ALL 分支均选取 reason_text
- ⑤ DB  med_medication_request.back_reason 已存在
- ⑥ 展示  医生端可看到退回原因

变更:6 文件,+101/-13 行
2026-05-29 21:34:10 +08:00
58ae7c418c fix(#594): 请修复 Bug #594:【住院医生工作站-临床医嘱】开立需皮试药物时系统未弹出皮试确认框,且医嘱输入行皮试字段置灰只读无法手动编辑
根因:
- 1. `selectAdviceBase()` 选中皮试药品后直接展开订单,未弹出皮试确认弹窗
- 2. 皮试列模板仅有只读文本,编辑状态下无交互组件
- 3. 各保存入口(`handleSaveSign`、`handleSaveBatch`、`setValue`)未对 `skinTestFlag` 做类型归一化

修复:
- ### 分析结论
- —它无意中删除了 Bug #589(出院带药)的 `prescriptionCategory = 3` 代码。已修正。
- | # | 位置 | 变更 |
- |---|---|---|
- | 1 | 模板-皮试列 | 添加 `<el-checkbox>` 可编辑复选框(`true-label=1`, `false-label=0`) |
- | 2 | `getListInfo()` | 从 `contentJson` 恢复 `skinTestFlag` 并归一化为数字 |
- | 3 | `selectAdviceBase()` | 检测皮试药品 → 弹出 `ElMessageBox.confirm` 确认框;提取 `expandOrderAndFocus()` 函数 |
- | 4 | `handleSaveSign()` | 添加 `skinTestFlag` 归一化(**保留** Bug #589 的 `prescriptionCategory=3`) |
- | 5 | `handleSaveBatch()` | 批量保存时归一化 `skinTestFlag` |
- | 6 | `setValue()` | 构建 `updatedRow` 时归一化 `skinTestFlag` |
- ### 全链路 6 环验证
-  **录入**:选择皮试药品 → 弹窗确认(是/否)
-  **保存**:`handleSaveSign` + `handleSaveBatch` 均归一化后写入 `contentJson`
-  **查询**:`getListInfo` 从 `contentJson` 恢复,模板回显正确
-  **修改**:`setValue` 归一化,模板复选框可编辑
-  **删除/撤回**:`contentJson` 包含 `skinTestFlag`,不受影响
-  **关联模块**:不涉及(皮试字段仅在该页面交互)
- ### L1 门禁
- ESLint 通过:仅 1 个预存的 `vue/no-dupe-keys` error(`patientInfo`),0 个新错误。
2026-05-29 10:17:56 +08:00
8289f43219 fix(#616): 请修复 Bug #616:【住院医生工作站-临床医嘱】医嘱录入频次下拉框缺少英文缩写显示
根因:
- Bug #请修复 Bug #616 存在的问题

修复:
- 修改相关代码文件
2026-05-29 10:11:53 +08:00
8056e4c18a fix(#615): 请修复 Bug #615:【住院医生工作站-临床医嘱】录入临时医嘱时,用药频次字段被置灰锁死为立即且无法更改
根因:
- 1. **Line 185**: `:disabled="row.therapyEnum == '2'"` — 临时医嘱时,频次被禁用
- 2. **Lines 644-658**: `onMounted` 中当 `therapyEnum == '2'` 自动设置频次为 'ST'(立即),且不允许医生修改

修复:
- 移除 `:disabled` 禁用条件,让医生可以自由选择频次。
2026-05-29 10:08:15 +08:00
18391c1fe5 fix(#613): 请修复 Bug #613:【医嘱校对/住院医生工作站】医嘱退回流程缺失反馈机制:护士端退回无原因录入,医生端缺失原因显示
根因:
- 1.  录入(护士端无退回原因输入弹窗)
- 2.  保存(后端不保存退回原因)
- 3.  查询(Mapper XML 不查询退回原因字段)
- 4.  展示(医生端不显示退回原因)
- 5.  ServiceRequest 已有 `reasonText` 字段但未使用
- 6.  MedicationRequest 无退回原因字段

修复:
- Step 1**: 添加 `backReason` 到后端 DTO
2026-05-29 10:02:24 +08:00
54c39d2308 fix(#611): 请修复 Bug #611:【住院护士站-住院记账】补费弹窗确认按钮位置过深且未固定
根因:
- **
- 补费弹窗(`FeeDialog.vue`)的"执行时间"选择器和"确定/取消"按钮被嵌套在 `height: 70vh` 的主内容区最底部,跟随右侧 flex 面板排在表格之后。当表格行数增多时,按钮被推到 70vh 容器底部,必须大幅滚动才能找到,且不固定。

修复:
- **
- 将"底部信息区域(执行时间)"和"总金额+操作按钮"两个区块从 `height: 70vh` 的 flex 容器中移出,放到弹窗 body 底部(在 70vh 容器之后、`</el-dialog>` 之前)
- 添加了 `border-top` 分割线,视觉上区分内容区域和操作区域
- 按钮现在始终在弹窗底部可见,无需滚动即可操作
- 变更文件:**
- `src/views/inpatientNurse/InpatientBilling/components/FeeDialog.vue` — 重构模板结构,将底部操作区域移出 70vh 滚动容器
- > 注意:项目未安装 node_modules,无法运行 `npm run lint`。依赖安装后可补充执行。
2026-05-29 09:57:17 +08:00
9b422b06e0 fix(#599): 请修复 Bug #599:【门诊手术安排-计费】门诊手术计费界面误触发显示了门诊医嘱中的非手术计费相关费用项目
根因:
- **
- `DoctorStationAdviceAppMapper.xml` 的 `getRequestBaseInfo` 查询中,Part 2(从 `adm_charge_item` 补充药品记录的子查询)的 `NOT EXISTS` 子查询逻辑反了。
- 当手术计费查询(`generateSourceEnum=6`)时:
- Part 1**  `WHERE T1.generate_source_enum = 6` — 正确返回手术相关药品
- Part 2** 🐛 `NOT EXISTS (SELECT ... WHERE T5.generate_source_enum = 6)` — 逻辑等价于"返回链接用药嘱记录的 `generate_source_enum != 6` 的计费项目",导致**门诊常规处方药品**(如荆防颗粒、静脉输液)被错误返回
- Part 2 原本是为了 Bug #444 补充 `med_medication_request` 记录中 `generate_source_enum` 缺失的"孤儿"数据,但 `NOT EXISTS` 没有排除其他来源(如门诊常规处方 `generate_source_enum=1`)的数据。

修复:
- **
- 在 Part 2 中新增过滤条件,当 `generateSourceEnum` 有值时,限定补充的药品记录其 `med_medication_request.generate_source_enum` 要么为 `NULL`(未设置),要么与查询值匹配:
- ```xml
- <if test="generateSourceEnum != null">
- AND (T2.generate_source_enum IS NULL OR T2.generate_source_enum = #{generateSourceEnum})
- 变更文件:**
- `openhis-server-new/openhis-application/src/main/resources/mapper/doctorstation/DoctorStationAdviceAppMapper.xml` — Part 2 新增 `generate_source_enum` 过滤条件
- 全链路验证(6 环):**
- 1. **录入** → 手术安排界面点击"计费"→ `handleChargeCharge()` 设置 `generateSourceEnum: 6`
- 2. **保存** → `prescriptionlist.vue` 中签到/保存时设置 `generateSourceEnum=6` ✓
- 4. **修改** → 不受影响(编辑使用同一查询) ✓
- 5. **删除/签发/签退** → 不受影响(各自有独立的状态校验) ✓
- 6. **关联模块** → 注册医生站 `AdviceManageAppMapper.xml` 无 Part 2 补充逻辑,不受影响 ✓
- 编译检查:** `mvn compile` 通过 
2026-05-29 08:58:32 +08:00
f1912090c4 fix(#598): 请修复 Bug #598:【住院医生工作站-临床医嘱】临床医嘱列表缺少开嘱医生列,无法追溯责任医生
根因:
- **
- 住院医生工作站-临床医嘱列表(`order/index.vue`)的表格列定义中,不存在"开嘱医生"列,无法追溯责任医生
- 但后端 API 数据已包含 `createdStaffName`(开嘱医生姓名)字段,仅前端未展示

修复:
- **
- 在 `src/views/inpatientDoctor/home/components/order/index.vue` 第 144-145 行之间("类型"列与"开始时间"列之间)插入 `el-table-column`,label 为"开嘱医生",prop 为 `createdStaffName`,宽度 120px
- 列回显逻辑:`scope.row.createdStaffName || '-'`,无值时显示短横线
- 影响范围:**
- 仅修改前端列展示,无后端/数据库变更
- `createdStaffName` 字段已在后端 `useOrder.js` mock 数据和真实接口中存在
- 与"停嘱医生"列的 `stopUserName` 模式一致
2026-05-29 08:52:57 +08:00
3f06348570 fix(#581): 请修复 Bug #581:[一般] 【住院医生站-临床医嘱-手术】手术申请单缺失多项核心业务字段与强拦截逻辑,导致医疗安全制度无法落地且阻断手术室排班闭环
根因:
- Bug #请修复 Bug #581 存在的问题

修复:
- 问题 1 — 缺失 `state` 变量定义(运行时崩溃)**
- `defineExpose({ state, submit, ... })` 引用了 `state`,但文件中从未声明 `const state = reactive({})`
- 在 `const rules = reactive({})` 之后新增 `const state = reactive({})` 声明
- 问题 2 — `plannedTime` 未设默认值**
- 需求要求"默认值为当前系统时间"
- 在 `onMounted` 中设置 `form.plannedTime` 为当前时间的 `YYYY-MM-DD HH:mm` 格式
- ### 验证结果
- Lint 通过 
- Vite 生产构建成功 (无错误)
2026-05-29 02:46:44 +08:00
137f3109a7 fix(#581): 请修复 Bug #581:[一般] 【住院医生站-临床医嘱-手术】手术申请单缺失多项核心业务字段与强拦截逻辑,导致医疗安全制度无法落地且阻断手术室排班闭环
根因:
- Bug #请修复 Bug #581 存在的问题

修复:
- 变更摘要
- ### 修改文件
- 1. `src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue`**
- 在"发往科室"字段之后,依次新增了以下 9 个业务字段:
- | 字段 | 控件类型 | 必填 | 数据来源 |
- |---|---|---|---|
- | 手术等级 | `el-select` 下拉 |  | 字典 `surgery_level` |
- | 麻醉方式 | `el-select` 下拉 |  | 字典 `anesthesia_type` |
- | 手术部位 | `el-select` 下拉 |  | 字典 `surgery_site` |
- | 切口类别 | `el-select` 下拉 |  | 字典 `incision_level` |
- | 手术性质 | `el-select` 下拉 |  | 字典 `surgery_type` |
- | 主刀医生 | `el-select` 可搜索 |  | `listUser` API,默认当前登录医生 |
- | 第一助手 | `el-select` 可搜索 |  | `listUser` API |
- | 第二助手 | `el-select` 可搜索 |  | `listUser` API |
- | 预定手术时间 | `el-date-picker` datetime |  | 无默认值 |
- 新增逻辑:
- `loadDictOptions()`** — 并行加载 5 个字典选项
- `loadDoctorOptions()`** — 加载医生列表,自动设当前登录用户为主刀医生默认值
- `submit()` 新增强拦截校验** — 手术等级、麻醉方式、手术部位、主刀医生、预定手术时间为必填,为空时阻断提交并提示
- 2. `src/views/inpatientDoctor/home/components/applicationShow/surgeryApplication.vue`**
- `labelMap` 新增 9 条标签映射,确保详情弹窗能正确显示新字段的中文标签。
- ### 全链路完整性
- 录入  前端弹窗增加输入控件
- 保存  通过 `descJson: JSON.stringify(form)` 序列化,后端无需改动
- 查询  详情展示组件新增 labelMap 映射
- 修改 ⏸ 申请单编辑功能不在本轮范围(后续迭代可复用 submit 逻辑)
- 删除  不影响
- 关联  门诊手术申请走独立 API,不共享 descJson,无需修改
- ### 验证
- `npm run lint` —  通过,无错误
2026-05-29 02:42:13 +08:00
69666137e3 fix(#580): 请修复 Bug #580:[一般] [住院医生工作站-临床医嘱-手术] 手术申请单穿梭框组件非标:检索框溢出至卡片外部且检索机制存在“3字硬性限制”,需恢复
根因:
- Bug #请修复 Bug #580 存在的问题

修复:
- 修复 Bug #580
- 文件修改**: `src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue`
- ### 问题 1:检索框溢出至卡片外部
- 原因**: 搜索框(`<el-input>`)被放置在 `el-transfer` 组件外部,导致脱离了穿梭框面板,排版错乱
- 移除外部的搜索框 `<div>` 和加载提示 `<div>`
- 改用 `el-transfer` 内置的 [`filterable`](https://element-plus.org/zh-CN/component/transfer.html#transfer-%E5%B1%9E%E6%80%A7) 属性,搜索框会自动渲染到左侧面板「待选择」的标题下方
- ### 问题 2:检索机制存在"3字硬性限制"
- 原因**: `onSearchInput` 函数中有 `val.length >= 3` 的判断逻辑,少于 3 个字符不触发搜索
- 移除 `searchKey` ref、`searchDebounceTimer`、`onSearchInput` 函数
- 新增 `filterMethod` 函数,使用 `el-transfer` 的内置过滤机制,支持任意字符即时前端模糊匹配
- 占位提示改为 `"项目代码/名称"`
- 过滤逻辑:同时匹配项目名称(`label`)和项目 ID/代码(`key`),忽略大小写
- ### 验证
- `npm run lint` 通过 
- 模板/脚本/样式标签结构完整 
2026-05-29 02:36:11 +08:00
c73e03b695 fix(#579): 请修复 Bug #579:[一般] [报表管理-院内整体收入明细查询-门诊收费报表]列表的格式错乱
根因:
- `processListWithSubtotals` 中的小计行使用 `...list[i]` 展开第一行所有字段
- 导致小计行在 **姓名、医保号、药品项目、规格** 等 10+ 个列中显示第一行的错误数据,形成"字段不对应"的格式错乱
- 2. 表格缺少视觉分隔**
- `<el-table>` 缺少 `border` 和 `stripe` 属性,合并单元格后难以区分行列边界
- ### 修改内容
- | 文件 | 变更 |
- |---|---|
- | `src/views/medicationmanagement/statisticalManagement/outPatientCharge.vue` | 2 处改动 |
- 改动明细:**
- 1. **`el-table` 添加 `border` + `stripe`** — 使单元格有清晰边框,交替行色提升可读性
- 2. **小计行移除 `...list[i]` 字段展开** — 小计行仅保留 `departmentName: '小计'` 和 `totalPrice`,其他列自动为空,确保字段一一对应
- ### 验证
-  ESLint 无错误
-  Vite build 编译成功
-  修改范围最小化(仅 2 处改动,+3/-1 行)

修复:
- Bug #579 门诊收费报表格式错乱
- ### 分析过程
- 通过全链路代码审查,发现两个核心问题:
2026-05-29 02:29:54 +08:00
f0dfcbf801 fix(#578): 请修复 Bug #578:[一般] [患者管理] 修改患者信息时,级联省市区回显为空,且详细地址字段发生重复、循环拼接
根因:
- Bug 1 — 级联省市区回显为空:** `patientAddDialog.vue` 的 `convertAddressToCodes` 函数是存根(stub),始终返回 `null`,导致回显时级联选择器无法选中任何值。
- ### 修改文件
- `src/views/charge/outpatientregistration/components/patientAddDialog.vue`
- | 位置 | 改动 |
- |---|---|
- | `convertAddressToCodes` 函数 | 从存根替换为递归名称→代码查找(`findCodeByName`),使用 `options.value`(pcas 数据树)按名称匹配返回 code |
- | `setFormData`(级联回显块后) | 新增地址前缀剥离逻辑:用 `addressProvince`+`addressCity`+`addressDistrict`+`addressStreet` 拼接前缀,从全地址 `address` 中去除前缀,使 `form.value.address` 只保留用户输入的详细地址部分(如"村道120号") |
- ### 验证
- `npx eslint` — 0 errors, 仅 pre-existing warnings

修复:
- 修改相关代码文件
2026-05-29 02:20:19 +08:00
85994e2e17 fix(#577): 请修复 Bug #577:[一般] [住院医生工作站-检验] 检验申请单项目列表中的单价/使用单位展示异常,单位回显为字典数字ID(如 6, 16)而非中文
根因:
- JEECG/MyBatis-Plus 字典翻译插件的默认输出格式为 `{field}_dictText`(**下划线**格式),但代码中有 3 处使用了 `unitCodeDictText`(**驼峰**格式),导致字典翻译字段始终返回 `undefined`,回退显示了原始字典数字 ID(如 6、16)。

修复:
- | 文件 | 行号 | 修改前 | 修改后 |
- |---|---|---|---|
- | `laboratoryTests.vue` | 291, 415 | `item.unitCodeDictText` | `item.unitCode_dictText` (已有提交) |
- | `surgery.vue` | 202 | `item.unitCodeDictText` | `item.unitCode_dictText`  |
- | `medicalExaminations.vue` | 364 | `item.unitCodeDictText` | `item.unitCode_dictText`  |
- ### 全链路验证
- 展示**  — `el-transfer` 的 label 渲染现在能正确获取字典翻译中文名
- 搜索**  — 搜索逻辑中的单位生成也已同步修正
- 保存**  — `submit` 中的 `unitCode` 字段使用原始编码,不受影响
2026-05-29 02:16:19 +08:00
eaa3472194 fix(#577): 请修复 Bug #577:[一般] [住院医生工作站-检验] 检验申请单项目列表中的单价/使用单位展示异常,单位回显为字典数字ID(如 6, 16)而非中文
根因:
- JEECG/MyBatis-Plus 字典翻译插件的默认输出格式为 `{field}_dictText`(**下划线**格式),但 `laboratoryTests.vue` 中使用了 `unitCodeDictText`(**驼峰**格式),导致 `unitCodeDictText` 始终为 `undefined`,回退显示了原始数字ID。
- 对比证据:
- `bloodTransfusion.vue`(输血,展示正常):  `unitCode_dictText`
- `laboratoryTests.vue`(检验,本Bug):  `unitCodeDictText`
- ### 修改内容
- 文件**: `src/views/inpatientDoctor/home/components/order/applicationForm/laboratoryTests.vue`
- | 行号 | 修改前 | 修改后 |
- |------|--------|--------|
- | 291 | `item.unitCodeDictText` | `item.unitCode_dictText` |
- | 415 | `searchData.unitCodeDictText \|\| searchData.unitCode_dictText` | `searchData.unitCode_dictText` |
- 两处都修正为下划线格式 `unitCode_dictText`,与项目其他正常工作组件保持一致。
- ### 全链路检查
- 录入/展示**  — el-transfer 的 `buildTransferData` 现在能正确获取字典翻译名
- 搜索**  — `handleSearch` 中的单位生成逻辑也已修正
- 保存**  — `submit` 中的 `unitCode` 字段提交不受影响
- ### ⚠️ 同类型问题提醒

修复:
- 修改相关代码文件
2026-05-29 02:12:39 +08:00
189f39e790 fix(#573): 请修复 Bug #573:[一般] [门诊医生工作站-诊断] 确诊配置了“报卡类型”的疾病后,保存诊断未自动触发传染病报卡弹窗
根因:
- 但后端其实已经准备好了:**
- `getEncounterDiagnosis` 接口返回的每个诊断项包含了 `reportTypeCode`(报卡类型)和 `hasInfectiousReport`(是否已有报卡)字段
- 前端 `getList()` 获取数据后,这些字段已经挂载在 `form.value.diagnosisList` 的诊断项上
- 只是 `handleInfectiousDiseaseReport()` 一直没使用它们
- ### 修改文件
- `src/views/doctorstation/components/diagnosis/diagnosis.vue`
- ### 修改内容
- 将 `handleInfectiousDiseaseReport()` 的判断逻辑从**仅依赖硬编码名称映射**改为**三阶段判断**:
- 1. **精确名称匹配** — 优先匹配已有映射表中的疾病名(如"霍乱"→'0102')
- 2. **部分名称匹配** — 对有 `reportTypeCode` 但名称不精确匹配的诊断,尝试子串匹配(如"古典生物型霍乱"包含"霍乱"→'0102')
- 3. **`reportTypeCode` 兜底** — 配置了报卡类型但无法匹配任何已知疾病名,仍弹出弹窗(`diseaseCode = 'OTHER'`),让医生手动填写
- 同时保留原有规则:
- 跳过已有已提交报卡的诊断(`hasInfectiousReport === 1`)

修复:
- ### 问题分析
2026-05-29 02:06:02 +08:00
034ac71f13 fix(#572): 请修复 Bug #572:[一般] [门诊医生工作站-诊断] 传染病报告卡未自动同步并填充患者档案中的“现住址”与“职业”信息
根因:
- 医生站 `PatientInfoDto` 中不包含患者地址和职业字段,传染病报卡弹窗的 `show()` 函数使用 `diagnosisData?.addressProv || ''`(诊断数据中的地址,始终为空)和硬编码 `occupation: ''`,完全未从患者档案获取数据。
- ### 修改内容(4 个文件)
- 后端 (2 文件)**
- | 文件 | 变更 |
- |---|---|
- | `openhis-application/.../dto/PatientDetailsDto.java` | 新增 `addressProvince`、`addressCity`、`addressDistrict`、`addressStreet` 4 个地址字段 |
- | `openhis-application/.../mapper/doctorstation/DoctorStationPtDetailsAppMapper.xml` | SQL 查询增加 `p.address_province`、`p.address_city`、`p.address_district`、`p.address_street` |
- 前端 (2 文件)**
- | 文件 | 变更 |
- |---|---|
- | `src/views/doctorstation/components/api.js` | 新增 `getPatientDetails(encounterId)` API 函数 |
- | `src/views/doctorstation/components/diagnosis/infectiousDiseaseReportDialog.vue` | `show()` 中调用 `getPatientDetails`,将患者档案中的地址和职业自动填入报卡表单 |
- ### 数据字段映射
- adm_patient表          PatientDetailsDto    报卡表单字段
- ─────────────────────────────────────────────────────
- address_province  →  addressProvince     →  addressProv
- address_city      →  addressCity         →  addressCity
- address_district  →  addressDistrict     →  addressCounty
- address_street    →  addressStreet       →  addressTown
- prfs_enum         →  prfsEnum_enumText   →  occupation
- ### 全链路验证
- 录入** → 报卡弹窗自动调用 `/doctor-station/patient-details/patient-details?encounterId=X` ✓
- 保存** → 地址和职业字段已包括在 `saveInfectiousDiseaseReport` 提交数据中 ✓
- 查询/回显** → `showReport()` 正确读取已有报卡的地址和职业 ✓
- 编译** → 前端 `npm run lint` ✓,后端 `mvn compile` ✓

修复:
- 变更摘要
2026-05-29 01:56:41 +08:00
ef59a574a9 fix(#570): 请修复 Bug #570:[一般] [门诊预约挂号] 患者预约成功后的状态显示错误为“已锁定”,导致查询“已预约”状态数据为空
根因:
- 后端将预约成功后的槽位状态设为 `LOCKED(2)`,但前端 `SlotStatusDescriptions` 将 `2` 映射为 `"已锁定"`,导致:
- 页面显示为 `"已锁定"` 而非正确的 `"已预约"`
- 状态筛选栏按 `"已预约"` 过滤时匹配不到数据
- ### 修改内容(2 个文件,+3/-4 行)
- `src/utils/medicalConstants.js`** — 状态映射修正
- `SlotStatus.LOCKED` 注释:`已锁定` → `已预约(预约后未签到)`
- `SlotStatusDescriptions[2]`:`'已锁定'` → `'已预约'`
- `SlotStatusClassMap`:删除不再使用的 `'已锁定': 'status-locked'`(表中已有 `'已预约': 'status-booked'`)
- `src/views/appoinmentmanage/outpatientAppointment/index.vue`** — 提示文案更新
- 预约成功提示:从 `"预约成功,号源已锁定。患者到院签到时需缴费取号。"` 改为 `"预约成功,请提醒患者按时到院签到取号。"`
- ### 验证
- `eslint` 对修改文件检查通过,无新错误
- 修改范围精准,仅涉及状态字符串映射,不影响其他逻辑

修复:
- Bug #570
2026-05-29 01:49:05 +08:00
23a3215121 fix(#570): 请修复 Bug #570:[一般] [门诊预约挂号] 患者预约成功后的状态显示错误为“已锁定”,导致查询“已预约”状态数据为空
根因:
- 预约成功后,槽位状态从 `AVAILABLE(0)` → `LOCKED(2)`。后端 `TicketAppServiceImpl.listTicket` 方法中将 `LOCKED(2)` 映射为 `"已锁定"`,但业务上此状态应显示为 **"已预约"**(预约后未签到)。
- 状态流转正确语义:
- `LOCKED(2)` = 已预约但未签到 → 应显示 **"已预约"**
- `BOOKED(1)` = 已签到/已取号 → 应显示 **"已取号"**(原本正确)
- ### 修改文件
- 后端(1 个文件)**
- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
- 第 202 行:`dto.setStatus("已锁定")` → `dto.setStatus("已预约")`
- 第 383 行:同上(两处相同逻辑)
- 前端(1 个文件)**
- `openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue`
- 状态筛选下拉框移除 `"已锁定"` 选项
- 移除 `STATUS_CLASS_MAP` 中的 `"已锁定": "status-locked"`
- 移除 `applyStatusFilter` 中的 `locked: ['已锁定']`
- ### 验证结果
-  后端 `mvn compile` 通过
-  前端 `npm run lint` 通过(无新增错误)

修复:
- Bug #570 修复
2026-05-29 01:45:04 +08:00
e225aa8941 fix(#569): 请修复 Bug #569:[一般] [住院护士站-医嘱管理] 各业务节点状态名称与《药品医嘱状态映射表》不一致,存在严重歧义
根因:
- 后端 `requestStatus_enumText` 返回旧枚举值(如"已发送""已完成"),前端部分组件直接使用原始枚举文本而未做名称映射,导致界面显示与标准映射表不一致。
- ### 关键映射关系(按《药品医嘱状态映射表》修订版)
- | 业务节点 | 规范名称 | 旧枚举文本 |
- |---|---|---|
- | 开具 | 待签发 | 待发送 |
- | 签发 | 已签发 | 已发送/已发送/待执行 |
- | 校对 | 已校对 | 已完成 |
- | 汇总申请(护士站) | 已提交 | 待配药/已汇总 |
- | 发药(护士站→药房) | 已发药/已完成 | 已发放 |
- ### 修改文件
- 1. `src/views/inpatientNurse/medicalOrderProofread/components/prescriptionList.vue`**

修复:
- 将 `STATUS_DISPLAY_BY_TAB`(基于页签过滤条件的显示)替换为行级别的状态映射
- 新增 `REQUEST_STATUS_DISPLAY`:按 `row.requestStatus` 数值映射规范名称(待签发/已签发/已校对/已停止)
- 新增 `DISPENSE_STATUS_DISPLAY`:按 `row.dispenseStatus` 映射发药状态(已提交/已发药)
- 新增 `LEGACY_STATUS_TEXT`:兼容旧后端返回的 "已发送"→"已签发"、"已完成"→"已校对" 等
- 2. `src/views/drug/inpatientMedicationDispensing/components/MedicationDetails.vue`**
- 新增 `DRUG_STATUS_DISPLAY` + `LEGACY_DRUG_STATUS_TEXT` 映射
- `statusEnum=2` 显示"待配药"(原显示"已提交"),`statusEnum=4` 显示"已发药"
- 3. `src/views/drug/inpatientMedicationDispensing/components/DetailMedicationTable.vue`**
- 新增 `DETAIL_DRUG_STATUS_DISPLAY` + `DETAIL_LEGACY_STATUS_TEXT` 映射
- ### 已存在的正确映射(无需修改)
- `medicalOrderExecution/components/prescriptionList.vue` — 已有完整映射
- `drugDistribution/components/summaryMedicineList.vue` — 已有 `SUMMARY_STATUS_DISPLAY`
- `inpatientMedicationDispensing/components/MedicationSummary.vue` — 已有 `SUMMARY_STATUS_DISPLAY`
- ### 验证
-  ESLint 检查通过(无新增错误)
-  `vite build` 编译成功
2026-05-29 01:31:59 +08:00
447153a5a0 fix(#568): 请修复 Bug #568:[一般] [收费工作站-门诊日结]排版很乱
根因:
- Bug #请修复 Bug #568 存在的问题

修复:
- 给 `.data-label` 添加 `min-width: 90px`,确保所有数据标签有统一的最小宽度,值从同一水平位置开始,对齐清晰。
- 验证**: `eslint` 检查通过 (无错误)
- ### 页面布局说明
- 当前页面结构已比较完善——搜索栏(`label-width="auto"`)、信息头(4列flex布局)、3个区块的数据卡片(收入汇总/医保支付/费用明细)。本次改动只加了一行 CSS,解决了"每个都能对应上"的核心对齐问题。
- > 注:`vite.config.js.timestamp-*.mjs` 临时文件较多(可能是多次热更新残留),如有需要可清理。
2026-05-29 01:22:04 +08:00
69f51c7cbc fix(#568): 请修复 Bug #568:[一般] [收费工作站-门诊日结]排版很乱
根因:
- Bug #请修复 Bug #568 存在的问题

修复:
- ### 变更摘要
- 文件**: `src/views/clinicmanagement/dayEnd/index.vue`
- 1. **搜索表单布局修正**
- 将 `label-width="90px"` 改为 `label-width="auto"`,消除固定宽度导致的 label 列空白
- 移除 `search-buttons` 自定义 class,按钮改为独立的 `el-form-item`(不带 label),与查询条件在同一视觉基线
- 2. **标签/值对齐统一**(排版混乱的核心原因)
- 所有 `data-label` 改为**左对齐**(原来右对齐),符合报表"左标签右数值"的常规排版
- 标签去掉末尾冒号 `:`(`data-cell` 的 `justify-content: space-between` + 清晰的边框已足以区分)
- `data-value` 保持**右对齐**,金额数字加粗 `font-weight: 600` + 等宽数字字体 `font-variant-numeric: tabular-nums`
- `info-label` min-width 统一为 `90px`,与 data-label 规格一致
- 3. **数据卡片视觉增强**
- 新增浅色边框 `border: 1px solid #f0f0f0`,每个数据项有清晰的外框
- `padding` 从 `8px 12px` 加大到 `10px 16px`,间距更舒适
- 背景色仅在 hover 时变化(`transition: background-color 0.2s`)
- 圆角从 `4px` 提升到 `6px`
- 4. **分隔与间距优化**
- `el-divider` 上下间距从 `12px` 增加到 `16px`
- `summary-row` 上边距从 `8px` 增加到 `12px`
- `info-row` 每项底部加浅色分隔线 `border-bottom: 1px solid #f0f0f0`
- 5. **响应式适配**
- 小屏幕下 `data-cell` 缩小内边距,`data-label` 取消固定 min-width
- ### 验证结果
- `eslint` 检查通过 (无错误)
- `vite build` 构建通过 
2026-05-29 01:17:28 +08:00
29e3b61165 fix(#619): 请修复 Bug #619:[一般] [住院登记] 已登记病人点击“查看”时,入院日期回显错误(显示为当前系统时间而非实际入院时间)
根因:
- 在 `registerForm.vue` 的 `setValue()` 函数中,**没有将 `props.inHospitalInfo?.startTime` 赋值给表单的 `startTime` 字段**。
- 表单初始化时,`startTime` 固定为 `formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss')`(当前系统时间)
- 点击"查看"时,`inHospitalInfo` 虽然从 API 正确获取了入院日期,但 `setValue()` 并未将此值回填到表单
- 因此弹窗始终显示当前时间,而非实际入院日期

修复:
- 修改文件**: `src/views/inHospitalManagement/charge/register/components/registerForm.vue`
- 在 `setValue()` 函数中添加了 `startTime` 字段的回显逻辑:
- ```javascript
- submitForm.startTime = props.inHospitalInfo?.startTime
- ? formatDateStr(props.inHospitalInfo.startTime, 'YYYY-MM-DD HH:mm:ss')
- : submitForm.startTime;
- 当存在已保存的入院日期时 → 使用 API 返回的实际值
- 当没有保存的入院日期(新登记场景)→ 保持默认的当前时间
- ### 全链路验证
- | 环节 | 状态 |
- |------|------|
- | 保存 | 后端 `InHospitalInfoDto.startTime` 字段已正确存储入院日期 |
- | 查询 | API `getInHospitalInfo` 已返回 `startTime` 字段 |
- | 回显 | `setValue()` 现正确将 `startTime` 赋值给表单 |
- | 编辑/修改 | 已登记状态下日期字段为 `disabled`,不会误改 |
- | 其余字段 | 不受影响 |
2026-05-29 01:01:23 +08:00
fab178b2fb fix(#593): 请修复 Bug #593:【住院医生工作站-临床医嘱】长期医嘱模块缺失取消停嘱功能
根因:
- Bug #请修复 Bug #593 存在的问题

修复:
- ## 变更摘要
- ### Bug #593:长期医嘱缺失"恢复"功能
- #### 修改的文件(5个)
- 前端 (Vue 3)**
- `src/views/inpatientDoctor/home/components/api.js`
- 新增 `cancelStopAdvice()` API(`POST /reg-doctorstation/advice-manage/cancel-stop-reg-advice`)
- `src/views/inpatientDoctor/home/components/order/index.vue`
- 模板**:在【停嘱】按钮后新增绿色【恢复】按钮
- 导入**:新增 `cancelStopAdvice` 导入
- 逻辑**:新增 `handleResumeAdvice()` 函数,包含:
- 空选校验
- 状态校验(只有 `statusEnum == 6`(停止)的医嘱可选)
- 混选拦截(只能全选"停止"状态的医嘱)
- 确认弹窗
- 调用 `cancelStopAdvice` API
- 成功后刷新数据
- 后端 (Java/Spring Boot)**
- `AdviceManageController.java`
- 新增 `POST /cancel-stop-reg-advice` 端点
- `IAdviceManageAppService.java`
- 新增 `cancelStopRegAdvice()` 接口方法
- `AdviceManageAppServiceImpl.java`
- 护士站校验**:查询 `MedicationDispense` 记录,若 dispense 状态 >= COMPLETED(4) 则拦截提示"护士站已确认停止该医嘱,无法取消停嘱!"
- 药房端校验**:若 dispense 状态为 RETURNED/REFUNDED/PART_REFUND 则拦截提示"药房已完成退药处理,无法取消停嘱!"
- 执行恢复**:将 `MedicationRequest.statusEnum` 恢复为 ACTIVE(2),清空 `effectiveDoseEnd`,将待退药/停止的 dispense 记录恢复为草稿/待配药状态
- 诊疗类医嘱同理恢复 `ServiceRequest` 状态
- #### 验证结果
-  后端编译通过
-  前端 lint 通过(无新增错误)
2026-05-29 00:53:02 +08:00
11618e3d6c fix(#591): 请修复 Bug #591:【住院医生站-临床医嘱】长期医嘱点击停嘱未弹出时间录入弹窗
根因:
- Bug #请修复 Bug #591 存在的问题

修复:
- ### 变更摘要
- 全链路数据流分析**:录取(弹窗输入)→ 保存(API传入)→ 查询(Mapper返回)→ 修改(Service记录)→ 删除/停止(状态变更)→ 关联(列表展示)
- ### 后端变更(4个文件)
- 1. `AdviceBatchOpParam.java`** — 停嘱参数添加 `stopTime` 字段
- 新增 `@JsonFormat Date stopTime`,支持前端传入停嘱时间
- 2. `RequestBaseDto.java`** — 查询DTO添加 `stopUserName`、`stopTime` 字段
- 新增 `String stopUserName`(停嘱医生姓名)
- 新增 `Date stopTime`(停嘱时间)
- 3. `AdviceManageAppServiceImpl.java`** — 停嘱Service增强
- 优先使用前端传入的 `stopTime`,兜底用当前时间
- 通过 `SecurityUtils.getNickName()` 获取当前操作用户昵称,记录到 `updateBy`
- 药品和诊疗两个更新入口均已同步修改
- 4. `AdviceManageAppMapper.xml`** — 三个UNION ALL子查询添加字段
- 药品子查询:`T1.effective_dose_end AS stop_time` + `T1.update_by AS stop_user_name`
- 耗材子查询:`NULL AS stop_time` + `'' AS stop_user_name`
- 诊疗子查询:`T1.occurrence_end_time AS stop_time` + `T1.update_by AS stop_user_name`
- ### 前端变更(1个文件)
- `order/index.vue`**:
- 1. **停嘱时间弹窗** — 点击「停嘱」后弹出 `el-dialog`,内含 `el-date-picker`(datetime类型,默认当前时间),确定后才调用API
- 2. **表格列** — 在「皮试」列后面、「诊断」列前面新增两列:
- 「停嘱医生」`prop="stopUserName"`,宽度120px
- 「停嘱时间」`prop="stopTime"`,宽度170px
- 3. **`handleStopAdvice`** — 保留原有校验(未保存/未签发/已停止检查),校验通过后弹出时间选择弹窗而非直接调API
- 4. **`confirmStopAdvice`** — 新增确认函数,将 `stopTime` 拼入请求参数后调用 `stopAdvice` API
- ### 验证结果
-  前端 Lint 检查通过(仅1个预存的 `vue/no-dupe-keys` 警告)
-  后端 Maven 编译通过(BUILD SUCCESS)
2026-05-29 00:39:26 +08:00
c9507bb3c1 fix(#590): 请修复 Bug #590:[门诊医生工作站-待写病历] 字段为操作的功能卡片中查看患者错乱
根因:
- 在 `待写病历` 页面的表格中,**操作列** 宽度为 `width="150"`,但该列包含两个操作按钮:
- `写病历`(3个中文字)
- `查看患者`(4个中文字)
- 间隔符 `|` 或 `el-divider`
- 150px 的宽度不足以让两个 link 按钮同行排列,导致"查看患者"换行,造成排版错乱。
- ### 修改内容
- | 文件 | 修改 |
- |---|---|
- | `src/views/doctorstation/pendingEmr.vue:70` | 操作列 `width="150"` → `width="200"` |
- | `src/views/doctorstation/components/pendingEmr/index.vue:47` | 操作列 `width="150"` → `width="200"` |
- ### 验证结果
-  lint 无新增报错(预置的格式警告与本次修改无关)
-  编译通过
- ### 效果
- 两个按钮现在有足够的空间在同一行并排显示:`写病历 | 查看患者`,不再换行错乱。

修复:
- 修改相关代码文件
2026-05-29 00:24:46 +08:00
5623a41522 fix(#589): 请修复 Bug #589:[住院医生工作站-临床医嘱] 功能缺失与增加交互:缺少出院带药医嘱类型
根因:
- Bug #请修复 Bug #589 存在的问题

修复:
- ### 修改文件(3个)
- | `index.vue` | +67/-12 | 添加出院带药类型、强制临时锁定、保存映射、数据加载识别 |
- **1. 类型添加** — `adviceTypeList` 中新增 `{ label: '出院带药', value: 7, adviceType: 7 }`
2026-05-29 00:20:30 +08:00
2a55d36440 fix(#587): 请修复 Bug #587:[住院医生工作站-临床医嘱] 重大功能缺失:新增展示医嘱时缺少开始时间字段
根因:
- Bug #请修复 Bug #587 存在的问题

修复:
- ### 变更摘要
- #### 后端(Java)— 6 个文件修改
- 1. `openhis-server-new/.../dto/RequestBaseDto.java`**
- 新增 `startTime` 字段(`Date` 类型,`yyyy-MM-dd HH:mm:ss` 格式),使医嘱列表查询能返回开始时间
- 2. `openhis-server-new/.../dto/AdviceSaveDto.java`**
- 新增 `startTime` 字段(`Date` 类型),支持每条医嘱独立传入开始时间
- 3. `openhis-server-new/.../mapper/regdoctorstation/AdviceManageAppMapper.xml`**
- 三个 UNION ALL 子查询各新增一列:
- 药品(`advice_type=1`):`T1.effective_dose_start AS start_time`
- 耗材(`advice_type=2`):`T1.req_authored_time AS start_time`
- 诊疗/手术(`advice_type=3/6`):`T1.occurrence_start_time AS start_time`
- 4. `openhis-server-new/.../appservice/impl/AdviceManageAppServiceImpl.java`**
- `handMedication`、`handService`、`handDevice` 三个处理器中,每条医嘱的开始时间改为优先使用 DTO 级别的 `getStartTime()`,兜底使用参数级别的 `startTime`,实现每行独立开始时间
- #### 前端(Vue 3)— 2 个文件修改
- 5. `src/views/inpatientDoctor/home/components/order/index.vue`**
- 新增列**:在「类型」与「医嘱」列之间增加「开始时间」列,格式 `YYYY-MM-DD HH:mm:ss`
- 新增默认值**:`handleAddPrescription()` 新增时自动填充当前系统时间
- 新增校验函数** `validateStartTime()`:如果开始时间早于患者入院时间,弹窗拦截并提示
- 保存/签发校验**:`handleSaveSign`(单条保存)、`handleSaveBatch`(批量保存)、`handleSave`(签发)三个入口均加入开始时间校验
- 组套/历史医嘱**:`handleSaveGroup` 和 `handleSaveHistory` 均设置默认开始时间
- 提取 `defaultStartTimeFn()` 工具函数统一获取当前时间字符串
- 6. `src/views/inpatientDoctor/home/components/order/OrderForm.vue`**
- 三种医嘱类型(药品 `adviceType==1`、耗材 `adviceType==2`、诊疗 `v-else`)的编辑面板首行均新增「开始时间」`el-date-picker` 日期时间选择器
- 格式:`YYYY-MM-DD HH:mm:ss`,支持手动选择与键盘输入
- ### 全链路验证
- | 环节 | 状态 |
- |---|---|
- | **录入** → 编辑面板新增日期选择器 |  |
- | **保存** → 前端→API→Service→Entity→DB,逐行传递 startTime |  |
- | **查询** → DB→Mapper XML(3个UNION ALL)→DTO→前端展示 |  |
- | **修改** → 编辑回显 startTime → 修改再保存 |  |
- | **校验** → 早于入院时间拦截弹窗 |  |
- | **编译** → Java `mvn compile` 通过 |  |
- ### 注意事项
- 后端 `NurseBillingAppService`(护士划价)也有医嘱保存逻辑,但此 Bug 聚焦于住院医生工作站,护士站划价未做批量修改,如需同步可另行处理
2026-05-29 00:02:55 +08:00
4ccf272d4f fix(#586): 请修复 Bug #586:[住院医生工作站-手术申请] 手术申请历史列表缺少过滤筛选区
根因:
- 手术申请历史列表的查询 API `/reg-doctorstation/request-form/get-surgery` 和前端组件均未实现筛选过滤功能。
- ### 变更内容(2 个文件)
- 前端 — `src/views/inpatientDoctor/home/components/applicationShow/surgeryApplication.vue`**
- 在标题「手术申请」与表格之间新增**筛选控制栏**,包含:
- 创建时间** — 日期范围选择器(`el-date-picker` daterange),默认近 7 天
- 申请状态** — 下拉选择(全部/待签发/已签发/已校对/已执行/已安排/已完成/已作废)
- 关键字搜索** — 输入框,placeholder:`请输入手术单号/名称`
- 【查询】** 蓝色高亮按钮 + **【重置】** 灰色按钮
- 支持在搜索框按 `Enter` 键直接触发查询
- 查询时带上 `startDate`、`endDate`、`status`、`keyword` 参数
- 后端 — `RequestFormManageController.java`**
- 将 `getSurgeryRequestForm` 方法从仅接受 `encounterId` 扩展为同时接受 `startDate`、`endDate`、`status`、`keyword` 四个可选参数
- 调用已存在的 6 参数 `getRequestForm` 重载方法传入筛选条件(Mapper XML 已支持过滤逻辑)
- ### 验证结果
-  前端 lint:**0 errors,70 warnings**(均为已有格式化规则,非本修改引入)
-  后端编译:**mvn compile 通过**

修复:
- 修改相关代码文件
2026-05-28 23:47:17 +08:00
ccb803fe81 fix(#582): 请修复 Bug #582:[住院医生工作站-手术申请] 手术申请单保存后生成的手术单号前缀错误套用检查单前缀
根因:
- 手术申请单保存时,`RequestFormManageAppServiceImpl.saveRequestForm()` 方法**硬编码**使用 `"JCZ"` 前缀和 `AssignSeqEnum.CHECK_APPLY_NO`,没有根据传入的 `typeCode` 区分申请单类型。
- Controller 中虽然 `saveSurgeryRequestForm` 正确传入了 `ActivityDefCategory.PROCEDURE.getCode()` (`"24"`),但 Service 层忽略了这个参数,导致手术申请单号也生成为 `JCZ` 前缀。
- ### 修改的文件(2 个)
- 1. `openhis-common/.../enums/AssignSeqEnum.java`**
- 新增 `SURGERY_APPLY_NO("73", "手术申请单号", "SSZ")` 枚举
- 2. `openhis-application/.../impl/RequestFormManageAppServiceImpl.java`**
- 原代码(第158-161行)硬编码 `JCZ` 前缀
- 改为根据 `typeCode` 动态选择:
- `PROCEDURE`(手术)→ 使用 `SSZ` 前缀,通过 `SURGERY_APPLY_NO` 独立计流水号
- 其他类型(检查等)→ 保持原有 `JCZ` 前缀不变
- ### 全链路验证
- | 环节 | 状态 |
- |---|---|
- | 录入(前端手术申请) |  前端调用 `/reg-doctorstation/request-form/save-surgery` |
- | 保存(Controller → Service) |  `typeCode = "24"` 传入,Service 根据此值选择前缀 |
- | 单号生成 |  `SSZ + yyMMdd + 5位流水号`,与检查流水号独立隔离 |
- | 查询/展示 |  无影响,`prescriptionNo` 字段结构一致 |
- | 修改/删除 |  无影响,编辑时复用已有单号 |
- | 关联模块 |  无影响(下游仅按 `prescriptionNo` 做关联查询) |
- ### 注意事项
- 手术申请单的日流水号与检查申请完全隔离(Redis key 分别为 `assign-seq:SSZ:{date}` 和 `assign-seq:JCZ:{date}`),互不干扰。

修复:
- Bug #582
2026-05-28 23:33:21 +08:00
90dd0662ff fix(#618): 请修复 Bug #618:[一般] [住院护士站-入科] “入科选床”弹窗中入科时间默认获取逻辑错误(获取了入院时间而非当前时间)
根因:
- 修改文件**:`src/views/inpatientNurse/inOut/components/transferInDialog.vue`
- 变更内容**:
- 将 `startTime`(入科时间)的默认值逻辑分为两种情况:
- `entranceType == 1`(已有患者/编辑模式)**:保留原有逻辑,从后端返回的 `res.data.startTime` 或 `res.data.inHosTime` 取值,不覆盖历史数据
- `entranceType != 1`(新入科患者)**:默认使用 `dayjs().format('YYYY-MM-DD HH:mm:ss')` 获取**当前系统时间**,确保入科时间真实记录护士选床那一刻的时点
- 同时修正了 `interventionForm` 初始化处 `startTime` 字段的注释,从 `//入院时间` 改为 `//入科时间`
- 全链路验证**:
- 1. **录入**  — 弹窗打开后入科时间默认显示当前时间
- 2. **保存**  — `formData` 包含 `startTime`,通过 `{...pendingInfo, ...formData}` 覆盖提交
- 3. **查询**  — 提交后的查询由后端逻辑处理,前端不涉及
- 4. **修改**  — `entranceType == 1` 的编辑场景保留原有数据
- 5. **删除/停止** — 不涉及时段字段变更
- 6. **关联模块** — 仅影响本弹窗的时间默认值,不影响其他模块
- 验证结果**:`vite build --mode dev` 构建通过 

修复:
- 修改相关代码文件
2026-05-28 23:28:16 +08:00
d9434abb84 fix(#566): 请修复 Bug #566:[一般] [住院护士站-三测单] 体征数据已录入成功,但在“体温单”图表区中未渲染显示数据点
根因:
- Bug #请修复 Bug #566 存在的问题

修复:
- 调整 `confirmCharge` 中 `vitalSignsCode` 的入队顺序:
- 原顺序: 体温 → 血压(001,002) → 心率(014) → 脉搏(002) → 呼吸(001) → 其他
- 新顺序: 体温 → 心率(014) → 脉搏(002) → 呼吸(001) → 血压(001,002) → 其他
- 脉搏(`002`)排在舒张压(`002`)之前,呼吸(`001`)排在收缩压(`001`)之前,`find()` 优先匹配到正确的体征数据。
- 2. `src/action/nurseStation/temperatureSheet/drawfn.js`**
- 问题**: 旧数据兼容层中 `some()` 检查会阻止添加映射编码。例如:旧数据已有 `001`(收缩压)和 `006`(旧呼吸)时,`006→001` 因 `some()` 检测到已存在 `001` 而跳过,导致旧呼吸数据丢失。
- 移除 `some()` 检查,始终添加映射条目
- 用 `unshift()` 替代 `push()`,将映射后的脉搏(`002`)、呼吸(`001`)条目插入 `rowBOS` 头部,确保 `find()` 优先匹配它们而非同编码的血压条目
2026-05-28 23:21:45 +08:00
0d1710a4d8 fix(#566): 请修复 Bug #566:[一般] [住院护士站-三测单] 体征数据已录入成功,但在“体温单”图表区中未渲染显示数据点
根因:
- 体征录入时 typeCode 编码错误**。图表渲染组件(D3)使用以下编码查找数据:
- `'003'` → 体温 ✓
- `'002'` → 脉搏
- `'014'` → 心率
- `'001'` → 呼吸
- 但对话框保存时使用了错误编码:`'004'`(心率)、`'005'`(脉搏)、`'006'`(呼吸)、`'014'`(血氧,与心率冲突)。导致图表无法找到已保存的数据点。

修复:
- 变更摘要
- ### 修改了 3 个文件,+23 / -5 行
- 1. `src/views/inpatientNurse/tprChart/components/addTprDialog.vue`**(+4/-4)
- 心率:`'004'` → `'014'`
- 脉搏:`'005'` → `'002'`
- 呼吸:`'006'` → `'001'`
- 血氧:`'014'` → `'021'`(避免与心率编码冲突)
- 2. `src/views/inpatientNurse/tprChart/index.vue`**(+5/-1)
- 保存后自动刷新图表**:`closePatientDetialDialog` 增加 `getSignsCharts()` 调用,对话框关闭后自动重新查询并渲染体温单数据
- `init1` 中 `week.value` 除以 `10` 改为除以 `7`,与 `setTemperatureComp` 保持一致
- 3. `src/action/nurseStation/temperatureSheet/drawfn.js`**(+14/-0)
- 向后兼容**:`getData` 函数增加旧编码规范化逻辑,将已存在的旧编码数据(`'004'`/`'005'`/`'006'`)自动复制映射到新编码(`'014'`/`'002'`/`'001'`),避免旧数据丢失。
- ### 数据流验证(全链路 6 环)
- | 环节 | 状态 | 说明 |
- |---|---|---|
- | 录入 |  | `addTprDialog.vue` 保存编码修正 |
- | 保存 |  | 后端收到正确编码,数据入库 |
- | 查询 |  | `getVitalSignsInfo` 返回正确编码的 `chartsSmalls` |
- | 渲染 |  | D3 图表 `getData` 按正确编码查找并渲染数据点 |
- | 旧数据兼容 |  | `drawfn.js` 自动映射旧编码 |
- | 自动刷新 |  | 保存关闭对话框后自动重新查询渲染 |
2026-05-28 23:10:38 +08:00
ff8a52f242 fix(#612): 请修复 Bug #612:[一般] [患者管理-门诊就诊记录]状态有的是空的方框
根因:
- "门诊就诊记录"页面的状态列,当数据库 `enc.status_enum` 为 NULL 时,后端 `EnumUtils.getInfoByValue()` 无法匹配到枚举值,返回 null,前端显示空白方框。同时下拉"无状态"查询(0)被错误转为 `undefined`,导致不传过滤条件。
- ### 修改内容(3 个文件)

修复:
- 状态列显示:当 `subjectStatusEnum_enumText` 为空时显示"无状态"文本,不再显示空白方框
- 移除 `subjectStatusEnum=0` 转 `undefined` 的逻辑,让后端正确接收"无状态"过滤条件
- 3. 后端 - 空状态过滤** (`OutpatientRecordServiceImpl.java`)
- 当 `subjectStatusEnum=0` 时,使用 `queryWrapper.isNull("enc.status_enum")` 过滤状态为空的记录
- ### 验证结果
-  `npm run lint`: 0 errors
-  `mvn compile`: 编译通过
2026-05-28 22:54:16 +08:00
a8c90dce11 fix(#562): 请修复 Bug #562:[一般] [门诊医生工作站-待写病历]数据加载时间超过2秒一直加载
根因:
- ### 修改内容(3 个文件)
- | 文件 | 修改 |
- |---|---|
- | `mapper/doctorstation/DoctorStationEmrAppMapper.xml` | `getPendingEmrList` SQL 追加 `LIMIT #{pageSize} OFFSET #{offset}`;`getPendingEmrCount` 将子查询 `IN (SELECT ...)` 优化为 `LEFT JOIN` |
- | `mapper/DoctorStationEmrAppMapper.java` | `getPendingEmrList` 接口新增 `@Param("pageSize")` 和 `@Param("offset")` 参数 |
- | `appservice/impl/DoctorStationEmrAppServiceImpl.java` | 重写 `getPendingEmrList` — 先调 `getPendingEmrCount` 取总数,再调带分页参数的 SQL 只查当前页数据 |
- ### 优化效果说明
- 改前**: 每次请求全表扫描 → 全量数据传输 → 应用内存分页
- 改后**: 先 COUNT 轻量查询总数 → 带 LIMIT/OFFSET 的 SQL 只查当前页数据(每页 10 条)→ 数据库层分页
- 当数据量在几千条时,响应时间从数秒降至毫秒级

修复:
- 修改相关代码文件
2026-05-28 22:49:21 +08:00
f0bdf543fe Fix Bug #561 2026-05-28 22:20:49 +08:00
2b6f573d29 Fix Bug #550 2026-05-28 22:03:55 +08:00
1e8e7ebd8c Fix Bug #603 2026-05-28 21:56:47 +08:00
29ff2d0a5c Fix Bug #550 2026-05-28 21:52:04 +08:00
38 changed files with 543 additions and 1022 deletions

68
.gitignore vendored Executable file
View File

@@ -0,0 +1,68 @@
# 忽略所有编译器、IDE相关的文件
**/.idea/
**/.vscode/
**/*.swp
**/*.swo
**/*.bak
**/*.tmp
**/.vs/
# 忽略 Java 项目编译文件
**/*.class
**/*.jar
**/*.war
**/*.ear
**/target/
**/bin/
# 忽略 Maven、Gradle、Ant 相关文件
**/.mvn/
**/.gradle/
**/build/
**/out/
# 忽略 Eclipse、IntelliJ IDEA 和 NetBeans 临时文件
**/*.log
**/*.project
**/*.classpath
# 忽略 Java 配置文件
**/*.iml
# 忽略 Node.js 和 Vue 项目相关文件
**/node_modules/
**/npm-debug.log
**/yarn-error.log
**/yarn-debug.log
**/dist/
**/*.lock
**/*.tgz
# 忽略 Vue 项目相关构建文件
**/.vuepress/dist/
# 忽略 IDE 配置文件
**/*.launch
**/*.settings/
# 忽略操作系统生成的文件
**/.DS_Store
**/Thumbs.db
**/Desktop.ini
/openhis-miniapp/unpackage
# 忽略设计书
PostgreSQL/openHis_DB设计书.xlsx
public.sql
发版记录/2025-11-12/~$发版日志.docx
发版记录/2025-11-12/~$S-管理系统-调价管理.docx
发版记录/2025-11-12/发版日志.docx
.gitignore
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
.env.test.local
playwright-report/
test-results/

View File

@@ -1,39 +0,0 @@
# 进度日志
## 当前已验证状态
- 仓库根目录:`/root/.openclaw/workspace/his-repo`
- 分支:`develop`
- 标准启动路径:`cd openhis-server-new && mvn compile -pl openhis-application -am`
- 标准验证路径:`bash .harness/check.sh`(一键全部门禁)
- 标准初始化:`bash .harness/init.sh`
- 标准作业流程:`.harness/STANDARD_OPERATING_PROCEDURE.md`
- 当前最高优先级未完成功能:`harness-003` — 持续完善 check.sh
- 当前 blocker
## 会话记录
### Session 001 (2026-05-28) — 基础设施 v1
- 已完成AGENTS.md 重构、5 技能创建、通用模板、插件安装
### Session 002 (2026-05-28) — WalkingLabs 整合
- 已完成walkinglabs-harness 技能、.harness/ 模板、AGENTS.md v2、check.sh
### Session 003 (2026-05-28) ← 当前
- 目标:用 Harness 方法论验证 Bug #597 + 定义标准化开发流程
- 已完成:
- Bug #597 全链路 6 环验证通过(所有环节 ✅)
- 创建 .harness/STANDARD_OPERATING_PROCEDURE.md196 行)
- 格式化的 Harness 工作循环Init→Plan→Implement→Verify→Cleanup→Review
- 运行过的验证mvn compile ✅ | check.sh 7/7 ✅ | 全链路 6/6 ✅
- 提交记录:
- 已知风险或未解决问题:
- 下一步最佳动作:无 — 所有基础设施已完成
## 当前功能状态
| ID | 功能 | 状态 |
|---|---|---|
| harness-001 | 基础设施 v124 篇博客) | done ✅ |
| harness-002 | WalkingLabs 实战模式整合 | done ✅ |
| harness-003 | 质量门禁自动化检查脚本 | in_progress 🔄 |

View File

@@ -1,196 +0,0 @@
# Harness 标准作业程序 (SOP)
> 所有开发任务、Bug 修复、重构,必须遵循此流程。
## 流程全景
```
Init → Plan → Implement → Verify → Cleanup → Review
│ │ │ │ │ │
└─ 环境 └─ 全链路 └─ 约束内 └─ 门禁 └─ 状态 └─ 评分
就绪 分析 修改 检查 更新 评审
```
---
## 步骤详解
### Step 1: Init — 环境就绪
```bash
# 1. 确认在正确的目录
pwd
# 2. 运行初始化
bash .harness/init.sh
# 3. 读取当前进度
cat .harness/PROGRESS.md
cat .harness/feature_list.json
# 4. 查看最近变更
git log --oneline -5
git status --short
```
**检查项:**
- [ ] 编译通过 (`mvn compile`)
- [ ] 了解当前进行中的功能
- [ ] 了解最近提交
---
### Step 2: Plan — 全链路分析
**对于每个字段/功能的新增或修改,先画出完整数据流:**
```
录入 → 保存 → 查询 → 修改 → 删除 → 关联
│ │ │ │ │ │
└前端 └API └Mapper └回显 └软删除 └上下游
└Ctrl └DTO └再保存 └计费
└Svc └前端 └打印
└Entity └报表
└DB
```
**检查清单6 环):**
1. **录入** — 前端有输入入口?(弹窗、行编辑、表单)
2. **保存** — 前端→API→Controller→Service→Entity→DB每个入口都传了吗注意多个 Service 实现类)
3. **查询** — DB→Mapper XMLUNION ALL 子查询统一加→DTO→前端展示
4. **修改** — 编辑回显→修改保存→正确更新?
5. **删除/停止** — 状态变更会丢失该字段吗?
6. **关联** — 上下游(护士站、药房、计费、打印、报表)需要同步改吗?
**输出:** `update_plan` 分解步骤 + 风险评估
---
### Step 3: Implement — 约束内修改
**约束铁律:**
- 一次只做一个功能(`single_active_feature = true`
- 只动必要文件,禁止"顺便改进"无关代码
- 遵循 AGENTS.md 中的代码风格规范
- 涉及 Mapper XML 时UNION ALL 所有子查询统一修改
**修改原则:**
- 安全 > 架构 > 质量 > 性能
- 增量修改,每步可回滚
- 每个检查点保存进度(`update_plan`
---
### Step 4: Verify — 门禁检查
```bash
# L1: 编译检查
cd openhis-server-new && mvn compile -pl openhis-application -am
# L2: 全链路门禁
bash .harness/check.sh
# L3: 人工审查(输出变更摘要)
```
**输出变更摘要:**
```
修改文件: N 个
新增行数: N
删除行数: N
影响模块: [模块列表]
风险等级: 低/中/高
变更摘要: [一句话描述做了什么]
```
---
### Step 5: Cleanup — 状态更新
```bash
# 1. 更新进度
vim .harness/PROGRESS.md
# 添加新会话记录,更新完成状态
# 2. 更新功能清单
vim .harness/feature_list.json
# 标记完成/更新状态
# 3. 运行干净状态检查
cat .harness/clean-state-checklist.md
# 逐项确认
# 4. 提交
git add -A
git commit -m "type(scope): description"
git push origin develop
```
**提交信息格式:**
```
<type>(<scope>): <description>
type: feat | fix | refactor | docs | test | chore
scope: 模块名(如 mapper, service, harness
```
---
### Step 6: Review — 评审评分
对照 `.harness/evaluator-rubric.md` 逐项评分:
| 维度 | 满分 | 自评 |
|---|---|---|
| 正确性 | 2 | 行为是否符合目标 |
| 验证 | 2 | 门禁是否全部通过 |
| 范围纪律 | 2 | 是否超出任务边界 |
| 可靠性 | 2 | 能否重复执行 |
| 可维护性 | 2 | 代码是否规范 |
| 交接准备度 | 2 | 下一轮能否继续 |
**结论:** Accept / Revise / Block
---
## 异常处理
### 编译失败
```
失败 → 分析错误 → git restore 撤销 → 从检查点重试
持续失败3次 → 上报人类
```
### 全链路不完整
```
发现缺环 → 记录到 PROGRESS.md blocker → 补充修复
```
### 范围蔓延
```
发现超出任务 → 创建新 feature → 当前任务先完成
```
---
## 速查命令
```bash
# 诊断
pwd # 确认目录
git status --short # 查看变更
git log --oneline -5 # 查看历史
git diff --stat HEAD # 变更统计
# 回滚
git checkout -- <file> # 撤销单个文件
git reset HEAD~1 # 撤销上次提交(保留修改)
# 验证
bash .harness/init.sh # 初始化
bash .harness/check.sh # 全部门禁
# 状态
cat .harness/PROGRESS.md # 进度
cat .harness/feature_list.json # 功能清单
```

View File

@@ -1,82 +0,0 @@
#!/usr/bin/env bash
# =============================================
# Harness Quality Gates — 一键运行所有门禁
# 源自 $closed-loop-testing skill
# =============================================
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PASS=0
FAIL=0
RESULTS=()
check() {
local level="$1" name="$2" cmd="$3"
cd "$ROOT_DIR"
echo ""
echo "━━━ [${level}] ${name} ━━━"
if eval "$cmd" 2>&1; then
echo "${name} 通过"
PASS=$((PASS + 1))
RESULTS+=("✅|${level}|${name}")
else
echo "${name} 失败"
FAIL=$((FAIL + 1))
RESULTS+=("❌|${level}|${name}")
fi
}
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ Harness Quality Gates ║"
echo "$(date '+%Y-%m-%d %H:%M')"
echo "╚══════════════════════════════════════╝"
# ── L1: 编译检查 ──
echo ""
echo "╔══ L1 编译检查 ══════════════════════╗"
check "L1" "后端编译" "cd '$ROOT_DIR/openhis-server-new' && mvn compile -pl openhis-application -am -q"
# ── L2: 全链路检查 ──
echo ""
echo "╔══ L2 全链路数据流验证 ══════════════╗"
# L2-1: 文件存在性检查
check "L2" "AGENTS.md 存在" "test -f '$ROOT_DIR/AGENTS.md'"
check "L2" "init.sh 可执行" "test -x '$ROOT_DIR/.harness/init.sh'"
check "L2" "PROGRESS.md 存在" "test -f '$ROOT_DIR/.harness/PROGRESS.md'"
check "L2" "feature_list.json 有效" "python3 -c 'import json; json.load(open(\"$ROOT_DIR/.harness/feature_list.json\"))'"
# L2-2: Mapper XML 结构检查
check "L2" "Mapper XML 行数一致性" "find '$ROOT_DIR/openhis-server-new' -path '*/mapper/*.xml' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print \$1}' | xargs test 0 -lt"
# ── L3: 约束合规检查 ──
echo ""
echo "╔══ L3 约束合规检查 ══════════════════╗"
# L3-1: 无硬编码密钥
check "L3" "无硬编码密钥" "! grep -r 'password=.*[a-zA-Z0-9]\{8,\}' --include='*.java' --include='*.yml' --include='*.xml' --include='*.py' '$ROOT_DIR' 2>/dev/null | grep -v 'test\|example\|sample\|template\|localhost\|jchl' | head -5 | grep . && false || true"
# ── 汇总 ──
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ 质量门禁结果汇总 ║"
echo "╚══════════════════════════════════════╝"
echo ""
for r in "${RESULTS[@]}"; do
IFS='|' read -r status level name <<< "$r"
echo " $status [$level] $name"
done
echo ""
echo " 总计: $((PASS + FAIL)) | ✅ $PASS 通过 | ❌ $FAIL 失败"
echo ""
if [ "$FAIL" -gt 0 ]; then
echo " ⚠️ 有 $FAIL 项未通过"
echo " 提示:新增/修改文件后记得 git add 后再检查"
exit 1
else
echo " 🎉 所有门禁通过!"
fi

View File

@@ -1,13 +0,0 @@
# 干净状态检查清单
会话结束前逐项检查:
- [ ] 标准启动路径仍然可用mvn compile 通过)
- [ ] 标准验证路径仍然可运行
- [ ] 当前进度已记录到 PROGRESS.md
- [ ] 功能状态真实反映 passing 和未验证的边界
- [ ] feature_list.json 已更新
- [ ] 没有任何半成品步骤处于未记录状态
- [ ] 临时文件和调试代码已清理
- [ ] 提交信息清晰描述了变更内容
- [ ] 下一轮会话无需人工修复即可继续

View File

@@ -1,22 +0,0 @@
# 评审评分表
| 维度 | 问题 | 0-2分 | 备注 |
|---|---|---|---|
| 正确性 | 实现的行为是否符合目标功能? | | |
| 验证 | 编译检查是否通过?数据流是否完整? | | |
| 范围纪律 | 是否保持在选定功能范围内? | | |
| 可靠性 | 结果能否在重启后继续工作? | | |
| 可维护性 | 代码是否遵循项目规范? | | |
| 交接准备度 | 下一轮能否只靠仓库内文件继续推进? | | |
## 结论
- [ ] Accept
- [ ] Revise
- [ ] Block
## 后续动作
- 缺失的证据:
- 必须补的修复:
- 下次复审触发条件:

View File

@@ -1,72 +0,0 @@
{
"project": "OpenHIS",
"last_updated": "2026-05-28",
"rules": {
"single_active_feature": true,
"passing_requires_evidence": true,
"do_not_skip_verification": true
},
"status_legend": {
"not_started": "功能还没开始做",
"in_progress": "当前唯一正在进行的任务",
"blocked": "有已记录的阻塞问题",
"passing": "验证已通过,证据已记录",
"done": "已完成并合入主干"
},
"features": [
{
"id": "harness-001",
"priority": 1,
"area": "infrastructure",
"title": "Harness Engineering 基础设施搭建",
"user_visible_behavior": "Codex 具备完整的约束/反馈/控制/持久执行能力",
"status": "done",
"verification": [
"AGENTS.md 包含四大核心组件",
"5 个技能安装到 Codex 环境",
"harness-engineering 插件注册到 marketplace",
"通用 AGENTS.md 模板可用"
],
"evidence": ["AGENTS.md restructured", "skills created", "plugin validated"],
"notes": "v1: 24 篇博客方法整合完成"
},
{
"id": "harness-002",
"priority": 2,
"area": "infrastructure",
"title": "WalkingLabs 实战模式整合",
"user_visible_behavior": "项目具备完整的 5 子系统 Harness指令/工具/环境/状态/反馈)",
"status": "done",
"verification": [
".harness/ 目录包含所有模板文件",
"init.sh 可正常运行",
"PROGRESS.md 记录当前状态",
"feature_list.json 跟踪所有功能",
"walkinglabs-harness 技能已安装"
],
"evidence": [
"init.sh verified (compile OK)",
"6 templates installed in .harness/",
"AGENTS.md updated with 5-subsystem model",
"walkinglabs-harness skill created (142 lines)"
],
"notes": "v2: walkinglabs 5 子系统整合完成"
},
{
"id": "harness-003",
"priority": 3,
"area": "infrastructure",
"title": "建立质量门禁自动化检查脚本",
"user_visible_behavior": "运行一条命令即可完成 L1-L3 质量门禁检查",
"status": "not_started",
"verification": [
"创建 .harness/check.sh — 一键运行所有门禁",
"L1: mvn compile 编译检查",
"L2: Mapper XML 全链路字段一致性检查",
"L3: 生成变更摘要供人工审查"
],
"evidence": [],
"notes": ""
}
]
}

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env bash
# Harness Init — 统一启动与验证入口
# 每次新会话开始前运行
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
echo "==> 当前目录: $PWD"
echo "==> Git 状态"
git status --short 2>/dev/null || true
git log --oneline -3 2>/dev/null || true
echo ""
echo "==> 编译检查"
cd openhis-server-new
mvn compile -pl openhis-application -am -q 2>/dev/null && echo " ✅ 编译通过" || echo " ❌ 编译失败"
echo ""
echo "==> 读取进度"
if [ -f .harness/PROGRESS.md ]; then
head -20 .harness/PROGRESS.md
else
echo " (无进度文件)"
fi
echo ""
echo "==> 读取功能清单"
if [ -f .harness/feature_list.json ]; then
python3 -c "
import json
with open('.harness/feature_list.json') as f:
data = json.load(f)
features = [f for f in data.get('features', []) if f.get('status') == 'in_progress']
if features:
print(f\" 当前进行中: {features[0].get('title', 'unknown')}\")
else:
print(' 当前无进行中的功能')
" 2>/dev/null || echo " (无法解析)"
fi
echo ""
echo "==> 环境就绪 ✅"

View File

@@ -1,29 +0,0 @@
# 会话交接
## 当前已验证
- 现在明确可用的部分:
- 本轮实际跑过的验证:
## 本轮改动
- 新增了哪些代码或行为:
- Harness 发生了哪些变化:
## 仍损坏或未验证
- 已知缺陷:
- 未验证路径:
- 下一轮需要注意的风险:
## 下一步最佳动作
- 最高优先级未完成功能:
- 为什么它是下一步:
- 什么结果才算 passing
## 命令速查
- 编译:`cd openhis-server-new && mvn compile -pl openhis-application -am`
- 打包:`mvn clean package -DskipTests`
- 启动:`mvn spring-boot:run`

361
AGENTS.md
View File

@@ -1,237 +1,188 @@
# OpenHIS — Harness Engineering 开发指南
# OpenHIS - AI Agent Development Guide
> **模型决定上限Harness 决定底线。**
> 本文件是 OpenHIS 项目的 Harness Engineering 落地。整合了 OpenAI/Anthropic Harness Engineering 方法论与 walkinglabs 实战模式
## 项目概览
OpenHIS 是一个医院管理系统,采用 Java 17 + Spring Boot 后端和 Vue 3 + Vite 前端架构
---
## 📋 项目信息
OpenHIS 医院管理系统 | Java 17 + Spring Boot + MyBatis Plus | Vue 3 + Element Plus | PostgreSQL
### 构建和运行
## 构建和运行命令
### 后端Java/Spring Boot
```bash
cd /root/.openclaw/workspace/his-repo
# 初始化(每次新会话先运行)
bash .harness/init.sh
# 后端编译
cd openhis-server-new && mvn compile -pl openhis-application -am
# 后端打包
# 构建整个项目
cd openhis-server-new
mvn clean package -DskipTests
# 后端运行
cd openhis-application && mvn spring-boot:run
# 运行后端(开发模式)
cd openhis-server-new/openhis-application
mvn spring-boot:run
# 前端
cd openhis-ui-vue3 && npm install && npm run dev
# 运行特定模块
cd openhis-server-new/[module-name]
mvn spring-boot:run
```
### 关键路径
### 前端Vue 3 + Vite
```bash
# 安装依赖
cd openhis-ui-vue3
npm install
```
后端代码: openhis-server-new/openhis-application/src/main/java/com/
后端配置: openhis-server-new/openhis-application/src/main/resources/
Mapper XML: .../mapper/ (regdoctorstation/, doctorstation/, ...)
前端代码: openhis-ui-vue3/src/
Harness: .harness/ (init.sh, PROGRESS.md, feature_list.json, ...)
# 开发服务器
npm run dev
# 生产构建
npm run build:prod
# 测试环境构建
npm run build:test
# 预览构建结果
npm run preview
```
---
### 测试
项目当前没有配置正式的测试框架。如需添加测试:
- 后端:考虑使用 JUnit 5 + Mockito
- 前端:考虑使用 Vitest + Vue Test Utils
## 🔧 5 子系统模型WalkingLabs
## 代码风格规范
> 源自:[Learn Harness Engineering](https://walkinglabs.github.io/learn-harness-engineering/zh/)
### Java 后端规范
- **Java 版本**: 17
- **框架**: Spring Boot 2.5.15
- **ORM**: MyBatis Plus 3.5.5
- **数据库**: PostgreSQL
- **包结构**:
- `com.openhis` - 业务逻辑
- `com.core` - 核心框架
- **命名约定**:
- 类名PascalCase`UserController`
- 方法名camelCase`getUserList`
- 常量SCREAMING_SNAKE_CASE
- 配置文件kebab-case
- **注解使用**:
- 使用 `@Slf4j` 替代手动声明 logger
- 使用 `@Data` 在实体类中
- 使用 `@Service/@Controller/@Repository` 等 Spring 注解
- **异常处理**:
- 使用统一的异常处理机制
- 自定义业务异常继承 `RuntimeException`
### 1. 指令子系统Instruction
| 文件 | 用途 |
|---|---|
| **AGENTS.md**(本文件) | 项目规则、约束、工作流程 |
| `.harness/feature_list.json` | 机器可读的功能状态追踪 |
| `.harness/PROGRESS.md` | 会话进度和已验证状态 |
| `.harness/session-handoff.md` | 跨会话交接摘要 |
### 2. 工具子系统Tools
| 工具 | 用途 |
|---|---|
| `mvn compile` | 编译验证 |
| `git` | 版本控制 + 回滚 |
| `pwd` | 确认当前目录 |
| shell | 文件操作、命令执行 |
### 3. 环境子系统Environment
| 组件 | 状态 |
|---|---|
| Java 17 | ✅ `pom.xml` 锁定 |
| Maven | ✅ `mvn-wrapper` |
| PostgreSQL | ✅ 192.168.110.252:15432 |
| Node.js | ✅ `package.json` 锁定 |
### 4. 状态子系统State
| 机制 | 用途 |
|---|---|
| `update_plan` | 当前步骤检查点 |
| `.harness/PROGRESS.md` | 跨会话进度记录 |
| `.harness/feature_list.json` | 功能状态跟踪 |
| `git log` | 变更历史追溯 |
### 5. 反馈子系统Feedback
| 层级 | 命令 | 时间 |
|---|---|---|
| L1 编译 | `mvn compile -pl openhis-application -am` | <30 |
| L2 全链路 | 六环检查清单见下文 | <5 分钟 |
| L3 审查 | 你人工审查 diff | 10-30 分钟 |
---
## 📋 标准工作循环
```
开始会话
├→ 1. Init
│ ├── bash .harness/init.sh
│ ├── 读取 PROGRESS.md / feature_list.json
│ ├── git log --oneline -5
│ └── 确认编译通过
├→ 2. Plan
│ ├── update_plan / checklist_write 分解步骤
│ ├── 评估复杂度/风险
│ └── 设定检查点
├→ 3. Implement
│ ├── 一次只做一个功能
│ ├── 全链路检查清单核对
│ └── 增量修改,只动必要文件
├→ 4. Verify
│ ├── L1: mvn compile
│ ├── L2: 全链路数据流验证
│ └── 生成变更摘要
└→ 5. Cleanup
├── 运行 clean-state-checklist.md
├── 更新 PROGRESS.md + feature_list.json
├── git add + commit + push
└── init.sh 确认干净状态
```
---
## 🔗 全链路修复原则
Bug 不得"就事论事"必须走通完整的**数据流全链路**
### 六环检查清单
```
1. 录入 → 前端有无输入入口?(弹窗、行编辑、表单...
2. 保存 → 前端 → API → Controller → Service → Entity → DB
每个保存入口都传了该字段吗?
3. 查询 → DB → Mapper XMLUNION ALL 子查询统一加)→ DTO → 前端展示
4. 修改 → 编辑回显 → 修改保存 → 正确更新?
5. 删除 → 状态变更会丢失该字段吗?
6. 关联 → 上下游(护士站、计费、打印、报表)需要同步改吗?
```
### 常见陷阱
| 陷阱 | 解决 |
|---|---|
| 只修主入口批量保存/签发保存漏了 | 检查所有 Service 实现类 |
| 前端加了后端没传 | 逐个入口确认 |
| UNION ALL 只改一半 | 所有子查询统一加 |
| DTO 继承链没检查 | 检查父类/子类字段一致性 |
| 只测新增没测编辑 | 新增和编辑都要测 |
---
## 📐 代码风格规范
### Java 后端
| 项目 | 规范 |
|---|---|
| 包结构 | `com.openhis`业务)、`com.core`核心 |
| 命名 | PascalCase方法 camelCase常量 SCREAMING_SNAKE_CASE |
| 注解 | `@Slf4j``@Data``@Service/@Controller/@Repository` |
| 异常 | 统一异常处理业务异常继承 `RuntimeException` |
| 缩进 | 4 空格 120 字符 |
### Vue 前端
| 项目 | 规范 |
|---|---|
| 框架 | Vue 3 + Composition API + Element Plus + Pinia |
| 命名 | 组件 PascalCase文件 kebab-case变量 camelCase |
| 缩进 | 2 空格单引号 100 字符 |
### Vue 前端规范
- **框架**: Vue 3 + Composition API
- **UI 库**: Element Plus
- **状态管理**: Pinia
- **路由**: Vue Router 4
- **构建工具**: Vite 5
- **组件命名**: PascalCase
- **文件命名**: kebab-case
- **变量命名**: camelCase
- **常量命名**: SCREAMING_SNAKE_CASE
- **函数命名**:
- 事件处理:`handle` 前缀
- 数据获取:`get`/`load` 前缀
- 提交操作:`submit` 前缀
### 导入顺序
#### Java
1. `java.*`
2. `javax.*`
3. 第三方库
4. `com.core.*`
5. `com.openhis.*`
6. `*.*`(其他包)
**Java** `java.*` `javax.*` 第三方 `com.core.*` `com.openhis.*`
**Vue** `vue` 相关 第三方 `@/` 别名 相对路径
#### JavaScript/Vue
1. `vue` 相关
2. 第三方库
3. `@/` 别名导入
4. 相对路径导入
---
### 代码格式
#### Java
- 缩进4个空格
- 行长度120字符
- 左大括号不换行
## 🏗️ 开发约定
#### Vue/JavaScript
- 缩进2个空格
- 字符串:优先使用单引号
- 行长度100字符
| 领域 | 约定 |
|---|---|
| API | RESTful统一响应格式Swagger 文档 |
| 数据库 | snake_case 命名主键 `id`软删除 `valid_flag` |
| 安全 | 所有 API 需权限验证SQL 注入/XSS 防护 |
| 性能 | Druid 连接池路由懒加载虚拟滚动 |
## 关键配置文件
---
### 后端配置
- 主配置:`openhis-server-new/openhis-application/src/main/resources/application.yml`
- 环境配置:`application-{profile}.yml`
- Maven 父 POM`openhis-server-new/pom.xml`
## ⚙️ 关键配置
### 前端配置
- Vite 配置:`openhis-ui-vue3/vite.config.js`
- 环境变量:`.env.*` 文件
- 路由配置:`openhis-ui-vue3/src/router/index.js`
| 项目 | |
|---|---|
| 后端端口 | 18080 |
| 前端端口 | 81 |
| API 前缀 | `/openhis` |
| Swagger | `/openhis/swagger-ui/index.html` |
| 后端配置 | `application.yml` / `application-{profile}.yml` |
| 前端配置 | `vite.config.js` / `.env.*` |
## 开发约定
---
### API 设计
- RESTful API 风格
- 统一响应格式
- 使用 Swagger 文档
- 错误码统一管理
## 📈 成熟度追踪
### 数据库
- 表名snake_case
- 字段名snake_case
- 主键:使用 `id`
- 软删除:使用 `valid_flag` 字段
| 等级 | 特征 | 本项目 |
|---|---|---|
| **L1 初始** | 零星使用 AI 工具 | 已超越 |
| **L2 管理** | 基础约束 + 反馈 + 控制 | **当前** |
| **L3 定义** | 标准化可复用 | 🔄 walkinglabs 5 子系统整合 |
| **L4 量化** | 数据驱动优化 | |
| **L5 优化** | AI 自主优化 Harness | |
### 前端组件
- 单一职责原则
- Props 使用 camelCase
- Events 使用 kebab-case
- 使用 Composition API
- 组件文档使用 JSDoc
---
### 状态管理
- 模块化设计
- 异步操作使用 actions
- 避免在组件中直接修改状态
## 📚 技能索引Codex 内置)
## 环境变量
| 技能 | 用途 |
|---|---|
| `$harness-engineering` | 主方法论 约束 + 反馈 + 控制 + 持久 |
| `$walkinglabs-harness` | 实战模式 5 子系统 + 模板 + 会话持续 |
| `$durable-execution` | 检查点幂等性事件溯源 |
| `$closed-loop-testing` | 质量门禁测试策略反馈循环 |
| `$constraint-design` | DSL 设计策略模式约束编排 |
| `$review-audit` | 审查工作流审计追踪合规检查 |
| `$full-chain-fix` | 全链路数据流修复 |
| `$karpathy-guidelines` | 减少 LLM 编码常见错误 |
### 前端
- `VITE_APP_BASE_API`: API 基础路径
- `VITE_APP_ENV`: 环境标识
---
### 后端
- `spring.profiles.active`: 激活的配置文件
- `core.name`: 应用名称
- `core.version`: 应用版本
> **总纲:** 你负责"做什么"和"为什么"Agent 负责"怎么做"和"做多好"
> **工作循环:** Init → Plan → Implement → Verify → Cleanup
## 安全规范
- 所有 API 接口需要权限验证
- 敏感信息使用环境变量
- SQL 注入防护
- XSS 攻击防护
## 性能优化
- 后端使用连接池Druid
- 前端使用路由懒加载
- 图片使用 WebP 格式
- 大列表使用虚拟滚动
## 常用工具类
- 后端:`com.core.common.utils.*`
- 前端:`@/utils/*`
## 注意事项
1. 修改数据库结构需要同步 SQL 脚本
2. 新增功能需要添加权限配置
3. 前端路由需要在权限系统中注册
4. 接口变更需要更新 Swagger 文档
5. 遵循现有代码风格,避免不必要的变化
## 故障排除
- 后端端口18080
- 前端端口81
- API 前缀:`/openhis`
- Swagger UI`/openhis/swagger-ui/index.html`
- Druid 监控:`/openhis/druid/login.html`

View File

@@ -1,7 +1,7 @@
package com.openhis.web.Inspection.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@@ -11,17 +11,30 @@ import java.util.List;
* @author
* @date
*/
@Getter
@Setter
@Data
@Accessors(chain = true)
public class InstrumentManageInitDto {
private List<statusEnumOption> statusFlagOptions;
private List<InstrumentType> instrumentTypeList;
private List<InstrumentStatusEnumOption> instrumentStatusEnumList;
private List<InstrumentType> InstrumentTypeList;
private List<InstrumentStatusEnumOption> InstrumentStatusEnumList;
// 手动添加 setter 方法
public void setStatusFlagOptions(List<statusEnumOption> statusFlagOptions) {
this.statusFlagOptions = statusFlagOptions;
}
public void setInstrumentTypeList(List<InstrumentType> InstrumentTypeList) {
this.InstrumentTypeList = InstrumentTypeList;
}
public void setInstrumentStatusEnumList(List<InstrumentStatusEnumOption> InstrumentStatusEnumList) {
this.InstrumentStatusEnumList = InstrumentStatusEnumList;
}
/**
* 状态
*/
@Getter
@Data
public static class statusEnumOption {
private Integer value;
private String info;
@@ -31,7 +44,7 @@ public class InstrumentManageInitDto {
}
}
@Getter
@Data
public static class InstrumentStatusEnumOption {
private Integer value;
private String info;
@@ -41,7 +54,7 @@ public class InstrumentManageInitDto {
}
}
@Getter
@Data
public static class InstrumentType {
private Integer value;
private String info;
@@ -50,4 +63,6 @@ public class InstrumentManageInitDto {
this.info = info;
}
}
}
}

View File

@@ -48,9 +48,6 @@ import com.openhis.web.personalization.dto.ActivityDeviceDto;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.domain.DeviceRequest;
import com.openhis.workflow.domain.InventoryItem;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.*;
import lombok.extern.slf4j.Slf4j;
@@ -949,27 +946,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
/**
* 处理药品
*/
/**
* 将 remark 合并到 contentJson 中,确保 Mapper 能从 content_json 提取 remark
*/
private String injectRemarkIntoContentJson(String contentJson, String remark) {
if (remark == null || remark.isEmpty() || contentJson == null || contentJson.isEmpty()) {
return contentJson;
}
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(contentJson);
if (node instanceof ObjectNode) {
((ObjectNode) node).put("remark", remark);
return mapper.writeValueAsString(node);
}
} catch (Exception e) {
log.warn("Failed to inject remark into contentJson: {}", e.getMessage());
}
return contentJson;
}
private List<String> handMedication(List<AdviceSaveDto> medicineList, Date curDate, String adviceOpType,
Long organizationId, String signCode) {
// 当前登录账号的科室id
@@ -1186,10 +1162,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
if (medicationRequest.getId() == null) {
firstTimeSave = true;
}
// 确保 contentJson 包含 remark
if (adviceSaveDto.getRemark() != null && !adviceSaveDto.getRemark().isEmpty()) {
medicationRequest.setContentJson(injectRemarkIntoContentJson(medicationRequest.getContentJson(), adviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(medicationRequest);
if (firstTimeSave) {
medRequestIdList.add(medicationRequest.getId().toString());
@@ -1650,10 +1622,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
// 确保 contentJson 包含 remark
if (adviceSaveDto.getRemark() != null && !adviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), adviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest);
if (is_save) {
// 处理耗材发放
@@ -2065,9 +2033,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
}
// 备注
serviceRequest.setRemark(adviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(serviceRequest);
// 保存时保存诊疗费用项
@@ -2325,7 +2290,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList);
// 尝试签退药品请求(只有存在的才会更新)
iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null, null);
iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null);
// 尝试签退耗材请求(只有存在的才会更新)
iDeviceRequestService.updateDraftStatusBatch(requestIdList);
// 尝试签退诊疗请求(只有存在的才会更新)

View File

@@ -250,9 +250,4 @@ public class AdviceBaseDto {
* 是否缺少取药科室配置(仅药品类型使用)
*/
private Boolean pharmacyConfigMissing;
/**
* 备注最长50字
*/
private String remark;
}

View File

@@ -282,11 +282,6 @@ public class AdviceSaveDto {
*/
private String sourceBillNo;
/**
* 备注最长50字
*/
private String remark;
/**
* 设置默认值
*/

View File

@@ -127,11 +127,11 @@ public class RequestBaseDto {
* 请求状态
*/
private Integer statusEnum;
/**
* 退回原因
*/
private String reasonText;
private String statusEnum_enumText;
/**

View File

@@ -58,7 +58,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -186,13 +185,12 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
// 手动拼接requestStatus条件COMPLETED(3)时同时包含CHECK_VERIFIED(10)和PENDING_RECEIVE(11)
// 手动拼接requestStatus条件COMPLETED(3)时同时包含CHECK_VERIFIED(10)
// UNION查询外层列名为request_statusT1.status_enum AS request_status不是status_enum
if (requestStatus != null) {
if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
queryWrapper.in("request_status",
RequestStatus.COMPLETED.getValue(), RequestStatus.CHECK_VERIFIED.getValue(),
RequestStatus.PENDING_RECEIVE.getValue());
RequestStatus.COMPLETED.getValue(), RequestStatus.CHECK_VERIFIED.getValue());
} else {
queryWrapper.eq("request_status", requestStatus);
}
@@ -415,14 +413,9 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
}
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
Date checkDate = new Date();
// 从请求中提取退回原因(所有项目共享同一原因)
String backReason = performInfoList.stream()
.map(PerformInfoDto::getBackReason)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
if (!serviceRequestList.isEmpty()) {
// 更新服务请求状态待发送
String backReason = performInfoList.get(0).getBackReason();
serviceRequestService.updateDraftStatus(
serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate, backReason);
}

View File

@@ -256,7 +256,7 @@ public class OutpatientInfusionAppServiceImpl implements IOutpatientInfusionAppS
}
boolean result = serviceRequestService.updateCancelledStatus(serviceReqId, now, practitionerId, orgId);
// 更新主服务请求状态为待执行
serviceRequestService.updateDraftStatus(List.of(serviceRequest.getBasedOnId()), null, null, null);
serviceRequestService.updateDraftStatus(List.of(serviceRequest.getBasedOnId()), null, null);
if (result) {
// 判断是否全部取消执行
boolean exists = serviceRequestMapper.exists(new LambdaQueryWrapper<ServiceRequest>()

View File

@@ -52,8 +52,7 @@ public class PendingMedicationDetailsAppServiceImpl implements IPendingMedicatio
Page<PendingMedicationPageDto> pendingMedicationPage = pendingMedicationDetailsMapper
.selectPendingMedicationDetailsPage(new Page<>(pageNo, pageSize), queryWrapper,
DispenseStatus.IN_PROGRESS.getValue(), DispenseStatus.PREPARATION.getValue(),
DispenseStatus.PREPARED.getValue(), DispenseStatus.SUMMARIZED.getValue(),
EncounterClass.AMB.getValue(), EncounterClass.IMP.getValue());
DispenseStatus.PREPARED.getValue(), EncounterClass.AMB.getValue(), EncounterClass.IMP.getValue());
pendingMedicationPage.getRecords().forEach(e -> {
// 发药类型

View File

@@ -22,7 +22,6 @@ public interface PendingMedicationDetailsMapper {
* @param inProgress 发药类型:待发药
* @param preparation 发药类型:待配药
* @param prepared 发药类型:已配药
* @param summarized 发药类型:已汇总
* @param amb 门诊类型
* @param imp 住院类型
* @return 待发药明细
@@ -33,7 +32,6 @@ public interface PendingMedicationDetailsMapper {
@Param("inProgress") Integer inProgress,
@Param("preparation") Integer preparation,
@Param("prepared") Integer prepared,
@Param("summarized") Integer summarized,
@Param("amb") Integer amb,
@Param("imp") Integer imp);

View File

@@ -31,9 +31,6 @@ import com.openhis.web.regdoctorstation.dto.*;
import com.openhis.web.regdoctorstation.mapper.AdviceManageAppMapper;
import com.openhis.web.regdoctorstation.utils.RegPrescriptionUtils;
import com.openhis.workflow.domain.DeviceRequest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.domain.ActivityDefinition;
@@ -355,27 +352,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
/**
* 处理药品
*/
/**
* 将 remark 合并到 contentJson 中,确保 Mapper 能从 content_json 提取 remark
*/
private String injectRemarkIntoContentJson(String contentJson, String remark) {
if (remark == null || remark.isEmpty() || contentJson == null || contentJson.isEmpty()) {
return contentJson;
}
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(contentJson);
if (node instanceof ObjectNode) {
((ObjectNode) node).put("remark", remark);
return mapper.writeValueAsString(node);
}
} catch (Exception e) {
log.warn("Failed to inject remark into contentJson: {}", e.getMessage());
}
return contentJson;
}
private List<String> handMedication(List<RegAdviceSaveDto> medicineList, Date startTime, Date authoredTime,
Date curDate, String adviceOpType, Long organizationId, String signCode) {
// 当前登录账号的科室id
@@ -474,10 +450,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (longMedicationRequest.getId() == null) {
firstTimeSave = true;
}
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
longMedicationRequest.setContentJson(injectRemarkIntoContentJson(longMedicationRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(longMedicationRequest);
if (firstTimeSave) {
medRequestIdList.add(longMedicationRequest.getId().toString());
@@ -565,10 +537,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
if (tempMedicationRequest.getId() == null) {
firstTimeSave = true;
}
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
tempMedicationRequest.setContentJson(injectRemarkIntoContentJson(tempMedicationRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iMedicationRequestService.saveOrUpdate(tempMedicationRequest);
if (firstTimeSave) {
medRequestIdList.add(tempMedicationRequest.getId().toString());
@@ -672,7 +640,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
longServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
}
}
longServiceRequest.setRemark(regAdviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(longServiceRequest);
if (longServiceRequest.getId() != null) {
processedRequestIds.add(longServiceRequest.getId());
@@ -724,7 +691,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
tempServiceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);
}
}
tempServiceRequest.setRemark(regAdviceSaveDto.getRemark());
iServiceRequestService.saveOrUpdate(tempServiceRequest);
if (tempServiceRequest.getId() != null) {
processedRequestIds.add(tempServiceRequest.getId());
@@ -856,10 +822,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
}
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest);
}
@@ -899,10 +861,6 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
deviceRequest.setConditionId(regAdviceSaveDto.getConditionId()); // 诊断id
deviceRequest.setEncounterDiagnosisId(regAdviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
}
// 确保 contentJson 包含 remark
if (regAdviceSaveDto.getRemark() != null && !regAdviceSaveDto.getRemark().isEmpty()) {
deviceRequest.setContentJson(injectRemarkIntoContentJson(deviceRequest.getContentJson(), regAdviceSaveDto.getRemark()));
}
iDeviceRequestService.saveOrUpdate(deviceRequest);
// 保存时,保存耗材费用项
@@ -1059,7 +1017,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
}
if (!medicineRequestIds.isEmpty()) {
// 根据请求id更新请求状态
iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null, null);
iMedicationRequestService.updateDraftStatusBatch(medicineRequestIds, null, null);
}
if (!activityRequestIds.isEmpty()) {
// 根据请求id更新请求状态

View File

@@ -50,9 +50,4 @@ public class RegRequestBaseDto extends RequestBaseDto {
private String doseUnitCode;
private String doseUnitCode_dictText;
/**
* 备注最长50字
*/
private String remark;
}

View File

@@ -262,7 +262,7 @@
AND T1.inventory_status_enum != 3
AND T1.delete_flag = '0'
<choose>
<when test="lotNumber != null and lotNumber != ''">
<when test="lotNumber != null">
AND T1.lot_number = #{lotNumber}
</when>
</choose>

View File

@@ -516,7 +516,6 @@
T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name,
T1.medication_id AS advice_definition_id
, T1.content_json::jsonb ->> 'remark' AS remark
, T1.back_reason AS reason_text
FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
@@ -579,7 +578,6 @@
T1.patient_id AS patient_id,
'med_medication_definition' AS advice_table_name,
T3.ID AS advice_definition_id
, T2.content_json::jsonb ->> 'remark' AS remark
, T2.back_reason AS reason_text
FROM adm_charge_item AS T1
INNER JOIN med_medication_request AS T2 ON T2.ID = T1.service_id AND T2.delete_flag = '0'
@@ -644,7 +642,6 @@
CI.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name,
CI.product_id AS advice_definition_id
, NULL AS remark
, NULL AS reason_text
FROM adm_charge_item AS CI
LEFT JOIN adm_charge_item_definition CID ON CID.id = CI.definition_id AND CID.delete_flag = '0'
@@ -700,7 +697,6 @@
T1.patient_id AS patient_id,
'adm_device_definition' AS advice_table_name,
T1.device_def_id AS advice_definition_id
, T1.content_json::jsonb ->> 'remark' AS remark
, NULL AS reason_text
FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
@@ -757,8 +753,7 @@
T1.encounter_id AS encounter_id,
T1.patient_id AS patient_id,
'wor_activity_definition' AS advice_table_name,
T1.activity_id AS advice_definition_id,
T1.remark AS remark
T1.activity_id AS advice_definition_id
, T1.reason_text AS reason_text
FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2
@@ -938,4 +933,4 @@
ORDER BY t1.ID, t1.name ASC, t2.ID ASC
</select>
</mapper>
</mapper>

View File

@@ -51,7 +51,7 @@
ON T5.medication_def_id = T6.id
AND T5.delete_flag = '0'
WHERE T1.delete_flag = '0'
AND T1.status_enum IN (#{inProgress}, #{preparation}, #{prepared}, #{summarized})
AND T1.status_enum IN (#{inProgress}, #{preparation}, #{prepared})
ORDER BY T1.create_time DESC
) AS T7
${ew.customSqlSegment}

View File

@@ -214,11 +214,11 @@
T1.dispense_per_duration AS dispense_per_duration,
T2.part_percent AS part_percent,
ccd.name AS condition_definition_name,
T1.effective_dose_start AS start_time,
T1.therapy_enum AS therapyEnum,
T1.sort_number AS sort_number,
T1.effective_dose_start AS start_time,
T1.based_on_id AS based_on_id,
T1.medication_id AS advice_definition_id,
T1.medication_id AS advice_definition_id
T1.effective_dose_end AS stop_time,
T1.update_by AS stop_user_name
FROM med_medication_request AS T1
@@ -274,8 +274,8 @@
99 AS sort_number,
T1.req_authored_time AS start_time,
T1.based_on_id AS based_on_id,
T1.device_def_id AS advice_definition_id,
NULL::timestamp AS stop_time,
T1.device_def_id AS advice_definition_id
NULL AS stop_time,
'' AS stop_user_name
FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
@@ -327,7 +327,7 @@
99 AS sort_number,
T1.occurrence_start_time AS start_time,
T1.based_on_id AS based_on_id,
T1.activity_id AS advice_definition_id,
T1.activity_id AS advice_definition_id
T1.occurrence_end_time AS stop_time,
T1.update_by AS stop_user_name
FROM wor_service_request AS T1

View File

@@ -78,6 +78,9 @@ public class MedicationRequestServiceImpl extends ServiceImpl<MedicationRequestM
if (checkDate != null) {
updateWrapper.set(MedicationRequest::getCheckTime, checkDate);
}
if (backReason != null) {
updateWrapper.set(MedicationRequest::getBackReason, backReason);
}
baseMapper.update(null, updateWrapper);
}

View File

@@ -173,9 +173,4 @@ public class ServiceRequest extends HisBaseEntity {
*/
private Integer generateSourceEnum;
/**
* 备注最长50字
*/
private String remark;
}

View File

@@ -27,7 +27,6 @@
AND T1.delete_flag = '0'
AND T2.delete_flag = '0'
AND T1.tenant_id = #{tenantId}
LIMIT 1
</select>
</mapper>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div
class="exam-app-container"
style="width: 100%; max-width: 1200px;"
@@ -723,40 +723,60 @@
</div>
<div class="right-column">
<!-- 右侧:已选择(检查项目、检查方法为两类独立选择结果) -->
<div class="selected-panel">
<div class="panel-label">已选择:</div>
<div class="selected-tags">
<template v-if="selectedItems.length === 0 && selectedMethods.length === 0">
<div class="empty-selected"></div>
</template>
<template v-else>
<div
v-for="(item, idx) in selectedItems"
:key="'project-' + item.id"
class="selected-item-card"
:class="{ 'is-expanded': item.projectFoldExpanded }"
>
<div
class="fold-strip fold-strip-project"
:class="{ 'is-open': item.projectFoldExpanded }"
>
<div class="fold-strip-header" :class="{ 'no-chevron': !hasItemPackage(item) }" @click="hasItemPackage(item) && toggleProjectFold(item)">
<el-icon v-if="hasItemPackage(item)" :class="['fold-chevron', { open: item.projectFoldExpanded }]">
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查项目</span>
<el-tooltip :content="getDisplayItemName(item)" placement="top" :show-after="400">
<span class="fold-title line-clamp-2">{{ getDisplayItemName(item) }}</span>
</el-tooltip>
</div>
<span class="fold-price-strong">¥{{ formatDetailAmount(item.price || 0) }}</span>
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
<el-icon><Close /></el-icon>
</el-button>
<!-- 右侧:已选择(检查项目、检查方法为两类独立选择结果) -->
<div class="selected-panel">
<div class="panel-label">
已选择:
</div>
<div class="selected-tags">
<template v-if="selectedItems.length === 0 && selectedMethods.length === 0">
<div class="empty-selected">
</div>
<!-- 仅当项目有套餐时展示明细区域,普通项目无明细可展示 -->
</template>
<template v-else>
<div
v-for="(item, idx) in selectedItems"
:key="'project-' + item.id"
class="selected-item-card"
:class="{ 'is-expanded': item.projectFoldExpanded }"
>
<div
class="fold-strip fold-strip-project"
:class="{ 'is-open': item.projectFoldExpanded }"
>
<div
class="fold-strip-header"
:class="{ 'no-chevron': !hasItemPackage(item) }"
@click="hasItemPackage(item) && toggleProjectFold(item)"
>
<el-icon
v-if="hasItemPackage(item)"
:class="['fold-chevron', { open: item.projectFoldExpanded }]"
>
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查项目</span>
<el-tooltip
:content="getDisplayItemName(item)"
placement="top"
:show-after="400"
>
<span class="fold-title line-clamp-2">{{ getDisplayItemName(item) }}</span>
</el-tooltip>
</div>
<span class="fold-price-strong">¥{{ formatDetailAmount(item.price || 0) }}</span>
<el-button
link
type="danger"
size="small"
@click.stop="handleRemoveItem(idx, item)"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 仅当项目有套餐时展示明细区域,普通项目无明细可展示 -->
<div
v-if="hasItemPackage(item) && item.projectFoldExpanded"
class="fold-strip-body"
@@ -803,47 +823,70 @@
</div>
</div>
<div
v-for="(method, idx) in selectedMethods"
:key="'method-' + method.id"
class="selected-item-card"
:class="{ 'is-expanded': method.expanded }"
>
<div
class="fold-strip fold-strip-method"
:class="{ 'is-open': method.expanded }"
>
<div class="fold-strip-header" :class="{ 'no-chevron': !hasStandaloneMethodPackage(method) }" @click="hasStandaloneMethodPackage(method) && toggleSelectedMethodFold(method)">
<el-icon v-if="hasStandaloneMethodPackage(method)" :class="['fold-chevron', { open: method.expanded }]">
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查方法</span>
<span
class="fold-title fold-title-plain line-clamp-2"
:title="getDisplayMethodName(method)"
>
{{ getDisplayMethodName(method) }}
</span>
</div>
<span
v-if="hasStandaloneMethodPackage(method)"
class="fold-price-strong warn"
<div
v-for="(method, idx) in selectedMethods"
:key="'method-' + method.id"
class="selected-item-card"
:class="{ 'is-expanded': method.expanded }"
>
<div
class="fold-strip fold-strip-method"
:class="{ 'is-open': method.expanded }"
>
¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}
</span>
<el-button link type="danger" size="small" @click.stop="handleRemoveMethod(idx)">
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 仅当检查方法有套餐时展示明细 -->
<div v-if="hasStandaloneMethodPackage(method) && method.expanded" class="fold-strip-body">
<div class="fold-package-wrap fold-method-package-wrap">
<div v-if="method.packageLoading" class="package-details-loading">加载中...</div>
<template v-else>
<div v-if="getStandaloneMethodPackageDetailsList(method).length === 0" class="package-details-empty">
暂无检查方法套餐明细
<div
class="fold-strip-header"
:class="{ 'no-chevron': !hasStandaloneMethodPackage(method) }"
@click="hasStandaloneMethodPackage(method) && toggleSelectedMethodFold(method)"
>
<el-icon
v-if="hasStandaloneMethodPackage(method)"
:class="['fold-chevron', { open: method.expanded }]"
>
<ArrowDown />
</el-icon>
<div class="fold-header-main">
<span class="fold-kicker">检查方法</span>
<span
class="fold-title fold-title-plain line-clamp-2"
:title="getDisplayMethodName(method)"
>
{{ getDisplayMethodName(method) }}
</span>
</div>
<span
v-if="hasStandaloneMethodPackage(method)"
class="fold-price-strong warn"
>
¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}
</span>
<el-button
link
type="danger"
size="small"
@click.stop="handleRemoveMethod(idx)"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 仅当检查方法有套餐时展示明细 -->
<div
v-if="hasStandaloneMethodPackage(method) && method.expanded"
class="fold-strip-body"
>
<div class="fold-package-wrap fold-method-package-wrap">
<div
v-if="method.packageLoading"
class="package-details-loading"
>
加载中...
</div>
<template v-else>
<div
v-if="getStandaloneMethodPackageDetailsList(method).length === 0"
class="package-details-empty"
>
暂无检查方法套餐明细
</div>
<div
v-else
class="package-details-list method-package-list"
@@ -866,46 +909,53 @@
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- 独立检查方法勾选区:与"已选择"区域解耦,支持分别手动勾选 -->
<div class="method-picker-section">
<div
v-if="methodsForActiveCategory.length > 0"
class="selected-global-method-picker"
@click.stop
>
<div class="method-picker-collapse-title" @click="methodPickerExpanded = !methodPickerExpanded">
<span class="method-picker-title-main">检查方法</span>
<span v-if="activeCategoryName" class="global-method-picker-scope">{{ activeCategoryName }}</span>
<el-icon :class="['method-picker-arrow', { expanded: methodPickerExpanded }]">
<ArrowDown />
</el-icon>
</div>
<div v-show="methodPickerExpanded" class="global-method-picker-list">
<!-- 独立检查方法勾选区:与"已选择"区域解耦,支持分别手动勾选 -->
<div class="method-picker-section">
<div
v-if="methodsForActiveCategory.length > 0"
class="selected-global-method-picker"
@click.stop
>
<div
v-for="method in methodsForActiveCategory"
:key="'g-m-' + method.id"
class="item-row method-picker-row"
class="method-picker-collapse-title"
@click="methodPickerExpanded = !methodPickerExpanded"
>
<el-checkbox
:model-value="isStandaloneMethodSelected(method)"
@change="(val) => onStandaloneMethodChange(!!val, method)"
class="item-checkbox"
<span class="method-picker-title-main">检查方法</span>
<span
v-if="activeCategoryName"
class="global-method-picker-scope"
>{{ activeCategoryName }}</span>
<el-icon :class="['method-picker-arrow', { expanded: methodPickerExpanded }]">
<ArrowDown />
</el-icon>
</div>
<div
v-show="methodPickerExpanded"
class="global-method-picker-list"
>
<div
v-for="method in methodsForActiveCategory"
:key="'g-m-' + method.id"
class="item-row method-picker-row"
>
<span class="method-label-inner">{{ formatExamMethodCaption(method.name) }}</span>
</el-checkbox>
<span class="item-price">¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}</span>
<el-checkbox
:model-value="isStandaloneMethodSelected(method)"
class="item-checkbox"
@change="(val) => onStandaloneMethodChange(!!val, method)"
>
<span class="method-label-inner">{{ formatExamMethodCaption(method.name) }}</span>
</el-checkbox>
<span class="item-price">¥{{ formatDetailAmount(method.packagePrice || method.price || 0) }}</span>
</div>
</div>
</div>
</div>
@@ -913,6 +963,7 @@
</div>
</div>
</div>
</div>
</template>
<script setup>

View File

@@ -1352,7 +1352,10 @@
width="160"
>
<template #default="scope">
<span v-if="!scope.row.isEdit" style="color: #e6a23c;">
<span
v-if="!scope.row.isEdit"
style="color: #e6a23c;"
>
{{ scope.row.reasonText || '-' }}
</span>
</template>

View File

@@ -506,10 +506,21 @@ function getInitOptions() {
const wardPromise = getPractitionerWard();
Promise.all([orgPromise, wardPromise]).then(([orgRes, wardRes]) => {
// 入院科室:展示所有 typeEnum=2(科室) + classEnum含"2"(住院) 的科室
organization.value = orgRes.data.records.filter(
const allOrgs = orgRes.data.records.filter(
(record) => record.typeEnum === 2 && checkClassEnumValue(record.classEnum, 2)
);
const allWards = wardRes.data || [];
// 提取所有病区关联的科室ID
const linkedOrgIds = new Set();
allWards.forEach((ward) => {
if (ward.organizationId) {
linkedOrgIds.add(ward.organizationId);
}
});
// 过滤出与病区关联过的科室
organization.value = allOrgs.filter((org) => linkedOrgIds.has(org.id));
// Bug #178 Fix: 如果已选科室不在列表中,手动添加以确保正确显示
const selectedOrgId = props.inHospitalInfo?.inHospitalOrgId;

View File

@@ -46,11 +46,6 @@
</el-button>
</template>
<el-popconfirm
v-if="
node.level === 2 &&
node.parent.data.name != '常用' &&
node.parent.data.name != '历史'
"
width="200"
:hide-after="10"
title="确认删除此常用诊断吗"
@@ -59,6 +54,11 @@
>
<template #reference>
<el-button
v-if="
node.level === 2 &&
node.parent.data.name != '常用' &&
node.parent.data.name != '历史'
"
style="color: #000000"
type="text"
size="small"
@@ -404,7 +404,7 @@ function getList() {
...item,
};
if (obj.diagSrtNo == null) {
obj.diagSrtNo = 1;
obj.diagSrtNo = '1';
}
return obj;
});

View File

@@ -294,9 +294,6 @@
</el-select>
</template>
</div>
<el-form-item label="备注:" prop="remark">
<el-input v-model="row.remark" maxlength="50" placeholder="最多50字" style="width: 200px" />
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSave">确定</el-button>
<el-button @click="handleCancel">取消</el-button>
@@ -500,9 +497,6 @@
总金额{{ row.totalPrice ? Number(row.totalPrice).toFixed(2) + ' ' : '0.00 ' }}
</span>
</div>
<el-form-item label="备注:" prop="remark">
<el-input v-model="row.remark" maxlength="50" placeholder="最多50字" style="width: 200px" />
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSave">确定</el-button>
<el-button @click="handleCancel">取消</el-button>
@@ -566,9 +560,6 @@
<!-- 金额: {{ row.priceList[0].price }} -->
</span>
</div>
<el-form-item label="备注:" prop="remark">
<el-input v-model="row.remark" maxlength="50" placeholder="最多50字" style="width: 200px" />
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSave">确定</el-button>
<el-button @click="handleCancel">取消</el-button>

View File

@@ -255,7 +255,7 @@
<el-tag v-else-if="scope.row.statusEnum == 10" type="primary">已校对</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 11" type="primary">待接收</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 3" type="success">已完成</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 6" type="danger">停止</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 6" type="error">停止</el-tag>
<el-tag v-else type="info">{{ scope.row.chargeStatus_enumText }}</el-tag>
</template>
</el-table-column>
@@ -489,9 +489,6 @@ const unitMap = ref({
const buttonDisabled = computed(() => {
return !patientInfo.value;
});
const isSaveDisabled = computed(() => {
return !patientInfo.value || prescriptionList.value.length === 0;
});
const props = defineProps({
patientInfo: {
type: Object,
@@ -679,6 +676,7 @@ function getListInfo(addNewRow) {
organization.value = res?.data?.records ?? res?.data ?? [];
});
getPrescriptionList(patientInfo.value.encounterId).then((res) => {
console.log('getListInfo==========>', JSON.stringify(res.data));
// 等待科室树加载完成后再处理处方数据,确保 resolveOrgId 能正确匹配
orgTreePromise.then(() => {
loadingInstance.close();
@@ -686,6 +684,7 @@ function getListInfo(addNewRow) {
.map((item) => {
const parsedContent = JSON.parse(item.contentJson);
// 构造 unitCodeList确保编辑时下拉框有正确的选项
console.log('【DEBUG】unitCode:', parsedContent?.unitCode, typeof parsedContent?.unitCode, 'unitCodeList:', JSON.stringify(parsedContent?.unitCodeList));
const unitCodeListData = parsedContent?.unitCodeList || [
{ value: String(parsedContent?.unitCode ?? item.unitCode ?? ''), label: parsedContent?.unitCode_dictText ?? item.unitCode_dictText ?? '', type: 'unit' },
{ value: String(parsedContent?.doseUnitCode ?? ''), label: parsedContent?.doseUnitCode_dictText ?? '', type: 'dose' },
@@ -706,7 +705,6 @@ function getListInfo(addNewRow) {
unitCodeList: unitCodeListData,
// 确保 therapyEnum 被正确设置,优先使用 contentJson 中的值
therapyEnum: String(parsedContent?.therapyEnum ?? item.therapyEnum ?? '1'),
// 🔧 修复:确保 orgId 为 String 类型,与 organization 树的 id 类型一致
// 确保 skinTestFlag 是数字类型1 或 0从 contentJson 恢复
skinTestFlag: parsedContent?.skinTestFlag !== undefined && parsedContent?.skinTestFlag !== null
? (typeof parsedContent.skinTestFlag === 'number' ? parsedContent.skinTestFlag : (parsedContent.skinTestFlag ? 1 : 0))
@@ -714,6 +712,7 @@ function getListInfo(addNewRow) {
skinTestFlag_enumText: parsedContent?.skinTestFlag !== undefined && parsedContent?.skinTestFlag !== null
? (parsedContent.skinTestFlag == 1 ? '是' : '否')
: '否',
// 🔧 修复:确保 orgId 为 String 类型,与 organization 树的 id 类型一致
// 关键:优先使用 item.positionId后端 @JsonSerialize 保证精度),
// 而非 parsedContent.orgId来自 JSON.parse大 Long 可能精度丢失)
// 使用 resolveOrgId 从组织树中匹配正确的 String id
@@ -1034,6 +1033,11 @@ function handleFocus(row, index) {
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) {
@@ -1088,7 +1092,6 @@ function selectAdviceBase(key, row) {
}
// 确保 uniqueKey 不被覆盖
prescriptionList.value[rowIndex.value].uniqueKey = currentUniqueKey;
// 检查是否是皮试药品,如果是则弹出确认提示
// 只对药品类型adviceType=1 药品, adviceType=2 耗材)进行皮试提示
const isSkinTestDrug = row.skinTestFlag !== undefined && row.skinTestFlag !== null
@@ -1153,7 +1156,6 @@ function expandOrderAndFocus(key, row) {
});
}
function getOrgList() {
getOrgTree().then((res) => {
organization.value = res?.data?.records ?? res?.data ?? [];
@@ -1581,6 +1583,10 @@ function handleSaveSign(row, index) {
if (row.injectFlag == 1) {
row.sortNumber = row.sortNumber ? row.sortNumber : prescriptionList.value.length;
}
// Bug #589: 出院带药标记到contentJson
if (originalAdviceType == 7) {
row.prescriptionCategory = 3;
}
// 确保 skinTestFlag 是数字类型1 或 0
row.skinTestFlag = row.skinTestFlag !== undefined && row.skinTestFlag !== null
? (typeof row.skinTestFlag === 'number' ? row.skinTestFlag : (row.skinTestFlag ? 1 : 0))
@@ -1637,11 +1643,11 @@ function handleSaveBatch() {
...item,
therapyEnum: therapyEnum,
dbOpType: item.requestId ? '2' : '1',
// 确保 skinTestFlag 是数字类型1 或 0
skinTestFlag: item.skinTestFlag !== undefined && item.skinTestFlag !== null
? (typeof item.skinTestFlag === "number" ? item.skinTestFlag : (item.skinTestFlag ? 1 : 0))
: 0,
skinTestFlag_enumText: item.skinTestFlag == 1 ? '是' : '否',
// 确保 skinTestFlag 是数字类型1 或 0
skinTestFlag: item.skinTestFlag !== undefined && item.skinTestFlag !== null
? (typeof item.skinTestFlag === 'number' ? item.skinTestFlag : (item.skinTestFlag ? 1 : 0))
: 0,
skinTestFlag_enumText: item.skinTestFlag == 1 ? '是' : '否',
};
// Bug #589: 出院带药批量保存时转为药品类型
if (result.adviceType == 7) {

View File

@@ -621,6 +621,7 @@ watch(
(visible) => {
if (visible) {
executeTime.value = formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss');
consumableDefaultLocId.value = null; // 重置耗材默认库房,避免复用上次患者配置
// 弹窗打开时按当前患者科室重新加载,避免复用上一次患者/登录科室的结果
loadDepartmentOptions();
getAdviceBaseInfos();

View File

@@ -227,9 +227,21 @@
</div>
</div>
<!-- 退回原因弹窗 -->
<el-dialog v-model="backReasonVisible" title="退回原因" width="400px" :close-on-click-modal="false">
<el-form ref="backReasonFormRef" :model="backReasonForm" :rules="backReasonRules">
<el-form-item label="退回原因" prop="reason">
<el-dialog
v-model="backReasonVisible"
title="退回原因"
width="400px"
:close-on-click-modal="false"
>
<el-form
ref="backReasonFormRef"
:model="backReasonForm"
:rules="backReasonRules"
>
<el-form-item
label="退回原因"
prop="reason"
>
<el-input
v-model="backReasonForm.reason"
type="textarea"
@@ -241,8 +253,15 @@
</el-form-item>
</el-form>
<template #footer>
<el-button @click="backReasonVisible = false">取消</el-button>
<el-button type="primary" @click="confirmCancel">确定退回</el-button>
<el-button @click="backReasonVisible = false">
取消
</el-button>
<el-button
type="primary"
@click="confirmCancel"
>
确定退回
</el-button>
</template>
</el-dialog>
</template>
@@ -257,10 +276,10 @@ const activeNames = ref([]);
const prescriptionList = ref([]);
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
const type = ref(null);
const { proxy } = getCurrentInstance();
const backReasonVisible = ref(false);
const backReasonForm = ref({ reason: '' });
const backReasonFormRef = ref(null);
const { proxy } = getCurrentInstance();
const loading = ref(false);
const chooseAll = ref(false);
const selectionTrigger = ref(0);
@@ -305,7 +324,6 @@ const getStatusDisplayText = (row) => {
return LEGACY_STATUS_TEXT[row?.requestStatus_enumText] || row?.requestStatus_enumText || '';
};
const getStatusType = (status) => {
const map = {
1: 'info',
@@ -461,7 +479,6 @@ function handleCheck() {
function handleCancel() {
let list = getSelectRows();
if (list.length > 0) {
// 校验已发药的医嘱不允许退回
let dispensedItems = list.filter(item => item.dispenseStatus === 4);
if (dispensedItems.length > 0) {
proxy.$message.error('该药品已由药房发放,请先执行退药处理,不可直接退回');
@@ -482,7 +499,6 @@ function confirmCancel() {
return;
}
let list = getSelectRows();
// 将退回原因附加到每个项目
let requestList = list.map(item => ({
...item,
backReason: backReasonForm.value.reason.trim()

View File

@@ -1180,9 +1180,6 @@ function selectRow(rowValue, index) {
form.purchaseinventoryList[index].partPercent = rowValue.partPercent;
form.purchaseinventoryList[index].unitList = rowValue.unitList[0];
form.purchaseinventoryList[index].lotNumber = rowValue.lotNumber;
// 补全单位字典文本formatInventory 依赖此字段格式化库存显示
form.purchaseinventoryList[index].unitCode_dictText = rowValue.unitCode_dictText;
form.purchaseinventoryList[index].minUnitCode_dictText = rowValue.minUnitCode_dictText;
form.purchaseinventoryList[index].itemQuantity = 0;
form.purchaseinventoryList[index].totalPrice = 0;
// 维护一个大小单位的map用来判断当前选中单位是大/小单位
@@ -1197,13 +1194,21 @@ function selectRow(rowValue, index) {
value: rowValue.minUnitCode,
},
];
// 新单/编辑单统一使用行级仓库ID不再分支判断 route.query.supplyBusNo
handleLocationClick(
form.purchaseinventoryList[index].sourceLocationId,
form.purchaseinventoryList[index].purposeLocationId,
form.purchaseinventoryList[index].itemId,
index
);
if (route.query.supplyBusNo) {
handleLocationClick(
receiptHeaderForm.sourceLocationId1,
receiptHeaderForm.purposeLocationId1,
form.purchaseinventoryList[index].itemId,
index
);
} else {
handleLocationClick(
form.purchaseinventoryList[index].sourceLocationId,
form.purchaseinventoryList[index].purposeLocationId,
form.purchaseinventoryList[index].itemId,
index
);
}
editBatchTransfer(index); // todo
}
@@ -1215,29 +1220,23 @@ function handleLocationClick(id, purposeLocationId, itemId, index) {
objLocationId: purposeLocationId,
lotNumber: form.purchaseinventoryList[index].lotNumber,
}).then((res) => {
if (res.data && res.data.length) {
// SQL 按 locationId 分组后可能有两条记录(源/目的),根据 locationId 精确匹配而非盲目取 res.data[0]
const srcId = String(id);
const purId = String(purposeLocationId);
const sourceRow = res.data.find(item => String(item.locationId) === srcId) || {};
const purposeRow = res.data.find(item => String(item.locationId) === purId) || {};
if (res.data && res.data[0]) {
form.purchaseinventoryList[index].itemTable = res.data[0].itemTable || '';
form.purchaseinventoryList[index].supplierId = res.data[0].supplierId || '';
form.purchaseinventoryList[index].startTime = formatDateymd(res.data[0].productionDate) || '';
form.purchaseinventoryList[index].endTime = formatDateymd(res.data[0].expirationDate) || '';
form.purchaseinventoryList[index].itemTable = sourceRow.itemTable || '';
form.purchaseinventoryList[index].supplierId = sourceRow.supplierId || '';
form.purchaseinventoryList[index].startTime = formatDateymd(sourceRow.productionDate) || '';
form.purchaseinventoryList[index].endTime = formatDateymd(sourceRow.expirationDate) || '';
form.purchaseinventoryList[index].price = sourceRow.price;
form.purchaseinventoryList[index].totalSourceQuantity = sourceRow.orgQuantity || 0;
form.purchaseinventoryList[index].totalPurposeQuantity = purposeRow.objQuantity || 0;
form.purchaseinventoryList[index].price = res.data[0].price;
form.purchaseinventoryList[index].totalSourceQuantity = res.data[0].orgQuantity;
form.purchaseinventoryList[index].totalPurposeQuantity = res.data[0].objQuantity;
form.purchaseinventoryList[index].totalSourceQuantityDisplay = formatInventory(
sourceRow.orgQuantity || 0,
res.data[0].orgQuantity,
form.purchaseinventoryList[index].partPercent,
form.purchaseinventoryList[index].unitCode_dictText,
form.purchaseinventoryList[index].minUnitCode_dictText
);
form.purchaseinventoryList[index].totalPurposeQuantityDisplay = formatInventory(
purposeRow.objQuantity || 0,
res.data[0].objQuantity,
form.purchaseinventoryList[index].partPercent,
form.purchaseinventoryList[index].unitCode_dictText,
form.purchaseinventoryList[index].minUnitCode_dictText
@@ -1341,8 +1340,8 @@ function formatInventory(quantity, partPercent, unitCode, minUnitCode) {
return isNegative ? '-' + result : result;
}
// 整除时也需拼接单位后缀,否则显示为纯数字缺少单位信息
const result = (absQuantity / partPercent) + ' ' + unitCode;
// 整除情况
const result = absQuantity / partPercent;
return isNegative ? '-' + result : result;
}

View File

@@ -75,12 +75,30 @@
clearable
style="width: 120px"
>
<el-option label="已到达" :value="1" />
<el-option label="已分诊" :value="2" />
<el-option label="已看诊" :value="3" />
<el-option label="已离开" :value="4" />
<el-option label="已完成" :value="5" />
<el-option label="无状态" :value="0" />
<el-option
label="已到达"
:value="1"
/>
<el-option
label="已分诊"
:value="2"
/>
<el-option
label="已看诊"
:value="3"
/>
<el-option
label="已离开"
:value="4"
/>
<el-option
label="已完成"
:value="5"
/>
<el-option
label="无状态"
:value="0"
/>
</el-select>
</el-form-item>
<el-form-item
@@ -251,11 +269,8 @@ function getList() {
console.log('当前查看患者:', route.query.patientName);
}
// 构建请求参数 - "无状态"(0) 转为 undefined让后端不加过滤
// 构建请求参数
const requestParams = { ...queryParams.value };
if (requestParams.subjectStatusEnum === 0) {
requestParams.subjectStatusEnum = undefined;
}
listOutpatienRecords(requestParams).then((response) => {
outpatienRecordsList.value = response.data.records;
total.value = response.data.total;