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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user