feat(MarketDashboard): 添加市场概况卡片(上证/深证/总市值/成交额)

新增组件:
- MarketSummaryCard: 紧凑型 2x2 网格布局
  - 上证指数:价格、涨跌额、涨跌幅
  - 深证指数:价格、涨跌额、涨跌幅
  - 总市值:万亿级格式化显示
  - 成交额:万亿级格式化显示

布局更新:
- MarketOverview: 从 3 列扩展为 4 列
- 市场概况卡片位于最左侧

Mock API:
- /api/market/summary: 返回实时市场概况数据
- 数据基于时间产生小波动,模拟真实行情

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-23 15:28:48 +08:00
parent 7d859e18ca
commit 145b6575d8
4 changed files with 219 additions and 4 deletions

View File

@@ -512,7 +512,55 @@ export const marketHandlers = [
});
}),
// 12. 市场统计数据(个股中心页面使用)
// 12. 市场概况数据(投资仪表盘使用)- 上证/深证/总市值/成交额
http.get('/api/market/summary', async () => {
await delay(150);
// 生成实时数据(基于当前时间产生小波动)
const now = new Date();
const seed = now.getHours() * 60 + now.getMinutes();
// 上证指数(基准 3400
const shBasePrice = 3400;
const shChange = parseFloat(((Math.sin(seed / 30) + Math.random() - 0.5) * 2).toFixed(2));
const shPrice = parseFloat((shBasePrice * (1 + shChange / 100)).toFixed(2));
const shChangeAmount = parseFloat((shPrice - shBasePrice).toFixed(2));
// 深证指数(基准 10800
const szBasePrice = 10800;
const szChange = parseFloat(((Math.sin(seed / 25) + Math.random() - 0.5) * 2.5).toFixed(2));
const szPrice = parseFloat((szBasePrice * (1 + szChange / 100)).toFixed(2));
const szChangeAmount = parseFloat((szPrice - szBasePrice).toFixed(2));
// 总市值(约 100-110 万亿波动)
const totalMarketCap = parseFloat((105 + (Math.sin(seed / 60) * 5)).toFixed(1)) * 1000000000000;
// 成交额(约 0.8-1.5 万亿波动)
const turnover = parseFloat((1.0 + (Math.random() * 0.5 - 0.2)).toFixed(2)) * 1000000000000;
console.log('[Mock Market] 获取市场概况数据');
return HttpResponse.json({
success: true,
data: {
shanghai: {
value: shPrice,
change: shChange,
changeAmount: shChangeAmount,
},
shenzhen: {
value: szPrice,
change: szChange,
changeAmount: szChangeAmount,
},
totalMarketCap,
turnover,
updateTime: now.toISOString(),
},
});
}),
// 13. 市场统计数据(个股中心页面使用)
http.get('/api/market/statistics', async ({ request }) => {
await delay(200);
const url = new URL(request.url);

View File

@@ -1,20 +1,24 @@
// 市场概况组件 - 顶部横条(与事件中心头部保持一致)
// 布局:上证指数 | 深证成指 | 创业板指+涨跌分布
// 布局:市场概况 | 上证指数 | 深证成指 | 创业板指+涨跌分布
import React from 'react';
import { Box, SimpleGrid } from '@chakra-ui/react';
import {
IndexKLineCard,
GemIndexCard,
MarketSummaryCard,
} from './atoms';
const MarketOverview = ({ marketStats = {} }) => {
return (
<Box borderRadius="xl">
{/* 3列网格布局:上证指数 | 深证成指 | 创业板指+涨跌 */}
{/* 4列网格布局:市场概况 | 上证指数 | 深证成指 | 创业板指+涨跌 */}
<SimpleGrid
columns={{ base: 2, md: 3 }}
columns={{ base: 2, md: 4 }}
spacing={3}
>
{/* 市场概况 - 上证/深证/总市值/成交额 */}
<MarketSummaryCard />
{/* 上证指数 - K线卡片 */}
<IndexKLineCard
indexCode="sh000001"

View File

@@ -0,0 +1,160 @@
// 市场概况卡片 - 紧凑版(上证/深证/总市值/成交额)
import React, { useEffect, useState } from 'react';
import { Box, Text, VStack, HStack, SimpleGrid } from '@chakra-ui/react';
import { THEME } from '../../constants';
import { getApiBase } from '@/utils/apiConfig';
/**
* 格式化大数字(万亿/亿)
*/
const formatLargeNumber = (num) => {
if (!num && num !== 0) return '--';
const trillion = 1000000000000; // 万亿
const billion = 100000000; // 亿
if (num >= trillion) {
return `${(num / trillion).toFixed(1)}万亿`;
} else if (num >= billion) {
return `${(num / billion).toFixed(1)}亿`;
}
return num.toLocaleString();
};
/**
* 单个指数/统计项
*/
const SummaryItem = ({ label, value, change, changeAmount, subValue, isIndex = true }) => {
const isUp = change > 0;
const isFlat = change === 0;
const changeColor = isFlat
? 'rgba(255, 255, 255, 0.6)'
: isUp ? THEME.status.up : THEME.status.down;
return (
<VStack align="flex-start" spacing={0.5} py={2}>
{/* 标签 */}
<Text fontSize="xs" color="rgba(255, 255, 255, 0.5)">
{label}
</Text>
{/* 主数值 */}
<Text
fontSize="lg"
fontWeight="bold"
color="rgba(255, 255, 255, 0.95)"
lineHeight="1.2"
>
{typeof value === 'number' ? value.toFixed(2) : value}
</Text>
{/* 涨跌信息(仅指数显示) */}
{isIndex && change !== undefined ? (
<Text fontSize="xs" color={changeColor}>
{changeAmount !== undefined && (
<Text as="span" mr={1}>
{isUp ? '+' : ''}{typeof changeAmount === 'number' ? changeAmount.toFixed(2) : changeAmount}
</Text>
)}
{isUp ? '+' : ''}{change.toFixed(2)}%
</Text>
) : (
// 非指数项显示副标签
<Text fontSize="xs" color="rgba(255, 255, 255, 0.4)">
{subValue || label}
</Text>
)}
</VStack>
);
};
/**
* 市场概况卡片
*/
const MarketSummaryCard = () => {
const [marketData, setMarketData] = useState({
shanghai: { value: 3391.88, change: 0.52, changeAmount: 17.55 },
shenzhen: { value: 10723.49, change: 0.68, changeAmount: 72.38 },
totalMarketCap: 105.6 * 1000000000000, // 105.6万亿
turnover: 1.0 * 1000000000000, // 1.0万亿
});
// 获取实时数据
useEffect(() => {
const fetchMarketData = async () => {
try {
const base = getApiBase();
const response = await fetch(`${base}/api/market/summary`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
if (data.success && data.data) {
setMarketData(prev => ({
...prev,
...data.data,
}));
}
}
} catch (error) {
// 使用默认数据
console.debug('Using default market data');
}
};
fetchMarketData();
// 每分钟更新一次
const interval = setInterval(fetchMarketData, 60000);
return () => clearInterval(interval);
}, []);
return (
<Box
bg="rgba(26, 26, 46, 0.6)"
borderRadius="lg"
px={3}
py={2}
border="1px solid"
borderColor="rgba(212, 175, 55, 0.15)"
backdropFilter="blur(8px)"
minW="200px"
>
<SimpleGrid columns={2} spacing={0}>
{/* 上证 */}
<SummaryItem
label="上证"
value={marketData.shanghai.value}
change={marketData.shanghai.change}
changeAmount={marketData.shanghai.changeAmount}
isIndex={true}
/>
{/* 深证 */}
<SummaryItem
label="深证"
value={marketData.shenzhen.value}
change={marketData.shenzhen.change}
changeAmount={marketData.shenzhen.changeAmount}
isIndex={true}
/>
{/* 总市值 */}
<SummaryItem
label="总市值"
value={formatLargeNumber(marketData.totalMarketCap)}
subValue={formatLargeNumber(marketData.totalMarketCap)}
isIndex={false}
/>
{/* 成交额 */}
<SummaryItem
label="成交额"
value={formatLargeNumber(marketData.turnover)}
subValue="成交万亿"
isIndex={false}
/>
</SimpleGrid>
</Box>
);
};
export default MarketSummaryCard;

View File

@@ -12,3 +12,6 @@ export { default as HotSectorsRanking } from './HotSectorsRanking';
export { default as IndexKLineCard } from './IndexKLineCard';
export { default as RiseFallProgressBar } from './RiseFallProgressBar';
export { default as GemIndexCard } from './GemIndexCard';
// 市场概况卡片(上证/深证/总市值/成交额)
export { default as MarketSummaryCard } from './MarketSummaryCard';