Explorar o código

fix (1.31.0 -> 1.31.1): 修复bug 168,添加曝光进度列

在worklist表格中添加曝光进度列,显示已曝光图像数量/总图像数量。

- 在 LocalColumnConfigAdapter.ts 中添加 ExposureProgress 列配置
- 在 WorklistTable.tsx 中实现曝光进度列的渲染逻辑
- 在 worklist.tsx 中集成曝光进度数据获取和状态管理
- 新增 getExposureProgress.ts 实现曝光进度计算逻辑
- 优化 AnnotationManager.ts 过滤 MaskTool 注释不保存到服务器

改动文件:
- src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts
- src/features/imageAnnotation/services/AnnotationManager.ts
- src/pages/patient/components/WorklistTable.tsx
- src/pages/patient/worklist.tsx
- src/domain/patient/getExposureProgress.ts
dengdx hai 2 semanas
pai
achega
8c45d832b4

+ 24 - 0
CHANGELOG.md

@@ -2,6 +2,30 @@
 
 本项目的所有重要变更都将记录在此文件中。
 
+## [1.31.1] - 2025-12-26 19:21
+
+### 修复 (Fixed)
+- **修复bug 168 - 添加曝光进度列** - 在worklist表格中添加曝光进度列,显示已曝光图像数量/总图像数量
+  - 新增曝光进度列配置,在默认列中显示
+  - 实现曝光进度获取逻辑,统计已曝光图像数量
+  - 在worklist表格中添加曝光进度列渲染逻辑
+  - 优化MaskTool注释过滤,不保存到服务器
+
+**核心改进:**
+- 用户体验提升:在工作列表中可以直接查看每个检查的曝光进度
+- 数据可视化:曝光进度以"已曝光/总数"的格式显示,便于快速了解检查完成情况
+- 代码优化:过滤MaskTool注释,减少服务器存储负担
+
+**改动文件:**
+- src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts
+- src/features/imageAnnotation/services/AnnotationManager.ts
+- src/pages/patient/components/WorklistTable.tsx
+- src/pages/patient/worklist.tsx
+- src/domain/patient/getExposureProgress.ts
+- package.json (版本更新: 1.31.0 -> 1.31.1)
+
+---
+
 ## [1.31.0] - 2025-12-26 19:16
 
 ### 新增 (Added)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.31.0",
+  "version": "1.31.1",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 5 - 4
src/config/tableColumns/adapters/LocalColumnConfigAdapter.ts

@@ -22,10 +22,11 @@ export class LocalColumnConfigAdapter implements IColumnConfigProvider {
             { key: 'StudyID', visible: true, order: 3, width: 120 },
             { key: 'AccessionNumber', visible: true, order: 4, width: 150 },
             { key: 'StudyStatus', visible: true, order: 5, width: 100 },
-            { key: 'Modality', visible: true, order: 6, width: 100 },
-            { key: 'StudyStartDatetime', visible: true, order: 7, width: 180 },
-            { key: 'PatientAge', visible: true, order: 8, width: 80 },
-            { key: 'PatientSex', visible: true, order: 9, width: 80 },
+            { key: 'ExposureProgress', visible: true, order: 6, width: 100 },
+            { key: 'Modality', visible: true, order: 7, width: 100 },
+            { key: 'StudyStartDatetime', visible: true, order: 8, width: 180 },
+            { key: 'PatientAge', visible: true, order: 9, width: 80 },
+            { key: 'PatientSex', visible: true, order: 10, width: 80 },
             // 其他列默认隐藏
             { key: 'StudyInstanceUID', visible: false, order: 100 },
             { key: 'SpecificCharacterSet', visible: false, order: 101 },

+ 53 - 0
src/domain/patient/getExposureProgress.ts

@@ -0,0 +1,53 @@
+/**
+ * 获取指定 Study 的曝光进度信息
+ */
+
+import { fetchTaskDetails } from '@/API/patient/workActions';
+
+/**
+ * 曝光进度信息
+ */
+export interface ExposureProgress {
+  /** 已曝光的图像数量 */
+  exposedCount: number;
+  /** 总图像数量 */
+  totalCount: number;
+}
+
+/**
+ * 获取指定 Study 的曝光进度
+ * @param studyId 研究ID
+ * @returns 曝光进度信息
+ */
+export async function getExposureProgress(
+  studyId: string
+): Promise<ExposureProgress> {
+  try {
+    // 1. 调用 fetchTaskDetails 获取完整的 study 数据(包含 series)
+    const studyData = await fetchTaskDetails(studyId);
+
+    // 2. 从 series 中提取所有 images
+    const allImages = studyData.series.flatMap((series) => series.images);
+
+    // 3. 统计已曝光的图像
+    const exposedCount = allImages.filter(
+      (image) => image.expose_status === 'Exposed'
+    ).length;
+
+    // 4. 返回统计结果
+    return {
+      exposedCount,
+      totalCount: allImages.length,
+    };
+  } catch (error) {
+    console.error(
+      `[getExposureProgress] Failed to get exposure progress for study ${studyId}:`,
+      error
+    );
+    // 出错时返回默认值
+    return {
+      exposedCount: 0,
+      totalCount: 0,
+    };
+  }
+}

+ 46 - 5
src/features/imageAnnotation/services/AnnotationManager.ts

@@ -350,8 +350,28 @@ export class AnnotationManager {
         }
       });
 
-      // 保存所有注释
-      await annotationAPI.saveAnnotations(this.activeImageId, allAnnotationStrings);
+      // 过滤掉MaskTool类型的注释(mask注释不保存到服务器)
+      const filteredAnnotationStrings = allAnnotationStrings.filter(annotationString => {
+        try {
+          const annotation = JSON.parse(annotationString);
+          const toolName = annotation.metadata?.toolName || annotation.toolName;
+          if (toolName === 'MaskTool') {
+            console.log('【annotationmanager】🚫 跳过MaskTool注释保存(本地专用)');
+            return false;
+          }
+          return true;
+        } catch (error) {
+          console.error('解析注释时出错,跳过该注释:', error);
+          return false;
+        }
+      });
+
+      if (filteredAnnotationStrings.length === 0 && allAnnotationStrings.length > 0) {
+        console.log('【annotationmanager】ℹ️ 删除后只剩下MaskTool注释,跳过服务器保存');
+      } else {
+        // 保存过滤后的注释
+        await annotationAPI.saveAnnotations(this.activeImageId, filteredAnnotationStrings);
+      }
 
       console.log(`【annotationmanager】✅ 注释删除完成: ${annotationUID} (剩余 ${allAnnotationStrings.length} 个注释)`);
     } catch (error) {
@@ -553,12 +573,33 @@ export class AnnotationManager {
     this.pendingChanges.clear();
 
     try {
-      console.log(`【annotationmanager】💾 批量保存 ${changes.length} 个注释`);
+      // 过滤掉MaskTool类型的注释(mask注释不保存到服务器)
+      const filteredChanges = changes.filter(change => {
+        try {
+          const annotation = JSON.parse(change);
+          const toolName = annotation.metadata?.toolName || annotation.toolName;
+          if (toolName === 'MaskTool') {
+            console.log('【annotationmanager】🚫 跳过MaskTool注释保存(本地专用)');
+            return false;
+          }
+          return true;
+        } catch (error) {
+          console.error('解析注释时出错,跳过该注释:', error);
+          return false;
+        }
+      });
+
+      if (filteredChanges.length === 0) {
+        console.log('【annotationmanager】ℹ️ 所有待保存注释都是MaskTool类型,跳过服务器保存');
+        return;
+      }
+
+      console.log(`【annotationmanager】💾 批量保存 ${filteredChanges.length} 个注释(过滤掉 ${changes.length - filteredChanges.length} 个MaskTool注释)`);
 
       // 调用API保存注释
-      await annotationAPI.saveAnnotations(this.activeImageId, changes);
+      await annotationAPI.saveAnnotations(this.activeImageId, filteredChanges);
 
-      console.log(`【annotationmanager】✅ 批量保存成功: ${changes.length} 个注释`);
+      console.log(`【annotationmanager】✅ 批量保存成功: ${filteredChanges.length} 个注释`);
     } catch (error) {
       console.error(`【annotationmanager】❌ 批量保存失败:`, error);
 

+ 353 - 323
src/pages/patient/components/WorklistTable.tsx

@@ -8,325 +8,9 @@ import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
 import { useTouchDoubleClick } from '@/hooks/useTouchDoubleClick';
 import { useMultiSelection } from '@/hooks/useMultiSelection';
 import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
+import { ExposureProgress } from '@/domain/patient/getExposureProgress';
+
 
-// 生成列定义的辅助函数
-const generateColumnsDef = (productName?: string) => [
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyInstanceUID"
-        defaultMessage="worklistTable.StudyInstanceUID"
-      />
-    ),
-    dataIndex: 'StudyInstanceUID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyID"
-        defaultMessage="worklistTable.StudyID"
-      />
-    ),
-    dataIndex: 'StudyID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.SpecificCharacterSet"
-        defaultMessage="worklistTable.SpecificCharacterSet"
-      />
-    ),
-    dataIndex: 'SpecificCharacterSet',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.AccessionNumber"
-        defaultMessage="worklistTable.AccessionNumber"
-      />
-    ),
-    dataIndex: 'AccessionNumber',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id={productName === 'VETDROS' ? 'animal.worklistTable.PatientID' : 'worklistTable.PatientID'}
-        defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientID' : 'worklistTable.PatientID'}
-      />
-    ),
-    dataIndex: 'PatientID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id={productName === 'VETDROS' ? 'animal.worklistTable.PatientName' : 'worklistTable.PatientName'}
-        defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientName' : 'worklistTable.PatientName'}
-      />
-    ),
-    dataIndex: 'PatientName',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.DisplayPatientName"
-        defaultMessage="worklistTable.DisplayPatientName"
-      />
-    ),
-    dataIndex: 'DisplayPatientName',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.PatientSize"
-        defaultMessage="worklistTable.PatientSize"
-      />
-    ),
-    dataIndex: 'PatientSize',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id={productName === 'VETDROS' ? 'animal.worklistTable.PatientAge' : 'worklistTable.PatientAge'}
-        defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientAge' : 'worklistTable.PatientAge'}
-      />
-    ),
-    dataIndex: 'PatientAge',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id={productName === 'VETDROS' ? 'animal.worklistTable.PatientSex' : 'worklistTable.PatientSex'}
-        defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientSex' : 'worklistTable.PatientSex'}
-      />
-    ),
-    dataIndex: 'PatientSex',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.AdmittingTime"
-        defaultMessage="worklistTable.AdmittingTime"
-      />
-    ),
-    dataIndex: 'AdmittingTime',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.RegSource"
-        defaultMessage="worklistTable.RegSource"
-      />
-    ),
-    dataIndex: 'RegSource',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyStatus"
-        defaultMessage="worklistTable.StudyStatus"
-      />
-    ),
-    dataIndex: 'StudyStatus',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.RequestedProcedureID"
-        defaultMessage="worklistTable.RequestedProcedureID"
-      />
-    ),
-    dataIndex: 'RequestedProcedureID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.PerformedProtocolCodeValue"
-        defaultMessage="worklistTable.PerformedProtocolCodeValue"
-      />
-    ),
-    dataIndex: 'PerformedProtocolCodeValue',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.PerformedProtocolCodeMeaning"
-        defaultMessage="worklistTable.PerformedProtocolCodeMeaning"
-      />
-    ),
-    dataIndex: 'PerformedProtocolCodeMeaning',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.PerformedProcedureStepID"
-        defaultMessage="worklistTable.PerformedProcedureStepID"
-      />
-    ),
-    dataIndex: 'PerformedProcedureStepID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyDescription"
-        defaultMessage="worklistTable.StudyDescription"
-      />
-    ),
-    dataIndex: 'StudyDescription',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyStartDatetime"
-        defaultMessage="worklistTable.StudyStartDatetime"
-      />
-    ),
-    dataIndex: 'StudyStartDatetime',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.ScheduledProcedureStepStartDate"
-        defaultMessage="worklistTable.ScheduledProcedureStepStartDate"
-      />
-    ),
-    dataIndex: 'ScheduledProcedureStepStartDate',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyLock"
-        defaultMessage="worklistTable.StudyLock"
-      />
-    ),
-    dataIndex: 'StudyLock',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.OperatorID"
-        defaultMessage="worklistTable.OperatorID"
-      />
-    ),
-    dataIndex: 'OperatorID',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.Modality"
-        defaultMessage="worklistTable.Modality"
-      />
-    ),
-    dataIndex: 'Modality',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.Views"
-        defaultMessage="worklistTable.Views"
-      />
-    ),
-    dataIndex: 'Views',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.Thickness"
-        defaultMessage="worklistTable.Thickness"
-      />
-    ),
-    dataIndex: 'Thickness',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.PatientType"
-        defaultMessage="worklistTable.PatientType"
-      />
-    ),
-    dataIndex: 'PatientType',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.StudyType"
-        defaultMessage="worklistTable.StudyType"
-      />
-    ),
-    dataIndex: 'StudyType',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.QRCode"
-        defaultMessage="worklistTable.QRCode"
-      />
-    ),
-    dataIndex: 'QRCode',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.IsExported"
-        defaultMessage="worklistTable.IsExported"
-      />
-    ),
-    dataIndex: 'IsExported',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.IsEdited"
-        defaultMessage="worklistTable.IsEdited"
-      />
-    ),
-    dataIndex: 'IsEdited',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.WorkRef"
-        defaultMessage="worklistTable.WorkRef"
-      />
-    ),
-    dataIndex: 'WorkRef',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.IsAppended"
-        defaultMessage="worklistTable.IsAppended"
-      />
-    ),
-    dataIndex: 'IsAppended',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.CreationTime"
-        defaultMessage="worklistTable.CreationTime"
-      />
-    ),
-    dataIndex: 'CreationTime',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.MappedStatus"
-        defaultMessage="worklistTable.MappedStatus"
-      />
-    ),
-    dataIndex: 'MappedStatus',
-  },
-  {
-    title: (
-      <FormattedMessage
-        id="worklistTable.IsDelete"
-        defaultMessage="worklistTable.IsDelete"
-      />
-    ),
-    dataIndex: 'IsDelete',
-  },
-];
 
 interface TitlePropsType {
   width: number;
@@ -378,6 +62,7 @@ const ResizableTitle: React.FC<
 interface WorklistTableProps {
   productName?: string;                       // 新增:产品名称(用于区分人医/宠物医)
   columnConfig?: ColumnConfig[];              // 列配置(可选)
+  exposureProgressMap?: Record<string, ExposureProgress>; // 新增:曝光进度映射
   worklistData: Task[];
   filters?: WorkFilter;
   page?: number;
@@ -392,6 +77,7 @@ interface WorklistTableProps {
 const WorklistTable: React.FC<WorklistTableProps> = ({
   productName,                                // 新增
   columnConfig = [],                          // 接收配置,默认为空数组
+  exposureProgressMap = {},                   // 新增:曝光进度映射
   worklistData,
   // filters,
   // page,
@@ -402,9 +88,353 @@ const WorklistTable: React.FC<WorklistTableProps> = ({
   handleRowDoubleClick,
   className,
 }) => {
-  // 根据 productName 生成列定义
-  const columnsDef = useMemo(() => generateColumnsDef(productName), [productName]);
-  
+  // 生成列定义的辅助函数
+  const generateColumnsDef = (productName?: string): any[] => [
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyInstanceUID"
+          defaultMessage="worklistTable.StudyInstanceUID"
+        />
+      ),
+      dataIndex: 'StudyInstanceUID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyID"
+          defaultMessage="worklistTable.StudyID"
+        />
+      ),
+      dataIndex: 'StudyID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.SpecificCharacterSet"
+          defaultMessage="worklistTable.SpecificCharacterSet"
+        />
+      ),
+      dataIndex: 'SpecificCharacterSet',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.AccessionNumber"
+          defaultMessage="worklistTable.AccessionNumber"
+        />
+      ),
+      dataIndex: 'AccessionNumber',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id={productName === 'VETDROS' ? 'animal.worklistTable.PatientID' : 'worklistTable.PatientID'}
+          defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientID' : 'worklistTable.PatientID'}
+        />
+      ),
+      dataIndex: 'PatientID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id={productName === 'VETDROS' ? 'animal.worklistTable.PatientName' : 'worklistTable.PatientName'}
+          defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientName' : 'worklistTable.PatientName'}
+        />
+      ),
+      dataIndex: 'PatientName',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.DisplayPatientName"
+          defaultMessage="worklistTable.DisplayPatientName"
+        />
+      ),
+      dataIndex: 'DisplayPatientName',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.PatientSize"
+          defaultMessage="worklistTable.PatientSize"
+        />
+      ),
+      dataIndex: 'PatientSize',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id={productName === 'VETDROS' ? 'animal.worklistTable.PatientAge' : 'worklistTable.PatientAge'}
+          defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientAge' : 'worklistTable.PatientAge'}
+        />
+      ),
+      dataIndex: 'PatientAge',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id={productName === 'VETDROS' ? 'animal.worklistTable.PatientSex' : 'worklistTable.PatientSex'}
+          defaultMessage={productName === 'VETDROS' ? 'animal.worklistTable.PatientSex' : 'worklistTable.PatientSex'}
+        />
+      ),
+      dataIndex: 'PatientSex',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.AdmittingTime"
+          defaultMessage="worklistTable.AdmittingTime"
+        />
+      ),
+      dataIndex: 'AdmittingTime',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.RegSource"
+          defaultMessage="worklistTable.RegSource"
+        />
+      ),
+      dataIndex: 'RegSource',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyStatus"
+          defaultMessage="worklistTable.StudyStatus"
+        />
+      ),
+      dataIndex: 'StudyStatus',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.RequestedProcedureID"
+          defaultMessage="worklistTable.RequestedProcedureID"
+        />
+      ),
+      dataIndex: 'RequestedProcedureID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.PerformedProtocolCodeValue"
+          defaultMessage="worklistTable.PerformedProtocolCodeValue"
+        />
+      ),
+      dataIndex: 'PerformedProtocolCodeValue',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.PerformedProtocolCodeMeaning"
+          defaultMessage="worklistTable.PerformedProtocolCodeMeaning"
+        />
+      ),
+      dataIndex: 'PerformedProtocolCodeMeaning',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.PerformedProcedureStepID"
+          defaultMessage="worklistTable.PerformedProcedureStepID"
+        />
+      ),
+      dataIndex: 'PerformedProcedureStepID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyDescription"
+          defaultMessage="worklistTable.StudyDescription"
+        />
+      ),
+      dataIndex: 'StudyDescription',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyStartDatetime"
+          defaultMessage="worklistTable.StudyStartDatetime"
+        />
+      ),
+      dataIndex: 'StudyStartDatetime',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.ScheduledProcedureStepStartDate"
+          defaultMessage="worklistTable.ScheduledProcedureStepStartDate"
+        />
+      ),
+      dataIndex: 'ScheduledProcedureStepStartDate',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyLock"
+          defaultMessage="worklistTable.StudyLock"
+        />
+      ),
+      dataIndex: 'StudyLock',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.OperatorID"
+          defaultMessage="worklistTable.OperatorID"
+        />
+      ),
+      dataIndex: 'OperatorID',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.Modality"
+          defaultMessage="worklistTable.Modality"
+        />
+      ),
+      dataIndex: 'Modality',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.Views"
+          defaultMessage="worklistTable.Views"
+        />
+      ),
+      dataIndex: 'Views',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.Thickness"
+          defaultMessage="worklistTable.Thickness"
+        />
+      ),
+      dataIndex: 'Thickness',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.PatientType"
+          defaultMessage="worklistTable.PatientType"
+        />
+      ),
+      dataIndex: 'PatientType',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.StudyType"
+          defaultMessage="worklistTable.StudyType"
+        />
+      ),
+      dataIndex: 'StudyType',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.QRCode"
+          defaultMessage="worklistTable.QRCode"
+        />
+      ),
+      dataIndex: 'QRCode',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.IsExported"
+          defaultMessage="worklistTable.IsExported"
+        />
+      ),
+      dataIndex: 'IsExported',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.IsEdited"
+          defaultMessage="worklistTable.IsEdited"
+        />
+      ),
+      dataIndex: 'IsEdited',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.WorkRef"
+          defaultMessage="worklistTable.WorkRef"
+        />
+      ),
+      dataIndex: 'WorkRef',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.IsAppended"
+          defaultMessage="worklistTable.IsAppended"
+        />
+      ),
+      dataIndex: 'IsAppended',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.CreationTime"
+          defaultMessage="worklistTable.CreationTime"
+        />
+      ),
+      dataIndex: 'CreationTime',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.MappedStatus"
+          defaultMessage="worklistTable.MappedStatus"
+        />
+      ),
+      dataIndex: 'MappedStatus',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.IsDelete"
+          defaultMessage="worklistTable.IsDelete"
+        />
+      ),
+      dataIndex: 'IsDelete',
+    },
+    {
+      title: (
+        <FormattedMessage
+          id="worklistTable.ExposureProgress"
+          defaultMessage="曝光进度"
+        />
+      ),
+      dataIndex: 'ExposureProgress',
+      render: (_: any, record: Task) => {
+        // 如果没有 StudyID,说明是 RIS 数据,不需要显示曝光进度
+        if (!record.StudyID || record.StudyID.trim() === '') {
+          return '-'; // RIS 数据显示 "-"
+        }
+
+        const progress = exposureProgressMap?.[record.StudyID];
+        if (!progress) {
+          return '加载中...'; // 数据还未加载完成
+        }
+
+        return `${progress.exposedCount}/${progress.totalCount}`;
+      }
+    },
+  ];
+
+  // 根据 productName 和 exposureProgressMap 生成列定义
+  const columnsDef = useMemo(() => {
+    const cols = generateColumnsDef(productName);
+    return cols;
+  }, [productName, exposureProgressMap]); // 依赖 productName 和 exposureProgressMap 的变化
+
   // 判断是否使用双ID模式
   const useDualIdMode = selectedSecondaryIds !== undefined;
   // 生成 rowKey
@@ -469,7 +499,7 @@ const WorklistTable: React.FC<WorklistTableProps> = ({
           columnConfig.find((c) => c.key === b.dataIndex)?.order ?? 999;
         return orderA - orderB;
       });
-  }, [columnConfig]);
+  }, [columnConfig, columnsDef]);
 
   // 列可调整大小的逻辑
   const [columns, setColumns] =
@@ -477,7 +507,7 @@ const WorklistTable: React.FC<WorklistTableProps> = ({
 
   useEffect(() => {
     setColumns(visibleColumns);
-  }, [visibleColumns]);
+  }, [visibleColumns, exposureProgressMap]);
 
   const handleResize =
     (index: number) =>

+ 42 - 0
src/pages/patient/worklist.tsx

@@ -23,6 +23,7 @@ import { saveRisData } from '../../domain/patient/risSaveLogic';
 import { columnConfigService } from '@/config/tableColumns';
 import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
 import { useMultiSelection } from '@/hooks/useMultiSelection';
+import { getExposureProgress, ExposureProgress } from '@/domain/patient/getExposureProgress';
 import AppendViewModal from '../exam/components/AppendViewModal';
 import useEffectiveBreakpoint from '../../hooks/useEffectiveBreakpoint';
 
@@ -30,6 +31,7 @@ const WorklistPage: React.FC = () => {
   const screens = useEffectiveBreakpoint();
   const [drawerVisible, setDrawerVisible] = useState(false);
   const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]); // 新增:列配置状态
+  const [exposureProgressMap, setExposureProgressMap] = useState<Record<string, ExposureProgress>>({}); // 曝光进度映射
   const [selectedPatientForPortrait, setSelectedPatientForPortrait] = useState<Task | null>(null); // 照片显示用的选中患者
   const [isAppendModalOpen, setIsAppendModalOpen] = useState(false); // 追加体位模态框状态
   const [currentStudy, setCurrentStudy] = useState<Task | null>(null); // 当前保存的study信息
@@ -70,6 +72,44 @@ const WorklistPage: React.FC = () => {
       });
   }, []);
 
+  // 批量获取曝光进度
+  useEffect(() => {
+    const fetchExposureProgress = async () => {
+      if (worklistData.length === 0) return;
+
+      const progressMap: Record<string, ExposureProgress> = {};
+
+      // 并发获取曝光进度,但限制并发数量避免过多请求
+      const batchSize = 5;
+      for (let i = 0; i < worklistData.length; i += batchSize) {
+        const batch = worklistData.slice(i, i + batchSize);
+        await Promise.all(
+          batch.map(async (task) => {
+            // 跳过没有 StudyID 的记录(主要是 RIS 数据)
+            if (!task.StudyID || task.StudyID.trim() === '') {
+              console.warn(`[getExposureProgress] Skipping task without StudyID:`, task);
+              return;
+            }
+
+            try {
+              console.log(`[getExposureProgress] Fetching progress for StudyID: ${task.StudyID}`);
+              const progress = await getExposureProgress(task.StudyID);
+              progressMap[task.StudyID] = progress;
+              console.log(`[getExposureProgress] Got progress for ${task.StudyID}:`, progress);
+            } catch (error) {
+              console.error(`[getExposureProgress] Failed to get progress for ${task.StudyID}:`, error);
+              progressMap[task.StudyID] = { exposedCount: 0, totalCount: 0 };
+            }
+          })
+        );
+      }
+
+      setExposureProgressMap({ ...progressMap }); // 使用展开运算符创建新对象
+    };
+
+    fetchExposureProgress();
+  }, [worklistData]);
+
   // 同步分页状态到过滤器
   useEffect(() => {
     console.log('[worklist] Syncing pagination to filters:', {
@@ -165,6 +205,7 @@ const WorklistPage: React.FC = () => {
             <WorklistTable
               productName={productName}
               columnConfig={columnConfig}
+              exposureProgressMap={exposureProgressMap}
               worklistData={worklistData}
               filters={filters}
               page={page}
@@ -214,6 +255,7 @@ const WorklistPage: React.FC = () => {
                 <WorklistTable
                   productName={productName}
                   columnConfig={columnConfig}
+                  exposureProgressMap={exposureProgressMap}
                   worklistData={worklistData}
                   filters={filters}
                   page={page}