feat(Concept): 新增统一图表容器组件
- ChartBreadcrumb.js: 通用面包屑导航组件 - ChartContainer.js: 统一图表容器 - 极光背景动画 - 面包屑导航(左上角) - 全屏按钮(右上角) - 底部图例(可选) - contentTopPadding 可配置内边距 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
96
src/views/Concept/components/ChartBreadcrumb.js
Normal file
96
src/views/Concept/components/ChartBreadcrumb.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* ChartBreadcrumb - 图表通用面包屑导航组件
|
||||
*
|
||||
* 用于矩形树图和层级图共享的面包屑导航
|
||||
*/
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
HStack,
|
||||
Text,
|
||||
Icon,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowLeft, ChevronRight } from 'lucide-react';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* @param {Array<{label: string, path: object|null}>} items - 面包屑项目
|
||||
* @param {function} onNavigate - 导航回调 (path) => void
|
||||
* @param {function} onGoBack - 返回上一层回调
|
||||
* @param {boolean} showBackButton - 是否显示返回按钮
|
||||
*/
|
||||
const ChartBreadcrumb = memo(({
|
||||
items = [],
|
||||
onNavigate,
|
||||
onGoBack,
|
||||
showBackButton = false,
|
||||
}) => {
|
||||
return (
|
||||
<HStack spacing={2}>
|
||||
{/* 返回按钮 */}
|
||||
{showBackButton && (
|
||||
<Tooltip label="返回上一层">
|
||||
<IconButton
|
||||
icon={<ArrowLeft size={16} />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onGoBack}
|
||||
bg="rgba(255, 255, 255, 0.08)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
color="whiteAlpha.800"
|
||||
borderRadius="full"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.15)',
|
||||
transform: 'scale(1.05)',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 面包屑路径 */}
|
||||
<HStack
|
||||
bg="rgba(255, 255, 255, 0.08)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
spacing={1}
|
||||
boxShadow="0 4px 16px rgba(0, 0, 0, 0.2)"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && (
|
||||
<Icon as={ChevronRight} boxSize={3} color="whiteAlpha.400" />
|
||||
)}
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={index === items.length - 1 ? 'purple.300' : 'whiteAlpha.700'}
|
||||
fontWeight={index === items.length - 1 ? 'bold' : 'normal'}
|
||||
cursor={index < items.length - 1 ? 'pointer' : 'default'}
|
||||
_hover={index < items.length - 1 ? { color: 'purple.300' } : {}}
|
||||
onClick={() => {
|
||||
if (index < items.length - 1 && onNavigate) {
|
||||
onNavigate(item.path);
|
||||
}
|
||||
}}
|
||||
transition="color 0.2s"
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
ChartBreadcrumb.displayName = 'ChartBreadcrumb';
|
||||
|
||||
export default ChartBreadcrumb;
|
||||
246
src/views/Concept/components/ChartContainer.js
Normal file
246
src/views/Concept/components/ChartContainer.js
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* ChartContainer - 图表统一容器组件
|
||||
*
|
||||
* 用于矩形树图和层级图的统一容器,包含:
|
||||
* - 极光背景动画
|
||||
* - 面包屑导航
|
||||
* - 全屏按钮
|
||||
* - 底部图例(可选)
|
||||
*/
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
Text,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
import ChartBreadcrumb from './ChartBreadcrumb';
|
||||
|
||||
// 极光动画
|
||||
const auroraAnimation = keyframes`
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
background-position: 50% 100%;
|
||||
filter: hue-rotate(10deg);
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
75% {
|
||||
background-position: 50% 0%;
|
||||
filter: hue-rotate(-10deg);
|
||||
}
|
||||
`;
|
||||
|
||||
// 光晕脉冲动画
|
||||
const glowPulse = keyframes`
|
||||
0%, 100% { opacity: 0.3; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.05); }
|
||||
`;
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* @param {Array} breadcrumbItems - 面包屑项目
|
||||
* @param {function} onNavigate - 导航回调
|
||||
* @param {function} onGoBack - 返回回调
|
||||
* @param {boolean} showBackButton - 是否显示返回按钮
|
||||
* @param {boolean} showLegend - 是否显示底部图例
|
||||
* @param {string} height - 容器高度
|
||||
* @param {number} contentTopPadding - 内容区顶部内边距(避开导航栏)
|
||||
* @param {ReactNode} children - 图表内容
|
||||
*/
|
||||
const ChartContainer = memo(({
|
||||
breadcrumbItems = [],
|
||||
onNavigate,
|
||||
onGoBack,
|
||||
showBackButton = false,
|
||||
showLegend = false,
|
||||
height,
|
||||
contentTopPadding = 14,
|
||||
children,
|
||||
}) => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
setIsFullscreen(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 计算容器高度
|
||||
const containerHeight = isFullscreen
|
||||
? 'calc(100vh - 60px)'
|
||||
: height || (isMobile ? '500px' : '700px');
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={isFullscreen ? 'fixed' : 'relative'}
|
||||
top={isFullscreen ? '60px' : 'auto'}
|
||||
left={isFullscreen ? 0 : 'auto'}
|
||||
right={isFullscreen ? '72px' : 'auto'}
|
||||
bottom={isFullscreen ? 0 : 'auto'}
|
||||
zIndex={isFullscreen ? 1000 : 'auto'}
|
||||
borderRadius={isFullscreen ? '0' : '3xl'}
|
||||
overflow="hidden"
|
||||
border={isFullscreen ? 'none' : '1px solid'}
|
||||
borderColor="whiteAlpha.100"
|
||||
h={containerHeight}
|
||||
bg="transparent"
|
||||
>
|
||||
{/* 极光背景层 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="linear-gradient(135deg, #0F172A 0%, #1E1B4B 25%, #312E81 50%, #1E1B4B 75%, #0F172A 100%)"
|
||||
backgroundSize="400% 400%"
|
||||
animation={`${auroraAnimation} 15s ease infinite`}
|
||||
/>
|
||||
|
||||
{/* 弥散光晕层 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w="300px"
|
||||
h="300px"
|
||||
bg="radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, transparent 70%)"
|
||||
filter="blur(60px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 4s ease-in-out infinite`}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="20%"
|
||||
right="15%"
|
||||
w="250px"
|
||||
h="250px"
|
||||
bg="radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)"
|
||||
filter="blur(50px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 5s ease-in-out infinite 1s`}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
right="30%"
|
||||
w="200px"
|
||||
h="200px"
|
||||
bg="radial-gradient(circle, rgba(236, 72, 153, 0.2) 0%, transparent 70%)"
|
||||
filter="blur(40px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 6s ease-in-out infinite 2s`}
|
||||
/>
|
||||
|
||||
{/* 左上角面包屑导航 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={4}
|
||||
left={4}
|
||||
zIndex={10}
|
||||
>
|
||||
<ChartBreadcrumb
|
||||
items={breadcrumbItems}
|
||||
onNavigate={onNavigate}
|
||||
onGoBack={onGoBack}
|
||||
showBackButton={showBackButton}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右上角全屏按钮 */}
|
||||
<Tooltip label={isFullscreen ? '退出全屏' : '全屏查看'}>
|
||||
<IconButton
|
||||
position="absolute"
|
||||
top={4}
|
||||
right={4}
|
||||
zIndex={10}
|
||||
icon={isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={toggleFullscreen}
|
||||
bg="rgba(255, 255, 255, 0.08)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
color="whiteAlpha.800"
|
||||
borderRadius="full"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.15)',
|
||||
transform: 'scale(1.05)',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 底部图例 */}
|
||||
{showLegend && (
|
||||
<HStack
|
||||
position="absolute"
|
||||
bottom={4}
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
zIndex={10}
|
||||
bg="rgba(255, 255, 255, 0.08)"
|
||||
backdropFilter={GLASS_BLUR.lg}
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
spacing={3}
|
||||
boxShadow="0 4px 16px rgba(0, 0, 0, 0.2)"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Box
|
||||
w={3}
|
||||
h={3}
|
||||
borderRadius="full"
|
||||
bg="linear-gradient(135deg, #EF4444, #DC2626)"
|
||||
boxShadow="0 0 8px rgba(239, 68, 68, 0.5)"
|
||||
/>
|
||||
<Text color="whiteAlpha.800" fontSize="xs">涨</Text>
|
||||
</HStack>
|
||||
<Box w="1px" h={4} bg="whiteAlpha.200" />
|
||||
<HStack spacing={2}>
|
||||
<Box
|
||||
w={3}
|
||||
h={3}
|
||||
borderRadius="full"
|
||||
bg="linear-gradient(135deg, #22C55E, #16A34A)"
|
||||
boxShadow="0 0 8px rgba(34, 197, 94, 0.5)"
|
||||
/>
|
||||
<Text color="whiteAlpha.800" fontSize="xs">跌</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 图表内容 - 添加顶部和底部内边距避开导航和图例 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
pt={contentTopPadding}
|
||||
pb={showLegend ? 14 : 4}
|
||||
px={4}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
ChartContainer.displayName = 'ChartContainer';
|
||||
|
||||
export default ChartContainer;
|
||||
Reference in New Issue
Block a user