Browse Source

feat: 实现MorePanel面板及全选反选功能,优化OperationPanel布局

- 新增 MorePanel 组件,提供更多功能的独立面板
- 新增 SelectionArea 组件,实现全选和反选图像功能
- 新增 ImageInfoArea 组件,添加图像详情和tag信息按钮UI(功能待实现)
- 在 panelSwitchSliceForView 中添加 MorePanel 面板类型和切换action
- 在 FunctionArea 中添加 More 按钮点击逻辑,支持切换到 MorePanel
- 优化 OperationPanel 布局,使用 Flex 布局替代 Layout 结构实现滚动
- 从 OperationPanel 中移除 SelectionArea 和 ImageInfoArea,简化主面板布局
- 在 ImageProcessingPageLarge 中添加样式确保 OperationPanel 获得 100% 高度

改动文件:
- src/states/panelSwitchSliceForView.ts
- src/pages/view/components/MorePanel.tsx
- src/pages/view/components/SelectionArea.tsx
- src/pages/view/components/ImageInfoArea.tsx
- src/pages/view/components/FunctionArea.tsx
- src/pages/view/components/OperationPanel.tsx
- src/pages/view/ImageProcessingPageLarge.tsx
sw 2 days ago
parent
commit
6dc98eb2aa

+ 9 - 1
src/pages/view/ImageProcessingPageLarge.tsx

@@ -22,7 +22,15 @@ const ImageProcessingPageLarge = () => {
       <Content>
         <ImageControl />
       </Content>
-      <Sider width={300}>
+      <Sider width={300} className="operation-panel-sider">
+        {/**使用style注入方式确保OperationPanel获得100%高度 */}
+        <style>{`
+            .operation-panel-sider > .ant-layout-sider-children {
+              display: flex;
+              flex-direction: column;
+              height: 100%;
+            }
+          `}</style>
         <OperationPanel />
       </Sider>
     </Layout>

+ 4 - 2
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 } from '@/states/panelSwitchSliceForView';
+import { switchToMeasurementPanel, switchToMorePanel } from '@/states/panelSwitchSliceForView';
 import Icon from '@/components/Icon';
 import { showNotImplemented } from '@/utils/notificationHelper';
 
@@ -24,7 +24,6 @@ const FunctionButton = ({
       action==='Crop Selected Area' ||
       ['Delete Mask',
         'Image Comparison','Zoom Image','Reset Cursor','Pan','Snapshot','Advanced Processing','Musician',
-        'More',
       ].includes(action)
     ){
 showNotImplemented('');
@@ -34,6 +33,9 @@ return;
     if (action === 'Image Measurement') {
       // 切换到测量面板
       dispatch(switchToMeasurementPanel());
+    } else if (action === 'More') {
+      // 切换到更多功能面板
+      dispatch(switchToMorePanel());
     } else {
       // 其他功能按钮保持原有逻辑
       dispatch(setAction(action));

+ 69 - 0
src/pages/view/components/ImageInfoArea.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import { Button, Flex } from 'antd';
+import Icon from '@/components/Icon';
+
+/**
+ * 图像信息区域组件
+ * 
+ * 功能:
+ * - 图像详情按钮:显示图像详细信息(功能待实现)
+ * - tag信息按钮:显示tag相关信息(功能待实现)
+ */
+const ImageInfoArea: React.FC = () => {
+  // 图像详情按钮处理函数(待实现)
+  const handleImageDetails = () => {
+    // TODO: 实现图像详情功能
+    console.log('图像详情功能待实现');
+  };
+
+  // tag信息按钮处理函数(待实现)
+  const handleTagInfo = () => {
+    // TODO: 实现tag信息功能
+    console.log('tag信息功能待实现');
+  };
+
+  return (
+    <Flex gap="small" align="center" justify="start">
+      <Button
+        onClick={handleImageDetails}
+        icon={
+          <Icon
+            module="module-process"
+            name="RejectImage"
+            userId="base"
+            theme="default"
+            size="2x"
+            state="normal"
+          />
+        }
+        style={{
+          width: '1.5rem',
+          height: '1.5rem',
+          padding: 0,
+        }}
+        title="图像详情"
+      />
+      <Button
+        onClick={handleTagInfo}
+        icon={
+          <Icon
+            module="module-process"
+            name="RejectImage"
+            userId="base"
+            theme="default"
+            size="2x"
+            state="normal"
+          />
+        }
+        style={{
+          width: '1.5rem',
+          height: '1.5rem',
+          padding: 0,
+        }}
+        title="tag信息"
+      />
+    </Flex>
+  );
+};
+
+export default ImageInfoArea;

+ 181 - 0
src/pages/view/components/MorePanel.tsx

@@ -0,0 +1,181 @@
+import React from 'react';
+import { Layout, Button, Typography, Divider, Flex } from 'antd';
+import { ArrowLeftOutlined } from '@ant-design/icons';
+import { useDispatch, useSelector } from 'react-redux';
+import { switchToOperationPanel } from '../../../states/panelSwitchSliceForView';
+import {
+  selectAllViewers,
+  selectAllViewerUrls,
+  selectSelectedViewers,
+  setSelectedViewers,
+} from '@/states/view/viewerContainerSlice';
+import { RootState } from '@/states/store';
+import Icon from '@/components/Icon';
+import '@/themes/truncateText.css';
+
+const { Header, Content } = Layout;
+const { Title, Text } = Typography;
+
+const FunctionButton = ({
+  title,
+  action,
+  iconName,
+  onClick,
+}: {
+  title: string;
+  action: string;
+  iconName: string;
+  onClick: () => void;
+}) => {
+  return (
+    <Button
+      onClick={onClick}
+      icon={
+        <Icon
+          module="module-process"
+          name={iconName}
+          userId="base"
+          theme="default"
+          size="2x"
+          state="normal"
+        />
+      }
+      style={{
+        width: '1.5rem',
+        height: '1.5rem',
+        padding: 0,
+      }}
+      title={title}
+      className="truncate-text"
+    />
+  );
+};
+
+const MorePanel = () => {
+  const dispatch = useDispatch();
+
+  // 获取所有 viewer URLs 和当前选中的 viewer URLs
+  const allViewers = useSelector(selectAllViewerUrls);
+  const selectedViewers = useSelector(selectSelectedViewers);
+
+  const handleReturn = () => {
+    dispatch(switchToOperationPanel());
+  };
+
+  // 全选处理函数
+  const handleSelectAll = () => {
+    dispatch(selectAllViewers());
+  };
+
+  // 反选处理函数
+  const handleInvertSelection = () => {
+    // 获取未选中的 viewers
+    const invertedSelection = allViewers.filter(
+      (url) => !selectedViewers.includes(url)
+    );
+    
+    // 如果反选后为空,则保持至少一个选中项(选中第一个)
+    if (invertedSelection.length === 0 && allViewers.length > 0) {
+      dispatch(setSelectedViewers([allViewers[0]]));
+    } else {
+      dispatch(setSelectedViewers(invertedSelection));
+    }
+  };
+
+  // 图像详情按钮处理函数(待实现)
+  const handleImageDetails = () => {
+    // TODO: 实现图像详情功能
+    console.log('图像详情功能待实现');
+  };
+
+  // tag信息按钮处理函数(待实现)
+  const handleTagInfo = () => {
+    // TODO: 实现tag信息功能
+    console.log('tag信息功能待实现');
+  };
+
+  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' }}
+      >
+        {/* 选择操作组 */}
+        <div style={{ marginBottom: '24px' }}>
+          <Text
+            strong
+            style={{ fontSize: '14px', marginBottom: '12px', display: 'block' }}
+          >
+            选择操作
+          </Text>
+          <Flex wrap gap="small" align="center" justify="start" className="p-1">
+            <FunctionButton
+              title="全选"
+              action="全选"
+              iconName="RejectImage"
+              onClick={handleSelectAll}
+            />
+            <FunctionButton
+              title="反选"
+              action="反选"
+              iconName="RejectImage"
+              onClick={handleInvertSelection}
+            />
+          </Flex>
+          <div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
+            说明:全选或反选所有 viewer 中的图像
+          </div>
+        </div>
+
+        <Divider />
+
+        {/* 信息查看组 */}
+        <div style={{ marginBottom: '24px' }}>
+          <Text
+            strong
+            style={{ fontSize: '14px', marginBottom: '12px', display: 'block' }}
+          >
+            信息查看
+          </Text>
+          <Flex wrap gap="small" align="center" justify="start" className="p-1">
+            <FunctionButton
+              title="图像详情"
+              action="图像详情"
+              iconName="RejectImage"
+              onClick={handleImageDetails}
+            />
+            <FunctionButton
+              title="tag信息"
+              action="tag信息"
+              iconName="RejectImage"
+              onClick={handleTagInfo}
+            />
+          </Flex>
+          <div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
+            说明:查看图像详细信息和tag相关信息(功能待实现)
+          </div>
+        </div>
+      </Content>
+    </Layout>
+  );
+};
+
+export default MorePanel;

+ 33 - 28
src/pages/view/components/OperationPanel.tsx

@@ -1,14 +1,13 @@
-import { Layout, Divider, Flex } from 'antd';
+import { Divider, Flex } from 'antd';
 import { useSelector } from 'react-redux';
 import FunctionArea from './FunctionArea';
 import TransferArea from './TransferArea';
 import SendPanelForView from '../../output/SendPanelForView';
 import MeasurementPanel from './MeasurementPanel';
+import MorePanel from './MorePanel';
 import ImageStateControl from './ImageStateControl';
 import { RootState } from '../../../states/store';
 
-const { Content, Footer } = Layout;
-
 const OperationPanel = () => {
   const currentPanel = useSelector(
     (state: RootState) => state.panelSwitchForView.currentPanel
@@ -18,44 +17,50 @@ const OperationPanel = () => {
     switch (currentPanel) {
       case 'OperationPanel':
         return (
-          <>
-            <Content>
-              <FunctionArea />
-            </Content>
+          <Flex 
+            vertical 
+            gap="small" 
+            style={{ 
+              height: '100%', 
+              overflowY: 'auto',
+              //padding: '0.5rem' 
+            }}
+          >
+            <FunctionArea />
+            <Divider style={{ margin: 0 }} />
+            <ImageStateControl />
             <Divider style={{ margin: 0 }} />
-            <Footer className="p-1">
-              <Flex vertical gap="small">
-                <ImageStateControl />
-                <Divider style={{ margin: '0.5rem 0' }} />
-                <TransferArea />
-              </Flex>
-            </Footer>
-          </>
+            <TransferArea />
+          </Flex>
         );
       case 'SendPanel':
         return <SendPanelForView />;
       case 'MeasurementPanel':
         return <MeasurementPanel />;
+      case 'MorePanel':
+        return <MorePanel />;
       default:
         return (
-          <>
-            <Content>
-              <FunctionArea />
-            </Content>
+          <Flex 
+            vertical 
+            gap="small" 
+            style={{ 
+              height: '100%', 
+              overflowY: 'auto',
+              padding: '0.5rem' 
+            }}
+          >
+            <FunctionArea />
+            <Divider style={{ margin: 0 }} />
+            <ImageStateControl />
             <Divider style={{ margin: 0 }} />
-            <Footer className="p-1">
-              <Flex vertical gap="small">
-                <ImageStateControl />
-                <Divider style={{ margin: '0.5rem 0' }} />
-                <TransferArea />
-              </Flex>
-            </Footer>
-          </>
+            <TransferArea />
+          </Flex>
         );
     }
   };
 
-  return <Layout className="h-full">{renderPanel()}</Layout>;
+  return <div style={{ height: '100%' }}>{renderPanel()}</div>;
 };
 
 export default OperationPanel;

+ 91 - 0
src/pages/view/components/SelectionArea.tsx

@@ -0,0 +1,91 @@
+import React from 'react';
+import { Button, Flex } from 'antd';
+import { useSelector } from 'react-redux';
+import { RootState, useAppDispatch } from '@/states/store';
+import {
+  selectAllViewers,
+  selectAllViewerUrls,
+  selectSelectedViewers,
+  setSelectedViewers,
+} from '@/states/view/viewerContainerSlice';
+import Icon from '@/components/Icon';
+
+/**
+ * 选择区域组件
+ * 
+ * 功能:
+ * - 全选按钮:选择所有 viewer 中的图像
+ * - 反选按钮:反向选择当前选中状态
+ */
+const SelectionArea: React.FC = () => {
+  const dispatch = useAppDispatch();
+
+  // 获取所有 viewer URLs 和当前选中的 viewer URLs
+  const allViewers = useSelector(selectAllViewerUrls);
+  const selectedViewers = useSelector(selectSelectedViewers);
+
+  // 全选处理函数
+  const handleSelectAll = () => {
+    dispatch(selectAllViewers());
+  };
+
+  // 反选处理函数
+  const handleInvertSelection = () => {
+    // 获取未选中的 viewers
+    const invertedSelection = allViewers.filter(
+      (url) => !selectedViewers.includes(url)
+    );
+    
+    // 如果反选后为空,则保持至少一个选中项(选中第一个)
+    if (invertedSelection.length === 0 && allViewers.length > 0) {
+      dispatch(setSelectedViewers([allViewers[0]]));
+    } else {
+      dispatch(setSelectedViewers(invertedSelection));
+    }
+  };
+
+  return (
+    <Flex gap="small" align="center" justify="start">
+      <Button
+        onClick={handleSelectAll}
+        icon={
+          <Icon
+            module="module-process"
+            name="RejectImage"
+            userId="base"
+            theme="default"
+            size="2x"
+            state="normal"
+          />
+        }
+        style={{
+          width: '1.5rem',
+          height: '1.5rem',
+          padding: 0,
+        }}
+        title="全选"
+      />
+      <Button
+        onClick={handleInvertSelection}
+        icon={
+          <Icon
+            module="module-process"
+            name="RejectImage"
+            userId="base"
+            theme="default"
+            size="2x"
+            state="normal"
+          />
+        }
+        style={{
+          width: '1.5rem',
+          height: '1.5rem',
+          padding: 0,
+        }}
+        title="反选"
+      />
+    </Flex>
+  );
+};
+
+export default SelectionArea;

+ 5 - 1
src/states/panelSwitchSliceForView.ts

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