Selaa lähdekoodia

添加图像高级处理的UI,部分实现(未联调)

sw 1 kuukausi sitten
vanhempi
commit
fae74b6d7c

+ 157 - 25
docs/DR.md

@@ -2924,7 +2924,139 @@ zh_CN
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8rAXoaHSK/60d74d30133e498c94a386e6d42657f9)
 
 
-## 18	登记检查信息
+## 18	图像处理
+
+## 18.1	获取当前图像处理参数
+
+> GET  /dr/api/v1/auth/image/{id}/preparation
+### 接口说明
+> 完成图像处理准备工作,并返回增益、细节、动态范围、噪声模式当前值
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||sop_instance_uid|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"data": {
+		"contrast": 2.1,
+		"detail": 2.2,
+		"latitude": 2.3,
+		"noise": 2.4
+	},
+	"description": "Success",
+	"solution": ""
+}
+```
+contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKX8qDZmEa/b6ea7f763c554e0f92529c72a69952b2)
+
+## 18.2	获取处理后dcm
+
+> GET  /dr/api/v1/auth/image/{id}/proc
+### 接口说明
+> 获取应用增益、细节、动态范围、噪声模式后的dcm
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||sop_instance_uid|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求参数(Query Param)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|contrast||增益 范围0.0~9.0 步长0.1|
+|detail||细节 范围0.0~9.0 步长0.1|
+|latitude||动态范围 范围0.0~9.0 步长0.1|
+|noise||噪声模式 范围0.0~9.0 步长0.1|
+### 响应体
+● 200: OK 响应数据格式:File
+直接返回dcm文件报错时:返500
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKX7EtxRZ2/d6c692ba28044670955f7579185ff16f)
+
+## 18.3	保存图像处理参数
+
+> POST  /dr/api/v1/auth/image/{id}/proc
+### 接口说明
+> 保存应用增益、细节、动态范围、噪声模式后的dcm
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||sop_instance_uid|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求体(Request Body)
+| 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
+| ------ | ------ | ------ | ------ | ------ |
+| contrast|number||true|增益 范围0.0~9.0 步长0.1|
+| detail|string||true|细节 范围0.0~9.0 步长0.1|
+| latitude|string||true|动态范围 范围0.0~9.0 步长0.1|
+| noise|string||true|噪声模式 范围0.0~9.0 步长0.1|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"data": {},
+	"description": "Success",
+	"solution": ""
+}
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKWwC47kkS/6f786bee6b8c4489b37c04c9793e8843)
+
+## 18.4	保存窗宽窗位到dcm
+
+> POST  /dr/api/v1/auth/image/{id}/wcww
+### 接口说明
+> 保存窗宽窗位到dcm
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||sop_instance_uid|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求体(Request Body)
+| 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
+| ------ | ------ | ------ | ------ | ------ |
+| window_center|number||true|窗位|
+| window_width|number||true|窗宽|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"data": {},
+	"description": "Success",
+	"solution": ""
+}
+```
+
+
+## 19	登记检查信息
 
 > POST  /dr/api/v1/auth/study
 ### 接口说明
@@ -3123,7 +3255,7 @@ zh_CN
 ```
 
 
-## 19	变更登记信息
+## 20	变更登记信息
 
 > PUT  /dr/api/v1/auth/study/{id}
 ### 接口说明
@@ -3318,7 +3450,7 @@ zh_CN
 ```
 
 
-## 20	锁定检查信息(不可删除)
+## 21	锁定检查信息(不可删除)
 
 > PUT  /dr/api/v1/auth/study/{id}/lock
 ### 接口说明
@@ -3352,7 +3484,7 @@ lock可选项
 Locked
 Unlocked
 
-## 21	获取检查信息列表
+## 22	获取检查信息列表
 
 > GET  /dr/api/v1/auth/study/
 ### 接口说明
@@ -3516,7 +3648,7 @@ Completed|
 ```
 
 
-## 22	获取检查信息
+## 23	获取检查信息
 
 > GET  /dr/api/v1/auth/study/{id}
 ### 接口说明
@@ -3750,7 +3882,7 @@ Completed|
 ```
 
 
-## 23	获取检查信息状态
+## 24	获取检查信息状态
 
 > GET  /dr/api/v1/auth/study/{id}/stat
 ### 接口说明
@@ -3782,7 +3914,7 @@ Completed|
 ```
 
 
-## 24	删除检查信息(批量)
+## 25	删除检查信息(批量)
 
 > DELETE  /dr/api/v1/auth/study
 ### 接口说明
@@ -3817,7 +3949,7 @@ Completed|
 ```
 
 
-## 25	[Study]存储拍摄的急诊患者影像
+## 26	[Study]存储拍摄的急诊患者影像
 
 > POST  /api/v1/auth/study/portrait
 ### 接口说明
@@ -3853,7 +3985,7 @@ Completed|
 ```
 
 
-## 26	[Study]批量添加体位和协议
+## 27	[Study]批量添加体位和协议
 
 > POST  /dr/api/v1/auth/image
 ### 接口说明
@@ -3968,7 +4100,7 @@ Completed|
 ```
 
 
-## 27	[Study]复制体位
+## 28	[Study]复制体位
 
 > POST  /dr/api/v1/auth/image/copy
 ### 接口说明
@@ -4032,7 +4164,7 @@ Completed|
 ```
 
 
-## 28	[Study]体位重新排序
+## 29	[Study]体位重新排序
 
 > POST  /dr/api/v1/auth/image/sort
 ### 接口说明
@@ -4082,7 +4214,7 @@ Completed|
 ```
 
 
-## 29	[Study]删除体位
+## 30	[Study]删除体位
 
 > DELETE  /dr/api/v1/auth/image/:id
 ### 接口说明
@@ -4110,7 +4242,7 @@ Completed|
 示例请求
 DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.1749539018.668478
 
-## 30	[Study]图像另存为
+## 31	[Study]图像另存为
 
 > POST  /dr/api/v1/auth/image/save_as
 ### 接口说明
@@ -4141,7 +4273,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 31	[Study]存储后处理dcm
+## 32	[Study]存储后处理dcm
 
 > POST  /api/v1/auth/image/post_proc
 ### 接口说明
@@ -4175,7 +4307,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 32	报告预览
+## 33	报告预览
 
 > POST  /dr/api/v1/auth/report/preview
 ### 接口说明
@@ -4233,7 +4365,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 宠物示例:
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/9/23/2LYIk58dZYG/74547ae49660497f90c3db8806c87a72)
 
-## 33	保存报告
+## 34	保存报告
 
 > POST  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -4299,7 +4431,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 示例
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/9/19/2LRW3rk4TRY/74ef9c252fcb4627bafd24e0e0752204)
 
-## 34	获取报告
+## 35	获取报告
 
 > GET  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -4320,7 +4452,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 请求示例
 /api/v1/auth/study/20250912063732016/report
 
-## 35	[Device]打开设备
+## 36	[Device]打开设备
 
 > POST  /api/v1/auth/device/open
 ### 接口说明
@@ -4351,7 +4483,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 36	[Device]执行Get
+## 37	[Device]执行Get
 
 > POST  /api/v1/auth/device/get
 ### 接口说明
@@ -4383,7 +4515,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 37	[Device]执行Action
+## 38	[Device]执行Action
 
 > POST  /api/v1/auth/device/action
 ### 接口说明
@@ -4418,7 +4550,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 38	[Task]开始检查
+## 39	[Task]开始检查
 
 > POST  /api/v1/auth/task/inspection/start
 ### 接口说明
@@ -4453,7 +4585,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 39	[Task]获取全局状态
+## 40	[Task]获取全局状态
 
 > GET  /api/v1/auth/task/inspection/status
 ### 接口说明
@@ -4500,7 +4632,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 40	[Task]软曝光(包含状态推送说明)
+## 41	[Task]软曝光(包含状态推送说明)
 
 > POST  /api/v1/auth/task/inspection/trigger
 ### 接口说明
@@ -4560,7 +4692,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 41	[Task]接受拒绝图像
+## 42	[Task]接受拒绝图像
 
 > POST  /api/v1/auth/task/inspection/judge
 ### 接口说明
@@ -4596,7 +4728,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 42	[Task]挂起或完成study
+## 43	[Task]挂起或完成study
 
 > POST  /api/v1/auth/task/inspection/leave
 ### 接口说明

+ 187 - 0
src/API/imageActions.ts

@@ -164,3 +164,190 @@ export const saveImageAs = async (
     throw error;
   }
 };
+
+/**
+ * 图像处理参数响应类型
+ */
+export interface GetProcessingParamsResponse {
+  /** 响应码 */
+  code: string;
+  /** 描述信息 */
+  description: string;
+  /** 解决方案 */
+  solution: string;
+  /** 数据内容 */
+  data: {
+    /** 增益 */
+    contrast: number;
+    /** 细节 */
+    detail: number;
+    /** 动态范围 */
+    latitude: number;
+    /** 噪声模式 */
+    noise: number;
+  };
+}
+
+/**
+ * 获取当前图像处理参数
+ * 完成图像处理准备工作,并返回增益、细节、动态范围、噪声模式当前值
+ * 
+ * @param sopInstanceUid 图像实例 UID (SOP Instance UID)
+ * @returns 当前图像处理参数
+ * @throws 当获取失败时抛出错误
+ * 
+ * @example
+ * ```typescript
+ * const params = await getImageProcessingParams('1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671');
+ * console.log('当前增益:', params.data.contrast);
+ * ```
+ */
+export const getImageProcessingParams = async (
+  sopInstanceUid: string
+): Promise<GetProcessingParamsResponse> => {
+  try {
+    const response = await axiosInstance.get<GetProcessingParamsResponse>(
+      `/auth/image/${sopInstanceUid}/preparation`
+    );
+
+    if (response.data.code !== '0x000000') {
+      throw new Error(`获取图像处理参数失败: ${response.data.description}`);
+    }
+
+    return response.data;
+  } catch (error) {
+    console.error('Error getting image processing params:', error);
+    throw error;
+  }
+};
+
+/**
+ * 保存图像处理参数请求类型
+ */
+export interface SaveProcessingParamsRequest {
+  /** 增益 */
+  contrast: number;
+  /** 细节 */
+  detail: number;
+  /** 动态范围 */
+  latitude: number;
+  /** 噪声模式 */
+  noise: number;
+}
+
+/**
+ * 保存图像处理参数响应类型
+ */
+export interface SaveProcessingParamsResponse {
+  /** 响应码 */
+  code: string;
+  /** 描述信息 */
+  description: string;
+  /** 解决方案 */
+  solution: string;
+  /** 数据内容 */
+  data: Record<string, never>;
+}
+
+/**
+ * 保存图像处理参数
+ * 保存应用增益、细节、动态范围、噪声模式后的 dcm
+ * 
+ * @param sopInstanceUid 图像实例 UID (SOP Instance UID)
+ * @param params 图像处理参数
+ * @returns 保存结果
+ * @throws 当保存失败时抛出错误
+ * 
+ * @example
+ * ```typescript
+ * await saveImageProcessingParams('1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671', {
+ *   contrast: 5.0,
+ *   detail: 9.0,
+ *   latitude: 25.0,
+ *   noise: 12.0
+ * });
+ * ```
+ */
+export const saveImageProcessingParams = async (
+  sopInstanceUid: string,
+  params: SaveProcessingParamsRequest
+): Promise<SaveProcessingParamsResponse> => {
+  try {
+    const response = await axiosInstance.post<SaveProcessingParamsResponse>(
+      `/auth/image/${sopInstanceUid}/proc`,
+      params
+    );
+
+    if (response.data.code !== '0x000000') {
+      throw new Error(`保存图像处理参数失败: ${response.data.description}`);
+    }
+
+    return response.data;
+  } catch (error) {
+    console.error('Error saving image processing params:', error);
+    throw error;
+  }
+};
+
+/**
+ * 保存窗宽窗位请求类型
+ */
+export interface SaveWindowCenterWidthRequest {
+  /** 窗位 */
+  window_center: number;
+  /** 窗宽 */
+  window_width: number;
+}
+
+/**
+ * 保存窗宽窗位响应类型
+ */
+export interface SaveWindowCenterWidthResponse {
+  /** 响应码 */
+  code: string;
+  /** 描述信息 */
+  description: string;
+  /** 解决方案 */
+  solution: string;
+  /** 数据内容 */
+  data: Record<string, never>;
+}
+
+/**
+ * 保存窗宽窗位到 dcm
+ * 
+ * @param sopInstanceUid 图像实例 UID (SOP Instance UID)
+ * @param windowCenter 窗位
+ * @param windowWidth 窗宽
+ * @returns 保存结果
+ * @throws 当保存失败时抛出错误
+ * 
+ * @example
+ * ```typescript
+ * await saveWindowCenterWidth('1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671', 50, 100);
+ * ```
+ */
+export const saveWindowCenterWidth = async (
+  sopInstanceUid: string,
+  windowCenter: number,
+  windowWidth: number
+): Promise<SaveWindowCenterWidthResponse> => {
+  try {
+    const response = await axiosInstance.post<SaveWindowCenterWidthResponse>(
+      `/auth/image/${sopInstanceUid}/wcww`,
+      {
+        window_center: windowCenter,
+        window_width: windowWidth,
+      }
+    );
+
+    if (response.data.code !== '0x000000') {
+      throw new Error(`保存窗宽窗位失败: ${response.data.description}`);
+    }
+
+    return response.data;
+  } catch (error) {
+    console.error('Error saving window center/width:', error);
+    throw error;
+  }
+};

+ 51 - 0
src/domain/lutConfig.ts

@@ -0,0 +1,51 @@
+/**
+ * LUT (Look-Up Table) 配置
+ * LUT 与风格独立,用户可以自由选择
+ */
+
+import type { LUTType } from '../types/imageProcessing';
+
+// LUT 选项列表
+export const LUT_OPTIONS: LUTType[] = [
+  'SIGMOID1',
+  'LINEAR',
+  'SIGMOID',
+  'SIGMOID2',
+  'SIGMOID3',
+  'LOG',
+  'LOG1',
+  'LOG2',
+  'EXP',
+  'EXP1',
+  'EXP2',
+];
+
+// 默认 LUT
+export const DEFAULT_LUT: LUTType = 'SIGMOID';
+
+// LUT 显示名称映射(如果需要中文显示可以扩展)
+export const LUT_DISPLAY_NAMES: Record<LUTType, string> = {
+  'SIGMOID1': 'SIGMOID1',
+  'LINEAR': 'LINEAR',
+  'SIGMOID': 'SIGMOID',
+  'SIGMOID2': 'SIGMOID2',
+  'SIGMOID3': 'SIGMOID3',
+  'LOG': 'LOG',
+  'LOG1': 'LOG1',
+  'LOG2': 'LOG2',
+  'EXP': 'EXP',
+  'EXP1': 'EXP1',
+  'EXP2': 'EXP2',
+};
+
+// 预留扩展:从后端获取LUT列表的适配器接口
+export interface LUTAdapter {
+  fetchLUTOptions: () => Promise<LUTType[]>;
+}
+
+// 默认适配器实现(使用本地配置)
+export class LocalLUTAdapter implements LUTAdapter {
+  async fetchLUTOptions(): Promise<LUTType[]> {
+    return Promise.resolve(LUT_OPTIONS);
+  }
+}

+ 121 - 0
src/domain/processingPresets.ts

@@ -0,0 +1,121 @@
+/**
+ * 图像处理风格预设配置
+ * 风格只定义参数组合,不包含 LUT 和算法
+ */
+
+import type { ProcessingStyle, ProcessingPreset, ParameterConfig } from '../types/imageProcessing';
+
+// 风格预设配置(只包含参数)
+export const PROCESSING_PRESETS: Record<ProcessingStyle, ProcessingPreset> = {
+  '均衡': {
+    style: '均衡',
+    params: {
+      contrast: 5.0,     // 增益
+      detail: 9.0,       // 细节
+      latitude: 25.0,    // 动态范围
+      noise: 12.0,       // 噪声模式
+      brightness: -2.0,  // 对比度(前端维护)
+      sharpness: 1.0,    // 亮度(前端维护)
+    },
+  },
+  '高对比': {
+    style: '高对比',
+    params: {
+      contrast: 7.0,
+      detail: 8.0,
+      latitude: 20.0,
+      noise: 10.0,
+      brightness: 0.0,
+      sharpness: 2.0,
+    },
+  },
+  '锐组织': {
+    style: '锐组织',
+    params: {
+      contrast: 6.0,
+      detail: 12.0,
+      latitude: 22.0,
+      noise: 8.0,
+      brightness: -1.0,
+      sharpness: 3.0,
+    },
+  },
+  '骨骼': {
+    style: '骨骼',
+    params: {
+      contrast: 8.0,
+      detail: 6.0,
+      latitude: 18.0,
+      noise: 15.0,
+      brightness: 1.0,
+      sharpness: 0.5,
+    },
+  },
+};
+
+// 参数范围配置(根据需求文档)
+export const PARAMETER_RANGES: ParameterConfig = {
+  gain: {
+    min: -5,
+    max: 45,
+    step: 1,
+    default: 5.0,
+  },
+  detail: {
+    min: -0.5,
+    max: 24.5,
+    step: 1,
+    default: 9.0,
+  },
+  latitude: {
+    min: -5,
+    max: 20,
+    step: 1,
+    default: 25.0,
+  },
+  noise: {
+    min: 0,
+    max: 20,
+    step: 1,
+    default: 12.0,
+  },
+  brightness: {
+    min: -8,
+    max: 8,
+    step: 1,
+    default: -2.0,
+  },
+  sharpness: {
+    min: -8,
+    max: 8,
+    step: 1,
+    default: 1.0,
+  },
+};
+
+// 算法选项(当前只有一个)
+export const ALGORITHM_OPTIONS = ['RSymphony'];
+export const DEFAULT_ALGORITHM = 'RSymphony';
+
+// 预留扩展:从后端获取风格配置的适配器接口
+export interface PresetAdapter {
+  fetchPresets: () => Promise<Record<ProcessingStyle, ProcessingPreset>>;
+}
+
+// 预留扩展:从后端获取算法列表的适配器接口
+export interface AlgorithmAdapter {
+  fetchAlgorithmOptions: () => Promise<string[]>;
+}
+
+// 默认适配器实现(使用本地配置)
+export class LocalPresetAdapter implements PresetAdapter {
+  async fetchPresets(): Promise<Record<ProcessingStyle, ProcessingPreset>> {
+    return Promise.resolve(PROCESSING_PRESETS);
+  }
+}
+
+export class LocalAlgorithmAdapter implements AlgorithmAdapter {
+  async fetchAlgorithmOptions(): Promise<string[]> {
+    return Promise.resolve(ALGORITHM_OPTIONS);
+  }
+}

+ 6 - 1
src/pages/view/components/AdvancedProcessingPanel.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { Layout, Button, Typography, Flex } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import { useDispatch, useSelector } from 'react-redux';
-import { switchToOperationPanel } from '../../../states/panelSwitchSliceForView';
+import { switchToOperationPanel, switchToSliderAdjustmentPanel } from '../../../states/panelSwitchSliceForView';
 import {
     setProcessingMode,
     performWindowAction,
@@ -178,6 +178,10 @@ const AdvancedProcessingPanel = () => {
         dispatch(switchToOperationPanel());
     };
 
+    const handleAdvancedClick = () => {
+        dispatch(switchToSliderAdjustmentPanel());
+    };
+
     return (
         <Layout className="h-full">
             {/* 顶部导航栏 */}
@@ -241,6 +245,7 @@ const AdvancedProcessingPanel = () => {
 
                 {/* 底部高级按钮 */}
                 <Button
+                    onClick={handleAdvancedClick}
                     style={{
                         width: '100%',
                         marginTop: '16px',

+ 14 - 11
src/pages/view/components/OperationPanel.tsx

@@ -10,6 +10,7 @@ import RectCropPanel from './RectCropPanel';
 import MarkPanel from './MarkPanel';
 import ImageStateControl from './ImageStateControl';
 import { RootState } from '../../../states/store';
+import SliderAdjustmentPanel from './SliderAdjustmentPanel';
 
 const OperationPanel = () => {
   const currentPanel = useSelector(
@@ -20,11 +21,11 @@ const OperationPanel = () => {
     switch (currentPanel) {
       case 'OperationPanel':
         return (
-          <Flex 
-            vertical 
-            gap="small" 
-            style={{ 
-              height: '100%', 
+          <Flex
+            vertical
+            gap="small"
+            style={{
+              height: '100%',
               overflowY: 'auto',
               //padding: '0.5rem' 
             }}
@@ -48,15 +49,17 @@ const OperationPanel = () => {
         return <RectCropPanel />;
       case 'MarkPanel':
         return <MarkPanel />;
+      case 'SliderAdjustmentPanel':
+        return <SliderAdjustmentPanel />
       default:
         return (
-          <Flex 
-            vertical 
-            gap="small" 
-            style={{ 
-              height: '100%', 
+          <Flex
+            vertical
+            gap="small"
+            style={{
+              height: '100%',
               overflowY: 'auto',
-              padding: '0.5rem' 
+              padding: '0.5rem'
             }}
           >
             <FunctionArea />

+ 117 - 0
src/pages/view/components/ParameterSlider.tsx

@@ -0,0 +1,117 @@
+import React from 'react';
+import { Slider, InputNumber, Button, Typography } from 'antd';
+import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
+
+const { Text } = Typography;
+
+interface ParameterSliderProps {
+  label: string;
+  value: number;
+  min: number;
+  max: number;
+  step: number;
+  range?: string;
+  onChange: (value: number) => void;
+  disabled?: boolean;
+}
+
+/**
+ * 参数滑块组件
+ * 包含:标签、范围提示、当前值显示、滑块、减/加按钮
+ */
+const ParameterSlider: React.FC<ParameterSliderProps> = ({
+  label,
+  value,
+  min,
+  max,
+  step,
+  range,
+  onChange,
+  disabled = false,
+}) => {
+  const handleSliderChange = (newValue: number | null) => {
+    if (newValue !== null) {
+      onChange(newValue);
+    }
+  };
+
+  const handleIncrement = () => {
+    const newValue = Math.min(value + step, max);
+    onChange(newValue);
+  };
+
+  const handleDecrement = () => {
+    const newValue = Math.max(value - step, min);
+    onChange(newValue);
+  };
+
+  return (
+    <div style={{ marginBottom: '20px' }}>
+      {/* 标签和当前值 */}
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          marginBottom: '8px',
+        }}
+      >
+        <div>
+          <Text strong>{label}</Text>
+          {range && (
+            <Text type="secondary" style={{ marginLeft: '8px', fontSize: '12px' }}>
+              ({range})
+            </Text>
+          )}
+        </div>
+        <Text strong style={{ fontSize: '14px' }}>
+          {value.toFixed(1)}
+        </Text>
+      </div>
+
+      {/* 滑块和按钮 */}
+      <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
+        <Button
+          icon={<MinusOutlined />}
+          size="small"
+          onClick={handleDecrement}
+          disabled={disabled || value <= min}
+          style={{
+            width: '32px',
+            height: '32px',
+            padding: 0,
+          }}
+        />
+        <div style={{ flex: 1 }}>
+          <Slider
+            min={min}
+            max={max}
+            step={step}
+            value={value}
+            onChange={handleSliderChange}
+            disabled={disabled}
+            tooltip={{ formatter: (val) => val?.toFixed(1) }}
+            styles={{
+              track: {
+                background: '#1890ff',
+              },
+            }}
+          />
+        </div>
+        <Button
+          icon={<PlusOutlined />}
+          size="small"
+          onClick={handleIncrement}
+          disabled={disabled || value >= max}
+          style={{
+            width: '32px',
+            height: '32px',
+            padding: 0,
+          }}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default ParameterSlider;

+ 425 - 0
src/pages/view/components/SliderAdjustmentPanel.tsx

@@ -0,0 +1,425 @@
+import React, { useEffect, useCallback, useRef } from 'react';
+import { Layout, Button, Typography, Select, message, Spin } from 'antd';
+import { ArrowLeftOutlined } from '@ant-design/icons';
+import { useDispatch } from 'react-redux';
+import { useAppSelector } from '@/states/store';
+import { switchToAdvancedProcessingPanel } from '../../../states/panelSwitchSliceForView';
+import {
+  loadImageProcessingParams,
+  saveProcessingParams,
+  updateParameter,
+  applyPreset,
+  resetToPreset,
+  setAlgorithm,
+  setLUT,
+  selectParameters,
+  selectSelectedStyle,
+  selectSelectedAlgorithm,
+  selectSelectedLUT,
+  selectIsLoading,
+  selectIsSaving,
+  selectIsInitialLoad,
+  selectError,
+  selectCurrentImageId,
+} from '../../../states/view/sliderAdjustmentPanelSlice';
+import { PARAMETER_RANGES, ALGORITHM_OPTIONS } from '../../../domain/processingPresets';
+import { LUT_OPTIONS } from '../../../domain/lutConfig';
+import type { ProcessingStyle, LUTType, FullProcessingParams } from '../../../types/imageProcessing';
+import ParameterSlider from './ParameterSlider';
+
+const { Header, Content } = Layout;
+const { Title } = Typography;
+const { Option } = Select;
+
+/**
+ * 滑动参数调节面板
+ * 三级面板,用于调整图像处理参数
+ */
+const SliderAdjustmentPanel = () => {
+  const dispatch = useDispatch();
+  
+  // 从 Redux 获取状态
+  const parameters = useAppSelector(selectParameters);
+  const selectedStyle = useAppSelector(selectSelectedStyle);
+  const selectedAlgorithm = useAppSelector(selectSelectedAlgorithm);
+  const selectedLUT = useAppSelector(selectSelectedLUT);
+  const isLoading = useAppSelector(selectIsLoading);
+  const isSaving = useAppSelector(selectIsSaving);
+  const isInitialLoad = useAppSelector(selectIsInitialLoad);
+  const error = useAppSelector(selectError);
+  const currentImageId = useAppSelector(selectCurrentImageId);
+  
+  // 防抖定时器
+  const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
+  
+  // 组件挂载时加载参数
+  useEffect(() => {
+    // TODO: 从当前选中的图像获取 sopInstanceUid
+    // 这里需要从 ViewerContainer 或其他地方获取当前图像ID
+    const sopInstanceUid = ''; // 临时占位
+    
+    if (sopInstanceUid) {
+      dispatch(loadImageProcessingParams(sopInstanceUid) as any);
+    }
+  }, [dispatch]);
+  
+  // 监听错误
+  useEffect(() => {
+    if (error) {
+      message.error(error);
+    }
+  }, [error]);
+  
+  /**
+   * 返回上级面板
+   */
+  const handleReturn = () => {
+    dispatch(switchToAdvancedProcessingPanel());
+  };
+  
+  /**
+   * 防抖保存参数
+   */
+  const debouncedSave = useCallback(
+    (params: FullProcessingParams) => {
+      // 如果是初始加载,不触发保存
+      if (isInitialLoad) {
+        return;
+      }
+      
+      // 清除之前的定时器
+      if (saveTimerRef.current) {
+        clearTimeout(saveTimerRef.current);
+      }
+      
+      // 设置新的定时器
+      saveTimerRef.current = setTimeout(() => {
+        if (currentImageId) {
+          dispatch(
+            saveProcessingParams({
+              sopInstanceUid: currentImageId,
+              params: {
+                contrast: params.contrast,
+                detail: params.detail,
+                latitude: params.latitude,
+                noise: params.noise,
+              },
+            }) as any
+          );
+        }
+      }, 500); // 500ms 防抖延迟
+    },
+    [dispatch, currentImageId, isInitialLoad]
+  );
+  
+  /**
+   * 参数变化处理
+   */
+  const handleParameterChange = (name: keyof FullProcessingParams, value: number) => {
+    dispatch(updateParameter({ name, value }));
+    // 触发防抖保存
+    const newParams = { ...parameters, [name]: value };
+    debouncedSave(newParams);
+  };
+  
+  /**
+   * 风格变化处理
+   */
+  const handleStyleChange = (value: ProcessingStyle) => {
+    dispatch(applyPreset(value));
+    // 风格切换后立即保存
+    if (currentImageId) {
+      const preset = parameters; // 应用风格后的参数会在 Redux 中更新
+      setTimeout(() => {
+        dispatch(
+          saveProcessingParams({
+            sopInstanceUid: currentImageId,
+            params: {
+              contrast: preset.contrast,
+              detail: preset.detail,
+              latitude: preset.latitude,
+              noise: preset.noise,
+            },
+          }) as any
+        );
+      }, 100);
+    }
+  };
+  
+  /**
+   * 算法变化处理
+   */
+  const handleAlgorithmChange = (value: string) => {
+    dispatch(setAlgorithm(value));
+  };
+  
+  /**
+   * LUT 变化处理
+   */
+  const handleLUTChange = (value: LUTType) => {
+    dispatch(setLUT(value));
+  };
+  
+  /**
+   * 重置参数
+   */
+  const handleReset = () => {
+    dispatch(resetToPreset());
+    message.success('已重置为当前风格的默认参数');
+    
+    // 重置后保存
+    if (currentImageId) {
+      setTimeout(() => {
+        dispatch(
+          saveProcessingParams({
+            sopInstanceUid: currentImageId,
+            params: {
+              contrast: parameters.contrast,
+              detail: parameters.detail,
+              latitude: parameters.latitude,
+              noise: parameters.noise,
+            },
+          }) as any
+        );
+      }, 100);
+    }
+  };
+  
+  /**
+   * 手动保存参数
+   */
+  const handleSave = () => {
+    if (currentImageId) {
+      dispatch(
+        saveProcessingParams({
+          sopInstanceUid: currentImageId,
+          params: {
+            contrast: parameters.contrast,
+            detail: parameters.detail,
+            latitude: parameters.latitude,
+            noise: parameters.noise,
+          },
+        }) as any
+      ).then(() => {
+        message.success('参数保存成功');
+      });
+    }
+  };
+  
+  // 清理定时器
+  useEffect(() => {
+    return () => {
+      if (saveTimerRef.current) {
+        clearTimeout(saveTimerRef.current);
+      }
+    };
+  }, []);
+  
+  return (
+    <Layout className="h-full">
+      {/* 顶部导航栏 */}
+      <Header
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          padding: '0 16px',
+        }}
+      >
+        <Button
+          type="text"
+          icon={<ArrowLeftOutlined />}
+          onClick={handleReturn}
+        />
+        <Title level={5} style={{ margin: 0, lineHeight: '48px' }}>
+          滑动参数调节
+        </Title>
+      </Header>
+
+      {/* 主体内容 */}
+      <Content
+        style={{ 
+          padding: '16px', 
+          maxHeight: '100%', 
+          overflowY: 'auto',
+          position: 'relative',
+        }}
+      >
+        {isLoading && (
+          <div
+            style={{
+              position: 'absolute',
+              top: '50%',
+              left: '50%',
+              transform: 'translate(-50%, -50%)',
+              zIndex: 1000,
+            }}
+          >
+            <Spin size="large" tip="加载参数中..." />
+          </div>
+        )}
+        
+        <div style={{ opacity: isLoading ? 0.5 : 1 }}>
+          {/* 增益滑块 */}
+          <ParameterSlider
+            label="增益"
+            value={parameters.contrast}
+            min={PARAMETER_RANGES.gain.min}
+            max={PARAMETER_RANGES.gain.max}
+            step={PARAMETER_RANGES.gain.step}
+            range={`${PARAMETER_RANGES.gain.min}~${PARAMETER_RANGES.gain.max}`}
+            onChange={(value) => handleParameterChange('contrast', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 细节滑块 */}
+          <ParameterSlider
+            label="细节"
+            value={parameters.detail}
+            min={PARAMETER_RANGES.detail.min}
+            max={PARAMETER_RANGES.detail.max}
+            step={PARAMETER_RANGES.detail.step}
+            range={`${PARAMETER_RANGES.detail.min}~${PARAMETER_RANGES.detail.max}`}
+            onChange={(value) => handleParameterChange('detail', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 动态范围滑块 */}
+          <ParameterSlider
+            label="动态范围"
+            value={parameters.latitude}
+            min={PARAMETER_RANGES.latitude.min}
+            max={PARAMETER_RANGES.latitude.max}
+            step={PARAMETER_RANGES.latitude.step}
+            range={`${PARAMETER_RANGES.latitude.min}~${PARAMETER_RANGES.latitude.max}`}
+            onChange={(value) => handleParameterChange('latitude', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 噪声模式滑块 */}
+          <ParameterSlider
+            label="噪声模式"
+            value={parameters.noise}
+            min={PARAMETER_RANGES.noise.min}
+            max={PARAMETER_RANGES.noise.max}
+            step={PARAMETER_RANGES.noise.step}
+            range={`${PARAMETER_RANGES.noise.min}~${PARAMETER_RANGES.noise.max}`}
+            onChange={(value) => handleParameterChange('noise', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 对比度滑块(前端维护) */}
+          <ParameterSlider
+            label="对比度"
+            value={parameters.brightness}
+            min={PARAMETER_RANGES.brightness.min}
+            max={PARAMETER_RANGES.brightness.max}
+            step={PARAMETER_RANGES.brightness.step}
+            range={`${PARAMETER_RANGES.brightness.min}~${PARAMETER_RANGES.brightness.max}`}
+            onChange={(value) => handleParameterChange('brightness', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 亮度滑块(前端维护) */}
+          <ParameterSlider
+            label="亮度"
+            value={parameters.sharpness}
+            min={PARAMETER_RANGES.sharpness.min}
+            max={PARAMETER_RANGES.sharpness.max}
+            step={PARAMETER_RANGES.sharpness.step}
+            range={`${PARAMETER_RANGES.sharpness.min}~${PARAMETER_RANGES.sharpness.max}`}
+            onChange={(value) => handleParameterChange('sharpness', value)}
+            disabled={isLoading}
+          />
+          
+          {/* 算法选择 */}
+          <div style={{ marginBottom: '16px' }}>
+            <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
+              算法
+            </Typography.Text>
+            <Select
+              value={selectedAlgorithm}
+              onChange={handleAlgorithmChange}
+              style={{ width: '100%' }}
+              disabled={isLoading}
+            >
+              {ALGORITHM_OPTIONS.map((algo) => (
+                <Option key={algo} value={algo}>
+                  {algo}
+                </Option>
+              ))}
+            </Select>
+          </div>
+          
+          {/* LUT 选择 */}
+          <div style={{ marginBottom: '16px' }}>
+            <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
+              LUT
+            </Typography.Text>
+            <Select
+              value={selectedLUT}
+              onChange={handleLUTChange}
+              style={{ width: '100%' }}
+              disabled={isLoading}
+            >
+              {LUT_OPTIONS.map((lut) => (
+                <Option key={lut} value={lut}>
+                  {lut}
+                </Option>
+              ))}
+            </Select>
+          </div>
+          
+          {/* 风格选择 */}
+          <div style={{ marginBottom: '16px' }}>
+            <Typography.Text strong style={{ display: 'block', marginBottom: '8px' }}>
+              风格
+            </Typography.Text>
+            <Select
+              value={selectedStyle}
+              onChange={handleStyleChange}
+              style={{ width: '100%' }}
+              disabled={isLoading}
+            >
+              <Option value="均衡">均衡</Option>
+              <Option value="高对比">高对比</Option>
+              <Option value="锐组织">锐组织</Option>
+              <Option value="骨骼">骨骼</Option>
+            </Select>
+          </div>
+          
+          {/* 底部按钮 */}
+          <div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
+            <Button
+              onClick={handleReset}
+              disabled={isLoading || isSaving}
+              style={{
+                flex: 1,
+                height: '40px',
+                fontSize: '14px',
+              }}
+            >
+              重置参数
+            </Button>
+            <Button
+              type="primary"
+              onClick={handleSave}
+              loading={isSaving}
+              disabled={isLoading}
+              style={{
+                flex: 1,
+                height: '40px',
+                fontSize: '14px',
+                backgroundColor: '#13c2c2',
+                borderColor: '#13c2c2',
+              }}
+            >
+              保存参数
+            </Button>
+          </div>
+        </div>
+      </Content>
+    </Layout>
+  );
+};
+
+export default SliderAdjustmentPanel;

+ 5 - 1
src/states/panelSwitchSliceForView.ts

@@ -1,7 +1,7 @@
 import { createSlice } from '@reduxjs/toolkit';
 
 interface PanelSwitchStateForView {
-  currentPanel: 'OperationPanel' | 'SendPanel' | 'MeasurementPanel' | 'MorePanel' | 'AdvancedProcessingPanel' | 'RectCropPanel' | 'MarkPanel';
+  currentPanel: 'OperationPanel' | 'SendPanel' | 'MeasurementPanel' | 'MorePanel' | 'AdvancedProcessingPanel' | 'SliderAdjustmentPanel' | 'RectCropPanel' | 'MarkPanel';
 }
 
 const initialState: PanelSwitchStateForView = {
@@ -27,6 +27,9 @@ const panelSwitchSliceForView = createSlice({
     switchToAdvancedProcessingPanel: (state) => {
       state.currentPanel = 'AdvancedProcessingPanel';
     },
+    switchToSliderAdjustmentPanel: (state) => {
+      state.currentPanel = 'SliderAdjustmentPanel';
+    },
     switchToRectCropPanel: (state) => {
       state.currentPanel = 'RectCropPanel';
     },
@@ -42,6 +45,7 @@ export const {
   switchToMeasurementPanel,
   switchToMorePanel,
   switchToAdvancedProcessingPanel,
+  switchToSliderAdjustmentPanel,
   switchToRectCropPanel,
   switchToMarkPanel,
 } = panelSwitchSliceForView.actions;

+ 2 - 0
src/states/store.ts

@@ -18,6 +18,7 @@ import { aprMiddleware } from './exam/aprSlice';
 import functionAreaReducer from './view/functionAreaSlice';
 import measurementPanelReducer from './view/measurementPanelSlice';
 import advancedProcessingPanelReducer from './view/advancedProcessingPanelSlice';
+import sliderAdjustmentPanelReducer from './view/sliderAdjustmentPanelSlice';
 import rectCropPanelReducer from './view/rectCropPanelSlice';
 import viewerContainerReducer from './view/viewerContainerSlice';
 import markPanelReducer from './view/markPanelSlice';
@@ -102,6 +103,7 @@ const store = configureStore({
     functionArea: functionAreaReducer,
     measurementPanel: measurementPanelReducer,
     advancedProcessingPanel: advancedProcessingPanelReducer,
+    sliderAdjustmentPanel: sliderAdjustmentPanelReducer,
     rectCropPanel: rectCropPanelReducer,
     viewerContainer: viewerContainerReducer,
     markPanel: markPanelReducer,

+ 275 - 0
src/states/view/sliderAdjustmentPanelSlice.ts

@@ -0,0 +1,275 @@
+/**
+ * 滑动参数调节面板状态管理
+ */
+
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import type { RootState } from '../store';
+import type { 
+  ProcessingStyle, 
+  LUTType, 
+  FullProcessingParams 
+} from '../../types/imageProcessing';
+import { 
+  PROCESSING_PRESETS, 
+  DEFAULT_ALGORITHM 
+} from '../../domain/processingPresets';
+import { DEFAULT_LUT } from '../../domain/lutConfig';
+import { 
+  getImageProcessingParams, 
+  saveImageProcessingParams 
+} from '../../API/imageActions';
+
+/**
+ * 状态接口
+ */
+interface SliderAdjustmentPanelState {
+  // 当前图像ID
+  currentImageId: string | null;
+  
+  // 当前参数值
+  parameters: FullProcessingParams;
+  
+  // 当前选择的风格
+  selectedStyle: ProcessingStyle;
+  
+  // 当前选择的算法(独立管理)
+  selectedAlgorithm: string;
+  
+  // 当前选择的LUT(独立管理)
+  selectedLUT: LUTType;
+  
+  // 是否正在加载
+  isLoading: boolean;
+  
+  // 是否正在保存
+  isSaving: boolean;
+  
+  // 初始加载标志(区分初始加载和用户操作)
+  isInitialLoad: boolean;
+  
+  // 错误信息
+  error: string | null;
+}
+
+/**
+ * 初始状态
+ */
+const initialState: SliderAdjustmentPanelState = {
+  currentImageId: null,
+  parameters: PROCESSING_PRESETS['均衡'].params,
+  selectedStyle: '均衡',
+  selectedAlgorithm: DEFAULT_ALGORITHM,
+  selectedLUT: DEFAULT_LUT,
+  isLoading: false,
+  isSaving: false,
+  isInitialLoad: true,
+  error: null,
+};
+
+/**
+ * 异步操作:加载图像处理参数
+ */
+export const loadImageProcessingParams = createAsyncThunk(
+  'sliderAdjustmentPanel/loadParams',
+  async (sopInstanceUid: string, { rejectWithValue }) => {
+    try {
+      const response = await getImageProcessingParams(sopInstanceUid);
+      return {
+        sopInstanceUid,
+        params: response.data,
+      };
+    } catch (error) {
+      return rejectWithValue(
+        error instanceof Error ? error.message : '加载参数失败'
+      );
+    }
+  }
+);
+
+/**
+ * 异步操作:保存图像处理参数
+ */
+export const saveProcessingParams = createAsyncThunk(
+  'sliderAdjustmentPanel/saveParams',
+  async (
+    { 
+      sopInstanceUid, 
+      params 
+    }: { 
+      sopInstanceUid: string; 
+      params: { contrast: number; detail: number; latitude: number; noise: number } 
+    },
+    { rejectWithValue }
+  ) => {
+    try {
+      await saveImageProcessingParams(sopInstanceUid, params);
+      return params;
+    } catch (error) {
+      return rejectWithValue(
+        error instanceof Error ? error.message : '保存参数失败'
+      );
+    }
+  }
+);
+
+/**
+ * Slice
+ */
+const sliderAdjustmentPanelSlice = createSlice({
+  name: 'sliderAdjustmentPanel',
+  initialState,
+  reducers: {
+    /**
+     * 设置当前图像ID
+     */
+    setCurrentImageId: (state, action: PayloadAction<string>) => {
+      state.currentImageId = action.payload;
+      state.isInitialLoad = true;
+    },
+    
+    /**
+     * 更新单个参数
+     */
+    updateParameter: (
+      state, 
+      action: PayloadAction<{ name: keyof FullProcessingParams; value: number }>
+    ) => {
+      const { name, value } = action.payload;
+      state.parameters[name] = value;
+      // 用户手动调整参数后,不再是初始加载状态
+      state.isInitialLoad = false;
+    },
+    
+    /**
+     * 应用风格预设
+     */
+    applyPreset: (state, action: PayloadAction<ProcessingStyle>) => {
+      const preset = PROCESSING_PRESETS[action.payload];
+      state.selectedStyle = action.payload;
+      state.parameters = { ...preset.params };
+      // 应用风格后,不再是初始加载状态
+      state.isInitialLoad = false;
+    },
+    
+    /**
+     * 重置为当前风格的默认值
+     */
+    resetToPreset: (state) => {
+      const preset = PROCESSING_PRESETS[state.selectedStyle];
+      state.parameters = { ...preset.params };
+      state.isInitialLoad = false;
+    },
+    
+    /**
+     * 设置算法
+     */
+    setAlgorithm: (state, action: PayloadAction<string>) => {
+      state.selectedAlgorithm = action.payload;
+    },
+    
+    /**
+     * 设置LUT
+     */
+    setLUT: (state, action: PayloadAction<LUTType>) => {
+      state.selectedLUT = action.payload;
+    },
+    
+    /**
+     * 清除错误
+     */
+    clearError: (state) => {
+      state.error = null;
+    },
+    
+    /**
+     * 重置状态
+     */
+    resetState: () => initialState,
+  },
+  extraReducers: (builder) => {
+    builder
+      // 加载参数
+      .addCase(loadImageProcessingParams.pending, (state) => {
+        state.isLoading = true;
+        state.error = null;
+      })
+      .addCase(loadImageProcessingParams.fulfilled, (state, action) => {
+        state.isLoading = false;
+        state.currentImageId = action.payload.sopInstanceUid;
+        // 只更新后端返回的4个参数,保持前端的 brightness 和 sharpness
+        state.parameters.contrast = action.payload.params.contrast;
+        state.parameters.detail = action.payload.params.detail;
+        state.parameters.latitude = action.payload.params.latitude;
+        state.parameters.noise = action.payload.params.noise;
+        // 标记为初始加载,避免触发自动保存
+        state.isInitialLoad = true;
+      })
+      .addCase(loadImageProcessingParams.rejected, (state, action) => {
+        state.isLoading = false;
+        state.error = action.payload as string;
+        // 加载失败时使用默认值
+        state.parameters = PROCESSING_PRESETS[state.selectedStyle].params;
+      })
+      // 保存参数
+      .addCase(saveProcessingParams.pending, (state) => {
+        state.isSaving = true;
+        state.error = null;
+      })
+      .addCase(saveProcessingParams.fulfilled, (state) => {
+        state.isSaving = false;
+      })
+      .addCase(saveProcessingParams.rejected, (state, action) => {
+        state.isSaving = false;
+        state.error = action.payload as string;
+      });
+  },
+});
+
+/**
+ * Actions
+ */
+export const {
+  setCurrentImageId,
+  updateParameter,
+  applyPreset,
+  resetToPreset,
+  setAlgorithm,
+  setLUT,
+  clearError,
+  resetState,
+} = sliderAdjustmentPanelSlice.actions;
+
+/**
+ * Selectors
+ */
+export const selectCurrentImageId = (state: RootState) => 
+  state.sliderAdjustmentPanel.currentImageId;
+
+export const selectParameters = (state: RootState) => 
+  state.sliderAdjustmentPanel.parameters;
+
+export const selectSelectedStyle = (state: RootState) => 
+  state.sliderAdjustmentPanel.selectedStyle;
+
+export const selectSelectedAlgorithm = (state: RootState) => 
+  state.sliderAdjustmentPanel.selectedAlgorithm;
+
+export const selectSelectedLUT = (state: RootState) => 
+  state.sliderAdjustmentPanel.selectedLUT;
+
+export const selectIsLoading = (state: RootState) => 
+  state.sliderAdjustmentPanel.isLoading;
+
+export const selectIsSaving = (state: RootState) => 
+  state.sliderAdjustmentPanel.isSaving;
+
+export const selectIsInitialLoad = (state: RootState) => 
+  state.sliderAdjustmentPanel.isInitialLoad;
+
+export const selectError = (state: RootState) => 
+  state.sliderAdjustmentPanel.error;
+
+/**
+ * Reducer
+ */
+export default sliderAdjustmentPanelSlice.reducer;

+ 67 - 0
src/types/imageProcessing.ts

@@ -0,0 +1,67 @@
+/**
+ * 图像处理相关类型定义
+ */
+
+// 图像处理参数(后端API字段)
+export interface ImageProcessingParams {
+  contrast: number;   // 增益
+  detail: number;     // 细节
+  latitude: number;   // 动态范围
+  noise: number;      // 噪声模式
+}
+
+// 前端使用的完整参数(包括前端维护的对比度和亮度)
+export interface FullProcessingParams extends ImageProcessingParams {
+  brightness: number; // 对比度(前端维护)
+  sharpness: number;  // 亮度(前端维护)
+}
+
+// 风格类型
+export type ProcessingStyle = '均衡' | '高对比' | '锐组织' | '骨骼';
+
+// LUT类型
+export type LUTType = 
+  | 'SIGMOID1' | 'LINEAR' | 'SIGMOID' 
+  | 'SIGMOID2' | 'SIGMOID3' 
+  | 'LOG' | 'LOG1' | 'LOG2' 
+  | 'EXP' | 'EXP1' | 'EXP2';
+
+// 风格预设配置(只包含参数,不包含LUT和算法)
+export interface ProcessingPreset {
+  style: ProcessingStyle;
+  params: FullProcessingParams;
+}
+
+// 参数范围配置
+export interface ParameterRange {
+  min: number;
+  max: number;
+  step: number;
+  default: number;
+}
+
+// 参数配置映射
+export interface ParameterConfig {
+  gain: ParameterRange;      // 增益 (contrast)
+  detail: ParameterRange;    // 细节
+  latitude: ParameterRange;  // 动态范围
+  noise: ParameterRange;     // 噪声模式
+  brightness: ParameterRange; // 对比度
+  sharpness: ParameterRange;  // 亮度
+}
+
+// API 请求参数
+export interface SaveProcessingParamsRequest {
+  contrast: number;
+  detail: number;
+  latitude: number;
+  noise: number;
+}
+
+// API 响应
+export interface GetProcessingParamsResponse {
+  contrast: number;
+  detail: number;
+  latitude: number;
+  noise: number;
+}