From 9aee8640171f53e695f3e6fff6af7c3826afd9c3 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 24 Dec 2025 17:22:30 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20K=20=E7=BA=BF=E5=9B=BE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 StockChartKLineModal.tsx(未被任何组件引用) - 删除 StockChartModal.js.backup(备份文件) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../StockChart/StockChartKLineModal.tsx | 276 --------- .../StockChart/StockChartModal.js.backup | 558 ------------------ 2 files changed, 834 deletions(-) delete mode 100644 src/components/StockChart/StockChartKLineModal.tsx delete mode 100644 src/components/StockChart/StockChartModal.js.backup diff --git a/src/components/StockChart/StockChartKLineModal.tsx b/src/components/StockChart/StockChartKLineModal.tsx deleted file mode 100644 index 745d0a54..00000000 --- a/src/components/StockChart/StockChartKLineModal.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/** - * StockChartKLineModal - K 线图表模态框组件 - * - * 使用 KLineChart 库实现的专业金融图表组件 - * 替换原有的 ECharts 实现(StockChartAntdModal.js) - */ - -import React, { useState, useCallback, useMemo } from 'react'; -import { Modal, Button, Radio, Select, Space, Spin, Alert } from 'antd'; -import type { RadioChangeEvent } from 'antd'; -import { - LineChartOutlined, - BarChartOutlined, - SettingOutlined, - ReloadOutlined, -} from '@ant-design/icons'; -import { Box } from '@chakra-ui/react'; - -// 自定义 Hooks -import { useKLineChart, useKLineData, useEventMarker } from './hooks'; - -// 类型定义 -import type { ChartType, StockInfo } from './types'; - -// 配置常量 -import { - CHART_TYPE_CONFIG, - CHART_HEIGHTS, - INDICATORS, - DEFAULT_SUB_INDICATORS, -} from './config'; - -// 工具函数 -import { createSubIndicators } from './utils'; - -// ==================== 组件 Props ==================== - -export interface StockChartKLineModalProps { - /** 是否显示模态框 */ - visible: boolean; - /** 关闭模态框回调 */ - onClose: () => void; - /** 股票信息 */ - stock: StockInfo; - /** 事件时间(ISO 字符串,可选) */ - eventTime?: string; - /** 事件标题(用于标记标签,可选) */ - eventTitle?: string; -} - -// ==================== 主组件 ==================== - -const StockChartKLineModal: React.FC = ({ - visible, - onClose, - stock, - eventTime, - eventTitle, -}) => { - // ==================== 状态管理 ==================== - - /** 图表类型(分时图/日K线) */ - const [chartType, setChartType] = useState('daily'); - - /** 选中的副图指标 */ - const [selectedIndicators, setSelectedIndicators] = useState( - DEFAULT_SUB_INDICATORS - ); - - // ==================== 自定义 Hooks ==================== - - /** 图表实例管理 */ - const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({ - containerId: `kline-chart-${stock.stock_code}`, - height: CHART_HEIGHTS.main, - autoResize: true, - }); - - /** 数据加载管理 */ - const { - data, - loading: dataLoading, - error: dataError, - loadData, - } = useKLineData({ - chart, - stockCode: stock.stock_code, - chartType, - eventTime, - autoLoad: visible, // 模态框打开时自动加载 - }); - - /** 事件标记管理 */ - const { marker } = useEventMarker({ - chart, - data, - eventTime, - eventTitle, - autoCreate: true, - }); - - // ==================== 事件处理 ==================== - - /** - * 切换图表类型(分时图 ↔ 日K线) - */ - const handleChartTypeChange = useCallback((e: RadioChangeEvent) => { - const newType = e.target.value as ChartType; - setChartType(newType); - }, []); - - /** - * 切换副图指标 - */ - const handleIndicatorChange = useCallback( - (values: string[]) => { - setSelectedIndicators(values); - - if (!chart) { - return; - } - - // 先移除所有副图指标(KLineChart 会自动移除) - // 然后创建新的指标 - createSubIndicators(chart, values); - }, - [chart] - ); - - /** - * 刷新数据 - */ - const handleRefresh = useCallback(() => { - loadData(); - }, [loadData]); - - // ==================== 计算属性 ==================== - - /** 是否有错误 */ - const hasError = useMemo(() => { - return !!chartError || !!dataError; - }, [chartError, dataError]); - - /** 错误消息 */ - const errorMessage = useMemo(() => { - if (chartError) { - return `图表初始化失败: ${chartError.message}`; - } - if (dataError) { - return `数据加载失败: ${dataError.message}`; - } - return null; - }, [chartError, dataError]); - - /** 模态框标题 */ - const modalTitle = useMemo(() => { - return `${stock.stock_name}(${stock.stock_code}) - ${CHART_TYPE_CONFIG[chartType].label}`; - }, [stock, chartType]); - - /** 是否显示加载状态 */ - const showLoading = useMemo(() => { - return dataLoading || !isInitialized; - }, [dataLoading, isInitialized]); - - // ==================== 副作用 ==================== - - // 无副作用,都在 Hooks 中管理 - - // ==================== 渲染 ==================== - - return ( - - {/* 工具栏 */} - - - {/* 图表类型切换 */} - - - 分时图 - - - 日K线 - - - - {/* 副图指标选择 */} - - - {/* 刷新按钮 */} - - - - - {/* 错误提示 */} - {hasError && ( - - )} - - {/* 图表容器 */} - - {/* 加载遮罩 */} - {showLoading && ( - - - - )} - - {/* KLineChart 容器 */} -
- - - {/* 数据信息(调试用,生产环境可移除) */} - {process.env.NODE_ENV === 'development' && ( - - - 数据点数: {data.length} - 事件标记: {marker ? marker.label : '无'} - 图表ID: {chart?.id || '未初始化'} - - - )} - - ); -}; - -export default StockChartKLineModal; diff --git a/src/components/StockChart/StockChartModal.js.backup b/src/components/StockChart/StockChartModal.js.backup deleted file mode 100644 index f4b0ca80..00000000 --- a/src/components/StockChart/StockChartModal.js.backup +++ /dev/null @@ -1,558 +0,0 @@ -// src/components/StockChart/StockChartModal.js - 统一的股票图表组件 -import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, Button, ButtonGroup, VStack, HStack, Text, Badge, Box, Flex, CircularProgress } from '@chakra-ui/react'; -import ReactECharts from 'echarts-for-react'; -import * as echarts from 'echarts'; -import dayjs from 'dayjs'; -import { stockService } from '../../services/eventService'; -import { logger } from '../../utils/logger'; -import RiskDisclaimer from '../RiskDisclaimer'; -import { RelationDescription } from '../StockRelation'; - -const StockChartModal = ({ - isOpen, - onClose, - stock, - eventTime, - isChakraUI = true, // 是否使用Chakra UI,默认true;如果false则使用Antd - size = "6xl", - initialChartType = 'timeline' // 初始图表类型(timeline/daily) -}) => { - const chartRef = useRef(null); - const chartInstanceRef = useRef(null); - const [chartType, setChartType] = useState(initialChartType); - const [loading, setLoading] = useState(false); - const [chartData, setChartData] = useState(null); - const [preloadedData, setPreloadedData] = useState({}); - - // 预加载数据 - const preloadData = async (type) => { - if (!stock || preloadedData[type]) return; - - try { - let adjustedEventTime = eventTime; - if (eventTime) { - try { - const eventMoment = dayjs(eventTime); - if (eventMoment.isValid() && eventMoment.hour() >= 15) { - const nextDay = eventMoment.clone().add(1, 'day'); - nextDay.hour(9).minute(30).second(0).millisecond(0); - adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm'); - } - } catch (e) { - logger.warn('StockChartModal', '事件时间解析失败', { - eventTime, - error: e.message - }); - } - } - - const response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime); - setPreloadedData(prev => ({...prev, [type]: response})); - } catch (err) { - logger.error('StockChartModal', 'preloadData', err, { - stockCode: stock?.stock_code, - type - }); - } - }; - - useEffect(() => { - if (isOpen && stock) { - // 预加载两种图表类型的数据 - preloadData('timeline'); - preloadData('daily'); - - // 清理图表实例 - return () => { - if (chartInstanceRef.current) { - window.removeEventListener('resize', chartInstanceRef.current.resizeHandler); - chartInstanceRef.current.dispose(); - chartInstanceRef.current = null; - } - }; - } - }, [isOpen, stock, eventTime]); - - useEffect(() => { - if (isOpen && stock) { - loadChartData(chartType); - } - }, [chartType, isOpen, stock]); - - const loadChartData = async (type) => { - if (!stock) return; - - try { - setLoading(true); - - // 先尝试使用预加载的数据 - let response = preloadedData[type]; - - if (!response) { - // 如果预加载数据不存在,则立即请求 - let adjustedEventTime = eventTime; - if (eventTime) { - try { - const eventMoment = dayjs(eventTime); - if (eventMoment.isValid() && eventMoment.hour() >= 15) { - const nextDay = eventMoment.clone().add(1, 'day'); - nextDay.hour(9).minute(30).second(0).millisecond(0); - adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm'); - } - } catch (e) { - logger.warn('StockChartModal', '事件时间解析失败', { - eventTime, - error: e.message - }); - } - } - - response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime); - } - - setChartData(response); - - // 初始化图表 - if (chartRef.current && !chartInstanceRef.current) { - const chart = echarts.init(chartRef.current); - chart.resizeHandler = () => chart.resize(); - window.addEventListener('resize', chart.resizeHandler); - chartInstanceRef.current = chart; - } - - if (chartInstanceRef.current) { - const option = generateChartOption(response, type, eventTime); - chartInstanceRef.current.setOption(option, true); - } - } catch (err) { - logger.error('StockChartModal', 'loadChartData', err, { - stockCode: stock?.stock_code, - chartType: type - }); - } finally { - setLoading(false); - } - }; - - const generateChartOption = (data, type, originalEventTime, adjustedEventTime) => { - if (!data || !data.data || data.data.length === 0) { - return { title: { text: '暂无数据', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 16 } } }; - } - - const stockData = data.data; - const tradeDate = data.trade_date; - - // 分时图 - if (type === 'timeline') { - const times = stockData.map(item => item.time); - const prices = stockData.map(item => item.close || item.price); - const avgPrices = stockData.map(item => item.avg_price); - const volumes = stockData.map(item => item.volume); - - // 获取昨收盘价作为基准 - const prevClose = data.prev_close || (prices.length > 0 ? prices[0] : 0); - - // 计算涨跌幅数据 - const changePercentData = prices.map(price => ((price - prevClose) / prevClose * 100)); - const avgChangePercentData = avgPrices.map(avgPrice => ((avgPrice - prevClose) / prevClose * 100)); - - const currentPrice = prices[prices.length - 1]; - const currentChange = ((currentPrice - prevClose) / prevClose * 100); - const isUp = currentChange >= 0; - const lineColor = isUp ? '#ef5350' : '#26a69a'; - - // 计算事件标记线位置 - let eventMarkLineData = []; - if (originalEventTime && times.length > 0) { - const eventMoment = dayjs(originalEventTime); - const eventDate = eventMoment.format('YYYY-MM-DD'); - const eventTime = eventMoment.format('HH:mm'); - - if (eventDate === tradeDate) { - // 找到最接近的时间点 - let nearestIdx = 0; - const eventMinutes = eventMoment.hour() * 60 + eventMoment.minute(); - - for (let i = 0; i < times.length; i++) { - const [h, m] = times[i].split(':').map(Number); - const timeMinutes = h * 60 + m; - const currentDiff = Math.abs(timeMinutes - eventMinutes); - const nearestDiff = Math.abs( - (times[nearestIdx].split(':').map(Number)[0] * 60 + times[nearestIdx].split(':').map(Number)[1]) - eventMinutes - ); - if (currentDiff < nearestDiff) { - nearestIdx = i; - } - } - - eventMarkLineData = [{ - name: '事件发生', - xAxis: nearestIdx, - label: { - formatter: '事件发生', - position: 'middle', - color: '#FFD700', - fontSize: 12 - }, - lineStyle: { - color: '#FFD700', - type: 'solid', - width: 2 - } - }]; - } - } - - return { - title: { - text: `${stock.stock_name || stock.stock_code} - 分时图`, - left: 'center', - textStyle: { fontSize: 16, fontWeight: 'bold' } - }, - tooltip: { - trigger: 'axis', - axisPointer: { type: 'cross' }, - formatter: function(params) { - if (!params || params.length === 0) return ''; - const point = params[0]; - const idx = point.dataIndex; - const priceChangePercent = ((prices[idx] - prevClose) / prevClose * 100); - const avgChangePercent = ((avgPrices[idx] - prevClose) / prevClose * 100); - const priceColor = priceChangePercent >= 0 ? '#ef5350' : '#26a69a'; - const avgColor = avgChangePercent >= 0 ? '#ef5350' : '#26a69a'; - - return `时间:${times[idx]}
现价:¥${prices[idx]?.toFixed(2)} (${priceChangePercent >= 0 ? '+' : ''}${priceChangePercent.toFixed(2)}%)
均价:¥${avgPrices[idx]?.toFixed(2)} (${avgChangePercent >= 0 ? '+' : ''}${avgChangePercent.toFixed(2)}%)
昨收:¥${prevClose?.toFixed(2)}
成交量:${Math.round(volumes[idx]/100)}手`; - } - }, - grid: [ - { left: '10%', right: '10%', height: '60%', top: '15%' }, - { left: '10%', right: '10%', top: '80%', height: '15%' } - ], - xAxis: [ - { type: 'category', data: times, gridIndex: 0, boundaryGap: false }, - { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false } } - ], - yAxis: [ - { - type: 'value', - gridIndex: 0, - scale: false, - position: 'left', - axisLabel: { - formatter: function(value) { - return (value >= 0 ? '+' : '') + value.toFixed(2) + '%'; - } - }, - splitLine: { - show: true, - lineStyle: { - color: '#f0f0f0' - } - } - }, - { - type: 'value', - gridIndex: 0, - scale: false, - position: 'right', - axisLabel: { - formatter: function(value) { - return (value >= 0 ? '+' : '') + value.toFixed(2) + '%'; - } - } - }, - { type: 'value', gridIndex: 1, scale: true, axisLabel: { formatter: v => Math.round(v/100) + '手' } } - ], - dataZoom: [ - { type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 }, - { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%' } - ], - series: [ - { - name: '分时价', - type: 'line', - xAxisIndex: 0, - yAxisIndex: 0, - data: changePercentData, - smooth: true, - showSymbol: false, - lineStyle: { color: lineColor, width: 2 }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' }, - { offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)' } - ]) - }, - markLine: { - symbol: 'none', - data: [ - // 昨收盘价基准线 (0%) - { - yAxis: 0, - lineStyle: { - color: '#666', - type: 'dashed', - width: 1.5, - opacity: 0.8 - }, - label: { - show: true, - formatter: '昨收盘价', - position: 'insideEndTop', - color: '#666', - fontSize: 12 - } - }, - ...eventMarkLineData - ], - animation: false - } - }, - { - name: '均价线', - type: 'line', - xAxisIndex: 0, - yAxisIndex: 1, - data: avgChangePercentData, - smooth: true, - showSymbol: false, - lineStyle: { color: '#FFA500', width: 1 } - }, - { - name: '成交量', - type: 'bar', - xAxisIndex: 1, - yAxisIndex: 2, - data: volumes, - itemStyle: { color: '#b0c4de', opacity: 0.6 } - } - ] - }; - } - - // 日K线 - if (type === 'daily') { - const dates = stockData.map(item => item.time || item.date); - const klineData = stockData.map(item => [item.open, item.close, item.low, item.high]); - const volumes = stockData.map(item => item.volume); - - // 计算事件标记线位置(重要修复) - let eventMarkLineData = []; - if (originalEventTime && dates.length > 0) { - const eventMoment = dayjs(originalEventTime); - const eventDate = eventMoment.format('YYYY-MM-DD'); - - // 找到事件发生日期或最接近的交易日 - let targetIndex = -1; - - // 1. 先尝试找到完全匹配的日期 - targetIndex = dates.findIndex(date => date === eventDate); - - // 2. 如果没有完全匹配,找到第一个大于等于事件日期的交易日 - if (targetIndex === -1) { - for (let i = 0; i < dates.length; i++) { - if (dates[i] >= eventDate) { - targetIndex = i; - break; - } - } - } - - // 3. 如果事件日期晚于所有交易日,则标记在最后一个交易日 - if (targetIndex === -1 && eventDate > dates[dates.length - 1]) { - targetIndex = dates.length - 1; - } - - // 4. 如果事件日期早于所有交易日,则标记在第一个交易日 - if (targetIndex === -1 && eventDate < dates[0]) { - targetIndex = 0; - } - - if (targetIndex >= 0) { - let labelText = '事件发生'; - let labelPosition = 'middle'; - - // 根据事件时间和交易日的关系调整标签 - if (eventDate === dates[targetIndex]) { - if (eventMoment.hour() >= 15) { - labelText = '事件发生\n(盘后)'; - } else if (eventMoment.hour() < 9 || (eventMoment.hour() === 9 && eventMoment.minute() < 30)) { - labelText = '事件发生\n(盘前)'; - } - } else if (eventDate < dates[targetIndex]) { - labelText = '事件发生\n(前一日)'; - labelPosition = 'start'; - } else { - labelText = '事件发生\n(影响日)'; - labelPosition = 'end'; - } - - eventMarkLineData = [{ - name: '事件发生', - xAxis: targetIndex, - label: { - formatter: labelText, - position: labelPosition, - color: '#FFD700', - fontSize: 12, - backgroundColor: 'rgba(0,0,0,0.5)', - padding: [4, 8], - borderRadius: 4 - }, - lineStyle: { - color: '#FFD700', - type: 'solid', - width: 2 - } - }]; - } - } - - return { - title: { - text: `${stock.stock_name || stock.stock_code} - 日K线`, - left: 'center', - textStyle: { fontSize: 16, fontWeight: 'bold' } - }, - tooltip: { - trigger: 'axis', - axisPointer: { type: 'cross' }, - formatter: function(params) { - if (!params || params.length === 0) return ''; - const kline = params[0]; - const volume = params[1]; - if (!kline || !kline.data) return ''; - - let tooltipHtml = `日期: ${kline.axisValue}
开盘: ¥${kline.data[0]}
收盘: ¥${kline.data[1]}
最低: ¥${kline.data[2]}
最高: ¥${kline.data[3]}`; - - if (volume && volume.data) { - tooltipHtml += `
成交量: ${Math.round(volume.data/100)}手`; - } - - return tooltipHtml; - } - }, - grid: [ - { left: '10%', right: '10%', height: '60%' }, - { left: '10%', right: '10%', top: '75%', height: '20%' } - ], - xAxis: [ - { type: 'category', data: dates, scale: true, boundaryGap: true, gridIndex: 0 }, - { type: 'category', data: dates, gridIndex: 1, axisLabel: { show: false } } - ], - yAxis: [ - { scale: true, splitArea: { show: true }, gridIndex: 0 }, - { scale: true, gridIndex: 1, axisLabel: { formatter: (value) => Math.round(value/100) + '手' } } - ], - dataZoom: [ - { type: 'inside', xAxisIndex: [0, 1], start: 70, end: 100 }, - { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%', start: 70, end: 100 } - ], - series: [ - { - name: 'K线', - type: 'candlestick', - yAxisIndex: 0, - data: klineData, - markLine: { - symbol: 'none', - data: eventMarkLineData, - animation: false - }, - itemStyle: { - color: '#ef5350', - color0: '#26a69a', - borderColor: '#ef5350', - borderColor0: '#26a69a' - } - }, - { - name: '成交量', - type: 'bar', - xAxisIndex: 1, - yAxisIndex: 1, - data: volumes.map((volume, index) => ({ - value: volume, - itemStyle: { - color: stockData[index].close >= stockData[index].open ? '#ef5350' : '#26a69a' - } - })) - } - ] - }; - } - - return {}; - }; - - if (!stock) return null; - - return ( - - - - - - - - {stock.stock_name || stock.stock_code} ({stock.stock_code}) - 股票详情 - - {chartData && ( - {chartData.trade_date} - )} - - - - - - - - - - {/* 图表区域 */} - - {loading && ( - - - - 加载图表数据... - - - )} -
- - - {/* 关联描述 */} - - - {/* 风险提示 */} - - - - - - - ); -}; - -export default StockChartModal; \ No newline at end of file