diff --git a/src/views/Profile/components/MarketDashboard/MarketDashboard.js b/src/views/Profile/components/MarketDashboard/MarketDashboard.js index 1e2c0e46..b9a15587 100644 --- a/src/views/Profile/components/MarketDashboard/MarketDashboard.js +++ b/src/views/Profile/components/MarketDashboard/MarketDashboard.js @@ -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 = ({ /> - {/* 市场概况:指数卡片 + 成交额 + 涨跌分布 + 热门板块 */} - + {/* 市场概况:上证 + 深证 + 创业板指+涨跌 + 热门板块 */} + ); }; diff --git a/src/views/Profile/components/MarketDashboard/components/MarketOverview.js b/src/views/Profile/components/MarketDashboard/components/MarketOverview.js index df09b713..d10479c4 100644 --- a/src/views/Profile/components/MarketDashboard/components/MarketOverview.js +++ b/src/views/Profile/components/MarketDashboard/components/MarketOverview.js @@ -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 ( - {/* 6列网格布局:3个指数卡片 + 成交额 + 涨跌分布 + 热门板块 */} + {/* 4列网格布局:上证指数 | 深证成指 | 创业板指+涨跌 | 热门板块 */} - {/* 指数卡片(带迷你图表) */} - {displayIndices.map((index) => ( - - ))} - - {/* 成交额柱状图 */} - - {/* 涨跌分布图 */} - + + {/* 创业板指 + 涨跌分布(垂直组合) */} + {/* 热门板块排行 */} diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/GemIndexCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/GemIndexCard.js new file mode 100644 index 00000000..c3f6b3c1 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/GemIndexCard.js @@ -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 ( + +
+ + + 加载中... + +
+
+ ); + } + + const isUp = latestData?.isPositive; + const accentColor = isUp ? UP_COLOR : DOWN_COLOR; + + return ( + + + {/* 上半部分:创业板指数据 */} + + + + + + {name} + + {isTrading && latestData?.isRealtime && ( + + + + )} + + + + + + {latestData?.close?.toFixed(2) || '-'} + + + + {isUp ? '▲' : '▼'} {isUp ? '+' : ''}{latestData?.changePct?.toFixed(2) || '0.00'}% + + + + + + {/* 分割线 */} + + + {/* 下半部分:涨跌分布 */} + + + + + + ); +}; + +export default GemIndexCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js deleted file mode 100644 index 9862d42c..00000000 --- a/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js +++ /dev/null @@ -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 ( - - - {/* 标题 */} - - {name} - - - {/* 数值和涨跌幅 */} - - - {typeof value === 'number' ? value.toLocaleString(undefined, { minimumFractionDigits: 2 }) : value} - - - {changeText} - - - - {/* 迷你图表 */} - {chartData.length > 0 && ( - - - {/* 填充区域 */} - - {/* 线条 */} - - - - )} - - - ); -}; - -export default IndexChartCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/IndexKLineCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/IndexKLineCard.js new file mode 100644 index 00000000..8566831c --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/IndexKLineCard.js @@ -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 ` +
+
${raw.time}
+
+ 收盘 + ${raw.close?.toFixed(2)} +
+
+ 涨跌 + ${sign}${changePct.toFixed(2)}% +
+
+ `; + } + }, + 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 ( + +
+ + + 加载{name}... + +
+
+ ); + } + + const isUp = latestData?.isPositive; + const accentColor = isUp ? UP_COLOR : DOWN_COLOR; + + return ( + + + {/* 顶部:名称 + 数值 + 涨跌 */} + + + + + {name} + + {/* 实时状态 */} + {isTrading && latestData?.isRealtime && ( + + + + )} + + + + + {latestData?.close?.toFixed(2) || '-'} + + + + {isUp ? '▲' : '▼'} {isUp ? '+' : ''}{latestData?.changePct?.toFixed(2) || '0.00'}% + + + + + + {/* K线图区域 */} + + {chartData && ( + + )} + + + + ); +}; + +export default IndexKLineCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallProgressBar.js b/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallProgressBar.js new file mode 100644 index 00000000..91ae8b88 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallProgressBar.js @@ -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 ( + + {/* 标题 */} + {showTitle && ( + + {title} + + )} + + {/* 进度条 */} + + + {/* 上涨部分 */} + + + {/* 平盘部分(如果有) */} + {flatPercent > 0 && ( + + )} + + {/* 下跌部分 */} + + + + + {/* 数值标签 */} + + + + + {riseCount} + + + + + {flatCount > 0 && ( + + + + {flatCount} + + + + )} + + + + + {fallCount} + + + + + + ); +}; + +export default RiseFallProgressBar; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js b/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js deleted file mode 100644 index 5b779893..00000000 --- a/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js +++ /dev/null @@ -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 ( - - - - {title} - - - {/* 柱状图 */} - - {chartData.map((item, index) => ( - - ))} - - - {/* 当前值 */} - - 1.25亿 - - - - ); -}; - -export default TurnoverChart; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/index.js b/src/views/Profile/components/MarketDashboard/components/atoms/index.js index 988447c5..b5879973 100644 --- a/src/views/Profile/components/MarketDashboard/components/atoms/index.js +++ b/src/views/Profile/components/MarketDashboard/components/atoms/index.js @@ -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';