标记功能实现.md 12 KB

标记功能实现方案

🎯 功能概述

实现图像标记管理功能,支持预定义标记和用户自定义标记的添加、显示和管理。

📋 需求分析

1. 触发方式

  • 用户点击 FunctionArea.tsx 中的 "Add Mark" 按钮
  • 切换到二级面板 MarkPanel

2. 面板功能

  • 预定义标记按钮: 保留图片中的所有标记(拉姿、仰卧、俯卧、斜倚、过屈、过伸、内旋、外旋、吸气、呼气、负重、无负重)
  • 时间戳按钮: 格式为 YYYY-MM-DD HH:mm:ss
  • 自定义标记: 用户输入文本后添加到下拉框,选择后添加到图像

3. 交互流程

  1. 预定义标记:用户直接点击预定义标记按钮 → 标记立即显示在图像上
  2. 自定义标记
    • 用户输入标记文本 → 点击"添加"按钮 → 文本进入下拉框
    • 用户从下拉框选择标记 → 点击"添加"按钮 → 标记显示在图像上
  3. 时间戳:用户点击时间戳按钮 → 当前时间立即显示在图像上
  4. 点击"删除标记"按钮 → 删除选中的标记

🏗️ 架构设计

类图

classDiagram
    class FunctionArea {
        +AddMark 按钮
        +handleButtonClick()
    }

    class OperationPanel {
        -currentPanel: string
        +renderPanel()
        +switchToMarkPanel()
    }

    class MarkPanel {
        -inputText: string
        -selectedMark: string
        -customMarks: string[]
        +renderInputArea()
        +renderSelectArea()
        +renderPredefinedButtons()
        +renderDeleteButton()
    }

    class ViewerContainer {
        -selectedViewportIds: string[]
        +handleMarkAction()
        +addCustomMark()
    }

    class StackViewer {
        +addLMark()
        +addRLabel()
        +addCustomMark()
        +deleteSelectedMark()
    }

    class MarkPanelSlice {
        -customMarks: string[]
        -selectedMark: string
        +addCustomMarkToList()
        +setSelectedMark()
        +addMarkToImage()
        +deleteSelectedMark()
    }

    FunctionArea --> OperationPanel : 触发切换
    OperationPanel --> MarkPanel : 渲染面板
    MarkPanel --> ViewerContainer : 触发标记动作
    ViewerContainer --> StackViewer : 调用底层函数
    MarkPanelSlice --> MarkPanel : 状态管理

流程图

flowchart TD
    A[用户点击 AddMark] --> B{是否已显示 MarkPanel?}
    B -->|否| C[切换到 MarkPanel]
    B -->|是| D[保持当前面板]
    C --> E[显示输入框和下拉框]
    D --> E
    E --> F[用户输入标记文本]
    F --> G[点击添加按钮]
    G --> H[文本进入下拉框]
    H --> I[用户从下拉框选择标记]
    I --> J{选择预定义按钮或时间戳?}
    J -->|预定义| K[添加预定义标记到图像]
    J -->|时间戳| L[添加当前时间到图像]
    K --> M[标记显示在图像上]
    L --> M
    M --> N{需要删除标记?}
    N -->|是| O[点击删除标记按钮]
    N -->|否| P[完成]
    O --> Q[删除选中的标记]
    Q --> P

🎨 UI 布局设计

┌─────────────────────────────────────┐
│ ← 标记管理                          │
├─────────────────────────────────────┤
│ 自定义标记输入:                     │
│ ┌─────────────────────────────────┐ │
│ │ [输入框]              [添加]    │ │
│ └─────────────────────────────────┘ │
│                                     │
│ 选择要添加的标记:                   │
│ ┌─────────────────────────────────┐ │
│ │ [下拉框 ▼]                      │ │
│ └─────────────────────────────────┘ │
│                                     │
│ 预定义标记按钮:                     │
│ [拉姿] [仰卧] [俯卧] [斜倚]        │
│ [过屈] [过伸] [内旋] [外旋]        │
│ [吸气] [呼气] [负重] [无负重]      │
│           [时间戳]                  │
│                                     │
│           ┌─────────┐               │
│           │ 删除标记 │               │
│           └─────────┘               │
└─────────────────────────────────────┘

💾 数据结构设计

1. 状态管理

interface MarkPanelState {
  customMarks: string[];        // 用户添加的自定义标记列表
  selectedMark: string;         // 当前选中的标记文本
  inputText: string;           // 输入框中的文本
  selectedViewportIds: string[]; // 选中的视口ID
}

2. 预定义标记配置

const PREDEFINED_MARKS = [
  '拉姿', '仰卧', '俯卧', '斜倚',
  '过屈', '过伸', '内旋', '外旋',
  '吸气', '呼气', '负重', '无负重'
];

3. 时间戳格式

const TIMESTAMP_FORMAT = 'YYYY-MM-DD HH:mm:ss';

🔄 交互序列图

sequenceDiagram
    participant 用户
    participant FunctionArea
    participant PanelSwitch
    participant OperationPanel
    participant MarkPanel
    participant Redux
    participant ViewerContainer
    participant StackViewer

    用户->>FunctionArea: 点击 AddMark 按钮
    FunctionArea->>PanelSwitch: dispatch(switchToMarkPanel())
    PanelSwitch->>OperationPanel: currentPanel = 'MarkPanel'
    OperationPanel->>MarkPanel: 渲染 MarkPanel

    用户->>MarkPanel: 输入标记文本
    用户->>MarkPanel: 点击"添加"按钮
    MarkPanel->>Redux: dispatch(addCustomMarkToList(text))
    Redux->>MarkPanel: 更新下拉框选项列表

    用户->>MarkPanel: 从下拉框选择标记
    MarkPanel->>Redux: dispatch(setSelectedMark(text))

    用户->>MarkPanel: 点击预定义标记按钮
    MarkPanel->>Redux: dispatch(addMarkToImage(selectedMark))
    Redux->>ViewerContainer: dispatch(addCustomMark(selectedMark))
    ViewerContainer->>StackViewer: 调用addCustomMark函数
    StackViewer->>StackViewer: 使用LabelTool添加标记到图像
    StackViewer->>用户: 显示标记在图像上

    用户->>MarkPanel: 点击删除标记按钮
    MarkPanel->>Redux: dispatch(deleteSelectedMark())
    Redux->>ViewerContainer: dispatch(deleteSelectedMark())
    ViewerContainer->>StackViewer: 调用deleteSelectedMark函数
    StackViewer->>用户: 从图像上删除标记

🧪 测试方案

功能测试场景

  1. 面板切换测试

    • 点击 AddMark 按钮,验证 MarkPanel 正确定位和显示
    • 测试从 MarkPanel 返回 OperationPanel 的功能
  2. 自定义标记测试

    • 输入文本并点击添加,验证下拉框正确更新
    • 从下拉框选择标记,验证能正确添加到图像
  3. 预定义标记测试

    • 点击各个预定义标记按钮,验证在图像上正确显示
    • 测试标记位置是否符合预期
  4. 时间戳测试

    • 点击时间戳按钮,验证时间格式正确(YYYY-MM-DD HH:mm:ss)
    • 验证时间戳能正确显示在图像上
  5. 删除功能测试

    • 测试删除选中标记功能
    • 验证多视口标记同步删除
  6. 边界情况测试

    • 空文本输入处理
    • 超长文本截断处理
    • 特殊字符处理

🐛 潜在问题分析

1. 性能问题

  • 问题: 大量自定义标记可能导致下拉框性能下降
  • 解决方案: 限制自定义标记数量,超过限制时删除最旧的

2. 状态同步问题

  • 问题: 多视口间标记状态不同步
  • 解决方案: 统一状态管理,确保所有视口标记一致

3. 用户体验问题

  • 问题: 标记文本过长导致界面拥挤
  • 解决方案: 实现文本自动换行和省略号显示

4. 数据持久化问题

  • 问题: 页面刷新后自定义标记列表丢失
  • 解决方案: 考虑添加本地存储功能

📋 详细实现步骤

1. 状态管理层 ✅

  • 创建 src/states/view/markPanelSlice.ts - 管理标记面板状态
  • 更新 src/states/panelSwitchSliceForView.ts - 添加 MarkPanel 切换功能

2. MarkPanel 组件 ✅

  • 创建 src/pages/view/components/MarkPanel.tsx - 标记面板组件
  • 实现 FunctionButton 风格的预定义标记按钮
  • 实现自定义标记输入和下拉选择功能
  • 实现时间戳按钮功能
  • 实现删除标记按钮

3. 底层标记功能扩展 ✅

  • 扩展 stack.image.viewer.tsx - 支持自定义文本标记
  • 实现时间戳格式化功能
  • 实现标记选择和删除逻辑

4. 集成到主流程 ✅

  • 更新 OperationPanel.tsx - 添加 MarkPanel 路由
  • 更新 FunctionArea.tsx - 连接 AddMark 按钮到面板切换

5. 测试和优化 ✅

  • 编译测试 - H5开发服务器成功启动
  • 功能集成测试 - 所有组件正确连接
  • Bug 修复 - 解决标记无法显示的问题(2025-10-22)
  • 单元测试各个组件
  • 性能优化和用户体验改进
  • 端到端测试完整流程

🎯 关键实现要点

  1. 按钮样式一致性: 所有按钮都使用 FunctionButton 的样式和布局
  2. 时间戳格式: YYYY-MM-DD HH:mm:ss
  3. 下拉框管理: 动态管理用户添加的自定义标记
  4. 状态同步: 确保标记状态在组件间正确传递
  5. 错误处理: 完善的边界情况和异常处理

这个实现方案确保了功能的完整性和代码的可维护性,同时提供了良好的用户体验和扩展性。

🔧 Bug 修复记录

标记无法显示问题修复(2025-10-22)

问题描述

点击预定义标记按钮后,标记无法在图像上显示。

根本原因

发现了两个关键问题:

  1. Action 分发和匹配不一致 ⚠️

    • MarkPanel.tsx 发送的 action 是 'AddPredefinedMark:拉姿' (带参数)
    • ViewerContainer.tsx 的 switch case 匹配的是 'AddPredefinedMark' (不带参数)
    • 导致无法匹配,标记添加逻辑从未执行
  2. StackViewer 缺少 id 属性 🔴 (关键问题)

    • addCustomMark 函数使用 document.getElementById(viewportId) 获取元素
    • 但 StackViewer 的根 div 没有设置 id 属性
    • 导致获取不到元素,无法计算标记位置

修复方案

修改的文件:

  1. src/pages/view/components/ViewerContainer.tsx

    // 在 switch 前添加预处理逻辑
    if (action.startsWith('AddPredefinedMark:')) {
     const markText = action.substring('AddPredefinedMark:'.length);
     selectedViewportIds.forEach((viewportId) => {
       addCustomMark(viewportId, markText);
     });
     dispatch(clearAction());
     return;
    }
    
    1. src/pages/view/components/MarkPanel.tsx typescript // 实现删除标记功能 const handleDeleteMark = () => { dispatch({ type: 'functionArea/setAction', payload: 'Delete Selected Mark' }); };
  2. src/pages/view/components/viewers/stack.image.viewer.tsx

    // 添加 id 属性
    return (
     <div
       id={viewportId}  // ✅ 添加此行
       ref={elementRef}
       ...
     />
    );
    

    验证结果

    修复后,以下功能正常工作:

    • ✅ 预定义标记 - 点击按钮后标记显示在图像中心
    • ✅ 时间戳标记 - 显示当前时间(YYYY-MM-DD HH:mm:ss)
    • ✅ 自定义标记 - 输入文本、选择并添加到图像
    • ✅ 删除标记 - 删除图像上的 L 和 R 标记

    详细的修复文档:标记功能Bug修复总结