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;