refactor(StockOverview): 优化布局与数据展示

- 头部统计卡片从 4 列精简为 3 列,移除冗余下跌家数
- 涨跌家数改为"多空对比"卡片,双色数值 + 进度条
- 各卡片新增环比趋势指示(放量/缩量等)
- 日期选择器移至 HotspotOverview 头部右侧
- 大盘分时图调整至统计卡片上方
- 异动标签支持点击筛选

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-30 15:55:53 +08:00
parent 91d89fb958
commit eaf11713e8
2 changed files with 196 additions and 113 deletions

View File

@@ -3,8 +3,9 @@
* 展示大盘分时走势 + 概念异动标注
*
* 布局设计:
* - 顶部:统计摘要(指数信息 + 异动统计)
* - 顶部:标题 + 日期选择器 + 异动数量
* - 中部:大尺寸分时图(主要展示区域)
* - 下方:统计卡片(指数信息 + 异动统计)
* - 底部:异动列表(横向滚动卡片)
*/
import React, { useState, useCallback } from 'react';
@@ -39,6 +40,7 @@ import {
import { useHotspotData } from './hooks';
import { IndexMinuteChart, AlertDetailDrawer } from './components';
import { ALERT_TYPE_CONFIG, getAlertTypeLabel } from './utils/chartHelpers';
import TradeDatePicker from '@components/TradeDatePicker';
import {
glassEffect,
colors,
@@ -192,10 +194,15 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => {
* 热点概览主组件
* @param {Object} props
* @param {Date|null} props.selectedDate - 选中的交易日期
* @param {Function} props.onDateChange - 日期变更回调
* @param {Date} props.minDate - 最小可选日期
* @param {Date} props.maxDate - 最大可选日期
*/
const HotspotOverview = ({ selectedDate }) => {
const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
const [selectedAlert, setSelectedAlert] = useState(null);
const [drawerAlertData, setDrawerAlertData] = useState(null);
// 选中的异动类型过滤器null 表示全部)
const [selectedAlertType, setSelectedAlertType] = useState(null);
// 右边栏抽屉控制
const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure();
@@ -231,6 +238,11 @@ const HotspotOverview = ({ selectedDate }) => {
onDrawerOpen();
}, [onDrawerOpen]);
// 点击异动类型标签 - 切换过滤器
const handleAlertTypeClick = useCallback((type) => {
setSelectedAlertType(prevType => prevType === type ? null : type);
}, []);
// 渲染加载状态 - Glassmorphism 风格
if (loading) {
return (
@@ -335,6 +347,11 @@ const HotspotOverview = ({ selectedDate }) => {
const { index, alerts, alert_summary } = data;
// 根据选中的类型过滤异动列表
const filteredAlerts = selectedAlertType
? alerts.filter(alert => alert.alert_type === selectedAlertType)
: alerts;
// 计算市场颜色
const marketColor = getMarketColor(index?.change_pct || 0);
const marketGlow = getMarketGlow(index?.change_pct || 0);
@@ -415,6 +432,18 @@ const HotspotOverview = ({ selectedDate }) => {
</HStack>
<Spacer />
<HStack spacing={3}>
{/* 日期选择器 */}
{onDateChange && (
<TradeDatePicker
value={selectedDate}
onChange={onDateChange}
latestTradeDate={null}
minDate={minDate}
maxDate={maxDate}
isDarkMode={true}
size="sm"
/>
)}
{alerts.length > 0 && (
<HStack
spacing={2}
@@ -441,7 +470,7 @@ const HotspotOverview = ({ selectedDate }) => {
</Text>
</HStack>
)}
<Tooltip label="展示大盘走势与概念异动关联" hasArrow maxW="200px">
<Tooltip label="大盘分时走势与概念异动关联分析" hasArrow maxW="200px">
<Box cursor="help" p={2} borderRadius="full" _hover={{ bg: 'rgba(255,255,255,0.05)' }}>
<Icon as={Info} color={subTextColor} boxSize={4} />
</Box>
@@ -449,7 +478,65 @@ const HotspotOverview = ({ selectedDate }) => {
</HStack>
</Flex>
{/* 统计摘要 - Glassmorphism Bento Grid */}
{/* 大尺寸分时图 - Glassmorphism(移到统计卡片前面) */}
<Box
bg={sectionBg}
backdropFilter={glassEffect.light.backdropFilter}
borderRadius="20px"
border={glassEffect.light.border}
p={5}
mb={5}
position="relative"
overflow="hidden"
>
{/* 图表区域背景光晕 */}
<Box
position="absolute"
bottom="-50px"
left="50%"
transform="translateX(-50%)"
w="60%"
h="100px"
borderRadius="full"
bg="rgba(139, 92, 246, 0.08)"
filter="blur(40px)"
pointerEvents="none"
/>
<HStack spacing={3} mb={4}>
<Box
p={2}
borderRadius="12px"
bg="rgba(139, 92, 246, 0.15)"
border="1px solid rgba(139, 92, 246, 0.25)"
>
<Icon
as={LineChart}
boxSize={5}
color={colors.accent.purple}
css={css`filter: drop-shadow(0 0 6px #8b5cf6);`}
/>
</Box>
<Text
fontSize="sm"
fontWeight="bold"
color={textColor}
css={css`text-shadow: 0 0 20px rgba(139, 92, 246, 0.3);`}
>
大盘分时走势
</Text>
<Tooltip label="图表上的标记点表示概念异动时刻,点击可查看详情" hasArrow>
<Icon as={Info} boxSize={3.5} color={colors.text.muted} cursor="help" />
</Tooltip>
</HStack>
<IndexMinuteChart
indexData={index}
alerts={alerts}
onAlertClick={handleChartAlertClick}
height="420px"
/>
</Box>
{/* 统计摘要 - Glassmorphism Bento Grid移到分时图后面 */}
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4} mb={5}>
{/* 指数信息卡片 */}
<Box
@@ -546,15 +633,31 @@ const HotspotOverview = ({ selectedDate }) => {
}}
>
<HStack justify="space-between" mb={3}>
<Text fontSize="sm" fontWeight="bold" color={textColor}>今日异动</Text>
<Text
fontSize="sm"
color={colors.accent.purple}
fontWeight="bold"
css={css`text-shadow: 0 0 10px rgba(139, 92, 246, 0.5);`}
>
{alerts.length}
</Text>
<HStack spacing={2}>
<Text fontSize="sm" fontWeight="bold" color={textColor}>今日异动</Text>
<Text fontSize="xs" color={colors.text.muted}>点击筛选</Text>
</HStack>
<HStack spacing={2}>
{selectedAlertType && (
<Text
fontSize="xs"
color={colors.accent.purple}
cursor="pointer"
onClick={() => setSelectedAlertType(null)}
_hover={{ textDecoration: 'underline' }}
>
清除筛选
</Text>
)}
<Text
fontSize="sm"
color={colors.accent.purple}
fontWeight="bold"
css={css`text-shadow: 0 0 10px rgba(139, 92, 246, 0.5);`}
>
{selectedAlertType ? `${filteredAlerts.length}/${alerts.length}` : alerts.length}
</Text>
</HStack>
</HStack>
<Flex gap={2} flexWrap="wrap">
{Object.entries(alert_summary || {})
@@ -563,6 +666,7 @@ const HotspotOverview = ({ selectedDate }) => {
.map(([type, count]) => {
const config = ALERT_TYPE_CONFIG[type];
if (!config) return null;
const isSelected = selectedAlertType === type;
return (
<HStack
key={type}
@@ -570,15 +674,20 @@ const HotspotOverview = ({ selectedDate }) => {
px={3}
py={1.5}
borderRadius="full"
bg={`${config.color}15`}
border={`1px solid ${config.color}25`}
bg={isSelected ? `${config.color}35` : `${config.color}15`}
border={isSelected ? `2px solid ${config.color}` : `1px solid ${config.color}25`}
cursor="pointer"
transition="all 0.2s"
transform={isSelected ? 'scale(1.05)' : 'scale(1)'}
boxShadow={isSelected ? `0 0 20px ${config.color}40` : 'none'}
onClick={() => handleAlertTypeClick(type)}
_hover={{
bg: `${config.color}25`,
boxShadow: `0 0 15px ${config.color}30`,
transform: 'scale(1.02)',
}}
>
<Text fontSize="xs" color={config.color}>{config.label}</Text>
<Text fontSize="xs" color={config.color} fontWeight={isSelected ? 'bold' : 'medium'}>{config.label}</Text>
<Text
fontSize="xs"
fontWeight="bold"
@@ -594,64 +703,6 @@ const HotspotOverview = ({ selectedDate }) => {
</Box>
</SimpleGrid>
{/* 大尺寸分时图 - Glassmorphism */}
<Box
bg={sectionBg}
backdropFilter={glassEffect.light.backdropFilter}
borderRadius="20px"
border={glassEffect.light.border}
p={5}
mb={5}
position="relative"
overflow="hidden"
>
{/* 图表区域背景光晕 */}
<Box
position="absolute"
bottom="-50px"
left="50%"
transform="translateX(-50%)"
w="60%"
h="100px"
borderRadius="full"
bg="rgba(139, 92, 246, 0.08)"
filter="blur(40px)"
pointerEvents="none"
/>
<HStack spacing={3} mb={4}>
<Box
p={2}
borderRadius="12px"
bg="rgba(139, 92, 246, 0.15)"
border="1px solid rgba(139, 92, 246, 0.25)"
>
<Icon
as={LineChart}
boxSize={5}
color={colors.accent.purple}
css={css`filter: drop-shadow(0 0 6px #8b5cf6);`}
/>
</Box>
<Text
fontSize="sm"
fontWeight="bold"
color={textColor}
css={css`text-shadow: 0 0 20px rgba(139, 92, 246, 0.3);`}
>
大盘分时走势
</Text>
<Tooltip label="图表上的标记点表示概念异动时刻,点击可查看详情" hasArrow>
<Icon as={Info} boxSize={3.5} color={colors.text.muted} cursor="help" />
</Tooltip>
</HStack>
<IndexMinuteChart
indexData={index}
alerts={alerts}
onAlertClick={handleChartAlertClick}
height="420px"
/>
</Box>
{/* 异动列表 - Glassmorphism 横向滚动 */}
{alerts.length > 0 && (
<Box>
@@ -670,7 +721,11 @@ const HotspotOverview = ({ selectedDate }) => {
/>
</Box>
<Text fontSize="sm" fontWeight="bold" color={textColor}>异动记录</Text>
<Text fontSize="xs" color={colors.text.muted}>点击卡片查看详情</Text>
<Text fontSize="xs" color={colors.text.muted}>
{selectedAlertType
? `(已筛选 ${ALERT_TYPE_CONFIG[selectedAlertType]?.label || selectedAlertType},共 ${filteredAlerts.length} 条)`
: '(点击卡片查看详情)'}
</Text>
</HStack>
{/* 横向滚动卡片 */}
@@ -688,7 +743,7 @@ const HotspotOverview = ({ selectedDate }) => {
}}
>
<HStack spacing={3} pb={1}>
{[...alerts]
{[...filteredAlerts]
.sort((a, b) => (b.time || '').localeCompare(a.time || ''))
.map((alert, idx) => (
<CompactAlertCard
@@ -704,7 +759,7 @@ const HotspotOverview = ({ selectedDate }) => {
)}
{/* 无异动提示 - Glassmorphism */}
{alerts.length === 0 && (
{filteredAlerts.length === 0 && (
<Center
py={12}
bg={sectionBg}
@@ -742,7 +797,22 @@ const HotspotOverview = ({ selectedDate }) => {
css={css`filter: drop-shadow(0 0 10px rgba(139, 92, 246, 0.3));`}
/>
</Box>
<Text color={colors.text.tertiary} fontSize="sm">当日暂无概念异动数据</Text>
<Text color={colors.text.tertiary} fontSize="sm">
{selectedAlertType
? `未找到「${ALERT_TYPE_CONFIG[selectedAlertType]?.label || selectedAlertType}」类型的异动`
: '当日暂无概念异动数据'}
</Text>
{selectedAlertType && (
<Text
fontSize="xs"
color={colors.accent.purple}
cursor="pointer"
onClick={() => setSelectedAlertType(null)}
_hover={{ textDecoration: 'underline' }}
>
查看全部异动
</Text>
)}
</VStack>
</Center>
)}

View File

@@ -48,7 +48,7 @@ import {
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 } from 'lucide-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 ConceptStocksModal from '@components/ConceptStocksModal';
import TradeDatePicker from '@components/TradeDatePicker';
import HotspotOverview from './components/HotspotOverview';
@@ -265,12 +265,14 @@ const StockOverview = () => {
const newStats = {
...(prevStats || {}), // 先保留所有现有字段(包括 rising_count/falling_count
...data.summary, // 然后覆盖 summary 字段
yesterday: data.yesterday, // 保存昨日对比数据
date: data.trade_date
};
return newStats;
});
const newStats = {
...data.summary,
yesterday: data.yesterday,
date: data.trade_date
};
// 日期和可选日期列表由 fetchTopConcepts 统一设置,这里不再设置
@@ -674,42 +676,69 @@ const StockOverview = () => {
onResultSelect: (item, index) => handleSelectStock(item.raw, index),
}}
stats={{
columns: { base: 2, md: 4 },
columns: { base: 1, sm: 3, 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: '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: 'rising',
label: '上涨家数',
value: marketStats?.rising_count,
valueColor: '#ff4d4d',
},
{
key: 'falling',
label: '下跌家数',
value: marketStats?.falling_count,
valueColor: 'green.400',
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 },
},
],
}}
/>
{/* 主内容区 */}
<Box py={10} px={6} position="relative" zIndex={1}>
{/* 日期选择器 */}
<Box mb={6}>
<Flex align="center" gap={4} flexWrap="wrap">
<TradeDatePicker
value={selectedDate}
onChange={(date) => {
{/* 主内容区 - 负 margin 使卡片向上浮动,与 Hero 产生重叠纵深感 */}
<Box pt={6} pb={10} px={6} mt={-6} position="relative" zIndex={2}>
{/* 热点概览 - 大盘走势 + 概念异动 */}
{/* 只在 selectedDate 确定后渲染,避免 null → 日期 的双重请求 */}
<Box mb={10}>
{selectedDate ? (
<HotspotOverview
selectedDate={selectedDate}
onDateChange={(date) => {
const dateStr = date.toISOString().split('T')[0];
const previousDateStr = selectedDate ? selectedDate.toISOString().split('T')[0] : null;
trackDateChanged(dateStr, previousDateStr);
@@ -718,25 +747,9 @@ const StockOverview = () => {
fetchMarketStats(dateStr);
fetchTopConcepts(dateStr);
}}
latestTradeDate={null}
minDate={tradingDays.length > 0 ? new Date(tradingDays[0]) : undefined}
maxDate={tradingDays.length > 0 ? new Date(tradingDays[tradingDays.length - 1]) : undefined}
label="交易日期"
isDarkMode={true}
/>
</Flex>
{selectedDate && (
<Text fontSize="sm" color={subTextColor} mt={2}>
当前显示 {selectedDate.toISOString().split('T')[0]} 的市场数据
</Text>
)}
</Box>
{/* 热点概览 - 大盘走势 + 概念异动 */}
{/* 只在 selectedDate 确定后渲染,避免 null → 日期 的双重请求 */}
<Box mb={10}>
{selectedDate ? (
<HotspotOverview selectedDate={selectedDate} />
) : (
<Card
bg={cardBg}