Переглянути джерело

feat: 实现图像状态控制功能(拒绝/恢复/另存为)

- 在 bodyPositionListSlice.ts 中添加 judgeImageThunk 异步操作和 loading/error 状态
- 创建 ImageStateControl.tsx 组件实现拒绝、恢复、另存为按钮
- 在 OperationPanel.tsx 中集成 ImageStateControl 并优化布局(添加分割线)
- 修正 BodyPositionList.tsx 和 ThumbnailList.tsx 中 judged_status 值为 'Reject'
- 添加完整的实现文档 docs/实现/图像状态控制功能.md

业务规则:
- 仅在单分格模式(1x1)下显示拒绝/恢复按钮
- 按钮基于 judged_status 互斥显示('Reject' 显示恢复,否则显示拒绝)
- 包含 loading 状态和错误处理

改动文件:
- docs/实现/图像状态控制功能.md (新增)
- src/pages/view/components/ImageStateControl.tsx (新增)
- src/pages/view/components/OperationPanel.tsx
- src/states/exam/bodyPositionListSlice.ts
- src/pages/exam/components/BodyPositionList.tsx
- src/pages/patient/components/ThumbnailList.tsx
sw 15 годин тому
батько
коміт
d6973d2bfe

+ 473 - 0
docs/实现/图像状态控制功能.md

@@ -0,0 +1,473 @@
+# 图像状态控制功能实现文档
+
+## 1. 功能概述
+
+在图像处理界面(view 模式)的 OperationPanel 底部,实现图像状态控制功能,包括:
+- **拒绝按钮**:将当前图像标记为拒绝状态
+- **恢复按钮**:将已拒绝的图像恢复为接受状态
+- **另存为按钮**:保存图像副本(UI 占位,暂不实现功能)
+
+### 业务规则
+1. 拒绝和恢复按钮互斥显示,基于当前选中图像的 `judged_status`
+2. 仅在单分格模式(1x1)下显示拒绝/恢复按钮
+3. 多分格模式下,拒绝/恢复按钮不可见
+4. 另存为按钮始终可见但禁用
+
+---
+
+## 2. 架构设计
+
+### 2.1 参与者
+
+#### UI 组件层
+- **OperationPanel** (`src/pages/view/components/OperationPanel.tsx`)
+  - 职责:父容器组件,Layout 结构
+  
+- **ImageStateControl** (`src/pages/view/components/ImageStateControl.tsx`) - **新建**
+  - 职责:图像状态控制组件,渲染按钮并处理用户操作
+
+#### 状态管理层(Redux)
+- **viewerContainerSlice** (`src/states/view/viewerContainerSlice.ts`)
+  - 职责:管理分格布局、选中的 viewer
+  - 提供:`selectGridLayout`, `selectSelectedViewers`
+
+- **bodyPositionListSlice** (`src/states/exam/bodyPositionListSlice.ts`) - **需修改**
+  - 职责:管理体位列表、图像数据、判断状态
+  - 新增:`judgeImageThunk` 异步操作
+  - 新增:loading/error 状态字段
+
+#### API 层
+- **judgeImage** (`src/API/exam/judgeImage.ts`)
+  - 已存在的接受/拒绝图像 API
+
+---
+
+## 3. 数据流设计
+
+### 3.1 数据流图
+
+```mermaid
+flowchart TD
+    A[用户点击拒绝/恢复按钮] --> B[ImageStateControl 组件]
+    B --> C{获取状态}
+    C --> D[viewerContainerSlice<br/>selectedViewers, gridLayout]
+    C --> E[bodyPositionList<br/>judged_status, sopInstanceUid]
+    
+    B --> F[dispatch judgeImageThunk]
+    F --> G[bodyPositionListSlice]
+    G --> H[调用 judgeImage API]
+    H --> I{API 结果}
+    
+    I -->|成功| J[extraReducers.fulfilled<br/>更新 judged_status]
+    I -->|失败| K[extraReducers.rejected<br/>设置 error]
+    
+    J --> L[Redux 通知状态变更]
+    K --> L
+    L --> B
+    B --> M[重新渲染按钮]
+```
+
+### 3.2 序列图
+
+```mermaid
+sequenceDiagram
+    participant User as 用户
+    participant UI as ImageStateControl
+    participant VCS as viewerContainerSlice
+    participant BPL as bodyPositionListSlice
+    participant API as judgeImage API
+
+    Note over UI: 组件初始化
+    UI->>VCS: selectSelectedViewers()
+    VCS-->>UI: [imageUrl]
+    UI->>VCS: selectGridLayout()
+    VCS-->>UI: '1x1'
+    UI->>BPL: 读取 bodyPositions
+    BPL-->>UI: {judged_status: '', sopInstanceUid}
+    
+    Note over UI: 决定按钮可见性
+    alt gridLayout === '1x1'
+        alt judged_status === 'Reject'
+            UI->>UI: 渲染"恢复"按钮
+        else
+            UI->>UI: 渲染"拒绝"按钮
+        end
+    else
+        UI->>UI: 不渲染拒绝/恢复按钮
+    end
+    
+    Note over User: 用户点击"拒绝"按钮
+    User->>UI: onClick
+    UI->>BPL: dispatch judgeImageThunk({sopInstanceUid, accept: false})
+    BPL->>BPL: pending → loading = true
+    BPL->>API: judgeImage(sopInstanceUid, false)
+    API-->>BPL: 成功
+    BPL->>BPL: fulfilled → 更新 judged_status = 'Reject'
+    BPL->>BPL: loading = false
+    BPL-->>UI: 状态变更通知
+    UI->>User: 显示成功提示
+    UI->>UI: 重新渲染,显示"恢复"按钮
+```
+
+---
+
+## 4. 数据结构设计
+
+### 4.1 judged_status 类型
+
+```typescript
+/**
+ * 图像判断状态
+ * - 'Accept': 已接受
+ * - 'Reject': 已拒绝
+ * - '': 未判断
+ */
+type ImageJudgedStatus = 'Accept' | 'Reject' | '';
+```
+
+### 4.2 bodyPositionListSlice 新增字段
+
+```typescript
+interface BodyPositionListState {
+  bodyPositions: BodyPosition[];
+  selectedBodyPosition: BodyPosition | null;
+  loading: boolean;  // 新增
+  error: string | null;  // 新增
+}
+```
+
+### 4.3 judgeImageThunk 参数
+
+```typescript
+interface JudgeImagePayload {
+  sopInstanceUid: string;
+  accept: boolean;
+}
+
+interface JudgeImageResult {
+  sopInstanceUid: string;
+  judgedStatus: ImageJudgedStatus;
+}
+```
+
+---
+
+## 5. 实现细节
+
+### 5.1 ImageStateControl 组件
+
+**文件**: `src/pages/view/components/ImageStateControl.tsx`
+
+**核心逻辑**:
+```typescript
+const ImageStateControl = () => {
+  const dispatch = useDispatch();
+  
+  // 获取状态
+  const selectedViewers = useSelector(selectSelectedViewers);
+  const gridLayout = useSelector(selectGridLayout);
+  const bodyPositionList = useSelector((state: RootState) => 
+    state.bodyPositionList.bodyPositions
+  );
+  const loading = useSelector((state: RootState) => 
+    state.bodyPositionList.loading
+  );
+
+  // 查找当前选中的图像
+  const selectedImage = useMemo(() => {
+    if (selectedViewers.length === 0) return null;
+    const imageUrl = selectedViewers[0];
+    return bodyPositionList.find(
+      bp => getDcmImageUrl(bp.sop_instance_uid) === imageUrl
+    );
+  }, [selectedViewers, bodyPositionList]);
+
+  // 判断按钮可见性
+  const isSingleGrid = gridLayout === '1x1';
+  const judgedStatus = selectedImage?.dview?.judged_status || '';
+  const showRejectButton = isSingleGrid && judgedStatus !== 'Reject';
+  const showRestoreButton = isSingleGrid && judgedStatus === 'Reject';
+
+  // 按钮处理函数
+  const handleReject = async () => {
+    if (!selectedImage) return;
+    try {
+      await dispatch(judgeImageThunk({
+        sopInstanceUid: selectedImage.sop_instance_uid,
+        accept: false
+      })).unwrap();
+      message.success('图像已拒绝');
+    } catch (err) {
+      message.error('拒绝图像失败');
+    }
+  };
+
+  const handleRestore = async () => {
+    if (!selectedImage) return;
+    try {
+      await dispatch(judgeImageThunk({
+        sopInstanceUid: selectedImage.sop_instance_uid,
+        accept: true
+      })).unwrap();
+      message.success('图像已恢复');
+    } catch (err) {
+      message.error('恢复图像失败');
+    }
+  };
+
+  return (
+    <Flex gap="small" align="center">
+      {showRejectButton && (
+        <Button
+          onClick={handleReject}
+          loading={loading}
+          icon={<Icon module="module-process" name="reject" userId="base" theme="default" size="2x" state="normal" />}
+          style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
+          title="拒绝"
+        />
+      )}
+      {showRestoreButton && (
+        <Button
+          onClick={handleRestore}
+          loading={loading}
+          icon={<Icon module="module-process" name="restore" userId="base" theme="default" size="2x" state="normal" />}
+          style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
+          title="恢复"
+        />
+      )}
+      <Button
+        disabled
+        icon={<Icon module="module-process" name="saveas" userId="base" theme="default" size="2x" state="normal" />}
+        style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
+        title="另存为"
+      />
+    </Flex>
+  );
+};
+```
+
+### 5.2 bodyPositionListSlice 修改
+
+**文件**: `src/states/exam/bodyPositionListSlice.ts`
+
+**新增 thunk**:
+```typescript
+import { judgeImage } from '@/API/exam/judgeImage';
+
+export const judgeImageThunk = createAsyncThunk(
+  'bodyPositionList/judgeImage',
+  async (payload: { sopInstanceUid: string; accept: boolean }) => {
+    await judgeImage(payload.sopInstanceUid, payload.accept);
+    return {
+      sopInstanceUid: payload.sopInstanceUid,
+      judgedStatus: payload.accept ? 'Accept' : 'Reject'
+    };
+  }
+);
+```
+
+**extraReducers**:
+```typescript
+extraReducers: (builder) => {
+  builder
+    .addCase(judgeImageThunk.pending, (state) => {
+      state.loading = true;
+      state.error = null;
+    })
+    .addCase(judgeImageThunk.fulfilled, (state, action) => {
+      state.loading = false;
+      const position = state.bodyPositions.find(
+        bp => bp.sop_instance_uid === action.payload.sopInstanceUid
+      );
+      if (position?.dview) {
+        position.dview.judged_status = action.payload.judgedStatus;
+      }
+    })
+    .addCase(judgeImageThunk.rejected, (state, action) => {
+      state.loading = false;
+      state.error = action.error.message || 'Failed to judge image';
+    });
+}
+```
+
+---
+
+## 6. 测试方案
+
+### 6.1 单元测试场景
+
+#### ImageStateControl 组件
+- ✅ 单分格 + 未判断状态 → 显示拒绝按钮
+- ✅ 单分格 + Accept 状态 → 显示拒绝按钮
+- ✅ 单分格 + Reject 状态 → 显示恢复按钮
+- ✅ 多分格 → 拒绝和恢复按钮都不可见
+- ✅ 另存为按钮始终可见但禁用
+- ✅ 点击拒绝 → 调用 judgeImageThunk (accept=false)
+- ✅ 点击恢复 → 调用 judgeImageThunk (accept=true)
+- ✅ loading 状态 → 按钮显示加载中
+
+#### bodyPositionListSlice
+- ✅ judgeImageThunk.pending → loading = true
+- ✅ judgeImageThunk.fulfilled → 更新 judged_status
+- ✅ judgeImageThunk.rejected → error 设置
+
+### 6.2 集成测试场景
+
+**场景 1: 拒绝图像完整流程**
+1. 初始状态:单分格,judgedStatus = ''
+2. 点击拒绝按钮
+3. API 调用成功
+4. judgedStatus 更新为 'Reject'
+5. 按钮切换为"恢复"
+
+**场景 2: 恢复图像完整流程**
+1. 初始状态:单分格,judgedStatus = 'Reject'
+2. 点击恢复按钮
+3. API 调用成功
+4. judgedStatus 更新为 'Accept'
+5. 按钮切换为"拒绝"
+
+**场景 3: 多分格场景**
+1. 设置 gridLayout = '2x2'
+2. 拒绝/恢复按钮不可见
+3. 仅显示另存为按钮
+
+**场景 4: API 失败处理**
+1. 模拟 API 调用失败
+2. 显示错误提示
+3. judgedStatus 保持不变
+
+### 6.3 E2E 测试
+
+```typescript
+describe('图像状态控制 E2E', () => {
+  it('完整拒绝和恢复流程', () => {
+    cy.navigateToView();
+    cy.get('[data-testid="layout-1x1"]').click();
+    cy.get('[data-testid="viewer-0"]').click();
+    cy.get('[title="拒绝"]').click();
+    cy.contains('图像已拒绝').should('be.visible');
+    cy.get('[title="恢复"]').should('be.visible');
+    cy.get('[title="恢复"]').click();
+    cy.contains('图像已恢复').should('be.visible');
+    cy.get('[title="拒绝"]').should('be.visible');
+  });
+});
+```
+
+---
+
+## 7. 边界情况处理
+
+### 7.1 多图像选中
+- **处理**: 取第一个选中的图像 `selectedViewers[0]`
+- **行为**: 按钮状态基于第一个图像的 judged_status
+
+### 7.2 并发操作
+- **处理**: loading 状态时禁用按钮
+- **实现**: `<Button loading={loading}>`
+
+### 7.3 网络错误
+- **处理**: catch 错误,显示提示
+- **行为**: judged_status 保持不变
+
+### 7.4 无选中图像
+- **处理**: selectedImage 为 null
+- **行为**: 不渲染拒绝/恢复按钮
+
+### 7.5 分格切换
+- **处理**: 响应式计算 showRejectButton/showRestoreButton
+- **行为**: 自动显示/隐藏按钮
+
+---
+
+## 8. 图标文件
+
+需要提供以下图标文件(放在 `src/assets/imgs/module-process/base/default/2x/` 目录下):
+
+1. **reject_normal.png** - 拒绝按钮图标
+2. **restore_normal.png** - 恢复按钮图标
+3. **saveas_normal.png** - 另存为按钮图标
+
+---
+
+## 9. 实施清单
+
+### 创建新文件
+- [x] `docs/实现/图像状态控制功能.md` - 实现文档
+- [ ] `src/pages/view/components/ImageStateControl.tsx` - 组件
+
+### 修改现有文件
+- [ ] `src/pages/view/components/OperationPanel.tsx` - 替换占位符
+- [ ] `src/states/exam/bodyPositionListSlice.ts` - 添加 judgeImageThunk
+
+### 图标资源
+- [ ] 提供 reject_normal.png
+- [ ] 提供 restore_normal.png
+- [ ] 提供 saveas_normal.png
+
+---
+
+## 10. 类图
+
+```mermaid
+classDiagram
+    class OperationPanel {
+        +renderPanel() React.ReactNode
+    }
+    
+    class ImageStateControl {
+        -selectedImage: BodyPosition | null
+        -judgedStatus: ImageJudgedStatus
+        -isSingleGrid: boolean
+        -loading: boolean
+        -showRejectButton: boolean
+        -showRestoreButton: boolean
+        +handleReject() Promise~void~
+        +handleRestore() Promise~void~
+        +render() React.ReactNode
+    }
+    
+    class viewerContainerSlice {
+        +gridLayout: GridLayout
+        +selectedViewers: string[]
+        +selectGridLayout() GridLayout
+        +selectSelectedViewers() string[]
+    }
+    
+    class bodyPositionListSlice {
+        +bodyPositions: BodyPosition[]
+        +loading: boolean
+        +error: string | null
+        +judgeImageThunk(payload) AsyncThunk
+    }
+    
+    class judgeImage_API {
+        +judgeImage(instanceUid, accept) Promise
+    }
+    
+    class dview {
+        +PrimarySopUID: string
+        +judged_status: ImageJudgedStatus
+        +expose_status: string
+    }
+    
+    OperationPanel --> ImageStateControl : 包含
+    ImageStateControl --> viewerContainerSlice : 读取状态
+    ImageStateControl --> bodyPositionListSlice : 调用 thunk
+    bodyPositionListSlice --> judgeImage_API : 调用
+    bodyPositionListSlice --> dview : 更新
+```
+
+---
+
+## 11. 总结
+
+本功能实现了图像处理界面的状态控制,主要特点:
+
+1. **简洁架构**: 不创建额外的 slice,直接在 bodyPositionListSlice 中处理
+2. **响应式 UI**: 基于 Redux 状态自动更新按钮显示
+3. **用户友好**: 提供 loading 状态和错误提示
+4. **可扩展**: 预留了"另存为"功能的 UI 位置
+
+实施完成后,用户可以在单分格模式下方便地拒绝或恢复图像,提升工作效率。

+ 1 - 1
src/pages/exam/components/BodyPositionList.tsx

@@ -128,7 +128,7 @@ const BodyPositionList: React.FC<BodyPositionListProps> = ({
                   count={
                     bodyPosition.dview.judged_status === 'Accept' ? (
                       <CheckOutlined style={{ color: '#fff', fontSize: '16px' }} />
-                    ) : bodyPosition.dview.judged_status === 'Rejected' ? (
+                    ) : bodyPosition.dview.judged_status === 'Reject' ? (
                       <CloseOutlined style={{ color: '#fff', fontSize: '16px' }} />
                     ) : 'Un'
                   }

+ 1 - 1
src/pages/patient/components/ThumbnailList.tsx

@@ -119,7 +119,7 @@ const ThumbnailList: React.FC<ThumbnailListProps> = ({ className }) => {
                         count={
                           thumbnail.judged_status === 'Accept' ? (
                             <CheckOutlined style={{ color: '#fff' }} />
-                          ) : thumbnail.judged_status === 'Rejected' ? (
+                          ) : thumbnail.judged_status === 'Reject' ? (
                             <CloseOutlined style={{ color: '#fff' }} />
                           ) : 'Un'
                         }

+ 155 - 8
src/pages/view/components/ImageStateControl.tsx

@@ -1,13 +1,160 @@
-import React from 'react';
-import { Button, Space } from 'antd';
+import React, { useMemo } from 'react';
+import { Button, Flex, message } from 'antd';
+import { useSelector } from 'react-redux';
+import { RootState, useAppDispatch } from '@/states/store';
+import {
+  selectGridLayout,
+  selectSelectedViewers,
+} from '@/states/view/viewerContainerSlice';
+import { judgeImageThunk } from '@/states/exam/bodyPositionListSlice';
+import { getDcmImageUrl } from '@/API/bodyPosition';
+import Icon from '@/components/Icon';
+
+/**
+ * 图像状态控制组件
+ * 
+ * 功能:
+ * - 拒绝按钮:将图像标记为拒绝状态
+ * - 恢复按钮:将已拒绝的图像恢复为接受状态
+ * - 另存为按钮:预留功能(当前禁用)
+ * 
+ * 业务规则:
+ * - 仅在单分格模式(1x1)下显示拒绝/恢复按钮
+ * - 拒绝和恢复按钮互斥显示,基于 judged_status
+ * - 另存为按钮始终可见但禁用
+ */
+const ImageStateControl: React.FC = () => {
+  const dispatch = useAppDispatch();
+
+  // 获取必要的状态
+  const selectedViewers = useSelector(selectSelectedViewers);
+  const gridLayout = useSelector(selectGridLayout);
+  const bodyPositionList = useSelector(
+    (state: RootState) => state.bodyPositionList.bodyPositions
+  );
+  const loading = useSelector(
+    (state: RootState) => state.bodyPositionList.loading
+  );
+
+  // 查找当前选中的图像
+  const selectedImage = useMemo(() => {
+    if (selectedViewers.length === 0) return null;
+    const imageUrl = selectedViewers[0];
+    return bodyPositionList.find(
+      (bp) => getDcmImageUrl(bp.sop_instance_uid) === imageUrl
+    );
+  }, [selectedViewers, bodyPositionList]);
+
+  // 判断按钮可见性
+  const isSingleGrid = gridLayout === '1x1';
+  const judgedStatus = selectedImage?.dview?.judged_status || '';
+  const showRejectButton = isSingleGrid && judgedStatus !== 'Reject';
+  const showRestoreButton = isSingleGrid && judgedStatus === 'Reject';
+
+  // 拒绝按钮处理函数
+  const handleReject = async () => {
+    if (!selectedImage) return;
+
+    try {
+      await dispatch(
+        judgeImageThunk({
+          sopInstanceUid: selectedImage.sop_instance_uid,
+          accept: false,
+        })
+      ).unwrap();
+
+      message.success('图像已拒绝');
+    } catch (err) {
+      console.error('拒绝图像失败:', err);
+      message.error('拒绝图像失败');
+    }
+  };
+
+  // 恢复按钮处理函数
+  const handleRestore = async () => {
+    if (!selectedImage) return;
+
+    try {
+      await dispatch(
+        judgeImageThunk({
+          sopInstanceUid: selectedImage.sop_instance_uid,
+          accept: true,
+        })
+      ).unwrap();
+
+      message.success('图像已恢复');
+    } catch (err) {
+      console.error('恢复图像失败:', err);
+      message.error('恢复图像失败');
+    }
+  };
 
-const ImageStateControl = () => {
   return (
-    <Space>
-      <Button>Reject Image</Button>
-      <Button>Restore Image</Button>
-      <Button>Save As</Button>
-    </Space>
+    <Flex gap="small" align="center" justify="start">
+      {showRejectButton && (
+        <Button
+          onClick={handleReject}
+          loading={loading}
+          icon={
+            <Icon
+              module="module-process"
+              name="RejectImage"
+              userId="base"
+              theme="default"
+              size="2x"
+              state="normal"
+            />
+          }
+          style={{
+            width: '1.5rem',
+            height: '1.5rem',
+            padding: 0,
+          }}
+          title="拒绝"
+        />
+      )}
+      {showRestoreButton && (
+        <Button
+          onClick={handleRestore}
+          loading={loading}
+          icon={
+            <Icon
+              module="module-process"
+              name="RestoreImage"
+              userId="base"
+              theme="default"
+              size="2x"
+              state="normal"
+            />
+          }
+          style={{
+            width: '1.5rem',
+            height: '1.5rem',
+            padding: 0,
+          }}
+          title="恢复"
+        />
+      )}
+      <Button
+        disabled
+        icon={
+          <Icon
+            module="module-process"
+            name="saveas"
+            userId="base"
+            theme="default"
+            size="2x"
+            state="normal"
+          />
+        }
+        style={{
+          width: '1.5rem',
+          height: '1.5rem',
+          padding: 0,
+        }}
+        title="另存为"
+      />
+    </Flex>
   );
 };
 

+ 14 - 5
src/pages/view/components/OperationPanel.tsx

@@ -1,9 +1,10 @@
-import { Layout } from 'antd';
+import { Layout, Divider, Flex } from 'antd';
 import { useSelector } from 'react-redux';
 import FunctionArea from './FunctionArea';
 import TransferArea from './TransferArea';
 import SendPanelForView from '../../output/SendPanelForView';
 import MeasurementPanel from './MeasurementPanel';
+import ImageStateControl from './ImageStateControl';
 import { RootState } from '../../../states/store';
 
 const { Content, Footer } = Layout;
@@ -21,9 +22,13 @@ const OperationPanel = () => {
             <Content>
               <FunctionArea />
             </Content>
+            <Divider style={{ margin: 0 }} />
             <Footer className="p-1">
-              <div>Image State Control</div>
-              <TransferArea />
+              <Flex vertical gap="small">
+                <ImageStateControl />
+                <Divider style={{ margin: '0.5rem 0' }} />
+                <TransferArea />
+              </Flex>
             </Footer>
           </>
         );
@@ -37,9 +42,13 @@ const OperationPanel = () => {
             <Content>
               <FunctionArea />
             </Content>
+            <Divider style={{ margin: 0 }} />
             <Footer className="p-1">
-              <div>Image State Control</div>
-              <TransferArea />
+              <Flex vertical gap="small">
+                <ImageStateControl />
+                <Divider style={{ margin: '0.5rem 0' }} />
+                <TransferArea />
+              </Flex>
             </Footer>
           </>
         );

+ 70 - 16
src/states/exam/bodyPositionListSlice.ts

@@ -1,4 +1,4 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
 import { View as BodyPosition } from '../../API/patient/viewActions';
 import { Work } from './examWorksCacheSlice';
 import { dview } from '@/domain/dview';
@@ -6,6 +6,7 @@ import emitter from '@/utils/eventEmitter';
 import store from '@/states/store';
 import { MqttMessage } from '@/domain/mqttService';
 import { fetchViewDetail } from '@/API/patient/viewActions';
+import { judgeImage } from '@/API/exam/judgeImage';
 /**
  * ExtendedBodyPosition即包含体位模块信息,又包含体位实例信息,也包括study(work)信息
  */
@@ -31,14 +32,38 @@ interface BodyPositionListState {
   bodyPositions: ExtendedBodyPosition[];
   selectedBodyPosition: ExtendedBodyPosition | null;
   exposureStatus: 'Half Exposed' | 'Fully Exposed' | 'Not Exposed' | null;
+  loading: boolean;
+  error: string | null;
 }
 
 const initialState: BodyPositionListState = {
   bodyPositions: [],
   selectedBodyPosition: null,
   exposureStatus: null,
+  loading: false,
+  error: null,
 };
 
+/**
+ * 图像判断状态类型
+ */
+type ImageJudgedStatus = 'Accept' | 'Reject' | '';
+
+/**
+ * 判断图像 Thunk
+ * 调用 API 接受或拒绝图像,成功后更新本地状态
+ */
+export const judgeImageThunk = createAsyncThunk(
+  'bodyPositionList/judgeImage',
+  async (payload: { sopInstanceUid: string; accept: boolean }) => {
+    await judgeImage(payload.sopInstanceUid, payload.accept);
+    return {
+      sopInstanceUid: payload.sopInstanceUid,
+      judgedStatus: (payload.accept ? 'Accept' : 'Reject') as ImageJudgedStatus,
+    };
+  }
+);
+
 emitter.on('TASK_SUCCESS', (message: MqttMessage) => {
   console.log(
     `[bodyPositionListSlice] TASK_SUCCESS received with message:`,
@@ -193,21 +218,50 @@ const bodyPositionListSlice = createSlice({
     },
   },
   extraReducers: (builder) => {
-    builder.addMatcher(
-      (action) =>
-        action.type === setBodyPositions.type ||
-        action.type === addViewsToBodyPosition.type ||
-        action.type === removeBodyPositionBySopInstanceUid.type ||
-        action.type === updateBodyPositionDview.type,
-      (state) => {
-        const newExposureStatus = determineExposureStatus(state.bodyPositions);
-        state.exposureStatus = newExposureStatus;
-        // bodyPositionListSlice.caseReducers.setExposureStatus(state, {
-        //   payload: newExposureStatus,
-        //   type: 'setExposureStatus',
-        // });
-      }
-    );
+    builder
+      // 处理 judgeImageThunk
+      .addCase(judgeImageThunk.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(judgeImageThunk.fulfilled, (state, action) => {
+        state.loading = false;
+        const position = state.bodyPositions.find(
+          (bp) => bp.sop_instance_uid === action.payload.sopInstanceUid
+        );
+        if (position?.dview) {
+          position.dview.judged_status = action.payload.judgedStatus;
+        }
+        // 同步更新 selectedBodyPosition
+        if (
+          state.selectedBodyPosition?.sop_instance_uid ===
+          action.payload.sopInstanceUid &&
+          state.selectedBodyPosition.dview
+        ) {
+          state.selectedBodyPosition.dview.judged_status =
+            action.payload.judgedStatus;
+        }
+      })
+      .addCase(judgeImageThunk.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.error.message || 'Failed to judge image';
+      })
+      // 处理曝光状态变化
+      .addMatcher(
+        (action) =>
+          action.type === setBodyPositions.type ||
+          action.type === addViewsToBodyPosition.type ||
+          action.type === removeBodyPositionBySopInstanceUid.type ||
+          action.type === updateBodyPositionDview.type,
+        (state) => {
+          const newExposureStatus = determineExposureStatus(state.bodyPositions);
+          state.exposureStatus = newExposureStatus;
+          // bodyPositionListSlice.caseReducers.setExposureStatus(state, {
+          //   payload: newExposureStatus,
+          //   type: 'setExposureStatus',
+          // });
+        }
+      );
   },
 });