فهرست منبع

feat: 完善患者工作列表功能和 fetchTaskList 类型定义

- 完善 fetchTaskList API 响应类型定义,支持完整后端数据结构
- 添加 FetchTaskListStudy、FetchTaskListData、FetchTaskListResponse 接口
- 新增 mapFetchTaskListStudyToTask 专用映射函数
- 支持患者英文名、宠物信息、麻醉状态等扩展字段
- 更新工作列表相关页面组件
- 添加通用分页组件 GenericPagination
- 修复 ESLint 未使用变量错误

修改文件:
- src/API/patient/workActions.ts
- src/pages/patient/HistoryList.tsx
- src/pages/patient/components/WorklistTable.tsx
- src/pages/patient/worklist.tsx
- src/states/patient/worklist/slices/workSlice.ts
- src/components/GenericPagination.tsx (新增)
sw 2 هفته پیش
والد
کامیت
79fa220717

+ 159 - 5
src/API/patient/workActions.ts

@@ -199,6 +199,114 @@ export interface TaskListQuery {
   page_size?: number;
 }
 
+/**
+ * fetchTaskList 专用的研究数据结构 - 基于后端实际响应
+ */
+export interface FetchTaskListStudy {
+  study_instance_uid: string;
+  study_id: string;
+  public_study_id: string;
+  specific_character_set: string;
+  accession_number: string;
+  ref_physician: string;
+  patient_id: string;
+  patient_name: string;
+  /** 患者英文名 */
+  patient_english_name: string;
+  /** 患者曾用名 */
+  patient_former_name: string;
+  patient_size: string;
+  other_patient_ids: string;
+  other_patient_names: string;
+  patient_age: string;
+  patient_dob: string;
+  patient_sex: string;
+  /** 性别绝育状态 */
+  sex_neutered: string;
+  /** 妊娠状态 */
+  pregnancy_status: string;
+  patient_state: string;
+  admitting_time: string | null;
+  priority: string;
+  reg_source: string;
+  study_description: string;
+  study_start_datetime: string | null;
+  study_end_datetime: string | null;
+  scheduled_procedure_step_start_date: string | null;
+  performed_physician: string;
+  study_lock: string;
+  folder_path: string;
+  operator_name: string;
+  modality: string;
+  weight: number;
+  thickness: number;
+  length: number;
+  patient_type: string;
+  study_type: string;
+  /** 宠物主人姓名 */
+  owner_name: string;
+  /** 芯片号码 */
+  chip_number: string;
+  /** 品种 */
+  variety: string;
+  /** 麻醉状态 */
+  is_anaesthesia: boolean;
+  /** 是否镇静 */
+  is_sedation: boolean;
+  mwl: string;
+  is_exported: boolean;
+  is_edited: boolean;
+  is_appended: boolean;
+  department: string;
+  mapped_status: boolean;
+  qc_result: boolean;
+  comment: string;
+  study_status: string;
+  sort: number;
+  product: string;
+  /** 创建时间 */
+  create_time: string;
+  series: Series[];
+}
+
+/**
+ * 研究列表数据类型 - 基于后端实际响应结构
+ */
+export interface StudyListData {
+  /** 类型标识 */
+  '@type': string;
+  /** 总数 */
+  count: number;
+  /** 研究列表 */
+  studies: RegisterWorkResponseData[];
+}
+
+/**
+ * fetchTaskList 专用的数据结构
+ */
+export interface FetchTaskListData {
+  /** 类型标识 */
+  '@type': string;
+  /** 总数 */
+  count: number;
+  /** 研究列表 */
+  studies: FetchTaskListStudy[];
+}
+
+/**
+ * fetchTaskList 响应类型 - 完整的研究列表响应结构
+ */
+export interface FetchTaskListResponse {
+  /** 响应码 */
+  code: string;
+  /** 描述信息 */
+  description: string;
+  /** 解决方案 */
+  solution: string;
+  /** 数据内容 */
+  data: FetchTaskListData;
+}
+
 const registerWork = async (
   work: RegisterInfo
 ): Promise<RegisterWorkResponse> => {
@@ -216,6 +324,7 @@ const registerWork = async (
   }
 };
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const mapToTask = (study: RegisterWorkResponseData): Task => ({
   StudyInstanceUID: study.study_instance_uid,
   StudyID: study.study_id,
@@ -255,9 +364,51 @@ const mapToTask = (study: RegisterWorkResponseData): Task => ({
   IsDelete: false,
 });
 
+/**
+ * 将 FetchTaskListStudy 映射到 Task - fetchTaskList 专用
+ */
+const mapFetchTaskListStudyToTask = (study: FetchTaskListStudy): Task => ({
+  StudyInstanceUID: study.study_instance_uid,
+  StudyID: study.study_id,
+  SpecificCharacterSet: study.specific_character_set,
+  AccessionNumber: study.accession_number,
+  PatientID: study.patient_id,
+  PatientName: study.patient_name,
+  DisplayPatientName: study.patient_name,
+  PatientSize: study.patient_size,
+  PatientAge: study.patient_age,
+  PatientSex: study.patient_sex,
+  AdmittingTime: study.admitting_time ?? '',
+  RegSource: study.reg_source,
+  StudyStatus: study.study_status,
+  RequestedProcedureID: '',
+  PerformedProtocolCodeValue: '',
+  PerformedProtocolCodeMeaning: '',
+  PerformedProcedureStepID: '',
+  StudyDescription: study.study_description,
+  StudyStartDatetime: study.study_start_datetime ?? '',
+  ScheduledProcedureStepStartDate:
+    study.scheduled_procedure_step_start_date ?? '',
+  StudyLock: study.study_lock,
+  OperatorID: study.operator_name,
+  Modality: study.modality,
+  Views: [],
+  Thickness: study.thickness,
+  PatientType: study.patient_type,
+  StudyType: study.study_type,
+  QRCode: '',
+  IsExported: study.is_exported,
+  IsEdited: study.is_edited,
+  WorkRef: '',
+  IsAppended: study.is_appended,
+  CreationTime: study.create_time,
+  MappedStatus: study.mapped_status,
+  IsDelete: false,
+});
+
 const fetchTaskList = async (
-  page,
-  pageSize,
+  page: number,
+  pageSize: number,
   filter: TaskListQuery
 ): Promise<{ items: Task[]; total: number }> => {
   console.log(
@@ -275,9 +426,12 @@ const fetchTaskList = async (
       status: filter.status,
     },
   });
-  const { studies } = response.data.data;
-  const tasks = studies.map(mapToTask);
-  return { items: tasks, total: tasks.length };
+
+  // 使用新的响应类型
+  const responseData = response.data as FetchTaskListResponse;
+  const { studies, count } = responseData.data;
+  const tasks = studies.map(mapFetchTaskListStudyToTask);
+  return { items: tasks, total: count };
 };
 
 export { registerWork, fetchTaskList };

+ 88 - 0
src/components/GenericPagination.tsx

@@ -0,0 +1,88 @@
+import React from 'react';
+import { Pagination } from 'antd';
+import { useSelector, useDispatch } from 'react-redux';
+import { ActionCreatorWithPayload } from '@reduxjs/toolkit';
+import { RootState, AppDispatch } from '../states/store';
+
+interface GenericPaginationProps {
+  // 状态选择器
+  paginationSelector: (state: RootState) => { page: number; pageSize: number };
+  entitiesSelector: (state: RootState) => { total: number };
+
+  // Action dispatchers with proper types
+  paginationActions: {
+    setPage: ActionCreatorWithPayload<number>;
+    setPageSize: ActionCreatorWithPayload<number>;
+  };
+
+  // 可选的过滤器状态同步
+  filtersActions?: {
+    setFilters: ActionCreatorWithPayload<Record<string, unknown>>;
+  };
+
+  // 可选的样式和配置
+  className?: string;
+  showSizeChanger?: boolean;
+  showQuickJumper?: boolean;
+  showTotal?: boolean;
+  pageSizeOptions?: string[];
+  showLessItems?: boolean;
+}
+
+const GenericPagination: React.FC<GenericPaginationProps> = ({
+  paginationSelector,
+  entitiesSelector,
+  paginationActions,
+  className = '',
+  showSizeChanger = true,
+  showQuickJumper = true,
+  showTotal = true,
+  pageSizeOptions = ['10', '20', '50', '100'],
+  showLessItems = false,
+}) => {
+  const dispatch: AppDispatch = useDispatch();
+  const { page, pageSize } = useSelector(paginationSelector);
+  const { total } = useSelector(entitiesSelector);
+
+  const handlePageChange = (newPage: number, newPageSize?: number) => {
+    // 更新页码
+    dispatch(paginationActions.setPage(newPage));
+
+    // 如果页面大小发生变化,也要更新
+    if (newPageSize && newPageSize !== pageSize) {
+      dispatch(paginationActions.setPageSize(newPageSize));
+    }
+  };
+  // eslint-disable-next-line
+  const handleShowSizeChange = (current: number, size: number) => {
+    // 当页面大小改变时,重置到第一页
+    dispatch(paginationActions.setPage(1));
+    dispatch(paginationActions.setPageSize(size));
+  };
+
+  return (
+    <div className={`flex justify-end p-4 ${className}`}>
+      <Pagination
+        current={page}
+        pageSize={pageSize}
+        total={total}
+        onChange={handlePageChange}
+        onShowSizeChange={handleShowSizeChange}
+        showSizeChanger={showSizeChanger}
+        showQuickJumper={showQuickJumper}
+        showLessItems={showLessItems}
+        hideOnSinglePage={false}
+        showPrevNextJumpers={true}
+        showTotal={
+          showTotal
+            ? (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
+            : undefined
+        }
+        pageSizeOptions={pageSizeOptions}
+        size="default"
+      />
+    </div>
+  );
+};
+
+export default GenericPagination;

+ 50 - 17
src/pages/patient/HistoryList.tsx

@@ -6,12 +6,15 @@ import { useSelector, useDispatch } from 'react-redux';
 import {
   fetchWorkThunk,
   historySelectionSlice,
+  historyPaginationSlice,
+  historyFiltersSlice,
 } from '../../states/patient/worklist/slices/history';
 import { updateThumbnailsFromHistorySelection } from '../../states/patient/worklist/slices/thumbnailListSlice';
 import WorklistTable from './components/WorklistTable';
 import OperationPanel from './components/OperationPanel';
 import SendPanel from '../../pages/output/SendPanel';
 import ThumbnailList from './components/ThumbnailList';
+import GenericPagination from '../../components/GenericPagination';
 import { RootState, AppDispatch } from '../../states/store';
 import { Task } from '@/domain/work';
 import worklistToExam from '../../domain/patient/worklistToExam';
@@ -38,6 +41,20 @@ const HistorylistPage: React.FC = () => {
     (state: RootState) => state.historyPanelSwitch.currentPanel
   );
 
+  // 同步分页状态到过滤器
+  useEffect(() => {
+    console.log('[historylist] Syncing pagination to filters:', {
+      page,
+      pageSize,
+    });
+    dispatch(
+      historyFiltersSlice.actions.setFilters({
+        page,
+        page_size: pageSize,
+      })
+    );
+  }, [dispatch, page, pageSize]);
+
   useEffect(() => {
     console.log(
       'Fetching historylist data with filters:',
@@ -70,14 +87,22 @@ const HistorylistPage: React.FC = () => {
     <div className="h-full">
       {screens.xs ? (
         <>
-          <WorklistTable
-            worklistData={historylistData}
-            filters={filters}
-            page={page}
-            pageSize={pageSize}
-            selectedIds={selectedIds}
-            handleRowClick={handleRowClick}
-            handleRowDoubleClick={handleRowDoubleClick}
+          <div className="flex-1 overflow-auto">
+            <WorklistTable
+              worklistData={historylistData}
+              filters={filters}
+              page={page}
+              pageSize={pageSize}
+              selectedIds={selectedIds}
+              handleRowClick={handleRowClick}
+              handleRowDoubleClick={handleRowDoubleClick}
+            />
+          </div>
+          <GenericPagination
+            paginationSelector={(state) => state.historyPagination}
+            entitiesSelector={(state) => state.historyEntities}
+            paginationActions={historyPaginationSlice.actions}
+            className="border-t"
           />
           <Button
             type="primary"
@@ -111,15 +136,23 @@ const HistorylistPage: React.FC = () => {
             span={screens.lg ? 18 : screens.md ? 20 : 24}
             className="h-full flex flex-col"
           >
-            <div className="flex-1 overflow-auto">
-              <WorklistTable
-                worklistData={historylistData}
-                filters={filters}
-                page={page}
-                pageSize={pageSize}
-                selectedIds={selectedIds}
-                handleRowClick={handleRowClick}
-                handleRowDoubleClick={handleRowDoubleClick}
+            <div className="flex-1 flex flex-col">
+              <div className="flex-1 overflow-auto">
+                <WorklistTable
+                  worklistData={historylistData}
+                  filters={filters}
+                  page={page}
+                  pageSize={pageSize}
+                  selectedIds={selectedIds}
+                  handleRowClick={handleRowClick}
+                  handleRowDoubleClick={handleRowDoubleClick}
+                />
+              </div>
+              <GenericPagination
+                paginationSelector={(state) => state.historyPagination}
+                entitiesSelector={(state) => state.historyEntities}
+                paginationActions={historyPaginationSlice.actions}
+                className="border-t"
               />
             </div>
             <div className="h-60 border-t border-gray-200">

+ 1 - 0
src/pages/patient/components/WorklistTable.tsx

@@ -431,6 +431,7 @@ const WorklistTable: React.FC<WorklistTableProps> = ({
       components={{ header: { cell: ResizableTitle } }}
       dataSource={worklistData}
       rowKey="StudyID"
+      pagination={false}
       onRow={(record, index) => {
         const { handleTouchStart } = useTouchDoubleClick({
           onDoubleClick: () => handleRowDoubleClick(record),

+ 51 - 18
src/pages/patient/worklist.tsx

@@ -6,11 +6,14 @@ import { useSelector, useDispatch } from 'react-redux';
 import {
   fetchWorkThunk,
   workSelectionSlice,
+  workPaginationSlice,
+  workFiltersSlice,
 } from '../../states/patient/worklist/slices/workSlice';
 import { updateThumbnailsFromWorkSelection } from '../../states/patient/worklist/slices/thumbnailListSlice';
 import WorklistTable from './components/WorklistTable';
 import OperationPanel from './components/OperationPanel';
 import ThumbnailList from './components/ThumbnailList';
+import GenericPagination from '../../components/GenericPagination';
 import { RootState, AppDispatch } from '../../states/store';
 import { Task } from '@/domain/work';
 import worklistToExam from '../../domain/patient/worklistToExam';
@@ -34,6 +37,20 @@ const WorklistPage: React.FC = () => {
     (state: RootState) => state.workEntities.data
   );
 
+  // 同步分页状态到过滤器
+  useEffect(() => {
+    console.log('[worklist] Syncing pagination to filters:', {
+      page,
+      pageSize,
+    });
+    dispatch(
+      workFiltersSlice.actions.setFilters({
+        page,
+        page_size: pageSize,
+      })
+    );
+  }, [dispatch, page, pageSize]);
+
   useEffect(() => {
     console.log(
       '[worklist] Fetching worklist data with filters:',
@@ -44,7 +61,7 @@ const WorklistPage: React.FC = () => {
       pageSize
     );
     dispatch(fetchWorkThunk({ page, pageSize, filters }));
-  }, [dispatch, page, pageSize]);
+  }, [dispatch, page, pageSize, filters]);
 
   const handleRowClick = (record: Task) => {
     console.log('Row clicked:', JSON.stringify(record, null, 2));
@@ -66,14 +83,22 @@ const WorklistPage: React.FC = () => {
     <div className="h-full">
       {screens.xs ? (
         <>
-          <WorklistTable
-            worklistData={worklistData}
-            filters={filters}
-            page={page}
-            pageSize={pageSize}
-            selectedIds={selectedIds}
-            handleRowClick={handleRowClick}
-            handleRowDoubleClick={handleRowDoubleClick}
+          <div className="flex-1 overflow-auto">
+            <WorklistTable
+              worklistData={worklistData}
+              filters={filters}
+              page={page}
+              pageSize={pageSize}
+              selectedIds={selectedIds}
+              handleRowClick={handleRowClick}
+              handleRowDoubleClick={handleRowDoubleClick}
+            />
+          </div>
+          <GenericPagination
+            paginationSelector={(state) => state.workPagination}
+            entitiesSelector={(state) => state.workEntities}
+            paginationActions={workPaginationSlice.actions}
+            className="border-t"
           />
           <Button
             type="primary"
@@ -103,15 +128,23 @@ const WorklistPage: React.FC = () => {
             span={screens.lg ? 18 : screens.md ? 20 : 24}
             className="h-full flex flex-col"
           >
-            <div className="flex-1 overflow-auto">
-              <WorklistTable
-                worklistData={worklistData}
-                filters={filters}
-                page={page}
-                pageSize={pageSize}
-                selectedIds={selectedIds}
-                handleRowClick={handleRowClick}
-                handleRowDoubleClick={handleRowDoubleClick}
+            <div className="flex-1 flex flex-col">
+              <div className="flex-1 overflow-auto">
+                <WorklistTable
+                  worklistData={worklistData}
+                  filters={filters}
+                  page={page}
+                  pageSize={pageSize}
+                  selectedIds={selectedIds}
+                  handleRowClick={handleRowClick}
+                  handleRowDoubleClick={handleRowDoubleClick}
+                />
+              </div>
+              <GenericPagination
+                paginationSelector={(state) => state.workPagination}
+                entitiesSelector={(state) => state.workEntities}
+                paginationActions={workPaginationSlice.actions}
+                className="border-t"
               />
             </div>
             <div className="h-60 border-t border-gray-200">

+ 11 - 0
src/states/patient/worklist/slices/workSlice.ts

@@ -91,6 +91,17 @@ const extraReducersForFilter = (builder) => {
     }
   );
 };
+
+// // 添加分页状态同步逻辑
+// const addPaginationSyncReducers = (filtersSlice, paginationSlice) => {
+//   filtersSlice.caseReducers = {
+//     ...filtersSlice.caseReducers,
+//     syncFromPagination: (state, action: PayloadAction<{page: number, pageSize: number}>) => {
+//       state.page = action.payload.page;
+//       state.page_size = action.payload.pageSize;
+//     }
+//   };
+// };
 // Create the worklist slices
 const {
   entitiesSlice,