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:
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user