exam体位列表-双击已曝光体位进入处理.md 16 KB

Exam 体位列表 - 双击已曝光体位进入处理功能实现文档

📋 功能概述

功能名称: Exam 页面体位列表 - 双击已曝光体位进入处理

实现状态: 🔄 待实施

核心需求: 区分单击和双击已曝光体位的行为,只有双击才切换到 process 模式

🎯 需求说明

问题背景

当前行为(不符合预期):

  • 在 exam 模式下,单击已曝光体位 → 自动切换到 process 模式

期望行为

  • 在 exam 模式下,单击已曝光体位 → 只选中并显示缩略图,不切换流程
  • 在 exam 模式下,双击已曝光体位 → 选中并切换到 process 模式

📊 完整交互行为表

模式 体位状态 单击行为 双击行为
exam Unexposed 选中 + 同步设备 + 显示示意图 🖼️ 同左
exam Exposed 只选中 + 显示缩略图(不切换) 📷 选中 + 切换到 process ➡️
process Unexposed 选中 + 切换到 exam + 同步设备 同左
process Exposed 选中 + 保持 process 同左

🔍 问题根源分析

当前实现问题

src/domain/exam/bodyPositionSelection.ts 中的 manualSelectBodyPositionWithFlowSwitch 函数:

// 第182-187行 - 问题代码
if (currentKey === 'exam' && isExposed) {
  targetFlow = 'process';
  needSwitch = true;  // ❌ 无论单击还是双击都会切换
  console.log(`Detected exposed position in exam mode, will switch to process`);
}

问题:函数没有区分是单击还是双击触发,导致单击也会切换流程。


💡 解决方案

核心思路

  1. 添加参数控制流程切换:在 manualSelectBodyPositionWithFlowSwitch 函数中添加 allowFlowSwitch 参数
  2. 分离单击和双击处理:在 BodyPositionList.tsx 中分别处理单击和双击事件
  3. 支持双击事件:在 ImageViewer.tsx 组件中添加 onDoubleClick 属性支持

🛠️ 实现方案

修改清单

文件 修改类型 具体内容
bodyPositionSelection.ts 🔧 修改函数签名 添加 allowFlowSwitch 参数
BodyPositionList.tsx ➕ 添加双击处理 添加 handleImageDoubleClick 函数
ImageViewer.tsx ➕ 添加属性 添加 onDoubleClick 属性支持

1️⃣ 修改 bodyPositionSelection.ts

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

修改函数签名

/**
 * 带流程切换的体位选择(用户点击体位时调用,根据曝光状态自动切换流程)
 * @param bodyPosition 要选中的体位
 * @param dispatch Redux dispatch
 * @param currentKey 当前业务流程 key
 * @param allowFlowSwitch 是否允许自动切换流程(默认 true)
 */
export const manualSelectBodyPositionWithFlowSwitch = async (
  bodyPosition: ExtendedBodyPosition,
  dispatch: AppDispatch,
  currentKey: string,
  allowFlowSwitch: boolean = true  // 🆕 新增参数,默认 true 保持向后兼容
): Promise<void> => {
  const isExposed = bodyPosition.dview.expose_status === 'Exposed';
  const isUnexposed = bodyPosition.dview.expose_status === 'Unexposed';

  let targetFlow = currentKey;
  let needSwitch = false;

  // 🆕 只有在允许流程切换时才判断是否需要切换
  if (allowFlowSwitch) {
    // 判断是否需要切换流程
    if (currentKey === 'exam' && isExposed) {
      targetFlow = 'process';
      needSwitch = true;
      console.log(
        `[bodyPositionSelection] Detected exposed position in exam mode, will switch to process`
      );
    } else if (currentKey === 'process' && isUnexposed) {
      targetFlow = 'exam';
      needSwitch = true;
      console.log(
        `[bodyPositionSelection] Detected unexposed position in process mode, will switch to exam`
      );
    }
  } else {
    console.log(
      `[bodyPositionSelection] Flow switch disabled, staying in ${currentKey} mode`
    );
  }

  // 后续流程切换和体位选中逻辑保持不变
  if (needSwitch) {
    // ... 流程切换代码
  }

  // 使用正确的流程 key 选中体位
  await selectBodyPositionWithFullLogic(
    bodyPosition,
    dispatch,
    targetFlow,
    true
  );
};

关键修改

  • ✅ 添加 allowFlowSwitch 参数(默认 true 保持向后兼容)
  • ✅ 只有当 allowFlowSwitch === true 时才执行流程切换判断
  • ✅ 添加日志记录是否禁用流程切换

2️⃣ 修改 BodyPositionList.tsx

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

添加双击处理函数

// 🆕 新增:处理双击事件
const handleImageDoubleClick = async (
  bodyPosition: ExtendedBodyPosition
): Promise<void> => {
  console.log(`[BodyPositionList] Double-click on: ${bodyPosition.view_name}`);
  
  // 双击时,允许自动流程切换
  await manualSelectBodyPositionWithFlowSwitch(
    bodyPosition,
    dispatch,
    currentKey,
    true  // allowFlowSwitch = true(允许流程切换)
  );
};

// 🔧 修改:处理单击事件
const handleImageClick = async (
  bodyPosition: ExtendedBodyPosition
): Promise<void> => {
  console.log(`[BodyPositionList] Single-click on: ${bodyPosition.view_name}`);
  
  // 单击时,禁止自动流程切换
  await manualSelectBodyPositionWithFlowSwitch(
    bodyPosition,
    dispatch,
    currentKey,
    false  // allowFlowSwitch = false(禁止流程切换)
  );
};

修改 ImageViewer 调用

<ImageViewer
  src={
    bodyPosition.dview.expose_status === 'Exposed'
      ? getExposedImageUrl(bodyPosition.sop_instance_uid)
      : getViewIconUrl(bodyPosition.view_icon_name)
  }
  className={`image-viewer-item hover:border-[var(--color-primary)] hover:border-4
    ${
      bodyPosition.sop_instance_uid ===
      selectedBodyPosition?.sop_instance_uid
        ? 'border-4 border-[var(--color-primary)] '
        : ''
    }`}
  onClick={() => handleImageClick(bodyPosition)}
  onDoubleClick={() => handleImageDoubleClick(bodyPosition)}  // 🆕 添加双击处理
/>

3️⃣ 修改 ImageViewer.tsx

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

添加 onDoubleClick 属性支持

interface ImageViewerProps {
  src: string;
  alt?: string;
  className?: string;
  onClick?: () => void;
  onDoubleClick?: () => void;  // 🆕 添加双击属性
}

const ImageViewer: React.FC<ImageViewerProps> = ({
  src,
  alt = 'Body Position',
  className = '',
  onClick,
  onDoubleClick,  // 🆕
}) => {
  return (
    <div className={`image-viewer ${className}`}>
      <Image
        src={src}
        alt={alt}
        onClick={onClick}
        onDoubleClick={onDoubleClick}  // 🆕 传递双击事件
        preview={false}
        fallback={defaultPosition}
      />
    </div>
  );
};

关键修改

  • ✅ Interface 中添加 onDoubleClick?: () => void
  • ✅ 组件接收 onDoubleClick 属性
  • ✅ 将 onDoubleClick 传递给 Image 组件

🔄 数据流分析

场景1: Exam 模式 - 单击已曝光体位 ⚠️ 核心场景

sequenceDiagram
    actor User as 👤 用户
    participant IV as ImageViewer
    participant BPL as BodyPositionList
    participant BS as bodyPositionSelection
    participant Redux as Redux Store

    User->>IV: 单击已曝光体位
    IV->>BPL: onClick() 触发
    BPL->>BS: manualSelectBodyPositionWithFlowSwitch<br/>(allowFlowSwitch=false)
    
    BS->>BS: 检查 allowFlowSwitch === false ✅
    Note over BS: 跳过流程切换判断
    BS->>BS: targetFlow = 'exam' (保持当前)
    
    BS->>Redux: setSelectedBodyPosition(bodyPosition)
    Redux-->>User: 更新选中状态 + 显示缩略图 📷
    
    Note over User: ❌ 不切换到 process 模式

场景2: Exam 模式 - 双击已曝光体位 ⚠️ 核心场景

sequenceDiagram
    actor User as 👤 用户
    participant IV as ImageViewer
    participant BPL as BodyPositionList
    participant BS as bodyPositionSelection
    participant Redux as Redux Store

    User->>IV: 双击已曝光体位
    IV->>BPL: onDoubleClick() 触发
    BPL->>BS: manualSelectBodyPositionWithFlowSwitch<br/>(allowFlowSwitch=true)
    
    BS->>BS: 检查 allowFlowSwitch === true ✅
    BS->>BS: 检查 currentKey='exam' && isExposed ✅
    BS->>BS: targetFlow = 'process' (需要切换)
    
    BS->>Redux: setBusinessFlow('process')
    Note over Redux: exam → process 流程切换
    
    BS->>Redux: setSelectedBodyPosition(bodyPosition)
    Redux-->>User: 切换到 process + 显示缩略图 📷
    
    Note over User: ✅ 成功切换到 process 模式

场景3: Exam 模式 - 单击未曝光体位(保持不变)

sequenceDiagram
    actor User as 👤 用户
    participant IV as ImageViewer
    participant BPL as BodyPositionList
    participant BS as bodyPositionSelection
    participant API as Device API

    User->>IV: 单击未曝光体位
    IV->>BPL: onClick() 触发
    BPL->>BS: manualSelectBodyPositionWithFlowSwitch<br/>(allowFlowSwitch=false)
    
    BS->>BS: 检查 allowFlowSwitch === false
    BS->>BS: 但 isUnexposed,不需要切换
    BS->>BS: targetFlow = 'exam'
    
    BS->>API: changeBodyPosition() + setExpEnable()
    API-->>BS: 设备同步成功
    
    Note over User: ✅ 同步设备 + 显示示意图 🖼️

🧪 测试方案

测试环境准备

需要准备包含以下体位的测试数据:

  • 至少 2 个未曝光体位
  • 至少 2 个已曝光体位

测试场景

场景1: Exam 模式 - 单击已曝光体位 ⚠️ 核心测试

前置条件:在 exam 模式

测试步骤

  1. 单击一个已曝光体位(显示缩略图的)

预期结果

  • ✅ 体位被选中(高亮显示)
  • ✅ BodyPositionDetail 显示该体位的缩略图
  • 保持在 exam 模式(不切换到 process)
  • ✅ 控制台输出:Flow switch disabled, staying in exam mode

验证方法

// Redux State 检查
currentKey === 'exam'  // 没有切换流程
bodyPositionDetail.expose_status === 'Exposed'
bodyPositionDetail.sop_instance_uid !== ''

// 控制台日志
'[BodyPositionList] Single-click on: ...'
'[bodyPositionSelection] Flow switch disabled, staying in exam mode'

场景2: Exam 模式 - 双击已曝光体位 ⚠️ 核心测试

前置条件:在 exam 模式

测试步骤

  1. 双击一个已曝光体位(显示缩略图的)

预期结果

  • ✅ 体位被选中
  • 自动切换到 process 模式
  • ✅ BodyPositionDetail 显示该体位的缩略图
  • ✅ 控制台输出:Detected exposed position in exam mode, will switch to process

验证方法

// Redux State 检查
currentKey === 'process'  // 已切换流程
bodyPositionDetail.expose_status === 'Exposed'

// 控制台日志
'[BodyPositionList] Double-click on: ...'
'[bodyPositionSelection] Detected exposed position in exam mode, will switch to process'

场景3: Exam 模式 - 单击未曝光体位(保持原逻辑)

前置条件:在 exam 模式

测试步骤

  1. 单击一个未曝光体位

预期结果

  • ✅ 体位被选中
  • ✅ 设备同步到该体位
  • ✅ 曝光使能
  • ✅ 保持在 exam 模式
  • ✅ 显示示意图

场景4: Exam 模式 - 双击未曝光体位(同单击)

前置条件:在 exam 模式

测试步骤

  1. 双击一个未曝光体位

预期结果

  • ✅ 与单击行为相同(因为未曝光体位不需要流程切换)

场景5: Process 模式 - 单击/双击行为(保持原逻辑)

前置条件:在 process 模式

测试步骤

  1. 单击未曝光体位 → 应切换到 exam
  2. 单击已曝光体位 → 应保持 process
  3. 双击行为应与单击相同

说明:process 模式下的行为不受本次修改影响,保持原有逻辑。


测试用例总结表

模式 体位状态 操作 流程切换 设备同步 详情显示 优先级
exam Unexposed 单击 ❌ 不切换 ✅ 同步 示意图 🖼️ P0
exam Unexposed 双击 ❌ 不切换 ✅ 同步 示意图 🖼️ P1
exam Exposed 单击 ❌ 不切换 ❌ 跳过 缩略图 📷 P0
exam Exposed 双击 ✅ 切换到 process ❌ 跳过 缩略图 📷 P0
process Unexposed 单击/双击 ✅ 切换到 exam ✅ 同步 示意图 🖼️ P1
process Exposed 单击/双击 ❌ 不切换 ❌ 跳过 缩略图 📷 P1

🐛 边界情况分析

1. 双击误触发两次单击

问题描述:双击时可能先触发单击事件,再触发双击事件

解决方案

  • React 的 onDoubleClick 事件会自动处理这种情况
  • 浏览器默认行为会在双击时抑制单击事件的完整执行
  • 实际测试中验证是否需要添加防抖逻辑

风险等级:🟡 中


2. 快速连续单击(非双击)

问题描述:用户快速连续单击两次,但不是双击(间隔较长)

当前处理:每次单击都会独立处理,不会误判为双击

风险等级:🟢 低


3. 流程切换中的状态同步

问题描述:双击触发流程切换时,状态可能不一致

当前处理:已有事件监听机制(BUSINESS_FLOW_CHANGED)确保流程切换完成

风险等级:🟢 低(已有保护)


📈 实现优势

1. 用户体验改善

之前

  • 单击已曝光体位 → 意外切换到 process(用户困惑)

现在

  • 单击已曝光体位 → 只查看,不切换(符合预期)
  • 双击已曝光体位 → 明确意图进入处理(操作清晰)

2. 操作直觉性

  • 单击 = 查看/选择(低风险操作)
  • 双击 = 进入/打开(高风险操作)

符合用户对单击/双击的常规认知。


3. 向后兼容

  • allowFlowSwitch 参数默认 true
  • 现有调用不需要修改
  • 渐进式升级

🔍 调试建议

控制台日志关键字

// 单击已曝光体位
'[BodyPositionList] Single-click on: ...'
'[bodyPositionSelection] Flow switch disabled, staying in exam mode'

// 双击已曝光体位
'[BodyPositionList] Double-click on: ...'
'[bodyPositionSelection] Detected exposed position in exam mode, will switch to process'

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

断点调试位置

  1. BodyPositionList.tsx:35 - handleImageClick 函数
  2. BodyPositionList.tsx:45 - handleImageDoubleClick 函数
  3. bodyPositionSelection.ts:182 - allowFlowSwitch 判断分支
  4. bodyPositionSelection.ts:187 - currentKey === 'exam' && isExposed 判断

📚 相关文档


✅ 实施检查清单

  • 理解单击/双击的区别和实现原理
  • 确认 allowFlowSwitch 参数的默认值和向后兼容性
  • 准备包含已曝光和未曝光体位的测试数据
  • 通知测试团队新的交互行为(单击不切换,双击切换)
  • 验证双击不会误触发两次单击事件

🚀 实施后验证

完成代码修改后,需要验证:

  1. 功能验证

    • ✅ Exam 模式单击已曝光 → 不切换流程
    • ✅ Exam 模式双击已曝光 → 切换到 process
    • ✅ 其他场景行为保持不变
  2. 交互验证

    • ✅ 单击响应迅速,无延迟
    • ✅ 双击不会误触发两次单击
    • ✅ 操作反馈清晰
  3. 兼容性验证

    • ✅ 不影响其他页面功能
    • ✅ 不影响自动选中逻辑
    • ✅ 向后兼容现有调用

文档版本: v1.0
创建日期: 2025/10/13
作者: Cline AI Assistant
状态: ✅ 待实施