Explorar o código

feat (1.42.3 -> 1.43.0): 实现自定义标记保存功能,解决标记数据持久化问题 ; bug 120

- 在 API/mark.ts 中新增标记相关API接口,包括获取、新建、删除标记
- 在 states/view/markPanelSlice.ts 中重构状态管理,支持异步API调用和错误处理
- 在 pages/view/components/MarkPanel.tsx 中更新组件逻辑,实现标记的自动保存和加载

改动文件:
- src/API/mark.ts
- src/states/view/markPanelSlice.ts
- src/pages/view/components/MarkPanel.tsx
dengdx hai 1 semana
pai
achega
f6807e7a9e
Modificáronse 5 ficheiros con 227 adicións e 57 borrados
  1. 14 1
      CHANGELOG.md
  2. 1 1
      package.json
  3. 69 0
      src/API/mark.ts
  4. 33 32
      src/pages/view/components/MarkPanel.tsx
  5. 110 23
      src/states/view/markPanelSlice.ts

+ 14 - 1
CHANGELOG.md

@@ -2,6 +2,20 @@
 
 本项目的所有重要变更都将记录在此文件中。
 
+## [1.43.0] - 2026-01-03 16:16
+
+### 新增 (Added)
+
+- **实现自定义标记保存功能** - 修复标记数据无法持久化问题
+  - 在 API/mark.ts 中新增标记相关API接口,支持获取、新建、删除标记
+  - 在 states/view/markPanelSlice.ts 中重构状态管理,支持异步API调用和错误处理
+  - 在 pages/view/components/MarkPanel.tsx 中更新组件逻辑,实现标记的自动保存和加载
+
+**改动文件:**
+- src/API/mark.ts
+- src/states/view/markPanelSlice.ts
+- src/pages/view/components/MarkPanel.tsx
+
 ## [1.42.3] - 2026-01-03 13:51
 
 ### 修复 (Fixed)
@@ -32,4 +46,3 @@
 **改动文件:**
 - src/components/GenericDataTable.tsx
 - src/pages/patient/components/WorklistTable.tsx
-

+ 1 - 1
package.json

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

+ 69 - 0
src/API/mark.ts

@@ -0,0 +1,69 @@
+// src/API/mark.ts
+// 标记相关API接口
+
+import axiosInstance from './interceptor';
+
+/**
+ * 标记数据类型
+ */
+export interface Mark {
+  id: number;
+  mark: string;
+  user_id: number;
+}
+
+/**
+ * 标记列表响应类型
+ */
+export interface MarkListResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: {
+    '@type': string;
+    count: number;
+    marks: Mark[];
+  };
+}
+
+/**
+ * 通用响应类型
+ */
+export interface ApiResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: any;
+}
+
+/**
+ * 获取标记列表
+ * @param scope 查询范围,'all' 或 'mine'
+ * @returns Promise<MarkListResponse>
+ */
+export async function fetchMarks(scope: 'all' | 'mine' = 'all'): Promise<MarkListResponse> {
+  const response = await axiosInstance.get(`/auth/protocol/mark`, {
+    params: { scope },
+  });
+  return response.data;
+}
+
+/**
+ * 新建标记
+ * @param mark 标记文本
+ * @returns Promise<ApiResponse>
+ */
+export async function createMark(mark: string): Promise<ApiResponse> {
+  const response = await axiosInstance.post(`/auth/protocol/mark`, { mark });
+  return response.data;
+}
+
+/**
+ * 删除标记
+ * @param markId 标记ID
+ * @returns Promise<ApiResponse>
+ */
+export async function deleteMark(markId: number): Promise<ApiResponse> {
+  const response = await axiosInstance.delete(`/auth/report/protocol/mark/${markId}`);
+  return response.data;
+}

+ 33 - 32
src/pages/view/components/MarkPanel.tsx

@@ -1,10 +1,12 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 import { Layout, Button, Typography, Input, Select, Flex, Divider } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import { useDispatch } from 'react-redux';
 import { switchToOperationPanel } from '../../../states/panelSwitchSliceForView';
 import {
-  addCustomMarkToList,
+  fetchMarksAsync,
+  addMarkAsync,
+  deleteMarkAsync,
   setSelectedMark,
   setInputText,
   clearInputText,
@@ -107,19 +109,32 @@ const MarkPanel = () => {
   const selectedMark = useAppSelector(selectSelectedMark);
   const inputText = useAppSelector(selectInputText);
 
+  // 组件挂载时获取标记列表
+  useEffect(() => {
+    dispatch(fetchMarksAsync() as any);
+  }, [dispatch]);
+
   const handleReturn = () => {
     dispatch(switchToOperationPanel());
   };
 
   const handleAddCustomMark = () => {
     if (inputText.trim()) {
-      dispatch(addCustomMarkToList(inputText.trim()));
-      dispatch(clearInputText());
+      dispatch(addMarkAsync(inputText.trim()) as any).then(() => {
+        dispatch(clearInputText());
+        // 重新获取列表以刷新数据
+        dispatch(fetchMarksAsync() as any);
+      });
     }
   };
 
   const handleSelectMark = (value: string) => {
     dispatch(setSelectedMark(value));
+    // 选择时自动添加到图像
+    if (value) {
+      dispatch({ type: 'functionArea/setAction', payload: `AddPredefinedMark:${value}` });
+      console.log(`添加自定义标记: ${value}`);
+    }
   };
 
   const handlePredefinedMarkClick = (markType: string) => {
@@ -194,34 +209,20 @@ const MarkPanel = () => {
             </Button>
           </Flex>
 
-          {/* 标记选择下拉框和添加按钮 */}
-          <Flex gap="small" align="center">
-            <Select
-              value={selectedMark}
-              onChange={handleSelectMark}
-              placeholder="选择要添加的标记"
-              style={{ flex: 1 }}
-              disabled={customMarks.length === 0}
-            >
-              {customMarks.map((mark, index) => (
-                <Option key={`${mark}-${index}`} value={mark}>
-                  {mark}
-                </Option>
-              ))}
-            </Select>
-            <Button
-              type="primary"
-              onClick={() => {
-                if (selectedMark) {
-                  dispatch({ type: 'functionArea/setAction', payload: `AddPredefinedMark:${selectedMark}` });
-                  console.log(`添加自定义标记: ${selectedMark}`);
-                }
-              }}
-              disabled={!selectedMark}
-            >
-              添加
-            </Button>
-          </Flex>
+          {/* 标记选择下拉框 */}
+          <Select
+            className='w-full'
+            value={selectedMark}
+            onChange={handleSelectMark}
+            placeholder="选择要添加的标记(选择后自动添加到图像)"
+            disabled={customMarks.length === 0}
+          >
+            {customMarks.map((mark, index) => (
+              <Option key={`${mark.id}-${index}`} value={mark.mark}>
+                {mark.mark}
+              </Option>
+            ))}
+          </Select>
         </div>
 
         <Divider />

+ 110 - 23
src/states/view/markPanelSlice.ts

@@ -1,10 +1,15 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { message } from 'antd';
+import { fetchMarks, createMark, deleteMark, Mark } from '../../API/mark';
 
 interface MarkPanelState {
-  customMarks: string[];        // 用户添加的自定义标记列表
+  customMarks: Mark[];          // 用户添加的自定义标记列表(包含ID)
   selectedMark: string;         // 当前选中的标记文本
   inputText: string;           // 输入框中的文本
   selectedViewportIds: string[]; // 选中的视口ID
+  loading: boolean;            // 加载状态
+  error: string | null;        // 错误信息
 }
 
 const initialState: MarkPanelState = {
@@ -12,24 +17,65 @@ const initialState: MarkPanelState = {
   selectedMark: '',
   inputText: '',
   selectedViewportIds: [],
+  loading: false,
+  error: null,
 };
 
+// 异步 thunk
+export const fetchMarksAsync = createAsyncThunk(
+  'markPanel/fetchMarks',
+  async (scope: 'all' | 'mine' = 'all', { rejectWithValue }) => {
+    try {
+      const response = await fetchMarks(scope);
+      if (response.code === '0x000000') {
+        return response.data.marks;
+      } else {
+        return rejectWithValue(response.description || '获取标记列表失败');
+      }
+    } catch (error: any) {
+      return rejectWithValue(error.message || '网络错误');
+    }
+  }
+);
+
+export const addMarkAsync = createAsyncThunk(
+  'markPanel/addMark',
+  async (markText: string, { rejectWithValue }) => {
+    try {
+      const response = await createMark(markText);
+      if (response.code === '0x000000') {
+        message.success('标记添加成功');
+        return { mark: markText };
+      } else {
+        return rejectWithValue(response.description || '添加标记失败');
+      }
+    } catch (error: any) {
+      return rejectWithValue(error.message || '网络错误');
+    }
+  }
+);
+
+export const deleteMarkAsync = createAsyncThunk(
+  'markPanel/deleteMark',
+  async (markId: number, { rejectWithValue }) => {
+    try {
+      const response = await deleteMark(markId);
+      if (response.code === '0x000000') {
+        message.success('标记删除成功');
+        return markId;
+      } else {
+        return rejectWithValue(response.description || '删除标记失败');
+      }
+    } catch (error: any) {
+      return rejectWithValue(error.message || '网络错误');
+    }
+  }
+);
+
 const markPanelSlice = createSlice({
   name: 'markPanel',
   initialState,
   reducers: {
-    // 添加自定义标记到列表
-    addCustomMarkToList: (state, action: PayloadAction<string>) => {
-      const text = action.payload.trim();
-      if (text && !state.customMarks.includes(text)) {
-        state.customMarks.unshift(text); // 添加到列表开头
-        // 限制列表长度,防止性能问题
-        if (state.customMarks.length > 50) {
-          state.customMarks = state.customMarks.slice(0, 50);
-        }
-      }
-    },
-
     // 设置选中的标记
     setSelectedMark: (state, action: PayloadAction<string>) => {
       state.selectedMark = action.payload;
@@ -50,14 +96,6 @@ const markPanelSlice = createSlice({
       state.inputText = '';
     },
 
-    // 删除自定义标记
-    removeCustomMark: (state, action: PayloadAction<string>) => {
-      state.customMarks = state.customMarks.filter(mark => mark !== action.payload);
-      if (state.selectedMark === action.payload) {
-        state.selectedMark = '';
-      }
-    },
-
     // 清空所有自定义标记
     clearAllCustomMarks: (state) => {
       state.customMarks = [];
@@ -65,15 +103,64 @@ const markPanelSlice = createSlice({
       state.inputText = '';
     },
   },
+  extraReducers: (builder) => {
+    builder
+      // fetchMarksAsync
+      .addCase(fetchMarksAsync.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(fetchMarksAsync.fulfilled, (state, action) => {
+        state.loading = false;
+        state.customMarks = action.payload;
+      })
+      .addCase(fetchMarksAsync.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.payload as string;
+        message.error(state.error);
+      })
+      // addMarkAsync
+      .addCase(addMarkAsync.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(addMarkAsync.fulfilled, (state, action) => {
+        state.loading = false;
+        // 刷新列表
+        // 这里可以选择重新获取列表或者手动添加
+        // 暂时手动添加,假设后端返回了完整的mark对象
+        // 实际情况可能需要重新调用 fetchMarksAsync
+      })
+      .addCase(addMarkAsync.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.payload as string;
+        message.error(state.error);
+      })
+      // deleteMarkAsync
+      .addCase(deleteMarkAsync.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(deleteMarkAsync.fulfilled, (state, action) => {
+        state.loading = false;
+        state.customMarks = state.customMarks.filter(mark => mark.id !== action.payload);
+        if (state.selectedMark === state.customMarks.find(m => m.id === action.payload)?.mark) {
+          state.selectedMark = '';
+        }
+      })
+      .addCase(deleteMarkAsync.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.payload as string;
+        message.error(state.error);
+      });
+  },
 });
 
 export const {
-  addCustomMarkToList,
   setSelectedMark,
   setInputText,
   setSelectedViewportIds,
   clearInputText,
-  removeCustomMark,
   clearAllCustomMarks,
 } = markPanelSlice.actions;