diff --git a/src/views/Community/components/StockDetailPanel.css b/src/views/Community/components/StockDetailPanel.css deleted file mode 100644 index 4b925b89..00000000 --- a/src/views/Community/components/StockDetailPanel.css +++ /dev/null @@ -1,235 +0,0 @@ -/* Drawer root */ -.stock-detail-panel .ant-drawer-body { - padding: 24px 16px; - background: #ffffff; -} - -/* Card common style */ -.stock-detail-panel .ant-card { - border-radius: 8px; -} - -.stock-detail-panel .ant-card-head-title { - font-weight: 600; -} - -/* Stock list items */ -.stock-item { - cursor: pointer; - transition: background-color 0.2s ease, border-left 0.2s ease; - border-left: 3px solid transparent; - padding-left: 4px; /* compensate for border shift */ -} - -.stock-item:hover { - background-color: #fafafa; -} - -.stock-item.selected { - background-color: #e6f7ff; - border-left-color: #1890ff; -} - -.stock-item .ant-list-item-meta-title { - font-size: 14px; - font-weight: 600; -} - -.stock-item .ant-list-item-meta-description { - font-size: 12px; - line-height: 1.4; -} - -.stock-item .ant-tag { - margin-left: 4px; -} - -/* ReactECharts */ -.stock-detail-panel .echarts-for-react { - width: 100%; -} - -/* Card spacing */ -.stock-detail-panel .ant-card:not(:last-child) { - margin-bottom: 16px; -} - -/* Close icon */ -.stock-detail-panel .anticon-close:hover { - color: #ff4d4f; -} - -.row-hover { - background: #f5faff !important; - box-shadow: 0 2px 8px rgba(24,144,255,0.10); - transition: background 0.2s, box-shadow 0.2s; - z-index: 2; -} - -/* 新增样式 - 相关标的Tab */ -.stock-header { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - padding: 16px; - margin-bottom: 16px; - color: white; -} - -.stock-header-icon { - width: 40px; - height: 40px; - background: rgba(255, 255, 255, 0.2); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; -} - -.stock-search-bar { - background: #f8f9fa; - border-radius: 8px; - padding: 12px; - margin-bottom: 16px; - border: 1px solid #e9ecef; - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; -} - -.stock-search-input { - border-radius: 6px; - border: 1px solid #d9d9d9; - transition: all 0.3s; - max-width: 300px; -} - -.stock-search-input:focus { - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); -} - -.monitoring-button { - background: linear-gradient(135deg, #4caf50 0%, #45a049 100%); - border: none; - border-radius: 6px; - color: white; - font-weight: 500; - transition: all 0.3s; - padding: 4px 12px; - height: auto; - font-size: 12px; -} - -.monitoring-button:hover { - background: linear-gradient(135deg, #45a049 0%, #3d8b40 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); -} - -.monitoring-button.monitoring { - background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); -} - -.monitoring-button.monitoring:hover { - background: linear-gradient(135deg, #ee5a52 0%, #d63031 100%); -} - -.add-stock-button { - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); - border: none; - border-radius: 6px; - color: white; - font-weight: 500; - transition: all 0.3s; - padding: 4px 12px; - height: auto; - font-size: 12px; -} - -.add-stock-button:hover { - background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); -} - -.refresh-button { - border-radius: 6px; - transition: all 0.3s; - padding: 4px 8px; - height: auto; - font-size: 12px; -} - -.refresh-button:hover { - transform: rotate(180deg); - transition: transform 0.5s ease; -} - -.monitoring-status { - background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%); - border: 1px solid #4caf50; - border-radius: 6px; - padding: 8px 12px; - margin-top: 12px; - display: flex; - align-items: center; - gap: 8px; - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.8; } - 100% { opacity: 1; } -} - -.stock-count { - font-size: 14px; - color: rgba(255, 255, 255, 0.8); - margin-top: 4px; -} - -.stock-title { - font-size: 18px; - font-weight: bold; - margin-bottom: 4px; -} - -/* 表格hover效果增强 */ -.ant-table-tbody > tr.row-hover > td { - background: #f5faff !important; - border-color: #91d5ff; -} - -.ant-table-tbody > tr.row-hover:hover > td { - background: #e6f7ff !important; -} - -/* 搜索图标样式 */ -.search-icon { - color: #666; - font-size: 16px; - margin-right: 8px; -} - -/* 按钮组样式 */ -.action-buttons { - display: flex; - gap: 8px; - align-items: center; -} - -/* 响应式设计 */ -@media (max-width: 768px) { - .stock-search-bar { - flex-direction: column; - gap: 12px; - } - - .action-buttons { - width: 100%; - justify-content: space-between; - } -} diff --git a/src/views/Community/components/StockDetailPanel.js b/src/views/Community/components/StockDetailPanel.js deleted file mode 100644 index c626b761..00000000 --- a/src/views/Community/components/StockDetailPanel.js +++ /dev/null @@ -1,346 +0,0 @@ -// src/views/Community/components/StockDetailPanel.js -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import { Drawer, Spin, Button, Alert } from 'antd'; -import { CloseOutlined, LockOutlined, CrownOutlined } from '@ant-design/icons'; -import { Tabs as AntdTabs } from 'antd'; -import dayjs from 'dayjs'; - -// Services and Utils -import { eventService } from '../../../services/eventService'; -import { logger } from '../../../utils/logger'; -import { getApiBase } from '../../../utils/apiConfig'; - -// Custom Hooks -import { useSubscription } from '../../../hooks/useSubscription'; -import { useEventStocks } from './StockDetailPanel/hooks/useEventStocks'; -import { useWatchlist } from './StockDetailPanel/hooks/useWatchlist'; -import { useStockMonitoring } from './StockDetailPanel/hooks/useStockMonitoring'; - -// Components -import { RelatedStocksTab, LockedContent } from './StockDetailPanel/components'; -import RelatedConcepts from '../../EventDetail/components/RelatedConcepts'; -import HistoricalEvents from '../../EventDetail/components/HistoricalEvents'; -import TransmissionChainAnalysis from '../../EventDetail/components/TransmissionChainAnalysis'; -import EventDiscussionModal from './EventDiscussionModal'; -import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeModal'; -import StockChartAntdModal from '../../../components/StockChart/StockChartAntdModal'; -import RiskDisclaimer from '../../../components/RiskDisclaimer'; - -// Styles -import './StockDetailPanel.css'; - -/** - * 股票详情 Drawer 组件 - * 显示事件相关的股票、概念、历史事件、传导链等信息 - * - * @param {boolean} visible - 是否显示 - * @param {Object} event - 事件对象 - * @param {Function} onClose - 关闭回调 - */ -function StockDetailPanel({ visible, event, onClose }) { - logger.debug('StockDetailPanel', '组件加载', { - visible, - eventId: event?.id, - eventTitle: event?.title - }); - - // ==================== Hooks ==================== - - // 权限控制 - const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription(); - - // 事件数据管理 (Redux + Hooks) - const { - stocks, - stocksWithQuotes, - quotes, - eventDetail, - historicalEvents, - chainAnalysis, - expectationScore, - loading, - refreshAllData, - refreshQuotes - } = useEventStocks(event?.id, event?.start_time); - - // 自选股管理(只在 Drawer 可见时加载) - const { - watchlistSet, - toggleWatchlist - } = useWatchlist(visible); - - // 实时监控管理 - const { - isMonitoring, - toggleMonitoring, - manualRefresh: refreshMonitoring - } = useStockMonitoring(stocks, event?.start_time); - - // ==================== Local State ==================== - - const [activeTab, setActiveTab] = useState('stocks'); - const [searchText, setSearchText] = useState(''); - const [filteredStocks, setFilteredStocks] = useState([]); - const [fixedCharts, setFixedCharts] = useState([]); - const [discussionModalVisible, setDiscussionModalVisible] = useState(false); - const [discussionType, setDiscussionType] = useState('事件讨论'); - const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); - const [upgradeFeature, setUpgradeFeature] = useState(''); - - // ==================== Effects ==================== - - // 过滤股票列表 - useEffect(() => { - if (!searchText.trim()) { - setFilteredStocks(stocks); - } else { - const filtered = stocks.filter(stock => - stock.stock_code.toLowerCase().includes(searchText.toLowerCase()) || - stock.stock_name.toLowerCase().includes(searchText.toLowerCase()) - ); - setFilteredStocks(filtered); - } - }, [searchText, stocks]); - - // ==================== Event Handlers ==================== - - // 搜索处理 - const handleSearch = useCallback((value) => { - setSearchText(value); - }, []); - - // 刷新数据 - const handleRefresh = useCallback(() => { - logger.debug('StockDetailPanel', '手动刷新数据'); - refreshAllData(); - refreshQuotes(); - }, [refreshAllData, refreshQuotes]); - - // 切换监控 - const handleMonitoringToggle = useCallback(() => { - toggleMonitoring(); - }, [toggleMonitoring]); - - // 自选股切换 - const handleWatchlistToggle = useCallback(async (stockCode, isInWatchlist) => { - const stockName = stocks.find(s => s.stock_code === stockCode)?.stock_name || ''; - await toggleWatchlist(stockCode, stockName); - }, [stocks, toggleWatchlist]); - - // 行点击 - 显示固定图表 - const handleRowClick = useCallback((stock) => { - setFixedCharts((prev) => { - if (prev.find(item => item.stock.stock_code === stock.stock_code)) return prev; - return [...prev, { stock, chartType: 'timeline' }]; - }); - }, []); - - // 移除固定图表 - const handleUnfixChart = useCallback((stock) => { - setFixedCharts((prev) => prev.filter(item => item.stock.stock_code !== stock.stock_code)); - }, []); - - // 权限检查和升级提示 - const handleUpgradeClick = useCallback((featureName) => { - const recommendation = getUpgradeRecommendation(featureName); - setUpgradeFeature(recommendation?.required || 'pro'); - setUpgradeModalOpen(true); - }, [getUpgradeRecommendation]); - - // 渲染锁定内容 - const renderLockedContent = useCallback((featureName, description) => { - const recommendation = getUpgradeRecommendation(featureName); - const isProRequired = recommendation?.required === 'pro'; - - return ( - handleUpgradeClick(featureName)} - /> - ); - }, [getUpgradeRecommendation, handleUpgradeClick]); - - // 渲染固定图表 - const renderFixedCharts = useMemo(() => { - if (fixedCharts.length === 0) return null; - - const formattedEventTime = event?.start_time - ? dayjs(event.start_time).format('YYYY-MM-DD HH:mm') - : undefined; - - return fixedCharts.map(({ stock }, index) => ( -
- handleUnfixChart(stock)} - stock={stock} - eventTime={formattedEventTime} - fixed={true} - width={800} - /> -
- )); - }, [fixedCharts, event, handleUnfixChart]); - - // ==================== Tab Items ==================== - - const tabItems = useMemo(() => [ - { - key: 'stocks', - label: ( - - 相关标的 - {!hasFeatureAccess('related_stocks') && ( - - )} - - ), - children: hasFeatureAccess('related_stocks') ? ( - { - setDiscussionType('事件讨论'); - setDiscussionModalVisible(true); - }} - fixedChartsContent={renderFixedCharts} - /> - ) : renderLockedContent('related_stocks', '相关标的') - }, - { - key: 'concepts', - label: ( - - 相关概念 - {!hasFeatureAccess('related_concepts') && ( - - )} - - ), - children: hasFeatureAccess('related_concepts') ? ( - - - - ) : renderLockedContent('related_concepts', '相关概念') - }, - { - key: 'historical', - label: ( - - 历史事件对比 - {!hasFeatureAccess('historical_events_full') && ( - - )} - - ), - children: hasFeatureAccess('historical_events_full') ? ( - - - - ) : renderLockedContent('historical_events_full', '历史事件对比') - }, - { - key: 'chain', - label: ( - - 传导链分析 - {!hasFeatureAccess('transmission_chain') && ( - - )} - - ), - children: hasFeatureAccess('transmission_chain') ? ( - - ) : renderLockedContent('transmission_chain', '传导链分析') - } - ], [ - hasFeatureAccess, - filteredStocks, - quotes, - event, - watchlistSet, - searchText, - loading, - isMonitoring, - eventDetail, - historicalEvents, - handleSearch, - handleRefresh, - handleMonitoringToggle, - handleWatchlistToggle, - handleRowClick, - renderFixedCharts, - renderLockedContent - ]); - - // ==================== Render ==================== - - return ( - <> - - {event?.title} - - - } - placement="right" - width={900} - open={visible} - onClose={onClose} - closable={false} - className="stock-detail-panel" - > - - - {/* 风险提示 */} -
- -
-
- - {/* 事件讨论模态框 */} - setDiscussionModalVisible(false)} - eventId={event?.id} - eventTitle={event?.title} - discussionType={discussionType} - /> - - {/* 订阅升级模态框 */} - setUpgradeModalOpen(false)} - requiredLevel={upgradeFeature} - featureName={upgradeFeature === 'pro' ? '相关分析功能' : '传导链分析'} - /> - - ); -} - -export default StockDetailPanel; diff --git a/src/views/Community/components/StockDetailPanel/components/LockedContent.js b/src/views/Community/components/StockDetailPanel/components/LockedContent.js deleted file mode 100644 index c061de74..00000000 --- a/src/views/Community/components/StockDetailPanel/components/LockedContent.js +++ /dev/null @@ -1,48 +0,0 @@ -// src/views/Community/components/StockDetailPanel/components/LockedContent.js -import React from 'react'; -import { Alert, Button } from 'antd'; -import { LockOutlined, CrownOutlined } from '@ant-design/icons'; - -/** - * 权限锁定内容组件 - * 显示功能被锁定的提示,引导用户升级订阅 - * - * @param {string} description - 功能描述 - * @param {boolean} isProRequired - 是否需要 Pro 版本(true: Pro, false: Max) - * @param {string} message - 自定义提示消息(可选) - * @param {Function} onUpgradeClick - 升级按钮点击回调 - * @returns {JSX.Element} - */ -const LockedContent = ({ - description = '此功能', - isProRequired = true, - message = null, - onUpgradeClick -}) => { - const versionName = isProRequired ? 'Pro版' : 'Max版'; - const defaultMessage = `此功能需要${versionName}订阅`; - - return ( -
-
- {isProRequired ? : } -
- - -
- ); -}; - -export default LockedContent; diff --git a/src/views/Community/components/StockDetailPanel/components/RelatedStocksTab.js b/src/views/Community/components/StockDetailPanel/components/RelatedStocksTab.js deleted file mode 100644 index 1cfc7058..00000000 --- a/src/views/Community/components/StockDetailPanel/components/RelatedStocksTab.js +++ /dev/null @@ -1,109 +0,0 @@ -// src/views/Community/components/StockDetailPanel/components/RelatedStocksTab.js -import React from 'react'; -import { Spin, Button } from 'antd'; -import StockSearchBar from './StockSearchBar'; -import StockTable from './StockTable'; - -/** - * 相关标的 Tab 组件 - * 显示事件相关的股票列表、搜索、监控等功能 - * - * @param {Array} stocks - 股票列表 - * @param {Object} quotes - 股票行情字典 - * @param {string} eventTime - 事件时间 - * @param {Set} watchlistSet - 自选股代码集合 - * @param {string} searchText - 搜索文本 - * @param {boolean} loading - 加载状态 - * @param {boolean} isMonitoring - 监控状态 - * @param {Function} onSearch - 搜索回调 - * @param {Function} onRefresh - 刷新回调 - * @param {Function} onMonitoringToggle - 切换监控回调 - * @param {Function} onWatchlistToggle - 切换自选股回调 - * @param {Function} onRowClick - 行点击回调 - * @param {Function} onDiscussionClick - 查看讨论回调 - * @param {React.ReactNode} fixedChartsContent - 固定图表内容(可选) - * @returns {JSX.Element} - */ -const RelatedStocksTab = ({ - stocks = [], - quotes = {}, - eventTime = null, - watchlistSet = new Set(), - searchText = '', - loading = false, - isMonitoring = false, - onSearch, - onRefresh, - onMonitoringToggle, - onWatchlistToggle, - onRowClick, - onDiscussionClick, - fixedChartsContent = null -}) => { - return ( - - {/* 头部信息 */} -
-
-
- 📊 -
-
-
- 相关标的 -
-
- 共 {stocks.length} 只股票 -
-
-
-
- -
- 每5秒自动更新行情数据 -
-
-
- - {/* 搜索和操作栏 */} - - - {/* 股票列表 */} - - - {/* 固定图表 (由父组件传入) */} - {fixedChartsContent} - - {/* 讨论按钮 */} -
- -
-
- ); -}; - -export default RelatedStocksTab; diff --git a/src/views/Community/components/StockDetailPanel/components/StockSearchBar.js b/src/views/Community/components/StockDetailPanel/components/StockSearchBar.js deleted file mode 100644 index 5dbcb592..00000000 --- a/src/views/Community/components/StockDetailPanel/components/StockSearchBar.js +++ /dev/null @@ -1,50 +0,0 @@ -// src/views/Community/components/StockDetailPanel/components/StockSearchBar.js -import React from 'react'; -import { Input, Button } from 'antd'; -import { ReloadOutlined } from '@ant-design/icons'; - -/** - * 股票搜索栏组件 - * 提供股票搜索和刷新功能 - * - * @param {string} searchText - 搜索文本 - * @param {Function} onSearch - 搜索回调函数 - * @param {number} stockCount - 股票总数 - * @param {Function} onRefresh - 刷新回调函数 - * @param {boolean} loading - 加载状态 - * @returns {JSX.Element} - */ -const StockSearchBar = ({ - searchText = '', - onSearch, - stockCount = 0, - onRefresh, - loading = false -}) => { - return ( -
-
- 🔍 - onSearch?.(e.target.value)} - className="stock-search-input" - style={{ flex: 1, maxWidth: '300px' }} - allowClear - /> -
-
-
-
- ); -}; - -export default StockSearchBar; diff --git a/src/views/Community/components/StockDetailPanel/components/StockTable.js b/src/views/Community/components/StockDetailPanel/components/StockTable.js deleted file mode 100644 index b2a9daf4..00000000 --- a/src/views/Community/components/StockDetailPanel/components/StockTable.js +++ /dev/null @@ -1,325 +0,0 @@ -// src/views/Community/components/StockDetailPanel/components/StockTable.js -import React, { useState, useCallback, useMemo, useEffect } from 'react'; -import { Table, Button } from 'antd'; -import { StarFilled, StarOutlined } from '@ant-design/icons'; -import dayjs from 'dayjs'; -import MiniTimelineChart from './MiniTimelineChart'; -import { fetchBatchKlineData, klineDataCache, getCacheKey } from '../utils/klineDataCache'; -import { logger } from '../../../../../utils/logger'; - -/** - * 标准化股票代码为6位格式 - * @param {string} code - 股票代码 - * @returns {string} 6位标准化代码 - */ -const normalizeStockCode = (code) => { - if (!code) return ''; - const s = String(code).trim(); - const m = s.match(/(\d{6})/); - return m ? m[1] : s; -}; - -/** - * 股票列表表格组件 - * 显示事件相关股票列表,包括分时图、涨跌幅、自选股操作等 - * - * @param {Array} stocks - 股票列表 - * @param {Object} quotes - 股票行情字典 { [stockCode]: quote } - * @param {string} eventTime - 事件时间 - * @param {Set} watchlistSet - 自选股代码集合 - * @param {Function} onWatchlistToggle - 切换自选股回调 (stockCode, isInWatchlist) => void - * @param {Function} onRowClick - 行点击回调 (stock) => void - * @returns {JSX.Element} - */ -const StockTable = ({ - stocks = [], - quotes = {}, - eventTime = null, - watchlistSet = new Set(), - onWatchlistToggle, - onRowClick -}) => { - // 展开/收缩的行 - const [expandedRows, setExpandedRows] = useState(new Set()); - // K线数据状态:{ [stockCode]: data[] } - const [klineDataMap, setKlineDataMap] = useState({}); - const [klineLoading, setKlineLoading] = useState(false); - // 用于追踪当前正在加载的 stocksKey,解决时序问题 - const [loadingStocksKey, setLoadingStocksKey] = useState(''); - - // 稳定的事件时间,避免重复渲染 - const stableEventTime = useMemo(() => { - return eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : ''; - }, [eventTime]); - - // 批量加载K线数据 - // 使用 stocks 的 JSON 字符串作为依赖项的 key,避免引用变化导致重复加载 - const stocksKey = useMemo(() => { - return stocks.map(s => s.stock_code).sort().join(','); - }, [stocks]); - - // 计算是否应该显示 loading:当前 stocksKey 和 loadingStocksKey 不匹配,或者正在加载 - // 这样可以在 stocks 变化时立即显示 loading,不需要等 useEffect - const shouldShowLoading = useMemo(() => { - if (stocks.length === 0) return false; - // 如果 stocksKey 变化了但 klineDataMap 还没更新,说明需要加载 - const currentDataKeys = Object.keys(klineDataMap).sort().join(','); - if (stocksKey !== currentDataKeys && stocksKey !== loadingStocksKey) { - return true; - } - return klineLoading; - }, [stocks.length, stocksKey, klineDataMap, loadingStocksKey, klineLoading]); - - useEffect(() => { - if (stocks.length === 0) { - setKlineDataMap({}); - setKlineLoading(false); - setLoadingStocksKey(''); - return; - } - - // 立即设置 loading 状态和正在加载的 key - setKlineLoading(true); - setLoadingStocksKey(stocksKey); - - const stockCodes = stocks.map(s => s.stock_code); - - // 先检查缓存,只请求未缓存的 - const cachedData = {}; - const uncachedCodes = []; - stockCodes.forEach(code => { - const cacheKey = getCacheKey(code, stableEventTime, 'timeline'); - const cached = klineDataCache.get(cacheKey); - if (cached !== undefined) { - cachedData[code] = cached; - } else { - uncachedCodes.push(code); - } - }); - - // 如果全部缓存命中,直接使用 - if (uncachedCodes.length === 0) { - setKlineDataMap(cachedData); - setKlineLoading(false); - logger.debug('StockTable', 'K线数据全部来自缓存', { stockCount: stockCodes.length }); - return; - } - - logger.debug('StockTable', '批量加载K线数据', { - totalCount: stockCodes.length, - cachedCount: Object.keys(cachedData).length, - uncachedCount: uncachedCodes.length, - eventTime: stableEventTime - }); - - // 批量请求未缓存的数据 - fetchBatchKlineData(stockCodes, stableEventTime, 'timeline') - .then((batchData) => { - // 合并缓存数据和新数据 - setKlineDataMap({ ...cachedData, ...batchData }); - setKlineLoading(false); - }) - .catch((error) => { - logger.error('StockTable', '批量加载K线数据失败', error); - // 失败时使用已有的缓存数据 - setKlineDataMap(cachedData); - setKlineLoading(false); - }); - }, [stocksKey, stableEventTime]); // 使用 stocksKey 而非 stocks 对象引用 - - // 切换行展开状态 - const toggleRowExpand = useCallback((stockCode) => { - setExpandedRows(prev => { - const newSet = new Set(prev); - if (newSet.has(stockCode)) { - newSet.delete(stockCode); - } else { - newSet.add(stockCode); - } - return newSet; - }); - }, []); - - // 行点击事件处理 - const handleRowEvents = useCallback((record) => ({ - onClick: () => { - onRowClick?.(record); - }, - style: { cursor: 'pointer' } - }), [onRowClick]); - - // 股票列表列定义 - const stockColumns = useMemo(() => [ - { - title: '股票代码', - dataIndex: 'stock_code', - key: 'stock_code', - width: 100, - render: (code) => ( - - ), - }, - { - title: '股票名称', - dataIndex: 'stock_name', - key: 'stock_name', - width: 120, - }, - { - title: '关联描述', - dataIndex: 'relation_desc', - key: 'relation_desc', - width: 300, - render: (relationDesc, record) => { - logger.debug('StockTable', '表格渲染 - 股票关联描述', { - stockCode: record.stock_code, - hasRelationDesc: !!relationDesc - }); - - // 处理 relation_desc 的两种格式 - let text = ''; - - if (!relationDesc) { - return '--'; - } else if (typeof relationDesc === 'string') { - // 旧格式:直接是字符串 - text = relationDesc; - } else if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) { - // 新格式:{data: [{query_part: "...", sentences: "..."}]} - // 提取所有 query_part,用逗号连接 - text = relationDesc.data - .map(item => item.query_part || item.sentences || '') - .filter(s => s) - .join(';') || '--'; - } else { - logger.warn('StockTable', '未知的 relation_desc 格式', { - stockCode: record.stock_code, - relationDescType: typeof relationDesc - }); - return '--'; - } - - if (!text || text === '--') return '--'; - - const isExpanded = expandedRows.has(record.stock_code); - const maxLength = 30; // 收缩时显示的最大字符数 - const needTruncate = text.length > maxLength; - - return ( -
-
- {isExpanded ? text : (needTruncate ? text.substring(0, maxLength) + '...' : text)} -
- {needTruncate && ( - - )} -
- ); - }, - }, - { - title: '分时图', - key: 'timeline', - width: 150, - render: (_, record) => ( - - ), - }, - { - title: '涨跌幅', - key: 'change', - width: 100, - render: (_, record) => { - const quote = quotes[record.stock_code]; - if (!quote) return '--'; - const color = quote.change > 0 ? 'red' : quote.change < 0 ? 'green' : 'inherit'; - return {quote.change > 0 ? '+' : ''}{quote.change?.toFixed(2)}%; - }, - }, - { - title: '操作', - key: 'action', - width: 150, - fixed: 'right', - render: (_, record) => { - // 标准化代码后再比较,确保 600000.SH 和 600000 能匹配 - const normalizedCode = normalizeStockCode(record.stock_code); - const isInWatchlist = watchlistSet.has(normalizedCode); - return ( -
- - -
- ); - }, - }, - ], [quotes, stableEventTime, expandedRows, toggleRowExpand, watchlistSet, onWatchlistToggle, klineDataMap, shouldShowLoading]); - - return ( -
- - - ); -}; - -export default StockTable; diff --git a/src/views/Community/components/StockDetailPanel/components/index.js b/src/views/Community/components/StockDetailPanel/components/index.js index 841c00c0..938e8b05 100644 --- a/src/views/Community/components/StockDetailPanel/components/index.js +++ b/src/views/Community/components/StockDetailPanel/components/index.js @@ -1,6 +1,2 @@ // src/views/Community/components/StockDetailPanel/components/index.js export { default as MiniTimelineChart } from './MiniTimelineChart'; -export { default as StockSearchBar } from './StockSearchBar'; -export { default as StockTable } from './StockTable'; -export { default as LockedContent } from './LockedContent'; -export { default as RelatedStocksTab } from './RelatedStocksTab'; diff --git a/src/views/Community/components/StockDetailPanel/hooks/useStockMonitoring.js b/src/views/Community/components/StockDetailPanel/hooks/useStockMonitoring.js deleted file mode 100644 index 884a0196..00000000 --- a/src/views/Community/components/StockDetailPanel/hooks/useStockMonitoring.js +++ /dev/null @@ -1,159 +0,0 @@ -// src/views/Community/components/StockDetailPanel/hooks/useStockMonitoring.js -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import { useState, useEffect, useRef, useCallback } from 'react'; -import { fetchStockQuotes } from '../../../../../store/slices/stockSlice'; -import { message } from 'antd'; -import { logger } from '../../../../../utils/logger'; - -/** - * 股票实时监控 Hook - * 提供定时刷新股票行情的功能 - * - * @param {Array} stocks - 股票列表 - * @param {string} eventTime - 事件时间 - * @param {number} interval - 刷新间隔(毫秒),默认 5000ms - * @returns {Object} 监控状态和控制方法 - */ -export const useStockMonitoring = (stocks = [], eventTime = null, interval = 5000) => { - const dispatch = useDispatch(); - const [isMonitoring, setIsMonitoring] = useState(false); - const monitoringIntervalRef = useRef(null); - - // 从 Redux 获取行情数据和加载状态 - const quotes = useSelector(state => state.stock.quotes, shallowEqual); - const quotesLoading = useSelector(state => state.stock.loading.quotes); - - /** - * 执行一次行情更新 - */ - const updateQuotes = useCallback(() => { - if (stocks.length === 0) { - logger.warn('useStockMonitoring', '股票列表为空,跳过更新'); - return; - } - - const codes = stocks.map(s => s.stock_code); - logger.debug('useStockMonitoring', '更新行情数据', { - stockCount: codes.length, - eventTime, - timestamp: new Date().toISOString() - }); - - dispatch(fetchStockQuotes({ codes, eventTime })); - }, [dispatch, stocks, eventTime]); - - /** - * 开启实时监控 - */ - const startMonitoring = useCallback(() => { - if (isMonitoring) { - logger.warn('useStockMonitoring', '监控已经在运行中'); - return; - } - - if (stocks.length === 0) { - message.warning('暂无股票数据,无法开启监控'); - return; - } - - logger.info('useStockMonitoring', '开启实时监控', { - interval, - stockCount: stocks.length - }); - - setIsMonitoring(true); - message.success(`已开启实时监控,每${interval / 1000}秒自动更新`); - - // 立即执行一次 - updateQuotes(); - }, [isMonitoring, stocks, interval, updateQuotes]); - - /** - * 停止实时监控 - */ - const stopMonitoring = useCallback(() => { - if (!isMonitoring) { - return; - } - - logger.info('useStockMonitoring', '停止实时监控'); - - setIsMonitoring(false); - message.info('已停止实时监控'); - }, [isMonitoring]); - - /** - * 切换监控状态 - */ - const toggleMonitoring = useCallback(() => { - if (isMonitoring) { - stopMonitoring(); - } else { - startMonitoring(); - } - }, [isMonitoring, startMonitoring, stopMonitoring]); - - /** - * 手动刷新一次 - */ - const manualRefresh = useCallback(() => { - logger.debug('useStockMonitoring', '手动刷新行情'); - updateQuotes(); - }, [updateQuotes]); - - // 监控定时器效果 - useEffect(() => { - // 清理旧的定时器 - if (monitoringIntervalRef.current) { - clearInterval(monitoringIntervalRef.current); - monitoringIntervalRef.current = null; - } - - if (isMonitoring && stocks.length > 0) { - // 设置定时器 - monitoringIntervalRef.current = setInterval(() => { - updateQuotes(); - }, interval); - - logger.debug('useStockMonitoring', '定时器已设置', { - interval, - stockCount: stocks.length - }); - } - - // 清理函数 - return () => { - if (monitoringIntervalRef.current) { - clearInterval(monitoringIntervalRef.current); - monitoringIntervalRef.current = null; - logger.debug('useStockMonitoring', '定时器已清理'); - } - }; - }, [isMonitoring, stocks.length, interval]); // 注意:不依赖 updateQuotes,避免重复创建定时器 - - // 组件卸载时自动停止监控 - useEffect(() => { - return () => { - if (isMonitoring) { - logger.debug('useStockMonitoring', '组件卸载,自动停止监控'); - setIsMonitoring(false); - } - }; - }, []); // 只在卸载时执行 - - return { - // 状态 - isMonitoring, - quotes, - quotesLoading, - - // 控制方法 - startMonitoring, - stopMonitoring, - toggleMonitoring, - manualRefresh, - - // 工具方法 - setIsMonitoring - }; -}; diff --git a/src/views/Community/components/StockDetailPanel/hooks/useWatchlist.js b/src/views/Community/components/StockDetailPanel/hooks/useWatchlist.js deleted file mode 100644 index 41aa38d7..00000000 --- a/src/views/Community/components/StockDetailPanel/hooks/useWatchlist.js +++ /dev/null @@ -1,163 +0,0 @@ -// src/views/Community/components/StockDetailPanel/hooks/useWatchlist.js -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import { useEffect, useCallback, useMemo } from 'react'; -import { loadWatchlist, toggleWatchlist as toggleWatchlistAction } from '@store/slices/stockSlice'; -import { message } from 'antd'; -import { logger } from '@utils/logger'; - -/** - * 标准化股票代码为6位格式 - * 支持: 600000, 600000.SH, 600000.SZ, SH600000 等格式 - * @param {string} code - 股票代码 - * @returns {string} 6位标准化代码 - */ -const normalizeStockCode = (code) => { - if (!code) return ''; - const s = String(code).trim().toUpperCase(); - // 匹配6位数字(可能带 .SH/.SZ 后缀) - const m1 = s.match(/^(\d{6})(?:\.(?:SH|SZ))?$/i); - if (m1) return m1[1]; - // 匹配 SH/SZ 前缀格式 - const m2 = s.match(/^(?:SH|SZ)(\d{6})$/i); - if (m2) return m2[1]; - // 尝试提取任意6位数字 - const m3 = s.match(/(\d{6})/); - if (m3) return m3[1]; - return s; -}; - -/** - * 自选股管理 Hook - * 封装自选股的加载、添加、移除逻辑 - * - * @param {boolean} shouldLoad - 是否立即加载自选股列表(默认 true) - * @returns {Object} 自选股数据和操作方法 - */ -export const useWatchlist = (shouldLoad = true) => { - const dispatch = useDispatch(); - - // 从 Redux 获取自选股列表 - const watchlistArray = useSelector(state => state.stock.watchlist, shallowEqual); - const loading = useSelector(state => state.stock.loading.watchlist); - - // 转换为 Set 方便快速查询(标准化为6位代码) - // 注意: watchlistArray 现在是 { stock_code, stock_name }[] 格式 - const watchlistSet = useMemo(() => { - return new Set(watchlistArray.map(item => normalizeStockCode(item.stock_code))); - }, [watchlistArray]); - - // 初始化时加载自选股列表(只在 shouldLoad 为 true 时) - useEffect(() => { - if (shouldLoad) { - logger.debug('useWatchlist', '条件加载自选股列表', { shouldLoad }); - dispatch(loadWatchlist()); - } - }, [dispatch, shouldLoad]); - - /** - * 检查股票是否在自选股中(支持带后缀的代码格式) - * @param {string} stockCode - 股票代码(支持 600000, 600000.SH 等格式) - * @returns {boolean} - */ - const isInWatchlist = useCallback((stockCode) => { - const normalized = normalizeStockCode(stockCode); - return watchlistSet.has(normalized); - }, [watchlistSet]); - - /** - * 切换自选股状态 - * @param {string} stockCode - 股票代码(支持带后缀格式,会自动标准化) - * @param {string} stockName - 股票名称 - * @returns {Promise} 操作是否成功 - */ - const toggleWatchlist = useCallback(async (stockCode, stockName) => { - const normalized = normalizeStockCode(stockCode); - const wasInWatchlist = watchlistSet.has(normalized); - - logger.debug('useWatchlist', '切换自选股状态', { - stockCode, - normalized, - stockName, - wasInWatchlist - }); - - try { - // 传递标准化后的6位代码给 Redux action - await dispatch(toggleWatchlistAction({ - stockCode: normalized, - stockName, - isInWatchlist: wasInWatchlist - })).unwrap(); - - message.success(wasInWatchlist ? '已从自选股移除' : '已加入自选股'); - return true; - } catch (error) { - logger.error('useWatchlist', '切换自选股失败', error, { - stockCode, - normalized, - stockName - }); - message.error(error.message || '操作失败,请稍后重试'); - return false; - } - }, [dispatch, watchlistSet]); - - /** - * 批量添加到自选股 - * @param {Array<{code: string, name: string}>} stocks - 股票列表 - * @returns {Promise} 成功添加的数量 - */ - const batchAddToWatchlist = useCallback(async (stocks) => { - logger.debug('useWatchlist', '批量添加自选股', { - count: stocks.length - }); - - let successCount = 0; - const promises = stocks.map(async ({ code, name }) => { - const normalized = normalizeStockCode(code); - if (!watchlistSet.has(normalized)) { - try { - await dispatch(toggleWatchlistAction({ - stockCode: normalized, - stockName: name, - isInWatchlist: false - })).unwrap(); - successCount++; - } catch (error) { - logger.error('useWatchlist', '添加失败', error, { code, normalized, name }); - } - } - }); - - await Promise.all(promises); - - if (successCount > 0) { - message.success(`成功添加 ${successCount} 只股票到自选股`); - } - - return successCount; - }, [dispatch, watchlistSet]); - - /** - * 刷新自选股列表 - */ - const refresh = useCallback(() => { - logger.debug('useWatchlist', '刷新自选股列表'); - dispatch(loadWatchlist()); - }, [dispatch]); - - return { - // 数据 - watchlist: watchlistArray, - watchlistSet, - loading, - - // 查询方法 - isInWatchlist, - - // 操作方法 - toggleWatchlist, - batchAddToWatchlist, - refresh - }; -};