From e37a8875f88274662efd844d9d2cafd0f55b55ad Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 31 Dec 2025 19:02:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(HotspotOverview):=20=E5=BC=82=E5=8A=A8?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=B8=83=E5=B1=80=E4=BC=98=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=E8=82=A1=E7=A5=A8=E5=88=97=E8=A1=A8=E5=B1=95=E5=BC=80=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompactAlertCard 布局重构: - 未选中:时间 + 概念名称/标签 + 评分(左)/α(右) - 选中:增加板块均涨/涨跌家数 + V2指标(确认率/Z-Score/成交额/动量) - 新增 StockListPanel 组件,选中卡片后展开显示相关股票列表 - 修复卡片点击高度闪烁问题(固定 minH + flexShrink) - 股票列表支持点击跳转到公司详情页 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/AlertDetailDrawer.js | 444 ++++-------- .../components/AlertFilterSection.js | 80 ++- .../HotspotOverview/components/index.js | 2 +- .../components/HotspotOverview/index.js | 542 ++++++++++---- src/views/StockOverview/index.js | 660 +----------------- 5 files changed, 625 insertions(+), 1103 deletions(-) diff --git a/src/views/StockOverview/components/HotspotOverview/components/AlertDetailDrawer.js b/src/views/StockOverview/components/HotspotOverview/components/AlertDetailDrawer.js index 6c1145d0..7c43f9e5 100644 --- a/src/views/StockOverview/components/HotspotOverview/components/AlertDetailDrawer.js +++ b/src/views/StockOverview/components/HotspotOverview/components/AlertDetailDrawer.js @@ -14,375 +14,168 @@ import { VStack, HStack, Text, - Badge, Icon, Collapse, Spinner, - Tooltip, - Flex, - Popover, - PopoverTrigger, - PopoverContent, - PopoverBody, - Portal, } from '@chakra-ui/react'; -import { keyframes, css } from '@emotion/react'; +import { css } from '@emotion/react'; import { GLASS_BLUR } from '@/constants/glassConfig'; import { Clock, Zap, - TrendingUp, - TrendingDown, - ChevronDown, - ChevronRight, - BarChart3, Flame, - Target, - Activity, - Rocket, - Waves, - Gauge, Sparkles, - ExternalLink, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import { getApiBase } from '@utils/apiConfig'; -import { colors, glassEffect } from '../../../theme/glassTheme'; +import { colors } from '../../../theme/glassTheme'; import { ALERT_TYPE_CONFIG, - getAlertTypeLabel, - getAlertTypeDescription, - getScoreColor, - formatScore, } from '../utils/chartHelpers'; -import MiniTimelineChart from '@components/Charts/Stock/MiniTimelineChart'; - -// 动画 -const pulseGlow = keyframes` - 0%, 100% { opacity: 0.6; } - 50% { opacity: 1; } -`; /** - * 获取异动类型图标 - */ -const getAlertIcon = (alertType) => { - const iconMap = { - surge_up: TrendingUp, - surge: Zap, - surge_down: TrendingDown, - volume_surge_up: Activity, - shrink_surge_up: Rocket, - volume_oscillation: Waves, - limit_up: Flame, - rank_jump: Target, - volume_spike: BarChart3, - }; - return iconMap[alertType] || Zap; -}; - -/** - * 单个异动详情卡片 + * 单个异动详情卡片(精简版 - 只显示统计和股票列表) */ const AlertDetailCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => { const navigate = useNavigate(); const alertConfig = ALERT_TYPE_CONFIG[alert.alert_type] || ALERT_TYPE_CONFIG.surge; - const isUp = alert.alert_type !== 'surge_down'; - const AlertIcon = getAlertIcon(alert.alert_type); const handleStockClick = (e, stockCode) => { e.stopPropagation(); navigate(`/company?scode=${stockCode}`); }; - const handleConceptClick = (e) => { - e.stopPropagation(); - if (alert.concept_id) { - navigate(`/concept/${alert.concept_id}`); - } + // 计算统计信息 + const getStockStats = () => { + if (!stocks || stocks.length === 0) return null; + const validStocks = stocks.filter(s => s.change_pct != null && !isNaN(s.change_pct)); + if (validStocks.length === 0) return null; + const avgChange = validStocks.reduce((sum, s) => sum + s.change_pct, 0) / validStocks.length; + const upCount = validStocks.filter(s => s.change_pct > 0).length; + const downCount = validStocks.filter(s => s.change_pct < 0).length; + return { avgChange, upCount, downCount }; }; return ( - - {/* 顶部渐变条 */} + /* 展开内容 - 只显示统计和股票列表(去除重复字段) */ + - - {/* 主内容区 - 可点击展开 */} - - {/* 第一行:展开箭头 + 概念名称 + 评分 */} - - - - - - - - - + + 加载相关股票... + + ) : stocks && stocks.length > 0 ? ( + + {/* 统计信息栏 */} + {(() => { + const stats = getStockStats(); + if (!stats) return null; + const { avgChange, upCount, downCount } = stats; + return ( + - {alert.concept_name} - - - - - - - - - {alert.time} - - - - - {/* 评分 */} - - - - {formatScore(alert.final_score)} - - - - - - {/* 第二行:类型标签 + Alpha + 其他指标 */} - - - - {getAlertTypeLabel(alert.alert_type)} - - - - {alert.alpha != null && ( - - Alpha - = 0 ? colors.market.up : colors.market.down} - > - {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(2)}% - - - )} - - {(alert.limit_up_ratio || 0) > 0.03 && ( - - - - {Math.round(alert.limit_up_ratio * 100)}% - - - )} - - {alert.is_v2 && alert.confirm_ratio != null && ( - - 确认 - = 0.8 ? '#52c41a' : alert.confirm_ratio >= 0.6 ? '#faad14' : '#ff4d4f'} - > - {Math.round(alert.confirm_ratio * 100)}% - - - )} - - - - {/* 展开内容 - 相关股票 */} - - - {loadingStocks ? ( - - - 加载相关股票... - - ) : stocks && stocks.length > 0 ? ( - - {/* 统计信息 */} - {(() => { - const validStocks = stocks.filter(s => s.change_pct != null && !isNaN(s.change_pct)); - if (validStocks.length === 0) return null; - const avgChange = validStocks.reduce((sum, s) => sum + s.change_pct, 0) / validStocks.length; - const upCount = validStocks.filter(s => s.change_pct > 0).length; - const downCount = validStocks.filter(s => s.change_pct < 0).length; - return ( - - - 均涨: - = 0 ? colors.market.up : colors.market.down}> - {avgChange >= 0 ? '+' : ''}{avgChange.toFixed(2)}% + + 均涨: + = 0 ? colors.market.up : colors.market.down}> + {avgChange >= 0 ? '+' : ''}{avgChange.toFixed(2)}% + + + + {upCount}涨 + / + {downCount}跌 + + {(alert.limit_up_ratio || 0) > 0.03 && ( + + + 涨停比 + + {Math.round(alert.limit_up_ratio * 100)}% - - {upCount}涨 - / - {downCount}跌 - - - ); - })()} + )} + + ); + })()} - {/* 股票列表 */} - - - {stocks.slice(0, 15).map((stock, idx) => { - const changePct = stock.change_pct; - const hasChange = changePct != null && !isNaN(changePct); - const stockCode = stock.code || stock.stock_code; - const stockName = stock.name || stock.stock_name || '-'; + {/* 股票列表 */} + + + {stocks.slice(0, 15).map((stock, idx) => { + const changePct = stock.change_pct; + const hasChange = changePct != null && !isNaN(changePct); + const stockCode = stock.code || stock.stock_code; + const stockName = stock.name || stock.stock_name || '-'; - return ( - handleStockClick(e, stockCode)} - _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} - transition="background 0.15s" - justify="space-between" - > - - {/* 股票名称 - 带迷你分时图悬停 */} - - - - {stockName} - - - - e.stopPropagation()} - > - - - {stockName} 分时走势 - - - - - - - - - - {stockCode} - - + return ( + handleStockClick(e, stockCode)} + _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} + transition="background 0.15s" + justify="space-between" + > + 0 ? colors.market.up : - hasChange && changePct < 0 ? colors.market.down : - colors.text.muted - } + color="#60a5fa" + fontWeight="medium" + _hover={{ color: '#93c5fd', textDecoration: 'underline' }} > - {hasChange ? `${changePct > 0 ? '+' : ''}${changePct.toFixed(2)}%` : '-'} + {stockName} + + + {stockCode} - ); - })} - - + 0 ? colors.market.up : + hasChange && changePct < 0 ? colors.market.down : + colors.text.muted + } + > + {hasChange ? `${changePct > 0 ? '+' : ''}${changePct.toFixed(2)}%` : '-'} + + + ); + })} + + - {stocks.length > 15 && ( - - 共 {stocks.length} 只相关股票,显示前 15 只 - - )} - - ) : ( - - 暂无相关股票数据 - - )} - - - + {stocks.length > 15 && ( + + 共 {stocks.length} 只相关股票,显示前 15 只 + + )} + + ) : ( + + 暂无相关股票数据 + + )} + + ); }; @@ -551,3 +344,6 @@ const AlertDetailDrawer = ({ isOpen, onClose, alertData }) => { }; export default AlertDetailDrawer; + +// 导出 AlertDetailCard 以便在其他地方复用 +export { AlertDetailCard }; diff --git a/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js b/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js index 34f6197b..a629aeaa 100644 --- a/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js +++ b/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js @@ -6,7 +6,7 @@ import React, { memo } from 'react'; import { Flex, HStack, Text, Icon } from '@chakra-ui/react'; import { css } from '@emotion/react'; -import { Zap } from 'lucide-react'; +import { Zap, X } from 'lucide-react'; import { ALERT_TYPE_CONFIG } from '../utils/chartHelpers'; import TradeDatePicker from '@components/TradeDatePicker'; @@ -74,7 +74,7 @@ const AlertCountBadge = memo(({ totalCount, filteredCount, selectedAlertType }) color={colors.accent.purple} css={css`text-shadow: 0 0 8px rgba(139, 92, 246, 0.5);`} > - {selectedAlertType ? `${filteredCount}/${totalCount}` : totalCount} + {filteredCount}/{totalCount} )); @@ -95,6 +95,10 @@ const AlertFilterSection = ({ onDateChange, minDate, maxDate, + // 分时图点击的时间段 + chartClickedTimeRange, + // 清除时间段筛选 + onClearTimeRange, }) => { return ( + {/* 时间段标签 - 点击分时图后显示 */} + {chartClickedTimeRange && ( + + + {chartClickedTimeRange} + + { + e.stopPropagation(); + onClearTimeRange?.(); + }} + /> + + )} + + {/* 全部按钮 - 放在最前面 */} + + + 全部 + + + {totalCount} + + + {/* 筛选标签 */} {Object.entries(alertSummary || {}) .filter(([_, count]) => count > 0) @@ -121,19 +184,6 @@ const AlertFilterSection = ({ ); })} - {/* 清除筛选按钮 */} - {selectedAlertType && ( - - 清除筛选 - - )} - {/* 异动总数徽章 */} { +const CompactAlertCard = ({ alert, onClick, isSelected, stocks, loadingStocks }) => { const config = ALERT_TYPE_CONFIG[alert.alert_type] || ALERT_TYPE_CONFIG.surge; const isUp = alert.alert_type !== 'surge_down'; + // 计算统计信息(选中时) + const stats = React.useMemo(() => { + if (!isSelected || !stocks || stocks.length === 0) return null; + const validStocks = stocks.filter(s => s.change_pct != null && !isNaN(s.change_pct)); + if (validStocks.length === 0) return null; + const avgChange = validStocks.reduce((sum, s) => sum + s.change_pct, 0) / validStocks.length; + const upCount = validStocks.filter(s => s.change_pct > 0).length; + const downCount = validStocks.filter(s => s.change_pct < 0).length; + return { avgChange, upCount, downCount }; + }, [isSelected, stocks]); + return ( { borderRadius="16px" border={isSelected ? `1px solid ${config.color}60` : glassEffect.light.border} p={3} - minW="180px" - maxW="200px" + minW={isSelected ? "340px" : "180px"} + maxW={isSelected ? "420px" : "200px"} + minH="80px" cursor="pointer" onClick={() => onClick?.(alert)} transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" position="relative" overflow="hidden" + flexShrink={0} _hover={{ bg: 'rgba(255, 255, 255, 0.05)', border: `1px solid ${config.color}50`, @@ -123,45 +138,65 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => { /> )} - {/* 时间 + 类型 */} - - - {alert.time} - - - - - {getAlertTypeLabel(alert.alert_type)} - - - - - {/* 概念名称 */} - - {alert.concept_name} + {/* 第一行:时间 */} + + {alert.time} - {/* 分数 + Alpha */} - + {/* 第二行:概念名称 + 类型标签 + (选中时显示板块统计) */} + + + + {alert.concept_name} + + + + + {getAlertTypeLabel(alert.alert_type)} + + + + + {/* 选中时显示板块统计 */} + {isSelected && ( + loadingStocks ? ( + + ) : stats ? ( + + 板块均涨: + = 0 ? colors.market.up : colors.market.down}> + {stats.avgChange >= 0 ? '+' : ''}{stats.avgChange.toFixed(2)}% + + {stats.upCount}涨 + / + {stats.downCount}跌 + + ) : null + )} + + + {/* 第三行:评分 + Alpha + (选中时显示更多V2指标) */} + + {/* 左侧:评分 */} 评分 { {Math.round(alert.final_score || 0)} - {alert.alpha != null && ( - - α {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(1)}% + + {/* 右侧:Alpha + (选中时更多V2指标) */} + + {alert.alpha != null && ( + + α + + {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(1)}% + + + )} + + {/* 选中时显示更多V2指标 */} + {isSelected && alert.is_v2 && ( + <> + {alert.confirm_ratio != null && ( + + 确认率 + = 0.8 ? '#52c41a' : + alert.confirm_ratio >= 0.6 ? '#faad14' : '#ff4d4f' + } + > + {Math.round(alert.confirm_ratio * 100)}% + + + )} + {alert.alpha_zscore != null && ( + + Z-Score + = 0 ? colors.market.up : colors.market.down} + > + {alert.alpha_zscore >= 0 ? '+' : ''}{alert.alpha_zscore.toFixed(1)}σ + + + )} + {alert.amt_zscore != null && alert.amt_zscore > 0.5 && ( + + 成交额 + = 2 ? '#eb2f96' : '#faad14'} + > + {alert.amt_zscore.toFixed(1)}σ + + + )} + {alert.momentum_3m != null && Math.abs(alert.momentum_3m) > 0.3 && ( + + 动量 + = 0 ? colors.market.up : colors.market.down} + > + {alert.momentum_3m >= 0 ? '+' : ''}{alert.momentum_3m.toFixed(2)} + + + )} + + )} + + + + ); +}; + +/** + * 股票列表面板 - 选中卡片后展开显示 + */ +const StockListPanel = ({ stocks, loading, alert }) => { + const navigate = useNavigate(); + + const handleStockClick = (stockCode) => { + navigate(`/company?scode=${stockCode}`); + }; + + if (loading) { + return ( + + + + 加载相关股票... + + + ); + } + + if (!stocks || stocks.length === 0) { + return ( + + + 暂无相关股票数据 + + + ); + } + + // 计算统计 + const validStocks = stocks.filter(s => s.change_pct != null && !isNaN(s.change_pct)); + const avgChange = validStocks.length > 0 + ? validStocks.reduce((sum, s) => sum + s.change_pct, 0) / validStocks.length + : 0; + const upCount = validStocks.filter(s => s.change_pct > 0).length; + const downCount = validStocks.filter(s => s.change_pct < 0).length; + + return ( + + {/* 统计信息栏 */} + + + 均涨: + = 0 ? colors.market.up : colors.market.down}> + {avgChange >= 0 ? '+' : ''}{avgChange.toFixed(2)}% + + + {upCount}涨 + / + {downCount}跌 + + {(alert?.limit_up_ratio || 0) > 0.03 && ( + + + 涨停比 + + {Math.round(alert.limit_up_ratio * 100)}% + + )} + + {/* 股票列表 */} + + + {stocks.slice(0, 15).map((stock, idx) => { + const changePct = stock.change_pct; + const hasChange = changePct != null && !isNaN(changePct); + const stockCode = stock.code || stock.stock_code; + const stockName = stock.name || stock.stock_name || '-'; + + return ( + handleStockClick(stockCode)} + _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} + transition="background 0.15s" + justify="space-between" + > + + + {stockName} + + + {stockCode} + + + 0 ? colors.market.up : + hasChange && changePct < 0 ? colors.market.down : + colors.text.muted + } + > + {hasChange ? `${changePct > 0 ? '+' : ''}${changePct.toFixed(2)}%` : '-'} + + + ); + })} + + + + {stocks.length > 15 && ( + + 共 {stocks.length} 只相关股票,显示前 15 只 + + )} ); }; @@ -199,10 +444,17 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataL const [drawerAlertData, setDrawerAlertData] = useState(null); // 选中的异动类型过滤器(null 表示全部) const [selectedAlertType, setSelectedAlertType] = useState(null); + // 分时图点击的数据(用于筛选异动记录) + const [chartClickedData, setChartClickedData] = useState(null); - // 右边栏抽屉控制 + // 右边栏抽屉控制(仅用于分时图点击) const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure(); + // 内联展开状态管理 + const [expandedAlertId, setExpandedAlertId] = useState(null); + const [conceptStocksMap, setConceptStocksMap] = useState({}); + const [loadingStocksMap, setLoadingStocksMap] = useState({}); + // 获取数据 const { loading, refreshing, error, data } = useHotspotData(selectedDate); @@ -220,25 +472,68 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataL const sectionBg = glassEffect.light.bg; const scrollbarColor = 'rgba(139, 92, 246, 0.3)'; - // 点击分时图上的异动标注 - 打开右边栏抽屉显示详情 + // 点击分时图上的异动标注 - 筛选异动记录并滚动 const handleChartAlertClick = useCallback((alertGroupData) => { // alertGroupData 包含 { alerts, timeRange, alertCount, time } - setDrawerAlertData(alertGroupData); - onDrawerOpen(); - }, [onDrawerOpen]); - - // 点击底部异动卡片 - 打开右边栏抽屉显示单个异动详情 - const handleCardAlertClick = useCallback((alert) => { - setSelectedAlert(alert); - // 构造单个异动的数据格式 - setDrawerAlertData({ - alerts: [alert], - timeRange: alert.time, - alertCount: 1, - time: alert.time, + console.log('Chart clicked:', alertGroupData); + setChartClickedData({ + timeRange: alertGroupData.timeRange, + alerts: alertGroupData.alerts || [], + alertCount: alertGroupData.alertCount, }); - onDrawerOpen(); - }, [onDrawerOpen]); + // 滚动到异动记录区域 + setTimeout(() => { + const alertRecordSection = document.getElementById('alert-record-section'); + if (alertRecordSection) { + alertRecordSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 100); + }, []); + + // 获取概念相关股票 + const fetchConceptStocks = useCallback(async (conceptId) => { + console.log('[HotspotOverview] fetchConceptStocks called:', { conceptId, alreadyLoaded: !!conceptStocksMap[conceptId], loading: !!loadingStocksMap[conceptId] }); + if (loadingStocksMap[conceptId] || conceptStocksMap[conceptId]) return; + + setLoadingStocksMap(prev => ({ ...prev, [conceptId]: true })); + try { + const url = `${getApiBase()}/api/concept/${encodeURIComponent(conceptId)}/stocks`; + console.log('[HotspotOverview] Fetching stocks from:', url); + const response = await fetch(url); + const data = await response.json(); + console.log('[HotspotOverview] Stocks response:', data); + if (data.success && data.data?.stocks) { + setConceptStocksMap(prev => ({ ...prev, [conceptId]: data.data.stocks })); + } else { + setConceptStocksMap(prev => ({ ...prev, [conceptId]: [] })); + } + } catch (error) { + console.error('获取概念股票失败:', error); + setConceptStocksMap(prev => ({ ...prev, [conceptId]: [] })); + } finally { + setLoadingStocksMap(prev => ({ ...prev, [conceptId]: false })); + } + }, [loadingStocksMap, conceptStocksMap]); + + // 点击底部异动卡片 - 选中/取消选中 + const handleCardAlertClick = useCallback((alert) => { + const alertId = `${alert.concept_id}-${alert.time}`; + console.log('[HotspotOverview] Card clicked:', { alertId, concept_id: alert.concept_id, currentSelected: expandedAlertId }); + + if (expandedAlertId === alertId) { + // 取消选中 + setExpandedAlertId(null); + setSelectedAlert(null); + } else { + // 选中 + setExpandedAlertId(alertId); + setSelectedAlert(alert); + // 加载概念股票(如果尚未加载且不在加载中) + if (alert.concept_id && conceptStocksMap[alert.concept_id] === undefined && !loadingStocksMap[alert.concept_id]) { + fetchConceptStocks(alert.concept_id); + } + } + }, [expandedAlertId, conceptStocksMap, loadingStocksMap, fetchConceptStocks]); // 点击异动类型标签 - 切换过滤器 const handleAlertTypeClick = useCallback((type) => { @@ -390,8 +685,9 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataL /> - {/* 头部 - Glassmorphism */} - + {/* 头部 - 标题 + 筛选区同一行 */} + + {/* 左侧:标题 */} - 热点概览 + 概念异动监控 - 实时概念异动监控 + 大盘分时 · 实时追踪 + + {/* 右侧:筛选区 */} + { + acc[alert.alert_type] = (acc[alert.alert_type] || 0) + 1; + return acc; + }, {}) : alert_summary} + selectedAlertType={selectedAlertType} + onAlertTypeClick={handleAlertTypeClick} + onClearFilter={() => { + setSelectedAlertType(null); + setChartClickedData(null); + }} + totalCount={chartClickedData ? chartClickedData.alertCount : alerts.length} + filteredCount={chartClickedData ? + (selectedAlertType ? + (chartClickedData.alerts || []).filter(a => a.alert_type === selectedAlertType).length : + chartClickedData.alertCount) : + filteredAlerts.length} + selectedDate={selectedDate} + onDateChange={onDateChange} + minDate={minDate} + maxDate={maxDate} + chartClickedTimeRange={chartClickedData?.timeRange} + onClearTimeRange={() => setChartClickedData(null)} + /> {/* 大尺寸分时图 - Glassmorphism */} @@ -455,49 +779,6 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataL filter="blur(40px)" pointerEvents="none" /> - {/* 标题行:大盘分时走势 + 筛选区 */} - - - - - - - 大盘分时走势 - - - - - - - {/* 筛选区内联 */} - setSelectedAlertType(null)} - totalCount={alerts.length} - filteredCount={filteredAlerts.length} - selectedDate={selectedDate} - onDateChange={onDateChange} - minDate={minDate} - maxDate={maxDate} - /> - 0 && ( - + 异动记录 - - {selectedAlertType - ? `(已筛选 ${ALERT_TYPE_CONFIG[selectedAlertType]?.label || selectedAlertType},共 ${filteredAlerts.length} 条)` - : '(点击卡片查看详情)'} - + {chartClickedData ? ( + + ({chartClickedData.timeRange}) + + ) : ( + + (点击卡片查看详情) + + )} {/* 横向滚动卡片 */} @@ -563,7 +848,7 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataL }} > - {[...filteredAlerts] + {(chartClickedData ? chartClickedData.alerts : [...filteredAlerts]) .sort((a, b) => (b.time || '').localeCompare(a.time || '')) .map((alert, idx) => ( ))} + + {/* 选中卡片后展开的股票列表 */} + + {selectedAlert && ( + + )} + )} diff --git a/src/views/StockOverview/index.js b/src/views/StockOverview/index.js index d41aaab2..d5f5007f 100644 --- a/src/views/StockOverview/index.js +++ b/src/views/StockOverview/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { getApiBase } from '@utils/apiConfig'; import { @@ -6,32 +6,25 @@ import { Heading, Text, Button, - SimpleGrid, Card, CardBody, VStack, HStack, - Badge, Flex, Spacer, Icon, - useToast, Spinner, Center, - Divider, - Tooltip, - Tag, - TagLabel, Skeleton, SkeletonText, + useToast, } from '@chakra-ui/react'; -import { TrendingUp, Info, ChevronRight, Flame, Rocket, ArrowUp, ArrowDown, BarChart2, Layers, Zap, Wallet, Banknote, Scale } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; import ConceptStocksModal from '@components/ConceptStocksModal'; import TradeDatePicker from '@components/TradeDatePicker'; import HotspotOverview from './components/HotspotOverview'; import FlexScreen from './components/FlexScreen'; -import { HeroSection } from '@components/HeroSection'; -import { echarts } from '@lib/echarts'; +import StockOverviewHeader from './components/StockOverviewHeader'; import { logger } from '../../utils/logger'; import { getConceptHtmlUrl } from '../../utils/textUtils'; import tradingDays from '../../data/tradingDays.json'; @@ -47,8 +40,6 @@ const StockOverview = () => { const navigate = useNavigate(); const toast = useToast(); const colorMode = 'dark'; // 固定为 dark 深色模式 - const heatmapRef = useRef(null); - const heatmapChart = useRef(null); // 🎯 事件追踪 Hook const { @@ -180,15 +171,12 @@ const StockOverview = () => { : 0 })); - // 日期由 fetchTopConcepts 统一设置,这里不再设置 logger.debug('StockOverview', '热力图数据加载成功', { count: data.data?.length || 0, date: data.trade_date, limitUpCount, limitDownCount }); - // 延迟渲染热力图,确保DOM已经准备好 - setTimeout(() => renderHeatmap(data.data), 100); } } catch (error) { logger.error('StockOverview', 'fetchHeatmapData', error, { date }); @@ -234,221 +222,6 @@ const StockOverview = () => { } }; - // 渲染热力图 - const renderHeatmap = useCallback((data) => { - if (!heatmapRef.current || !data || !data.length) return; - - try { - // 初始化或获取ECharts实例 - if (!heatmapChart.current) { - heatmapChart.current = echarts.init(heatmapRef.current, colorMode === 'dark' ? 'dark' : null); - } - - // 按市值分组 - const groupedData = {}; - data.forEach(item => { - const capRange = getMarketCapRange(item.market_cap); - if (!groupedData[capRange]) { - groupedData[capRange] = []; - } - groupedData[capRange].push(item); - }); - - // 构建树图数据 - 修复格式问题 - const treeData = Object.entries(groupedData).map(([range, stocks]) => ({ - name: range, - children: stocks.map(stock => { - const change = stock.change_percent || 0; - let color = colorMode === 'dark' ? '#333333' : '#9ca3af'; // 默认灰色 - - if (change > 0) { - const intensity = Math.min(change / 10, 1); - if (colorMode === 'dark') { - // 夜间模式:红色带金色调 - color = `rgba(255, 77, 77, ${0.4 + intensity * 0.6})`; - } else { - color = `rgba(239, 68, 68, ${0.3 + intensity * 0.7})`; - } - } else if (change < 0) { - const intensity = Math.min(Math.abs(change) / 10, 1); - if (colorMode === 'dark') { - // 夜间模式:绿色带暗色调 - color = `rgba(34, 197, 94, ${0.3 + intensity * 0.5})`; - } else { - color = `rgba(34, 197, 94, ${0.3 + intensity * 0.7})`; - } - } - - return { - name: stock.stock_name, - value: Math.abs(stock.market_cap), - change: stock.change_percent, - code: stock.stock_code, - amount: stock.amount, - industry: stock.industry, - province: stock.province, - itemStyle: { - color: color - } - }; - }) - })); - - const option = { - backgroundColor: colorMode === 'dark' ? '#0a0a0a' : 'transparent', - tooltip: { - backgroundColor: colorMode === 'dark' ? '#1a1a1a' : 'white', - borderColor: colorMode === 'dark' ? goldColor : '#ccc', - borderWidth: colorMode === 'dark' ? 2 : 1, - textStyle: { - color: colorMode === 'dark' ? 'white' : '#333' - }, - formatter: function(info) { - const data = info.data; - const isDark = colorMode === 'dark'; - // 如果是父节点(市值分组) - if (data.children) { - return ` -
-
${data.name}
-
包含 ${data.children.length} 只股票
-
总市值: ${data.children.reduce((sum, item) => sum + item.value, 0).toFixed(2)} 亿元
-
- `; - } - // 个股详情 - return ` -
-
${data.name}
-
代码: ${data.code || '-'}
-
涨跌幅: - ${data.change > 0 ? '+' : ''}${data.change?.toFixed(2) || 0}% -
-
市值: ${data.value?.toFixed(2) || 0} 亿元
-
成交额: ${data.amount?.toFixed(2) || 0} 亿元
-
行业: ${data.industry || '未知'}
-
地区: ${data.province || '未知'}
-
- `; - } - }, - series: [{ - name: 'A股市场', - type: 'treemap', - data: treeData, - leafDepth: 1, - roam: false, - breadcrumb: { - show: true, - top: 10, - left: 10, - itemStyle: { - color: colorMode === 'dark' ? '#1a1a2e' : '#f0f0f0', - borderColor: colorMode === 'dark' ? goldColor : '#ccc', - borderWidth: 1, - shadowBlur: colorMode === 'dark' ? 5 : 0, - shadowColor: colorMode === 'dark' ? `${goldColor}40` : 'transparent', - textStyle: { - color: colorMode === 'dark' ? goldColor : '#333' - } - }, - emphasis: { - itemStyle: { - color: colorMode === 'dark' ? goldColor : '#e0e0e0', - textStyle: { - color: colorMode === 'dark' ? '#0a0a0a' : '#333' - } - } - } - }, - - levels: [ - { - itemStyle: { - borderColor: colorMode === 'dark' ? '#1a1a1a' : '#fff', - borderWidth: 3, - gapWidth: 3 - } - }, - { - itemStyle: { - borderColor: colorMode === 'dark' ? '#0a0a0a' : '#fff', - borderWidth: 1, - gapWidth: 1 - } - } - ], - itemStyle: { - borderColor: colorMode === 'dark' ? '#0a0a0a' : '#fff', - borderWidth: 1 - }, - label: { - show: true, - formatter: function(params) { - const data = params.data; - // 父节点(市值分组)显示名称 - if (data.children) { - return params.name; - } - // 子节点(个股)根据市值大小决定是否显示 - return data.value > 5 ? data.name : ''; - }, - fontSize: 12, - color: function(params) { - if (colorMode === 'dark') { - // 夜间模式:根据背景色调整文字颜色 - const change = params.data.change || 0; - if (Math.abs(change) > 5) { - return 'white'; - } - return '#ccc'; - } - return '#333'; - } - } - }] - }; - - // 设置配置项 - heatmapChart.current.setOption(option); - - // 先移除之前的点击事件,避免重复绑定 - heatmapChart.current.off('click'); - - // 添加点击事件 - heatmapChart.current.on('click', function(params) { - // 只有点击个股(有code的节点)才跳转 - if (params.data && params.data.code && !params.data.children) { - const stock = { - code: params.data.code, - name: params.data.name, - change_percent: params.data.change - }; - const marketCapRange = getMarketCapRange(params.data.value); - - // 🎯 追踪热力图股票点击 - trackHeatmapStockClicked(stock, marketCapRange); - - navigate(`/company?scode=${params.data.code}`); - } - }); - } catch (error) { - logger.error('StockOverview', 'renderHeatmap', error, { - dataLength: data?.length || 0 - }); - // ❌ 移除热力图渲染失败 toast(非关键操作) - } - }, [colorMode, goldColor, navigate, trackHeatmapStockClicked]); // ✅ 添加追踪函数依赖 - - // 获取市值区间 - const getMarketCapRange = (cap) => { - if (cap >= 1000) return '超大盘股(>1000亿)'; - if (cap >= 500) return '大盘股(500-1000亿)'; - if (cap >= 100) return '中盘股(100-500亿)'; - if (cap >= 50) return '小盘股(50-100亿)'; - return '微盘股(<50亿)'; - }; - // 查看概念详情(模仿概念中心:打开对应HTML页) const handleConceptClick = (concept, rank = 0) => { // 🎯 追踪概念点击 @@ -477,35 +250,8 @@ const StockOverview = () => { fetchTopConcepts(); fetchHeatmapData(); fetchMarketStats(); - - // 监听窗口大小变化,重新渲染热力图 - const handleResize = () => { - if (heatmapChart.current) { - heatmapChart.current.resize(); - } - }; - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - if (heatmapChart.current) { - heatmapChart.current.dispose(); - } - }; }, []); - // 监听colorMode和heatmapData变化,重新渲染热力图 - useEffect(() => { - if (heatmapData.length > 0) { - // 如果已有实例,先销毁再重新创建 - if (heatmapChart.current) { - heatmapChart.current.dispose(); - heatmapChart.current = null; - } - renderHeatmap(heatmapData); - } - }, [heatmapData, colorMode, renderHeatmap]); - // 概念卡片骨架屏 const ConceptSkeleton = () => ( @@ -542,114 +288,26 @@ const StockOverview = () => { zIndex={0} /> - {/* Hero Section - 使用通用 HeroSection 组件 */} - = 0 ? '+' : ''}${hotspotData.index.change_pct.toFixed(2)}%` - : null, - valueColor: (hotspotData?.index?.change_pct ?? 0) >= 0 ? '#ff4d4d' : '#22c55e', - watermark: { icon: TrendingUp, color: goldColor, opacity: 0.1 }, - }, - { - key: 'limitUpDown', - label: '涨停/跌停', - value: (limitStats.limitUpCount > 0 || limitStats.limitDownCount > 0) - ? `${limitStats.limitUpCount}/${limitStats.limitDownCount}` - : null, - progressBar: (limitStats.limitUpCount > 0 || limitStats.limitDownCount > 0) ? { - value: limitStats.limitUpCount, - total: limitStats.limitDownCount || 1, // 避免除零 - positiveColor: '#ff4d4d', - negativeColor: '#22c55e', - } : undefined, - watermark: { icon: Zap, color: '#ff4d4d', opacity: 0.1 }, - }, - { - key: 'rising', - label: '多空对比', - value: (marketStats?.rising_count && marketStats?.falling_count) - ? `${marketStats.rising_count}/${marketStats.falling_count}` - : null, - progressBar: (marketStats?.rising_count && marketStats?.falling_count) ? { - value: marketStats.rising_count, - total: marketStats.falling_count, - positiveColor: '#ff4d4d', - negativeColor: '#22c55e', - } : undefined, - watermark: { icon: Scale, color: goldColor, opacity: 0.1 }, - }, - // 第二行:辅助指标 - { - key: 'amount', - label: '今日成交额', - value: marketStats ? `${(marketStats.total_amount / 10000).toFixed(1)}万亿` : null, - trend: marketStats?.yesterday?.total_amount ? (() => { - const change = ((marketStats.total_amount - marketStats.yesterday.total_amount) / marketStats.yesterday.total_amount) * 100; - return { - direction: change > 0.01 ? 'up' : change < -0.01 ? 'down' : 'flat', - percent: Math.abs(change), - label: change > 5 ? '放量' : change < -5 ? '缩量' : undefined, - }; - })() : undefined, - watermark: { icon: Banknote, color: goldColor, opacity: 0.1 }, - }, - { - key: 'marketCap', - label: 'A股总市值', - value: marketStats ? `${(marketStats.total_market_cap / 10000).toFixed(1)}万亿` : null, - trend: marketStats?.yesterday?.total_market_cap ? (() => { - const change = ((marketStats.total_market_cap - marketStats.yesterday.total_market_cap) / marketStats.yesterday.total_market_cap) * 100; - return { - direction: change > 0.01 ? 'up' : change < -0.01 ? 'down' : 'flat', - percent: Math.abs(change), - }; - })() : undefined, - watermark: { icon: Wallet, color: goldColor, opacity: 0.1 }, - }, - { - key: 'continuousLimit', - label: '连板龙头', - value: limitStats.limitUpCount > 0 - ? `${limitStats.limitUpCount}只` - : '暂无', - helpText: limitStats.maxContinuousDays > 1 - ? `最高${limitStats.maxContinuousDays}天` - : undefined, - valueColor: '#f59e0b', - watermark: { icon: Rocket, color: '#f59e0b', opacity: 0.1 }, - }, - ], + {/* 新头部组件 - 左右分栏:左侧标题+指标,右侧热力图 */} + { + trackHeatmapStockClicked({ code, name }, ''); + navigate(`/company?scode=${code}`); }} /> {/* 主内容区 - 负 margin 使卡片向上浮动,与 Hero 产生重叠纵深感 */} + {/* 灵活屏 - 实时行情监控 */} + + + + {/* 热点概览 - 大盘走势 + 概念异动 */} {/* 只在 selectedDate 确定后渲染,避免 null → 日期 的双重请求 */} @@ -690,288 +348,8 @@ const StockOverview = () => { )} - {/* 灵活屏 - 实时行情监控 */} - - - {/* 今日热门概念 */} - - - - - 今日热门概念 - - - - - - {loadingConcepts ? ( - - {[...Array(6)].map((_, i) => ( - - ))} - - ) : ( - - {topConcepts.map((concept, index) => ( - handleConceptClick(concept, index)} - position="relative" - overflow="hidden" - > - {/* 排名标签 */} - - TOP {index + 1} - - - {/* 涨跌幅标签 */} - 0 ? '#ff4d4d' : '#22c55e' : 'transparent'} - > - - 0 ? ArrowUp : ArrowDown} - boxSize={3} - /> - {formatChangePercent(concept.change_percent)} - - - - - - {/* 概念名称 */} - - {concept.concept_name} - - - {/* 层级信息 */} - {concept.hierarchy && ( - - - - {[concept.hierarchy.lv1, concept.hierarchy.lv2, concept.hierarchy.lv3] - .filter(Boolean) - .join(' > ')} - - - )} - - {/* 描述 */} - - {concept.description || '暂无描述'} - - - {/* 标签 */} - {concept.tags && concept.tags.length > 0 && ( - - {concept.tags.slice(0, 4).map((tag, idx) => ( - - - {tag} - - ))} - {concept.tags.length > 4 && ( - - +{concept.tags.length - 4} - - )} - - )} - - {/* 爆发日期 */} - {concept.outbreak_dates && concept.outbreak_dates.length > 0 && ( - - - - 近期爆发: {concept.outbreak_dates.slice(0, 2).join(', ')} - {concept.outbreak_dates.length > 2 && ` 等${concept.outbreak_dates.length}次`} - - - )} - - - - {/* 相关股票 */} - handleViewStocks(e, concept)} - _hover={{ bg: hoverBg }} - p={2} - borderRadius="md" - transition="background 0.2s" - > - - 包含 {concept.stock_count} 只个股 - - - {concept.stocks && concept.stocks.length > 0 && ( - - {concept.stocks.slice(0, 5).map((stock, idx) => ( - - {stock.stock_name || stock.name} - - ))} - {concept.stocks.length > 5 && ( - - +{concept.stocks.length - 5} - - )} - - )} - - - - - - - - - ))} - - )} - - - {/* 市值热力图 */} - - - - - 市值热力图 - - - - - - - - - {loadingHeatmap ? ( -
- - - 正在加载热力图数据... - -
- ) : ( - - {/* 图例说明 */} - - - - 上涨 - - - - 平盘 - - - - 下跌 - - - - {/* 热力图容器 */} - - - )} -
-
-
{/* 个股列表弹窗 */}