diff --git a/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js b/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js
new file mode 100644
index 00000000..34f6197b
--- /dev/null
+++ b/src/views/StockOverview/components/HotspotOverview/components/AlertFilterSection.js
@@ -0,0 +1,162 @@
+/**
+ * AlertFilterSection - 异动筛选区组件(内联版本)
+ * 包含:筛选标签(带数量)+ 异动总数徽章 + 日期选择器
+ * 设计为与标题行内联使用
+ */
+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 { ALERT_TYPE_CONFIG } from '../utils/chartHelpers';
+import TradeDatePicker from '@components/TradeDatePicker';
+import { colors } from '../../../theme/glassTheme';
+
+/**
+ * 筛选标签组件
+ */
+const FilterTag = memo(({ type, count, isSelected, onClick, config }) => (
+ onClick(type)}
+ _hover={{
+ bg: `${config.color}25`,
+ boxShadow: `0 0 10px ${config.color}30`,
+ transform: 'scale(1.02)',
+ }}
+ >
+
+ {config.label}
+
+
+ {count}
+
+
+));
+FilterTag.displayName = 'FilterTag';
+
+/**
+ * 异动总数徽章
+ */
+const AlertCountBadge = memo(({ totalCount, filteredCount, selectedAlertType }) => (
+
+
+
+ {selectedAlertType ? `${filteredCount}/${totalCount}` : totalCount}
+
+
+));
+AlertCountBadge.displayName = 'AlertCountBadge';
+
+/**
+ * AlertFilterSection 主组件 - 内联版本,无外层包裹
+ */
+const AlertFilterSection = ({
+ alertSummary,
+ selectedAlertType,
+ onAlertTypeClick,
+ onClearFilter,
+ totalCount,
+ filteredCount,
+ // 日期选择器相关
+ selectedDate,
+ onDateChange,
+ minDate,
+ maxDate,
+}) => {
+ return (
+
+ {/* 筛选标签 */}
+ {Object.entries(alertSummary || {})
+ .filter(([_, count]) => count > 0)
+ .slice(0, 5)
+ .map(([type, count]) => {
+ const config = ALERT_TYPE_CONFIG[type];
+ if (!config) return null;
+ return (
+
+ );
+ })}
+
+ {/* 清除筛选按钮 */}
+ {selectedAlertType && (
+
+ 清除筛选
+
+ )}
+
+ {/* 异动总数徽章 */}
+
+
+ {/* 日期选择器 */}
+ {onDateChange && (
+
+ )}
+
+ );
+};
+
+export default memo(AlertFilterSection);
diff --git a/src/views/StockOverview/components/HotspotOverview/components/index.js b/src/views/StockOverview/components/HotspotOverview/components/index.js
index 91c8c1a5..1c152b38 100644
--- a/src/views/StockOverview/components/HotspotOverview/components/index.js
+++ b/src/views/StockOverview/components/HotspotOverview/components/index.js
@@ -2,3 +2,4 @@ export { default as IndexMinuteChart } from './IndexMinuteChart';
export { default as ConceptAlertList } from './ConceptAlertList';
export { default as AlertSummary } from './AlertSummary';
export { default as AlertDetailDrawer } from './AlertDetailDrawer';
+export { default as AlertFilterSection } from './AlertFilterSection';
diff --git a/src/views/StockOverview/components/HotspotOverview/index.js b/src/views/StockOverview/components/HotspotOverview/index.js
index b0709970..bd6358c2 100644
--- a/src/views/StockOverview/components/HotspotOverview/index.js
+++ b/src/views/StockOverview/components/HotspotOverview/index.js
@@ -17,11 +17,10 @@ import {
VStack,
Spinner,
Center,
+ Spacer,
Icon,
Flex,
- Spacer,
Tooltip,
- SimpleGrid,
useDisclosure,
} from '@chakra-ui/react';
import { keyframes, css } from '@emotion/react';
@@ -38,15 +37,12 @@ import {
} from 'lucide-react';
import { useHotspotData } from './hooks';
-import { IndexMinuteChart, AlertDetailDrawer } from './components';
+import { IndexMinuteChart, AlertDetailDrawer, AlertFilterSection } from './components';
import { ALERT_TYPE_CONFIG, getAlertTypeLabel } from './utils/chartHelpers';
-import TradeDatePicker from '@components/TradeDatePicker';
import {
glassEffect,
colors,
- glowEffects,
getMarketColor,
- getMarketGlow,
} from '../../theme/glassTheme';
// 动画效果
@@ -198,7 +194,7 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => {
* @param {Date} props.minDate - 最小可选日期
* @param {Date} props.maxDate - 最大可选日期
*/
-const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
+const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate, onDataLoaded }) => {
const [selectedAlert, setSelectedAlert] = useState(null);
const [drawerAlertData, setDrawerAlertData] = useState(null);
// 选中的异动类型过滤器(null 表示全部)
@@ -208,11 +204,17 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure();
// 获取数据
- const { loading, error, data } = useHotspotData(selectedDate);
+ const { loading, refreshing, error, data } = useHotspotData(selectedDate);
+
+ // 当数据加载完成时,通知父组件
+ React.useEffect(() => {
+ if (data && onDataLoaded) {
+ onDataLoaded(data);
+ }
+ }, [data, onDataLoaded]);
// Glassmorphism 颜色主题
const cardBg = glassEffect.card.bg;
- const borderColor = colors.border.primary;
const textColor = colors.text.primary;
const subTextColor = colors.text.secondary;
const sectionBg = glassEffect.light.bg;
@@ -352,9 +354,6 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
? alerts.filter(alert => alert.alert_type === selectedAlertType)
: alerts;
- // 计算市场颜色
- const marketColor = getMarketColor(index?.change_pct || 0);
- const marketGlow = getMarketGlow(index?.change_pct || 0);
return (
{
-
-
- {/* 日期选择器 */}
- {onDateChange && (
-
- )}
- {alerts.length > 0 && (
-
-
-
- {alerts.length}
-
-
- )}
-
-
-
-
-
-
- {/* 大尺寸分时图 - Glassmorphism(移到统计卡片前面) */}
+ {/* 大尺寸分时图 - Glassmorphism */}
{
filter="blur(40px)"
pointerEvents="none"
/>
-
-
-
-
-
- 大盘分时走势
-
-
-
-
-
-
-
-
- {/* 统计摘要 - Glassmorphism Bento Grid(移到分时图后面) */}
-
- {/* 指数信息卡片 */}
-
- {/* 背景光晕 */}
-
-
-
-
- {index?.name || '上证指数'}
-
-
- {index?.latest_price?.toFixed(2) || '-'}
-
-
-
-
- = 0 ? TrendingUp : TrendingDown}
- boxSize={4}
- color={marketColor}
- css={css`filter: drop-shadow(0 0 4px ${marketColor});`}
- />
-
- {(index?.change_pct || 0) >= 0 ? '+' : ''}{(index?.change_pct || 0).toFixed(2)}%
-
-
-
-
- 高
- {index?.high?.toFixed(2)}
-
-
- 低
- {index?.low?.toFixed(2)}
-
-
-
-
-
-
- {/* 异动统计卡片 */}
-
-
-
- 今日异动
- (点击筛选)
-
-
- {selectedAlertType && (
- setSelectedAlertType(null)}
- _hover={{ textDecoration: 'underline' }}
- >
- 清除筛选
-
- )}
-
+
+
+
- {selectedAlertType ? `${filteredAlerts.length}/${alerts.length}` : alerts.length} 次
-
-
+ css={css`filter: drop-shadow(0 0 6px #8b5cf6);`}
+ />
+
+
+ 大盘分时走势
+
+
+
+
-
- {Object.entries(alert_summary || {})
- .filter(([_, count]) => count > 0)
- .slice(0, 5)
- .map(([type, count]) => {
- const config = ALERT_TYPE_CONFIG[type];
- if (!config) return null;
- const isSelected = selectedAlertType === type;
- return (
- handleAlertTypeClick(type)}
- _hover={{
- bg: `${config.color}25`,
- boxShadow: `0 0 15px ${config.color}30`,
- transform: 'scale(1.02)',
- }}
- >
- {config.label}
-
- {count}
-
-
- );
- })}
-
+
+ {/* 筛选区内联 */}
+ setSelectedAlertType(null)}
+ totalCount={alerts.length}
+ filteredCount={filteredAlerts.length}
+ selectedDate={selectedDate}
+ onDateChange={onDateChange}
+ minDate={minDate}
+ maxDate={maxDate}
+ />
+
+
+
+ {/* 刷新时的轻量级加载指示器 */}
+ {refreshing && (
+
+
+
+ )}
-
+
{/* 异动列表 - Glassmorphism 横向滚动 */}
{alerts.length > 0 && (
diff --git a/src/views/StockOverview/index.js b/src/views/StockOverview/index.js
index 4cb1130b..d41aaab2 100644
--- a/src/views/StockOverview/index.js
+++ b/src/views/StockOverview/index.js
@@ -5,24 +5,13 @@ import {
Box,
Heading,
Text,
- Input,
- InputGroup,
- InputLeftElement,
- InputRightElement,
- IconButton,
Button,
SimpleGrid,
Card,
CardBody,
- CardHeader,
VStack,
HStack,
Badge,
- Stat,
- StatLabel,
- StatNumber,
- StatHelpText,
- StatArrow,
Flex,
Spacer,
Icon,
@@ -30,25 +19,13 @@ import {
Spinner,
Center,
Divider,
- List,
- ListItem,
Tooltip,
- Menu,
- MenuButton,
- MenuList,
- MenuItem,
- useDisclosure,
- Image,
- Fade,
- Collapse,
- Stack,
- Progress,
Tag,
TagLabel,
Skeleton,
SkeletonText,
} from '@chakra-ui/react';
-import { Search, X, ArrowRight, TrendingUp, Info, ChevronRight, Calendar, LineChart, Flame, Rocket, Brain, ArrowUp, ArrowDown, BarChart2, Tag as TagIcon, Layers, Zap, Wallet, Banknote, Scale } from 'lucide-react';
+import { TrendingUp, Info, ChevronRight, Flame, Rocket, ArrowUp, ArrowDown, BarChart2, Layers, Zap, Wallet, Banknote, Scale } from 'lucide-react';
import ConceptStocksModal from '@components/ConceptStocksModal';
import TradeDatePicker from '@components/TradeDatePicker';
import HotspotOverview from './components/HotspotOverview';
@@ -76,22 +53,12 @@ const StockOverview = () => {
// 🎯 事件追踪 Hook
const {
trackMarketStatsViewed,
- trackSearchInitiated,
- trackStockSearched,
- trackSearchResultClicked,
trackConceptClicked,
- trackConceptStockClicked,
trackHeatmapStockClicked,
- trackStockDetailViewed,
- trackConceptDetailViewed,
trackDateChanged,
} = useStockOverviewEvents({ navigate });
// 状态管理
- const [searchQuery, setSearchQuery] = useState('');
- const [searchResults, setSearchResults] = useState([]);
- const [isSearching, setIsSearching] = useState(false);
- const [showResults, setShowResults] = useState(false);
const [topConcepts, setTopConcepts] = useState([]);
const [heatmapData, setHeatmapData] = useState([]);
const [loadingConcepts, setLoadingConcepts] = useState(true);
@@ -104,6 +71,17 @@ const StockOverview = () => {
const [isStockModalOpen, setIsStockModalOpen] = useState(false);
const [selectedConcept, setSelectedConcept] = useState(null);
+ // 涨停/跌停统计状态
+ const [limitStats, setLimitStats] = useState({
+ limitUpCount: 0,
+ limitDownCount: 0,
+ continuousLimitCount: 0,
+ maxContinuousDays: 0
+ });
+
+ // HotspotOverview 数据(用于 Hero 区展示大盘涨跌幅和异动数量)
+ const [hotspotData, setHotspotData] = useState(null);
+
// 深色主题配色 - 参考概念中心
const bgColor = '#0a0a0f'; // 深色背景
const cardBg = 'rgba(255, 255, 255, 0.03)'; // 玻璃态卡片背景
@@ -123,59 +101,6 @@ const StockOverview = () => {
setIsStockModalOpen(true);
}, []);
- // 防抖搜索
- const debounceSearch = useCallback(
- (() => {
- let timeoutId;
- return (query) => {
- clearTimeout(timeoutId);
- if (!query.trim()) {
- setSearchResults([]);
- setShowResults(false);
- return;
- }
- timeoutId = setTimeout(() => {
- searchStocks(query);
- }, 300);
- };
- })(),
- []
- );
-
- // 搜索股票
- const searchStocks = async (query) => {
- setIsSearching(true);
- try {
- logger.debug('StockOverview', '开始搜索股票', { query });
- const response = await fetch(`${getApiBase()}/api/stocks/search?q=${encodeURIComponent(query)}&limit=10`);
- const data = await response.json();
- logger.debug('StockOverview', 'API返回数据', {
- status: response.status,
- resultCount: data.data?.length || 0
- });
-
- if (data.success) {
- const results = data.data || [];
- setSearchResults(results);
- setShowResults(true);
-
- // 🎯 追踪搜索查询
- trackStockSearched(query, results.length);
- } else {
- logger.warn('StockOverview', '搜索失败', data.error || '请稍后重试', { query });
- // ❌ 移除搜索失败 toast(非关键操作)
-
- // 🎯 追踪搜索无结果
- trackStockSearched(query, 0);
- }
- } catch (error) {
- logger.error('StockOverview', 'searchStocks', error, { query });
- // ❌ 移除搜索失败 toast(非关键操作)
- } finally {
- setIsSearching(false);
- }
- };
-
// 获取每日涨幅靠前的概念
const fetchTopConcepts = async (date = null) => {
setLoadingConcepts(true);
@@ -237,10 +162,30 @@ const StockOverview = () => {
falling_count: data.statistics.falling_count
}));
}
+
+ // 计算涨停/跌停家数
+ const stocks = data.data || [];
+ const limitUpCount = stocks.filter(s => s.change_percent >= 9.9).length;
+ const limitDownCount = stocks.filter(s => s.change_percent <= -9.9).length;
+ // 计算连板股(简化版:统计涨幅超过 19% 的作为 2 连板估算)
+ const continuousLimitCount = stocks.filter(s => s.change_percent >= 19.8).length;
+ setLimitStats(prev => ({
+ ...prev,
+ limitUpCount,
+ limitDownCount,
+ continuousLimitCount,
+ // 最高连板天数暂时用估算值(涨幅除以10取整)
+ maxContinuousDays: stocks.length > 0
+ ? Math.max(...stocks.map(s => Math.floor((s.change_percent || 0) / 9.9)))
+ : 0
+ }));
+
// 日期由 fetchTopConcepts 统一设置,这里不再设置
logger.debug('StockOverview', '热力图数据加载成功', {
count: data.data?.length || 0,
- date: data.trade_date
+ date: data.trade_date,
+ limitUpCount,
+ limitDownCount
});
// 延迟渲染热力图,确保DOM已经准备好
setTimeout(() => renderHeatmap(data.data), 100);
@@ -504,35 +449,6 @@ const StockOverview = () => {
return '微盘股(<50亿)';
};
- // 处理搜索输入
- const handleSearchChange = (e) => {
- const value = e.target.value;
- setSearchQuery(value);
-
- // 🎯 追踪搜索开始(首次输入时)
- if (value && !searchQuery) {
- trackSearchInitiated();
- }
-
- debounceSearch(value);
- };
-
- // 清空搜索
- const handleClearSearch = () => {
- setSearchQuery('');
- setSearchResults([]);
- setShowResults(false);
- };
-
- // 选择股票
- const handleSelectStock = (stock, index = 0) => {
- // 🎯 追踪搜索结果点击
- trackSearchResultClicked(stock, index);
-
- navigate(`/company?scode=${stock.stock_code}`);
- handleClearSearch();
- };
-
// 查看概念详情(模仿概念中心:打开对应HTML页)
const handleConceptClick = (concept, rank = 0) => {
// 🎯 追踪概念点击
@@ -647,57 +563,52 @@ const StockOverview = () => {
],
},
]}
- search={{
- placeholder: '搜索股票代码、名称或拼音首字母...',
- showSearchButton: true,
- searchButtonText: '搜索',
- maxWidth: '2xl',
- // 受控模式
- value: searchQuery,
- onChange: (value) => {
- setSearchQuery(value);
- if (value && !searchQuery) {
- trackSearchInitiated();
- }
- debounceSearch(value);
- },
- onClear: handleClearSearch,
- results: searchResults.map((stock) => ({
- id: stock.stock_code,
- label: stock.stock_name,
- subLabel: stock.stock_code,
- extra: stock.pinyin_abbr?.toUpperCase(),
- tags: stock.exchange ? [{ text: stock.exchange }] : [],
- raw: stock,
- })),
- isSearching: isSearching,
- showDropdown: showResults,
- onSearch: async () => [],
- onResultSelect: (item, index) => handleSelectStock(item.raw, index),
- }}
stats={{
- columns: { base: 1, sm: 3, md: 3 },
+ columns: { base: 1, sm: 2, md: 3 },
items: [
+ // 第一行:核心指标
{
- 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: 'indexChange',
+ label: '大盘涨跌幅',
+ value: hotspotData?.index?.change_pct != null
+ ? `${hotspotData.index.change_pct >= 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 {
@@ -706,25 +617,32 @@ const StockOverview = () => {
label: change > 5 ? '放量' : change < -5 ? '缩量' : undefined,
};
})() : undefined,
- // 水印背景图标 - 钞票图标
watermark: { icon: Banknote, color: goldColor, opacity: 0.1 },
},
{
- key: 'rising',
- label: '多空对比',
- // 显示为 "上涨/下跌" 格式,使用自定义渲染
- value: (marketStats?.rising_count && marketStats?.falling_count)
- ? `${marketStats.rising_count}/${marketStats.falling_count}`
- : marketStats?.rising_count,
- // 涨跌进度条(不显示底部标签)
- 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: '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 },
},
],
}}
@@ -749,6 +667,7 @@ const StockOverview = () => {
}}
minDate={tradingDays.length > 0 ? new Date(tradingDays[0]) : undefined}
maxDate={tradingDays.length > 0 ? new Date(tradingDays[tradingDays.length - 1]) : undefined}
+ onDataLoaded={setHotspotData}
/>
) : (