|
|
@@ -0,0 +1,724 @@
|
|
|
+# 反色对比功能实现方案 (Modal版本)
|
|
|
+
|
|
|
+## 📋 需求整理
|
|
|
+
|
|
|
+### 功能描述
|
|
|
+在图像处理页面中,用户可以点击"反色对比"按钮,系统会打开一个**Modal弹窗**,在弹窗内部显示1x2布局,左侧显示原始图像,右侧显示反色效果图,方便用户对比查看同一张图像的不同呈现效果。
|
|
|
+
|
|
|
+### 详细需求
|
|
|
+1. **触发方式**:点击 [`FunctionArea.tsx:156-160`](src/pages/view/components/FunctionArea.tsx:156-160) 中的"反色对比"按钮
|
|
|
+2. **显示方式**:打开一个**全屏或大尺寸Modal**弹窗
|
|
|
+3. **Modal内容**:
|
|
|
+ - 内部使用 **1x2 横向布局**(左右并排)
|
|
|
+ - 左侧:显示原始图像(当前选中的体位)
|
|
|
+ - 右侧:显示同一张图像的反色版本(应用 `invert` 属性)
|
|
|
+4. **数据源**:使用同一个图像URL,通过 Cornerstone 的 `viewport.setProperties({ invert: true })` 实现反色
|
|
|
+5. **操作范围**:针对当前选中的单张图像(单体位)
|
|
|
+6. **退出方式**:点击Modal的关闭按钮或取消按钮关闭弹窗
|
|
|
+7. **页面主体**:保持原有布局不变,不影响主页面状态
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🎨 UI设计布局
|
|
|
+
|
|
|
+### 完整界面布局描述(适用于模型输入)
|
|
|
+
|
|
|
+整个界面呈现为一个医学影像查看系统的布局。
|
|
|
+主要区域为5列分屏结构:左侧垂直缩略图栏 + 中间左主影像 + 中间右主影像 + 底部工具栏 + 顶部标题栏。
|
|
|
+
|
|
|
+### 左侧垂直缩略图栏
|
|
|
+- 界面最左侧有一个垂直排列的缩略图列表,用于切换查看影像。
|
|
|
+- 共显示五张缩略图,分别为相似体位的不同影像。
|
|
|
+- 每个缩略图左上角有一个选择复选标记,当选中时显示对勾。
|
|
|
+- 当前被选中的影像对应主视区左右两屏。
|
|
|
+
|
|
|
+### 中间左主影像区
|
|
|
+- 显示一张动物头颈及胸前肢的侧位 X 光片,背景为深灰色。
|
|
|
+- 影像区域靠中间偏左,头部朝向右侧。
|
|
|
+- 能看到头骨、颈椎、部分胸腔前段及前肢骨骼。
|
|
|
+- 影像左下与右下有常见的影像参数文字(如 kV、mA、曝光量等)。
|
|
|
+- 左上角有影像编号、ID、采集日期和性别等文字信息。
|
|
|
+
|
|
|
+### 中间右主影像区
|
|
|
+- 同一动物的同角度 X 光影像,但整体色调较亮,背景为浅灰或接近白色。
|
|
|
+- 头部同样朝向右侧,姿势与左图相同。
|
|
|
+- 显示内容同样包括头部骨骼、颈椎和部分前肢。
|
|
|
+- 右下方也有影像拍摄参数文字信息。
|
|
|
+- 右上角同样显示影像编号、ID 和日期等。
|
|
|
+
|
|
|
+### 底部工具栏
|
|
|
+- 以深色背景呈现,包含多个图标按钮。
|
|
|
+- 常见按钮包括:旋转、翻转、放大、缩放适配、图像重置等典型影像查看工具。
|
|
|
+
|
|
|
+### 顶部标题栏
|
|
|
+- 上方中央显示影像编号,如 "ER_202512100910071"。
|
|
|
+- 右侧有关闭按钮(X 图标)。
|
|
|
+
|
|
|
+### 总体结构总结
|
|
|
+* **界面 = 左侧缩略图栏 + 中间左主影像 + 中间右主影像 + 底部工具栏 + 顶部标题栏**
|
|
|
+* 中间左右主影像皆为动物头颈部的侧位 X 光,但背景亮度不同用于反色对比效果。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🎯 核心参与者
|
|
|
+
|
|
|
+### 组件层 (Component Layer)
|
|
|
+
|
|
|
+#### 主要组件
|
|
|
+1. **[`FunctionButton`](src/pages/view/components/FunctionArea.tsx:12-88)**
|
|
|
+ - 职责:渲染"反色对比"按钮,触发 action
|
|
|
+ - 修改:已存在,无需修改
|
|
|
+
|
|
|
+2. **`InvertContrastModal`** (新建)
|
|
|
+ - 职责:反色对比Modal组件,包含双viewport布局
|
|
|
+ - 路径:`src/pages/view/components/InvertContrastModal.tsx`
|
|
|
+ - 内容:
|
|
|
+ - Modal外壳(使用Ant Design Modal)
|
|
|
+ - 内部1x2布局
|
|
|
+ - 左右两个 StackViewer
|
|
|
+
|
|
|
+3. **[`ViewerContainer`](src/pages/view/components/ViewerContainer.tsx:205-782)**
|
|
|
+ - 职责:处理"Invert Contrast" action,打开Modal
|
|
|
+ - 修改:在 [`useEffect`](src/pages/view/components/ViewerContainer.tsx:314-555) 中添加 case 处理
|
|
|
+
|
|
|
+4. **[`StackViewer`](src/pages/view/components/viewers/stack.image.viewer.tsx:1494-1888)**
|
|
|
+ - 职责:渲染单个 viewport
|
|
|
+ - 修改:可能需要接收 `applyInvert` prop
|
|
|
+
|
|
|
+### 状态管理层 (State Management)
|
|
|
+
|
|
|
+#### 现有 Slices
|
|
|
+1. **[`functionAreaSlice`](src/states/view/functionAreaSlice.ts:1-25)**
|
|
|
+ - 职责:存储当前功能按钮触发的 action
|
|
|
+ - 修改:无需修改(已支持 action 机制)
|
|
|
+
|
|
|
+#### 新增 Slice
|
|
|
+2. **`invertContrastSlice`** (新建)
|
|
|
+ - 职责:管理反色对比Modal的状态
|
|
|
+ - 路径:`src/states/view/invertContrastSlice.ts`
|
|
|
+ - 状态:
|
|
|
+ - `isModalOpen: boolean` - Modal是否打开
|
|
|
+ - `imageUrl: string` - 要对比的图像URL
|
|
|
+
|
|
|
+### 工具层 (Utility Layer)
|
|
|
+
|
|
|
+1. **[`invertContrast()`](src/pages/view/components/viewers/stack.image.viewer.tsx:732-740)**
|
|
|
+ - 职责:切换单个 viewport 的反色状态
|
|
|
+ - 修改:已存在,无需修改
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📝 功能实现详细方案
|
|
|
+
|
|
|
+### 🔍 **核心实现原理**
|
|
|
+
|
|
|
+#### **图像数据源问题解决**
|
|
|
+原始实现使用了 `getExposedImageUrl()` 显示JPG图像,但Modal应该显示DCM文件。
|
|
|
+**需要修改:**
|
|
|
+- 缩略图:使用 `getDcmImageUrl(bodyPosition.sop_instance_uid)` 获取DCM URL
|
|
|
+- 对比例:传递DCM URL给StackViewer显示Cornerstone渲染的DCM图像
|
|
|
+
|
|
|
+#### **实现逻辑调整**
|
|
|
+1. **缩略图组件**:显示真实DCM图像缩略图,而非JPG预览
|
|
|
+2. **多选机制**:支持选择多个DCM URL用于对比
|
|
|
+3. **对比显示**:左边显示第一个选中DCM,右边显示第二个选中DCM(反色)
|
|
|
+4. **工具栏**:预留工具按钮接口,功能等待后续实现
|
|
|
+
|
|
|
+### 📋 实现状态追踪
|
|
|
+
|
|
|
+#### ✅ 已完成
|
|
|
+- [x] Redux状态管理 (invertContrastSlice)
|
|
|
+- [x] Modal三栏布局 (缩略图 + 对比区域 + 工具栏)
|
|
|
+- [x] 渲染引擎初始化
|
|
|
+- [x] 多选体位逻辑
|
|
|
+- [x] 工具栏基础结构
|
|
|
+
|
|
|
+#### 🔄 进行中:DCM图像显示修正
|
|
|
+- **问题**:目前显示JPG文件,需要改为显示Cornerstone渲染的DCM文件
|
|
|
+- **影响**:InvertContrastThumbnailList和InvertContrastModal的图像源
|
|
|
+- **解决**:使用getDcmImageUrl而不是getExposedImageUrl,并通过StackViewer显示
|
|
|
+
|
|
|
+#### 📋 TODO List:需要创建/修改的文件
|
|
|
+
|
|
|
+### 新建文件
|
|
|
+1. **创建 Modal 组件**
|
|
|
+ - [x] `src/pages/view/components/InvertContrastModal.tsx`
|
|
|
+ - [x] Modal外壳(全屏或大尺寸)
|
|
|
+ - [x] 3栏布局容器 + 工具栏
|
|
|
+ - [x] 左右两个 StackViewer 实例显示DCM图像
|
|
|
+ - [x] 渲染引擎初始化
|
|
|
+
|
|
|
+2. **创建缩略图列表组件**
|
|
|
+ - [x] `src/pages/view/components/InvertContrastThumbnailList.tsx`
|
|
|
+ - [ ] ~~修正:使用getDcmImageUrl显示DCM缩略图~~
|
|
|
+ - [x] 多选复选框
|
|
|
+ - [x] 实时更新Modal显示
|
|
|
+
|
|
|
+3. **创建工具栏组件**
|
|
|
+ - [x] `src/pages/view/components/InvertContrastToolbar.tsx`
|
|
|
+ - 旋转、翻转、缩放、重置等按钮(预留逻辑)
|
|
|
+
|
|
|
+4. **创建 Redux Slice**
|
|
|
+ - [x] `src/states/view/invertContrastSlice.ts`
|
|
|
+ - [x] 状态:`isModalOpen`, `selectedPositions: string[]`
|
|
|
+ - [x] Actions:`openInvertContrastModal`, `updateSelectedPositions`, `closeInvertContrastModal`
|
|
|
+ - [x] Selectors:`selectIsModalOpen`, `selectSelectedPositions`
|
|
|
+
|
|
|
+### 修改文件
|
|
|
+5. **修改 ViewerContainer**
|
|
|
+ - [x] `src/pages/view/components/ViewerContainer.tsx`
|
|
|
+ - [x] 在 [`useEffect`](src/pages/view/components/ViewerContainer.tsx:314-555) 中添加 `Invert Contrast` 的 case 处理
|
|
|
+ - [x] dispatch 打开Modal的 action
|
|
|
+
|
|
|
+6. **修改 Redux Store**
|
|
|
+ - [x] `src/states/store.ts`
|
|
|
+ - [x] 注册 `invertContrastSlice` reducer
|
|
|
+
|
|
|
+7. **修改文档**
|
|
|
+ - [x] 更新文档,增加UI设计描述和实现进度
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔄 参与者交互流程
|
|
|
+
|
|
|
+### 用户操作序列
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant U as 用户
|
|
|
+ participant FB as FunctionButton
|
|
|
+ participant FA as functionAreaSlice
|
|
|
+ participant VC as ViewerContainer
|
|
|
+ participant IC as invertContrastSlice
|
|
|
+ participant Modal as InvertContrastModal
|
|
|
+ participant SV1 as StackViewer左
|
|
|
+ participant SV2 as StackViewer右
|
|
|
+
|
|
|
+ U->>FB: 点击反色对比按钮
|
|
|
+ FB->>FA: dispatch setAction Invert Contrast
|
|
|
+
|
|
|
+ FA->>VC: action = Invert Contrast
|
|
|
+ VC->>IC: dispatch openInvertContrastModal imageUrl
|
|
|
+ IC->>IC: 设置 isModalOpen = true
|
|
|
+ IC->>IC: 保存 imageUrl
|
|
|
+
|
|
|
+ IC->>Modal: isModalOpen = true
|
|
|
+ Modal->>Modal: 打开Modal
|
|
|
+ Modal->>SV1: 创建左侧viewport 渲染原图
|
|
|
+ Modal->>SV2: 创建右侧viewport 渲染反色图
|
|
|
+ SV2->>SV2: 应用 invert=true
|
|
|
+
|
|
|
+ SV1->>U: 显示原图
|
|
|
+ SV2->>U: 显示反色图
|
|
|
+
|
|
|
+ U->>Modal: 点击关闭按钮
|
|
|
+ Modal->>IC: dispatch closeInvertContrastModal
|
|
|
+ IC->>IC: 设置 isModalOpen = false
|
|
|
+ Modal->>U: 关闭Modal
|
|
|
+
|
|
|
+ VC->>FA: dispatch clearAction
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📊 数据流设计
|
|
|
+
|
|
|
+### 状态流转
|
|
|
+
|
|
|
+```mermaid
|
|
|
+stateDiagram-v2
|
|
|
+ [*] --> Normal: 初始状态
|
|
|
+
|
|
|
+ Normal --> ModalOpen: 点击反色对比 保存 imageUrl
|
|
|
+
|
|
|
+ state ModalOpen {
|
|
|
+ [*] --> RenderModal: 打开Modal
|
|
|
+ RenderModal --> CreateViewports: 创建1x2布局
|
|
|
+ CreateViewports --> LeftOriginal: 左侧原图
|
|
|
+ CreateViewports --> RightInverted: 右侧反色
|
|
|
+ }
|
|
|
+
|
|
|
+ ModalOpen --> Normal: 点击关闭 关闭Modal
|
|
|
+
|
|
|
+ Normal --> [*]
|
|
|
+```
|
|
|
+
|
|
|
+### Redux State 结构
|
|
|
+
|
|
|
+```typescript
|
|
|
+// invertContrastSlice State
|
|
|
+interface InvertContrastState {
|
|
|
+ isModalOpen: boolean; // Modal是否打开
|
|
|
+ imageUrl: string; // 要对比的图像URL
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔀 核心逻辑设计
|
|
|
+
|
|
|
+### ViewerContainer 处理逻辑
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ViewerContainer.tsx 中的处理逻辑
|
|
|
+case 'Invert Contrast': {
|
|
|
+ const selectedUrl = selectedViewerUrls[0]; // 当前选中的图像URL
|
|
|
+
|
|
|
+ if (!selectedUrl) {
|
|
|
+ message.warning('请先选择一张图像');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开Modal
|
|
|
+ dispatch(openInvertContrastModal({ imageUrl: selectedUrl }));
|
|
|
+
|
|
|
+ break;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### InvertContrastModal 组件设计
|
|
|
+
|
|
|
+```typescript
|
|
|
+// src/pages/view/components/InvertContrastModal.tsx
|
|
|
+import React, { useEffect } from 'react';
|
|
|
+import { Modal } from 'antd';
|
|
|
+import { useSelector, useDispatch } from 'react-redux';
|
|
|
+import { selectIsModalOpen, selectImageUrl, closeInvertContrastModal } from '@/states/view/invertContrastSlice';
|
|
|
+import { StackViewerWithErrorBoundary } from './viewers/stack.image.viewer';
|
|
|
+import { invertContrast } from './viewers/stack.image.viewer';
|
|
|
+
|
|
|
+const InvertContrastModal: React.FC = () => {
|
|
|
+ const dispatch = useDispatch();
|
|
|
+ const isOpen = useSelector(selectIsModalOpen);
|
|
|
+ const imageUrl = useSelector(selectImageUrl);
|
|
|
+ const renderingEngineId = 'invertContrastEngine'; // 独立的渲染引擎
|
|
|
+
|
|
|
+ const handleClose = () => {
|
|
|
+ dispatch(closeInvertContrastModal());
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (isOpen && imageUrl) {
|
|
|
+ // Modal打开后,等待右侧viewport创建完成,然后应用反色
|
|
|
+ setTimeout(() => {
|
|
|
+ const rightViewportId = 'invert-contrast-right';
|
|
|
+ try {
|
|
|
+ invertContrast(rightViewportId);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to apply invert:', error);
|
|
|
+ }
|
|
|
+ }, 200);
|
|
|
+ }
|
|
|
+ }, [isOpen, imageUrl]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ title="反色对比"
|
|
|
+ open={isOpen}
|
|
|
+ onCancel={handleClose}
|
|
|
+ width="90vw"
|
|
|
+ style={{ top: 20 }}
|
|
|
+ bodyStyle={{ height: 'calc(90vh - 110px)' }}
|
|
|
+ footer={null}
|
|
|
+ destroyOnClose // 关闭时销毁内容,避免状态残留
|
|
|
+ >
|
|
|
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', height: '100%' }}>
|
|
|
+ {/* 左侧:原图 */}
|
|
|
+ <div style={{ border: '1px solid #d9d9d9', position: 'relative' }}>
|
|
|
+ <div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10, background: 'rgba(0,0,0,0.7)', color: '#fff', padding: '4px 8px', borderRadius: '4px' }}>
|
|
|
+ 原图
|
|
|
+ </div>
|
|
|
+ <StackViewerWithErrorBoundary
|
|
|
+ imageIndex={0}
|
|
|
+ imageUrls={[imageUrl]}
|
|
|
+ viewportId="invert-contrast-left"
|
|
|
+ renderingEngineId={renderingEngineId}
|
|
|
+ selected={false}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 右侧:反色图 */}
|
|
|
+ <div style={{ border: '1px solid #d9d9d9', position: 'relative' }}>
|
|
|
+ <div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10, background: 'rgba(0,0,0,0.7)', color: '#fff', padding: '4px 8px', borderRadius: '4px' }}>
|
|
|
+ 反色图
|
|
|
+ </div>
|
|
|
+ <StackViewerWithErrorBoundary
|
|
|
+ imageIndex={0}
|
|
|
+ imageUrls={[imageUrl]}
|
|
|
+ viewportId="invert-contrast-right"
|
|
|
+ renderingEngineId={renderingEngineId}
|
|
|
+ selected={false}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default InvertContrastModal;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🗂️ 相关数据结构
|
|
|
+
|
|
|
+### invertContrastSlice 完整定义
|
|
|
+
|
|
|
+```typescript
|
|
|
+// src/states/view/invertContrastSlice.ts
|
|
|
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
|
+import { RootState } from '../store';
|
|
|
+
|
|
|
+interface InvertContrastState {
|
|
|
+ isModalOpen: boolean;
|
|
|
+ imageUrl: string;
|
|
|
+}
|
|
|
+
|
|
|
+const initialState: InvertContrastState = {
|
|
|
+ isModalOpen: false,
|
|
|
+ imageUrl: '',
|
|
|
+};
|
|
|
+
|
|
|
+const invertContrastSlice = createSlice({
|
|
|
+ name: 'invertContrast',
|
|
|
+ initialState,
|
|
|
+ reducers: {
|
|
|
+ openInvertContrastModal: (
|
|
|
+ state,
|
|
|
+ action: PayloadAction<{ imageUrl: string }>
|
|
|
+ ) => {
|
|
|
+ state.isModalOpen = true;
|
|
|
+ state.imageUrl = action.payload.imageUrl;
|
|
|
+ },
|
|
|
+ closeInvertContrastModal: (state) => {
|
|
|
+ state.isModalOpen = false;
|
|
|
+ state.imageUrl = '';
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const {
|
|
|
+ openInvertContrastModal,
|
|
|
+ closeInvertContrastModal,
|
|
|
+} = invertContrastSlice.actions;
|
|
|
+
|
|
|
+// Selectors
|
|
|
+export const selectIsModalOpen = (state: RootState) => state.invertContrast.isModalOpen;
|
|
|
+export const selectImageUrl = (state: RootState) => state.invertContrast.imageUrl;
|
|
|
+
|
|
|
+export default invertContrastSlice.reducer;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🎬 功能执行流程
|
|
|
+
|
|
|
+### 整体流程图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ Start([用户点击反色对比按钮]) --> CheckImage{检查是否选中图像?}
|
|
|
+
|
|
|
+ CheckImage -->|未选中| ShowWarning[显示提示信息]
|
|
|
+ CheckImage -->|已选中| OpenModal[打开Modal]
|
|
|
+
|
|
|
+ ShowWarning --> End([结束])
|
|
|
+
|
|
|
+ OpenModal --> SaveUrl[保存图像URL到state]
|
|
|
+ SaveUrl --> RenderModal[渲染Modal组件]
|
|
|
+ RenderModal --> CreateLayout[创建1x2布局]
|
|
|
+ CreateLayout --> CreateLeft[创建左侧viewport 原图]
|
|
|
+ CreateLayout --> CreateRight[创建右侧viewport]
|
|
|
+ CreateRight --> ApplyInvert[应用invert属性到右侧]
|
|
|
+ ApplyInvert --> Display[显示对比效果]
|
|
|
+
|
|
|
+ Display --> UserClose{用户点击关闭?}
|
|
|
+ UserClose -->|是| CloseModal[关闭Modal]
|
|
|
+ UserClose -->|否| Display
|
|
|
+
|
|
|
+ CloseModal --> ClearState[清空state]
|
|
|
+ ClearState --> End
|
|
|
+```
|
|
|
+
|
|
|
+### 详细执行步骤
|
|
|
+
|
|
|
+#### 打开Modal
|
|
|
+1. **用户操作**:点击"反色对比"按钮
|
|
|
+2. **触发 Action**:`setAction("Invert Contrast")`
|
|
|
+3. **检查选中**:检查是否有选中的图像URL
|
|
|
+4. **打开Modal**:`dispatch(openInvertContrastModal({ imageUrl }))`
|
|
|
+5. **渲染Modal**:Modal组件监听到 `isModalOpen = true`,显示弹窗
|
|
|
+6. **创建Viewports**:
|
|
|
+ - 左侧:`invert-contrast-left` (invert=false)
|
|
|
+ - 右侧:`invert-contrast-right` (invert=true)
|
|
|
+7. **应用反色**:调用 `invertContrast('invert-contrast-right')`
|
|
|
+
|
|
|
+#### 关闭Modal
|
|
|
+1. **用户操作**:点击Modal关闭按钮或取消按钮
|
|
|
+2. **触发关闭**:`dispatch(closeInvertContrastModal())`
|
|
|
+3. **更新状态**:`isModalOpen = false`
|
|
|
+4. **销毁内容**:Modal的 `destroyOnClose` 自动清理viewport
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🧪 测试方案
|
|
|
+
|
|
|
+### 功能测试场景
|
|
|
+
|
|
|
+#### 场景1:基本打开/关闭
|
|
|
+1. **前置条件**:
|
|
|
+ - 打开图像处理页面
|
|
|
+ - 已选中一张图像
|
|
|
+2. **操作步骤**:
|
|
|
+ - 点击"反色对比"按钮
|
|
|
+3. **预期结果**:
|
|
|
+ - 打开全屏或大尺寸Modal
|
|
|
+ - Modal内显示1x2布局
|
|
|
+ - 左侧显示原图
|
|
|
+ - 右侧显示反色图
|
|
|
+ - 两侧图像内容相同,仅颜色反转
|
|
|
+4. **关闭操作**:
|
|
|
+ - 点击Modal关闭按钮(X)或取消按钮
|
|
|
+5. **预期结果**:
|
|
|
+ - Modal关闭
|
|
|
+ - 主页面布局不变
|
|
|
+
|
|
|
+#### 场景2:未选中图像
|
|
|
+1. **前置条件**:
|
|
|
+ - 打开图像处理页面
|
|
|
+ - 未选中任何图像
|
|
|
+2. **操作步骤**:
|
|
|
+ - 点击"反色对比"按钮
|
|
|
+3. **预期结果**:
|
|
|
+ - 显示提示信息:"请先选择一张图像"
|
|
|
+ - Modal不打开
|
|
|
+
|
|
|
+#### 场景3:多帧DICOM图像
|
|
|
+1. **前置条件**:
|
|
|
+ - 打开多帧DICOM图像
|
|
|
+ - 当前显示第5帧
|
|
|
+2. **操作步骤**:
|
|
|
+ - 点击"反色对比"按钮
|
|
|
+ - 在Modal内的任一viewport滚动鼠标滚轮
|
|
|
+3. **预期结果**:
|
|
|
+ - Modal打开,显示第5帧的对比
|
|
|
+ - 左右两侧同步切换帧
|
|
|
+ - 右侧始终保持反色效果
|
|
|
+
|
|
|
+#### 场景4:Modal内的工具操作
|
|
|
+1. **前置条件**:
|
|
|
+ - 已打开反色对比Modal
|
|
|
+2. **操作步骤**:
|
|
|
+ - 在Modal内的viewport上执行缩放、平移等操作
|
|
|
+3. **预期结果**:
|
|
|
+ - 工具正常工作
|
|
|
+ - 右侧反色效果不受影响
|
|
|
+
|
|
|
+#### 场景5:页面状态独立性
|
|
|
+1. **前置条件**:
|
|
|
+ - 主页面为2x2布局
|
|
|
+ - 打开反色对比Modal
|
|
|
+2. **操作步骤**:
|
|
|
+ - 关闭Modal
|
|
|
+3. **预期结果**:
|
|
|
+ - 主页面仍为2x2布局
|
|
|
+ - 主页面状态完全不受影响
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🐛 潜在问题与边界情况
|
|
|
+
|
|
|
+### 边界情况分析
|
|
|
+
|
|
|
+#### 1. **未选中图像**
|
|
|
+- **问题**:用户未选中任何图像就点击按钮
|
|
|
+- **解决方案**:
|
|
|
+ - 在ViewerContainer中检查 `selectedViewerUrls.length`
|
|
|
+ - 如为空,显示 `message.warning('请先选择一张图像')`
|
|
|
+
|
|
|
+#### 2. **多选图像**
|
|
|
+- **问题**:用户选中了多张图像
|
|
|
+- **解决方案**:
|
|
|
+ - 只对第一张选中的图像生效:`selectedViewerUrls[0]`
|
|
|
+
|
|
|
+#### 3. **渲染引擎冲突**
|
|
|
+- **问题**:Modal内的viewport可能与主页面的viewport共用渲染引擎,导致冲突
|
|
|
+- **解决方案**:
|
|
|
+ - 为Modal创建独立的渲染引擎:`invertContrastEngine`
|
|
|
+ - 或使用不同的 viewportId 前缀区分
|
|
|
+
|
|
|
+#### 4. **Viewport 创建时机**
|
|
|
+- **问题**:Modal刚打开时,viewport可能未完全创建,立即调用 `invertContrast()` 失败
|
|
|
+- **解决方案**:
|
|
|
+ ```typescript
|
|
|
+ useEffect(() => {
|
|
|
+ if (isOpen && imageUrl) {
|
|
|
+ setTimeout(() => {
|
|
|
+ invertContrast('invert-contrast-right');
|
|
|
+ }, 200); // 延迟200ms
|
|
|
+ }
|
|
|
+ }, [isOpen, imageUrl]);
|
|
|
+ ```
|
|
|
+
|
|
|
+#### 5. **状态残留**
|
|
|
+- **问题**:关闭Modal后,viewport状态可能残留
|
|
|
+- **解决方案**:
|
|
|
+ - Modal设置 `destroyOnClose={true}`
|
|
|
+ - 自动销毁内部组件
|
|
|
+
|
|
|
+#### 6. **多帧同步**
|
|
|
+- **问题**:多帧DICOM中,左右两侧帧数不同步
|
|
|
+- **解决方案**:
|
|
|
+ - 两个 viewport 使用相同的 `imageUrls` 数组
|
|
|
+ - 监听帧切换事件,同步更新两侧
|
|
|
+ - 或使用共享的 `imageIndex` 状态
|
|
|
+
|
|
|
+#### 7. **性能问题**
|
|
|
+- **问题**:Modal内同时渲染两个 viewport 可能影响性能
|
|
|
+- **解决方案**:
|
|
|
+ - 使用相同的图像缓存
|
|
|
+ - 避免重复加载DICOM数据
|
|
|
+ - 利用 Cornerstone 的缓存机制
|
|
|
+
|
|
|
+#### 8. **Modal尺寸适配**
|
|
|
+- **问题**:不同屏幕尺寸下,Modal显示可能不佳
|
|
|
+- **解决方案**:
|
|
|
+ ```typescript
|
|
|
+ width="90vw"
|
|
|
+ style={{ top: 20 }}
|
|
|
+ bodyStyle={{ height: 'calc(90vh - 110px)' }}
|
|
|
+ ```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📐 类图与架构
|
|
|
+
|
|
|
+### 核心类关系
|
|
|
+
|
|
|
+```mermaid
|
|
|
+classDiagram
|
|
|
+ class FunctionButton {
|
|
|
+ +string title
|
|
|
+ +string action
|
|
|
+ +string iconName
|
|
|
+ +handleButtonClick()
|
|
|
+ }
|
|
|
+
|
|
|
+ class ViewerContainer {
|
|
|
+ +useEffect() handleAction
|
|
|
+ }
|
|
|
+
|
|
|
+ class InvertContrastModal {
|
|
|
+ +boolean isOpen
|
|
|
+ +string imageUrl
|
|
|
+ +handleClose()
|
|
|
+ +useEffect() applyInvert
|
|
|
+ }
|
|
|
+
|
|
|
+ class StackViewer {
|
|
|
+ +string viewportId
|
|
|
+ +string[] imageUrls
|
|
|
+ +number imageIndex
|
|
|
+ +useEffect() setup
|
|
|
+ }
|
|
|
+
|
|
|
+ class invertContrastSlice {
|
|
|
+ +boolean isModalOpen
|
|
|
+ +string imageUrl
|
|
|
+ +openInvertContrastModal()
|
|
|
+ +closeInvertContrastModal()
|
|
|
+ }
|
|
|
+
|
|
|
+ class functionAreaSlice {
|
|
|
+ +string action
|
|
|
+ +setAction()
|
|
|
+ +clearAction()
|
|
|
+ }
|
|
|
+
|
|
|
+ FunctionButton --> functionAreaSlice : dispatch setAction
|
|
|
+ ViewerContainer --> functionAreaSlice : useSelector action
|
|
|
+ ViewerContainer --> invertContrastSlice : dispatch open
|
|
|
+ InvertContrastModal --> invertContrastSlice : useSelector state
|
|
|
+ InvertContrastModal --> StackViewer : renders 2 instances
|
|
|
+ StackViewer --> invertContrast : applies to right
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📄 实现优先级与建议
|
|
|
+
|
|
|
+### Phase 1: 核心功能(MVP)
|
|
|
+1. ✅ 创建 `invertContrastSlice`
|
|
|
+2. ✅ 创建 `InvertContrastModal` 组件(基础版)
|
|
|
+3. ✅ 修改 `ViewerContainer` 处理 "Invert Contrast" action
|
|
|
+4. ✅ 测试基本打开/关闭流程
|
|
|
+
|
|
|
+### Phase 2: 优化与边界处理
|
|
|
+1. ✅ 处理未选中图像的情况
|
|
|
+2. ✅ 优化 viewport 创建时机
|
|
|
+3. ✅ 添加多帧同步支持
|
|
|
+4. ✅ 处理渲染引擎冲突
|
|
|
+
|
|
|
+### Phase 3: 用户体验提升
|
|
|
+1. ✅ 添加加载状态提示
|
|
|
+2. ✅ 优化Modal尺寸适配
|
|
|
+3. ✅ 完善错误处理
|
|
|
+4. ✅ 添加快捷键支持(ESC关闭)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🚀 实施进度与更新
|
|
|
+
|
|
|
+### 已完成的改进(2025-12-10)
|
|
|
+
|
|
|
+#### ✅ 完整状态管理重构
|
|
|
+- **invertContrastSlice**扩展:添加`selectedPositions: string[]`数组,支持多选体位
|
|
|
+- **向后兼容**:保持`selectImageUrl`选择器返回第一个选中体位
|
|
|
+- **新增actions**:`updateSelectedPositions`用于更新选中数组
|
|
|
+
|
|
|
+#### ✅ 完整UI布局实现
|
|
|
+- **3栏布局**:
|
|
|
+ - 左侧缩略图栏(280px):`InvertContrastThumbnailList`组件
|
|
|
+ - 中间对比区域:1x2网格显示原图和反色图
|
|
|
+ - 底部工具栏:`InvertContrastToolbar`组件(基础按钮结构)
|
|
|
+
|
|
|
+#### ✅ 缩略图列表组件
|
|
|
+- **多选逻辑**:最多选择2个体位通过复选框
|
|
|
+- **实时更新**:选中状态变化立即更新Modal对比显示
|
|
|
+- **状态显示**:体位名称、曝光状态、判定状态标签
|
|
|
+
|
|
|
+#### ✅ Modal显示逻辑修复
|
|
|
+- **渲染引擎初始化**:Modal独立创建`invertContrastEngine`渲染引擎
|
|
|
+- **多选支持**:
|
|
|
+ - 1个选中:左边原图,右边反色(同图但反色)
|
|
|
+ - 2个选中:左边第一个体位,右边第二个体位(第二个反色)
|
|
|
+- **反色应用**:500ms延迟确保viewport加载完成后再应用反色
|
|
|
+
|
|
|
+#### ✅ 工具栏基础结构
|
|
|
+- **按钮布局**:旋转、翻转、缩放、重置等按钮(逻辑待实现)
|
|
|
+- **事件处理**:预留handler方法供后续实现工具功能
|
|
|
+
|
|
|
+### 当前使用流程
|
|
|
+
|
|
|
+1. **打开Modal**:点击"反色对比"按钮,Modal默认选择当前选中体位
|
|
|
+2. **选择对比体位**:在左侧缩略图列表中通过复选框选择最多2个体位
|
|
|
+3. **实时对比**:选择后Modal中间区域立即显示对应的原图vs反色图对比
|
|
|
+4. **工具操作**:底部工具栏预留工具功能(旋转、缩放等)
|
|
|
+
|
|
|
+### 待完善的优化项
|
|
|
+
|
|
|
+- **工具栏功能实现**:连接实际的旋转、翻转、缩放逻辑到工具按钮
|
|
|
+- **图像加载优化**:确保多体位切换时图像加载流畅
|
|
|
+- **用户体验**:添加加载状态、最小化选择提示等
|
|
|
+- **错误处理**:完善渲染引擎创建失败的处理逻辑
|
|
|
+
|
|
|
+### 下一步
|
|
|
+
|
|
|
+请测试当前实现是否正常工作。如需调整界面逻辑或实现具体工具功能,请提供反馈。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档版本**: v2.0 (Modal版本)
|
|
|
+**创建日期**: 2025-12-10
|
|
|
+**更新日期**: 2025-12-10
|
|
|
+**作者**: Roo (Architect Mode)
|