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