Browse Source

feat: 实现高级图像处理面板功能

- 创建 AdvancedProcessingPanel 组件,包含处理模式选择、窗宽窗位调整和曲线显示
- 创建 advancedProcessingPanelSlice 状态管理,支持处理模式和窗宽窗位操作
- 在 panelSwitchSliceForView 中添加 AdvancedProcessingPanel 面板类型
- 在 store 中注册 advancedProcessingPanel reducer
- 在 FunctionArea 中添加切换到高级处理面板的逻辑
- 在 OperationPanel 中添加高级处理面板的渲染

改动文件:
- src/pages/view/components/AdvancedProcessingPanel.tsx (新增)
- src/states/view/advancedProcessingPanelSlice.ts (新增)
- src/pages/view/components/FunctionArea.tsx
- src/pages/view/components/OperationPanel.tsx
- src/states/panelSwitchSliceForView.ts
- src/states/store.ts
sw 2 days ago
parent
commit
cb2413ab43

+ 258 - 0
src/pages/view/components/AdvancedProcessingPanel.tsx

@@ -0,0 +1,258 @@
+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 {
+    setProcessingMode,
+    performWindowAction,
+    type ProcessingMode,
+    type WindowAction,
+} from '../../../states/view/advancedProcessingPanelSlice';
+import { RootState } from '../../../states/store';
+import Icon from '@/components/Icon';
+
+const { Header, Content } = Layout;
+const { Title } = Typography;
+
+// 处理模式按钮组件
+const ModeButton = ({
+    title,
+    mode,
+    isSelected,
+}: {
+    title: string;
+    mode: ProcessingMode;
+    isSelected: boolean;
+}) => {
+    const dispatch = useDispatch();
+
+    const handleClick = () => {
+        dispatch(setProcessingMode(mode));
+    };
+
+    return (
+        <Button
+            onClick={handleClick}
+            style={{
+                flex: 1,
+                fontSize: '14px',
+                backgroundColor: isSelected ? '#d4a373' : undefined,
+                color: isSelected ? '#000' : undefined,
+                borderColor: isSelected ? '#d4a373' : undefined,
+            }}
+        >
+            {title}
+        </Button>
+    );
+};
+
+// 窗宽窗位调整按钮组件
+const WindowButton = ({
+    action,
+    iconName,
+}: {
+    action: WindowAction;
+    iconName: string;
+}) => {
+    const dispatch = useDispatch();
+
+    const handleClick = () => {
+        dispatch(performWindowAction(action));
+    };
+
+    return (
+        <Button
+            onClick={handleClick}
+            icon={
+                iconName ? (
+                    <Icon
+                        module="module-process"
+                        name={iconName}
+                        userId="base"
+                        theme="default"
+                        size="2x"
+                        state="normal"
+                    />
+                ) : undefined
+            }
+            style={{
+                width: '1.5rem',
+                height: '1.5rem',
+                padding: 0,
+                backgroundColor: '#333',
+                borderColor: '#555',
+            }}
+            title={action}
+        />
+    );
+};
+
+// 曲线显示组件(静态)
+const CurveDisplay = () => {
+    return (
+        <div
+            style={{
+                width: '100%',
+                height: '240px',
+                backgroundColor: '#2a2a2a',
+                borderRadius: '4px',
+                position: 'relative',
+                marginTop: '12px',
+            }}
+        >
+            <svg
+                width="100%"
+                height="100%"
+                viewBox="0 0 300 240"
+                style={{ display: 'block' }}
+            >
+                {/* 网格背景 */}
+                <defs>
+                    <pattern
+                        id="grid"
+                        width="30"
+                        height="24"
+                        patternUnits="userSpaceOnUse"
+                    >
+                        <path
+                            d="M 30 0 L 0 0 0 24"
+                            fill="none"
+                            stroke="#444"
+                            strokeWidth="0.5"
+                        />
+                    </pattern>
+                </defs>
+                <rect width="300" height="240" fill="url(#grid)" />
+
+                {/* 直方图柱状图 */}
+                <g opacity="0.5">
+                    {Array.from({ length: 50 }, (_, i) => {
+                        const x = (i * 300) / 50;
+                        const height = Math.random() * 180 + 20;
+                        const y = 240 - height;
+                        return (
+                            <rect
+                                key={i}
+                                x={x}
+                                y={y}
+                                width="5"
+                                height={height}
+                                fill="#888"
+                            />
+                        );
+                    })}
+                </g>
+
+                {/* S型曲线 */}
+                <path
+                    d="M 20,200 Q 80,180 120,120 T 280,40"
+                    stroke="#fff"
+                    strokeWidth="2"
+                    fill="none"
+                />
+
+                {/* 可拖拽点(暂不实现交互) */}
+                <circle cx="20" cy="200" r="5" fill="#fff" stroke="#000" />
+                <rect
+                    x="115"
+                    y="115"
+                    width="10"
+                    height="10"
+                    fill="#fff"
+                    stroke="#000"
+                />
+                <circle cx="280" cy="40" r="5" fill="#fff" stroke="#000" />
+            </svg>
+        </div>
+    );
+};
+
+const AdvancedProcessingPanel = () => {
+    const dispatch = useDispatch();
+    const selectedMode = useSelector(
+        (state: RootState) => state.advancedProcessingPanel.selectedMode
+    );
+
+    const handleReturn = () => {
+        dispatch(switchToOperationPanel());
+    };
+
+    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' }}
+            >
+                {/* 快捷功能按钮区域 */}
+                <Flex gap="small" style={{ marginBottom: '16px' }}>
+                    <ModeButton
+                        title="均衡"
+                        mode="均衡"
+                        isSelected={selectedMode === '均衡'}
+                    />
+                    <ModeButton
+                        title="高对比"
+                        mode="高对比"
+                        isSelected={selectedMode === '高对比'}
+                    />
+                </Flex>
+                <Flex gap="small" style={{ marginBottom: '16px' }}>
+                    <ModeButton
+                        title="锐组织"
+                        mode="锐组织"
+                        isSelected={selectedMode === '锐组织'}
+                    />
+                    <ModeButton
+                        title="骨骼"
+                        mode="骨骼"
+                        isSelected={selectedMode === '骨骼'}
+                    />
+                </Flex>
+
+                {/* 窗宽窗位调整工具 */}
+                <Flex gap="small" style={{ marginBottom: '12px' }}>
+                    <WindowButton action="减小窗宽" iconName="btn_DecreaseWindowWidth" />
+                    <WindowButton action="增大窗宽" iconName="btn_IncreaseWindowWidth" />
+                    <WindowButton action="减小窗位" iconName="btn_DecreaseWindowLevel" />
+                    <WindowButton action="增大窗位" iconName="btn_IncreaseWindowLevel" />
+                </Flex>
+
+                {/* 曲线显示区域 */}
+                <CurveDisplay />
+
+                {/* 底部高级按钮 */}
+                <Button
+                    style={{
+                        width: '100%',
+                        marginTop: '16px',
+                        height: '40px',
+                        fontSize: '14px',
+                    }}
+                >
+                    高级
+                </Button>
+            </Content>
+        </Layout>
+    );
+};
+
+export default AdvancedProcessingPanel;

+ 13 - 10
src/pages/view/components/FunctionArea.tsx

@@ -3,7 +3,7 @@ import { Button, Flex } from 'antd';
 import '@/themes/truncateText.css';
 import { useDispatch } from 'react-redux';
 import { setAction } from '@/states/view/functionAreaSlice';
-import { switchToMeasurementPanel, switchToMorePanel } from '@/states/panelSwitchSliceForView';
+import { switchToMeasurementPanel, switchToMorePanel, switchToAdvancedProcessingPanel } from '@/states/panelSwitchSliceForView';
 import Icon from '@/components/Icon';
 import { showNotImplemented } from '@/utils/notificationHelper';
 
@@ -19,23 +19,26 @@ const FunctionButton = ({
   const dispatch = useDispatch();
 
   const handleButtonClick = () => {
-    if(action==='AddMask'||
-      action==='Delete Digital Mask'||
-      action==='Crop Selected Area' ||
+    if (action === 'AddMask' ||
+      action === 'Delete Digital Mask' ||
+      action === 'Crop Selected Area' ||
       ['Delete Mask',
-        'Image Comparison','Zoom Image','Reset Cursor','Pan','Snapshot','Advanced Processing','Musician',
+        'Image Comparison', 'Zoom Image', 'Reset Cursor', 'Pan', 'Snapshot', 'Musician',
       ].includes(action)
-    ){
-showNotImplemented('');
-return;
+    ) {
+      showNotImplemented('');
+      return;
     }
-    
+
     if (action === 'Image Measurement') {
       // 切换到测量面板
       dispatch(switchToMeasurementPanel());
     } else if (action === 'More') {
       // 切换到更多功能面板
       dispatch(switchToMorePanel());
+    } else if (action === 'Advanced Processing') {
+      // 切换到高级图像处理面板
+      dispatch(switchToAdvancedProcessingPanel());
     } else {
       // 其他功能按钮保持原有逻辑
       dispatch(setAction(action));
@@ -109,7 +112,7 @@ const FunctionArea = () => {
       <FunctionButton
         title="Delete Digital Mask"
         action="Delete Digital Mask"
-        iconName="EraseMark"        
+        iconName="EraseMark"
       />
       <FunctionButton
         title="Adjust Brightness and Contrast"

+ 3 - 0
src/pages/view/components/OperationPanel.tsx

@@ -5,6 +5,7 @@ import TransferArea from './TransferArea';
 import SendPanelForView from '../../output/SendPanelForView';
 import MeasurementPanel from './MeasurementPanel';
 import MorePanel from './MorePanel';
+import AdvancedProcessingPanel from './AdvancedProcessingPanel';
 import ImageStateControl from './ImageStateControl';
 import { RootState } from '../../../states/store';
 
@@ -39,6 +40,8 @@ const OperationPanel = () => {
         return <MeasurementPanel />;
       case 'MorePanel':
         return <MorePanel />;
+      case 'AdvancedProcessingPanel':
+        return <AdvancedProcessingPanel />;
       default:
         return (
           <Flex 

+ 5 - 1
src/states/panelSwitchSliceForView.ts

@@ -1,7 +1,7 @@
 import { createSlice } from '@reduxjs/toolkit';
 
 interface PanelSwitchStateForView {
-  currentPanel: 'OperationPanel' | 'SendPanel' | 'MeasurementPanel' | 'MorePanel';
+  currentPanel: 'OperationPanel' | 'SendPanel' | 'MeasurementPanel' | 'MorePanel' | 'AdvancedProcessingPanel';
 }
 
 const initialState: PanelSwitchStateForView = {
@@ -24,6 +24,9 @@ const panelSwitchSliceForView = createSlice({
     switchToMorePanel: (state) => {
       state.currentPanel = 'MorePanel';
     },
+    switchToAdvancedProcessingPanel: (state) => {
+      state.currentPanel = 'AdvancedProcessingPanel';
+    },
   },
 });
 
@@ -32,5 +35,6 @@ export const {
   switchToSendPanel,
   switchToMeasurementPanel,
   switchToMorePanel,
+  switchToAdvancedProcessingPanel,
 } = panelSwitchSliceForView.actions;
 export default panelSwitchSliceForView.reducer;

+ 2 - 0
src/states/store.ts

@@ -17,6 +17,7 @@ import bodyPositionListenerMiddleware from './exam/bodyPositionListener';
 import { aprMiddleware } from './exam/aprSlice';
 import functionAreaReducer from './view/functionAreaSlice';
 import measurementPanelReducer from './view/measurementPanelSlice';
+import advancedProcessingPanelReducer from './view/advancedProcessingPanelSlice';
 import viewerContainerReducer from './view/viewerContainerSlice';
 import searchReducer from './patient/worklist/slices/searchSlice';
 import businessFlowMiddleware from './businessFlowMiddleware';
@@ -98,6 +99,7 @@ const store = configureStore({
     apr: aprReducer,
     functionArea: functionAreaReducer,
     measurementPanel: measurementPanelReducer,
+    advancedProcessingPanel: advancedProcessingPanelReducer,
     viewerContainer: viewerContainerReducer,
     workEntities: workEntitiesSlice.reducer,
     workFilters: workFiltersSlice.reducer,

+ 38 - 0
src/states/view/advancedProcessingPanelSlice.ts

@@ -0,0 +1,38 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export type ProcessingMode = '均衡' | '高对比' | '锐组织' | '骨骼';
+
+export type WindowAction = 
+  | '减小窗宽'
+  | '增大窗宽'
+  | '减小窗位'
+  | '增大窗位';
+
+interface AdvancedProcessingPanelState {
+  selectedMode: ProcessingMode;
+}
+
+const initialState: AdvancedProcessingPanelState = {
+  selectedMode: '骨骼', // 默认选中骨骼模式
+};
+
+const advancedProcessingPanelSlice = createSlice({
+  name: 'advancedProcessingPanel',
+  initialState,
+  reducers: {
+    setProcessingMode: (state, action: PayloadAction<ProcessingMode>) => {
+      state.selectedMode = action.payload;
+    },
+    performWindowAction: (state, action: PayloadAction<WindowAction>) => {
+      // 暂时只记录 action,实际图像处理逻辑将在 ViewerContainer 中监听此 action 后实现
+      console.log(`执行窗宽窗位操作: ${action.payload}`);
+    },
+  },
+});
+
+export const {
+  setProcessingMode,
+  performWindowAction,
+} = advancedProcessingPanelSlice.actions;
+
+export default advancedProcessingPanelSlice.reducer;