Browse Source

添加对任务清单管理相关的模板实现与针对任务清单的实现,模板实现可方便用于以后的history等列表管理;添加获取任务清单的API,path以及参数 只是占位,以后还需要基于后端的规格更新

dengdx 2 months ago
parent
commit
5d2979de2d

+ 75 - 0
src/API/patient/workAction.md

@@ -200,3 +200,78 @@ example :
   }
 }
 ```
+
+## 2. 获取任务清单
+
+### HTTP Method: GET
+
+- **Authorization** : 默认值是 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTEyNzc5NzAsImlkIjoxLCJuYW1lIjoiYWRtaW4ifQ.ooTGwBXaNhtunbKbpqteWbjDwJLjnRmSIl80r5dp1pY
+- **Language**: `en` 或 `zh`
+- **Product**: `DROS` 或 `VETDROS`
+- **Source**: `Electron` 或 `Browser` 或 `Android`
+
+### Endpoint URL: /dr/api/v1/auth/studys 【后期会变动】
+
+### query parameters
+
+| 名称          | 类型   | 必填  | 默认值 | 描述               |
+| ------------- | ------ | ----- | ------ | ------------------ |
+| patient_id    | string | false |        | 患者编号           |
+| patient_name  | string | false |        | 患者姓名           |
+| start_date    | string | false |        | 从身体部位接口获取 |
+| end_date      | string | false |        | true 或 false      |
+| access_number | string | false |        | 从协议列表接口获取 |
+
+### Response Status: 200 (OK)
+
+### Response Data: Returns a JSON
+
+示例:
+
+```json
+[
+  {
+    "StudyInstanceUID": "1.2.156.112677.10.1000.901.1217361531520084.8.1747360703",
+    "StudyID": "2505160958230002",
+    "SpecificCharacterSet": "GB18030",
+    "AccessionNumber": "202505160955581",
+    "PatientID": "202505160955581",
+    "PatientName": "ER_202505160958231^^^^",
+    "DisplayPatientName": "ER_202505160958231",
+    "PatientSize": "Medium",
+    "PatientAge": "",
+    "PatientSex": "F",
+    "AdmittingTime": "2025-05-16T09:58:23.6708694",
+    "RegSource": "Local",
+    "StudyStatus": "Arrived",
+    "RequestedProcedureID": "",
+    "PerformedProtocolCodeValue": "PE-001",
+    "PerformedProtocolCodeMeaning": "PE-001",
+    "PerformedProcedureStepID": "ebd5a754483d18aa",
+    "StudyDescription": "腹腔 (2个摆位)",
+    "StudyStartDatetime": "2025-05-16T09:58:23.6708694",
+    "ScheduledProcedureStepStartDate": "2025-05-16T09:58:23.6708694",
+    "StudyLock": "UNLOCKED",
+    "OperatorID": "Mgr",
+    "Modality": "DX",
+    "Views": [],
+    "Thickness": 10.0,
+    "PatientType": "Cat",
+    "StudyType": "NORMAL",
+    "QRCode": "",
+    "IsExported": false,
+    "IsEdited": false,
+    "WorkRef": "{\"RejectedStatus\":false}",
+    "IsAppended": false,
+    "CreationTime": "2025-05-16T09:58:23.6708694",
+    "MappedStatus": false,
+    "IsDelete": false
+  }
+]
+```
+
+### Dynamic Behavior: None
+
+### Delay: 500ms to simulate network latency
+
+### Error Handling: None

+ 57 - 1
src/API/patient/workActions.ts

@@ -1,4 +1,5 @@
 import axiosInstance from '../interceptor';
+import { View as ViewDetail } from './viewActions';
 
 interface View {
   view_id: string;
@@ -114,6 +115,52 @@ interface RegisterWorkResponse {
   };
 }
 
+export interface TaskListQuery {
+  patient_id?: string;
+  patient_name?: string;
+  start_date?: string;
+  end_date?: string;
+  access_number?: string;
+}
+
+export interface TaskListResponse {
+  StudyInstanceUID: string;
+  StudyID: string;
+  SpecificCharacterSet: string;
+  AccessionNumber: string;
+  PatientID: string;
+  PatientName: string;
+  DisplayPatientName: string;
+  PatientSize: string;
+  PatientAge: string;
+  PatientSex: string;
+  AdmittingTime: string;
+  RegSource: string;
+  StudyStatus: string;
+  RequestedProcedureID: string;
+  PerformedProtocolCodeValue: string;
+  PerformedProtocolCodeMeaning: string;
+  PerformedProcedureStepID: string;
+  StudyDescription: string;
+  StudyStartDatetime: string;
+  ScheduledProcedureStepStartDate: string;
+  StudyLock: string;
+  OperatorID: string;
+  Modality: string;
+  Views: ViewDetail[];
+  Thickness: number;
+  PatientType: string;
+  StudyType: string;
+  QRCode: string;
+  IsExported: boolean;
+  IsEdited: boolean;
+  WorkRef: string;
+  IsAppended: boolean;
+  CreationTime: string;
+  MappedStatus: boolean;
+  IsDelete: boolean;
+}
+
 const registerWork = async (
   work: RegisterInfo
 ): Promise<RegisterWorkResponse> => {
@@ -123,4 +170,13 @@ const registerWork = async (
   return response.data;
 };
 
-export { registerWork };
+const fetchTaskList = async (
+  query: TaskListQuery
+): Promise<TaskListResponse[]> => {
+  const response = await axiosInstance.get('/auth/studys', {
+    params: query,
+  });
+  return response.data;
+};
+
+export { registerWork, fetchTaskList };

+ 108 - 0
src/states/list_template/createListSlices.ts

@@ -0,0 +1,108 @@
+import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit';
+import {
+  EntitiesState,
+  FiltersState,
+  PaginationState,
+  SelectionState,
+  UIState,
+} from './type.model';
+
+export function createEntityListSlices<T, F extends object>(
+  namespace: string,
+  fetchThunk,
+  deleteThunk
+) {
+  const entitiesSlice = createSlice({
+    name: `${namespace}/entities`,
+    initialState: { data: [], total: 0 } as EntitiesState<T>,
+    reducers: {},
+    extraReducers(builder) {
+      builder.addCase(
+        fetchThunk.fulfilled,
+        (state, action: PayloadAction<{ data: T[]; total: number }>) => {
+          state.data = action.payload.data as unknown as Draft<T>[];
+          state.total = action.payload.total;
+          // return {
+          //   ...state,
+          //   data: action.payload.data,
+          //   total: action.payload.total,
+          // };
+        }
+      );
+      builder.addCase(
+        deleteThunk.fulfilled,
+        (state, action: PayloadAction<string[]>) => {
+          state.data = state.data.filter(
+            (item) => !action.payload.includes((item as { id: string }).id)
+          );
+        }
+      );
+    },
+  });
+
+  const filtersSlice = createSlice({
+    name: `${namespace}/filters`,
+    initialState: {} as FiltersState<F>,
+    reducers: {
+      setFilters(state, action: PayloadAction<Partial<FiltersState<F>>>) {
+        return { ...state, ...action.payload };
+      },
+      resetFilters() {
+        return {} as FiltersState<F>;
+      },
+    },
+  });
+
+  const paginationSlice = createSlice({
+    name: `${namespace}/pagination`,
+    initialState: { page: 1, pageSize: 10 } as PaginationState,
+    reducers: {
+      setPage(state, action: PayloadAction<number>) {
+        state.page = action.payload;
+      },
+      setPageSize(state, action: PayloadAction<number>) {
+        state.pageSize = action.payload;
+      },
+    },
+  });
+
+  const selectionSlice = createSlice({
+    name: `${namespace}/selection`,
+    initialState: { selectedIds: [] } as SelectionState,
+    reducers: {
+      setSelectedIds(state, action: PayloadAction<string[]>) {
+        state.selectedIds = action.payload;
+      },
+      clearSelection(state) {
+        state.selectedIds = [];
+      },
+    },
+  });
+
+  const uiSlice = createSlice({
+    name: `${namespace}/ui`,
+    initialState: { loading: false, error: null } as UIState,
+    reducers: {},
+    extraReducers(builder) {
+      builder.addCase(fetchThunk.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      });
+      builder.addCase(fetchThunk.fulfilled, (state) => {
+        state.loading = false;
+      });
+      builder.addCase(fetchThunk.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.error.message ?? 'Unknown error';
+      });
+    },
+  });
+
+  return {
+    entitiesSlice,
+    filtersSlice,
+    paginationSlice,
+    selectionSlice,
+    uiSlice,
+  };
+}

+ 27 - 0
src/states/list_template/thunk.factory.ts

@@ -0,0 +1,27 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+export function createFetchThunk<TFilter, TItem>(
+  namespace: string,
+  fetcher: (args: {
+    page: number;
+    pageSize: number;
+    filters: TFilter;
+  }) => Promise<{ data: TItem[]; total: number }>
+) {
+  return createAsyncThunk(
+    `${namespace}/fetch`,
+    async (args: { page: number; pageSize: number; filters: TFilter }) => {
+      return await fetcher(args);
+    }
+  );
+}
+
+export function createDeleteThunk(
+  namespace: string,
+  deleter: (ids: string[]) => Promise<void>
+) {
+  return createAsyncThunk(`${namespace}/delete`, async (ids: string[]) => {
+    await deleter(ids);
+    return ids;
+  });
+}

+ 23 - 0
src/states/list_template/type.model.ts

@@ -0,0 +1,23 @@
+export interface PaginationState {
+  page: number;
+  pageSize: number;
+}
+
+export type FiltersState<T extends object> = T & {
+  sortBy?: string;
+  sortOrder?: 'asc' | 'desc';
+};
+
+export interface SelectionState {
+  selectedIds: string[];
+}
+
+export interface UIState {
+  loading: boolean;
+  error?: string | null;
+}
+
+export interface EntitiesState<T> {
+  data: T[];
+  total: number;
+}

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

@@ -0,0 +1,56 @@
+import { createEntityListSlices } from '../../../list_template/createListSlices';
+import {
+  createFetchThunk,
+  createDeleteThunk,
+} from '../../../list_template/thunk.factory';
+import { work } from '../types/worklist';
+import { WorkFilter } from '../types/workfilter';
+
+// Define the fetch thunk
+import { fetchTaskList } from '../../../../API/patient/workActions';
+
+const fetchWorkThunk = createFetchThunk<WorkFilter, work>(
+  'worklist',
+  // 暂时不使用分页
+  // eslint-disable-next-line
+  async ({ page, pageSize, filters }) => {
+    const data = await fetchTaskList(filters);
+    return { data, total: data.length };
+  }
+);
+
+// Define the delete thunk
+const deleteWorkThunk = createDeleteThunk(
+  'worklist',
+  // eslint-disable-next-line
+  async (ids) => {
+    // todo 还没有实现API
+    // Implement the deleter function to delete worklist items
+    // await fetch(`/api/worklist`, {
+    //   method: 'DELETE',
+    //   headers: {
+    //     'Content-Type': 'application/json',
+    //   },
+    //   body: JSON.stringify({ ids }),
+    // });
+  }
+);
+
+// Create the worklist slices
+const {
+  entitiesSlice,
+  filtersSlice,
+  paginationSlice,
+  selectionSlice,
+  uiSlice,
+} = createEntityListSlices<work, WorkFilter>(
+  'worklist',
+  fetchWorkThunk,
+  deleteWorkThunk
+);
+
+export const workEntitiesSlice = entitiesSlice;
+export const workFiltersSlice = filtersSlice;
+export const workPaginationSlice = paginationSlice;
+export const workSelectionSlice = selectionSlice;
+export const workUISlice = uiSlice;

+ 1 - 0
src/states/patient/worklist/types/workfilter.ts

@@ -0,0 +1 @@
+export { TaskListQuery as WorkFilter } from '@/API/patient/workActions';

+ 1 - 0
src/states/patient/worklist/types/worklist.ts

@@ -0,0 +1 @@
+export { TaskListResponse as work } from '@/API/patient/workActions';

+ 12 - 0
src/states/store.ts

@@ -5,6 +5,13 @@ import patientTypeReducer from './patientTypeSlice';
 import bodyPartReducer from './bodyPartSlice';
 import selectionReducer from './patient/register/SelectionTypeSlice';
 import productReducer from './productSlice';
+import {
+  workEntitiesSlice,
+  workFiltersSlice,
+  workPaginationSlice,
+  workSelectionSlice,
+  workUISlice,
+} from './patient/worklist/slices/workSlice';
 
 const store = configureStore({
   reducer: {
@@ -14,6 +21,11 @@ const store = configureStore({
     bodyPart: bodyPartReducer,
     selection: selectionReducer,
     product: productReducer,
+    workEntities: workEntitiesSlice.reducer,
+    workFilters: workFiltersSlice.reducer,
+    workPagination: workPaginationSlice.reducer,
+    workSelection: workSelectionSlice.reducer,
+    workUI: workUISlice.reducer,
   },
 });