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 (
{title}
{/* 发光效果 */}
{/* 左Y轴 - 价格 */}
`${v.toFixed(2)}%`}
orientation="left"
/>
{/* 右Y轴 - 成交量(隐藏) */}
`时间: ${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];
}}
/>
{/* 零轴参考线 */}
{/* 成交量柱状图 */}
{series.map((entry, index) => (
|
))}
{/* 价格折线 */}
{/* 移动的亮点 - 使用 ReferenceDot 贴合主数据坐标系 */}
{activePoint && (
(
)}
/>
)}
);
};
// 数据流动线条组件
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 (
{lines.map((line) => (
))}
);
}
// 主组件
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 (
{/* 粒子背景 */}
{/* 数据流动效果 */}
{/* 内容容器 */}
{/* 左侧文本内容 */}
{/* 标签 */}
AI-Assisted Curation
{/* 主标题 */}
ME-Agent
实时分析系统
{/* 副标题 */}
基于微调版{' '}
deepseek-r1
{' '}
进行深度研究
{/* 描述文本 */}
ME (Money Edge) 是一款以大模型为底座、由资深分析师参与校准的信息辅助系统,
专为金融研究与企业决策等场景设计。系统侧重于多源信息的汇聚、清洗与结构化整理,
结合自主训练的领域知识图谱,并配合专家人工复核与整合,帮助用户高效获取相关线索与参考资料。
{/* 特性标签 */}
{['海量信息整理', '领域知识图谱', '分析师复核', '结构化呈现'].map((tag) => (
{tag}
))}
{/* 按钮组 */}
{/* 统计数据 */}
{[
{ label: '数据源', value: '10K+' },
{ label: '日处理', value: '1M+' },
{ label: '准确率', value: '98%' }
].map((stat) => (
{stat.value}
{stat.label}
))}
{/* 右侧金融图表可视化 */}
{/* 图表网格布局 */}
{/* 上证指数 */}
{/* 深证成指 */}
{/* 创业板指 */}
{/* 装饰性光效 */}
{/* 底部渐变遮罩 */}
{/* 全局样式 */}
);
}