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} ))}
{/* 右侧金融图表可视化 */} {/* 图表网格布局 */} {/* 上证指数 */} {/* 深证成指 */} {/* 创业板指 */} {/* 装饰性光效 */}
{/* 底部渐变遮罩 */} {/* 全局样式 */} ); }