update ui

This commit is contained in:
2025-11-13 16:34:34 +08:00
parent 5ddf8d3c09
commit 9d6c0ac55c

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/HeroPanel.js // src/views/Community/components/HeroPanel.js
// 顶部说明面板组件:产品功能介绍 + 沪深指数折线图 + 热门概念词云图 // 顶部说明面板组件:产品功能介绍 + 沪深指数折线图 + 热门概念词云图
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { import {
Box, Box,
Card, Card,
@@ -52,76 +52,164 @@ const fetchPopularConcepts = async () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
query: '', query: '',
size: 50, // 获取前50个概念用于词云 size: 18, // 获取前18个概念
page: 1, page: 1,
sort_by: 'change_pct' sort_by: 'change_pct'
}) })
}); });
const data = await response.json(); 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 => ({ return data.results.map(item => ({
name: item.concept, 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, change_pct: item.price_info?.avg_change_pct || 0,
})); }));
} }
return []; return [];
} catch (error) { } catch (error) {
logger.error('HeroPanel', 'fetchPopularConcepts', error); logger.error('HeroPanel', 'fetchPopularConcepts error', error);
return []; 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 MiniIndexChart = ({ indexCode, indexName }) => {
const [chartData, setChartData] = useState(null); const [chartData, setChartData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [latestData, setLatestData] = useState(null); const [latestData, setLatestData] = useState(null);
const [currentDate, setCurrentDate] = useState('');
const chartBg = useColorModeValue('transparent', 'transparent'); const chartBg = useColorModeValue('transparent', 'transparent');
const lineColor = useColorModeValue('#FFD700', '#FFD700'); // 金色 const upColor = '#00da3c';
const areaColor = useColorModeValue('rgba(255, 215, 0, 0.15)', 'rgba(255, 215, 0, 0.1)'); const downColor = '#ec0000';
useEffect(() => { // 加载日线数据
let isMounted = true; const loadDailyData = useCallback(async () => {
const data = await fetchIndexKline(indexCode);
const loadData = async () => { if (data && data.data && data.data.length > 0) {
setLoading(true); // 取最近一个交易日的数据
const data = await fetchIndexKline(indexCode); 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 latest = data.data[data.data.length - 1];
const prevClose = latest.prev_close || latest.close; // 分钟线没有 prev_close,使用第一条数据的 open 作为开盘价
const dayOpen = data.data[0].open;
setLatestData({ setLatestData({
close: latest.close, close: latest.close,
change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', change: dayOpen ? (((latest.close - dayOpen) / dayOpen) * 100).toFixed(2) : '0.00',
isPositive: latest.close >= prevClose isPositive: latest.close >= dayOpen
}); });
// 准备图表数据最近60个交易日 logger.debug('HeroPanel', 'Minute data updated', {
const recentData = data.data.slice(-60); indexCode,
setChartData({ close: latest.close,
dates: recentData.map(item => item.time), time: latest.time,
values: recentData.map(item => item.close) 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 (isMounted) {
// 如果在交易时间,立即加载一次分钟数据
if (isInTradingTime()) {
await loadMinuteData();
}
setLoading(false); setLoading(false);
} }
}; };
loadData(); init();
// 设置定时器:交易时间内每分钟更新
if (isInTradingTime()) {
intervalId = setInterval(() => {
if (isInTradingTime()) {
loadMinuteData();
} else {
// 如果超出交易时间,清除定时器
if (intervalId) {
clearInterval(intervalId);
}
}
}, 60000); // 每60秒更新一次
}
return () => { return () => {
isMounted = false; isMounted = false;
if (intervalId) {
clearInterval(intervalId);
}
}; };
}, [indexCode]); }, [indexCode, loadDailyData, loadMinuteData]);
const chartOption = useMemo(() => { const chartOption = useMemo(() => {
if (!chartData) return {}; if (!chartData) return {};
@@ -129,8 +217,8 @@ const MiniIndexChart = ({ indexCode, indexName }) => {
return { return {
backgroundColor: chartBg, backgroundColor: chartBg,
grid: { grid: {
left: 5, left: 10,
right: 5, right: 10,
top: 5, top: 5,
bottom: 20, bottom: 20,
containLabel: false containLabel: false
@@ -146,36 +234,18 @@ const MiniIndexChart = ({ indexCode, indexName }) => {
scale: true scale: true
}, },
series: [{ series: [{
type: 'line', type: 'candlestick',
data: chartData.values, data: chartData.klineData,
smooth: true, itemStyle: {
symbol: 'none', color: upColor,
lineStyle: { color0: downColor,
color: lineColor, borderColor: upColor,
width: 2, borderColor0: downColor
shadowColor: lineColor,
shadowBlur: 8,
shadowOffsetY: 2
}, },
areaStyle: { barWidth: '60%'
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: areaColor
}, {
offset: 1,
color: 'rgba(255, 215, 0, 0)'
}]
}
}
}] }]
}; };
}, [chartData, chartBg, lineColor, areaColor]); }, [chartData, chartBg, upColor, downColor]);
if (loading) { if (loading) {
return ( return (
@@ -186,15 +256,18 @@ const MiniIndexChart = ({ indexCode, indexName }) => {
} }
return ( return (
<VStack spacing={2} align="stretch" h="120px"> <VStack spacing={2} align="stretch" h="140px">
<HStack justify="space-between"> <HStack justify="space-between">
<VStack align="start" spacing={0}> <VStack align="start" spacing={0}>
<Text fontSize="xs" color="whiteAlpha.700">{indexName}</Text> <Text fontSize="xs" color="whiteAlpha.700">{indexName}</Text>
<Text fontSize="lg" fontWeight="bold" color="white"> <Text fontSize="lg" fontWeight="bold" color="white">
{latestData?.close.toFixed(2)} {latestData?.close.toFixed(2)}
</Text> </Text>
<Text fontSize="xs" color="whiteAlpha.500">
{currentDate}
</Text>
</VStack> </VStack>
<HStack spacing={1}> <VStack align="end" spacing={0}>
<Text <Text
fontSize="sm" fontSize="sm"
fontWeight="bold" fontWeight="bold"
@@ -202,12 +275,17 @@ const MiniIndexChart = ({ indexCode, indexName }) => {
> >
{latestData?.isPositive ? '↑' : '↓'} {latestData?.isPositive ? '+' : ''}{latestData?.change}% {latestData?.isPositive ? '↑' : '↓'} {latestData?.isPositive ? '+' : ''}{latestData?.change}%
</Text> </Text>
</HStack> {isInTradingTime() && (
<Text fontSize="xs" color="green.400">
实时更新
</Text>
)}
</VStack>
</HStack> </HStack>
<Box flex="1"> <Box flex="1">
<ReactECharts <ReactECharts
option={chartOption} option={chartOption}
style={{ height: '80px', width: '100%' }} style={{ height: '90px', width: '100%' }}
opts={{ renderer: 'canvas' }} opts={{ renderer: 'canvas' }}
/> />
</Box> </Box>