exam体位列表-单击已曝光体位优化.md 27 KB

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 选择图像源

关键代码:

const handleImageClick = async (bodyPosition: ExtendedBodyPosition): Promise<void> => {
  await manualSelectBodyPositionWithFlowSwitch(
    bodyPosition,
    dispatch,
    currentKey
  );
};

// 图像显示逻辑
<ImageViewer
  src={
    bodyPosition.dview.expose_status === 'Exposed'
      ? getExposedImageUrl(bodyPosition.sop_instance_uid)  // 已曝光:缩略图
      : getViewIconUrl(bodyPosition.view_icon_name)        // 未曝光:示意图
  }
  onClick={() => handleImageClick(bodyPosition)}
/>

BodyPositionDetail.tsx

文件路径: src/pages/exam/components/BodyPositionDetail.tsx

元素 类型 职责 需要修改
BodyPositionDetail React Component 显示选中体位的详细信息 ✅ 需要修改
体位图像显示区域 UI Element 第三行,显示体位大图 ✅ 需要根据曝光状态切换图像源

当前显示逻辑(只显示示意图):

backgroundImage: `url(${getViewIconUrl(bodyPositionDetail.body_position_image)})`

需要改为(根据曝光状态选择):

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 模式下无条件同步设备):

// 3. 如果在exam模式,同步设备到对应体位
if (currentKey === 'exam') {
  await changeBodyPosition(bodyPosition.sop_instance_uid);
  await setExpEnable();
  // ...
}

需要改为(仅未曝光时同步设备):

// 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,自动更新详情 ✅ 需要更新字段

需要添加的字段:

interface BodyPositionDetailState {
  // ... 现有字段
  expose_status: string;         // 🆕 'Exposed' | 'Unexposed'
  sop_instance_uid: string;      // 🆕 用于获取缩略图 URL
}

初始状态:

const initialState: BodyPositionDetailState = {
  // ... 现有字段
  expose_status: 'Unexposed',    // 🆕 默认未曝光
  sop_instance_uid: '',          // 🆕 默认空
};

Reducer 更新:

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 ✅ 已实现
// 示意图 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 模式)

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 模式)⚠️ 核心修改

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'<br/>sop_instance_uid = '...'
    
    BPD->>BPD: imageUrl = getExposedImageUrl(sop_instance_uid)
    BPD-->>User: 显示缩略图 📷

关键差异:

  1. 不调用 changeBodyPosition - 不同步设备
  2. 不调用 setExpEnable - 不使能曝光
  3. ✅ 更新 Redux 状态(包含 expose_statussop_instance_uid
  4. ✅ BodyPositionDetail 显示缩略图

场景3: 单击未曝光体位(process 模式)

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<br/>(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_statussop_instance_uid 字段
bodyPositionDetailSlice.ts 🔧 修改 Reducer extraReducers 更新字段映射逻辑
BodyPositionDetail.tsx 🔧 修改显示 第三行图像区域 根据 expose_status 动态选择图像源

1️⃣ 修改 bodyPositionSelection.ts

文件: src/domain/exam/bodyPositionSelection.ts

export const selectBodyPositionWithFullLogic = async (
  bodyPosition: ExtendedBodyPosition,
  dispatch: AppDispatch,
  currentKey: string,
  showMessage = false
): Promise<void> => {
  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') 判断
  • ✅ 只有未曝光时才调用 changeBodyPositionsetExpEnable
  • ✅ 已曝光时只记录日志,可选显示提示信息

2️⃣ 修改 bodyPositionDetailSlice.ts

文件: src/states/exam/bodyPositionDetailSlice.ts

Step 1: 更新 Interface

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

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

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

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<HTMLDivElement>(null);

  // 🆕 根据曝光状态选择图像源
  const imageUrl = bodyPositionDetail.expose_status === 'Exposed'
    ? getExposedImageUrl(bodyPositionDetail.sop_instance_uid)  // 已曝光:缩略图
    : getViewIconUrl(bodyPositionDetail.body_position_image);  // 未曝光:示意图

  return (
    <Flex vertical className="h-full">
      {/* 第一行 : 患者姓名*/}
      <div className="text-center">
        <Title level={4}>{bodyPositionDetail.patient_name}</Title>
      </div>
      
      {/* 第二行 :患者id,登记号,study描述*/}
      <Row>
        <Col span={8}>
          <Text>Patient ID: {bodyPositionDetail.patient_id}</Text>
        </Col>
        <Col span={8}>
          <Text>
            Accession Number: {bodyPositionDetail.registration_number}
          </Text>
        </Col>
        <Col span={8}>
          <Text>Study Description: {bodyPositionDetail.study_description}</Text>
        </Col>
      </Row>

      {/* 第三行 :体位示意图或缩略图 🆕 修改此处 */}
      <Flex
        style={{ minHeight: 0 }}
        className="flex-grow"
        ref={wrapRef}
      >
        <div
          style={{
            width: '100%',
            backgroundImage: `url(${imageUrl})`,  // 🆕 使用动态 imageUrl
            backgroundSize: 'contain',
            backgroundPosition: 'center',
            backgroundRepeat: 'no-repeat',
          }}
        ></div>
      </Flex>

      {/* 第四行 :体位描述*/}
      <div className="text-center">
        <Text>View Description: {bodyPositionDetail.view_description}</Text>
      </div>
      
      {/* 第五行 :设备信息*/}
      <div className="flex flex-row items-center justify-center">
        <div className="text-center">
          <Image src={CollimatorIcon} alt="Logo" height={'100%'} />
        </div>
        <div className="text-center">
          <Text>Length: {bodyPositionDetail.collimator_length}</Text>
        </div>
        <div className="text-center ml-2">
          <Text>Width: {bodyPositionDetail.collimator_width}</Text>
        </div>
      </div>
      
      {/* 第六行 */}
      <Flex justify="center" align="center">
        <Image src={SidIcon} alt="Logo" className="mr-2" height={'100%'} />
        <Text>SID: {bodyPositionDetail.sid}</Text>
      </Flex>
    </Flex>
  );
};

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 显示体位示意图
  • ✅ 选中框高亮该体位

验证方法:

// 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 显示缩略图(而非示意图)
  • ✅ 选中框高亮该体位
  • ✅ 可选:显示提示信息"已选中已曝光体位"

验证方法:

// 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

处理方案:

// 添加防护判断
if (bodyPosition.dview.expose_status === 'Unexposed') {
  // 同步设备
}

说明: undefined !== 'Unexposed',所以会跳过设备同步,行为安全。


2. SOP UID 为空

潜在问题: 已曝光体位但 sop_instance_uid 为空字符串

影响:

  • getExposedImageUrl('') 会返回 /pub/thumbnail/.webp
  • 导致图像加载失败

处理方案:

// 在 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
  • 状态管理遵循单一数据源原则
  • 视图层根据状态自动更新

🔍 调试建议

控制台日志关键字

// 未曝光体位被选中
'[bodyPositionSelection] Device synced for unexposed position'

// 已曝光体位被选中
'[bodyPositionSelection] Exposed position selected, skipping device sync'

// 流程切换
'[bodyPositionSelection] Switching flow from'
'[bodyPositionSelection] Flow switch completed'

Redux DevTools 检查点

// 检查体位详情状态
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 字段更新

📚 相关文档


✅ 实施检查清单

在实施前,确保以下准备工作已完成:

  • 理解现有的 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
状态: ✅ 待实施