From bb878c5346b355b505e679d1ee9b1935cc2a4d56 Mon Sep 17 00:00:00 2001
From: zdl <3489966805@qq.com>
Date: Tue, 25 Nov 2025 16:57:48 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20deviceSlice=E6=B7=BB=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/store/slices/deviceSlice.test.js | 63 ++++++
.../slices/deviceSlice.usage.example.jsx | 190 ++++++++++++++++
.../components/RightSidebar/ToolSelector.tsx | 203 ++++++++++++++++++
3 files changed, 456 insertions(+)
create mode 100644 src/store/slices/deviceSlice.test.js
create mode 100644 src/store/slices/deviceSlice.usage.example.jsx
create mode 100644 src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
diff --git a/src/store/slices/deviceSlice.test.js b/src/store/slices/deviceSlice.test.js
new file mode 100644
index 00000000..f17eae0b
--- /dev/null
+++ b/src/store/slices/deviceSlice.test.js
@@ -0,0 +1,63 @@
+/**
+ * deviceSlice 单元测试
+ *
+ * 测试用例:
+ * 1. 初始状态检查
+ * 2. updateScreenSize action 测试
+ * 3. selector 函数测试
+ */
+
+import deviceReducer, { updateScreenSize, selectIsMobile } from './deviceSlice';
+
+describe('deviceSlice', () => {
+ describe('reducer', () => {
+ it('should return the initial state', () => {
+ const initialState = deviceReducer(undefined, { type: '@@INIT' });
+ expect(initialState).toHaveProperty('isMobile');
+ expect(typeof initialState.isMobile).toBe('boolean');
+ });
+
+ it('should handle updateScreenSize', () => {
+ // 模拟初始状态
+ const initialState = { isMobile: false };
+
+ // 执行 action(注意:实际 isMobile 值由 detectIsMobile() 决定)
+ const newState = deviceReducer(initialState, updateScreenSize());
+
+ // 验证状态结构
+ expect(newState).toHaveProperty('isMobile');
+ expect(typeof newState.isMobile).toBe('boolean');
+ });
+ });
+
+ describe('selectors', () => {
+ it('selectIsMobile should return correct value', () => {
+ const mockState = {
+ device: {
+ isMobile: true,
+ },
+ };
+
+ const result = selectIsMobile(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('selectIsMobile should return false for desktop', () => {
+ const mockState = {
+ device: {
+ isMobile: false,
+ },
+ };
+
+ const result = selectIsMobile(mockState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('actions', () => {
+ it('updateScreenSize action should have correct type', () => {
+ const action = updateScreenSize();
+ expect(action.type).toBe('device/updateScreenSize');
+ });
+ });
+});
diff --git a/src/store/slices/deviceSlice.usage.example.jsx b/src/store/slices/deviceSlice.usage.example.jsx
new file mode 100644
index 00000000..3d715ceb
--- /dev/null
+++ b/src/store/slices/deviceSlice.usage.example.jsx
@@ -0,0 +1,190 @@
+/**
+ * deviceSlice 使用示例
+ *
+ * 本文件展示如何在 React 组件中使用 deviceSlice 来实现响应式设计
+ */
+
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { selectIsMobile, updateScreenSize } from '@/store/slices/deviceSlice';
+import { Box, Text, VStack } from '@chakra-ui/react';
+
+/**
+ * 示例 1: 基础使用 - 根据设备类型渲染不同内容
+ */
+export const BasicUsageExample = () => {
+ const isMobile = useSelector(selectIsMobile);
+
+ return (
+
+ {isMobile ? (
+ 📱 移动端视图
+ ) : (
+ 💻 桌面端视图
+ )}
+
+ );
+};
+
+/**
+ * 示例 2: 监听窗口尺寸变化 - 动态更新设备状态
+ */
+export const ResizeListenerExample = () => {
+ const isMobile = useSelector(selectIsMobile);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ // 监听窗口尺寸变化
+ const handleResize = () => {
+ dispatch(updateScreenSize());
+ };
+
+ // 监听屏幕方向变化(移动设备)
+ const handleOrientationChange = () => {
+ dispatch(updateScreenSize());
+ };
+
+ window.addEventListener('resize', handleResize);
+ window.addEventListener('orientationchange', handleOrientationChange);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ window.removeEventListener('orientationchange', handleOrientationChange);
+ };
+ }, [dispatch]);
+
+ return (
+
+ 当前设备: {isMobile ? '移动设备' : '桌面设备'}
+
+ 试试调整浏览器窗口大小
+
+
+ );
+};
+
+/**
+ * 示例 3: 响应式布局 - 根据设备类型调整样式
+ */
+export const ResponsiveLayoutExample = () => {
+ const isMobile = useSelector(selectIsMobile);
+
+ return (
+
+
+ 响应式内容区域
+
+
+ Padding: {isMobile ? '16px' : '32px'}
+
+
+ );
+};
+
+/**
+ * 示例 4: 条件渲染组件 - 移动端显示简化版
+ */
+export const ConditionalRenderExample = () => {
+ const isMobile = useSelector(selectIsMobile);
+
+ return (
+
+ {isMobile ? (
+ // 移动端:简化版导航栏
+
+ ☰ 菜单
+
+ ) : (
+ // 桌面端:完整导航栏
+
+
+ 首页 | 产品 | 关于我们 | 联系方式
+
+
+ )}
+
+ );
+};
+
+/**
+ * 示例 5: 在 App.js 中全局监听(推荐方式)
+ *
+ * 将以下代码添加到 src/App.js 中:
+ */
+export const AppLevelResizeListenerExample = () => {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const handleResize = () => {
+ dispatch(updateScreenSize());
+ };
+
+ window.addEventListener('resize', handleResize);
+ window.addEventListener('orientationchange', handleResize);
+
+ // 初始化时也调用一次(可选)
+ handleResize();
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ window.removeEventListener('orientationchange', handleResize);
+ };
+ }, [dispatch]);
+
+ // 返回 null 或组件内容
+ return null;
+};
+
+/**
+ * 示例 6: 自定义 Hook 封装(推荐)
+ *
+ * 在 src/hooks/useDevice.js 中创建自定义 Hook:
+ */
+// import { useSelector } from 'react-redux';
+// import { selectIsMobile } from '@/store/slices/deviceSlice';
+//
+// export const useDevice = () => {
+// const isMobile = useSelector(selectIsMobile);
+//
+// return {
+// isMobile,
+// isDesktop: !isMobile,
+// };
+// };
+
+/**
+ * 使用自定义 Hook:
+ */
+export const CustomHookUsageExample = () => {
+ // const { isMobile, isDesktop } = useDevice();
+
+ return (
+
+ {/* 移动设备: {isMobile ? '是' : '否'} */}
+ {/* 桌面设备: {isDesktop ? '是' : '否'} */}
+
+ );
+};
+
+/**
+ * 推荐实践:
+ *
+ * 1. 在 App.js 中添加全局 resize 监听器
+ * 2. 创建自定义 Hook (useDevice) 简化使用
+ * 3. 结合 Chakra UI 的响应式 Props(优先使用 Chakra 内置响应式)
+ * 4. 仅在需要 JS 逻辑判断时使用 Redux(如条件渲染、动态导入)
+ *
+ * Chakra UI 响应式示例(推荐优先使用):
+ *
+ * 内容
+ *
+ */
diff --git a/src/views/AgentChat/components/RightSidebar/ToolSelector.tsx b/src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
new file mode 100644
index 00000000..29e20778
--- /dev/null
+++ b/src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
@@ -0,0 +1,203 @@
+// src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
+// 工具选择组件
+
+import React from 'react';
+import { motion } from 'framer-motion';
+import {
+ Button,
+ Badge,
+ Checkbox,
+ CheckboxGroup,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
+ HStack,
+ VStack,
+ Box,
+ Text,
+} from '@chakra-ui/react';
+import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
+
+/**
+ * ToolSelector 组件的 Props 类型
+ */
+interface ToolSelectorProps {
+ /** 已选工具 ID 列表 */
+ selectedTools: string[];
+ /** 工具选择变化回调 */
+ onToolsChange: (tools: string[]) => void;
+}
+
+/**
+ * ToolSelector - 工具选择组件
+ *
+ * 职责:
+ * 1. 按分类展示工具列表(Accordion 手风琴)
+ * 2. 复选框选择/取消工具
+ * 3. 显示每个分类的已选/总数(如 "3/5")
+ * 4. 全选/清空按钮
+ *
+ * 设计特性:
+ * - 手风琴分类折叠
+ * - 悬停工具项右移 4px
+ * - 全选/清空按钮渐变色
+ * - 分类徽章显示选中数量
+ */
+const ToolSelector: React.FC = ({ selectedTools, onToolsChange }) => {
+ /**
+ * 全选所有工具
+ */
+ const handleSelectAll = () => {
+ onToolsChange(MCP_TOOLS.map((t) => t.id));
+ };
+
+ /**
+ * 清空所有选择
+ */
+ const handleClearAll = () => {
+ onToolsChange([]);
+ };
+
+ return (
+ <>
+ {/* 工具分类手风琴 */}
+
+ {Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => {
+ const selectedCount = tools.filter((t) => selectedTools.includes(t.id)).length;
+ const totalCount = tools.length;
+
+ return (
+
+
+ {/* 手风琴标题 */}
+
+
+
+ {category}
+
+
+ {selectedCount}/{totalCount}
+
+
+
+
+
+ {/* 手风琴内容 */}
+
+
+
+ {tools.map((tool) => (
+
+
+
+ {/* 工具图标 */}
+
+ {tool.icon}
+
+
+ {/* 工具信息 */}
+
+
+ {tool.name}
+
+
+ {tool.description}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+ })}
+
+
+ {/* 全选/清空按钮 */}
+
+ {/* 全选按钮 */}
+
+
+
+
+
+
+ {/* 清空按钮 */}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ToolSelector;