|
@@ -0,0 +1,167 @@
|
|
|
+# 体位选择与Viewer同步修复文档
|
|
|
+
|
|
|
+## 问题描述
|
|
|
+在使用图像处理页面时,切换到1x1布局后,viewer总是显示第一个图像,而非选中体位对应图像。同时,发现切换体位时会导致无限渲染循环错误。
|
|
|
+
|
|
|
+### 表现症状
|
|
|
+1. 切换到1x1布局时,总是选择第一个图像
|
|
|
+2. 体位选择时出现无限渲染循环:"Maximum update depth exceeded"
|
|
|
+3. 1x1布局无法正确显示对应选中的体位图像
|
|
|
+
|
|
|
+## 根本原因分析
|
|
|
+
|
|
|
+### 1. ViewerContainer.tsx 无限循环问题
|
|
|
+- **现象**:renderGrid函数中在渲染期间dispatch状态变化
|
|
|
+- **问题**:
|
|
|
+ - useEffect监听selectedBodyPosition和gridLayout,每次变化时调用renderGrid()
|
|
|
+ - renderGrid()中1x1 case内dispatch toggleViewerSelection
|
|
|
+ - dispatch导致组件重新渲染,又触发useEffect → renderGrid() → dispatch → ...
|
|
|
+- **结果**:无限渲染循环,导致应用卡死
|
|
|
+
|
|
|
+### 2. Viewer选择逻辑问题
|
|
|
+- **现象**:1x1布局硬编码选择allViewerUrls[0]
|
|
|
+- **问题**:未与selectedBodyPosition关联
|
|
|
+- **结果**:切换布局时无法保持选中体位的高亮状态,且不显示对应图像
|
|
|
+
|
|
|
+### 3. 显示逻辑问题
|
|
|
+- **现象**:renderViewers(0, 1) 总是显示图像数组的第一个元素
|
|
|
+- **问题**:未利用selectedBodyPosition确定显示的图像索引
|
|
|
+- **结果**:1x1布局锁定了第一个图像,无法同步到选中体位
|
|
|
+
|
|
|
+### 4. 数据流分析
|
|
|
+```
|
|
|
+BodyPositionList 点击 → selectedBodyPosition 更新
|
|
|
+ ↓
|
|
|
+ViewerContainer useEffect触发 → renderGrid() → dispatch → 重新渲染
|
|
|
+ ↓
|
|
|
+无限循环异常
|
|
|
+ ↓
|
|
|
+布局切换 → 固定选择第一个图像
|
|
|
+```
|
|
|
+
|
|
|
+## 修复方案
|
|
|
+
|
|
|
+### 修改1: 移除引发无限循环的useEffect
|
|
|
+**文件**: `src/pages/view/components/ViewerContainer.tsx`
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 移除以下useEffect,因为它会导致renderGrid在每次渲染时被调用
|
|
|
+// 删除此useEffect以避免循环
|
|
|
+```
|
|
|
+
|
|
|
+**变更点**:
|
|
|
+- 删除 `useEffect(() => { renderGrid(); }, [selectedBodyPosition, gridLayout])`
|
|
|
+- 避免在渲染前调用renderGrid导致的循环
|
|
|
+
|
|
|
+### 修改2: 添加状态检查,避免重复dispatch
|
|
|
+**文件**: `src/pages/view/components/ViewerContainer.tsx`
|
|
|
+
|
|
|
+在1x1 case中添加状态检查逻辑:
|
|
|
+
|
|
|
+```typescript
|
|
|
+case '1x1': {
|
|
|
+ // 根据 selectedBodyPosition 找到对应URL
|
|
|
+ let urlToSelect = allViewerUrls[0]; // fallback
|
|
|
+
|
|
|
+ if (selectedBodyPosition?.sop_instance_uid) {
|
|
|
+ const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
|
|
|
+ const urlIndex = allViewerUrls.indexOf(correspondingUrl);
|
|
|
+ if (urlIndex !== -1) {
|
|
|
+ urlToSelect = allViewerUrls[urlIndex];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查当前选中状态,避免重复dispatch导致的无限循环
|
|
|
+ const currentSelected = selectedViewerUrls[0];
|
|
|
+ if (currentSelected !== urlToSelect) {
|
|
|
+ if (currentSelected) {
|
|
|
+ dispatch(toggleViewerSelection(currentSelected)); // 取消选中当前
|
|
|
+ }
|
|
|
+ dispatch(toggleViewerSelection(urlToSelect)); // 选中新的
|
|
|
+ }
|
|
|
+
|
|
|
+ // 修改renderViewers调用,根据selectedBodyPosition确定显示的图像索引
|
|
|
+ let viewerIndex = 0;
|
|
|
+ if (selectedBodyPosition?.sop_instance_uid) {
|
|
|
+ const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
|
|
|
+ const index = imageUrls.indexOf(correspondingUrl);
|
|
|
+ if (index !== -1) {
|
|
|
+ viewerIndex = index;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="h-full w-full" style={{ display: 'grid', gridTemplateColumns: '1fr' }}>
|
|
|
+ {renderViewers(viewerIndex, viewerIndex + 1)}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**变更点**:
|
|
|
+- 添加状态检查:只有当选中状态不同时才dispatch,避免重复触发
|
|
|
+- 修改renderViewers:根据selectedBodyPosition找到对应图像索引,确保1x1布局显示正确的图像
|
|
|
+- 导入 `getDcmImageUrl` 函数用于URL匹配
|
|
|
+
|
|
|
+## 技术细节
|
|
|
+
|
|
|
+### URL映射原理
|
|
|
+- `sop_instance_uid` → `getDcmImageUrl()` → 生成唯一URL
|
|
|
+- `allViewerUrls`/`imageUrls` 数组查找 → 找到索引 → 选择/display对应viewer/图像
|
|
|
+- 确保了 sop_instance_uid ↔ image URL 的一一对应
|
|
|
+
|
|
|
+### 边界情况处理
|
|
|
+- 未选择体位:使用默认索引0(通常是第一个曝光图像)
|
|
|
+- 体位未曝光:如果某一索引对应未曝光URL,则渲染为空,但实际多是曝光体位
|
|
|
+- 多个曝光体位:1x1布局会根据选中体位显示对应的单个图像
|
|
|
+
|
|
|
+### 状态更新链路
|
|
|
+```
|
|
|
+点击体位 → selectedBodyPosition change → ViewerContainer 检测到变化
|
|
|
+ ↓
|
|
|
+renderGrid re-render → 重新计算选择和显示逻辑
|
|
|
+ ↓
|
|
|
+1x1布局 → 选择正确viewer + 显示对应图像
|
|
|
+```
|
|
|
+
|
|
|
+## 测试验证
|
|
|
+
|
|
|
+### 预期行为
|
|
|
+1. **避免无限循环**:体位选择不再出现"Maximum update depth exceeded"错误
|
|
|
+2. **选择同步**:切换到1x1时,高亮选中体位对应的viewer
|
|
|
+3. **图像显示**:1x1布局显示selectedBodyPosition对应的图像,而非第一个
|
|
|
+4. **边缘情况**:未选择体位时,使用默认fallback (索引0)
|
|
|
+
|
|
|
+### 控制台日志
|
|
|
+- `[ViewerContainer]` - viewer容器渲染日志
|
|
|
+- SOP Instance UID匹配检查 - 确认URL映射是否正确
|
|
|
+- 选中状态变化日志 - 验证dispatch逻辑
|
|
|
+
|
|
|
+## 实现优势
|
|
|
+
|
|
|
+### 修复循环问题
|
|
|
+- 移除了不当的useEffect调用,避免渲染期间dispatch
|
|
|
+- 添加条件检查,防止重复状态修改触发循环
|
|
|
+- 维持单一渲染循环,提高应用稳定性
|
|
|
+
|
|
|
+### 保持架构一致性
|
|
|
+- 利用现有Redux状态管理系统
|
|
|
+- 不新增事件或全局变量
|
|
|
+- 使用组件渲染 Circ响应式更新
|
|
|
+
|
|
|
+### 性能考虑
|
|
|
+- 数组操作查找时间与曝光体位数相关
|
|
|
+- 只在必要时执行状态修改,避免多余render
|
|
|
+- 避免频繁的DOM更新
|
|
|
+
|
|
|
+### 维护性
|
|
|
+- 逻辑集中于renderGrid函数中,便于排查
|
|
|
+- 清晰的状态流转,有助于理解组件行为
|
|
|
+- 边界情况处理明确,易于扩展
|
|
|
+
|
|
|
+## 总结
|
|
|
+通过两个关键修复解决了ViewerContainer组件的问题:
|
|
|
+- **修复无限循环**:移除useEffect引起的渲染循环,添加条件dispatch防止无限调用
|
|
|
+- **完善选择逻辑**:根据selectedBodyPosition智能选择viewer并正确显示对应的图像
|
|
|
+
|
|
|
+修复确保了体位选择时应用的稳定性和正确显示,为用户提供可靠的图像查看体验。若需要进一步优化为仅显示选中图像,可考虑修改ImageControl组件。
|