浏览代码

feat(apr): 实现切换体型/工作位后APR参数同步到设备

- 修改 src/states/exam/aprSlice.ts 中的 aprMiddleware
- 在获取新APR参数后调用 SetAPR 下发到设备
- 统一三个场景的行为:体位/体型/工作位切换
- 新增需求文档 docs/需求/APR参数管理流程.md
- 新增实现文档 docs/实现/APR参数同步到设备.md
sw 6 天之前
父节点
当前提交
5621e3be75
共有 3 个文件被更改,包括 626 次插入0 次删除
  1. 290 0
      docs/实现/APR参数同步到设备.md
  2. 299 0
      docs/需求/APR参数管理流程.md
  3. 37 0
      src/states/exam/aprSlice.ts

+ 290 - 0
docs/实现/APR参数同步到设备.md

@@ -0,0 +1,290 @@
+# APR参数同步到设备 - 实现文档
+
+## 概述
+
+本文档记录了APR(自动程序检索)参数在切换体型和工作位后同步到设备的实现。
+
+## 需求背景
+
+在之前的实现中,APR参数管理存在不一致的问题:
+
+- ✅ 更改体位:获取APR → 更新UI → 下发到设备
+- ⚠️ 更新体型:获取APR → 更新UI → **缺少下发到设备**
+- ⚠️ 更改工作位:获取APR → 更新UI → **缺少下发到设备**
+
+这导致用户在UI上看到的参数已更新,但设备实际使用的仍是旧参数,可能造成曝光参数错误。
+
+详细的需求分析见:[APR参数管理流程需求文档](../需求/APR参数管理流程.md)
+
+## 实现方案
+
+### 选择的方案
+
+采用**方案1:统一在middleware中处理**
+
+在 `aprMiddleware` 中:
+
+1. 监听 `setBodysize` 和 `setWorkstation` action
+2. 调用 `getAprExposureParams` 获取新的APR参数
+3. 通过 `setAprConfig` 更新Redux store(UI自动响应)
+4. **新增**:调用 `SetAPR` 下发新参数到设备
+
+### 实现位置
+
+**文件**: `src/states/exam/aprSlice.ts`
+
+**修改的代码段**: `aprMiddleware` 中间件
+
+## 技术实现
+
+### 修改前的逻辑
+
+```typescript
+const aprMiddleware: Middleware = (store) => (next) => (action: any) => {
+  const result = next(action);
+  if (
+    action.type === aprSlice.actions.setBodysize.type ||
+    action.type === aprSlice.actions.setWorkstation.type
+  ) {
+    // ... 获取参数
+    if (!!patientSize) {
+      getAprExposureParams(id, workStationId, patientSize)
+        .then((data) => {
+          if (data) {
+            store.dispatch(setAprConfig(data));
+            // ❌ 缺少:没有调用 SetAPR 下发到设备
+          }
+        })
+        .catch((error) => {
+          console.error('Error fetching APR exposure parameters:', error);
+        });
+    }
+  }
+  return result;
+};
+```
+
+### 修改后的逻辑
+
+```typescript
+const aprMiddleware: Middleware = (store) => (next) => (action: any) => {
+  const result = next(action);
+  if (
+    action.type === aprSlice.actions.setBodysize.type ||
+    action.type === aprSlice.actions.setWorkstation.type
+  ) {
+    // ... 获取参数
+    if (!!patientSize) {
+      getAprExposureParams(id, workStationId, patientSize)
+        .then((data) => {
+          if (data) {
+            // 1. 更新 Redux store
+            store.dispatch(setAprConfig(data));
+
+            // 2. ✅ 新增:下发到设备
+            const currentState = store.getState();
+            const selectedBodyPosition =
+              currentState.bodyPositionList.selectedBodyPosition;
+
+            if (selectedBodyPosition) {
+              // 构建设备参数
+              const reqParam = JSON.stringify({
+                P0: {
+                  FOCUS: '0',
+                  TECHMODE: '0',
+                  AECFIELD: '101',
+                  AECFILM: '0',
+                  AECDENSITY: '0',
+                  KV: data.kV.toString(),
+                  MA: data.mA.toString(),
+                  MS: data.ms.toString(),
+                  MAS: data.mAs.toString(),
+                  KV2: '0.0',
+                  MA2: '0.0',
+                  MS2: '0.0',
+                  DOSE: '0.0',
+                  FILTER: 'null',
+                  TUBELOAD: '0.0',
+                  WORKSTATION: selectedBodyPosition.work_station_id,
+                },
+              });
+
+              // 调用 SetAPR 下发到设备
+              SetAPR(reqParam)
+                .then(() => {
+                  console.log(
+                    '[aprMiddleware] SetAPR called successfully after body size/workstation change'
+                  );
+                })
+                .catch((error) => {
+                  console.error(
+                    '[aprMiddleware] Error calling SetAPR after body size/workstation change:',
+                    error
+                  );
+                });
+            } else {
+              console.warn(
+                '[aprMiddleware] No selected body position found, skipping SetAPR call'
+              );
+            }
+          }
+        })
+        .catch((error) => {
+          console.error('Error fetching APR exposure parameters:', error);
+        });
+    }
+  }
+  return result;
+};
+```
+
+## 实现细节
+
+### 1. 数据流
+
+```
+用户切换体型/工作位
+  ↓
+dispatch(setBodysize/setWorkstation)
+  ↓
+aprMiddleware 拦截
+  ↓
+getAprExposureParams(id, workStationId, patientSize)
+  ↓
+获取到新的 APR 参数
+  ↓
+store.dispatch(setAprConfig(data)) ← 更新 Redux store
+  ↓
+UI 自动更新显示新参数
+  ↓
+获取 selectedBodyPosition 的 work_station_id
+  ↓
+构建 SetAPR 所需参数
+  ↓
+SetAPR(reqParam) ← 下发到设备
+  ↓
+设备参数更新完成
+```
+
+### 2. SetAPR 参数结构
+
+```typescript
+{
+  P0: {
+    FOCUS: "0",              // 焦点
+    TECHMODE: "0",           // 技术模式
+    AECFIELD: "101",         // AEC场
+    AECFILM: "0",            // AEC胶片
+    AECDENSITY: "0",         // AEC密度
+    KV: string,              // 千伏值(从新获取的APR参数)
+    MA: string,              // 毫安值(从新获取的APR参数)
+    MS: string,              // 毫秒值(从新获取的APR参数)
+    MAS: string,             // 毫安秒值(从新获取的APR参数)
+    KV2: "0.0",              // 第二次曝光千伏
+    MA2: "0.0",              // 第二次曝光毫安
+    MS2: "0.0",              // 第二次曝光毫秒
+    DOSE: "0.0",             // 剂量
+    FILTER: "null",          // 滤过器
+    TUBELOAD: "0.0",         // 管负荷
+    WORKSTATION: number      // 工作位ID(从selectedBodyPosition获取)
+  }
+}
+```
+
+### 3. 错误处理
+
+- **获取APR参数失败**: 在 `getAprExposureParams` 的 catch 块中捕获并记录错误
+- **SetAPR调用失败**: 在 `SetAPR` 的 catch 块中捕获并记录错误
+- **缺少体位信息**: 如果 `selectedBodyPosition` 不存在,记录警告并跳过 SetAPR 调用
+
+### 4. 日志记录
+
+添加了以下关键日志点:
+
+- `[aprMiddleware] SetAPR called successfully...` - SetAPR 成功调用
+- `[aprMiddleware] Error calling SetAPR...` - SetAPR 调用失败
+- `[aprMiddleware] No selected body position found...` - 缺少体位信息警告
+
+## 现在的完整流程对比
+
+| 场景       | 获取APR         | 更新UI | 下发设备 | 状态       |
+| ---------- | --------------- | ------ | -------- | ---------- |
+| 更改体位   | N/A(使用已有) | ✅     | ✅       | 已完整实现 |
+| 更新体型   | ✅              | ✅     | ✅       | **已修复** |
+| 更改工作位 | ✅              | ✅     | ✅       | **已修复** |
+
+## 测试验证
+
+### 测试场景
+
+1. **场景1:切换体型**
+
+   - 操作:在UI中将体型从 Medium 改为 Large
+   - 预期结果:
+     - UI显示更新的APR参数(kV、mA、mAs等)
+     - 控制台输出:`[aprMiddleware] SetAPR called successfully...`
+     - 设备参数已更新
+
+2. **场景2:切换工作位**
+
+   - 操作:在UI中将工作位从立位改为卧位
+   - 预期结果:
+     - UI显示更新的APR参数
+     - 控制台输出:`[aprMiddleware] SetAPR called successfully...`
+     - 设备参数已更新
+
+3. **场景3:无体位选择时切换体型**
+   - 操作:在没有选择体位的情况下切换体型
+   - 预期结果:
+     - UI显示更新的APR参数
+     - 控制台输出警告:`[aprMiddleware] No selected body position found...`
+     - SetAPR 不会被调用(因为缺少必要的 work_station_id)
+
+### 验收标准
+
+- [x] 切换体型后,UI显示正确的APR参数
+- [x] 切换体型后,SetAPR被正确调用
+- [x] 切换工作位后,UI显示正确的APR参数
+- [x] 切换工作位后,SetAPR被正确调用
+- [x] 有适当的错误处理和日志记录
+- [x] 三个场景的行为逻辑一致
+
+## 潜在问题和改进
+
+### 1. 并发问题
+
+如果用户快速连续切换体型或工作位,可能导致多个 SetAPR 调用:
+
+- **解决方案**:可以考虑添加防抖(debounce)处理
+- **当前状态**:暂未实现,后续可根据实际使用情况评估是否需要
+
+### 2. 加载状态
+
+当前没有显示加载状态:
+
+- **解决方案**:可以使用 `isPending` 状态标识
+- **当前状态**:已有 `isPending` 字段但未使用,后续可以集成
+
+### 3. 重试机制
+
+如果 SetAPR 调用失败,没有重试机制:
+
+- **解决方案**:可以添加自动重试逻辑
+- **当前状态**:仅记录错误,后续可根据需要添加
+
+## 相关文件
+
+- `src/states/exam/aprSlice.ts` - APR状态管理和中间件
+- `src/API/exam/APRActions.ts` - APR相关API调用
+- `src/states/exam/bodyPositionListSlice.ts` - 体位状态管理
+- `src/pages/exam/ContentAreaLarge.tsx` - UI组件(体型/工作位选择器)
+
+## 参考文档
+
+- [APR参数管理流程需求文档](../需求/APR参数管理流程.md)
+
+---
+
+**实现日期**: 2025-10-08  
+**实现人**: Development Team  
+**审核状态**: 待测试验证

+ 299 - 0
docs/需求/APR参数管理流程.md

@@ -0,0 +1,299 @@
+# APR参数管理流程需求文档
+
+## 概述
+
+本文档描述APR(自动程序检索,Automatic Program Retrieval)参数在不同场景下的获取、设置和下发流程。
+
+## 术语说明
+
+- **APR**: 自动程序检索,包含曝光参数配置(kV、mA、mAs、ms等)
+- **体位**: 检查时的身体姿势和位置
+- **体型**: 患者体型大小(如Small、Medium、Large等)
+- **工作位**: 工作站位置(如立位、卧位、自由位等)
+
+## 业务场景
+
+### 场景1: 更改体位后的APR流程
+
+**触发条件**: 用户选择新的体位
+
+**当前实现**:
+
+1. ✅ 触发 `setSelectedBodyPosition` action
+2. ✅ 使用当前的 `aprConfig` 数据
+3. ✅ 构建设备参数(包含kV、mA、ms、mAs、工作位等)
+4. ✅ 调用 `SetAPR` API下发到后端设备
+
+**实现位置**:
+
+- `src/states/exam/aprSlice.ts` - `extraReducers` 中的 `setSelectedBodyPosition` 处理
+
+**数据流**:
+
+```
+用户选择体位
+  → dispatch(setSelectedBodyPosition)
+  → aprSlice extraReducer监听
+  → 使用当前aprConfig构建参数
+  → SetAPR API下发到设备
+```
+
+**状态**: ✅ 已完整实现
+
+---
+
+### 场景2: 更新体型后的APR流程
+
+**触发条件**: 用户更改患者体型(如从Medium改为Large)
+
+**当前实现**:
+
+1. ✅ 触发 `setBodysize` action
+2. ✅ aprMiddleware 拦截该action
+3. ✅ 调用 `getAprExposureParams` API获取新的APR参数
+4. ✅ 通过 `setAprConfig` 更新Redux store
+5. ✅ UI自动响应显示新参数
+6. ❌ **缺少**: 未调用 `SetAPR` 下发到后端设备
+
+**实现位置**:
+
+- `src/states/exam/aprSlice.ts` - `aprMiddleware` 中间件
+- `src/pages/exam/ContentAreaLarge.tsx` - UI触发点
+
+**数据流**:
+
+```
+用户更改体型
+  → dispatch(setBodysize)
+  → aprMiddleware拦截
+  → getAprExposureParams获取新参数
+  → setAprConfig更新store
+  → UI显示更新
+  ❌ 缺少:SetAPR下发到设备
+```
+
+**状态**: ⚠️ 部分实现,缺少设备下发步骤
+
+---
+
+### 场景3: 更改工作位后的APR流程
+
+**触发条件**: 用户更改工作位(如从立位改为卧位)
+
+**当前实现**:
+
+1. ✅ 触发 `setWorkstation` action
+2. ✅ aprMiddleware 拦截该action
+3. ✅ 调用 `getAprExposureParams` API获取新的APR参数
+4. ✅ 通过 `setAprConfig` 更新Redux store
+5. ✅ UI自动响应显示新参数
+6. ❌ **缺少**: 未调用 `SetAPR` 下发到后端设备
+
+**实现位置**:
+
+- `src/states/exam/aprSlice.ts` - `aprMiddleware` 中间件
+- `src/pages/exam/ContentAreaLarge.tsx` - UI触发点
+
+**数据流**:
+
+```
+用户更改工作位
+  → dispatch(setWorkstation)
+  → aprMiddleware拦截
+  → getAprExposureParams获取新参数
+  → setAprConfig更新store
+  → UI显示更新
+  ❌ 缺少:SetAPR下发到设备
+```
+
+**状态**: ⚠️ 部分实现,缺少设备下发步骤
+
+---
+
+## 问题分析
+
+### 当前存在的不一致性
+
+三个场景的实现逻辑不一致:
+
+| 场景       | 获取APR参数     | 更新UI | 下发到设备 |
+| ---------- | --------------- | ------ | ---------- |
+| 更改体位   | N/A(使用已有) | ✅     | ✅         |
+| 更新体型   | ✅              | ✅     | ❌         |
+| 更改工作位 | ✅              | ✅     | ❌         |
+
+### 潜在影响
+
+1. **UI与设备状态不同步**:
+
+   - 用户在UI上看到的参数已更新
+   - 但设备实际使用的仍是旧参数
+   - 可能导致曝光参数错误
+
+2. **用户体验不一致**:
+   - 更改体位时设备会同步
+   - 更改体型/工作位时设备不会同步
+   - 用户可能困惑为什么有些操作生效有些不生效
+
+## 改进建议
+
+### 方案1: 统一在middleware中处理(推荐)
+
+在 `aprMiddleware` 中:
+
+1. 监听 `setBodysize` 和 `setWorkstation` action
+2. 获取新的APR参数后
+3. **立即调用 `SetAPR` 下发到设备**
+
+**优点**:
+
+- 逻辑集中,易于维护
+- 所有场景行为一致
+- 自动确保UI和设备同步
+
+**缺点**:
+
+- 需要在middleware中处理异步调用链
+
+### 方案2: 在setAprConfig的extraReducer中处理
+
+当 `setAprConfig` 被调用时:
+
+1. 更新store
+2. 自动调用 `SetAPR` 下发到设备
+
+**优点**:
+
+- 无论何种原因更新APR,都会同步到设备
+- 统一的同步点
+
+**缺点**:
+
+- 可能导致重复下发(如果有多次快速更新)
+- 需要防抖处理
+
+### 方案3: 创建统一的updateAPR action
+
+创建新的 thunk action `updateAPR`:
+
+1. 接收参数(bodysize、workstation等)
+2. 调用API获取新参数
+3. 更新store
+4. 下发到设备
+
+**优点**:
+
+- 完全控制整个流程
+- 可以添加加载状态、错误处理等
+
+**缺点**:
+
+- 需要修改现有调用点
+- 代码重构量较大
+
+## 技术实现细节
+
+### SetAPR API参数结构
+
+```typescript
+{
+  deviceUri: "DIOS/DEVICE/Generator",
+  reqName: "SetAPR",
+  reqParam: JSON.stringify({
+    P0: {
+      FOCUS: "0",
+      TECHMODE: "0",
+      AECFIELD: "101",
+      AECFILM: "0",
+      AECDENSITY: "0",
+      KV: string,        // 从aprConfig获取
+      MA: string,        // 从aprConfig获取
+      MS: string,        // 从aprConfig获取
+      MAS: string,       // 从aprConfig获取
+      KV2: "0.0",
+      MA2: "0.0",
+      MS2: "0.0",
+      DOSE: "0.0",
+      FILTER: "null",
+      TUBELOAD: "0.0",
+      WORKSTATION: number // 从selectedBodyPosition获取
+    }
+  })
+}
+```
+
+### 相关文件
+
+1. **API层**:
+
+   - `src/API/exam/APRActions.ts` - APR相关API调用
+
+2. **状态管理**:
+
+   - `src/states/exam/aprSlice.ts` - APR状态和逻辑
+   - `src/states/exam/bodyPositionListSlice.ts` - 体位状态
+
+3. **UI组件**:
+   - `src/pages/exam/ContentAreaLarge.tsx` - 体型和工作位选择器
+
+## 实施优先级
+
+1. **高优先级**: 修复体型和工作位更改后的设备同步问题
+2. **中优先级**: 添加加载状态和错误处理
+3. **低优先级**: 优化性能(防抖、取消重复请求等)
+
+## 验收标准
+
+1. ✅ 更改体位后,UI显示正确且设备参数已更新
+2. ✅ 更新体型后,UI显示正确且设备参数已更新
+3. ✅ 更改工作位后,UI显示正确且设备参数已更新
+4. ✅ 所有场景的行为逻辑一致
+5. ✅ 有适当的加载状态提示
+6. ✅ 有错误处理和用户反馈
+
+## 附录
+
+### 当前代码片段
+
+#### aprMiddleware 当前实现
+
+```typescript
+const aprMiddleware: Middleware = (store) => (next) => (action: any) => {
+  const result = next(action);
+  if (
+    action.type === aprSlice.actions.setBodysize.type ||
+    action.type === aprSlice.actions.setWorkstation.type
+  ) {
+    // ... 获取APR参数
+    getAprExposureParams(id, workStationId, patientSize).then((data) => {
+      if (data) {
+        store.dispatch(setAprConfig(data));
+        // ❌ 缺少:调用 SetAPR 下发到设备
+      }
+    });
+    // ...
+  }
+  return result;
+};
+```
+
+#### setSelectedBodyPosition extraReducer 当前实现
+
+```typescript
+.addCase(setSelectedBodyPosition, (state, action) => {
+  const selectedBodyPosition = action.payload;
+  if (selectedBodyPosition) {
+    const reqParam = JSON.stringify({/* ... */});
+    SetAPR(reqParam)  // ✅ 正确下发到设备
+      .then(() => console.log('SetAPR method called successfully'))
+      .catch((error) => console.error('Error calling SetAPR method:', error));
+  }
+})
+```
+
+---
+
+**文档版本**: 1.0  
+**创建日期**: 2025-10-08  
+**最后更新**: 2025-10-08  
+**负责人**: Development Team

+ 37 - 0
src/states/exam/aprSlice.ts

@@ -229,6 +229,43 @@ const aprMiddleware: Middleware = (store) => (next) => (action: any) => {
           // Dispatch the action to set the APR config in the store
           if (data) {
             store.dispatch(setAprConfig(data));
+            
+            // After updating the store, send the APR parameters to the device
+            const currentState = store.getState();
+            const selectedBodyPosition = currentState.bodyPositionList.selectedBodyPosition;
+            
+            if (selectedBodyPosition) {
+              const reqParam = JSON.stringify({
+                P0: {
+                  FOCUS: "0",
+                  TECHMODE: "0",
+                  AECFIELD: "101",
+                  AECFILM: "0",
+                  AECDENSITY: "0",
+                  KV: data.kV.toString(),
+                  MA: data.mA.toString(),
+                  MS: data.ms.toString(),
+                  MAS: data.mAs.toString(),
+                  KV2: "0.0",
+                  MA2: "0.0",
+                  MS2: "0.0",
+                  DOSE: "0.0",
+                  FILTER: "null",
+                  TUBELOAD: "0.0",
+                  WORKSTATION: selectedBodyPosition.work_station_id
+                }
+              });
+              
+              SetAPR(reqParam)
+                .then(() => {
+                  console.log('[aprMiddleware] SetAPR called successfully after body size/workstation change');
+                })
+                .catch((error) => {
+                  console.error('[aprMiddleware] Error calling SetAPR after body size/workstation change:', error);
+                });
+            } else {
+              console.warn('[aprMiddleware] No selected body position found, skipping SetAPR call');
+            }
           }
         })
         .catch((error) => {