diff --git a/src/components/StockChart/KLineChartModal.tsx b/src/components/StockChart/KLineChartModal.tsx index 75492d53..68bfa502 100644 --- a/src/components/StockChart/KLineChartModal.tsx +++ b/src/components/StockChart/KLineChartModal.tsx @@ -1,9 +1,11 @@ // src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件 import React, { useEffect, useRef, useState, useCallback } from 'react'; import { createPortal } from 'react-dom'; +import { useSelector } from 'react-redux'; import * as echarts from 'echarts'; import dayjs from 'dayjs'; import { stockService } from '@services/eventService'; +import { selectIsMobile } from '@store/slices/deviceSlice'; /** * 股票信息 @@ -83,6 +85,9 @@ const KLineChartModal: React.FC = ({ const [earliestDate, setEarliestDate] = useState(null); const [totalDaysLoaded, setTotalDaysLoaded] = useState(0); + // H5 响应式适配 + const isMobile = useSelector(selectIsMobile); + // 调试日志 console.log('[KLineChartModal] 渲染状态:', { isOpen, @@ -296,16 +301,16 @@ const KLineChartModal: React.FC = ({ } } - // 图表配置 + // 图表配置(H5 响应式) const option: echarts.EChartsOption = { backgroundColor: '#1a1a1a', title: { text: `${stock?.stock_name || stock?.stock_code} - 日K线`, left: 'center', - top: 10, + top: isMobile ? 5 : 10, textStyle: { color: '#e0e0e0', - fontSize: 18, + fontSize: isMobile ? 14 : 18, fontWeight: 'bold', }, }, @@ -370,16 +375,16 @@ const KLineChartModal: React.FC = ({ }, grid: [ { - left: '5%', - right: '5%', - top: '12%', - height: '60%', + left: isMobile ? '12%' : '5%', + right: isMobile ? '5%' : '5%', + top: isMobile ? '12%' : '12%', + height: isMobile ? '55%' : '60%', }, { - left: '5%', - right: '5%', - top: '77%', - height: '18%', + left: isMobile ? '12%' : '5%', + right: isMobile ? '5%' : '5%', + top: isMobile ? '72%' : '77%', + height: isMobile ? '20%' : '18%', }, ], xAxis: [ @@ -394,7 +399,8 @@ const KLineChartModal: React.FC = ({ }, axisLabel: { color: '#999', - interval: Math.floor(dates.length / 8), + fontSize: isMobile ? 10 : 12, + interval: Math.floor(dates.length / (isMobile ? 4 : 8)), }, splitLine: { show: false, @@ -411,7 +417,8 @@ const KLineChartModal: React.FC = ({ }, axisLabel: { color: '#999', - interval: Math.floor(dates.length / 8), + fontSize: isMobile ? 10 : 12, + interval: Math.floor(dates.length / (isMobile ? 4 : 8)), }, }, ], @@ -419,6 +426,7 @@ const KLineChartModal: React.FC = ({ { scale: true, gridIndex: 0, + splitNumber: isMobile ? 4 : 5, splitLine: { show: true, lineStyle: { @@ -432,12 +440,14 @@ const KLineChartModal: React.FC = ({ }, axisLabel: { color: '#999', + fontSize: isMobile ? 10 : 12, formatter: (value: number) => value.toFixed(2), }, }, { scale: true, gridIndex: 1, + splitNumber: isMobile ? 2 : 3, splitLine: { show: false, }, @@ -448,6 +458,7 @@ const KLineChartModal: React.FC = ({ }, axisLabel: { color: '#999', + fontSize: isMobile ? 10 : 12, formatter: (value: number) => { if (value >= 100000000) { return (value / 100000000).toFixed(1) + '亿'; @@ -545,7 +556,7 @@ const KLineChartModal: React.FC = ({ return () => clearTimeout(retryTimer); } - }, [data, stock]); + }, [data, stock, isMobile]); // 加载数据 useEffect(() => { @@ -600,13 +611,13 @@ const KLineChartModal: React.FC = ({ top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: '90vw', - maxWidth: '1400px', - maxHeight: '85vh', + width: isMobile ? '96vw' : '90vw', + maxWidth: isMobile ? 'none' : '1400px', + maxHeight: isMobile ? '85vh' : '85vh', backgroundColor: '#1a1a1a', border: '2px solid #ffd700', boxShadow: '0 0 30px rgba(255, 215, 0, 0.5)', - borderRadius: '8px', + borderRadius: isMobile ? '12px' : '8px', zIndex: 10002, display: 'flex', flexDirection: 'column', @@ -616,7 +627,7 @@ const KLineChartModal: React.FC = ({ {/* Header */}
= ({ }} >
-
- +
+ {stock.stock_name || stock.stock_code} ({stock.stock_code}) {data.length > 0 && ( - + 共{data.length}个交易日 {hasMore ? '(向左滑动加载更多)' : '(已加载全部)'} )} {loadingMore && ( - + = ({ )}
-
- 日K线图 - - 💡 鼠标滚轮缩放 | 拖动查看不同时间段 +
+ 日K线图 + + 💡 {isMobile ? '滚轮缩放 | 拖动查看' : '鼠标滚轮缩放 | 拖动查看不同时间段'}
@@ -675,26 +686,33 @@ const KLineChartModal: React.FC = ({
{/* Body */} -
+
{error && (
- {error} + {error}
)} -
+
{loading && (
= ({ const [error, setError] = useState(null); const [data, setData] = useState([]); + // H5 响应式适配 + const isMobile = useSelector(selectIsMobile); + // 加载分时图数据(优先使用缓存) const loadData = async () => { if (!stock?.stock_code) return; @@ -187,16 +192,16 @@ const TimelineChartModal: React.FC = ({ } } - // 图表配置 + // 图表配置(H5 响应式) const option: echarts.EChartsOption = { backgroundColor: '#1a1a1a', title: { text: `${stock?.stock_name || stock?.stock_code} - 分时图`, left: 'center', - top: 10, + top: isMobile ? 5 : 10, textStyle: { color: '#e0e0e0', - fontSize: 18, + fontSize: isMobile ? 14 : 18, fontWeight: 'bold', }, }, @@ -247,16 +252,16 @@ const TimelineChartModal: React.FC = ({ }, grid: [ { - left: '5%', - right: '5%', - top: '15%', - height: '55%', + left: isMobile ? '12%' : '5%', + right: isMobile ? '5%' : '5%', + top: isMobile ? '12%' : '15%', + height: isMobile ? '58%' : '55%', }, { - left: '5%', - right: '5%', - top: '75%', - height: '15%', + left: isMobile ? '12%' : '5%', + right: isMobile ? '5%' : '5%', + top: isMobile ? '75%' : '75%', + height: isMobile ? '18%' : '15%', }, ], xAxis: [ @@ -271,7 +276,8 @@ const TimelineChartModal: React.FC = ({ }, axisLabel: { color: '#999', - interval: Math.floor(times.length / 6), + fontSize: isMobile ? 10 : 12, + interval: Math.floor(times.length / (isMobile ? 4 : 6)), }, splitLine: { show: true, @@ -291,7 +297,8 @@ const TimelineChartModal: React.FC = ({ }, axisLabel: { color: '#999', - interval: Math.floor(times.length / 6), + fontSize: isMobile ? 10 : 12, + interval: Math.floor(times.length / (isMobile ? 4 : 6)), }, }, ], @@ -299,6 +306,7 @@ const TimelineChartModal: React.FC = ({ { scale: true, gridIndex: 0, + splitNumber: isMobile ? 4 : 5, splitLine: { show: true, lineStyle: { @@ -312,12 +320,14 @@ const TimelineChartModal: React.FC = ({ }, axisLabel: { color: '#999', + fontSize: isMobile ? 10 : 12, formatter: (value: number) => value.toFixed(2), }, }, { scale: true, gridIndex: 1, + splitNumber: isMobile ? 2 : 3, splitLine: { show: false, }, @@ -328,6 +338,7 @@ const TimelineChartModal: React.FC = ({ }, axisLabel: { color: '#999', + fontSize: isMobile ? 10 : 12, formatter: (value: number) => { if (value >= 10000) { return (value / 10000).toFixed(1) + '万'; @@ -443,7 +454,7 @@ const TimelineChartModal: React.FC = ({ return () => clearTimeout(retryTimer); } - }, [data, stock]); + }, [data, stock, isMobile]); // 加载数据 useEffect(() => { @@ -455,29 +466,30 @@ const TimelineChartModal: React.FC = ({ if (!stock) return null; return ( - + - - + + - + {stock.stock_name || stock.stock_code} ({stock.stock_code}) - + 分时走势图 - + {error && ( @@ -485,7 +497,7 @@ const TimelineChartModal: React.FC = ({ )} - + {loading && ( { return data; }; +/** + * 计算简单移动均价(用于分时图均价线) + * @param {Array} data - 已有数据 + * @param {number} currentPrice - 当前价格 + * @param {number} period - 均线周期(默认5) + * @returns {number} 均价 + */ +function calculateAvgPrice(data, currentPrice, period = 5) { + const recentPrices = data.slice(-period).map(d => d.price || d.close); + recentPrices.push(currentPrice); + const sum = recentPrices.reduce((acc, p) => acc + p, 0); + return parseFloat((sum / recentPrices.length).toFixed(2)); +} + /** * 生成时间范围内的数据 */ @@ -80,6 +94,11 @@ function generateTimeRange(data, startTime, endTime, basePrice, session) { // ✅ 修复:为分时图添加完整的 OHLC 字段 const closePrice = parseFloat(price.toFixed(2)); + + // 计算均价和涨跌幅 + const avgPrice = calculateAvgPrice(data, closePrice); + const changePercent = parseFloat(((closePrice - basePrice) / basePrice * 100).toFixed(2)); + data.push({ time: formatTime(current), timestamp: current.getTime(), // ✅ 新增:毫秒时间戳 @@ -88,6 +107,8 @@ function generateTimeRange(data, startTime, endTime, basePrice, session) { low: parseFloat((price * 0.9997).toFixed(2)), // ✅ 新增:最低价(略低于收盘) close: closePrice, // ✅ 保留:收盘价 price: closePrice, // ✅ 保留:兼容字段(供 MiniTimelineChart 使用) + avg_price: avgPrice, // ✅ 新增:均价(供 TimelineChartModal 使用) + change_percent: changePercent, // ✅ 新增:涨跌幅(供 TimelineChartModal 使用) volume: volume, prev_close: basePrice }); diff --git a/src/mocks/handlers/stock.js b/src/mocks/handlers/stock.js index 623702c2..701c97f0 100644 --- a/src/mocks/handlers/stock.js +++ b/src/mocks/handlers/stock.js @@ -224,4 +224,59 @@ export const stockHandlers = [ ); } }), + + // 批量获取股票K线数据 + http.post('/api/stock/batch-kline', async ({ request }) => { + await delay(400); + + try { + const body = await request.json(); + const { codes, type = 'timeline', event_time } = body; + + console.log('[Mock Stock] 批量获取K线数据:', { + stockCount: codes?.length, + type, + eventTime: event_time + }); + + if (!codes || !Array.isArray(codes) || codes.length === 0) { + return HttpResponse.json( + { error: '股票代码列表不能为空' }, + { status: 400 } + ); + } + + // 为每只股票生成数据 + const batchData = {}; + codes.forEach(stockCode => { + let data; + if (type === 'timeline') { + data = generateTimelineData('000001.SH'); + } else if (type === 'daily') { + data = generateDailyData('000001.SH', 60); + } else { + data = []; + } + + batchData[stockCode] = { + success: true, + data: data, + stock_code: stockCode + }; + }); + + return HttpResponse.json({ + success: true, + data: batchData, + type: type, + message: '批量获取成功' + }); + } catch (error) { + console.error('[Mock Stock] 批量获取K线数据失败:', error); + return HttpResponse.json( + { error: '批量获取K线数据失败' }, + { status: 500 } + ); + } + }), ]; diff --git a/src/views/Community/components/EventDetailModal.tsx b/src/views/Community/components/EventDetailModal.tsx index 7b61af97..acd94f50 100644 --- a/src/views/Community/components/EventDetailModal.tsx +++ b/src/views/Community/components/EventDetailModal.tsx @@ -35,9 +35,9 @@ const EventDetailModal: React.FC = ({ className="event-detail-modal" styles={{ mask: { background: 'transparent' }, - content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto' }, - header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0 }, - body: { padding: 0 }, + content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }, + header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0, flexShrink: 0 }, + body: { padding: 0, overflowY: 'auto', flex: 1 }, }} > {event && }