update ui
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user