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 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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="涨跌分布"
|
||||
/>
|
||||
|
||||
{/* 热门板块排行 */}
|
||||
|
||||
@@ -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 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';
|
||||
|
||||
Reference in New Issue
Block a user