# Exam 体位列表 - 单击已曝光体位优化实现文档 ## 📋 功能概述 **功能名称**: Exam 页面体位列表单击已曝光体位行为优化 **实现状态**: 🔄 **待实现** **核心需求**: 优化 Exam 页面体位列表的交互行为,区分已曝光和未曝光体位的单击响应: ### 在检查(exam)模式 - **单击未曝光体位**:✅ 保持现有逻辑(选中体位、同步设备、使能曝光) - **单击已曝光体位**:⚠️ 需要优化 - **不下发设备 action**(不调用 `changeBodyPosition`、不调用 `setExpEnable`) - 在 `BodyPositionDetail` 中显示**已曝光的缩略图**(而非示意图) ### 在处理(process)模式 - **单击未曝光体位**:✅ 保持现有逻辑(切换回 exam 模式) - **单击已曝光体位**:✅ 保持现有逻辑(选中体位) --- ## 🎯 涉及的参与者分析 ### 1️⃣ 组件层 #### BodyPositionList.tsx **文件路径**: `src/pages/exam/components/BodyPositionList.tsx` | 元素 | 类型 | 职责 | 当前行为 | |------|------|------|---------| | BodyPositionList | React Component | 渲染体位列表,显示体位图像(示意图或缩略图) | 已实现 | | handleImageClick | Method | 处理体位图像点击事件 | 调用 `manualSelectBodyPositionWithFlowSwitch` | | ImageViewer | Component | 显示单个体位图像 | 根据 `expose_status` 选择图像源 | **关键代码**: ```typescript const handleImageClick = async (bodyPosition: ExtendedBodyPosition): Promise => { await manualSelectBodyPositionWithFlowSwitch( bodyPosition, dispatch, currentKey ); }; // 图像显示逻辑 handleImageClick(bodyPosition)} /> ``` #### BodyPositionDetail.tsx **文件路径**: `src/pages/exam/components/BodyPositionDetail.tsx` | 元素 | 类型 | 职责 | 需要修改 | |------|------|------|---------| | BodyPositionDetail | React Component | 显示选中体位的详细信息 | ✅ 需要修改 | | 体位图像显示区域 | UI Element | 第三行,显示体位大图 | ✅ 需要根据曝光状态切换图像源 | **当前显示逻辑**(只显示示意图): ```typescript backgroundImage: `url(${getViewIconUrl(bodyPositionDetail.body_position_image)})` ``` **需要改为**(根据曝光状态选择): ```typescript const imageUrl = bodyPositionDetail.expose_status === 'Exposed' ? getExposedImageUrl(bodyPositionDetail.sop_instance_uid) // 已曝光:缩略图 : getViewIconUrl(bodyPositionDetail.body_position_image); // 未曝光:示意图 backgroundImage: `url(${imageUrl})` ``` --- ### 2️⃣ 业务逻辑层 #### bodyPositionSelection.ts **文件路径**: `src/domain/exam/bodyPositionSelection.ts` | 函数 | 职责 | 需要修改 | |------|------|---------| | selectBodyPositionWithFullLogic | 核心选中逻辑,处理状态更新和设备同步 | ✅ 需要修改 | | manualSelectBodyPositionWithFlowSwitch | 带流程切换的手动选中(用户点击时调用) | ✅ 已实现自动流程切换 | | autoSelectFirstBodyPosition | 自动选中第一个体位 | ✅ 无需修改 | **当前逻辑**(exam 模式下无条件同步设备): ```typescript // 3. 如果在exam模式,同步设备到对应体位 if (currentKey === 'exam') { await changeBodyPosition(bodyPosition.sop_instance_uid); await setExpEnable(); // ... } ``` **需要改为**(仅未曝光时同步设备): ```typescript // 3. 如果在exam模式,根据曝光状态决定是否同步设备 if (currentKey === 'exam') { if (bodyPosition.dview.expose_status === 'Unexposed') { // 未曝光:同步设备,使能曝光 await changeBodyPosition(bodyPosition.sop_instance_uid); await setExpEnable(); console.log(`[bodyPositionSelection] Device synced for unexposed position`); } else { // 已曝光:跳过设备同步,仅更新显示 console.log(`[bodyPositionSelection] Exposed position, skip device sync`); } } ``` --- ### 3️⃣ 状态管理层 #### bodyPositionDetailSlice.ts **文件路径**: `src/states/exam/bodyPositionDetailSlice.ts` | 元素 | 类型 | 职责 | 需要修改 | |------|------|------|---------| | BodyPositionDetailState | Interface | 体位详情状态定义 | ✅ 需要添加字段 | | setBodyPositionDetail | Reducer | 设置体位详情 | ✅ 需要更新字段 | | extraReducers | Listener | 监听 `setSelectedBodyPosition`,自动更新详情 | ✅ 需要更新字段 | **需要添加的字段**: ```typescript interface BodyPositionDetailState { // ... 现有字段 expose_status: string; // 🆕 'Exposed' | 'Unexposed' sop_instance_uid: string; // 🆕 用于获取缩略图 URL } ``` **初始状态**: ```typescript const initialState: BodyPositionDetailState = { // ... 现有字段 expose_status: 'Unexposed', // 🆕 默认未曝光 sop_instance_uid: '', // 🆕 默认空 }; ``` **Reducer 更新**: ```typescript builder.addCase(setSelectedBodyPosition, (state, action) => { const selectedBodyPosition = action.payload; if (selectedBodyPosition) { return { ...state, // ... 现有字段 expose_status: selectedBodyPosition.dview.expose_status, // 🆕 sop_instance_uid: selectedBodyPosition.sop_instance_uid, // 🆕 }; } return state; }); ``` --- ### 4️⃣ API 层 #### bodyPosition.ts **文件路径**: `src/API/bodyPosition.ts` | 函数 | 职责 | 状态 | |------|------|------| | getViewIconUrl | 获取体位示意图 URL | ✅ 已实现 | | getExposedImageUrl | 获取已曝光图像的缩略图 URL | ✅ 已实现 | ```typescript // 示意图 URL(用于未曝光体位) export function getViewIconUrl(viewIconName: string): string { return `${API_BASE_URL}pub${viewIconName}`; } // 缩略图 URL(用于已曝光体位) export function getExposedImageUrl(sopInstanceUid: string): string { return `${API_BASE_URL}pub/thumbnail/${sopInstanceUid}.webp`; } ``` --- ## 🔄 数据流分析 ### 场景1: 单击未曝光体位(exam 模式) ```mermaid sequenceDiagram actor User as 👤 用户 participant BPL as BodyPositionList participant BS as bodyPositionSelection participant API as Device API participant Redux as Redux Store participant BPD as BodyPositionDetail User->>BPL: 单击未曝光体位 BPL->>BS: manualSelectBodyPositionWithFlowSwitch(bodyPosition) BS->>BS: 检查 currentKey === 'exam' BS->>BS: 检查 expose_status === 'Unexposed' ✅ BS->>API: changeBodyPosition(sop_instance_uid) Note over API: 同步设备到该体位 API-->>BS: 成功 BS->>API: setExpEnable() Note over API: 使能曝光 API-->>BS: 成功 BS->>Redux: setSelectedBodyPosition(bodyPosition) Redux->>Redux: 自动触发 bodyPositionDetail 更新 Redux-->>BPD: expose_status = 'Unexposed' BPD->>BPD: imageUrl = getViewIconUrl(body_position_image) BPD-->>User: 显示示意图 🖼️ ``` **关键步骤**: 1. ✅ 调用 `changeBodyPosition` - 同步设备 2. ✅ 调用 `setExpEnable` - 使能曝光 3. ✅ 更新 Redux 状态 4. ✅ BodyPositionDetail 显示**示意图** --- ### 场景2: 单击已曝光体位(exam 模式)⚠️ **核心修改** ```mermaid sequenceDiagram actor User as 👤 用户 participant BPL as BodyPositionList participant BS as bodyPositionSelection participant Redux as Redux Store participant BPD as BodyPositionDetail User->>BPL: 单击已曝光体位 BPL->>BS: manualSelectBodyPositionWithFlowSwitch(bodyPosition) BS->>BS: 检查 currentKey === 'exam' BS->>BS: 检查 expose_status === 'Exposed' ✅ Note over BS: ❌ 跳过 changeBodyPosition Note over BS: ❌ 跳过 setExpEnable Note over BS: 只更新显示状态 BS->>Redux: setSelectedBodyPosition(bodyPosition) Redux->>Redux: 自动触发 bodyPositionDetail 更新 Redux-->>BPD: expose_status = 'Exposed'
sop_instance_uid = '...' BPD->>BPD: imageUrl = getExposedImageUrl(sop_instance_uid) BPD-->>User: 显示缩略图 📷 ``` **关键差异**: 1. ❌ **不调用** `changeBodyPosition` - 不同步设备 2. ❌ **不调用** `setExpEnable` - 不使能曝光 3. ✅ 更新 Redux 状态(包含 `expose_status` 和 `sop_instance_uid`) 4. ✅ BodyPositionDetail 显示**缩略图** --- ### 场景3: 单击未曝光体位(process 模式) ```mermaid sequenceDiagram actor User as 👤 用户 participant BPL as BodyPositionList participant BS as bodyPositionSelection participant Redux as Redux Store User->>BPL: 单击未曝光体位 BPL->>BS: manualSelectBodyPositionWithFlowSwitch(bodyPosition) BS->>BS: 检查 currentKey === 'process' BS->>BS: 检查 expose_status === 'Unexposed' ✅ Note over BS: 需要切换到 exam 模式 BS->>Redux: setBusinessFlow('exam') Note over Redux: currentKey: 'process' → 'exam' BS->>BS: 等待流程切换完成 BS->>BS: selectBodyPositionWithFullLogic
(targetFlow = 'exam') Note over BS: 现在在 exam 模式,未曝光体位 Note over BS: 执行场景1的逻辑 ``` **关键行为**: ✅ 保持现有逻辑(自动切换到 exam 并同步设备) --- ## 📊 状态对比表 | 场景 | 模式 | 体位状态 | changeBodyPosition | setExpEnable | 详情显示 | |------|------|---------|-------------------|--------------|---------| | 1 | exam | Unexposed | ✅ 调用 | ✅ 调用 | 示意图 🖼️ | | 2 | exam | Exposed | ❌ 跳过 | ❌ 跳过 | **缩略图 📷** | | 3 | process | Unexposed | ✅ 调用(切换后) | ✅ 调用(切换后) | 示意图 🖼️ | | 4 | process | Exposed | ❌ 跳过 | ❌ 跳过 | **缩略图 📷** | --- ## 💻 实现方案 ### 修改清单 | 文件 | 修改类型 | 行号/位置 | 具体内容 | |------|---------|----------|---------| | `bodyPositionSelection.ts` | 🔧 修改逻辑 | 第47-60行 | 在 `selectBodyPositionWithFullLogic` 添加曝光状态判断 | | `bodyPositionDetailSlice.ts` | ➕ 添加字段 | Interface 定义 | 添加 `expose_status` 和 `sop_instance_uid` 字段 | | `bodyPositionDetailSlice.ts` | 🔧 修改 Reducer | extraReducers | 更新字段映射逻辑 | | `BodyPositionDetail.tsx` | 🔧 修改显示 | 第三行图像区域 | 根据 `expose_status` 动态选择图像源 | --- ### 1️⃣ 修改 `bodyPositionSelection.ts` **文件**: `src/domain/exam/bodyPositionSelection.ts` ```typescript export const selectBodyPositionWithFullLogic = async ( bodyPosition: ExtendedBodyPosition, dispatch: AppDispatch, currentKey: string, showMessage = false ): Promise => { try { console.log( `[bodyPositionSelection] Selecting body position: ${bodyPosition.view_name}` ); // 1. 更新选中状态 dispatch(setSelectedBodyPosition(bodyPosition)); // 2. 设置详情显示区域的数据 dispatch( setBodyPositionDetail({ view_name: bodyPosition.view_name, view_description: bodyPosition.view_description, view_icon_name: bodyPosition.view_icon_name, patient_name: bodyPosition.patient_name, patient_id: bodyPosition.patient_id, registration_number: bodyPosition.registration_number, study_description: bodyPosition.study_description, body_position_image: bodyPosition.view_big_icon_name, collimator_length: bodyPosition.collimator_length, collimator_width: bodyPosition.collimator_width, sid: bodyPosition.sid, }) ); // 3. 🆕 如果在exam模式,根据曝光状态决定是否同步设备 if (currentKey === 'exam') { if (bodyPosition.dview.expose_status === 'Unexposed') { // 未曝光体位:同步设备,使能曝光 await changeBodyPosition(bodyPosition.sop_instance_uid); await setExpEnable(); const successMsg = `Body position changed successfully: ${bodyPosition.view_name}`; console.log(`[bodyPositionSelection] ${successMsg}`); if (showMessage) { message.success(successMsg); } } else { // 已曝光体位:跳过设备同步,仅更新显示 console.log( `[bodyPositionSelection] Exposed position selected, skipping device sync: ${bodyPosition.view_name}` ); if (showMessage) { message.info(`已选中已曝光体位: ${bodyPosition.view_name}`); } } } else { console.log( `[bodyPositionSelection] Current key is ${currentKey}, not executing changeBodyPosition.` ); } } catch (error) { const errorMsg = 'Failed to change body position'; console.error(`[bodyPositionSelection] ${errorMsg}:`, error); if (showMessage) { message.error(errorMsg); } throw error; } }; ``` **关键修改**: - ✅ 添加 `if (bodyPosition.dview.expose_status === 'Unexposed')` 判断 - ✅ 只有未曝光时才调用 `changeBodyPosition` 和 `setExpEnable` - ✅ 已曝光时只记录日志,可选显示提示信息 --- ### 2️⃣ 修改 `bodyPositionDetailSlice.ts` **文件**: `src/states/exam/bodyPositionDetailSlice.ts` #### Step 1: 更新 Interface ```typescript interface BodyPositionDetailState { view_name: string; view_description: string; view_icon_name: string; patient_name: string; patient_id: string; registration_number: string; study_description: string; body_position_image: string; collimator_length: string | number; collimator_width: string | number; sid: string; // 🆕 添加以下字段 expose_status: string; // 'Exposed' | 'Unexposed' sop_instance_uid: string; // 用于获取缩略图 URL } ``` #### Step 2: 更新 initialState ```typescript const initialState: BodyPositionDetailState = { view_name: '', view_description: '', view_icon_name: '', patient_name: '', patient_id: '', registration_number: '', study_description: '', body_position_image: '', collimator_length: '', collimator_width: '', sid: '', // 🆕 添加初始值 expose_status: 'Unexposed', sop_instance_uid: '', }; ``` #### Step 3: 更新 extraReducers ```typescript extraReducers: (builder) => { builder.addCase(setSelectedBodyPosition, (state, action) => { const selectedBodyPosition = action.payload; if (selectedBodyPosition) { return { ...state, view_name: selectedBodyPosition.view_name, view_description: selectedBodyPosition.view_description, view_icon_name: selectedBodyPosition.view_icon_name, patient_name: selectedBodyPosition.patient_name, patient_id: selectedBodyPosition.patient_id, registration_number: selectedBodyPosition.registration_number, study_description: selectedBodyPosition.study_description, body_position_image: selectedBodyPosition.body_position_image, collimator_length: selectedBodyPosition.collimator_length, collimator_width: selectedBodyPosition.collimator_width, sid: selectedBodyPosition.sid, // 🆕 添加字段映射 expose_status: selectedBodyPosition.dview.expose_status, sop_instance_uid: selectedBodyPosition.sop_instance_uid, }; } return state; }); }, ``` --- ### 3️⃣ 修改 `BodyPositionDetail.tsx` **文件**: `src/pages/exam/components/BodyPositionDetail.tsx` ```typescript import React, { useRef } from 'react'; import { useSelector } from 'react-redux'; import { Row, Col, Typography, Image, Flex } from 'antd'; import { getViewIconUrl, getExposedImageUrl } from '@/API/bodyPosition'; // 🆕 添加 import import CollimatorIcon from '../../../assets/imgs/Collimator_normal.png'; import SidIcon from '../../../assets/imgs/SID.png'; import { RootState } from '@/states/store'; const { Title, Text } = Typography; const BodyPositionDetail: React.FC = () => { const bodyPositionDetail = useSelector( (state: RootState) => state.bodyPositionDetail ); const wrapRef = useRef(null); // 🆕 根据曝光状态选择图像源 const imageUrl = bodyPositionDetail.expose_status === 'Exposed' ? getExposedImageUrl(bodyPositionDetail.sop_instance_uid) // 已曝光:缩略图 : getViewIconUrl(bodyPositionDetail.body_position_image); // 未曝光:示意图 return ( {/* 第一行 : 患者姓名*/}
{bodyPositionDetail.patient_name}
{/* 第二行 :患者id,登记号,study描述*/} Patient ID: {bodyPositionDetail.patient_id} Accession Number: {bodyPositionDetail.registration_number} Study Description: {bodyPositionDetail.study_description} {/* 第三行 :体位示意图或缩略图 🆕 修改此处 */}
{/* 第四行 :体位描述*/}
View Description: {bodyPositionDetail.view_description}
{/* 第五行 :设备信息*/}
Logo
Length: {bodyPositionDetail.collimator_length}
Width: {bodyPositionDetail.collimator_width}
{/* 第六行 */} Logo SID: {bodyPositionDetail.sid}
); }; export default BodyPositionDetail; ``` **关键修改**: - ✅ 添加 `getExposedImageUrl` import - ✅ 添加 `imageUrl` 计算逻辑(根据 `expose_status` 选择) - ✅ 使用 `imageUrl` 而非固定的 `getViewIconUrl` --- ## 🧪 测试方案 ### 测试环境准备 1. 确保有包含已曝光和未曝光体位的检查数据 2. 测试数据示例: - Study 1: 2个体位(1个已曝光,1个未曝光) - Study 2: 3个体位(全部已曝光) - Study 3: 2个体位(全部未曝光) --- ### 测试场景 #### 场景1: Exam 模式 - 单击未曝光体位 ✅ **前置条件**: 在 exam 模式,有未曝光体位 **测试步骤**: 1. 进入 exam 页面 2. 单击一个未曝光体位(显示示意图的) **预期结果**: - ✅ 控制台输出设备同步日志 - ✅ 设备参数更新到该体位配置 - ✅ 曝光功能使能 - ✅ BodyPositionDetail 显示体位**示意图** - ✅ 选中框高亮该体位 **验证方法**: ```javascript // Redux State 检查 bodyPositionDetail.expose_status === 'Unexposed' bodyPositionDetail.body_position_image !== '' // 控制台日志 '[bodyPositionSelection] Device synced for unexposed position' ``` --- #### 场景2: Exam 模式 - 单击已曝光体位 ⚠️ **核心场景** **前置条件**: 在 exam 模式,有已曝光体位 **测试步骤**: 1. 在 exam 页面 2. 单击一个已曝光体位(显示缩略图的) **预期结果**: - ✅ **不调用** `changeBodyPosition` - ✅ **不调用** `setExpEnable` - ✅ 控制台输出跳过设备同步日志 - ✅ BodyPositionDetail 显示**缩略图**(而非示意图) - ✅ 选中框高亮该体位 - ✅ 可选:显示提示信息"已选中已曝光体位" **验证方法**: ```javascript // Redux State 检查 bodyPositionDetail.expose_status === 'Exposed' bodyPositionDetail.sop_instance_uid !== '' // 控制台日志 '[bodyPositionSelection] Exposed position selected, skipping device sync' // 图像 URL 检查 backgroundImage.includes('/pub/thumbnail/') // 缩略图路径 ``` --- #### 场景3: Process 模式 - 单击未曝光体位 ✅ **前置条件**: 在 process 模式 **测试步骤**: 1. 进入 process 页面 2. 单击一个未曝光体位 **预期结果**: - ✅ 自动切换到 exam 模式 - ✅ 执行场景1的逻辑(同步设备) --- #### 场景4: Process 模式 - 单击已曝光体位 ✅ **前置条件**: 在 process 模式 **测试步骤**: 1. 在 process 页面 2. 单击一个已曝光体位 **预期结果**: - ✅ 保持在 process 模式 - ✅ 不同步设备 - ✅ BodyPositionDetail 显示**缩略图** --- #### 场景5: 曝光后状态更新 🔄 **前置条件**: 在 exam 模式,有未曝光体位 **测试步骤**: 1. 选中一个未曝光体位 2. 执行曝光操作 3. 曝光成功后,MQTT 消息更新体位状态为 'Exposed' 4. 再次单击该体位 **预期结果**: - ✅ 第一次单击:同步设备,显示示意图 - ✅ 曝光后:BodyPositionList 中该体位显示缩略图 - ✅ 第二次单击:不同步设备,BodyPositionDetail 显示缩略图 --- ### 测试用例总结表 | 场景 | 模式 | 体位状态 | 设备同步 | 详情显示 | 优先级 | |------|------|---------|---------|---------|--------| | 1 | exam | Unexposed | ✅ 执行 | 示意图 🖼️ | P0 | | 2 | exam | Exposed | ❌ 跳过 | **缩略图 📷** | P0 | | 3 | process | Unexposed | ✅ 执行(切换后) | 示意图 🖼️ | P1 | | 4 | process | Exposed | ❌ 跳过 | **缩略图 📷** | P1 | | 5 | exam | Unexposed → Exposed | 第一次✅ / 第二次❌ | 示意图→缩略图 | P1 | --- ## 🐛 边界情况分析 ### 1. 曝光状态字段缺失 **潜在问题**: 如果 `dview.expose_status` 字段不存在或为 `undefined` **处理方案**: ```typescript // 添加防护判断 if (bodyPosition.dview.expose_status === 'Unexposed') { // 同步设备 } ``` **说明**: `undefined !== 'Unexposed'`,所以会跳过设备同步,行为安全。 --- ### 2. SOP UID 为空 **潜在问题**: 已曝光体位但 `sop_instance_uid` 为空字符串 **影响**: - `getExposedImageUrl('')` 会返回 `/pub/thumbnail/.webp` - 导致图像加载失败 **处理方案**: ```typescript // 在 BodyPositionDetail.tsx 中添加防护 const imageUrl = (bodyPositionDetail.expose_status === 'Exposed' && bodyPositionDetail.sop_instance_uid) ? getExposedImageUrl(bodyPositionDetail.sop_instance_uid) : getViewIconUrl(bodyPositionDetail.body_position_image); ``` **风险等级**: 🟡 中 - 需要确保后端数据完整性 --- ### 3. 流程切换时的状态同步 **潜在问题**: 从 process 切换到 exam 时,状态可能不同步 **当前处理**: `manualSelectBodyPositionWithFlowSwitch` 中使用事件监听等待流程切换完成 **验证**: ✅ 已有 2秒超时保护机制 --- ### 4. 并发点击 **潜在问题**: 用户快速连续点击多个体位 **当前处理**: - 异步操作会按顺序执行 - Redux 状态更新是同步的,最后一次点击生效 **风险等级**: 🟢 低 - 实际使用场景较少 --- ## 📈 实现优势 ### 1. 用户体验改善 **之前**: - 单击已曝光体位 → 设备重新同步(不必要) - 详情区始终显示示意图(不直观) **现在**: - 单击已曝光体位 → 不同步设备(避免不必要操作) - 详情区显示实际缩略图(更直观) --- ### 2. 设备资源节省 - 减少不必要的设备通信 - 避免已曝光体位的重复配置 - 提高系统响应速度 --- ### 3. 代码清晰度 - 业务逻辑集中在 `selectBodyPositionWithFullLogic` - 状态管理遵循单一数据源原则 - 视图层根据状态自动更新 --- ## 🔍 调试建议 ### 控制台日志关键字 ```javascript // 未曝光体位被选中 '[bodyPositionSelection] Device synced for unexposed position' // 已曝光体位被选中 '[bodyPositionSelection] Exposed position selected, skipping device sync' // 流程切换 '[bodyPositionSelection] Switching flow from' '[bodyPositionSelection] Flow switch completed' ``` --- ### Redux DevTools 检查点 ```javascript // 检查体位详情状态 store.getState().bodyPositionDetail // { // expose_status: 'Exposed', // sop_instance_uid: '1.2.3.4.5...', // body_position_image: '/icons/chest_pa.png', // // ... // } // 检查选中的体位 store.getState().bodyPositionList.selectedBodyPosition // { // dview: { // expose_status: 'Exposed', // // ... // }, // sop_instance_uid: '1.2.3.4.5...', // // ... // } ``` --- ### 断点调试位置 1. **bodyPositionSelection.ts:47** - 进入 exam 模式判断 2. **bodyPositionSelection.ts:48** - 曝光状态判断分支 3. **BodyPositionDetail.tsx:20** - 图像 URL 计算 4. **bodyPositionDetailSlice.ts:40** - extraReducers 字段更新 --- ## 📚 相关文档 - [bodyPositionSelection.ts](../../src/domain/exam/bodyPositionSelection.ts) - 体位选择逻辑 - [bodyPositionDetailSlice.ts](../../src/states/exam/bodyPositionDetailSlice.ts) - 体位详情状态 - [BodyPositionDetail.tsx](../../src/pages/exam/components/BodyPositionDetail.tsx) - 详情显示组件 - [BodyPositionList.tsx](../../src/pages/exam/components/BodyPositionList.tsx) - 体位列表组件 --- ## ✅ 实施检查清单 在实施前,确保以下准备工作已完成: - [ ] 理解现有的 `manualSelectBodyPositionWithFlowSwitch` 逻辑 - [ ] 确认 `expose_status` 字段在后端数据中始终存在 - [ ] 确认 `sop_instance_uid` 在已曝光体位中始终有值 - [ ] 准备测试数据(包含已曝光和未曝光体位) - [ ] 通知测试团队新的交互行为变化 --- ## 🚀 实施后验证 完成代码修改后,需要验证: 1. **功能验证** - ✅ Exam 模式单击未曝光 → 设备同步 - ✅ Exam 模式单击已曝光 → 不同步设备,显示缩略图 - ✅ Process 模式行为保持不变 2. **性能验证** - ✅ 减少设备 API 调用次数 - ✅ 图像加载速度正常 3. **兼容性验证** - ✅ 不影响其他页面功能 - ✅ 不影响自动选中逻辑 --- ## 📞 问题反馈 如遇到问题,请记录以下信息: 1. **场景描述**: 在什么模式下,点击了什么状态的体位 2. **预期行为**: 应该发生什么 3. **实际行为**: 实际发生了什么 4. **控制台日志**: 相关的日志输出 5. **Redux 状态**: bodyPositionDetail 和 selectedBodyPosition 的值 --- **文档版本**: v1.0 **创建日期**: 2025/10/13 **作者**: Cline AI Assistant **状态**: ✅ 待实施