refactor(MarketDashboard): 重构投资仪表盘布局
- 上证指数、深证成指使用 K 线图,与事件中心一致 - 移除成交额模块 - 创业板指与涨跌分布上下组合 - 涨跌分布改用进度条样式 - 布局从 6 列改为 4 列
This commit is contained in:
@@ -4,10 +4,9 @@ import { Box, Text, HStack, Icon } from '@chakra-ui/react';
|
|||||||
import { TrendingUp } from 'lucide-react';
|
import { TrendingUp } from 'lucide-react';
|
||||||
import GlassCard from '@components/GlassCard';
|
import GlassCard from '@components/GlassCard';
|
||||||
import { MarketOverview } from './components';
|
import { MarketOverview } from './components';
|
||||||
import { MOCK_INDICES, MOCK_MARKET_STATS } from './constants';
|
import { MOCK_MARKET_STATS } from './constants';
|
||||||
|
|
||||||
const MarketDashboard = ({
|
const MarketDashboard = ({
|
||||||
indices = MOCK_INDICES,
|
|
||||||
marketStats = MOCK_MARKET_STATS,
|
marketStats = MOCK_MARKET_STATS,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@@ -41,8 +40,8 @@ const MarketDashboard = ({
|
|||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 市场概况:指数卡片 + 成交额 + 涨跌分布 + 热门板块 */}
|
{/* 市场概况:上证 + 深证 + 创业板指+涨跌 + 热门板块 */}
|
||||||
<MarketOverview indices={indices} marketStats={marketStats} />
|
<MarketOverview marketStats={marketStats} />
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,71 +1,40 @@
|
|||||||
// 市场概况组件 - 顶部横条(匹配设计图布局)
|
// 市场概况组件 - 顶部横条(与事件中心头部保持一致)
|
||||||
|
// 布局:上证指数 | 深证成指 | 创业板指+涨跌分布 | 热门板块
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, SimpleGrid } from '@chakra-ui/react';
|
import { Box, SimpleGrid } from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
IndexChartCard,
|
IndexKLineCard,
|
||||||
TurnoverChart,
|
GemIndexCard,
|
||||||
RiseFallChart,
|
|
||||||
HotSectorsRanking,
|
HotSectorsRanking,
|
||||||
} from './atoms';
|
} from './atoms';
|
||||||
|
|
||||||
const MarketOverview = ({ indices = [], marketStats = {} }) => {
|
const MarketOverview = ({ marketStats = {} }) => {
|
||||||
// 默认指数数据(带图表数据)
|
|
||||||
const defaultIndices = [
|
|
||||||
{
|
|
||||||
code: 'sh000001',
|
|
||||||
name: '上证指数',
|
|
||||||
value: 3391.88,
|
|
||||||
change: 1.23,
|
|
||||||
chartData: [3350, 3360, 3355, 3370, 3365, 3380, 3375, 3390, 3385, 3392],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'sz399001',
|
|
||||||
name: '深证成指',
|
|
||||||
value: 10728.54,
|
|
||||||
change: 0.86,
|
|
||||||
chartData: [10650, 10680, 10660, 10700, 10690, 10720, 10710, 10730, 10720, 10728],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'sz399006',
|
|
||||||
name: '创业板指',
|
|
||||||
value: 2156.32,
|
|
||||||
change: -0.45,
|
|
||||||
chartData: [2180, 2175, 2170, 2165, 2168, 2160, 2165, 2158, 2160, 2156],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const displayIndices = indices.length > 0 ? indices : defaultIndices;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderRadius="xl">
|
<Box borderRadius="xl">
|
||||||
{/* 6列网格布局:3个指数卡片 + 成交额 + 涨跌分布 + 热门板块 */}
|
{/* 4列网格布局:上证指数 | 深证成指 | 创业板指+涨跌 | 热门板块 */}
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
columns={{ base: 2, md: 3, lg: 6 }}
|
columns={{ base: 2, md: 4 }}
|
||||||
spacing={3}
|
spacing={3}
|
||||||
>
|
>
|
||||||
{/* 指数卡片(带迷你图表) */}
|
{/* 上证指数 - K线卡片 */}
|
||||||
{displayIndices.map((index) => (
|
<IndexKLineCard
|
||||||
<IndexChartCard
|
indexCode="sh000001"
|
||||||
key={index.code}
|
name="上证指数"
|
||||||
name={index.name}
|
|
||||||
value={index.value}
|
|
||||||
change={index.change}
|
|
||||||
chartData={index.chartData || []}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 成交额柱状图 */}
|
|
||||||
<TurnoverChart
|
|
||||||
data={marketStats.turnoverData || []}
|
|
||||||
title="成交额"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 涨跌分布图 */}
|
{/* 深证成指 - K线卡片 */}
|
||||||
<RiseFallChart
|
<IndexKLineCard
|
||||||
|
indexCode="sz399001"
|
||||||
|
name="深证成指"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 创业板指 + 涨跌分布(垂直组合) */}
|
||||||
|
<GemIndexCard
|
||||||
|
indexCode="sz399006"
|
||||||
|
name="创业板指"
|
||||||
riseCount={marketStats.riseCount || 2156}
|
riseCount={marketStats.riseCount || 2156}
|
||||||
fallCount={marketStats.fallCount || 2034}
|
fallCount={marketStats.fallCount || 2034}
|
||||||
flatCount={marketStats.flatCount || 312}
|
flatCount={marketStats.flatCount || 312}
|
||||||
title="涨跌分布"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 热门板块排行 */}
|
{/* 热门板块排行 */}
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// 创业板指+涨跌分布组合卡片
|
||||||
|
// 上半部分显示创业板指数据,下半部分显示涨跌分布进度条
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
Tooltip,
|
||||||
|
Divider,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useIndexQuote } from '@hooks/useIndexQuote';
|
||||||
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
import { THEME } from '../../constants';
|
||||||
|
import RiseFallProgressBar from './RiseFallProgressBar';
|
||||||
|
|
||||||
|
// 涨跌颜色
|
||||||
|
const UP_COLOR = '#ef5350'; // 涨 - 红色
|
||||||
|
const DOWN_COLOR = '#26a69a'; // 跌 - 绿色
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指数最新数据
|
||||||
|
*/
|
||||||
|
const fetchIndexLatest = async (indexCode) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBase()}/api/index/${indexCode}/kline?type=daily`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data?.data?.length > 0) {
|
||||||
|
const latest = data.data[data.data.length - 1];
|
||||||
|
const prev = data.data[data.data.length - 2];
|
||||||
|
const prevClose = latest.prev_close || prev?.close || latest.open;
|
||||||
|
const changeAmount = latest.close - prevClose;
|
||||||
|
const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: latest.close,
|
||||||
|
changeAmount,
|
||||||
|
changePct,
|
||||||
|
isPositive: changeAmount >= 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('GemIndexCard', 'fetchIndexLatest error', { indexCode, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创业板指+涨跌分布组合卡片
|
||||||
|
* @param {string} indexCode - 指数代码(默认 'sz399006' 创业板指)
|
||||||
|
* @param {string} name - 指数名称(默认 '创业板指')
|
||||||
|
* @param {number} riseCount - 上涨家数
|
||||||
|
* @param {number} fallCount - 下跌家数
|
||||||
|
* @param {number} flatCount - 平盘家数(可选)
|
||||||
|
*/
|
||||||
|
const GemIndexCard = ({
|
||||||
|
indexCode = 'sz399006',
|
||||||
|
name = '创业板指',
|
||||||
|
riseCount = 0,
|
||||||
|
fallCount = 0,
|
||||||
|
flatCount = 0,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [latestData, setLatestData] = useState(null);
|
||||||
|
|
||||||
|
// 转换指数代码格式
|
||||||
|
const apiIndexCode = indexCode.replace(/^(sh|sz)/, '');
|
||||||
|
|
||||||
|
// 使用实时行情 Hook
|
||||||
|
const { quote, isTrading } = useIndexQuote(apiIndexCode, {
|
||||||
|
refreshInterval: 60000,
|
||||||
|
autoRefresh: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!quote) {
|
||||||
|
const data = await fetchIndexLatest(apiIndexCode);
|
||||||
|
if (data) {
|
||||||
|
setLatestData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, [apiIndexCode, quote]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
// 实时行情更新
|
||||||
|
useEffect(() => {
|
||||||
|
if (quote) {
|
||||||
|
setLatestData({
|
||||||
|
close: quote.price,
|
||||||
|
changeAmount: quote.change,
|
||||||
|
changePct: quote.change_pct,
|
||||||
|
isPositive: quote.change >= 0,
|
||||||
|
isRealtime: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [quote]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={THEME.bg.card}
|
||||||
|
borderRadius="xl"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
p={3}
|
||||||
|
h="140px"
|
||||||
|
>
|
||||||
|
<Center h="100%">
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Spinner size="sm" color="gold" thickness="2px" />
|
||||||
|
<Text fontSize="10px" color="whiteAlpha.500">加载中...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUp = latestData?.isPositive;
|
||||||
|
const accentColor = isUp ? UP_COLOR : DOWN_COLOR;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={THEME.bg.card}
|
||||||
|
borderRadius="xl"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
p={3}
|
||||||
|
h="140px"
|
||||||
|
transition="all 0.3s"
|
||||||
|
_hover={{
|
||||||
|
borderColor: `${accentColor}40`,
|
||||||
|
boxShadow: `0 4px 20px ${accentColor}15`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VStack h="100%" spacing={2} align="stretch">
|
||||||
|
{/* 上半部分:创业板指数据 */}
|
||||||
|
<Box flex="1">
|
||||||
|
<Flex justify="space-between" align="flex-start">
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box
|
||||||
|
w="3px"
|
||||||
|
h="12px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg={accentColor}
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="whiteAlpha.700" fontWeight="semibold">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{isTrading && latestData?.isRealtime && (
|
||||||
|
<Tooltip label="实时行情" placement="top">
|
||||||
|
<Box
|
||||||
|
w="5px"
|
||||||
|
h="5px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg="#00da3c"
|
||||||
|
animation="pulse 1.5s infinite"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex mt={2} align="baseline" gap={2}>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color="white" fontFamily="monospace">
|
||||||
|
{latestData?.close?.toFixed(2) || '-'}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
px={1.5}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={`${accentColor}15`}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="monospace"
|
||||||
|
color={accentColor}
|
||||||
|
>
|
||||||
|
{isUp ? '▲' : '▼'} {isUp ? '+' : ''}{latestData?.changePct?.toFixed(2) || '0.00'}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 分割线 */}
|
||||||
|
<Divider borderColor="whiteAlpha.100" />
|
||||||
|
|
||||||
|
{/* 下半部分:涨跌分布 */}
|
||||||
|
<Box>
|
||||||
|
<RiseFallProgressBar
|
||||||
|
riseCount={riseCount}
|
||||||
|
fallCount={fallCount}
|
||||||
|
flatCount={flatCount}
|
||||||
|
showTitle={false}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GemIndexCard;
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// 指数图表卡片 - 带迷你K线图
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { Box, Text, VStack, HStack } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
const IndexChartCard = ({ name, value, change, chartData = [] }) => {
|
|
||||||
const isUp = change >= 0;
|
|
||||||
const changeColor = isUp ? '#EF4444' : '#22C55E';
|
|
||||||
const changeText = isUp ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`;
|
|
||||||
|
|
||||||
// 生成迷你图表路径
|
|
||||||
const chartPath = useMemo(() => {
|
|
||||||
if (!chartData || chartData.length < 2) return '';
|
|
||||||
|
|
||||||
const width = 120;
|
|
||||||
const height = 40;
|
|
||||||
const padding = 4;
|
|
||||||
|
|
||||||
const min = Math.min(...chartData);
|
|
||||||
const max = Math.max(...chartData);
|
|
||||||
const range = max - min || 1;
|
|
||||||
|
|
||||||
const points = chartData.map((val, i) => {
|
|
||||||
const x = padding + (i / (chartData.length - 1)) * (width - padding * 2);
|
|
||||||
const y = height - padding - ((val - min) / range) * (height - padding * 2);
|
|
||||||
return `${x},${y}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return `M ${points.join(' L ')}`;
|
|
||||||
}, [chartData]);
|
|
||||||
|
|
||||||
// 生成填充区域
|
|
||||||
const areaPath = useMemo(() => {
|
|
||||||
if (!chartPath) return '';
|
|
||||||
const width = 120;
|
|
||||||
const height = 40;
|
|
||||||
return `${chartPath} L ${width - 4},${height - 4} L 4,${height - 4} Z`;
|
|
||||||
}, [chartPath]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
bg="rgba(26, 26, 46, 0.7)"
|
|
||||||
borderRadius="lg"
|
|
||||||
p={3}
|
|
||||||
minW="160px"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(212, 175, 55, 0.15)"
|
|
||||||
backdropFilter="blur(8px)"
|
|
||||||
transition="all 0.2s"
|
|
||||||
_hover={{
|
|
||||||
borderColor: 'rgba(212, 175, 55, 0.3)',
|
|
||||||
boxShadow: '0 0 12px rgba(212, 175, 55, 0.15)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VStack align="stretch" spacing={2}>
|
|
||||||
{/* 标题 */}
|
|
||||||
<Text fontSize="xs" color="rgba(255, 255, 255, 0.6)" fontWeight="medium">
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* 数值和涨跌幅 */}
|
|
||||||
<HStack justify="space-between" align="baseline">
|
|
||||||
<Text fontSize="lg" fontWeight="bold" color={changeColor}>
|
|
||||||
{typeof value === 'number' ? value.toLocaleString(undefined, { minimumFractionDigits: 2 }) : value}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color={changeColor} fontWeight="medium">
|
|
||||||
{changeText}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 迷你图表 */}
|
|
||||||
{chartData.length > 0 && (
|
|
||||||
<Box h="40px" w="100%">
|
|
||||||
<svg width="100%" height="40" viewBox="0 0 120 40" preserveAspectRatio="none">
|
|
||||||
{/* 填充区域 */}
|
|
||||||
<path
|
|
||||||
d={areaPath}
|
|
||||||
fill={isUp ? 'rgba(239, 68, 68, 0.1)' : 'rgba(34, 197, 94, 0.1)'}
|
|
||||||
/>
|
|
||||||
{/* 线条 */}
|
|
||||||
<path
|
|
||||||
d={chartPath}
|
|
||||||
fill="none"
|
|
||||||
stroke={changeColor}
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IndexChartCard;
|
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
// 指数K线卡片组件 - 与事件中心头部保持一致
|
||||||
|
// 使用 ECharts 渲染 K 线图,调用实时行情和日线 API
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
import { useIndexQuote } from '@hooks/useIndexQuote';
|
||||||
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
import { THEME } from '../../constants';
|
||||||
|
|
||||||
|
// 涨跌颜色
|
||||||
|
const UP_COLOR = '#ef5350'; // 涨 - 红色
|
||||||
|
const DOWN_COLOR = '#26a69a'; // 跌 - 绿色
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指数K线数据
|
||||||
|
*/
|
||||||
|
const fetchIndexKline = async (indexCode) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBase()}/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('IndexKLineCard', 'fetchIndexKline error', { indexCode, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* K线指数卡片组件
|
||||||
|
* @param {string} indexCode - 指数代码(如 'sh000001', 'sz399001')
|
||||||
|
* @param {string} name - 指数名称(如 '上证指数')
|
||||||
|
*/
|
||||||
|
const IndexKLineCard = ({ indexCode, name }) => {
|
||||||
|
const [chartData, setChartData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [latestData, setLatestData] = useState(null);
|
||||||
|
|
||||||
|
// 转换指数代码格式(sh000001 -> 000001)
|
||||||
|
const apiIndexCode = indexCode.replace(/^(sh|sz)/, '');
|
||||||
|
|
||||||
|
// 使用实时行情 Hook
|
||||||
|
const { quote, isTrading } = useIndexQuote(apiIndexCode, {
|
||||||
|
refreshInterval: 60000,
|
||||||
|
autoRefresh: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载K线数据
|
||||||
|
const loadChartData = useCallback(async () => {
|
||||||
|
const data = await fetchIndexKline(apiIndexCode);
|
||||||
|
if (data?.data?.length > 0) {
|
||||||
|
const recentData = data.data.slice(-30); // 最近30天
|
||||||
|
setChartData({
|
||||||
|
dates: recentData.map(item => item.time),
|
||||||
|
klineData: recentData.map(item => [item.open, item.close, item.low, item.high]),
|
||||||
|
volumes: recentData.map(item => item.volume || 0),
|
||||||
|
rawData: recentData
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有实时行情,使用日线数据的最新值
|
||||||
|
if (!quote) {
|
||||||
|
const latest = data.data[data.data.length - 1];
|
||||||
|
const prevClose = latest.prev_close || data.data[data.data.length - 2]?.close || latest.open;
|
||||||
|
const changeAmount = latest.close - prevClose;
|
||||||
|
const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0;
|
||||||
|
|
||||||
|
setLatestData({
|
||||||
|
close: latest.close,
|
||||||
|
changeAmount,
|
||||||
|
changePct,
|
||||||
|
isPositive: changeAmount >= 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, [apiIndexCode, quote]);
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
useEffect(() => {
|
||||||
|
loadChartData();
|
||||||
|
}, [loadChartData]);
|
||||||
|
|
||||||
|
// 实时行情更新时更新 latestData
|
||||||
|
useEffect(() => {
|
||||||
|
if (quote) {
|
||||||
|
setLatestData({
|
||||||
|
close: quote.price,
|
||||||
|
changeAmount: quote.change,
|
||||||
|
changePct: quote.change_pct,
|
||||||
|
isPositive: quote.change >= 0,
|
||||||
|
updateTime: quote.update_time,
|
||||||
|
isRealtime: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [quote]);
|
||||||
|
|
||||||
|
// ECharts 配置
|
||||||
|
const chartOption = useMemo(() => {
|
||||||
|
if (!chartData) return {};
|
||||||
|
return {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
grid: [
|
||||||
|
{ left: 0, right: 0, top: 4, bottom: 20, containLabel: false },
|
||||||
|
{ left: 0, right: 0, top: '78%', bottom: 2, containLabel: false }
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
crossStyle: { color: 'rgba(255, 215, 0, 0.6)', width: 1 },
|
||||||
|
lineStyle: { color: 'rgba(255, 215, 0, 0.4)', width: 1, type: 'dashed' }
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(15, 15, 25, 0.98)',
|
||||||
|
borderColor: 'rgba(255, 215, 0, 0.5)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: [8, 12],
|
||||||
|
textStyle: { color: '#e0e0e0', fontSize: 11 },
|
||||||
|
formatter: (params) => {
|
||||||
|
const idx = params[0]?.dataIndex;
|
||||||
|
if (idx === undefined) return '';
|
||||||
|
const raw = chartData.rawData[idx];
|
||||||
|
if (!raw) return '';
|
||||||
|
|
||||||
|
const prevClose = raw.prev_close || (idx > 0 ? chartData.rawData[idx - 1]?.close : raw.open) || raw.open;
|
||||||
|
const changeAmount = (raw.close - prevClose);
|
||||||
|
const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0;
|
||||||
|
const isUp = changeAmount >= 0;
|
||||||
|
const color = isUp ? UP_COLOR : DOWN_COLOR;
|
||||||
|
const sign = isUp ? '+' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="min-width: 140px; font-size: 11px;">
|
||||||
|
<div style="color: #FFD700; margin-bottom: 6px; font-weight: bold;">${raw.time}</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 3px;">
|
||||||
|
<span style="color: #999;">收盘</span>
|
||||||
|
<span style="color: ${color}; font-weight: bold;">${raw.close?.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span style="color: #999;">涨跌</span>
|
||||||
|
<span style="color: ${color};">${sign}${changePct.toFixed(2)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{ type: 'category', data: chartData.dates, gridIndex: 0, show: false, boundaryGap: true },
|
||||||
|
{ type: 'category', data: chartData.dates, gridIndex: 1, show: false, boundaryGap: true }
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', gridIndex: 0, show: false, scale: true },
|
||||||
|
{ type: 'value', gridIndex: 1, show: false, scale: true }
|
||||||
|
],
|
||||||
|
dataZoom: [{
|
||||||
|
type: 'inside',
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
start: 30,
|
||||||
|
end: 100,
|
||||||
|
zoomOnMouseWheel: true,
|
||||||
|
moveOnMouseMove: true
|
||||||
|
}],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'K线',
|
||||||
|
type: 'candlestick',
|
||||||
|
data: chartData.klineData,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: UP_COLOR,
|
||||||
|
color0: DOWN_COLOR,
|
||||||
|
borderColor: UP_COLOR,
|
||||||
|
borderColor0: DOWN_COLOR,
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
barWidth: '70%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '成交量',
|
||||||
|
type: 'bar',
|
||||||
|
data: chartData.volumes,
|
||||||
|
xAxisIndex: 1,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
itemStyle: {
|
||||||
|
color: (params) => {
|
||||||
|
const idx = params.dataIndex;
|
||||||
|
const raw = chartData.rawData[idx];
|
||||||
|
return raw && raw.close >= raw.open ? 'rgba(239,83,80,0.4)' : 'rgba(38,166,154,0.4)';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
barWidth: '70%'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}, [chartData]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={THEME.bg.card}
|
||||||
|
borderRadius="xl"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
p={3}
|
||||||
|
h="140px"
|
||||||
|
>
|
||||||
|
<Center h="100%">
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Spinner size="sm" color="gold" thickness="2px" />
|
||||||
|
<Text fontSize="10px" color="whiteAlpha.500">加载{name}...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUp = latestData?.isPositive;
|
||||||
|
const accentColor = isUp ? UP_COLOR : DOWN_COLOR;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={THEME.bg.card}
|
||||||
|
borderRadius="xl"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
p={3}
|
||||||
|
h="140px"
|
||||||
|
transition="all 0.3s"
|
||||||
|
_hover={{
|
||||||
|
borderColor: `${accentColor}40`,
|
||||||
|
boxShadow: `0 4px 20px ${accentColor}15`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex direction="column" h="100%">
|
||||||
|
{/* 顶部:名称 + 数值 + 涨跌 */}
|
||||||
|
<Flex justify="space-between" align="center" mb={1}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box
|
||||||
|
w="3px"
|
||||||
|
h="12px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg={accentColor}
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="whiteAlpha.700" fontWeight="semibold">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{/* 实时状态 */}
|
||||||
|
{isTrading && latestData?.isRealtime && (
|
||||||
|
<Tooltip label="实时行情" placement="top">
|
||||||
|
<Box
|
||||||
|
w="5px"
|
||||||
|
h="5px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg="#00da3c"
|
||||||
|
animation="pulse 1.5s infinite"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color="white" fontFamily="monospace">
|
||||||
|
{latestData?.close?.toFixed(2) || '-'}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
px={1.5}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={`${accentColor}15`}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="monospace"
|
||||||
|
color={accentColor}
|
||||||
|
>
|
||||||
|
{isUp ? '▲' : '▼'} {isUp ? '+' : ''}{latestData?.changePct?.toFixed(2) || '0.00'}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* K线图区域 */}
|
||||||
|
<Box flex="1" position="relative">
|
||||||
|
{chartData && (
|
||||||
|
<ReactECharts
|
||||||
|
option={chartOption}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
opts={{ renderer: 'canvas' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndexKLineCard;
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// 涨跌分布进度条组件
|
||||||
|
// 使用进度条样式展示市场涨跌分布
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { THEME } from '../../constants';
|
||||||
|
|
||||||
|
// 涨跌颜色
|
||||||
|
const UP_COLOR = '#ef5350'; // 涨 - 红色
|
||||||
|
const DOWN_COLOR = '#26a69a'; // 跌 - 绿色
|
||||||
|
const FLAT_COLOR = '#FFD700'; // 平 - 金色
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 涨跌分布进度条组件
|
||||||
|
* @param {number} riseCount - 上涨家数
|
||||||
|
* @param {number} fallCount - 下跌家数
|
||||||
|
* @param {number} flatCount - 平盘家数(可选)
|
||||||
|
* @param {string} title - 标题(可选,默认"涨跌分布")
|
||||||
|
* @param {boolean} showTitle - 是否显示标题(默认 true)
|
||||||
|
* @param {boolean} compact - 紧凑模式(默认 false)
|
||||||
|
*/
|
||||||
|
const RiseFallProgressBar = ({
|
||||||
|
riseCount = 0,
|
||||||
|
fallCount = 0,
|
||||||
|
flatCount = 0,
|
||||||
|
title = '涨跌分布',
|
||||||
|
showTitle = true,
|
||||||
|
compact = false,
|
||||||
|
}) => {
|
||||||
|
const total = riseCount + fallCount + flatCount;
|
||||||
|
|
||||||
|
// 计算百分比
|
||||||
|
const risePercent = total > 0 ? (riseCount / total) * 100 : 50;
|
||||||
|
const fallPercent = total > 0 ? (fallCount / total) * 100 : 50;
|
||||||
|
const flatPercent = total > 0 ? (flatCount / total) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 标题 */}
|
||||||
|
{showTitle && (
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color="whiteAlpha.600"
|
||||||
|
mb={compact ? 1 : 2}
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<Box
|
||||||
|
h={compact ? '6px' : '8px'}
|
||||||
|
borderRadius="full"
|
||||||
|
overflow="hidden"
|
||||||
|
bg="rgba(255,255,255,0.05)"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Flex h="100%">
|
||||||
|
{/* 上涨部分 */}
|
||||||
|
<Box
|
||||||
|
w={`${risePercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={UP_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
borderRightRadius={flatPercent === 0 && fallPercent === 0 ? 'full' : 0}
|
||||||
|
borderLeftRadius="full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 平盘部分(如果有) */}
|
||||||
|
{flatPercent > 0 && (
|
||||||
|
<Box
|
||||||
|
w={`${flatPercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={FLAT_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 下跌部分 */}
|
||||||
|
<Box
|
||||||
|
w={`${fallPercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={DOWN_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
borderRightRadius="full"
|
||||||
|
borderLeftRadius={risePercent === 0 && flatPercent === 0 ? 'full' : 0}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 数值标签 */}
|
||||||
|
<Flex justify="space-between" mt={compact ? 1 : 1.5}>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={UP_COLOR} />
|
||||||
|
<Text fontSize="xs" color={UP_COLOR} fontWeight="bold" fontFamily="monospace">
|
||||||
|
{riseCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="10px" color="whiteAlpha.500">涨</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{flatCount > 0 && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={FLAT_COLOR} />
|
||||||
|
<Text fontSize="xs" color={FLAT_COLOR} fontWeight="bold" fontFamily="monospace">
|
||||||
|
{flatCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="10px" color="whiteAlpha.500">平</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={DOWN_COLOR} />
|
||||||
|
<Text fontSize="xs" color={DOWN_COLOR} fontWeight="bold" fontFamily="monospace">
|
||||||
|
{fallCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="10px" color="whiteAlpha.500">跌</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RiseFallProgressBar;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// 成交额柱状图组件
|
|
||||||
import React from 'react';
|
|
||||||
import { Box, Text, VStack, HStack } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
const TurnoverChart = ({ data = [], title = '成交额' }) => {
|
|
||||||
// 默认数据
|
|
||||||
const chartData = data.length > 0 ? data : [
|
|
||||||
{ time: '10:30', value: 0.85 },
|
|
||||||
{ time: '11:00', value: 0.92 },
|
|
||||||
{ time: '11:15', value: 0.78 },
|
|
||||||
{ time: '13:00', value: 1.05 },
|
|
||||||
{ time: '13:30', value: 1.12 },
|
|
||||||
{ time: '14:00', value: 0.95 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const maxValue = Math.max(...chartData.map(d => d.value));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
bg="rgba(26, 26, 46, 0.7)"
|
|
||||||
borderRadius="lg"
|
|
||||||
p={3}
|
|
||||||
minW="140px"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(212, 175, 55, 0.15)"
|
|
||||||
backdropFilter="blur(8px)"
|
|
||||||
>
|
|
||||||
<VStack align="stretch" spacing={2}>
|
|
||||||
<Text fontSize="xs" color="rgba(255, 255, 255, 0.6)" fontWeight="medium">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* 柱状图 */}
|
|
||||||
<HStack spacing={1} align="flex-end" h="50px">
|
|
||||||
{chartData.map((item, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
flex={1}
|
|
||||||
h={`${(item.value / maxValue) * 100}%`}
|
|
||||||
bg="linear-gradient(180deg, rgba(212, 175, 55, 0.8) 0%, rgba(212, 175, 55, 0.4) 100%)"
|
|
||||||
borderRadius="sm"
|
|
||||||
minH="4px"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 当前值 */}
|
|
||||||
<Text fontSize="sm" fontWeight="bold" color="rgba(212, 175, 55, 0.9)">
|
|
||||||
1.25亿
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TurnoverChart;
|
|
||||||
@@ -5,7 +5,10 @@ export { default as StatCard } from './StatCard';
|
|||||||
export { default as ConceptItem } from './ConceptItem';
|
export { default as ConceptItem } from './ConceptItem';
|
||||||
export { default as DayCell } from './DayCell';
|
export { default as DayCell } from './DayCell';
|
||||||
export { default as StatItem } from './StatItem';
|
export { default as StatItem } from './StatItem';
|
||||||
export { default as IndexChartCard } from './IndexChartCard';
|
|
||||||
export { default as TurnoverChart } from './TurnoverChart';
|
|
||||||
export { default as RiseFallChart } from './RiseFallChart';
|
export { default as RiseFallChart } from './RiseFallChart';
|
||||||
export { default as HotSectorsRanking } from './HotSectorsRanking';
|
export { default as HotSectorsRanking } from './HotSectorsRanking';
|
||||||
|
|
||||||
|
// 新增:K线指数卡片和涨跌分布进度条
|
||||||
|
export { default as IndexKLineCard } from './IndexKLineCard';
|
||||||
|
export { default as RiseFallProgressBar } from './RiseFallProgressBar';
|
||||||
|
export { default as GemIndexCard } from './GemIndexCard';
|
||||||
|
|||||||
Reference in New Issue
Block a user