597 lines
18 KiB
JavaScript
597 lines
18 KiB
JavaScript
// src/views/Community/components/HeroPanel.js
|
||
// 顶部说明面板组件:事件中心 + 沪深指数K线图 + 热门概念3D动画
|
||
|
||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||
import {
|
||
Box,
|
||
Card,
|
||
CardBody,
|
||
Flex,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Heading,
|
||
useColorModeValue,
|
||
Icon,
|
||
Spinner,
|
||
Center,
|
||
} from '@chakra-ui/react';
|
||
import { AlertCircle, Clock, TrendingUp, Info } from 'lucide-react';
|
||
import ReactECharts from 'echarts-for-react';
|
||
import { logger } from '../../../utils/logger';
|
||
|
||
// 定义动画
|
||
const animations = `
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.6; transform: scale(1.1); }
|
||
}
|
||
@keyframes shimmer {
|
||
0% { background-position: -200% 0; }
|
||
100% { background-position: 200% 0; }
|
||
}
|
||
@keyframes floatSlow {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-10px); }
|
||
}
|
||
`;
|
||
|
||
// 注入样式
|
||
if (typeof document !== 'undefined') {
|
||
const styleId = 'hero-panel-animations';
|
||
if (!document.getElementById(styleId)) {
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.id = styleId;
|
||
styleSheet.innerText = animations;
|
||
document.head.appendChild(styleSheet);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取指数行情数据(日线数据)
|
||
*/
|
||
const fetchIndexKline = async (indexCode) => {
|
||
try {
|
||
const response = await fetch(`/api/index/${indexCode}/kline?type=daily`);
|
||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||
const data = await response.json();
|
||
return data;
|
||
} catch (error) {
|
||
logger.error('HeroPanel', 'fetchIndexKline error', { indexCode, error: error.message });
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取热门概念数据
|
||
*/
|
||
const fetchPopularConcepts = async () => {
|
||
try {
|
||
const response = await fetch('/concept-api/search', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ query: '', size: 60, page: 1, sort_by: 'change_pct' })
|
||
});
|
||
const data = await response.json();
|
||
if (data.results?.length > 0) {
|
||
return data.results.map(item => ({
|
||
name: item.concept,
|
||
change_pct: item.price_info?.avg_change_pct || 0,
|
||
}));
|
||
}
|
||
return [];
|
||
} catch (error) {
|
||
logger.error('HeroPanel', 'fetchPopularConcepts error', error);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 判断当前是否在交易时间内
|
||
*/
|
||
const isInTradingTime = () => {
|
||
const now = new Date();
|
||
const timeInMinutes = now.getHours() * 60 + now.getMinutes();
|
||
return timeInMinutes >= 570 && timeInMinutes <= 900;
|
||
};
|
||
|
||
/**
|
||
* 紧凑型K线指数卡片
|
||
*/
|
||
const CompactIndexCard = ({ indexCode, indexName }) => {
|
||
const [chartData, setChartData] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [latestData, setLatestData] = useState(null);
|
||
|
||
const upColor = '#ec0000';
|
||
const downColor = '#00da3c';
|
||
|
||
const loadData = useCallback(async () => {
|
||
const data = await fetchIndexKline(indexCode);
|
||
if (data?.data?.length > 0) {
|
||
const latest = data.data[data.data.length - 1];
|
||
const prevClose = latest.prev_close || latest.close;
|
||
setLatestData({
|
||
close: latest.close,
|
||
change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00',
|
||
isPositive: latest.close >= prevClose
|
||
});
|
||
const recentData = data.data.slice(-40);
|
||
setChartData({
|
||
dates: recentData.map(item => item.time),
|
||
klineData: recentData.map(item => [item.open, item.close, item.low, item.high]),
|
||
rawData: recentData
|
||
});
|
||
}
|
||
setLoading(false);
|
||
}, [indexCode]);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, [loadData]);
|
||
|
||
const chartOption = useMemo(() => {
|
||
if (!chartData) return {};
|
||
return {
|
||
backgroundColor: 'transparent',
|
||
grid: { left: 5, right: 5, top: 5, bottom: 5, containLabel: false },
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'cross', lineStyle: { color: 'rgba(255, 215, 0, 0.5)', width: 1, type: 'dashed' } },
|
||
backgroundColor: 'rgba(20, 20, 20, 0.95)',
|
||
borderColor: '#FFD700',
|
||
borderWidth: 1,
|
||
textStyle: { color: '#fff', fontSize: 10, fontFamily: 'monospace' },
|
||
padding: [6, 10],
|
||
formatter: (params) => {
|
||
const idx = params[0].dataIndex;
|
||
const raw = chartData.rawData[idx];
|
||
if (!raw) return '';
|
||
const prevClose = raw.prev_close || raw.open;
|
||
const changePct = prevClose ? (((raw.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00';
|
||
const isUp = raw.close >= prevClose;
|
||
const color = isUp ? '#ec0000' : '#00da3c';
|
||
return `<div style="font-size:11px"><b>${raw.time}</b><br/>收盘: ${raw.close.toFixed(2)}<br/><span style="color:${color}">${isUp ? '+' : ''}${changePct}%</span></div>`;
|
||
}
|
||
},
|
||
xAxis: { type: 'category', data: chartData.dates, show: false },
|
||
yAxis: { type: 'value', show: false, scale: true },
|
||
series: [{
|
||
type: 'candlestick',
|
||
data: chartData.klineData,
|
||
itemStyle: { color: upColor, color0: downColor, borderColor: upColor, borderColor0: downColor },
|
||
barWidth: '70%'
|
||
}]
|
||
};
|
||
}, [chartData, upColor, downColor]);
|
||
|
||
if (loading) return <Center h="80px"><Spinner size="sm" color="gold" /></Center>;
|
||
|
||
return (
|
||
<Flex align="center" gap={4} h="80px">
|
||
{/* 左侧数据 */}
|
||
<VStack align="start" spacing={0} minW="100px">
|
||
<Text fontSize="xs" color="whiteAlpha.600" fontWeight="medium">{indexName}</Text>
|
||
<Text fontSize="xl" fontWeight="bold" color="white">
|
||
{latestData?.close.toFixed(2)}
|
||
</Text>
|
||
<Text
|
||
fontSize="sm"
|
||
fontWeight="bold"
|
||
color={latestData?.isPositive ? '#ec0000' : '#00da3c'}
|
||
>
|
||
{latestData?.isPositive ? '▲' : '▼'} {latestData?.isPositive ? '+' : ''}{latestData?.change}%
|
||
</Text>
|
||
</VStack>
|
||
{/* 右侧K线图 */}
|
||
<Box flex="1" h="70px">
|
||
<ReactECharts option={chartOption} style={{ height: '100%', width: '100%' }} opts={{ renderer: 'canvas' }} />
|
||
</Box>
|
||
</Flex>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 概念漂浮动画 - 稀疏美观版
|
||
*/
|
||
const ConceptFloat = () => {
|
||
const [concepts, setConcepts] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [hoveredIdx, setHoveredIdx] = useState(null);
|
||
const containerRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
const load = async () => {
|
||
const data = await fetchPopularConcepts();
|
||
// 只取前12个,更稀疏
|
||
setConcepts(data.slice(0, 12));
|
||
setLoading(false);
|
||
};
|
||
load();
|
||
}, []);
|
||
|
||
const getColor = (pct) => {
|
||
if (pct > 5) return '#ff1744';
|
||
if (pct > 2) return '#ff5252';
|
||
if (pct > 0) return '#ff8a80';
|
||
if (pct === 0) return '#FFD700';
|
||
if (pct > -2) return '#69f0ae';
|
||
if (pct > -5) return '#00e676';
|
||
return '#00c853';
|
||
};
|
||
|
||
const handleClick = (name) => {
|
||
window.open(`https://valuefrontier.cn/htmls/${name}.html`, '_blank');
|
||
};
|
||
|
||
if (loading) return <Center h="100%"><Spinner size="sm" color="gold" /></Center>;
|
||
|
||
// 预设12个位置,均匀分布且不重叠
|
||
const positions = [
|
||
{ x: 15, y: 20 }, { x: 50, y: 15 }, { x: 85, y: 25 },
|
||
{ x: 25, y: 50 }, { x: 60, y: 45 }, { x: 80, y: 55 },
|
||
{ x: 10, y: 75 }, { x: 40, y: 80 }, { x: 70, y: 75 },
|
||
{ x: 20, y: 35 }, { x: 55, y: 65 }, { x: 90, y: 40 },
|
||
];
|
||
|
||
return (
|
||
<Box
|
||
ref={containerRef}
|
||
position="relative"
|
||
h="100%"
|
||
w="100%"
|
||
overflow="hidden"
|
||
>
|
||
{/* 概念标签 */}
|
||
{concepts.map((concept, idx) => {
|
||
const pos = positions[idx] || { x: 50, y: 50 };
|
||
const color = getColor(concept.change_pct);
|
||
const isActive = hoveredIdx === idx;
|
||
const size = 11 + (idx % 3) * 2; // 11-15px
|
||
|
||
return (
|
||
<Box
|
||
key={idx}
|
||
position="absolute"
|
||
left={`${pos.x}%`}
|
||
top={`${pos.y}%`}
|
||
transform={`translate(-50%, -50%) scale(${isActive ? 1.3 : 1})`}
|
||
opacity={isActive ? 1 : 0.85}
|
||
zIndex={isActive ? 100 : 10}
|
||
cursor="pointer"
|
||
transition="all 0.3s ease"
|
||
onMouseEnter={() => setHoveredIdx(idx)}
|
||
onMouseLeave={() => setHoveredIdx(null)}
|
||
onClick={() => handleClick(concept.name)}
|
||
animation={`floatSlow ${5 + (idx % 3)}s ease-in-out infinite ${idx * 0.3}s`}
|
||
>
|
||
<Text
|
||
fontSize={`${size}px`}
|
||
fontWeight={isActive ? 'bold' : 'medium'}
|
||
color={color}
|
||
textShadow={isActive
|
||
? `0 0 15px ${color}, 0 0 30px ${color}50`
|
||
: `0 0 8px ${color}40`
|
||
}
|
||
whiteSpace="nowrap"
|
||
>
|
||
{concept.name}
|
||
</Text>
|
||
|
||
{/* 悬停提示 */}
|
||
{isActive && (
|
||
<Box
|
||
position="absolute"
|
||
top="-28px"
|
||
left="50%"
|
||
transform="translateX(-50%)"
|
||
bg="rgba(0,0,0,0.9)"
|
||
border="1px solid"
|
||
borderColor={color}
|
||
borderRadius="md"
|
||
px={2}
|
||
py={1}
|
||
whiteSpace="nowrap"
|
||
>
|
||
<Text fontSize="xs" color={color} fontWeight="bold">
|
||
{concept.change_pct > 0 ? '+' : ''}{concept.change_pct.toFixed(2)}%
|
||
</Text>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 极简提示标签 - 悬停显示详情
|
||
*/
|
||
const InfoTooltip = () => {
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
|
||
return (
|
||
<Box position="relative" display="inline-block">
|
||
{/* 触发器:小标签 */}
|
||
<HStack
|
||
spacing={1.5}
|
||
px={2.5}
|
||
py={1}
|
||
bg="rgba(255,215,0,0.08)"
|
||
border="1px solid rgba(255,215,0,0.2)"
|
||
borderRadius="full"
|
||
cursor="pointer"
|
||
transition="all 0.2s"
|
||
_hover={{
|
||
bg: 'rgba(255,215,0,0.15)',
|
||
borderColor: 'rgba(255,215,0,0.4)',
|
||
}}
|
||
onMouseEnter={() => setIsOpen(true)}
|
||
onMouseLeave={() => setIsOpen(false)}
|
||
onClick={() => setIsOpen(!isOpen)}
|
||
>
|
||
<Icon as={Info} color="gold" boxSize={3} />
|
||
<Text fontSize="xs" color="gold" fontWeight="medium">
|
||
使用说明
|
||
</Text>
|
||
</HStack>
|
||
|
||
{/* 悬浮提示框 */}
|
||
<Box
|
||
position="absolute"
|
||
top="calc(100% + 8px)"
|
||
left="0"
|
||
zIndex={1000}
|
||
minW="320px"
|
||
p={4}
|
||
bg="rgba(10,10,20,0.98)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(255,215,0,0.3)"
|
||
boxShadow="0 10px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,215,0,0.1)"
|
||
opacity={isOpen ? 1 : 0}
|
||
visibility={isOpen ? 'visible' : 'hidden'}
|
||
transform={isOpen ? 'translateY(0)' : 'translateY(-10px)'}
|
||
transition="all 0.2s ease"
|
||
onMouseEnter={() => setIsOpen(true)}
|
||
onMouseLeave={() => setIsOpen(false)}
|
||
>
|
||
{/* 小箭头 */}
|
||
<Box
|
||
position="absolute"
|
||
top="-6px"
|
||
left="20px"
|
||
w="12px"
|
||
h="12px"
|
||
bg="rgba(10,10,20,0.98)"
|
||
borderTop="1px solid rgba(255,215,0,0.3)"
|
||
borderLeft="1px solid rgba(255,215,0,0.3)"
|
||
transform="rotate(45deg)"
|
||
/>
|
||
|
||
<VStack align="start" spacing={3}>
|
||
{/* SABC说明 */}
|
||
<HStack spacing={2} align="start">
|
||
<Text color="gold" fontWeight="bold" fontSize="xs" minW="50px">SABC</Text>
|
||
<Text fontSize="xs" color="whiteAlpha.800">
|
||
基于事件影响力评级,非收益预测
|
||
</Text>
|
||
</HStack>
|
||
|
||
{/* 涨跌幅说明 */}
|
||
<HStack spacing={2} align="start">
|
||
<Text color="cyan.400" fontWeight="bold" fontSize="xs" minW="50px">涨跌幅</Text>
|
||
<Text fontSize="xs" color="whiteAlpha.800">
|
||
新闻发布时股价 → 当前价格的变化
|
||
</Text>
|
||
</HStack>
|
||
|
||
{/* 延迟提醒 */}
|
||
<HStack spacing={2} align="start">
|
||
<HStack spacing={1} minW="50px">
|
||
<Icon as={Clock} color="orange.400" boxSize={3} />
|
||
</HStack>
|
||
<Text fontSize="xs" color="whiteAlpha.800">
|
||
数据延迟<Text as="span" color="orange.400" fontWeight="bold"> 2-3分钟</Text>
|
||
,<Text as="span" color="#ec0000" fontWeight="bold">切勿追高</Text>
|
||
</Text>
|
||
</HStack>
|
||
|
||
{/* 分隔线 */}
|
||
<Box w="100%" h="1px" bg="rgba(255,255,255,0.1)" />
|
||
|
||
{/* 盘前新闻 */}
|
||
<HStack spacing={2} align="start">
|
||
<Icon as={AlertCircle} color="blue.300" boxSize={3} mt={0.5} />
|
||
<Text fontSize="xs" color="whiteAlpha.800">
|
||
关注<Text as="span" color="blue.300" fontWeight="bold">盘前新闻</Text>
|
||
(收盘后至次日开盘前的消息)
|
||
</Text>
|
||
</HStack>
|
||
|
||
{/* 利好利空 */}
|
||
<HStack spacing={2} align="start">
|
||
<Icon as={TrendingUp} color="whiteAlpha.500" boxSize={3} mt={0.5} />
|
||
<Text fontSize="xs" color="whiteAlpha.800">
|
||
事件含
|
||
<Text as="span" color="#ec0000" fontWeight="bold"> 利好</Text>/
|
||
<Text as="span" color="#00da3c" fontWeight="bold">利空</Text>
|
||
,需自行判断
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 顶部说明面板主组件
|
||
*/
|
||
const HeroPanel = () => {
|
||
const gradientBg = 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #1a1a2e 75%, #0a0a0a 100%)';
|
||
const borderColor = useColorModeValue('rgba(255, 215, 0, 0.3)', 'rgba(255, 215, 0, 0.25)');
|
||
|
||
return (
|
||
<Card
|
||
bg={gradientBg}
|
||
borderColor={borderColor}
|
||
borderWidth="1px"
|
||
boxShadow="0 20px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,215,0,0.1) inset"
|
||
mb={6}
|
||
overflow="hidden"
|
||
position="relative"
|
||
>
|
||
{/* 装饰性光晕 */}
|
||
<Box
|
||
position="absolute"
|
||
top="-50%"
|
||
right="-30%"
|
||
width="600px"
|
||
height="600px"
|
||
borderRadius="full"
|
||
bg="radial-gradient(circle, rgba(255,215,0,0.08) 0%, transparent 70%)"
|
||
pointerEvents="none"
|
||
filter="blur(40px)"
|
||
/>
|
||
<Box
|
||
position="absolute"
|
||
bottom="-40%"
|
||
left="-20%"
|
||
width="400px"
|
||
height="400px"
|
||
borderRadius="full"
|
||
bg="radial-gradient(circle, rgba(100,150,255,0.05) 0%, transparent 70%)"
|
||
pointerEvents="none"
|
||
filter="blur(50px)"
|
||
/>
|
||
|
||
<CardBody p={{ base: 4, md: 5 }}>
|
||
{/* 标题行:标题 + 使用说明 + 交易状态 */}
|
||
<Flex align="center" justify="space-between" mb={4} wrap="wrap" gap={2}>
|
||
<HStack spacing={3}>
|
||
<Heading size="lg">
|
||
<Text
|
||
bgGradient="linear(to-r, #FFD700, #FFA500, #FFD700)"
|
||
bgClip="text"
|
||
backgroundSize="200% 100%"
|
||
animation="shimmer 3s linear infinite"
|
||
fontWeight="extrabold"
|
||
>
|
||
事件中心
|
||
</Text>
|
||
</Heading>
|
||
{/* 使用说明 - 悬浮提示 */}
|
||
<InfoTooltip />
|
||
</HStack>
|
||
|
||
{/* 右侧:交易状态 */}
|
||
{isInTradingTime() && (
|
||
<HStack
|
||
spacing={1.5}
|
||
px={2.5}
|
||
py={1}
|
||
borderRadius="full"
|
||
bg="rgba(0,218,60,0.1)"
|
||
border="1px solid rgba(0,218,60,0.3)"
|
||
>
|
||
<Box
|
||
w="6px"
|
||
h="6px"
|
||
borderRadius="full"
|
||
bg="#00da3c"
|
||
animation="pulse 1.5s infinite"
|
||
boxShadow="0 0 8px #00da3c"
|
||
/>
|
||
<Text fontSize="xs" color="#00da3c" fontWeight="bold">
|
||
交易中
|
||
</Text>
|
||
</HStack>
|
||
)}
|
||
</Flex>
|
||
|
||
{/* 内容区:指数 + 概念 */}
|
||
<Flex
|
||
direction={{ base: 'column', lg: 'row' }}
|
||
gap={4}
|
||
>
|
||
{/* 左侧:双指数横向排列 */}
|
||
<Flex
|
||
direction={{ base: 'column', md: 'row' }}
|
||
gap={4}
|
||
flex={{ lg: '0 0 58%' }}
|
||
>
|
||
{/* 上证指数 */}
|
||
<Box
|
||
flex="1"
|
||
p={3}
|
||
bg="rgba(255,255,255,0.03)"
|
||
borderRadius="xl"
|
||
border="1px solid rgba(255,255,255,0.08)"
|
||
transition="all 0.3s"
|
||
_hover={{
|
||
bg: 'rgba(255,255,255,0.05)',
|
||
borderColor: 'rgba(236,0,0,0.3)',
|
||
boxShadow: '0 4px 20px rgba(236,0,0,0.1)',
|
||
}}
|
||
>
|
||
<CompactIndexCard indexCode="000001" indexName="上证指数" />
|
||
</Box>
|
||
|
||
{/* 深证成指 */}
|
||
<Box
|
||
flex="1"
|
||
p={3}
|
||
bg="rgba(255,255,255,0.03)"
|
||
borderRadius="xl"
|
||
border="1px solid rgba(255,255,255,0.08)"
|
||
transition="all 0.3s"
|
||
_hover={{
|
||
bg: 'rgba(255,255,255,0.05)',
|
||
borderColor: 'rgba(0,218,60,0.3)',
|
||
boxShadow: '0 4px 20px rgba(0,218,60,0.1)',
|
||
}}
|
||
>
|
||
<CompactIndexCard indexCode="399001" indexName="深证成指" />
|
||
</Box>
|
||
</Flex>
|
||
|
||
{/* 右侧:热门概念 */}
|
||
<Box
|
||
flex={{ lg: '1' }}
|
||
minH={{ base: '150px', lg: '100px' }}
|
||
bg="rgba(0,0,0,0.2)"
|
||
borderRadius="xl"
|
||
border="1px solid rgba(255,215,0,0.1)"
|
||
overflow="hidden"
|
||
position="relative"
|
||
>
|
||
{/* 标题 */}
|
||
<HStack
|
||
position="absolute"
|
||
top={2}
|
||
left={3}
|
||
zIndex={10}
|
||
spacing={2}
|
||
>
|
||
<Icon as={TrendingUp} color="gold" boxSize={3.5} />
|
||
<Text fontSize="xs" fontWeight="bold" color="gold">
|
||
热门概念
|
||
</Text>
|
||
<Text fontSize="xs" color="whiteAlpha.400">
|
||
点击查看
|
||
</Text>
|
||
</HStack>
|
||
|
||
<Box pt={7} h="100%">
|
||
<ConceptFloat />
|
||
</Box>
|
||
</Box>
|
||
</Flex>
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default HeroPanel;
|