Quellcode durchsuchen

feat: implement conditional thumbnail logic based on expose_status in src/states/patient/worklist/slices/thumbnailListSlice.ts

- Add thumbnail list slice with conditional logic for exposed/unexposed images
- Use getExposedImageUrl() for exposed images and getViewIconUrl() for view icons
- Add async view detail fetching to obtain view_icon_name
- Include proper error handling and fallback mechanisms
- Update store configuration and related components
sw vor 2 Wochen
Ursprung
Commit
a0aa93ff59

+ 2 - 2
config/dev.ts

@@ -11,7 +11,7 @@ export default {
     stats: true,
   },
   defineConstants: {
-    MQTT_BROKER_URL_FROM_WEBPACK: '"ws://192.168.110.13:8083/mqtt"',
+    MQTT_BROKER_URL_FROM_WEBPACK: '"ws://192.168.110.112:8083/mqtt"',
   },
   mini: {},
   h5: {
@@ -20,7 +20,7 @@ export default {
     devServer: {
       proxy: {
         '/dr': {
-          target: 'http://192.168.110.13:6001', // 你的后端服务地址
+          target: 'http://192.168.110.112:6001', // 你的后端服务地址
           changeOrigin: true, // 允许跨域
           // pathRewrite: {
           //   '^/dr/api': '' // 可选,用于重写路径

+ 20 - 11
src/pages/patient/HistoryList.tsx

@@ -7,9 +7,11 @@ import {
   fetchWorkThunk,
   historySelectionSlice,
 } 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 { RootState, AppDispatch } from '../../states/store';
 import { Task } from '@/domain/work';
 import worklistToExam from '../../domain/patient/worklistToExam';
@@ -51,7 +53,9 @@ const HistorylistPage: React.FC = () => {
   const handleRowClick = (record: Task) => {
     console.log('Row clicked:', JSON.stringify(record, null, 2));
     console.log('Selected IDs before:', record.StudyInstanceUID);
-    dispatch(historySelectionSlice.actions.setSelectedIds([record.StudyID]));
+    const newSelectedIds = [record.StudyID];
+    dispatch(historySelectionSlice.actions.setSelectedIds(newSelectedIds));
+    dispatch(updateThumbnailsFromHistorySelection(newSelectedIds));
   };
 
   const handleRowDoubleClick = (record: Task) => {
@@ -105,17 +109,22 @@ const HistorylistPage: React.FC = () => {
         <Row className="h-full">
           <Col
             span={screens.lg ? 18 : screens.md ? 20 : 24}
-            className="overflow-auto"
+            className="h-full flex flex-col"
           >
-            <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>
+            <div className="h-60 border-t border-gray-200">
+              <ThumbnailList />
+            </div>
           </Col>
           <Col
             span={screens.lg ? 6 : screens.md ? 4 : 0}

+ 129 - 0
src/pages/patient/components/ThumbnailList.tsx

@@ -0,0 +1,129 @@
+import React from 'react';
+import { Row, Col, Image, Card, Typography, Tag, Empty } from 'antd';
+import { FormattedMessage } from 'react-intl';
+import { useSelector } from 'react-redux';
+import { RootState } from '@/states/store';
+import { dview } from '@/domain/dview';
+
+const { Text, Title } = Typography;
+
+interface ThumbnailListProps {
+  className?: string;
+}
+
+const ThumbnailList: React.FC<ThumbnailListProps> = ({ className }) => {
+  const thumbnails = useSelector(
+    (state: RootState) => state.thumbnailList.thumbnails
+  );
+  const loading = useSelector(
+    (state: RootState) => state.thumbnailList.loading
+  );
+  const error = useSelector((state: RootState) => state.thumbnailList.error);
+
+  if (error) {
+    return (
+      <div className={`p-4 ${className}`}>
+        <Text type="danger">{error}</Text>
+      </div>
+    );
+  }
+
+  if (loading) {
+    return (
+      <div className={`p-4 ${className}`}>
+        <Text>
+          <FormattedMessage id="loading" defaultMessage="Loading..." />
+        </Text>
+      </div>
+    );
+  }
+
+  if (thumbnails.length === 0) {
+    return (
+      <div className={`p-4 ${className}`}>
+        <Empty
+          description={
+            <FormattedMessage
+              id="thumbnailList.noImages"
+              defaultMessage="No images to display"
+            />
+          }
+        />
+      </div>
+    );
+  }
+
+  return (
+    <div className={`p-4 overflow-auto ${className}`}>
+      <Title level={5} className="mb-3">
+        <FormattedMessage
+          id="thumbnailList.title"
+          defaultMessage="Image Thumbnails"
+        />
+      </Title>
+      <Row gutter={[16, 16]}>
+        {thumbnails.map((thumbnail: dview) => (
+          <Col xs={24} sm={12} md={8} lg={6} xl={4} key={thumbnail.view_id}>
+            <Card
+              size="small"
+              hoverable
+              cover={
+                <div className="relative">
+                  <Image
+                    src={thumbnail.thumbnail_file}
+                    alt={thumbnail.view_description || thumbnail.view_id}
+                    style={{
+                      width: '100%',
+                      height: '120px',
+                      objectFit: 'cover',
+                    }}
+                    fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3Ik1RUG8M+Q4cMRcNHuS2BAYiBD/PIOCSFfYQ6QHBI7XhAGLpA5v4RFmEQGBFCCMxkYHJw9DQpIKjNZ/lN6JHV9937d8/o2+32uj9/t6j7n3F8nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+                    preview={{
+                      mask: (
+                        <div className="text-white text-center">
+                          <FormattedMessage
+                            id="preview"
+                            defaultMessage="Preview"
+                          />
+                        </div>
+                      ),
+                    }}
+                  />
+                  <div className="absolute top-1 right-1">
+                    <Tag
+                      color={
+                        thumbnail.expose_status === 'Exposed'
+                          ? 'green'
+                          : 'orange'
+                      }
+                    >
+                      {thumbnail.expose_status}
+                    </Tag>
+                  </div>
+                </div>
+              }
+            >
+              <Card.Meta
+                title={
+                  <Text
+                    ellipsis={{ tooltip: true }}
+                    style={{ fontSize: '12px' }}
+                  >
+                    {thumbnail.view_description || thumbnail.view_type}
+                  </Text>
+                }
+                description={
+                  <Text type="secondary" style={{ fontSize: '10px' }}>
+                    ID: {thumbnail.view_id}
+                  </Text>
+                }
+              />
+            </Card>
+          </Col>
+        ))}
+      </Row>
+    </div>
+  );
+};
+
+export default ThumbnailList;

+ 20 - 11
src/pages/patient/worklist.tsx

@@ -7,8 +7,10 @@ import {
   fetchWorkThunk,
   workSelectionSlice,
 } 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 { RootState, AppDispatch } from '../../states/store';
 import { Task } from '@/domain/work';
 import worklistToExam from '../../domain/patient/worklistToExam';
@@ -47,7 +49,9 @@ const WorklistPage: React.FC = () => {
   const handleRowClick = (record: Task) => {
     console.log('Row clicked:', JSON.stringify(record, null, 2));
     console.log('Selected IDs before:', record.StudyID);
-    dispatch(workSelectionSlice.actions.setSelectedIds([record.StudyID]));
+    const newSelectedIds = [record.StudyID];
+    dispatch(workSelectionSlice.actions.setSelectedIds(newSelectedIds));
+    dispatch(updateThumbnailsFromWorkSelection(newSelectedIds));
   };
 
   const handleRowDoubleClick = (record: Task) => {
@@ -97,17 +101,22 @@ const WorklistPage: React.FC = () => {
         <Row className="h-full">
           <Col
             span={screens.lg ? 18 : screens.md ? 20 : 24}
-            className="overflow-auto"
+            className="h-full flex flex-col"
           >
-            <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>
+            <div className="h-60 border-t border-gray-200">
+              <ThumbnailList />
+            </div>
           </Col>
           <Col
             span={screens.lg ? 6 : screens.md ? 4 : 0}

+ 206 - 0
src/states/patient/worklist/slices/thumbnailListSlice.ts

@@ -0,0 +1,206 @@
+import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
+import { dview } from '@/domain/dview';
+import { Task } from '@/domain/work';
+import { workSelectionSlice } from './workSlice';
+import { historySelectionSlice } from './history';
+import { fetchTaskDetails } from '@/API/patient/workActions';
+import { fetchViewDetail } from '@/API/patient/viewActions';
+import { Series } from '@/domain/series';
+import { XImage } from '@/domain/xImage';
+import { getExposedImageUrl, getViewIconUrl } from '@/API/bodyPosition';
+
+interface ThumbnailListState {
+  thumbnails: dview[];
+  loading: boolean;
+  error: string | null;
+}
+
+const initialState: ThumbnailListState = {
+  thumbnails: [],
+  loading: false,
+  error: null,
+};
+
+async function getAllDView(selectedIds: string[]) {
+  const allViews: dview[] = [];
+  for (const taskId of selectedIds) {
+    try {
+      // 通过 API 获取任务详情
+      const taskDetails = await fetchTaskDetails(taskId);
+
+      // 将获取的数据转换为 dview 类型
+      const views = await Promise.all(
+        taskDetails.series.flatMap<Promise<dview>>((series: Series) =>
+          series.images.map<Promise<dview>>(async (image: XImage) => {
+            // 获取view详情以获得view_icon_name
+            let viewDetail;
+            try {
+              viewDetail = await fetchViewDetail(image.view_id);
+            } catch (error) {
+              console.error(
+                `Error fetching view detail for ${image.view_id}:`,
+                error
+              );
+              viewDetail = null;
+            }
+
+            // 基于expose_status设置thumbnail_file
+            const thumbnail_file =
+              image.expose_status === 'Exposed'
+                ? getExposedImageUrl(image.sop_instance_uid)
+                : viewDetail?.view_icon_name
+                  ? getViewIconUrl(viewDetail.view_icon_name)
+                  : '';
+
+            return {
+              view_id: image.view_id,
+              series_instance_uid: series.series_instance_uid,
+              study_instance_uid: taskDetails.study_instance_uid,
+              study_id: taskDetails.study_id,
+              procedure_id: series.procedure_id,
+              view_description: image.view_description,
+              view_type: '',
+              PrimarySopUID: image.sop_instance_uid,
+              expose_status: image.expose_status,
+              image_file_path: image.image_file_path,
+              image_file: image.image_file_path,
+              thumbnail_file,
+            } satisfies dview;
+          })
+        )
+      );
+
+      allViews.push(...views);
+    } catch (error) {
+      console.error(`Error fetching details for task ${taskId}:`, error);
+    }
+  }
+
+  return allViews;
+}
+
+// 创建 thunk 来处理工作列表选中项变化
+export const updateThumbnailsFromWorkSelection = createAsyncThunk(
+  'thumbnailList/updateFromWorkSelection',
+  async (selectedIds: string[]) => {
+    if (selectedIds.length === 0) {
+      return [];
+    }
+
+    const allViews: dview[] = getAllDView(selectedIds) as unknown as dview[];
+
+    return allViews;
+  }
+);
+
+// 创建 thunk 来处理历史列表选中项变化
+export const updateThumbnailsFromHistorySelection = createAsyncThunk(
+  'thumbnailList/updateFromHistorySelection',
+  async (selectedIds: string[]) => {
+    if (selectedIds.length === 0) {
+      return [];
+    }
+
+    const allViews: dview[] = getAllDView(selectedIds) as unknown as dview[];
+
+    return allViews;
+  }
+);
+
+const thumbnailListSlice = createSlice({
+  name: 'thumbnailList',
+  initialState,
+  reducers: {
+    setThumbnails: (state, action: PayloadAction<dview[]>) => {
+      state.thumbnails = action.payload;
+      state.loading = false;
+      state.error = null;
+    },
+    setLoading: (state, action: PayloadAction<boolean>) => {
+      state.loading = action.payload;
+    },
+    setError: (state, action: PayloadAction<string>) => {
+      state.error = action.payload;
+      state.loading = false;
+    },
+    clearThumbnails: (state) => {
+      state.thumbnails = [];
+      state.loading = false;
+      state.error = null;
+    },
+    updateThumbnailsFromSelection: (
+      state,
+      action: PayloadAction<{ selectedIds: string[]; tasksData: Task[] }>
+    ) => {
+      const { selectedIds, tasksData } = action.payload;
+
+      if (selectedIds.length === 0) {
+        state.thumbnails = [];
+        state.loading = false;
+        return;
+      }
+
+      // 找到选中的任务并提取所有视图
+      const selectedTasks = tasksData.filter((task) =>
+        selectedIds.includes(task.StudyID)
+      );
+
+      const allViews = selectedTasks.flatMap((task) => task.Views || []);
+      state.thumbnails = allViews;
+      state.loading = false;
+      state.error = null;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      // eslint-disable-next-line
+      .addCase(workSelectionSlice.actions.setSelectedIds, (state, action) => {
+        
+        state.loading = true;
+      })
+      // eslint-disable-next-line
+      .addCase(historySelectionSlice.actions.setSelectedIds, (state, action) => {
+          state.loading = true;
+        }
+      )
+      .addCase(updateThumbnailsFromWorkSelection.pending, (state) => {
+        state.loading = true;
+      })
+      .addCase(updateThumbnailsFromWorkSelection.fulfilled, (state, action) => {
+        state.thumbnails = action.payload;
+        state.loading = false;
+        state.error = null;
+      })
+      .addCase(updateThumbnailsFromWorkSelection.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.error.message || 'Failed to update thumbnails';
+      })
+      .addCase(updateThumbnailsFromHistorySelection.pending, (state) => {
+        state.loading = true;
+      })
+      .addCase(
+        updateThumbnailsFromHistorySelection.fulfilled,
+        (state, action) => {
+          state.thumbnails = action.payload;
+          state.loading = false;
+          state.error = null;
+        }
+      )
+      .addCase(
+        updateThumbnailsFromHistorySelection.rejected,
+        (state, action) => {
+          state.loading = false;
+          state.error = action.error.message || 'Failed to update thumbnails';
+        }
+      );
+  },
+});
+
+export const {
+  setThumbnails,
+  setLoading,
+  setError,
+  clearThumbnails,
+  updateThumbnailsFromSelection,
+} = thumbnailListSlice.actions;
+export default thumbnailListSlice.reducer;

+ 2 - 0
src/states/store.ts

@@ -38,6 +38,7 @@ import generatorMonitorReducer from './exam/generatorMonitorSlice';
 import largeScreenReducer from './exam/largeScreenSlice';
 import deviceAreaReducer from './exam/deviceAreaSlice';
 import historyPanelSwitchReducer from './patient/worklist/slices/historyPanelSwitchSlice';
+import thumbnailListReducer from './patient/worklist/slices/thumbnailListSlice';
 import panelSwitchForViewReducer from './panelSwitchSliceForView';
 import quotaModalReducer from './security/quotaModalSlice';
 import quotaReducer from './security/quotaSlice';
@@ -87,6 +88,7 @@ const store = configureStore({
     largeScreen: largeScreenReducer,
     deviceArea: deviceAreaReducer,
     historyPanelSwitch: historyPanelSwitchReducer,
+    thumbnailList: thumbnailListReducer,
     panelSwitchForView: panelSwitchForViewReducer,
     quotaModal: quotaModalReducer,
     quota: quotaReducer,