// src/views/EventDetail/components/RelatedStocks.js - 完整修改版本 import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Box, HStack, VStack, Text, Button, Badge, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, useDisclosure, Skeleton, Alert, AlertIcon, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Input, Textarea, FormControl, FormLabel, ButtonGroup, IconButton, Tooltip, useColorModeValue, CircularProgress, CircularProgressLabel, Flex, Spacer, useToast } from '@chakra-ui/react'; import { FaPlus, FaTrash, FaChartLine, FaRedo, FaSearch } from 'react-icons/fa'; import * as echarts from 'echarts'; import StockChartModal from '../../../components/StockChart/StockChartModal'; import { eventService, stockService } from '../../../services/eventService'; import { logger } from '../../../utils/logger'; const RelatedStocks = ({ eventId, eventTime, // 新增:从父组件传递事件时间 stocks = [], loading = false, error = null, onStockAdded = () => {}, onStockDeleted = () => {} }) => { // ==================== 状态管理 ==================== const [stocksData, setStocksData] = useState(stocks); const [quotes, setQuotes] = useState({}); const [quotesLoading, setQuotesLoading] = useState(false); const [selectedStock, setSelectedStock] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [sortField, setSortField] = useState('correlation'); const [sortOrder, setSortOrder] = useState('desc'); // 模态框状态 const { isOpen: isChartModalOpen, onOpen: onChartModalOpen, onClose: onChartModalClose } = useDisclosure(); // 主题和工具 const toast = useToast(); const tableBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.600'); // ==================== 副作用处理 ==================== useEffect(() => { setStocksData(stocks); }, [stocks]); useEffect(() => { if (stocksData.length > 0) { fetchQuotes(); } }, [stocksData, eventTime]); // 添加 eventTime 依赖 // ==================== API 调用函数 ==================== const fetchQuotes = useCallback(async () => { if (stocksData.length === 0) return; try { setQuotesLoading(true); const codes = stocksData.map(stock => stock.stock_code); logger.debug('RelatedStocks', '获取股票报价', { codes, eventTime, stockCount: codes.length }); const response = await stockService.getQuotes(codes, eventTime); logger.debug('RelatedStocks', '股票报价响应', { hasResponse: !!response, quotesCount: response ? Object.keys(response).length : 0 }); setQuotes(response || {}); } catch (err) { logger.error('RelatedStocks', 'fetchQuotes', err, { stockCount: stocksData.length, eventTime }); toast({ title: '获取股票报价失败', description: err.message, status: 'error', duration: 3000, isClosable: true, }); } finally { setQuotesLoading(false); } }, [stocksData, eventTime, toast]); const handleRefreshQuotes = () => { fetchQuotes(); toast({ title: '正在刷新报价...', status: 'info', duration: 2000, isClosable: true, }); }; const handleDeleteStock = async (stockId, stockCode) => { if (!window.confirm(`确定要删除股票 ${stockCode} 吗?`)) { return; } try { await eventService.deleteRelatedStock(stockId); toast({ title: '删除成功', status: 'success', duration: 3000, isClosable: true, }); onStockDeleted(); } catch (err) { toast({ title: '删除失败', description: err.message, status: 'error', duration: 5000, isClosable: true, }); } }; const handleShowChart = (stock) => { setSelectedStock(stock); onChartModalOpen(); }; // ==================== 辅助函数 ==================== const getCorrelationColor = (correlation) => { const value = (correlation || 0) * 100; if (value >= 80) return 'red'; if (value >= 60) return 'orange'; return 'green'; }; const formatPriceChange = (change) => { if (!change && change !== 0) return '--'; const prefix = change > 0 ? '+' : ''; return `${prefix}${change.toFixed(2)}%`; }; const getStockName = (stock) => { // 优先使用API返回的名称 const quote = quotes[stock.stock_code]; if (quote && quote.name) { return quote.name; } // 其次使用数据库中的名称 if (stock.stock_name) { return stock.stock_name; } // 最后使用默认格式 return `股票${stock.stock_code.split('.')[0]}`; }; const getRelationDesc = (relationDesc) => { // 处理空值 if (!relationDesc) return '--'; // 如果是字符串,直接返回 if (typeof relationDesc === 'string') { return relationDesc; } // 如果是对象且包含data数组 if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) { const firstItem = relationDesc.data[0]; if (firstItem) { // 优先使用 query_part,其次使用 sentences return firstItem.query_part || firstItem.sentences || '--'; } } // 其他情况返回默认值 return '--'; }; // ==================== 数据处理 ==================== const filteredAndSortedStocks = stocksData .filter(stock => { const stockName = getStockName(stock); return stock.stock_code.toLowerCase().includes(searchTerm.toLowerCase()) || stockName.toLowerCase().includes(searchTerm.toLowerCase()); }) .sort((a, b) => { let aValue, bValue; switch (sortField) { case 'correlation': aValue = a.correlation || 0; bValue = b.correlation || 0; break; case 'change': aValue = quotes[a.stock_code]?.change || 0; bValue = quotes[b.stock_code]?.change || 0; break; case 'price': aValue = quotes[a.stock_code]?.price || 0; bValue = quotes[b.stock_code]?.price || 0; break; default: aValue = a[sortField] || ''; bValue = b[sortField] || ''; } if (sortOrder === 'asc') { return aValue > bValue ? 1 : -1; } else { return aValue < bValue ? 1 : -1; } }); // ==================== 渲染状态 ==================== if (loading) { return ( {[1, 2, 3].map((i) => ( ))} ); } if (error) { return ( 加载相关股票失败: {error} ); } // ==================== 主要渲染 ==================== return ( <> {/* 工具栏 */} {/* 搜索框 */} setSearchTerm(e.target.value)} size="sm" maxW="200px" /> {/* 操作按钮 */} } size="sm" onClick={handleRefreshQuotes} isLoading={quotesLoading} aria-label="刷新报价" /> {/* 股票表格 */} {filteredAndSortedStocks.length === 0 ? ( ) : ( filteredAndSortedStocks.map((stock) => { const quote = quotes[stock.stock_code] || {}; const correlation = (stock.correlation || 0) * 100; const stockName = getStockName(stock); return ( {/* 股票代码 */} {/* 股票名称 */} {/* 产业链 */} {/* 最新价 */} {/* 涨跌幅 */} {/* 相关度 */} {/* 关联描述 */} {/* 操作 */} ); }) )}
{ if (sortField === 'stock_code') { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField('stock_code'); setSortOrder('asc'); } }}> 股票代码 {sortField === 'stock_code' && (sortOrder === 'asc' ? '↑' : '↓')} 股票名称 产业链 { if (sortField === 'price') { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField('price'); setSortOrder('desc'); } }}> 最新价 {sortField === 'price' && (sortOrder === 'asc' ? '↑' : '↓')} { if (sortField === 'change') { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField('change'); setSortOrder('desc'); } }}> 涨跌幅 {sortField === 'change' && (sortOrder === 'asc' ? '↑' : '↓')} { if (sortField === 'correlation') { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField('correlation'); setSortOrder('desc'); } }}> 相关度 {sortField === 'correlation' && (sortOrder === 'asc' ? '↑' : '↓')} 关联描述 操作
{searchTerm ? '未找到匹配的股票' : '暂无相关股票数据'}
{stockName} {quotesLoading && ( 加载中... )} {stock.sector || '未知'} {quote.price ? `¥${quote.price.toFixed(2)}` : '--'} 0 ? 'red' : quote.change < 0 ? 'green' : 'gray' } size="sm" > {formatPriceChange(quote.change)} {Math.round(correlation)}% {getRelationDesc(stock.relation_desc)} } size="xs" onClick={() => handleShowChart(stock)} aria-label="查看K线图" /> } size="xs" colorScheme="red" variant="ghost" onClick={() => handleDeleteStock(stock.id, stock.stock_code)} aria-label="删除股票" />
{/* 股票图表模态框 */} ); }; // ==================== 子组件 ==================== // 现在使用统一的StockChartModal组件,无需重复代码 export default RelatedStocks;