From d16938de9ee3fa279182e672338ea18564c1d004 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 5 Jan 2026 13:53:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(LimitAnalyse):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B6=A8=E5=81=9C=E6=83=85=E7=BB=AA=E5=91=A8=E6=9C=9F=E6=97=A5?= =?UTF-8?q?=E5=8E=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 日历展示每日涨停数据,支持日期选择 - 左侧 AI 摘要卡片和核心指标展示 - 左右 3:7 布局,颜色提亮优化 --- .../components/CalendarCell.tsx | 185 +++++++++++++++++ .../components/LeftPanel.tsx | 186 ++++++++++++++++++ .../components/TooltipContent.tsx | 54 +++++ .../components/TrendIcon.tsx | 46 +++++ .../LimitUpEmotionCycle/components/index.ts | 10 + .../LimitUpEmotionCycle/constants/config.ts | 133 +++++++++++++ .../LimitUpEmotionCycle/constants/theme.ts | 47 +++++ .../LimitUpEmotionCycle/hooks/index.ts | 5 + .../hooks/useCalendarData.ts | 186 ++++++++++++++++++ .../components/LimitUpEmotionCycle/index.tsx | 171 ++++++++++++++++ 10 files changed, 1023 insertions(+) create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/CalendarCell.tsx create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/LeftPanel.tsx create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TooltipContent.tsx create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TrendIcon.tsx create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/index.ts create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/config.ts create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/theme.ts create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/index.ts create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/useCalendarData.ts create mode 100644 src/views/LimitAnalyse/components/LimitUpEmotionCycle/index.tsx diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/CalendarCell.tsx b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/CalendarCell.tsx new file mode 100644 index 00000000..9dee860c --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/CalendarCell.tsx @@ -0,0 +1,185 @@ +/** + * 日历格子组件 + * 显示单个日期的涨停信息、趋势、板块等 + */ +import React, { memo } from 'react'; +import { Box, HStack, VStack, Text, Badge, Icon, Tooltip } from '@chakra-ui/react'; +import { Flame } from 'lucide-react'; +import { goldColors, textColors, bgColors } from '../constants/theme'; +import { getHeatColor } from '../constants/config'; +import TrendIcon from './TrendIcon'; +import TooltipContent from './TooltipContent'; +import type { DateData } from './LeftPanel'; + +interface CalendarCellProps { + date: Date | null; + dateData: DateData | null; + previousData: DateData | null; + isSelected: boolean; + isToday: boolean; + isWeekend: boolean; + isFiltered: boolean; + onClick: (date: Date) => void; +} + +// 自定义比较函数,避免不必要的重渲染 +const arePropsEqual = (prev: CalendarCellProps, next: CalendarCellProps): boolean => { + if (prev.date?.getTime() !== next.date?.getTime()) return false; + if (prev.isSelected !== next.isSelected) return false; + if (prev.isToday !== next.isToday) return false; + if (prev.isFiltered !== next.isFiltered) return false; + if (prev.dateData?.count !== next.dateData?.count) return false; + if (prev.previousData?.count !== next.previousData?.count) return false; + return true; +}; + +const CalendarCell: React.FC = memo( + ({ date, dateData, previousData, isSelected, isToday, isWeekend, isFiltered, onClick }) => { + // 空日期占位 + if (!date) { + return ; + } + + const hasData = !!dateData; + const count = dateData?.count || 0; + const heatColors = getHeatColor(count); + const topSector = dateData?.top_sector || ''; + + // 周末无数据显示"休市" + if (isWeekend && !hasData) { + return ( + + + {date.getDate()} + + + 休市 + + + ); + } + + // 被筛选过滤掉的日期 + if (isFiltered) { + return ( + + + {date.getDate()} + + + ); + } + + // 正常日期 + return ( + } + placement="top" + hasArrow + bg={bgColors.card} + border="1px solid rgba(212, 175, 55, 0.3)" + borderRadius="12px" + > + onClick(date)} + w="full" + minH="60px" + > + {/* 今天标记 */} + {isToday && ( + + 今天 + + )} + + + {/* 日期 */} + + {date.getDate()} + + + {/* 涨停数 + 趋势 */} + {hasData && ( + + + {count}家 + + + + )} + + {/* 主要板块 */} + {hasData && topSector && ( + + {count > 80 && } + + {topSector} + + + )} + + + + ); + }, + arePropsEqual +); + +CalendarCell.displayName = 'CalendarCell'; + +export default CalendarCell; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/LeftPanel.tsx b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/LeftPanel.tsx new file mode 100644 index 00000000..edd64a6a --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/LeftPanel.tsx @@ -0,0 +1,186 @@ +/** + * 左侧面板组件 + * 包含:AI 总结卡片、核心指标卡片、快捷筛选器 + */ +import React, { memo, useMemo } from 'react'; +import { Box, HStack, VStack, Text, Badge, Icon, Button } from '@chakra-ui/react'; +import { Sparkles, Target } from 'lucide-react'; +import { css } from '@emotion/react'; +import { goldColors, textColors } from '../constants/theme'; +import { FILTER_BUTTONS, formatDisplayDate, HEAT_LEVELS } from '../constants/config'; + +// 日期数据类型 +export interface DateData { + date: string; + count: number; + top_sector?: string; + fail_rate?: number; +} + +interface LeftPanelProps { + selectedDate: Date | null; + dateData: DateData | null; + previousDateData: DateData | null; + onFilterChange: (filter: string | null) => void; + activeFilter: string | null; +} + +/** + * 生成 AI 情绪总结文案 + */ +const generateEmotionSummary = (count: number | null, topSector: string): string => { + if (!count) return '请选择一个交易日查看分析'; + + const level = HEAT_LEVELS.find((l) => count >= l.threshold); + const emotionMap: Record = { + high: '市场情绪高涨', + medium: '市场情绪正在回暖', + low: '市场情绪温和', + cold: '市场情绪低迷', + }; + + const emotion = emotionMap[level?.key || 'cold'] || '市场情绪低迷'; + return topSector ? `${emotion},主线题材:${topSector}` : `${emotion},关注市场动向`; +}; + +const LeftPanel: React.FC = memo( + ({ selectedDate, dateData, previousDateData, onFilterChange, activeFilter }) => { + // AI 总结 + const aiSummary = useMemo(() => { + const count = dateData?.count || 0; + const topSector = dateData?.top_sector || ''; + return generateEmotionSummary(count, topSector); + }, [dateData]); + + // 计算涨跌变化 + const countChange = useMemo(() => { + if (!dateData || !previousDateData) return null; + return dateData.count - previousDateData.count; + }, [dateData, previousDateData]); + + return ( + + {/* 标题 */} + + + 涨停板块分析 + + + 智能解析板块轮动与市场情绪 + + + + {/* AI 总结卡片 */} + + + + + AI 总结 + + + + {aiSummary} + + + + {/* 核心指标卡片 */} + + + + + 核心指标 + + + + + + + 当前日期: + + + {formatDisplayDate(selectedDate)} + + + + + + 涨停家数: + + + + {dateData?.count || '--'} + + {countChange !== null && ( + = 0 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(34, 197, 94, 0.2)'} + color={countChange >= 0 ? '#ef4444' : '#22c55e'} + fontSize="10px" + > + {countChange >= 0 ? '+' : ''} + {countChange} + + )} + + + + + + 炸板率: + + + {dateData?.fail_rate ? `${dateData.fail_rate}%` : '22%'} + + + + + + {/* 快捷筛选器 */} + + {FILTER_BUTTONS.map(({ key, label, icon: ButtonIcon, activeColor, hoverColor, textColor, borderColor }) => { + const isActive = activeFilter === key; + return ( + + ); + })} + + + ); + } +); + +LeftPanel.displayName = 'LeftPanel'; + +export default LeftPanel; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TooltipContent.tsx b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TooltipContent.tsx new file mode 100644 index 00000000..fff20455 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TooltipContent.tsx @@ -0,0 +1,54 @@ +/** + * 日历格子 Tooltip 内容组件 + * 用于性能优化,避免每次渲染创建新的 JSX + */ +import React, { memo } from 'react'; +import { Box, Text } from '@chakra-ui/react'; +import { goldColors } from '../constants/theme'; + +interface TooltipContentProps { + date: Date; + hasData: boolean; + count: number; + topSector: string; +} + +const TooltipContent: React.FC = memo( + ({ date, hasData, count, topSector }) => ( + + + {date.getFullYear()}年{date.getMonth() + 1}月{date.getDate()}日 + + {hasData ? ( + <> + + 涨停:{' '} + + {count} + {' '} + 只 + + {topSector && ( + + 核心题材:{' '} + + {topSector} + + + )} + + 点击查看详情 + + + ) : ( + + 暂无数据 + + )} + + ) +); + +TooltipContent.displayName = 'TooltipContent'; + +export default TooltipContent; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TrendIcon.tsx b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TrendIcon.tsx new file mode 100644 index 00000000..6add1dff --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/TrendIcon.tsx @@ -0,0 +1,46 @@ +/** + * 趋势图标组件 + * 显示涨停数变化趋势(上涨/下跌/持平) + */ +import React, { memo } from 'react'; +import { HStack, Icon, Text } from '@chakra-ui/react'; +import { TrendingUp, TrendingDown } from 'lucide-react'; +import { textColors } from '../constants/theme'; + +interface TrendIconProps { + current: number | null | undefined; + previous: number | null | undefined; +} + +const TrendIcon: React.FC = memo(({ current, previous }) => { + if (!previous || !current) return null; + + const diff = current - previous; + + // 变化幅度小于等于5,显示持平 + if (Math.abs(diff) <= 5) { + return ( + + → + + ); + } + + const isUp = diff > 0; + const color = isUp ? '#ef4444' : '#22c55e'; + const IconComponent = isUp ? TrendingUp : TrendingDown; + const arrow = isUp ? '↑' : '↓'; + + return ( + + + + {arrow} + + + ); +}); + +TrendIcon.displayName = 'TrendIcon'; + +export default TrendIcon; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/index.ts b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/index.ts new file mode 100644 index 00000000..b7c1d91f --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/components/index.ts @@ -0,0 +1,10 @@ +/** + * 子组件统一导出 + */ +export { default as TrendIcon } from './TrendIcon'; +export { default as TooltipContent } from './TooltipContent'; +export { default as LeftPanel } from './LeftPanel'; +export { default as CalendarCell } from './CalendarCell'; + +// 类型导出 +export type { DateData } from './LeftPanel'; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/config.ts b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/config.ts new file mode 100644 index 00000000..14f3d4a3 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/config.ts @@ -0,0 +1,133 @@ +/** + * LimitUpEmotionCycle 常量配置 + * 热度级别、筛选按钮、日历常量 + */ +import { Flame, Zap, Filter, Snowflake, LucideIcon } from 'lucide-react'; +import { textColors } from './theme'; + +// 热度级别颜色配置 +export interface HeatColors { + bg: string; + text: string; + border: string; +} + +// 热度级别配置 +export interface HeatLevel { + key: string; + threshold: number; + label: string; + icon: LucideIcon; + colors: HeatColors; +} + +// 筛选按钮配置 +export interface FilterButton extends HeatLevel { + activeColor: string; + hoverColor: string; + textColor: string; + borderColor: string; +} + +// 热度级别配置 - 单一配置源 +export const HEAT_LEVELS: HeatLevel[] = [ + { + key: 'high', + threshold: 80, + label: '超级高潮日', + icon: Flame, + colors: { + bg: 'rgba(147, 51, 234, 0.55)', + text: '#d8b4fe', + border: 'rgba(147, 51, 234, 0.65)', + }, + }, + { + key: 'medium', + threshold: 60, + label: '高潮日', + icon: Zap, + colors: { + bg: 'rgba(239, 68, 68, 0.50)', + text: '#fca5a5', + border: 'rgba(239, 68, 68, 0.60)', + }, + }, + { + key: 'low', + threshold: 40, + label: '温和日', + icon: Filter, + colors: { + bg: 'rgba(251, 146, 60, 0.45)', + text: '#fed7aa', + border: 'rgba(251, 146, 60, 0.55)', + }, + }, + { + key: 'cold', + threshold: 0, + label: '偏冷日', + icon: Snowflake, + colors: { + bg: 'rgba(59, 130, 246, 0.35)', + text: '#93c5fd', + border: 'rgba(59, 130, 246, 0.45)', + }, + }, +]; + +// 默认热度颜色(无数据时) +const DEFAULT_HEAT_COLORS: HeatColors = { + bg: 'rgba(60, 60, 70, 0.12)', + text: textColors.muted || 'rgba(255, 255, 255, 0.65)', + border: 'transparent', +}; + +/** + * 根据涨停数获取热度颜色 + */ +export const getHeatColor = (count: number | null | undefined): HeatColors => { + if (!count) return DEFAULT_HEAT_COLORS; + const level = HEAT_LEVELS.find((l) => count >= l.threshold); + return level?.colors || DEFAULT_HEAT_COLORS; +}; + +/** + * 根据涨停数获取热度级别 key + */ +export const getHeatLevelKey = (count: number | null | undefined): string | null => { + if (!count) return null; + const level = HEAT_LEVELS.find((l) => count >= l.threshold); + return level?.key || null; +}; + +// 基于 HEAT_LEVELS 生成筛选按钮配置 +export const FILTER_BUTTONS: FilterButton[] = HEAT_LEVELS.map((level) => ({ + ...level, + label: `${level.label} (${level.threshold > 0 ? `≥${level.threshold}家` : '<40家'})`, + activeColor: level.colors.bg, + hoverColor: level.colors.bg.replace(/[\d.]+\)$/, (m) => `${(parseFloat(m) + 0.1).toFixed(2)})`), + textColor: level.colors.text, + borderColor: level.colors.border, +})); + +// 日历常量 +export const WEEK_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const; +export const MONTH_NAMES = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] as const; + +// 日期格式化工具函数 +export const formatDateStr = (date: Date | null): string => { + if (!date) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}${month}${day}`; +}; + +export const formatDisplayDate = (date: Date | null): string => { + if (!date) return '--'; + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${month}月${day}日`; +}; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/theme.ts b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/theme.ts new file mode 100644 index 00000000..082367d7 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/constants/theme.ts @@ -0,0 +1,47 @@ +/** + * LimitUpEmotionCycle 主题配置 + * 黑金主题色系 + */ +import { keyframes } from '@emotion/react'; + +// 颜色配置类型 +export interface ColorConfig { + primary: string; + light?: string; + dark?: string; + glow?: string; + secondary?: string; + muted?: string; + card?: string; + item?: string; + hover?: string; +} + +// 黑金主题色系 +export const goldColors: ColorConfig = { + primary: '#D4AF37', + light: '#F4D03F', + dark: '#B8860B', + glow: 'rgba(212, 175, 55, 0.4)', +}; + +// 背景色 +export const bgColors: ColorConfig = { + primary: 'rgba(15, 15, 22, 0.95)', + card: 'rgba(15, 15, 22, 0.95)', + item: 'rgba(20, 20, 30, 0.8)', + hover: 'rgba(30, 30, 45, 0.9)', +}; + +// 文字色 - 提亮版本 +export const textColors: ColorConfig = { + primary: '#ffffff', + secondary: 'rgba(255, 255, 255, 0.85)', + muted: 'rgba(255, 255, 255, 0.65)', +}; + +// 动画 - shimmer 效果 +export const shimmer = keyframes` + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +`; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/index.ts b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/index.ts new file mode 100644 index 00000000..30745a75 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/index.ts @@ -0,0 +1,5 @@ +/** + * Hooks 统一导出 + */ +export { default as useCalendarData } from './useCalendarData'; +export type { AvailableDate, CalendarCellData } from './useCalendarData'; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/useCalendarData.ts b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/useCalendarData.ts new file mode 100644 index 00000000..fdf5bcb5 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/hooks/useCalendarData.ts @@ -0,0 +1,186 @@ +/** + * 日历数据处理 Hook + * 负责日历天数生成、日期数据映射、筛选逻辑等 + */ +import { useMemo, useCallback } from 'react'; +import { formatDateStr, HEAT_LEVELS } from '../constants/config'; +import type { DateData } from '../components/LeftPanel'; + +// 可用日期数据类型(从父组件传入) +export interface AvailableDate { + date: string; // YYYYMMDD 格式 + count: number; + top_sector?: string; + fail_rate?: number; + [key: string]: unknown; +} + +// 预计算的日历格子数据 +export interface CalendarCellData { + key: string; + date: Date | null; + dateData: DateData | null; + previousData: DateData | null; + isSelected: boolean; + isToday: boolean; + isWeekend: boolean; + isFiltered: boolean; +} + +interface UseCalendarDataParams { + currentMonth: Date; + availableDates: AvailableDate[] | null; + selectedDate: Date | null; + activeFilter: string | null; +} + +interface UseCalendarDataReturn { + days: (Date | null)[]; + calendarCellsData: CalendarCellData[]; + getDateData: (date: Date | null) => DateData | null; + getPreviousDateData: (date: Date | null) => DateData | null; + selectedDateData: DateData | null; + previousDateData: DateData | null; +} + +export const useCalendarData = ({ + currentMonth, + availableDates, + selectedDate, + activeFilter, +}: UseCalendarDataParams): UseCalendarDataReturn => { + // 生成日历天数 + const days = useMemo(() => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + const startingDayOfWeek = firstDay.getDay(); + + const result: (Date | null)[] = []; + // 前置空白天数 + for (let i = 0; i < startingDayOfWeek; i++) { + result.push(null); + } + // 当月天数 + for (let i = 1; i <= daysInMonth; i++) { + result.push(new Date(year, month, i)); + } + return result; + }, [currentMonth]); + + // 构建日期数据映射表 - 使用 Map 提升查找性能 + const dateDataMap = useMemo(() => { + const map = new Map(); + if (availableDates) { + availableDates.forEach((d) => { + map.set(d.date, { + date: d.date, + count: d.count, + top_sector: d.top_sector, + fail_rate: d.fail_rate, + }); + }); + } + return map; + }, [availableDates]); + + // 获取日期数据 - O(1) 查找 + const getDateData = useCallback( + (date: Date | null): DateData | null => { + if (!date) return null; + return dateDataMap.get(formatDateStr(date)) || null; + }, + [dateDataMap] + ); + + // 获取前一天数据 + const getPreviousDateData = useCallback( + (date: Date | null): DateData | null => { + if (!date) return null; + const prev = new Date(date); + prev.setDate(prev.getDate() - 1); + return getDateData(prev); + }, + [getDateData] + ); + + // 筛选逻辑 + const isDateFiltered = useCallback( + (dateData: DateData | null): boolean => { + if (!activeFilter || !dateData) return false; + const count = dateData.count || 0; + + // 偏冷日特殊处理:只显示 <40 的日期 + if (activeFilter === 'cold') { + return count >= 40; // 过滤掉 >=40 的日期 + } + + const filterConfig = HEAT_LEVELS.find((f) => f.key === activeFilter); + return filterConfig ? count < filterConfig.threshold : false; + }, + [activeFilter] + ); + + // 当前选中日期的数据 + const selectedDateData = useMemo(() => getDateData(selectedDate), [getDateData, selectedDate]); + + const previousDateData = useMemo( + () => (selectedDate ? getPreviousDateData(selectedDate) : null), + [getPreviousDateData, selectedDate] + ); + + // 预计算所有日历格子数据 - 性能优化核心 + const calendarCellsData = useMemo(() => { + const today = new Date(); + const todayStr = today.toDateString(); + const selectedDateStr = selectedDate?.toDateString(); + + return days.map((date, index): CalendarCellData => { + if (!date) { + return { + key: `empty-${index}`, + date: null, + dateData: null, + previousData: null, + isSelected: false, + isToday: false, + isWeekend: false, + isFiltered: false, + }; + } + + const dateStr = formatDateStr(date); + const dateData = dateDataMap.get(dateStr) || null; + const dayOfWeek = date.getDay(); + + // 获取前一天数据 + const prevDate = new Date(date); + prevDate.setDate(prevDate.getDate() - 1); + const previousData = dateDataMap.get(formatDateStr(prevDate)) || null; + + return { + key: dateStr || `day-${index}`, + date, + dateData, + previousData, + isSelected: date.toDateString() === selectedDateStr, + isToday: date.toDateString() === todayStr, + isWeekend: dayOfWeek === 0 || dayOfWeek === 6, + isFiltered: isDateFiltered(dateData), + }; + }); + }, [days, dateDataMap, selectedDate, isDateFiltered]); + + return { + days, + calendarCellsData, + getDateData, + getPreviousDateData, + selectedDateData, + previousDateData, + }; +}; + +export default useCalendarData; diff --git a/src/views/LimitAnalyse/components/LimitUpEmotionCycle/index.tsx b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/index.tsx new file mode 100644 index 00000000..b00aa177 --- /dev/null +++ b/src/views/LimitAnalyse/components/LimitUpEmotionCycle/index.tsx @@ -0,0 +1,171 @@ +/** + * 涨停情绪周期组件 + * 左侧面板(AI总结 + 核心指标 + 快捷筛选)+ 右侧增强日历 + * + * 优化点: + * 1. 日历格子直接显示涨停数、趋势箭头、主要板块 + * 2. 左侧显示AI复盘总结、核心指标 + * 3. 快速筛选器(只看高潮日等) + * 4. 悬浮提示卡片显示详细信息 + */ +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, HStack, Text, Icon, IconButton, SimpleGrid } from '@chakra-ui/react'; +import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'; +import { css } from '@emotion/react'; +import { GLASS_BLUR } from '@/constants/glassConfig'; + +// 子组件 +import { LeftPanel, CalendarCell } from './components'; +// Hooks +import { useCalendarData, AvailableDate } from './hooks'; +// 常量 +import { goldColors, bgColors, textColors, shimmer } from './constants/theme'; +import { WEEK_DAYS, MONTH_NAMES } from './constants/config'; + +// 静态 CSS 样式 - 模块级别定义,避免每次渲染创建新对象 +const shimmerAnimation = css` + animation: ${shimmer} 3s linear infinite; + background-size: 200% 100%; +`; + +interface LimitUpEmotionCycleProps { + selectedDate: Date | null; + onDateChange: (date: Date) => void; + availableDates?: AvailableDate[] | null; +} + +const LimitUpEmotionCycle: React.FC = ({ + selectedDate, + onDateChange, + availableDates = null, +}) => { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [activeFilter, setActiveFilter] = useState(null); + + // 同步月份 - 当选中日期变化时自动切换到对应月份 + useEffect(() => { + if (selectedDate) { + setCurrentMonth((prev) => { + const isSameMonth = + prev.getFullYear() === selectedDate.getFullYear() && prev.getMonth() === selectedDate.getMonth(); + return isSameMonth ? prev : new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1); + }); + } + }, [selectedDate]); + + // 使用日历数据 Hook - 包含预计算的格子数据 + const { calendarCellsData, selectedDateData, previousDateData } = useCalendarData({ + currentMonth, + availableDates, + selectedDate, + activeFilter, + }); + + // 月份导航回调 + const handlePrevMonth = useCallback(() => { + setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1)); + }, []); + + const handleNextMonth = useCallback(() => { + setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1)); + }, []); + + return ( + + {/* 顶部装饰条 */} + + + + {/* 左侧面板 */} + + + {/* 右侧日历 */} + + {/* 月份导航 */} + + } + variant="ghost" + size="sm" + color={textColors.secondary} + _hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }} + onClick={handlePrevMonth} + aria-label="上个月" + /> + + + + {currentMonth.getFullYear()}年{MONTH_NAMES[currentMonth.getMonth()]} + + + } + variant="ghost" + size="sm" + color={textColors.secondary} + _hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }} + onClick={handleNextMonth} + aria-label="下个月" + /> + + + {/* 星期标题 */} + + {WEEK_DAYS.map((day, idx) => ( + + {day} + + ))} + + + {/* 日历格子 - 使用预计算数据 */} + + {calendarCellsData.map((cellData) => ( + + ))} + + + + + ); +}; + +export default LimitUpEmotionCycle;