refactor(MarketDashboard): 重构投资仪表盘布局

- 上证指数、深证成指使用 K 线图,与事件中心一致
 - 移除成交额模块
 - 创业板指与涨跌分布上下组合
 - 涨跌分布改用进度条样式
 - 布局从 6 列改为 4 列
This commit is contained in:
zdl
2025-12-23 10:05:28 +08:00
parent 18ba36a539
commit 30b831e880
8 changed files with 684 additions and 210 deletions

View File

@@ -4,10 +4,9 @@ import { Box, Text, HStack, Icon } from '@chakra-ui/react';
import { TrendingUp } from 'lucide-react';
import GlassCard from '@components/GlassCard';
import { MarketOverview } from './components';
import { MOCK_INDICES, MOCK_MARKET_STATS } from './constants';
import { MOCK_MARKET_STATS } from './constants';
const MarketDashboard = ({
indices = MOCK_INDICES,
marketStats = MOCK_MARKET_STATS,
}) => {
return (
@@ -41,8 +40,8 @@ const MarketDashboard = ({
/>
</HStack>
{/* 市场概况:指数卡片 + 成交额 + 涨跌分布 + 热门板块 */}
<MarketOverview indices={indices} marketStats={marketStats} />
{/* 市场概况:上证 + 深证 + 创业板指+涨跌 + 热门板块 */}
<MarketOverview marketStats={marketStats} />
</GlassCard>
);
};

View File

@@ -1,71 +1,40 @@
// 市场概况组件 - 顶部横条(匹配设计图布局
// 市场概况组件 - 顶部横条(与事件中心头部保持一致
// 布局:上证指数 | 深证成指 | 创业板指+涨跌分布 | 热门板块
import React from 'react';
import { Box, SimpleGrid } from '@chakra-ui/react';
import {
IndexChartCard,
TurnoverChart,
RiseFallChart,
IndexKLineCard,
GemIndexCard,
HotSectorsRanking,
} from './atoms';
const MarketOverview = ({ indices = [], 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;
const MarketOverview = ({ marketStats = {} }) => {
return (
<Box borderRadius="xl">
{/* 6列网格布局:3个指数卡片 + 成交额 + 涨跌分布 + 热门板块 */}
{/* 4列网格布局:上证指数 | 深证成指 | 创业板指+涨跌 | 热门板块 */}
<SimpleGrid
columns={{ base: 2, md: 3, lg: 6 }}
columns={{ base: 2, md: 4 }}
spacing={3}
>
{/* 指数卡片(带迷你图表) */}
{displayIndices.map((index) => (
<IndexChartCard
key={index.code}
name={index.name}
value={index.value}
change={index.change}
chartData={index.chartData || []}
/>
))}
{/* 成交额柱状图 */}
<TurnoverChart
data={marketStats.turnoverData || []}
title="成交额"
{/* 上证指数 - K线卡片 */}
<IndexKLineCard
indexCode="sh000001"
name="上证指数"
/>
{/* 涨跌分布图 */}
<RiseFallChart
{/* 深证成指 - K线卡片 */}
<IndexKLineCard
indexCode="sz399001"
name="深证成指"
/>
{/* 创业板指 + 涨跌分布(垂直组合) */}
<GemIndexCard
indexCode="sz399006"
name="创业板指"
riseCount={marketStats.riseCount || 2156}
fallCount={marketStats.fallCount || 2034}
flatCount={marketStats.flatCount || 312}
title="涨跌分布"
/>
{/* 热门板块排行 */}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -5,7 +5,10 @@ export { default as StatCard } from './StatCard';
export { default as ConceptItem } from './ConceptItem';
export { default as DayCell } from './DayCell';
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 HotSectorsRanking } from './HotSectorsRanking';
// 新增K线指数卡片和涨跌分布进度条
export { default as IndexKLineCard } from './IndexKLineCard';
export { default as RiseFallProgressBar } from './RiseFallProgressBar';
export { default as GemIndexCard } from './GemIndexCard';