diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index d3340dae..c628681a 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -1,7 +1,7 @@ // src/views/Community/components/HeroPanel.js // 顶部说明面板组件:产品功能介绍 + 沪深指数折线图 + 热门概念词云图 -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Box, Card, @@ -52,76 +52,164 @@ const fetchPopularConcepts = async () => { }, body: JSON.stringify({ query: '', - size: 50, // 获取前50个概念用于词云 + size: 18, // 只获取前18个概念 page: 1, sort_by: 'change_pct' }) }); const data = await response.json(); - if (data.results) { + logger.debug('HeroPanel', 'fetchPopularConcepts response', { + total: data.total, + resultsCount: data.results?.length + }); + + if (data.results && data.results.length > 0) { return data.results.map(item => ({ name: item.concept, - value: Math.abs(item.price_info?.avg_change_pct || 1), // 使用涨跌幅绝对值作为权重 + value: Math.abs(item.price_info?.avg_change_pct || 1) + 5, // 使用涨跌幅绝对值 + 基础权重 change_pct: item.price_info?.avg_change_pct || 0, })); } return []; } catch (error) { - logger.error('HeroPanel', 'fetchPopularConcepts', error); + logger.error('HeroPanel', 'fetchPopularConcepts error', error); return []; } }; /** - * 迷你折线图组件 + * 判断当前是否在交易时间内 + */ +const isInTradingTime = () => { + const now = new Date(); + const hours = now.getHours(); + const minutes = now.getMinutes(); + const timeInMinutes = hours * 60 + minutes; + + // 9:30 - 15:00 (570分钟 - 900分钟) + return timeInMinutes >= 570 && timeInMinutes <= 900; +}; + +/** + * 迷你K线图组件(支持实时更新) */ const MiniIndexChart = ({ indexCode, indexName }) => { const [chartData, setChartData] = useState(null); const [loading, setLoading] = useState(true); const [latestData, setLatestData] = useState(null); + const [currentDate, setCurrentDate] = useState(''); const chartBg = useColorModeValue('transparent', 'transparent'); - const lineColor = useColorModeValue('#FFD700', '#FFD700'); // 金色 - const areaColor = useColorModeValue('rgba(255, 215, 0, 0.15)', 'rgba(255, 215, 0, 0.1)'); + const upColor = '#00da3c'; + const downColor = '#ec0000'; - useEffect(() => { - let isMounted = true; + // 加载日线数据 + const loadDailyData = useCallback(async () => { + const data = await fetchIndexKline(indexCode); - const loadData = async () => { - setLoading(true); - const data = await fetchIndexKline(indexCode); + if (data && data.data && data.data.length > 0) { + // 取最近一个交易日的数据 + const latest = data.data[data.data.length - 1]; + const prevClose = latest.prev_close || latest.close; - if (isMounted && data && data.data && data.data.length > 0) { - // 取最近一个交易日的数据 + setLatestData({ + close: latest.close, + change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', + isPositive: latest.close >= prevClose + }); + + setCurrentDate(latest.time); + + // 准备K线图数据(最近60个交易日) + const recentData = data.data.slice(-60); + setChartData({ + dates: recentData.map(item => item.time), + klineData: recentData.map(item => [ + item.open, + item.close, + item.low, + item.high + ]) + }); + } + + setLoading(false); + }, [indexCode]); + + // 加载分钟线数据(仅在交易时间) + const loadMinuteData = useCallback(async () => { + try { + const response = await fetch(`/api/index/${indexCode}/kline?type=minute`); + if (!response.ok) return; + + const data = await response.json(); + + if (data && data.data && data.data.length > 0) { + // 取最新分钟数据 const latest = data.data[data.data.length - 1]; - const prevClose = latest.prev_close || latest.close; + // 分钟线没有 prev_close,使用第一条数据的 open 作为开盘价 + const dayOpen = data.data[0].open; setLatestData({ close: latest.close, - change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', - isPositive: latest.close >= prevClose + change: dayOpen ? (((latest.close - dayOpen) / dayOpen) * 100).toFixed(2) : '0.00', + isPositive: latest.close >= dayOpen }); - // 准备图表数据(最近60个交易日) - const recentData = data.data.slice(-60); - setChartData({ - dates: recentData.map(item => item.time), - values: recentData.map(item => item.close) + logger.debug('HeroPanel', 'Minute data updated', { + indexCode, + close: latest.close, + time: latest.time, + change: (((latest.close - dayOpen) / dayOpen) * 100).toFixed(2) }); } + } catch (error) { + logger.error('HeroPanel', 'loadMinuteData error', error); + } + }, [indexCode]); + + // 初始加载和定时更新 + useEffect(() => { + let isMounted = true; + let intervalId = null; + + const init = async () => { + setLoading(true); + await loadDailyData(); if (isMounted) { + // 如果在交易时间,立即加载一次分钟数据 + if (isInTradingTime()) { + await loadMinuteData(); + } setLoading(false); } }; - loadData(); + init(); + + // 设置定时器:交易时间内每分钟更新 + if (isInTradingTime()) { + intervalId = setInterval(() => { + if (isInTradingTime()) { + loadMinuteData(); + } else { + // 如果超出交易时间,清除定时器 + if (intervalId) { + clearInterval(intervalId); + } + } + }, 60000); // 每60秒更新一次 + } return () => { isMounted = false; + if (intervalId) { + clearInterval(intervalId); + } }; - }, [indexCode]); + }, [indexCode, loadDailyData, loadMinuteData]); const chartOption = useMemo(() => { if (!chartData) return {}; @@ -129,8 +217,8 @@ const MiniIndexChart = ({ indexCode, indexName }) => { return { backgroundColor: chartBg, grid: { - left: 5, - right: 5, + left: 10, + right: 10, top: 5, bottom: 20, containLabel: false @@ -146,36 +234,18 @@ const MiniIndexChart = ({ indexCode, indexName }) => { scale: true }, series: [{ - type: 'line', - data: chartData.values, - smooth: true, - symbol: 'none', - lineStyle: { - color: lineColor, - width: 2, - shadowColor: lineColor, - shadowBlur: 8, - shadowOffsetY: 2 + type: 'candlestick', + data: chartData.klineData, + itemStyle: { + color: upColor, + color0: downColor, + borderColor: upColor, + borderColor0: downColor }, - areaStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [{ - offset: 0, - color: areaColor - }, { - offset: 1, - color: 'rgba(255, 215, 0, 0)' - }] - } - } + barWidth: '60%' }] }; - }, [chartData, chartBg, lineColor, areaColor]); + }, [chartData, chartBg, upColor, downColor]); if (loading) { return ( @@ -186,15 +256,18 @@ const MiniIndexChart = ({ indexCode, indexName }) => { } return ( - + {indexName} {latestData?.close.toFixed(2)} + + {currentDate} + - + { > {latestData?.isPositive ? '↑' : '↓'} {latestData?.isPositive ? '+' : ''}{latestData?.change}% - + {isInTradingTime() && ( + + ● 实时更新 + + )} +