feat: deviceSlice添加
This commit is contained in:
63
src/store/slices/deviceSlice.test.js
Normal file
63
src/store/slices/deviceSlice.test.js
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
190
src/store/slices/deviceSlice.usage.example.jsx
Normal file
190
src/store/slices/deviceSlice.usage.example.jsx
Normal file
@@ -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 (
|
||||
<Box>
|
||||
{isMobile ? (
|
||||
<Text>📱 移动端视图</Text>
|
||||
) : (
|
||||
<Text>💻 桌面端视图</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例 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 (
|
||||
<VStack>
|
||||
<Text>当前设备: {isMobile ? '移动设备' : '桌面设备'}</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
试试调整浏览器窗口大小
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例 3: 响应式布局 - 根据设备类型调整样式
|
||||
*/
|
||||
export const ResponsiveLayoutExample = () => {
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={isMobile ? 4 : 8}
|
||||
bg={isMobile ? 'blue.50' : 'gray.50'}
|
||||
borderRadius={isMobile ? 'md' : 'xl'}
|
||||
maxW={isMobile ? '100%' : '800px'}
|
||||
mx="auto"
|
||||
>
|
||||
<Text fontSize={isMobile ? 'md' : 'lg'}>
|
||||
响应式内容区域
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.600" mt={2}>
|
||||
Padding: {isMobile ? '16px' : '32px'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例 4: 条件渲染组件 - 移动端显示简化版
|
||||
*/
|
||||
export const ConditionalRenderExample = () => {
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isMobile ? (
|
||||
// 移动端:简化版导航栏
|
||||
<Box bg="blue.500" p={2}>
|
||||
<Text color="white" fontSize="sm">☰ 菜单</Text>
|
||||
</Box>
|
||||
) : (
|
||||
// 桌面端:完整导航栏
|
||||
<Box bg="blue.500" p={4}>
|
||||
<Text color="white" fontSize="lg">
|
||||
首页 | 产品 | 关于我们 | 联系方式
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例 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 (
|
||||
<Box>
|
||||
{/* <Text>移动设备: {isMobile ? '是' : '否'}</Text> */}
|
||||
{/* <Text>桌面设备: {isDesktop ? '是' : '否'}</Text> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 推荐实践:
|
||||
*
|
||||
* 1. 在 App.js 中添加全局 resize 监听器
|
||||
* 2. 创建自定义 Hook (useDevice) 简化使用
|
||||
* 3. 结合 Chakra UI 的响应式 Props(优先使用 Chakra 内置响应式)
|
||||
* 4. 仅在需要 JS 逻辑判断时使用 Redux(如条件渲染、动态导入)
|
||||
*
|
||||
* Chakra UI 响应式示例(推荐优先使用):
|
||||
* <Box
|
||||
* fontSize={{ base: 'sm', md: 'md', lg: 'lg' }} // Chakra 内置响应式
|
||||
* p={{ base: 4, md: 6, lg: 8 }}
|
||||
* >
|
||||
* 内容
|
||||
* </Box>
|
||||
*/
|
||||
203
src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
Normal file
203
src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
Normal file
@@ -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<ToolSelectorProps> = ({ selectedTools, onToolsChange }) => {
|
||||
/**
|
||||
* 全选所有工具
|
||||
*/
|
||||
const handleSelectAll = () => {
|
||||
onToolsChange(MCP_TOOLS.map((t) => t.id));
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空所有选择
|
||||
*/
|
||||
const handleClearAll = () => {
|
||||
onToolsChange([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 工具分类手风琴 */}
|
||||
<Accordion allowMultiple>
|
||||
{Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => {
|
||||
const selectedCount = tools.filter((t) => selectedTools.includes(t.id)).length;
|
||||
const totalCount = tools.length;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={category}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: catIdx * 0.05 }}
|
||||
>
|
||||
<AccordionItem
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
borderRadius="lg"
|
||||
mb={2}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
backdropFilter="blur(12px)"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.08)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
>
|
||||
{/* 手风琴标题 */}
|
||||
<AccordionButton>
|
||||
<HStack flex={1} justify="space-between" pr={2}>
|
||||
<Text color="gray.100" fontSize="sm">
|
||||
{category}
|
||||
</Text>
|
||||
<Badge
|
||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||
color="white"
|
||||
variant="subtle"
|
||||
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||
>
|
||||
{selectedCount}/{totalCount}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<AccordionIcon color="gray.400" />
|
||||
</AccordionButton>
|
||||
|
||||
{/* 手风琴内容 */}
|
||||
<AccordionPanel pb={4}>
|
||||
<CheckboxGroup value={selectedTools} onChange={onToolsChange}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{tools.map((tool) => (
|
||||
<motion.div
|
||||
key={tool.id}
|
||||
whileHover={{ x: 4 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
>
|
||||
<Checkbox
|
||||
value={tool.id}
|
||||
colorScheme="purple"
|
||||
p={2}
|
||||
borderRadius="lg"
|
||||
bg="rgba(255, 255, 255, 0.02)"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||
transition="background 0.2s"
|
||||
>
|
||||
<HStack spacing={2} align="start">
|
||||
{/* 工具图标 */}
|
||||
<Box color="purple.400" mt={0.5}>
|
||||
{tool.icon}
|
||||
</Box>
|
||||
|
||||
{/* 工具信息 */}
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.200">
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{tool.description}
|
||||
</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Checkbox>
|
||||
</motion.div>
|
||||
))}
|
||||
</VStack>
|
||||
</CheckboxGroup>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
|
||||
{/* 全选/清空按钮 */}
|
||||
<HStack mt={4} spacing={2}>
|
||||
{/* 全选按钮 */}
|
||||
<Box flex={1}>
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
w="full"
|
||||
variant="ghost"
|
||||
onClick={handleSelectAll}
|
||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||
color="white"
|
||||
_hover={{
|
||||
bgGradient: 'linear(to-r, blue.600, purple.600)',
|
||||
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.4)',
|
||||
}}
|
||||
>
|
||||
全选
|
||||
</Button>
|
||||
</motion.div>
|
||||
</Box>
|
||||
|
||||
{/* 清空按钮 */}
|
||||
<Box flex={1}>
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
w="full"
|
||||
variant="ghost"
|
||||
onClick={handleClearAll}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.300"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSelector;
|
||||
Reference in New Issue
Block a user