814 lines
25 KiB
JavaScript
814 lines
25 KiB
JavaScript
import React, { useRef, useMemo, useState, useEffect } from 'react';
|
||
import { motion } from 'framer-motion';
|
||
import Particles from 'react-tsparticles';
|
||
import { loadSlim } from 'tsparticles-slim';
|
||
import {
|
||
Box,
|
||
Container,
|
||
Heading,
|
||
Text,
|
||
Button,
|
||
HStack,
|
||
VStack,
|
||
Badge,
|
||
Grid,
|
||
GridItem,
|
||
Stat,
|
||
StatLabel,
|
||
StatNumber,
|
||
Flex,
|
||
Tag,
|
||
useColorModeValue,
|
||
} from '@chakra-ui/react';
|
||
import {
|
||
LineChart,
|
||
Line,
|
||
AreaChart,
|
||
Area,
|
||
BarChart,
|
||
Bar,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
ComposedChart,
|
||
ReferenceLine,
|
||
ReferenceDot,
|
||
Cell,
|
||
} from 'recharts';
|
||
import { indexService } from '../../../services/eventService';
|
||
|
||
// 将后端分钟/分时数据转换为 Recharts 数据
|
||
const toLineSeries = (resp) => {
|
||
const arr = resp?.data || [];
|
||
return arr.map((d, i) => ({ time: d.time || i, value: d.price ?? d.close, volume: d.volume }));
|
||
};
|
||
|
||
// 提取昨日收盘价:优先使用最后一条记录的 prev_close;否则回退到倒数第二条的 close
|
||
const getPrevClose = (resp) => {
|
||
const arr = resp?.data || [];
|
||
if (!arr.length) return null;
|
||
const last = arr[arr.length - 1] || {};
|
||
if (last.prev_close !== undefined && last.prev_close !== null && isFinite(Number(last.prev_close))) {
|
||
return Number(last.prev_close);
|
||
}
|
||
const idx = arr.length >= 2 ? arr.length - 2 : arr.length - 1;
|
||
const k = arr[idx] || {};
|
||
const candidate = k.close ?? k.c ?? k.price ?? null;
|
||
return candidate != null ? Number(candidate) : null;
|
||
};
|
||
|
||
// 组合图表组件(折线图 + 成交量柱状图)
|
||
const CombinedChart = ({ series, title, color = "#FFD700", basePrice = null }) => {
|
||
const [cursorIndex, setCursorIndex] = useState(0);
|
||
const cursorRef = useRef(0);
|
||
|
||
// 直接将光标设置到最后一个数据点,不再使用动画
|
||
useEffect(() => {
|
||
if (!series || series.length === 0) return;
|
||
// 直接设置到最后一个点
|
||
const lastIndex = series.length - 1;
|
||
cursorRef.current = lastIndex;
|
||
setCursorIndex(lastIndex);
|
||
}, [series && series.length]);
|
||
|
||
|
||
const yDomain = useMemo(() => {
|
||
if (!series || series.length === 0) return ['auto', 'auto'];
|
||
const values = series
|
||
.map((d) => d?.value)
|
||
.filter((v) => typeof v === 'number' && isFinite(v));
|
||
if (values.length === 0) return ['auto', 'auto'];
|
||
const minVal = Math.min(...values);
|
||
const maxVal = Math.max(...values);
|
||
const maxAbs = Math.max(Math.abs(minVal), Math.abs(maxVal));
|
||
const padding = Math.max(maxAbs * 0.1, 0.2);
|
||
return [-maxAbs - padding, maxAbs + padding];
|
||
}, [series]);
|
||
|
||
// 当前高亮点
|
||
const activePoint = useMemo(() => {
|
||
if (!series || series.length === 0) return null;
|
||
if (cursorIndex < 0 || cursorIndex >= series.length) return null;
|
||
return series[cursorIndex];
|
||
}, [series, cursorIndex]);
|
||
|
||
// 稳定的X轴ticks,避免随渲染跳动而闪烁
|
||
const xTicks = useMemo(() => {
|
||
if (!series || series.length === 0) return [];
|
||
const desiredLabels = ['09:30', '10:30', '11:30', '14:00', '15:00'];
|
||
const set = new Set(series.map(d => d?.time));
|
||
let ticks = desiredLabels.filter(t => set.has(t));
|
||
if (ticks.length === 0) {
|
||
// 回退到首/中/尾的稳定采样,避免空白
|
||
const len = series.length;
|
||
const idxs = [0, Math.round(len * 0.25), Math.round(len * 0.5), Math.round(len * 0.75), len - 1];
|
||
ticks = idxs.map(i => series[i]?.time).filter(Boolean);
|
||
}
|
||
return ticks;
|
||
}, [series && series.length]);
|
||
|
||
return (
|
||
<Box h="full" position="relative">
|
||
<Text
|
||
fontSize="xs"
|
||
color={color}
|
||
fontFamily="monospace"
|
||
mb={1}
|
||
px={2}
|
||
>
|
||
{title}
|
||
</Text>
|
||
<ResponsiveContainer width="100%" height="90%">
|
||
<ComposedChart data={series} margin={{ top: 10, right: 10, left: 0, bottom: 30 }}>
|
||
<defs>
|
||
<linearGradient id={`gradient-${title.replace(/[.\s]/g, '')}`} x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={color} stopOpacity={0.8}/>
|
||
<stop offset="100%" stopColor={color} stopOpacity={0.2}/>
|
||
</linearGradient>
|
||
<linearGradient id={`barGradient-${title.replace(/[.\s]/g, '')}`} x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={color} stopOpacity={0.3}/>
|
||
<stop offset="100%" stopColor={color} stopOpacity={0.05}/>
|
||
</linearGradient>
|
||
<linearGradient id={`barGradientActive-${title.replace(/[.\s]/g, '')}`} x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={color} stopOpacity={0.8}/>
|
||
<stop offset="100%" stopColor={color} stopOpacity={0.3}/>
|
||
</linearGradient>
|
||
{/* 发光效果 */}
|
||
<filter id={`glow-${title.replace(/[.\s]/g, '')}`}>
|
||
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
|
||
<feMerge>
|
||
<feMergeNode in="coloredBlur"/>
|
||
<feMergeNode in="SourceGraphic"/>
|
||
</feMerge>
|
||
</filter>
|
||
</defs>
|
||
|
||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255, 215, 0, 0.1)" />
|
||
|
||
<XAxis
|
||
dataKey="time"
|
||
stroke={color}
|
||
tick={{ fill: color, fontSize: 10 }}
|
||
tickLine={false}
|
||
axisLine={{ stroke: `${color}33` }}
|
||
ticks={xTicks}
|
||
interval={0}
|
||
allowDuplicatedCategory={false}
|
||
/>
|
||
|
||
{/* 左Y轴 - 价格 */}
|
||
<YAxis
|
||
yAxisId="price"
|
||
stroke={color}
|
||
domain={yDomain}
|
||
tickFormatter={(v) => `${v.toFixed(2)}%`}
|
||
orientation="left"
|
||
/>
|
||
|
||
{/* 右Y轴 - 成交量(隐藏) */}
|
||
<YAxis
|
||
yAxisId="volume"
|
||
orientation="right"
|
||
hide
|
||
domain={[0, 'dataMax + 1000']}
|
||
/>
|
||
|
||
<Tooltip
|
||
contentStyle={{
|
||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||
border: `1px solid ${color}`,
|
||
borderRadius: '8px'
|
||
}}
|
||
labelStyle={{ color: '#fff' }}
|
||
itemStyle={{ color: '#fff' }}
|
||
labelFormatter={(label) => `时间: ${label}`}
|
||
formatter={(value, name) => {
|
||
if (name === 'value') {
|
||
const pct = Number(value);
|
||
if (typeof basePrice === 'number' && isFinite(basePrice)) {
|
||
const price = basePrice * (1 + pct / 100);
|
||
return [price.toFixed(2), '价格'];
|
||
}
|
||
return [`${pct.toFixed(2)}%`, '涨跌幅'];
|
||
}
|
||
if (name === 'volume') return [`${(Number(value) / 100000000).toFixed(2)}亿`, '成交量'];
|
||
return [value, name];
|
||
}}
|
||
/>
|
||
|
||
{/* 零轴参考线 */}
|
||
<ReferenceLine yAxisId="price" y={0} stroke="#666" strokeDasharray="4 4" />
|
||
|
||
{/* 成交量柱状图 */}
|
||
<Bar
|
||
yAxisId="volume"
|
||
dataKey="volume"
|
||
fill={`url(#barGradient-${title.replace(/[.\s]/g, '')})`}
|
||
radius={[2, 2, 0, 0]}
|
||
isAnimationActive={false}
|
||
barSize={20}
|
||
>
|
||
{series.map((entry, index) => (
|
||
<Cell
|
||
key={`cell-${index}`}
|
||
fill={index <= cursorIndex ? `url(#barGradientActive-${title.replace(/[.\s]/g, '')})` : `url(#barGradient-${title.replace(/[.\s]/g, '')})`}
|
||
/>
|
||
))}
|
||
</Bar>
|
||
|
||
{/* 价格折线 */}
|
||
<Line
|
||
yAxisId="price"
|
||
isAnimationActive={false}
|
||
type="monotone"
|
||
dataKey="value"
|
||
stroke={color}
|
||
strokeWidth={2}
|
||
dot={false}
|
||
/>
|
||
|
||
{/* 移动的亮点 - 使用 ReferenceDot 贴合主数据坐标系 */}
|
||
{activePoint && (
|
||
<ReferenceDot
|
||
xAxisId={0}
|
||
yAxisId="price"
|
||
x={activePoint.time}
|
||
y={activePoint.value}
|
||
r={6}
|
||
isFront
|
||
ifOverflow="hidden"
|
||
shape={(props) => (
|
||
<g>
|
||
<circle
|
||
cx={props.cx}
|
||
cy={props.cy}
|
||
r={8}
|
||
fill={color}
|
||
stroke="#fff"
|
||
strokeWidth={2}
|
||
filter={`url(#glow-${title.replace(/[.\s]/g, '')})`}
|
||
/>
|
||
</g>
|
||
)}
|
||
/>
|
||
)}
|
||
</ComposedChart>
|
||
</ResponsiveContainer>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// 数据流动线条组件
|
||
function DataStreams() {
|
||
const lines = useMemo(() => {
|
||
return [...Array(15)].map((_, i) => ({
|
||
id: i,
|
||
startX: Math.random() * 100,
|
||
delay: Math.random() * 5,
|
||
duration: 3 + Math.random() * 2,
|
||
height: 30 + Math.random() * 70
|
||
}));
|
||
}, []);
|
||
|
||
return (
|
||
<Box position="absolute" inset={0} overflow="hidden" pointerEvents="none">
|
||
{lines.map((line) => (
|
||
<motion.div
|
||
key={line.id}
|
||
style={{
|
||
position: 'absolute',
|
||
width: '1px',
|
||
background: 'linear-gradient(to bottom, transparent, rgba(255, 215, 0, 0.3), transparent)',
|
||
left: `${line.startX}%`,
|
||
height: `${line.height}%`,
|
||
}}
|
||
initial={{ y: '-100%', opacity: 0 }}
|
||
animate={{
|
||
y: '200%',
|
||
opacity: [0, 0.5, 0.5, 0]
|
||
}}
|
||
transition={{
|
||
duration: line.duration,
|
||
delay: line.delay,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
))}
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// 主组件
|
||
export default function MidjourneyHeroSection() {
|
||
const [sse, setSse] = useState({
|
||
sh: { data: [], base: null },
|
||
sz: { data: [], base: null },
|
||
cyb: { data: [], base: null }
|
||
});
|
||
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
try {
|
||
const [shTL, szTL, cybTL, shDaily, szDaily, cybDaily] = await Promise.all([
|
||
// 指数不传 event_time,后端自动返回"最新可用"交易日
|
||
indexService.getKlineData('000001.SH', 'timeline'),
|
||
indexService.getKlineData('399001.SZ', 'timeline'),
|
||
indexService.getKlineData('399006.SZ', 'timeline'), // 创业板指
|
||
indexService.getKlineData('000001.SH', 'daily'),
|
||
indexService.getKlineData('399001.SZ', 'daily'),
|
||
indexService.getKlineData('399006.SZ', 'daily'),
|
||
]);
|
||
|
||
const shPrevClose = getPrevClose(shDaily);
|
||
const szPrevClose = getPrevClose(szDaily);
|
||
const cybPrevClose = getPrevClose(cybDaily);
|
||
|
||
const shSeries = toLineSeries(shTL);
|
||
const szSeries = toLineSeries(szTL);
|
||
const cybSeries = toLineSeries(cybTL);
|
||
|
||
const baseSh = (typeof shPrevClose === 'number' && isFinite(shPrevClose))
|
||
? shPrevClose
|
||
: (shSeries.length ? shSeries[0].value : 1);
|
||
const baseSz = (typeof szPrevClose === 'number' && isFinite(szPrevClose))
|
||
? szPrevClose
|
||
: (szSeries.length ? szSeries[0].value : 1);
|
||
const baseCyb = (typeof cybPrevClose === 'number' && isFinite(cybPrevClose))
|
||
? cybPrevClose
|
||
: (cybSeries.length ? cybSeries[0].value : 1);
|
||
|
||
const shPct = shSeries.map(p => ({
|
||
time: p.time,
|
||
value: ((p.value / baseSh) - 1) * 100,
|
||
volume: p.volume || 0
|
||
}));
|
||
const szPct = szSeries.map(p => ({
|
||
time: p.time,
|
||
value: ((p.value / baseSz) - 1) * 100,
|
||
volume: p.volume || 0
|
||
}));
|
||
const cybPct = cybSeries.map(p => ({
|
||
time: p.time,
|
||
value: ((p.value / baseCyb) - 1) * 100,
|
||
volume: p.volume || 0
|
||
}));
|
||
|
||
setSse({
|
||
sh: { data: shPct, base: baseSh },
|
||
sz: { data: szPct, base: baseSz },
|
||
cyb: { data: cybPct, base: baseCyb }
|
||
});
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
};
|
||
fetchData();
|
||
}, []);
|
||
|
||
const particlesInit = async (engine) => {
|
||
await loadSlim(engine);
|
||
};
|
||
|
||
const particlesOptions = {
|
||
particles: {
|
||
number: {
|
||
value: 80,
|
||
density: {
|
||
enable: true,
|
||
value_area: 800
|
||
}
|
||
},
|
||
color: {
|
||
value: ["#FFD700", "#FF9800", "#FFC107", "#FFEB3B"]
|
||
},
|
||
shape: {
|
||
type: "circle"
|
||
},
|
||
opacity: {
|
||
value: 0.3,
|
||
random: true,
|
||
anim: {
|
||
enable: true,
|
||
speed: 1,
|
||
opacity_min: 0.1,
|
||
sync: false
|
||
}
|
||
},
|
||
size: {
|
||
value: 2,
|
||
random: true,
|
||
anim: {
|
||
enable: true,
|
||
speed: 2,
|
||
size_min: 0.1,
|
||
sync: false
|
||
}
|
||
},
|
||
line_linked: {
|
||
enable: true,
|
||
distance: 150,
|
||
color: "#FFD700",
|
||
opacity: 0.2,
|
||
width: 1
|
||
},
|
||
move: {
|
||
enable: true,
|
||
speed: 0.5,
|
||
direction: "none",
|
||
random: false,
|
||
straight: false,
|
||
out_mode: "out",
|
||
bounce: false,
|
||
}
|
||
},
|
||
interactivity: {
|
||
detect_on: "canvas",
|
||
events: {
|
||
onhover: {
|
||
enable: true,
|
||
mode: "grab"
|
||
},
|
||
onclick: {
|
||
enable: true,
|
||
mode: "push"
|
||
},
|
||
resize: true
|
||
},
|
||
modes: {
|
||
grab: {
|
||
distance: 140,
|
||
line_linked: {
|
||
opacity: 0.5
|
||
}
|
||
},
|
||
push: {
|
||
particles_nb: 4
|
||
}
|
||
}
|
||
},
|
||
retina_detect: true
|
||
};
|
||
|
||
const containerVariants = {
|
||
hidden: { opacity: 0 },
|
||
visible: {
|
||
opacity: 1,
|
||
transition: {
|
||
staggerChildren: 0.1
|
||
}
|
||
}
|
||
};
|
||
|
||
const itemVariants = {
|
||
hidden: { opacity: 0, y: 20 },
|
||
visible: {
|
||
opacity: 1,
|
||
y: 0,
|
||
transition: {
|
||
duration: 0.6,
|
||
ease: "easeOut"
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Box
|
||
position="relative"
|
||
minH="100vh"
|
||
bg="linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #000000 100%)"
|
||
overflow="hidden"
|
||
pointerEvents="none"
|
||
>
|
||
{/* 粒子背景 */}
|
||
<Box position="absolute" inset={0} zIndex={-1} pointerEvents="none">
|
||
<Particles
|
||
id="tsparticles"
|
||
init={particlesInit}
|
||
options={particlesOptions}
|
||
style={{
|
||
position: 'absolute',
|
||
width: '100%',
|
||
height: '100%',
|
||
}}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 数据流动效果 */}
|
||
<DataStreams />
|
||
|
||
{/* 内容容器 */}
|
||
<Container maxW="7xl" position="relative" zIndex={1} pt={20} pb={20}>
|
||
<Grid templateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }} gap={12} alignItems="center" minH="70vh">
|
||
|
||
{/* 左侧文本内容 */}
|
||
<GridItem>
|
||
<motion.div
|
||
variants={containerVariants}
|
||
initial="hidden"
|
||
animate="visible"
|
||
>
|
||
<VStack align="start" spacing={6}>
|
||
{/* 标签 */}
|
||
<motion.div variants={itemVariants}>
|
||
<Badge
|
||
colorScheme="yellow"
|
||
variant="subtle"
|
||
px={4}
|
||
py={2}
|
||
borderRadius="full"
|
||
fontSize="sm"
|
||
fontFamily="monospace"
|
||
display="inline-flex"
|
||
alignItems="center"
|
||
>
|
||
<Box
|
||
as="span"
|
||
w={2}
|
||
h={2}
|
||
bg="yellow.400"
|
||
borderRadius="full"
|
||
mr={2}
|
||
animation="pulse 2s ease-in-out infinite"
|
||
/>
|
||
AI-Assisted Curation
|
||
</Badge>
|
||
</motion.div>
|
||
|
||
{/* 主标题 */}
|
||
<motion.div variants={itemVariants}>
|
||
<Heading
|
||
fontSize={{ base: '4xl', md: '5xl', lg: '6xl' }}
|
||
fontWeight="bold"
|
||
lineHeight="shorter"
|
||
>
|
||
<Text
|
||
as="span"
|
||
bgGradient="linear(to-r, yellow.400, orange.400, yellow.500)"
|
||
bgClip="text"
|
||
>
|
||
ME-Agent
|
||
</Text>
|
||
<br />
|
||
<Text as="span" color="white">
|
||
实时分析系统
|
||
</Text>
|
||
</Heading>
|
||
</motion.div>
|
||
|
||
{/* 副标题 */}
|
||
<motion.div variants={itemVariants}>
|
||
<Heading
|
||
as="h3"
|
||
fontSize="xl"
|
||
color="gray.300"
|
||
fontWeight="semibold"
|
||
>
|
||
基于微调版{' '}
|
||
<Text as="span" color="yellow.400" fontFamily="monospace">
|
||
deepseek-r1
|
||
</Text>{' '}
|
||
进行深度研究
|
||
</Heading>
|
||
</motion.div>
|
||
|
||
{/* 描述文本 */}
|
||
<motion.div variants={itemVariants}>
|
||
<Text
|
||
color="gray.400"
|
||
fontSize="md"
|
||
lineHeight="tall"
|
||
maxW="xl"
|
||
>
|
||
ME (Money Edge) 是一款以大模型为底座、由资深分析师参与校准的信息辅助系统,
|
||
专为金融研究与企业决策等场景设计。系统侧重于多源信息的汇聚、清洗与结构化整理,
|
||
结合自主训练的领域知识图谱,并配合专家人工复核与整合,帮助用户高效获取相关线索与参考资料。
|
||
</Text>
|
||
</motion.div>
|
||
|
||
{/* 特性标签 */}
|
||
<motion.div variants={itemVariants}>
|
||
<HStack spacing={3} flexWrap="wrap">
|
||
{['海量信息整理', '领域知识图谱', '分析师复核', '结构化呈现'].map((tag) => (
|
||
<Tag
|
||
key={tag}
|
||
size="md"
|
||
variant="subtle"
|
||
colorScheme="gray"
|
||
borderRadius="lg"
|
||
px={3}
|
||
py={1}
|
||
bg="gray.800"
|
||
color="gray.300"
|
||
borderWidth="1px"
|
||
borderColor="gray.600"
|
||
>
|
||
{tag}
|
||
</Tag>
|
||
))}
|
||
</HStack>
|
||
</motion.div>
|
||
|
||
{/* 按钮组 */}
|
||
<motion.div variants={itemVariants}>
|
||
<HStack spacing={4} pt={4}>
|
||
<Button
|
||
size="lg"
|
||
variant="outline"
|
||
color="gray.300"
|
||
borderColor="gray.600"
|
||
borderRadius="full"
|
||
px={8}
|
||
_hover={{
|
||
bg: "gray.800",
|
||
borderColor: "gray.500",
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
了解更多
|
||
</Button>
|
||
</HStack>
|
||
</motion.div>
|
||
|
||
{/* 统计数据 */}
|
||
<motion.div variants={itemVariants}>
|
||
<Grid
|
||
templateColumns="repeat(3, 1fr)"
|
||
gap={6}
|
||
pt={8}
|
||
borderTop="1px"
|
||
borderTopColor="gray.800"
|
||
w="full"
|
||
>
|
||
{[
|
||
{ label: '数据源', value: '10K+' },
|
||
{ label: '日处理', value: '1M+' },
|
||
{ label: '准确率', value: '98%' }
|
||
].map((stat) => (
|
||
<Stat key={stat.label}>
|
||
<StatNumber
|
||
fontSize="2xl"
|
||
fontWeight="bold"
|
||
color="yellow.400"
|
||
fontFamily="monospace"
|
||
>
|
||
{stat.value}
|
||
</StatNumber>
|
||
<StatLabel fontSize="sm" color="gray.500">
|
||
{stat.label}
|
||
</StatLabel>
|
||
</Stat>
|
||
))}
|
||
</Grid>
|
||
</motion.div>
|
||
</VStack>
|
||
</motion.div>
|
||
</GridItem>
|
||
|
||
{/* 右侧金融图表可视化 */}
|
||
<GridItem>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.9 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ duration: 1, delay: 0.5 }}
|
||
>
|
||
<Box position="relative" h={{ base: '400px', md: '500px', lg: '600px' }}>
|
||
{/* 图表网格布局 */}
|
||
<Grid
|
||
templateColumns="repeat(2, 1fr)"
|
||
templateRows="repeat(2, 1fr)"
|
||
gap={4}
|
||
h="full"
|
||
p={4}
|
||
bg="rgba(0, 0, 0, 0.3)"
|
||
borderRadius="xl"
|
||
border="1px solid"
|
||
borderColor="rgba(255, 215, 0, 0.2)"
|
||
backdropFilter="blur(10px)"
|
||
>
|
||
{/* 上证指数 */}
|
||
<GridItem colSpan={2}>
|
||
<Box
|
||
h="full"
|
||
bg="rgba(0, 0, 0, 0.4)"
|
||
borderRadius="lg"
|
||
p={2}
|
||
border="1px solid"
|
||
borderColor="rgba(255, 215, 0, 0.1)"
|
||
>
|
||
<CombinedChart
|
||
series={sse.sh?.data || []}
|
||
basePrice={sse.sh?.base}
|
||
title="000001.SH 上证指数"
|
||
color="#FFD700"
|
||
/>
|
||
</Box>
|
||
</GridItem>
|
||
|
||
{/* 深证成指 */}
|
||
<GridItem>
|
||
<Box
|
||
h="full"
|
||
bg="rgba(0, 0, 0, 0.4)"
|
||
borderRadius="lg"
|
||
p={2}
|
||
border="1px solid"
|
||
borderColor="rgba(255, 215, 0, 0.1)"
|
||
>
|
||
<CombinedChart
|
||
series={sse.sz?.data || []}
|
||
basePrice={sse.sz?.base}
|
||
title="399001.SZ 深证成指"
|
||
color="#00E0FF"
|
||
/>
|
||
</Box>
|
||
</GridItem>
|
||
|
||
{/* 创业板指 */}
|
||
<GridItem>
|
||
<Box
|
||
h="full"
|
||
bg="rgba(0, 0, 0, 0.4)"
|
||
borderRadius="lg"
|
||
p={2}
|
||
border="1px solid"
|
||
borderColor="rgba(255, 215, 0, 0.1)"
|
||
>
|
||
<CombinedChart
|
||
series={sse.cyb?.data || []}
|
||
basePrice={sse.cyb?.base}
|
||
title="399006.SZ 创业板指"
|
||
color="#FF69B4"
|
||
/>
|
||
</Box>
|
||
</GridItem>
|
||
</Grid>
|
||
|
||
{/* 装饰性光效 */}
|
||
<Box
|
||
position="absolute"
|
||
top="50%"
|
||
left="50%"
|
||
transform="translate(-50%, -50%)"
|
||
w="150%"
|
||
h="150%"
|
||
pointerEvents="none"
|
||
>
|
||
<Box
|
||
position="absolute"
|
||
top="20%"
|
||
left="20%"
|
||
w="200px"
|
||
h="200px"
|
||
bg="radial-gradient(circle, rgba(255, 215, 0, 0.15), transparent)"
|
||
borderRadius="full"
|
||
filter="blur(40px)"
|
||
animation="pulse 4s ease-in-out infinite"
|
||
/>
|
||
<Box
|
||
position="absolute"
|
||
bottom="20%"
|
||
right="20%"
|
||
w="150px"
|
||
h="150px"
|
||
bg="radial-gradient(circle, rgba(255, 152, 0, 0.15), transparent)"
|
||
borderRadius="full"
|
||
filter="blur(40px)"
|
||
animation="pulse 4s ease-in-out infinite"
|
||
sx={{ animationDelay: '2s' }}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</motion.div>
|
||
</GridItem>
|
||
</Grid>
|
||
</Container>
|
||
|
||
{/* 底部渐变遮罩 */}
|
||
<Box
|
||
position="absolute"
|
||
bottom={0}
|
||
left={0}
|
||
right={0}
|
||
h="128px"
|
||
bgGradient="linear(to-t, black, transparent)"
|
||
zIndex={-1}
|
||
/>
|
||
|
||
{/* 全局样式 */}
|
||
<style>{`
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 0.4; transform: scale(1); }
|
||
50% { opacity: 0.6; transform: scale(1.1); }
|
||
}
|
||
`}</style>
|
||
</Box>
|
||
);
|
||
} |