|
@@ -0,0 +1,582 @@
|
|
|
+# 选中后显示患者照片功能实现文档
|
|
|
+
|
|
|
+## 📋 功能概述
|
|
|
+
|
|
|
+在 worklist 或 historylist 中,当表格行被点击后,在页面右上角浮动显示被点击患者的照片。照片组件支持拖拽调整位置,显示 3 秒后自动关闭,同时提供手动关闭按钮。
|
|
|
+
|
|
|
+## 🎯 涉及的参与者
|
|
|
+
|
|
|
+### 1. 数据层(Domain)
|
|
|
+- **`src/domain/work.ts`** - Task 接口
|
|
|
+ - 添加 `portrait_status?: string` - 照片保存状态("Saved" | "NotSaved" | "")
|
|
|
+ - 添加 `portrait_file?: string` - 照片文件名
|
|
|
+
|
|
|
+### 2. API 层
|
|
|
+- **`src/API/patient/portraitActions.ts`** - 照片相关 API
|
|
|
+ - `getPortraitUrl(filename: string)` - 构建照片完整 URL
|
|
|
+ - `hasPortrait(portraitStatus?: string)` - 检查照片是否可用
|
|
|
+
|
|
|
+### 3. 状态管理层(Redux)
|
|
|
+- **`src/states/patient/worklist/slices/selectedPatientSlice.ts`** - 选中患者状态管理
|
|
|
+ - 监听 `workSelectionSlice` 的选中变化
|
|
|
+ - 监听 `historySelectionSlice` 的选中变化
|
|
|
+ - 提供清除选中状态的 action
|
|
|
+- **`src/states/store.ts`** - 注册新的 reducer
|
|
|
+
|
|
|
+### 4. 组件层(UI)
|
|
|
+- **`src/pages/patient/components/PatientPortraitFloat.tsx`** - 浮动照片组件
|
|
|
+ - 支持拖拽调整位置
|
|
|
+ - 3 秒自动关闭
|
|
|
+ - 手动关闭按钮
|
|
|
+ - 加载状态和错误处理
|
|
|
+- **`src/pages/patient/worklist.tsx`** - Worklist 页面(集成照片组件)
|
|
|
+- **`src/pages/patient/HistoryList.tsx`** - HistoryList 页面(集成照片组件)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📊 系统架构图
|
|
|
+
|
|
|
+### 类图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+classDiagram
|
|
|
+ class Task {
|
|
|
+ +string StudyID
|
|
|
+ +string PatientName
|
|
|
+ +string? portrait_status
|
|
|
+ +string? portrait_file
|
|
|
+ }
|
|
|
+
|
|
|
+ class SelectedPatientState {
|
|
|
+ +string? patientId
|
|
|
+ +string? patientName
|
|
|
+ +string? portraitStatus
|
|
|
+ +string? portraitFile
|
|
|
+ }
|
|
|
+
|
|
|
+ class PatientPortraitFloat {
|
|
|
+ -boolean isVisible
|
|
|
+ -boolean imageLoading
|
|
|
+ -boolean imageError
|
|
|
+ -string imageUrl
|
|
|
+ -Position position
|
|
|
+ -boolean isDragging
|
|
|
+ +handleClose()
|
|
|
+ +handleMouseDown()
|
|
|
+ }
|
|
|
+
|
|
|
+ class portraitActions {
|
|
|
+ +getPortraitUrl(filename: string)
|
|
|
+ +hasPortrait(status?: string)
|
|
|
+ }
|
|
|
+
|
|
|
+ Task --> SelectedPatientState : provides data
|
|
|
+ SelectedPatientState --> PatientPortraitFloat : state
|
|
|
+ PatientPortraitFloat --> portraitActions : uses
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔄 交互流程
|
|
|
+
|
|
|
+### 序列图 - 用户点击表格行显示照片
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant User as 用户
|
|
|
+ participant UI as WorkList/HistoryList
|
|
|
+ participant Handler as handleRowClick
|
|
|
+ participant WorkSel as workSelectionSlice
|
|
|
+ participant PatientSel as selectedPatientSlice
|
|
|
+ participant Float as PatientPortraitFloat
|
|
|
+ participant API as Portrait API
|
|
|
+ participant Backend as 后端服务器
|
|
|
+
|
|
|
+ User->>UI: 点击表格行
|
|
|
+ UI->>Handler: 触发 handleRowClick(record)
|
|
|
+ Handler->>WorkSel: dispatch(setSelectedIds([id]))
|
|
|
+
|
|
|
+ Note over WorkSel,PatientSel: 通过 extraReducers 监听
|
|
|
+ WorkSel->>PatientSel: 自动触发状态更新
|
|
|
+ PatientSel->>PatientSel: 保存 patientId
|
|
|
+
|
|
|
+ PatientSel->>Float: state 变化触发重渲染
|
|
|
+ Float->>Float: 从 entities 获取完整 Task 数据
|
|
|
+
|
|
|
+ alt portrait_status === "Saved"
|
|
|
+ Float->>API: getPortraitUrl(portrait_file)
|
|
|
+ API-->>Float: 返回完整 URL
|
|
|
+ Float->>Backend: GET /api/v1/pub/capture/{filename}
|
|
|
+ Backend-->>Float: 返回图像文件
|
|
|
+ Float->>Float: 设置 imageUrl,显示加载状态
|
|
|
+ Float->>User: 显示患者照片
|
|
|
+
|
|
|
+ Note over Float: 启动 3 秒定时器
|
|
|
+ Float->>Float: setTimeout(handleClose, 3000)
|
|
|
+
|
|
|
+ alt 用户点击关闭按钮
|
|
|
+ User->>Float: 点击关闭按钮
|
|
|
+ Float->>Float: handleClose()
|
|
|
+ Float->>User: 隐藏组件
|
|
|
+ else 3 秒后自动关闭
|
|
|
+ Float->>Float: 定时器触发 handleClose()
|
|
|
+ Float->>User: 隐藏组件
|
|
|
+ end
|
|
|
+ else portrait_status !== "Saved"
|
|
|
+ Float->>User: 不显示(或显示占位符)
|
|
|
+ end
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📈 数据流图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TD
|
|
|
+ A[后端 API] -->|返回 Study 数据<br/>含 portrait_status & portrait_file| B[fetchWorkThunk]
|
|
|
+ B -->|存储数据| C[Redux Store]
|
|
|
+
|
|
|
+ C -->|workEntities| D1[WorklistTable]
|
|
|
+ C -->|historyEntities| D2[HistorylistTable]
|
|
|
+
|
|
|
+ D1 -->|用户点击行| E1[handleRowClick]
|
|
|
+ D2 -->|用户点击行| E2[handleRowClick]
|
|
|
+
|
|
|
+ E1 -->|dispatch setSelectedIds| F1[workSelectionSlice]
|
|
|
+ E2 -->|dispatch setSelectedIds| F2[historySelectionSlice]
|
|
|
+
|
|
|
+ F1 -->|extraReducers 监听| G[selectedPatientSlice]
|
|
|
+ F2 -->|extraReducers 监听| G
|
|
|
+
|
|
|
+ G -->|更新 patientId| H[PatientPortraitFloat 组件]
|
|
|
+
|
|
|
+ H -->|根据 patientId| I{从 entities<br/>查找 Task}
|
|
|
+
|
|
|
+ I -->|找到| J{检查<br/>portrait_status}
|
|
|
+ I -->|未找到| K[不显示]
|
|
|
+
|
|
|
+ J -->|Saved| L[构建图片 URL]
|
|
|
+ J -->|NotSaved/空| K
|
|
|
+
|
|
|
+ L -->|/api/v1/pub/capture/filename| M[加载并显示照片]
|
|
|
+ M -->|3 秒后| N[自动关闭]
|
|
|
+ M -->|用户点击| O[手动关闭]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🔄 状态同步机制流程图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph LR
|
|
|
+ A[用户操作] --> B{在哪个页面?}
|
|
|
+
|
|
|
+ B -->|Worklist| C[workSelectionSlice<br/>setSelectedIds]
|
|
|
+ B -->|HistoryList| D[historySelectionSlice<br/>setSelectedIds]
|
|
|
+
|
|
|
+ C --> E[selectedPatientSlice<br/>监听 action]
|
|
|
+ D --> E
|
|
|
+
|
|
|
+ E --> F{选中 ID 数量}
|
|
|
+
|
|
|
+ F -->|0| G[清空状态]
|
|
|
+ F -->|>0| H[保存 patientId]
|
|
|
+
|
|
|
+ H --> I[PatientPortraitFloat<br/>读取 state]
|
|
|
+
|
|
|
+ I --> J[从对应的<br/>entities 获取数据]
|
|
|
+
|
|
|
+ J --> K{有照片?}
|
|
|
+
|
|
|
+ K -->|是| L[显示照片<br/>启动定时器]
|
|
|
+ K -->|否| M[不显示]
|
|
|
+
|
|
|
+ L --> N[3 秒后自动关闭]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🗂️ 关键数据结构
|
|
|
+
|
|
|
+### Task 接口(扩展)
|
|
|
+
|
|
|
+```typescript
|
|
|
+export interface Task {
|
|
|
+ // ... 现有字段
|
|
|
+
|
|
|
+ /**---患者照片相关字段--- */
|
|
|
+ portrait_status?: string; // "Saved" | "NotSaved" | ""
|
|
|
+ portrait_file?: string; // 照片文件名,如 "xxx.dcm"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### SelectedPatientState
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface SelectedPatientState {
|
|
|
+ patientId: string | null; // 选中患者的 Study ID
|
|
|
+ patientName: string | null; // 患者姓名(保留字段)
|
|
|
+ portraitStatus: string | null; // 照片状态(保留字段)
|
|
|
+ portraitFile: string | null; // 照片文件名(保留字段)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### API 响应示例
|
|
|
+
|
|
|
+根据 API 文档 `13.5 获取图像(pub)`:
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/v1/pub/capture/{filename}
|
|
|
+```
|
|
|
+
|
|
|
+其中 `filename` 来自 `Task.portrait_file` 字段。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🚀 执行流程
|
|
|
+
|
|
|
+### 起点:用户点击表格行
|
|
|
+
|
|
|
+1. **用户操作**
|
|
|
+ - 用户在 worklist 或 historylist 中点击某一行
|
|
|
+
|
|
|
+2. **触发事件**
|
|
|
+ - `handleRowClick(record: Task)` 被调用
|
|
|
+
|
|
|
+3. **更新选中状态**
|
|
|
+ - dispatch `workSelectionSlice.actions.setSelectedIds([record.StudyID])`
|
|
|
+ - 或 dispatch `historySelectionSlice.actions.setSelectedIds([record.StudyID])`
|
|
|
+
|
|
|
+4. **状态自动同步**
|
|
|
+ - `selectedPatientSlice` 通过 `extraReducers` 监听到变化
|
|
|
+ - 自动更新 `patientId`
|
|
|
+
|
|
|
+5. **组件响应**
|
|
|
+ - `PatientPortraitFloat` 组件监听 `selectedPatient` state
|
|
|
+ - 通过 `useMemo` 从 `workEntities` 或 `historyEntities` 获取完整的 Task 数据
|
|
|
+
|
|
|
+6. **条件渲染**
|
|
|
+ - 如果 `portrait_status === "Saved"`,显示照片
|
|
|
+ - 否则,不显示
|
|
|
+
|
|
|
+7. **加载照片**
|
|
|
+ - 调用 `getPortraitUrl(portrait_file)` 构建完整 URL
|
|
|
+ - 使用 `<img>` 标签加载照片
|
|
|
+ - 显示加载状态(Spin)
|
|
|
+
|
|
|
+8. **自动关闭**
|
|
|
+ - 启动 3 秒定时器
|
|
|
+ - 定时器到期后调用 `handleClose()`
|
|
|
+
|
|
|
+9. **手动关闭**
|
|
|
+ - 用户点击关闭按钮
|
|
|
+ - 立即调用 `handleClose()`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🧪 测试方案
|
|
|
+
|
|
|
+### 功能测试场景
|
|
|
+
|
|
|
+#### 1. 基础显示功能
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| 有照片的患者 | 1. 点击有照片的患者行<br/>2. 观察右上角 | 应显示浮动照片组件,显示患者照片 |
|
|
|
+| 无照片的患者 | 1. 点击无照片的患者行<br/>2. 观察右上角 | 不应显示浮动组件 |
|
|
|
+| 照片加载中 | 1. 点击有照片的患者行<br/>2. 立即观察 | 应显示 loading 状态(转圈图标) |
|
|
|
+| 照片加载失败 | 1. 模拟网络错误<br/>2. 点击患者行 | 应显示错误提示图标和文字 |
|
|
|
+
|
|
|
+#### 2. 自动关闭功能
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| 3 秒自动关闭 | 1. 点击有照片的患者行<br/>2. 等待 3 秒 | 照片应自动消失 |
|
|
|
+| 切换患者重置定时器 | 1. 点击患者 A<br/>2. 2 秒后点击患者 B | 患者 B 的照片显示,定时器重新计时 3 秒 |
|
|
|
+
|
|
|
+#### 3. 手动关闭功能
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| 点击关闭按钮 | 1. 点击有照片的患者行<br/>2. 点击关闭按钮(X) | 照片立即消失 |
|
|
|
+| 关闭后定时器清除 | 1. 点击有照片的患者行<br/>2. 1 秒后手动关闭<br/>3. 等待 3 秒 | 照片不应再次出现 |
|
|
|
+
|
|
|
+#### 4. 拖拽功能
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| 拖拽移动位置 | 1. 显示照片<br/>2. 鼠标按住拖拽 | 照片应跟随鼠标移动 |
|
|
|
+| 拖拽到边界 | 1. 显示照片<br/>2. 尝试拖出窗口范围 | 照片应被限制在窗口内 |
|
|
|
+| 拖拽时鼠标样式 | 1. 显示照片<br/>2. 鼠标悬停 | 鼠标应显示 grab 光标 |
|
|
|
+| 拖拽中鼠标样式 | 1. 显示照片<br/>2. 拖拽中 | 鼠标应显示 grabbing 光标 |
|
|
|
+
|
|
|
+#### 5. 跨页面功能
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| Worklist 中选中 | 1. 在 worklist 点击患者行 | 显示照片 |
|
|
|
+| HistoryList 中选中 | 1. 在 historylist 点击患者行 | 显示照片 |
|
|
|
+| 跨页面切换 | 1. 在 worklist 选中患者 A<br/>2. 切换到 historylist | 照片应消失或保持显示患者 A |
|
|
|
+
|
|
|
+#### 6. 多选场景
|
|
|
+
|
|
|
+| 测试场景 | 操作步骤 | 预期结果 |
|
|
|
+|---------|---------|---------|
|
|
|
+| 快速切换选中 | 1. 快速连续点击多个患者行 | 只显示最后一个患者的照片 |
|
|
|
+| 取消选中 | 1. 点击患者行<br/>2. 再次点击同一行取消选中 | 照片应消失 |
|
|
|
+
|
|
|
+### 测试数据准备
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 1. 有照片的测试数据
|
|
|
+const taskWithPortrait: Task = {
|
|
|
+ StudyID: "TEST001",
|
|
|
+ PatientName: "张三",
|
|
|
+ portrait_status: "Saved",
|
|
|
+ portrait_file: "test-portrait-001.dcm",
|
|
|
+ // ... 其他必需字段
|
|
|
+};
|
|
|
+
|
|
|
+// 2. 无照片的测试数据(状态为 NotSaved)
|
|
|
+const taskWithoutPortrait1: Task = {
|
|
|
+ StudyID: "TEST002",
|
|
|
+ PatientName: "李四",
|
|
|
+ portrait_status: "NotSaved",
|
|
|
+ portrait_file: "",
|
|
|
+ // ... 其他必需字段
|
|
|
+};
|
|
|
+
|
|
|
+// 3. 无照片的测试数据(字段为空)
|
|
|
+const taskWithoutPortrait2: Task = {
|
|
|
+ StudyID: "TEST003",
|
|
|
+ PatientName: "王五",
|
|
|
+ portrait_status: "",
|
|
|
+ portrait_file: "",
|
|
|
+ // ... 其他必需字段
|
|
|
+};
|
|
|
+
|
|
|
+// 4. 照片文件名不存在(测试错误处理)
|
|
|
+const taskWithInvalidPortrait: Task = {
|
|
|
+ StudyID: "TEST004",
|
|
|
+ PatientName: "赵六",
|
|
|
+ portrait_status: "Saved",
|
|
|
+ portrait_file: "non-existent-file.dcm",
|
|
|
+ // ... 其他必需字段
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🐛 潜在问题和边界情况分析
|
|
|
+
|
|
|
+### 1. 数据异常处理
|
|
|
+
|
|
|
+#### 问题 1.1:`portrait_file` 为空或格式错误
|
|
|
+**影响**:可能导致请求错误的 URL
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+```typescript
|
|
|
+// 在 getPortraitUrl 中添加验证
|
|
|
+export const getPortraitUrl = (filename: string): string => {
|
|
|
+ if (!filename || filename.trim() === '') {
|
|
|
+ return ''; // 返回空字符串,组件不显示
|
|
|
+ }
|
|
|
+ return `${IP_PORT}/api/v1/pub/capture/${filename}`;
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+#### 问题 1.2:后端返回的数据缺少照片字段
|
|
|
+**影响**:TypeScript 可选字段可能为 undefined
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 使用可选字段(`?`)定义
|
|
|
+- 在组件中使用 `hasPortrait()` 函数统一检查
|
|
|
+- 使用 `||` 提供默认值
|
|
|
+
|
|
|
+### 2. 网络问题
|
|
|
+
|
|
|
+#### 问题 2.1:图片加载超时
|
|
|
+**影响**:用户一直看到 loading 状态
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+```typescript
|
|
|
+// 添加超时处理
|
|
|
+const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
+
|
|
|
+useEffect(() => {
|
|
|
+ if (imageUrl) {
|
|
|
+ setImageLoading(true);
|
|
|
+
|
|
|
+ // 设置 10 秒超时
|
|
|
+ timeoutRef.current = setTimeout(() => {
|
|
|
+ setImageLoading(false);
|
|
|
+ setImageError(true);
|
|
|
+ }, 10000);
|
|
|
+ }
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ if (timeoutRef.current) {
|
|
|
+ clearTimeout(timeoutRef.current);
|
|
|
+ }
|
|
|
+ };
|
|
|
+}, [imageUrl]);
|
|
|
+```
|
|
|
+
|
|
|
+#### 问题 2.2:频繁切换导致多次请求
|
|
|
+**影响**:浪费带宽,可能导致显示错乱
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 图片请求由浏览器缓存处理
|
|
|
+- 可以添加请求取消机制(使用 AbortController)
|
|
|
+
|
|
|
+### 3. 性能问题
|
|
|
+
|
|
|
+#### 问题 3.1:大图片加载慢
|
|
|
+**影响**:影响用户体验
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 后端提供缩略图 API
|
|
|
+- 使用渐进式加载
|
|
|
+- 添加图片懒加载
|
|
|
+
|
|
|
+#### 问题 3.2:内存泄漏
|
|
|
+**影响**:长时间使用导致页面卡顿
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+```typescript
|
|
|
+// 清理定时器和事件监听
|
|
|
+useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ if (autoCloseTimerRef.current) {
|
|
|
+ clearTimeout(autoCloseTimerRef.current);
|
|
|
+ }
|
|
|
+ };
|
|
|
+}, []);
|
|
|
+
|
|
|
+// 清理拖拽事件
|
|
|
+useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ document.removeEventListener('mousemove', handleMouseMove);
|
|
|
+ document.removeEventListener('mouseup', handleMouseUp);
|
|
|
+ };
|
|
|
+}, [isDragging]);
|
|
|
+```
|
|
|
+
|
|
|
+### 4. 用户体验问题
|
|
|
+
|
|
|
+#### 问题 4.1:照片遮挡重要内容
|
|
|
+**影响**:用户无法操作被遮挡的区域
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 支持拖拽调整位置(已实现)
|
|
|
+- 3 秒自动关闭(已实现)
|
|
|
+- 提供手动关闭按钮(已实现)
|
|
|
+
|
|
|
+#### 问题 4.2:快速切换导致闪烁
|
|
|
+**影响**:视觉体验差
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+```typescript
|
|
|
+// 添加防抖延迟
|
|
|
+const debouncedPatientId = useDebounce(selectedPatientId, 200);
|
|
|
+```
|
|
|
+
|
|
|
+### 5. 兼容性问题
|
|
|
+
|
|
|
+#### 问题 5.1:老数据没有照片字段
|
|
|
+**影响**:可能显示 undefined
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 使用可选字段(`?`)
|
|
|
+- 提供默认值处理
|
|
|
+- 优雅降级,不影响现有功能
|
|
|
+
|
|
|
+#### 问题 5.2:不同浏览器拖拽行为差异
|
|
|
+**影响**:拖拽可能不流畅
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 使用标准的 `mousedown/mousemove/mouseup` 事件
|
|
|
+- 添加 `e.preventDefault()` 防止默认行为
|
|
|
+- 测试主流浏览器
|
|
|
+
|
|
|
+### 6. 状态同步问题
|
|
|
+
|
|
|
+#### 问题 6.1:从 worklist 切换到 historylist 时状态残留
|
|
|
+**影响**:可能显示错误的照片
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+- 在 `selectedPatientSlice` 中正确处理两个页面的选中事件
|
|
|
+- 确保组件从正确的 entities 获取数据
|
|
|
+- 页面卸载时清理状态(如果需要)
|
|
|
+
|
|
|
+#### 问题 6.2:选中状态与 entities 数据不同步
|
|
|
+**影响**:找不到对应的 Task 数据
|
|
|
+
|
|
|
+**解决方案**:
|
|
|
+```typescript
|
|
|
+// 使用 useMemo 确保及时更新
|
|
|
+const selectedPatient = React.useMemo(() => {
|
|
|
+ if (!selectedPatientId) return null;
|
|
|
+
|
|
|
+ const fromWork = workEntities.find(item => item.StudyID === selectedPatientId);
|
|
|
+ if (fromWork) return fromWork;
|
|
|
+
|
|
|
+ const fromHistory = historyEntities.find(item => item.StudyID === selectedPatientId);
|
|
|
+ return fromHistory || null;
|
|
|
+}, [selectedPatientId, workEntities, historyEntities]);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📝 实现要点总结
|
|
|
+
|
|
|
+### 1. 状态管理设计
|
|
|
+
|
|
|
+**设计思路**:使用 Redux 的 `extraReducers` 监听机制实现自动状态同步
|
|
|
+
|
|
|
+**优点**:
|
|
|
+- 解耦:不需要在每个 handleRowClick 中手动更新照片状态
|
|
|
+- 单向数据流:符合 Redux 设计原则
|
|
|
+- 易维护:新增页面时只需添加监听规则
|
|
|
+
|
|
|
+### 2. 组件设计
|
|
|
+
|
|
|
+**设计思路**:独立的浮动组件,通过 Redux state 驱动显示
|
|
|
+
|
|
|
+**优点**:
|
|
|
+- 可复用:可在任何页面使用
|
|
|
+- 独立性:不影响现有布局
|
|
|
+- 灵活性:支持拖拽和自定义位置
|
|
|
+
|
|
|
+### 3. API 设计
|
|
|
+
|
|
|
+**设计思路**:简单的工具函数,封装 URL 构建逻辑
|
|
|
+
|
|
|
+**优点**:
|
|
|
+- 简单:不需要复杂的请求逻辑
|
|
|
+- 可测试:纯函数易于测试
|
|
|
+- 可维护:API 变更只需修改一处
|
|
|
+
|
|
|
+### 4. 交互设计
|
|
|
+
|
|
|
+**核心功能**:
|
|
|
+1. ✅ 拖拽调整位置
|
|
|
+2. ✅ 3 秒自动关闭
|
|
|
+3. ✅ 手动关闭按钮
|
|
|
+4. ✅ 加载状态提示
|
|
|
+5. ✅ 错误处理
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🎉 总结
|
|
|
+
|
|
|
+本功能成功实现了以下目标:
|
|
|
+
|
|
|
+1. ✅ **数据模型扩展**:在 Task 接口中添加照片相关字段
|
|
|
+2. ✅ **状态管理**:创建独立的 selectedPatientSlice 监听选中变化
|
|
|
+3. ✅ **UI 组件**:实现功能完整的浮动照片组件
|
|
|
+4. ✅ **页面集成**:在 worklist 和 historylist 中集成组件
|
|
|
+5. ✅ **用户体验**:支持拖拽、自动关闭、手动关闭
|
|
|
+6. ✅ **错误处理**:完善的加载状态和错误提示
|
|
|
+
|
|
|
+该实现遵循了 React + Redux 的最佳实践,代码结构清晰,易于维护和扩展。
|