# 保存注释功能实现文档 ## 需求整理 ### 功能概述 实现图像注释的保存和重现功能,用户可以使用各种注释工具(包括测量工具、文本注释等)在图像上添加注释,后端提供注释相关的API,客户端需要实现保存注释、获取注释并重现注释的逻辑。 ### 详细需求 1. **保存注释** - 当用户使用注释工具添加注释后,自动保存到后端 - 支持多种注释类型(测量工具结果、文本标签、预定义标记、时间戳等) - 注释数据以JSON格式存储 - 支持Cornerstone Tools的所有注释类型 2. **获取注释** - 打开图像时,获取该图像的所有注释数据 - 支持多个注释的管理和重现 3. **重现注释** - 将注释应用到图像上显示 - 重现时机:在打开图像时触发 - 确保注释位置和内容准确还原 4. **注释数据结构** - JSON格式存储注释信息 - 包含注释位置、类型、内容、工具类型等信息 ## 参与者分析 ### 粗粒度参与者 - **前端UI组件**: - ImageOperationPanel.tsx - 图像操作面板 - MeasurementPanel.tsx - 测量工具面板 - MarkPanel.tsx - 标记管理面板(文本注释) - **状态管理**:Redux状态管理注释相关状态 - **API服务**:注释保存和获取API调用 - **Cornerstone工具**:各种Annotation Tool(测量工具、LabelTool等) - **后端API**:注释保存和获取接口 ### 中粒度参与者 - **注释工具集成**: - 多种Cornerstone Tools集成(LengthTool, AngleTool, LabelTool等) - 注释数据序列化/反序列化 - 工具状态管理和切换 - **UI交互组件**: - ImageOperationPanel:提供注释工具选择 - MeasurementPanel:提供测量工具选择 - MarkPanel:提供文本注释管理 - **API层**: - GET /dr/api/v1/auth/image/{id}/annotation - 获取注释 - POST /dr/api/v1/auth/image/{id}/annotation - 保存注释 - 错误处理和重试机制 ### 细粒度参与者 - **注释数据结构**: - 注释ID和元数据 - 图像SOP Instance UID关联 - 注释类型(测量结果、文本标签等) - 几何数据(点、线、角度等) - 位置坐标和变换信息 - 创建时间和用户信息 - **工具集成**: - Cornerstone Tools状态管理 - 工具激活/停用逻辑 - 注释事件监听和处理 ## TODO List - [ ] 分析现有注释工具类型和数据结构 - [ ] 实现注释API调用函数(基于现有API) - [ ] 设计统一的注释数据格式 - [ ] 实现注释保存逻辑(工具操作时触发) - [ ] 实现注释重现逻辑(图像加载时触发) - [ ] 集成Cornerstone Tools注释序列化 - [ ] 添加注释状态管理到Redux - [ ] 实现注释冲突解决和版本控制 - [ ] 添加错误处理和用户反馈 - [ ] 实现注释数据验证和清理 - [ ] 编写集成测试 ## 交互流程(泳道图) ```mermaid sequenceDiagram participant User as 用户 participant UI as UI组件 participant Cornerstone as Cornerstone工具 participant State as 状态管理 participant API as API服务 participant Backend as 后端 User->>UI: 选择注释工具 UI->>Cornerstone: 激活工具 User->>Cornerstone: 在图像上创建注释 Cornerstone->>Cornerstone: 生成注释数据 Cornerstone->>State: 保存到本地状态 State->>API: 触发保存API API->>Backend: POST /auth/image/{id}/annotation Backend-->>API: 保存成功响应 User->>Viewer: 打开图像 Viewer->>API: 获取注释API API->>Backend: GET /auth/image/{id}/annotation Backend-->>API: 返回注释数据 API->>State: 加载注释到状态 State->>Cornerstone: 重现注释到图像 Cornerstone-->>User: 显示注释在图像上 ``` ## 数据流 1. **注释创建流**: 用户操作 → UI选择工具 → Cornerstone创建注释 → 本地状态 → API调用 → 后端保存 2. **注释重现流**: 图像打开 → API获取数据 → 状态更新 → Cornerstone反序列化 → UI显示 ## 数据结构 ### 注释数据接口(基于HipNHAAngleMeasurementTool分析) ```typescript interface AnnotationData { // 基础信息 id: string; toolName: string; // 工具类型,如 'LengthTool', 'AngleTool', 'LabelTool' sopInstanceUid: string; // 图像SOP Instance UID // 几何数据 handles: { points: Point3[]; // 关键点坐标 activeHandleIndex?: number; // 激活的手柄索引 textBox?: Point2; // 文本框位置 }; // 元数据 metadata: { viewPlaneNormal: Point3; viewUp: Point3; FrameOfReferenceUID: string; referencedImageId: string; }; // 计算结果(针对测量工具) cachedStats?: { [targetId: string]: { // 测量结果,如长度、角度等 length?: number; angle?: number; area?: number; // 其他统计数据 }; }; // 显示属性 label?: string; // 显示标签 highlighted?: boolean; // 是否高亮 isSelected?: boolean; // 是否选中 // 时间戳 createdAt: string; updatedAt: string; userId?: string; } ``` ### API接口(基于DR.md) ```typescript // 获取注释 interface GetAnnotationResponse { code: string; description: string; data: any; // JSON格式的注释数据 } // 保存注释 interface SaveAnnotationRequest { // 请求体为任意JSON数据 [key: string]: any; } interface SaveAnnotationResponse { code: string; description: string; data: {}; } ``` ## 执行流程 ### 起点:用户操作触发 1. 用户在ImageOperationPanel或MeasurementPanel中选择注释工具 2. 使用工具在图像上创建注释 3. 注释自动保存到后端 ### 完整执行流程 1. **初始化**:用户打开图像处理页面 2. **获取注释**:调用API获取图像的历史注释数据 3. **重现注释**:将注释数据反序列化并应用到Cornerstone视口 4. **用户操作**:用户使用各种工具添加新注释 5. **保存注释**:新注释自动保存到后端 6. **状态同步**:更新本地状态和UI显示 ## 测试方案 ### 功能测试场景 - ✅ 使用测量工具(LengthTool, AngleTool等)添加注释并保存 - ✅ 使用文本工具(LabelTool)添加注释并保存 - ✅ 删除注释并同步到后端 - ✅ 打开图像时正确重现所有类型的注释 - ✅ 多个注释的同时显示和交互 - ✅ 注释位置和几何数据的准确性 - ✅ 注释测量结果的正确显示 ### 边界情况测试 - ❌ 网络错误时的保存失败处理和重试机制 - ❌ 大量注释的性能表现和内存管理 - ❌ 特殊字符和复杂几何数据处理 - ❌ 注释数据格式兼容性(不同工具类型) - ❌ 并发操作下的数据一致性 - ❌ 后端返回异常数据的处理 ### 异常处理 - API调用超时和重试 - 后端服务不可用时的离线处理 - 数据格式错误和验证 - 权限不足和访问控制 - 注释数据冲突解决 ## 潜在问题分析 ### 边界情况 1. **数据兼容性**:不同版本Cornerstone Tools的注释数据格式兼容 2. **性能问题**:大量复杂注释的加载和渲染性能 3. **存储限制**:注释数据的序列化大小限制 4. **并发控制**:多用户同时编辑同一图像的注释冲突 ### 异常处理 1. **序列化错误**:注释数据序列化/反序列化失败 2. **API错误**:网络错误、服务器错误等各种API异常 3. **数据验证**:注释数据的完整性和有效性验证 4. **状态同步**:本地状态与后端状态的同步机制 ## 注释变化监控 ### 监控机制设计 #### Cornerstone Tools事件监听 基于Cornerstone Tools的事件系统,实现对注释变化的实时监控: ```typescript import { annotation, eventTarget } from '@cornerstonejs/tools'; // 监听注释完成事件 eventTarget.addEventListener('CORNERSTONE_TOOLS_ANNOTATION_COMPLETED', (evt) => { const { annotation, toolName } = evt.detail; handleAnnotationCreated(annotation, toolName); }); // 监听注释修改事件 eventTarget.addEventListener('CORNERSTONE_TOOLS_ANNOTATION_MODIFIED', (evt) => { const { annotation, toolName } = evt.detail; handleAnnotationModified(annotation, toolName); }); // 监听注释删除事件 eventTarget.addEventListener('CORNERSTONE_TOOLS_ANNOTATION_REMOVED', (evt) => { const { annotationUID } = evt.detail; handleAnnotationRemoved(annotationUID); }); ``` #### 注释生命周期事件 1. **创建事件** (`ANNOTATION_COMPLETED`) - 用户完成注释绘制时触发 - 传递完整的注释对象和工具类型 - 立即触发保存流程 2. **修改事件** (`ANNOTATION_MODIFIED`) - 用户拖拽手柄或编辑注释时触发 - 传递修改后的注释对象 - 触发增量保存或延迟保存 3. **删除事件** (`ANNOTATION_REMOVED`) - 用户删除注释时触发 - 传递注释UID - 触发删除同步 #### 防抖保存机制 为了避免频繁的API调用,实现防抖保存: ```typescript class AnnotationAutoSave { private saveTimeout: NodeJS.Timeout | null = null; private pendingChanges: Map = new Map(); // 延迟保存(防抖) scheduleSave(annotationId: string, data: any, delay: number = 1000) { this.pendingChanges.set(annotationId, data); if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(() => { this.performBulkSave(); }, delay); } // 立即保存(重要操作) async saveImmediately(annotationId: string, data: any) { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } try { await this.saveAnnotation(annotationId, data); this.pendingChanges.delete(annotationId); } catch (error) { console.error('立即保存失败:', error); } } // 批量保存待处理的更改 private async performBulkSave() { const changes = Array.from(this.pendingChanges.entries()); this.pendingChanges.clear(); try { await this.saveMultipleAnnotations(changes); } catch (error) { console.error('批量保存失败:', error); // 失败时重新加入待保存队列 changes.forEach(([id, data]) => { this.pendingChanges.set(id, data); }); } } } ``` #### 注释状态跟踪 维护注释的状态变化: ```typescript interface AnnotationState { id: string; lastModified: number; isDirty: boolean; // 是否有未保存的更改 isSaving: boolean; // 是否正在保存 lastSavedData: any; // 最后保存的数据 retryCount: number; // 保存失败重试次数 } class AnnotationStateManager { private states: Map = new Map(); markAsDirty(annotationId: string, data: any) { const state = this.getOrCreateState(annotationId); state.isDirty = true; state.lastModified = Date.now(); } markAsSaving(annotationId: string) { const state = this.getOrCreateState(annotationId); state.isSaving = true; } markAsSaved(annotationId: string, data: any) { const state = this.getOrCreateState(annotationId); state.isDirty = false; state.isSaving = false; state.lastSavedData = data; state.retryCount = 0; } markAsFailed(annotationId: string) { const state = this.getOrCreateState(annotationId); state.isSaving = false; state.retryCount++; } shouldRetry(annotationId: string): boolean { const state = this.states.get(annotationId); return state ? state.retryCount < 3 : false; } } ``` #### 冲突解决机制 处理多用户并发编辑: ```typescript class AnnotationConflictResolver { // 乐观锁版本控制 private versions: Map = new Map(); async resolveConflict(annotationId: string, localData: any, serverData: any): Promise { const localVersion = this.getVersion(annotationId); const serverVersion = serverData.version || 0; if (serverVersion > localVersion) { // 服务器版本更新,提示用户 return await this.promptUserResolution(localData, serverData); } // 本地版本更新,覆盖服务器版本 return localData; } private async promptUserResolution(localData: any, serverData: any): Promise { // 显示冲突解决对话框 return new Promise((resolve) => { // 实现用户选择逻辑 // 选择保留本地更改、接受服务器更改、或手动合并 }); } } ``` ## 统一图像注释管理器 ### 管理器架构设计 由于注释的添加/编辑监控、加载和重现都是针对同一个图像的,这些功能应该由一个统一的`ImageAnnotationManager`来管理: ```typescript class ImageAnnotationManager { private annotationMonitor: AnnotationMonitor; private annotationLoader: AnnotationLoader; private annotationRenderer: AnnotationRenderer; private imageEventListener: ImageEventListener; private activeImageId: string | null = null; constructor( private cornerstoneIntegration: CornerstoneIntegration, private annotationAPI: AnnotationAPI, private stateManager: AnnotationStateManager ) { this.annotationMonitor = new AnnotationMonitor(this); this.annotationLoader = new AnnotationLoader(this); this.annotationRenderer = new AnnotationRenderer(this); this.imageEventListener = new ImageEventListener(this); } // 绑定到特定图像 async bindToImage(imageId: string, viewportId: string): Promise { if (this.activeImageId === imageId) return; // 解绑之前的图像 if (this.activeImageId) { await this.unbindFromImage(); } this.activeImageId = imageId; // 绑定新图像的事件监听 this.imageEventListener.bindToImage(imageId, viewportId); // 加载注释数据 await this.annotationLoader.loadAnnotations(imageId); // 启动监控 this.annotationMonitor.startMonitoring(viewportId); } // 解绑图像 async unbindFromImage(): Promise { if (!this.activeImageId) return; // 停止监控 this.annotationMonitor.stopMonitoring(); // 取消事件监听 this.imageEventListener.unbindFromImage(); // 保存所有待保存的更改 await this.annotationMonitor.flushPendingChanges(); this.activeImageId = null; } // 处理注释创建 async handleAnnotationCreated(annotation: any, toolName: string): Promise { const serializedData = this.serializeAnnotation(annotation, toolName); await this.annotationMonitor.handleAnnotationCreated(this.activeImageId!, serializedData); } // 处理注释修改 async handleAnnotationModified(annotation: any, toolName: string): Promise { const serializedData = this.serializeAnnotation(annotation, toolName); await this.annotationMonitor.handleAnnotationModified(this.activeImageId!, serializedData); } // 处理注释删除 async handleAnnotationRemoved(annotationUID: string): Promise { await this.annotationMonitor.handleAnnotationRemoved(this.activeImageId!, annotationUID); } // 序列化注释数据 private serializeAnnotation(annotation: any, toolName: string): any { return this.cornerstoneIntegration.serializeAnnotation(annotation, toolName); } // 反序列化注释数据 private deserializeAnnotation(data: any): any { return this.cornerstoneIntegration.deserializeAnnotation(data); } } ``` ### 图像生命周期事件监听 扩展事件监听,包含图像相关的生命周期事件: ```typescript class ImageEventListener { constructor(private manager: ImageAnnotationManager) {} bindToImage(imageId: string, viewportId: string): void { const { eventTarget } = cornerstoneTools; // 图像加载完成事件 eventTarget.addEventListener( cornerstone.EVENTS.IMAGE_LOAD_COMPLETED, this.handleImageLoadCompleted ); // 图像渲染完成事件 eventTarget.addEventListener( cornerstone.EVENTS.IMAGE_RENDERED, this.handleImageRendered ); // 视口大小改变事件 eventTarget.addEventListener( cornerstone.EVENTS.VIEWPORT_NEW_IMAGE_SET, this.handleViewportChanged ); // 工具模式改变事件 eventTarget.addEventListener( cornerstoneTools.Enums.Events.TOOL_MODE_CHANGED, this.handleToolModeChanged ); } unbindFromImage(): void { const { eventTarget } = cornerstoneTools; eventTarget.removeEventListener( cornerstone.EVENTS.IMAGE_LOAD_COMPLETED, this.handleImageLoadCompleted ); eventTarget.removeEventListener( cornerstone.EVENTS.IMAGE_RENDERED, this.handleImageRendered ); eventTarget.removeEventListener( cornerstone.EVENTS.VIEWPORT_NEW_IMAGE_SET, this.handleViewportChanged ); eventTarget.removeEventListener( cornerstoneTools.Enums.Events.TOOL_MODE_CHANGED, this.handleToolModeChanged ); } private handleImageLoadCompleted = (evt: any) => { const { imageId } = evt.detail; if (imageId === this.manager.getActiveImageId()) { console.log(`图像 ${imageId} 加载完成,开始加载注释`); // 图像加载完成后,延迟一下再加载注释,确保Cornerstone完全就绪 setTimeout(() => { this.manager.loadAnnotationsForImage(imageId); }, 100); } }; private handleImageRendered = (evt: any) => { // 图像渲染完成后,确保注释正确显示 const { viewportId } = evt.detail; this.manager.ensureAnnotationsVisible(viewportId); }; private handleViewportChanged = (evt: any) => { // 视口改变时,可能需要重新计算注释位置 const { viewportId } = evt.detail; this.manager.handleViewportResize(viewportId); }; private handleToolModeChanged = (evt: any) => { // 工具模式改变时,调整注释的交互状态 const { toolName, toolMode } = evt.detail; this.manager.handleToolModeChange(toolName, toolMode); }; } ``` ### 注释加载器 (AnnotationLoader) 专门负责注释数据的加载: ```typescript class AnnotationLoader { private loadedAnnotations: Map = new Map(); private isLoading: boolean = false; async loadAnnotations(imageId: string): Promise { if (this.loadedAnnotations.has(imageId)) { // 已有缓存,直接重现 await this.renderCachedAnnotations(imageId); return; } if (this.isLoading) return; // 防止重复加载 this.isLoading = true; try { const annotationsData = await this.annotationAPI.getAnnotation(imageId); if (annotationsData && annotationsData.length > 0) { this.loadedAnnotations.set(imageId, annotationsData); await this.renderAnnotations(imageId, annotationsData); } } catch (error) { console.error(`加载注释失败 ${imageId}:`, error); // 可以在这里实现重试逻辑或降级处理 } finally { this.isLoading = false; } } private async renderCachedAnnotations(imageId: string): Promise { const cachedData = this.loadedAnnotations.get(imageId); if (cachedData) { await this.renderAnnotations(imageId, cachedData); } } private async renderAnnotations(imageId: string, annotationsData: any[]): Promise { for (const annotationData of annotationsData) { try { const deserializedAnnotation = this.manager.deserializeAnnotation(annotationData); await this.annotationRenderer.renderAnnotation(deserializedAnnotation); } catch (error) { console.error(`反序列化注释失败:`, error); // 继续处理其他注释 } } } clearCache(imageId?: string): void { if (imageId) { this.loadedAnnotations.delete(imageId); } else { this.loadedAnnotations.clear(); } } } ``` ### 注释渲染器 (AnnotationRenderer) 专门负责注释的重现: ```typescript class AnnotationRenderer { private renderedAnnotations: Map = new Map(); async renderAnnotation(annotation: any): Promise { const annotationId = annotation.annotationUID || annotation.id; try { // 检查是否已渲染 if (this.renderedAnnotations.has(annotationId)) { return; // 已渲染,跳过 } // 应用到Cornerstone await this.cornerstoneIntegration.applyAnnotation(annotation); // 标记为已渲染 this.renderedAnnotations.set(annotationId, annotation); } catch (error) { console.error(`渲染注释失败 ${annotationId}:`, error); throw error; } } removeAnnotation(annotationId: string): void { const annotation = this.renderedAnnotations.get(annotationId); if (annotation) { this.cornerstoneIntegration.removeAnnotation(annotation); this.renderedAnnotations.delete(annotationId); } } updateAnnotation(annotationId: string, newAnnotation: any): void { this.removeAnnotation(annotationId); this.renderAnnotation(newAnnotation); } clearAll(): void { for (const [annotationId] of this.renderedAnnotations) { this.removeAnnotation(annotationId); } } } ``` ### 集成到React组件 在React组件中的使用方式: ```typescript const ImageViewerComponent: React.FC = () => { const annotationManagerRef = useRef(null); useEffect(() => { // 初始化注释管理器 annotationManagerRef.current = new ImageAnnotationManager( cornerstoneIntegration, annotationAPI, stateManager ); return () => { // 清理 annotationManagerRef.current?.unbindFromImage(); }; }, []); const handleImageLoad = useCallback(async (imageId: string, viewportId: string) => { if (annotationManagerRef.current) { await annotationManagerRef.current.bindToImage(imageId, viewportId); } }, []); const handleImageUnload = useCallback(async () => { if (annotationManagerRef.current) { await annotationManagerRef.current.unbindFromImage(); } }, []); return (
{/* 图像显示组件 */}
); }; ``` ### 补充的事件监听 除了注释相关事件,还需要监听以下图像相关事件: 1. **图像加载事件** - `IMAGE_LOAD_START` - 图像开始加载 - `IMAGE_LOAD_COMPLETED` - 图像加载完成 - `IMAGE_LOAD_FAILED` - 图像加载失败 2. **图像渲染事件** - `IMAGE_RENDERED` - 图像渲染完成 - `IMAGE_BEFORE_RENDER` - 图像渲染前 3. **视口事件** - `VIEWPORT_NEW_IMAGE_SET` - 视口设置新图像 - `VIEWPORT_CAMERA_MODIFIED` - 相机参数修改 - `VIEWPORT_CAMERA_RESET` - 相机重置 4. **工具事件** - `TOOL_MODE_CHANGED` - 工具模式改变 - `TOOL_ACTIVATED` - 工具激活 - `TOOL_DEACTIVATED` - 工具停用 5. **注释相关补充事件** - `ANNOTATION_SELECTION_CHANGE` - 注释选择改变 - `ANNOTATION_VISIBILITY_CHANGE` - 注释可见性改变 - `ANNOTATION_LOCK_CHANGE` - 注释锁定状态改变 ### 内存管理和性能优化 ```typescript class AnnotationMemoryManager { private maxCacheSize: number = 50; // 最大缓存图像数量 private cacheSize: number = 0; // LRU缓存策略 private accessOrder: string[] = []; addToCache(imageId: string): void { // 更新访问顺序 const index = this.accessOrder.indexOf(imageId); if (index > -1) { this.accessOrder.splice(index, 1); } this.accessOrder.push(imageId); // 检查缓存大小限制 if (this.cacheSize > this.maxCacheSize) { this.evictOldest(); } } private evictOldest(): void { const oldestImageId = this.accessOrder.shift(); if (oldestImageId) { // 清理最老的图像注释数据 this.annotationLoader.clearCache(oldestImageId); this.annotationRenderer.clearForImage(oldestImageId); this.cacheSize--; } } cleanup(): void { this.annotationLoader.clearCache(); this.annotationRenderer.clearAll(); this.accessOrder = []; this.cacheSize = 0; } } ``` ## 注释获取、解读和渲染流程 ### 注释获取时机 #### 触发条件 注释数据的获取主要在以下时机触发: 1. **图像首次加载完成时** ```typescript // 监听图像加载完成事件 eventTarget.addEventListener( cornerstone.EVENTS.IMAGE_LOAD_COMPLETED, (evt) => { const { imageId } = evt.detail; // 图像加载完成后立即获取注释 this.loadAnnotationsForImage(imageId); } ); ``` 2. **图像切换时** ```typescript // 当用户切换到新的图像时 async bindToImage(newImageId: string, viewportId: string) { // 解绑旧图像 await this.unbindFromImage(); // 绑定新图像并加载注释 await this.loadAnnotationsForImage(newImageId); } ``` 3. **手动刷新时** ```typescript // 用户点击刷新按钮或需要重新同步 async refreshAnnotations(imageId: string) { this.clearAnnotationCache(imageId); await this.loadAnnotationsForImage(imageId); } ``` #### 获取策略 1. **缓存优先策略** ```typescript async loadAnnotations(imageId: string) { // 首先检查本地缓存 if (this.hasCachedAnnotations(imageId)) { return this.renderCachedAnnotations(imageId); } // 缓存不存在,从服务器获取 try { const serverData = await this.annotationAPI.getAnnotation(imageId); this.cacheAnnotations(imageId, serverData); await this.renderAnnotations(imageId, serverData); } catch (error) { console.error('获取注释失败:', error); // 可以显示错误提示或降级处理 } } ``` 2. **增量更新策略** ```typescript // 只获取上次同步时间之后的新注释 async loadIncrementalAnnotations(imageId: string, since: Date) { const newAnnotations = await this.annotationAPI.getAnnotationsSince(imageId, since); // 合并到现有注释中 this.mergeAnnotations(imageId, newAnnotations); } ``` ### 注释数据解读(反序列化) #### 数据格式转换 注释数据从JSON格式转换为Cornerstone Tools可识别的对象: ```typescript class AnnotationDeserializer { // 反序列化单个注释 deserializeAnnotation(jsonData: any): CornerstoneAnnotation { const { toolName, handles, metadata, cachedStats, ...otherProps } = jsonData; // 1. 验证数据完整性 this.validateAnnotationData(jsonData); // 2. 转换坐标系(如果需要) const transformedHandles = this.transformCoordinates(handles, metadata); // 3. 重建注释对象 const annotation = { annotationUID: jsonData.id || this.generateUID(), metadata: { ...metadata, toolName, referencedImageId: metadata.referencedImageId, FrameOfReferenceUID: metadata.FrameOfReferenceUID, }, data: { handles: transformedHandles, cachedStats: cachedStats || {}, ...otherProps, }, isSelected: false, highlighted: false, }; // 4. 工具特定的后处理 return this.postProcessByTool(annotation, toolName); } // 坐标系转换(世界坐标 ↔ 画布坐标) private transformCoordinates(handles: any, metadata: any) { // 如果存储的是世界坐标,需要转换 if (this.isWorldCoordinate(handles)) { return handles; // 直接使用世界坐标 } // 如果是相对坐标,需要转换为世界坐标 return this.convertToWorldCoordinates(handles, metadata); } // 工具特定的后处理 private postProcessByTool(annotation: any, toolName: string) { switch (toolName) { case 'LengthTool': return this.processLengthTool(annotation); case 'AngleTool': return this.processAngleTool(annotation); case 'LabelTool': return this.processLabelTool(annotation); case 'HipNHAAngleMeasurementTool': return this.processHipNHATool(annotation); default: return annotation; } } // 处理长度测量工具 private processLengthTool(annotation: any) { // 确保有两个端点 const points = annotation.data.handles.points; if (points.length !== 2) { console.warn('LengthTool需要正好2个点'); } // 重新计算长度(如果cachedStats不存在或无效) if (!annotation.data.cachedStats.length) { const length = this.calculateDistance(points[0], points[1]); annotation.data.cachedStats = { length }; } return annotation; } // 处理角度测量工具 private processAngleTool(annotation: any) { const points = annotation.data.handles.points; if (points.length !== 3) { console.warn('AngleTool需要正好3个点'); } // 重新计算角度 if (!annotation.data.cachedStats.angle) { const angle = this.calculateAngle(points[0], points[1], points[2]); annotation.data.cachedStats = { angle }; } return annotation; } } ``` #### 数据验证 ```typescript class AnnotationValidator { validateAnnotationData(data: any): void { // 必需字段检查 this.checkRequiredFields(data, ['toolName', 'handles', 'metadata']); // 工具特定验证 this.validateToolSpecificData(data.toolName, data); // 坐标数据验证 this.validateCoordinates(data.handles.points); // 元数据验证 this.validateMetadata(data.metadata); } private checkRequiredFields(data: any, requiredFields: string[]): void { const missing = requiredFields.filter(field => !data[field]); if (missing.length > 0) { throw new Error(`缺少必需字段: ${missing.join(', ')}`); } } private validateCoordinates(points: any[]): void { if (!Array.isArray(points) || points.length < 2) { throw new Error('坐标点数据无效'); } points.forEach((point, index) => { if (!this.isValidPoint(point)) { throw new Error(`坐标点 ${index} 无效: ${JSON.stringify(point)}`); } }); } private isValidPoint(point: any): boolean { return point && typeof point.x === 'number' && !isNaN(point.x) && typeof point.y === 'number' && !isNaN(point.y) && (point.z === undefined || (typeof point.z === 'number' && !isNaN(point.z))); } } ``` ### 注释渲染流程 #### Cornerstone Tools渲染机制 注释渲染通过Cornerstone Tools的渲染管道实现: ```typescript class AnnotationRenderer { async renderAnnotation(annotation: CornerstoneAnnotation): Promise { const { toolName } = annotation.metadata; const toolInstance = this.getToolInstance(toolName); // 1. 注册注释到Cornerstone await this.registerAnnotation(annotation); // 2. 触发渲染更新 this.triggerViewportRender(annotation.metadata.referencedImageId); // 3. 设置交互状态 this.setupInteraction(annotation); } private async registerAnnotation(annotation: CornerstoneAnnotation): Promise { // 将注释添加到Cornerstone的annotation state中 cornerstoneTools.annotation.state.addAnnotation(annotation); // 如果是测量工具,确保统计数据正确 if (this.isMeasurementTool(annotation.metadata.toolName)) { await this.ensureStatsCalculated(annotation); } } private triggerViewportRender(imageId: string): void { // 找到对应的视口 const viewports = cornerstone.getEnabledElementByViewportId(imageId); if (viewports) { viewports.viewport.render(); } } private setupInteraction(annotation: CornerstoneAnnotation): void { // 设置注释的交互属性 annotation.isSelected = false; annotation.highlighted = false; // 添加事件监听器 this.addAnnotationEventListeners(annotation); } } ``` #### SVG渲染管道 Cornerstone Tools使用SVG进行注释渲染: ```typescript // 工具的renderAnnotation方法示例 renderAnnotation = ( enabledElement: CoreTypes.IEnabledElement, svgDrawingHelper: SVGDrawingHelper ): boolean => { const { viewport } = enabledElement; const { element } = viewport; // 获取该工具的所有注释 const annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations?.length) { return false; } // 为每个注释创建SVG元素 for (const ann of annotations) { const { annotationUID, data, highlighted } = ann; // 转换坐标 const canvasPoints = this.worldToCanvas(data.handles.points, viewport); // 确定颜色和样式 const color = highlighted ? '#00AAFF' : '#FFFFFF'; // 绘制几何形状 this.drawGeometry(svgDrawingHelper, annotationUID, canvasPoints, color); // 绘制统计文本 if (data.cachedStats) { this.drawStatsText(svgDrawingHelper, annotationUID, data.cachedStats, canvasPoints[0]); } // 绘制交互手柄 if (highlighted || ann.isSelected) { this.drawHandles(svgDrawingHelper, annotationUID, canvasPoints); } } return true; }; ``` #### 渲染优化策略 ```typescript class RenderOptimizer { private renderQueue: Map = new Map(); private isRendering: boolean = false; // 批量渲染优化 async batchRender(annotations: CornerstoneAnnotation[], viewportId: string): Promise { // 添加到渲染队列 if (!this.renderQueue.has(viewportId)) { this.renderQueue.set(viewportId, []); } this.renderQueue.get(viewportId)!.push(...annotations); // 如果正在渲染,等待完成 if (this.isRendering) { return this.waitForCurrentRender(viewportId); } // 开始批量渲染 return this.performBatchRender(viewportId); } private async performBatchRender(viewportId: string): Promise { this.isRendering = true; try { const annotations = this.renderQueue.get(viewportId) || []; this.renderQueue.delete(viewportId); // 批量注册注释 for (const annotation of annotations) { await this.registerAnnotation(annotation); } // 单次渲染更新 this.triggerViewportRender(viewportId); } finally { this.isRendering = false; } } // 视口裁剪优化 isAnnotationVisible(annotation: CornerstoneAnnotation, viewport: any): boolean { const bounds = this.calculateAnnotationBounds(annotation); const viewportBounds = this.getViewportBounds(viewport); return this.intersects(bounds, viewportBounds); } // 细节级别优化(LOD) shouldRenderDetailed(annotation: CornerstoneAnnotation, zoomLevel: number): boolean { // 在低缩放级别下简化渲染 if (zoomLevel < 0.5) { return annotation.metadata.toolName === 'LabelTool'; // 只显示文本标签 } return true; } } ``` ### 完整流程图 ```mermaid flowchart TD A[图像加载完成] --> B[触发注释加载] B --> C{检查本地缓存} C -->|有缓存| D[从缓存加载] C -->|无缓存| E[调用API获取注释] E --> F{API调用成功?} F -->|是| G[接收JSON数据] F -->|否| H[显示错误信息] G --> I[数据验证] I --> J{数据有效?} J -->|否| K[记录错误,继续下一个] J -->|是| L[反序列化数据] L --> M[坐标系转换] M --> N[工具特定处理] N --> O[重建注释对象] O --> P[注册到Cornerstone] P --> Q[计算统计数据] Q --> R[添加到渲染队列] R --> S[触发视口渲染] S --> T[SVG元素生成] T --> U[绘制几何形状] U --> V[绘制文本标签] V --> W[绘制交互手柄] W --> X[设置交互状态] X --> Y[添加事件监听] Y --> Z[注释渲染完成] D --> O K --> G ``` ### 渲染在图像上的机制 #### SVG图层架构 ```typescript // Cornerstone的渲染架构
Length: 14.2mm
``` #### 坐标系映射 ```typescript class CoordinateMapper { // 世界坐标 → Canvas坐标 worldToCanvas(worldPoint: Point3, viewport: CornerstoneViewport): Point2 { return viewport.worldToCanvas(worldPoint); } // Canvas坐标 → 世界坐标 canvasToWorld(canvasPoint: Point2, viewport: CornerstoneViewport): Point3 { return viewport.canvasToWorld(canvasPoint); } // 处理视口变换 transformWithViewport(point: Point3, viewport: CornerstoneViewport): Point2 { // 考虑缩放、平移、旋转等变换 const transformed = this.applyViewportTransform(point, viewport.getCamera()); return this.worldToCanvas(transformed, viewport); } private applyViewportTransform(point: Point3, camera: Camera): Point3 { // 应用相机变换矩阵 // 处理缩放、平移、旋转 return this.applyMatrixTransform(point, camera.viewMatrix); } } ``` #### 交互事件绑定 ```typescript class InteractionManager { setupAnnotationInteraction(annotation: CornerstoneAnnotation, svgElement: SVGElement): void { // 鼠标悬停 svgElement.addEventListener('mouseenter', () => { annotation.highlighted = true; this.triggerRender(); }); svgElement.addEventListener('mouseleave', () => { annotation.highlighted = false; this.triggerRender(); }); // 点击选择 svgElement.addEventListener('click', () => { this.selectAnnotation(annotation); }); // 拖拽手柄 const handles = svgElement.querySelectorAll('.annotation-handle'); handles.forEach(handle => { this.makeDraggable(handle, annotation); }); } private makeDraggable(handleElement: Element, annotation: CornerstoneAnnotation): void { let isDragging = false; let startPos: Point2; handleElement.addEventListener('mousedown', (e) => { isDragging = true; startPos = { x: e.clientX, y: e.clientY }; }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; const delta = { x: e.clientX - startPos.x, y: e.clientY - startPos.y }; this.updateHandlePosition(annotation, handleElement, delta); }); window.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; this.finalizeHandlePosition(annotation); } }); } } ``` 这个完整的流程确保了注释数据能够正确地从服务器获取、解析、验证,然后准确地渲染在图像上,并支持丰富的用户交互。渲染机制充分利用了SVG的矢量图形特性,保证了在不同缩放级别下的清晰显示和精确的坐标映射。 ## 实现思路 ### 核心实现点 1. **注释数据模型**:基于HipNHAAngleMeasurementTool的数据结构设计 2. **API集成**:封装现有的注释保存和获取API 3. **Cornerstone集成**:实现各种Tools的注释序列化支持 4. **状态管理**:扩展Redux管理注释状态 5. **UI集成**:在现有面板中集成注释管理 6. **变化监控**:实现事件驱动的注释变化监听和自动保存 ### 技术方案 - 使用Redux Toolkit管理注释状态 - 基于Axios封装API调用 - 利用Cornerstone Tools的annotation系统和事件机制 - JSON格式存储注释数据 - 事件驱动的保存机制 - 防抖和批量保存优化 - 冲突解决和版本控制 ### 开发步骤 1. 分析现有注释工具的数据结构和事件系统 2. 设计统一的注释数据格式和状态管理 3. 实现注释API封装和错误处理 4. 实现注释变化监听和自动保存逻辑 5. 集成Cornerstone Tools序列化和事件监听 6. 添加状态管理和UI集成 7. 实现冲突解决和离线支持 8. 实现错误处理和数据验证 9. 编写测试和文档 ## 📊 生成 Mermaid 图表 ### 序列图 ```mermaid sequenceDiagram participant User as 用户 participant Panel as 操作面板 participant Tool as Cornerstone工具 participant State as Redux状态 participant API as 注释API participant DB as 后端存储 User->>Panel: 选择注释工具 Panel->>Tool: 激活工具 User->>Tool: 创建注释 Tool->>Tool: 生成注释数据 Tool->>State: 更新本地状态 State->>API: 自动保存 API->>DB: POST /auth/image/{id}/annotation DB-->>API: 保存确认 User->>Viewer: 打开图像 Viewer->>API: 获取注释 API->>DB: GET /auth/image/{id}/annotation DB-->>API: 返回注释数据 API-->>State: 加载到状态 State-->>Tool: 反序列化注释 Tool-->>User: 显示注释 ``` ### 类图 ```mermaid classDiagram class AnnotationManager { +saveAnnotation(sopId: string, data: any): Promise +loadAnnotation(sopId: string): Promise +serializeAnnotation(toolName: string, annotation: any): any +deserializeAnnotation(data: any): any } class CornerstoneIntegration { +registerAnnotationTools(): void +activateTool(toolName: string): void +getAnnotationData(toolName: string): any +applyAnnotationData(data: any): void } class AnnotationAPI { +getAnnotation(sopId: string): Promise +saveAnnotation(sopId: string, data: any): Promise +handleErrors(response: any): void } class AnnotationState { +annotations: Map +addAnnotation(sopId: string, data: any): void +removeAnnotation(sopId: string): void +getAnnotations(sopId: string): any[] } AnnotationManager --> AnnotationAPI AnnotationManager --> CornerstoneIntegration AnnotationManager --> AnnotationState ``` ### 流程图 ```mermaid flowchart TD A[用户选择注释工具] --> B[激活Cornerstone工具] B --> C[用户在图像上操作] C --> D[工具生成注释数据] D --> E[序列化注释数据] E --> F[调用保存API] F --> G{保存成功?} G -->|是| H[更新本地状态] G -->|否| I[显示保存失败] J[用户打开图像] --> K[检查本地缓存] K --> L{有缓存?} L -->|是| M[从缓存加载] L -->|否| N[调用获取API] N --> O{获取成功?} O -->|是| P[反序列化注释] O -->|否| Q[显示错误信息] P --> R[应用到Cornerstone] R --> S[显示注释在图像上] ``` ## 🧪 制定测试方案 ### 功能测试场景 1. **测量工具注释** - 线段测量工具:创建、保存、重现、删除 - 角度测量工具:创建、保存、重现、删除 - 其他测量工具:面积、圆形、椭圆等 2. **文本注释** - LabelTool:创建文本标签、保存、重现 - 预定义标记:L/R标记、时间戳等 3. **复杂注释** - 多点测量工具(如HipNHAAngleMeasurementTool) - 复合几何形状注释 4. **注释管理** - 批量注释的重现 - 注释的选择和编辑 - 注释的删除和清理 ### 集成测试 1. **完整工作流测试** - 创建注释 → 保存 → 关闭图像 → 重新打开 → 验证重现 - 多工具混合使用测试 - 大量注释性能测试 2. **错误场景测试** - 网络断开时的保存重试 - API错误时的用户反馈 - 数据格式错误处理 3. **边界条件测试** - 超大注释数据的处理 - 特殊字符和Unicode内容 - 坐标超出图像边界的情况 ## 🐛 检查潜在问题 ### 性能问题 - 大量注释时的加载性能 - 复杂几何数据序列化开销 - 内存泄漏风险(未清理的注释对象) ### 数据一致性 - 多用户并发编辑冲突 - 离线操作的数据同步 - 版本控制和冲突解决 ### 兼容性问题 - 不同版本Cornerstone Tools的数据格式兼容 - 浏览器兼容性(Canvas 2D API支持) - 移动设备触摸操作支持 ### 安全问题 - 注释数据输入验证 - XSS攻击防护(文本内容) - API访问权限控制 ### 用户体验问题 - 保存过程中的加载状态显示 - 错误信息的友好提示 - 操作撤销和重做功能 ## 测试操作步骤 ### 基本功能测试 1. **打开图像处理页面**,加载一张测试图像 2. **选择测量工具**:在MeasurementPanel中选择线段测量工具 3. **创建注释**:在图像上绘制一条线段 4. **验证保存**:观察网络请求,确认注释保存到后端 5. **重现测试**:关闭并重新打开图像,验证注释正确显示 ### 文本注释测试 1. **选择文本工具**:在MarkPanel中选择文本注释 2. **添加文本**:输入文本内容并放置在图像上 3. **验证保存和重现**:重复上述保存和重现流程 ### 复杂注释测试 1. **使用高级测量工具**:如HipNHAAngleMeasurementTool 2. **创建多点注释**:按照工具要求创建复杂的几何注释 3. **验证数据完整性**:确保所有几何数据和计算结果正确保存和重现 ### 错误处理测试 1. **网络断开测试**:断开网络连接后尝试保存注释 2. **API错误测试**:模拟后端错误响应 3. **数据恢复测试**:网络恢复后验证数据同步 预期结果:所有注释都能正确保存、重现,错误情况有适当的用户反馈。 ## 功能模块组织和代码放置方案 ### 📍 初始化位置:app.tsx 集成方案 基于全局事件监听方案,注释管理器通过修改 `src/app.tsx` 即可集成到应用中,无需修改其他现有代码: ```typescript // src/app.tsx import { initializeAnnotationManager, cleanupAnnotationManager } from '@/features/imageAnnotation'; function AppContent({ children }: { children: ReactNode }): JSX.Element { // ... 现有代码保持不变 ... useEffect(() => { // 浏览器环境不需要服务器连接检查,直接初始化应用 if (platform.isBrowser) { console.log('浏览器环境,跳过服务器连接检查,直接初始化应用'); initializeApp(); return; } // Electron/Cordova 环境:检查服务器连接 dispatch(checkServerConnection()) .unwrap() .then((result) => { if (result.needsConfig) { // 需要配置,显示对话框 console.log('检测到需要服务器配置,显示配置对话框'); setShowConfigModal(true); } else { // 连接正常,继续正常初始化 console.log('服务器连接正常,开始应用初始化'); return initializeApp(); } }) .catch((error) => { console.error('连接检查失败:', error); setConnectionChecked(true); setShowConfigModal(true); }); }, [dispatch]); // 应用正常初始化函数 const initializeApp = async () => { try { const productState = await dispatch(initializeProductState()).unwrap(); console.log(`初始化,拉取到产品信息:${JSON.stringify(productState)}`); const languageCode = productState.language; await dispatch(loadI18nMessages(languageCode)).unwrap(); // ✅ 新增:注释管理器初始化(在Cornerstone初始化之后) await initializeAnnotationManager(); setIsI18nReady(true); } catch (error) { console.error('应用初始化失败:', error); // 显示配置对话框,让用户重新配置 setShowConfigModal(true); } }; // ... 现有代码保持不变 ... console.log('当前语言:', currentLocale); console.log('messages', messages); // children 是将要会渲染的页面 return ( ) || {}} > {/* 加载状态覆盖层 */} {(loading || (!isI18nReady && !connectionChecked)) && (
加载多语言资源中...
)} {/* 服务器配置对话框 */} setShowConfigModal(false)} /> {/* 错误状态覆盖层 */} {error && (
多语言资源加载失败: {error}
)} {/* children 始终被渲染,满足 Taro 框架要求 */}
dispatch(setFeatureNotAvailableOpen(false))} onContinue={() => { dispatch(setFeatureNotAvailableOpen(false)); dispatch(setBusinessFlow('continueAfterFeatureNotAvailable')); }} /> {children} {process.env.NODE_ENV === 'development' && }
); } function App({ children }: { children: ReactNode }): JSX.Element { // 只在 Cypress 测试环境下暴露 store 到 window 对象 if ( typeof window !== 'undefined' && (window as unknown as { Cypress: unknown }).Cypress ) { (window as unknown as { store: typeof store }).store = store; } return ( {children} ); } export default App; ``` ### 🔄 集成逻辑说明 1. **导入注释管理器**: ```typescript import { initializeAnnotationManager, cleanupAnnotationManager } from '@/features/imageAnnotation'; ``` 2. **在应用初始化时启动**: ```typescript // 在产品状态和国际化初始化完成后启动注释管理器 await initializeAnnotationManager(); ``` 3. **应用退出时清理**: ```typescript useEffect(() => { // 初始化逻辑... return () => { // 应用退出时清理注释管理器 cleanupAnnotationManager().catch(console.error); }; }, []); ``` ### 💡 优势分析 - **✅ 零侵入**:只需修改app.tsx一处,无需改动其他现有代码 - **✅ 自动生效**:注释功能自动对所有图像和工具生效 - **✅ 生命周期安全**:正确处理应用的初始化和退出 - **✅ 向后兼容**:现有功能完全不受影响 ### 📁 建议的文件夹结构 基于项目的feature-based架构,新功能应该放置在独立的feature文件夹中: ``` src/features/ ├── imageAnnotation/ # 图像注释功能模块 │ ├── index.ts # 模块导出 │ ├── components/ # UI组件 │ │ ├── AnnotationManager.tsx # 注释管理组件 │ │ └── AnnotationStatus.tsx # 注释状态显示组件 │ ├── services/ # 业务逻辑服务 │ │ ├── AnnotationAPI.ts # API调用封装 │ │ ├── AnnotationSerializer.ts # 注释序列化/反序列化 │ │ └── AnnotationValidator.ts # 数据验证 │ ├── state/ # Redux状态管理 │ │ ├── annotationSlice.ts # 注释状态管理 │ │ └── annotationThunks.ts # 异步操作 │ ├── types/ # TypeScript类型定义 │ │ ├── annotation.ts # 注释数据类型 │ │ └── api.ts # API接口类型 │ └── utils/ # 工具函数 │ ├── coordinateMapper.ts # 坐标系转换 │ ├── eventHandlers.ts # 事件处理 │ └── storage.ts # 本地存储 └── serverConfig/ # 现有功能 ``` ### 🔄 与现有代码的集成方案 #### 现有代码分析 **当前注释相关实现:** - `MarkPanel.tsx` - 纯UI组件,只管理标记文本,没有后端交互 - `markPanelSlice.ts` - Redux状态管理,只处理UI状态 - Cornerstone Tools - 已实现各种注释工具,但没有持久化 **集成影响评估:** 1. **✅ 低影响集成点** - 扩展`markPanelSlice.ts`添加注释持久化状态 - 在`ImageOperationPanel.tsx`和`MeasurementPanel.tsx`中添加保存触发逻辑 - 添加新的API调用函数 2. **⚠️ 中等影响点** - 修改MarkPanel组件集成注释管理器 - 在图像加载流程中添加注释加载逻辑 3. **❌ 最小化核心修改** - 不修改现有Cornerstone Tools实现 - 不破坏现有UI交互逻辑 - 保持向后兼容性 #### 集成策略 **全局事件监听方案(推荐):** 基于我们的讨论,最终采用**全局事件监听**策略,只需要修改`src/app.tsx`即可集成注释管理器,无需修改其他现有代码: ```typescript // src/app.tsx - 唯一需要修改的文件 import { initializeAnnotationManager, cleanupAnnotationManager } from '@/features/imageAnnotation'; // 在initializeApp函数中添加注释管理器初始化 const initializeApp = async () => { try { const productState = await dispatch(initializeProductState()).unwrap(); const languageCode = productState.language; await dispatch(loadI18nMessages(languageCode)).unwrap(); // ✅ 初始化注释管理器(在产品状态和国际化之后) await initializeAnnotationManager(); setIsI18nReady(true); } catch (error) { console.error('应用初始化失败:', error); setShowConfigModal(true); } }; // 在useEffect cleanup中添加注释管理器清理 useEffect(() => { // ... 现有初始化逻辑 ... // 应用退出时清理注释管理器 return () => { cleanupAnnotationManager().catch(console.error); }; }, [dispatch]); ``` **为什么不修改markPanelSlice?** 经过分析,`markPanelSlice`目前只管理UI状态(自定义标记列表、输入框状态等),注释数据应该: - 直接从Cornerstone Tools API获取(运行时状态) - 通过注释管理器处理持久化 - 不应与UI状态管理器混合 **为什么ImageViewerComponent不存在?** 经过代码分析,实际的图像查看组件是`ViewerContainer.tsx`,它负责: - 管理多个图像的网格布局显示 - 处理各种图像操作(测量、标记、变换等) - 响应Redux action并调用相应的Cornerstone函数 全局事件监听方案的优势: - ✅ **零侵入**:只需修改app.tsx一处 - ✅ **自动生效**:注释功能对所有图像和工具自动生效 - ✅ **生命周期安全**:正确处理应用的初始化和退出 - ✅ **完全解耦**:注释逻辑与现有UI逻辑完全分离 #### API集成方案 **新增API文件:** ```typescript // src/API/annotation.ts (新增) import { request } from './interceptor'; export const annotationAPI = { // 获取图像注释 getAnnotations: (imageId: string) => request.get(`/dr/api/v1/auth/image/${imageId}/annotation`), // 保存图像注释 saveAnnotations: (imageId: string, data: any) => request.post(`/dr/api/v1/auth/image/${imageId}/annotation`, data), }; ``` #### 状态管理扩展 无 #### 组件集成方案 **最小化侵入式集成:** ### 📊 影响评估矩阵 基于全局事件监听方案,最终实现**零侵入集成**: | 模块 | 修改类型 | 影响程度 | 向后兼容性 | 备注 | |------|----------|----------|------------|------| | `app.tsx` | **新增2行导入 + 2行代码** | **极低** | ✅ 完全兼容 | 唯一修改点 | | `MarkPanel.tsx` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有UI功能 | | `markPanelSlice.ts` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有状态管理 | | `ImageOperationPanel.tsx` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有操作逻辑 | | `MeasurementPanel.tsx` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有测量功能 | | `ViewerContainer.tsx` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有显示逻辑 | | `Cornerstone Tools` | **无需修改** | **无** | ✅ 完全兼容 | 保持现有实现 | | `API层` | **新增文件** | **无** | ✅ 不影响现有 | 新增注释API | | `类型定义` | **扩展** | **低** | ✅ 完全兼容 | 添加注释类型 | ### 🚀 实施路线图 #### Phase 1: 基础设施搭建 (1-2天) - [ ] 创建`src/features/imageAnnotation`文件夹结构 - [ ] 定义注释数据类型和API接口 - [ ] 实现基础的注释序列化/反序列化逻辑 #### Phase 2: 核心功能实现 (2-3天) - [ ] 实现注释保存和加载逻辑 - [ ] 实现注释变化监控机制 - [ ] 实现注释渲染和重现功能 #### Phase 3: UI集成 (1-2天) - [ ] 在操作面板中集成注释管理器 - [ ] 添加错误处理和用户反馈 #### Phase 4: 测试和优化 (1-2天) - [ ] 编写单元测试和集成测试 - [ ] 性能优化和错误处理完善 - [ ] 文档更新和代码审查 ### 💡 设计原则 1. **渐进式增强**:在不破坏现有功能的前提下添加新功能 2. **关注点分离**:注释持久化逻辑与现有UI逻辑解耦 3. **向后兼容**:确保现有代码无需修改即可正常工作 4. **可扩展性**:架构设计支持未来功能扩展 5. **性能优化**:实现缓存、批量操作等优化策略 这个方案确保了新功能能够平滑集成到现有系统中,同时将影响降到最低。