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:
zdl
2026-01-08 19:03:49 +08:00
parent 831c65ee53
commit 5ae05eebd8
2 changed files with 342 additions and 0 deletions

View 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;

View 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;