From 16c30b45b92caa03bd43a3168c657553c2d62cc0 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:50:41 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(GlassCard):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=AF=9B=E7=8E=BB=E7=92=83=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持多种变体: default, elevated, subtle, transparent - 支持悬停效果、发光效果、角落装饰 - 黑金配色主题,可全局复用 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/GlassCard/index.js | 179 ++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/components/GlassCard/index.js diff --git a/src/components/GlassCard/index.js b/src/components/GlassCard/index.js new file mode 100644 index 00000000..c5385bda --- /dev/null +++ b/src/components/GlassCard/index.js @@ -0,0 +1,179 @@ +/** + * GlassCard - 通用毛玻璃卡片组件 + * + * 复用自 Company 页面的 Glassmorphism 风格 + * 可在全局使用 + */ + +import React, { memo, forwardRef } from 'react'; +import { Box } from '@chakra-ui/react'; + +// 主题配置 +const GLASS_THEME = { + colors: { + gold: { + 400: '#D4AF37', + 500: '#B8960C', + }, + bg: { + deep: '#0A0A14', + primary: '#0F0F1A', + elevated: '#1A1A2E', + surface: '#252540', + }, + line: { + subtle: 'rgba(212, 175, 55, 0.1)', + default: 'rgba(212, 175, 55, 0.2)', + emphasis: 'rgba(212, 175, 55, 0.4)', + }, + }, + blur: { + sm: 'blur(8px)', + md: 'blur(16px)', + lg: 'blur(24px)', + }, + glow: { + sm: '0 0 8px rgba(212, 175, 55, 0.3)', + md: '0 0 16px rgba(212, 175, 55, 0.4)', + }, +}; + +// 变体样式 +const VARIANTS = { + default: { + bg: `linear-gradient(135deg, ${GLASS_THEME.colors.bg.elevated} 0%, ${GLASS_THEME.colors.bg.primary} 100%)`, + border: `1px solid ${GLASS_THEME.colors.line.default}`, + backdropFilter: GLASS_THEME.blur.md, + }, + elevated: { + bg: `linear-gradient(145deg, ${GLASS_THEME.colors.bg.surface} 0%, ${GLASS_THEME.colors.bg.elevated} 100%)`, + border: `1px solid ${GLASS_THEME.colors.line.emphasis}`, + backdropFilter: GLASS_THEME.blur.lg, + }, + subtle: { + bg: 'rgba(212, 175, 55, 0.05)', + border: `1px solid ${GLASS_THEME.colors.line.subtle}`, + backdropFilter: GLASS_THEME.blur.sm, + }, + transparent: { + bg: 'rgba(15, 15, 26, 0.8)', + border: `1px solid ${GLASS_THEME.colors.line.default}`, + backdropFilter: GLASS_THEME.blur.lg, + }, +}; + +const ROUNDED_MAP = { + sm: '8px', + md: '12px', + lg: '16px', + xl: '20px', + '2xl': '24px', +}; + +const PADDING_MAP = { + none: 0, + sm: 3, + md: 4, + lg: 6, +}; + +// 角落装饰 +const CornerDecor = memo(({ position }) => { + const baseStyle = { + position: 'absolute', + width: '12px', + height: '12px', + borderColor: GLASS_THEME.colors.gold[400], + borderStyle: 'solid', + borderWidth: 0, + opacity: 0.6, + }; + + const positions = { + tl: { top: '8px', left: '8px', borderTopWidth: '2px', borderLeftWidth: '2px' }, + tr: { top: '8px', right: '8px', borderTopWidth: '2px', borderRightWidth: '2px' }, + bl: { bottom: '8px', left: '8px', borderBottomWidth: '2px', borderLeftWidth: '2px' }, + br: { bottom: '8px', right: '8px', borderBottomWidth: '2px', borderRightWidth: '2px' }, + }; + + return ; +}); + +CornerDecor.displayName = 'CornerDecor'; + +/** + * GlassCard 组件 + * + * @param {string} variant - 变体: 'default' | 'elevated' | 'subtle' | 'transparent' + * @param {boolean} hoverable - 是否启用悬停效果 + * @param {boolean} glowing - 是否启用发光效果 + * @param {boolean} cornerDecor - 是否显示角落装饰 + * @param {string} rounded - 圆角: 'sm' | 'md' | 'lg' | 'xl' | '2xl' + * @param {string} padding - 内边距: 'none' | 'sm' | 'md' | 'lg' + */ +const GlassCard = forwardRef( + ( + { + children, + variant = 'default', + hoverable = true, + glowing = false, + cornerDecor = false, + rounded = 'lg', + padding = 'md', + ...props + }, + ref + ) => { + const variantStyle = VARIANTS[variant] || VARIANTS.default; + + return ( + + {/* 角落装饰 */} + {cornerDecor && ( + <> + + + + + + )} + + {/* 内容 */} + + {children} + + + ); + } +); + +GlassCard.displayName = 'GlassCard'; + +export default memo(GlassCard); +export { GLASS_THEME }; From 1a55e037c9e6259b709ad76b53862d865c409488 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:50:49 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(MarketDashboard):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8A=95=E8=B5=84=E4=BB=AA=E8=A1=A8=E7=9B=98=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 指数卡片组件(带迷你面积图) - 成交额柱状图、涨跌分布图组件 - 热门板块排行组件 - 毛玻璃背景,黑金配色 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../MarketDashboard/MarketDashboard.js | 50 +++++ .../MarketDashboard/components/HotConcepts.js | 45 +++++ .../components/MarketOverview.js | 81 ++++++++ .../components/PlatformStats.js | 34 ++++ .../components/TradingCalendar.js | 179 ++++++++++++++++++ .../components/atoms/ConceptItem.js | 57 ++++++ .../components/atoms/DayCell.js | 87 +++++++++ .../components/atoms/HotSectorsRanking.js | 78 ++++++++ .../components/atoms/IndexCard.js | 55 ++++++ .../components/atoms/IndexChartCard.js | 96 ++++++++++ .../components/atoms/MiniTrendLine.js | 50 +++++ .../components/atoms/RiseFallChart.js | 92 +++++++++ .../components/atoms/StatCard.js | 44 +++++ .../components/atoms/StatItem.js | 47 +++++ .../components/atoms/TurnoverChart.js | 56 ++++++ .../MarketDashboard/components/atoms/index.js | 11 ++ .../MarketDashboard/components/index.js | 6 + .../components/MarketDashboard/constants.ts | 108 +++++++++++ .../components/MarketDashboard/index.js | 3 + .../components/MarketDashboard/types.ts | 88 +++++++++ 20 files changed, 1267 insertions(+) create mode 100644 src/views/Profile/components/MarketDashboard/MarketDashboard.js create mode 100644 src/views/Profile/components/MarketDashboard/components/HotConcepts.js create mode 100644 src/views/Profile/components/MarketDashboard/components/MarketOverview.js create mode 100644 src/views/Profile/components/MarketDashboard/components/PlatformStats.js create mode 100644 src/views/Profile/components/MarketDashboard/components/TradingCalendar.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/ConceptItem.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/DayCell.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/HotSectorsRanking.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/IndexCard.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/MiniTrendLine.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/RiseFallChart.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/StatCard.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/StatItem.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js create mode 100644 src/views/Profile/components/MarketDashboard/components/atoms/index.js create mode 100644 src/views/Profile/components/MarketDashboard/components/index.js create mode 100644 src/views/Profile/components/MarketDashboard/constants.ts create mode 100644 src/views/Profile/components/MarketDashboard/index.js create mode 100644 src/views/Profile/components/MarketDashboard/types.ts diff --git a/src/views/Profile/components/MarketDashboard/MarketDashboard.js b/src/views/Profile/components/MarketDashboard/MarketDashboard.js new file mode 100644 index 00000000..1e2c0e46 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/MarketDashboard.js @@ -0,0 +1,50 @@ +// 市场概览仪表盘主组件 - 投资仪表盘 +import React from 'react'; +import { Box, Text, HStack, Icon } from '@chakra-ui/react'; +import { TrendingUp } from 'lucide-react'; +import GlassCard from '@components/GlassCard'; +import { MarketOverview } from './components'; +import { MOCK_INDICES, MOCK_MARKET_STATS } from './constants'; + +const MarketDashboard = ({ + indices = MOCK_INDICES, + marketStats = MOCK_MARKET_STATS, +}) => { + return ( + + {/* 标题栏 */} + + + + 投资仪表盘 + + + + + {/* 市场概况:指数卡片 + 成交额 + 涨跌分布 + 热门板块 */} + + + ); +}; + +export default MarketDashboard; diff --git a/src/views/Profile/components/MarketDashboard/components/HotConcepts.js b/src/views/Profile/components/MarketDashboard/components/HotConcepts.js new file mode 100644 index 00000000..d228c901 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/HotConcepts.js @@ -0,0 +1,45 @@ +// 热点概念组件 +import React from 'react'; +import { Box, Text, VStack, SimpleGrid, HStack, Icon } from '@chakra-ui/react'; +import { Flame } from 'lucide-react'; +import { ConceptItem } from './atoms'; +import { THEME } from '../constants'; + +const HotConcepts = ({ concepts = [], onConceptClick }) => { + return ( + + + {/* 标题 */} + + + + 热点概念 + + + + {/* 概念列表 */} + + {concepts.map((concept) => ( + onConceptClick?.(concept)} + /> + ))} + + + + ); +}; + +export default HotConcepts; diff --git a/src/views/Profile/components/MarketDashboard/components/MarketOverview.js b/src/views/Profile/components/MarketDashboard/components/MarketOverview.js new file mode 100644 index 00000000..df09b713 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/MarketOverview.js @@ -0,0 +1,81 @@ +// 市场概况组件 - 顶部横条(匹配设计图布局) +import React from 'react'; +import { Box, SimpleGrid } from '@chakra-ui/react'; +import { + IndexChartCard, + TurnoverChart, + RiseFallChart, + HotSectorsRanking, +} from './atoms'; + +const MarketOverview = ({ indices = [], marketStats = {} }) => { + // 默认指数数据(带图表数据) + const defaultIndices = [ + { + code: 'sh000001', + name: '上证指数', + value: 3391.88, + change: 1.23, + chartData: [3350, 3360, 3355, 3370, 3365, 3380, 3375, 3390, 3385, 3392], + }, + { + code: 'sz399001', + name: '深证成指', + value: 10728.54, + change: 0.86, + chartData: [10650, 10680, 10660, 10700, 10690, 10720, 10710, 10730, 10720, 10728], + }, + { + code: 'sz399006', + name: '创业板指', + value: 2156.32, + change: -0.45, + chartData: [2180, 2175, 2170, 2165, 2168, 2160, 2165, 2158, 2160, 2156], + }, + ]; + + const displayIndices = indices.length > 0 ? indices : defaultIndices; + + return ( + + {/* 6列网格布局:3个指数卡片 + 成交额 + 涨跌分布 + 热门板块 */} + + {/* 指数卡片(带迷你图表) */} + {displayIndices.map((index) => ( + + ))} + + {/* 成交额柱状图 */} + + + {/* 涨跌分布图 */} + + + {/* 热门板块排行 */} + + + + ); +}; + +export default MarketOverview; diff --git a/src/views/Profile/components/MarketDashboard/components/PlatformStats.js b/src/views/Profile/components/MarketDashboard/components/PlatformStats.js new file mode 100644 index 00000000..68b622d8 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/PlatformStats.js @@ -0,0 +1,34 @@ +// AI平台能力统计组件 - 底部横条 +import React from 'react'; +import { Box, HStack, Divider } from '@chakra-ui/react'; +import { StatItem } from './atoms'; +import { THEME } from '../constants'; + +const PlatformStats = ({ stats = [] }) => { + return ( + + + }> + {stats.map((stat, index) => ( + + ))} + + + ); +}; + +export default PlatformStats; diff --git a/src/views/Profile/components/MarketDashboard/components/TradingCalendar.js b/src/views/Profile/components/MarketDashboard/components/TradingCalendar.js new file mode 100644 index 00000000..f250aae6 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/TradingCalendar.js @@ -0,0 +1,179 @@ +// 交易日历组件 +import React, { useState, useMemo } from 'react'; +import { + Box, + Text, + VStack, + HStack, + Grid, + GridItem, + IconButton, +} from '@chakra-ui/react'; +import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'; +import { DayCell } from './atoms'; +import { THEME, WEEKDAY_LABELS } from '../constants'; + +const TradingCalendar = ({ tradingDays = [] }) => { + const [currentDate, setCurrentDate] = useState(new Date()); + + const calendarData = useMemo(() => { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + // 当月第一天 + const firstDay = new Date(year, month, 1); + // 当月最后一天 + const lastDay = new Date(year, month + 1, 0); + // 第一天是星期几 + const startWeekday = firstDay.getDay(); + // 当月天数 + const daysInMonth = lastDay.getDate(); + + // 上月最后几天 + const prevMonthLastDay = new Date(year, month, 0).getDate(); + + const days = []; + + // 填充上月日期 + for (let i = startWeekday - 1; i >= 0; i--) { + days.push({ + day: prevMonthLastDay - i, + isCurrentMonth: false, + isWeekend: false, + isTrading: false, + }); + } + + // 填充当月日期 + const today = new Date(); + for (let day = 1; day <= daysInMonth; day++) { + const date = new Date(year, month, day); + const weekday = date.getDay(); + const isWeekend = weekday === 0 || weekday === 6; + const isToday = + day === today.getDate() && + month === today.getMonth() && + year === today.getFullYear(); + + // 检查是否为交易日(简化逻辑:非周末即交易日) + // 实际应用中应该从 tradingDays 数组判断 + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const isTrading = tradingDays.length > 0 + ? tradingDays.includes(dateStr) + : !isWeekend; + + days.push({ + day, + isCurrentMonth: true, + isWeekend, + isTrading, + isToday, + }); + } + + // 填充下月日期(补满 6 行 * 7 天 = 42 格) + const remaining = 42 - days.length; + for (let day = 1; day <= remaining; day++) { + days.push({ + day, + isCurrentMonth: false, + isWeekend: false, + isTrading: false, + }); + } + + return days; + }, [currentDate, tradingDays]); + + const handlePrevMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); + }; + + const handleNextMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); + }; + + const monthText = `${currentDate.getFullYear()}年${currentDate.getMonth() + 1}月`; + + return ( + + + {/* 日历头部 */} + + + + + 交易日历 + + + + } + size="xs" + variant="ghost" + color="rgba(255, 255, 255, 0.6)" + onClick={handlePrevMonth} + aria-label="上月" + _hover={{ bg: 'rgba(212, 175, 55, 0.15)' }} + /> + + {monthText} + + } + size="xs" + variant="ghost" + color="rgba(255, 255, 255, 0.6)" + onClick={handleNextMonth} + aria-label="下月" + _hover={{ bg: 'rgba(212, 175, 55, 0.15)' }} + /> + + + + {/* 星期标题 */} + + {WEEKDAY_LABELS.map((label, index) => ( + + + {label} + + + ))} + + + {/* 日期网格 */} + + {calendarData.map((dayData, index) => ( + + + + ))} + + + + ); +}; + +export default TradingCalendar; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/ConceptItem.js b/src/views/Profile/components/MarketDashboard/components/atoms/ConceptItem.js new file mode 100644 index 00000000..a9202f57 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/ConceptItem.js @@ -0,0 +1,57 @@ +// 概念项组件 +import React from 'react'; +import { Box, Text, HStack } from '@chakra-ui/react'; +import { THEME } from '../../constants'; +import MiniTrendLine from './MiniTrendLine'; + +const ConceptItem = ({ name, change, trend = [], onClick }) => { + const isUp = change >= 0; + const changeColor = isUp ? THEME.status.up : THEME.status.down; + const changeText = isUp ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`; + + return ( + + + + {name} + + + {trend.length > 0 && ( + + )} + + {changeText} + + + + + ); +}; + +export default ConceptItem; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/DayCell.js b/src/views/Profile/components/MarketDashboard/components/atoms/DayCell.js new file mode 100644 index 00000000..d4241718 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/DayCell.js @@ -0,0 +1,87 @@ +// 日期单元格组件 +import React from 'react'; +import { Box, Text } from '@chakra-ui/react'; +import { THEME } from '../../constants'; + +const DayCell = ({ + day, + isTrading = true, + isToday = false, + isWeekend = false, + isCurrentMonth = true, +}) => { + // 今天的样式(金色背景) + if (isToday) { + return ( + + + {day} + + + ); + } + + // 非当月日期 + if (!isCurrentMonth) { + return ( + + + {day} + + + ); + } + + // 周末(非交易日) + if (isWeekend || !isTrading) { + return ( + + + {day} + + + ); + } + + // 普通交易日 + return ( + + + {day} + + + ); +}; + +export default DayCell; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/HotSectorsRanking.js b/src/views/Profile/components/MarketDashboard/components/atoms/HotSectorsRanking.js new file mode 100644 index 00000000..d15c44db --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/HotSectorsRanking.js @@ -0,0 +1,78 @@ +// 热门板块排行组件 +import React from 'react'; +import { Box, Text, VStack, HStack } from '@chakra-ui/react'; +import MiniTrendLine from './MiniTrendLine'; + +const HotSectorsRanking = ({ sectors = [], title = '热门板块排行' }) => { + // 默认数据 + const defaultSectors = [ + { rank: 1, name: '人工智能', change: 3.2, trend: [100, 102, 101, 104, 103, 106] }, + { rank: 2, name: '新能源车', change: 1.8, trend: [100, 99, 101, 102, 101, 103] }, + { rank: 3, name: '生物医药', change: 1.3, trend: [100, 101, 100, 102, 101, 102] }, + { rank: 4, name: '消费科技', change: 1.2, trend: [100, 100, 101, 100, 102, 102] }, + { rank: 5, name: '其他', change: 0.4, trend: [100, 100, 100, 101, 100, 101] }, + ]; + + const data = sectors.length > 0 ? sectors : defaultSectors; + + return ( + + + + {title} + + + {/* 排行列表 */} + + {data.map((sector, index) => ( + + {/* 排名 */} + + + {sector.rank} + + + {sector.name} + + + + {/* 趋势线 */} + + = 0 ? 'red' : 'green'} + width={40} + height={14} + /> + + + {/* 涨跌幅 */} + = 0 ? '#EF4444' : '#22C55E'} + fontWeight="medium" + w="50px" + textAlign="right" + > + {sector.change >= 0 ? '+' : ''}{sector.change.toFixed(1)}% + + + ))} + + + + ); +}; + +export default HotSectorsRanking; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/IndexCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/IndexCard.js new file mode 100644 index 00000000..f1f0facf --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/IndexCard.js @@ -0,0 +1,55 @@ +// 指数卡片组件 +import React from 'react'; +import { Box, Text, HStack, VStack } from '@chakra-ui/react'; +import { THEME } from '../../constants'; +import MiniTrendLine from './MiniTrendLine'; + +const IndexCard = ({ name, value, change, trend = [] }) => { + const isUp = change >= 0; + const changeColor = isUp ? THEME.status.up : THEME.status.down; + const changeText = isUp ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`; + + return ( + + + + {name} + + + + + {value.toLocaleString()} + + + {changeText} + + + {trend.length > 0 && ( + + )} + + + + ); +}; + +export default IndexCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js new file mode 100644 index 00000000..9862d42c --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/IndexChartCard.js @@ -0,0 +1,96 @@ +// 指数图表卡片 - 带迷你K线图 +import React, { useMemo } from 'react'; +import { Box, Text, VStack, HStack } from '@chakra-ui/react'; + +const IndexChartCard = ({ name, value, change, chartData = [] }) => { + const isUp = change >= 0; + const changeColor = isUp ? '#EF4444' : '#22C55E'; + const changeText = isUp ? `+${change.toFixed(2)}%` : `${change.toFixed(2)}%`; + + // 生成迷你图表路径 + const chartPath = useMemo(() => { + if (!chartData || chartData.length < 2) return ''; + + const width = 120; + const height = 40; + const padding = 4; + + const min = Math.min(...chartData); + const max = Math.max(...chartData); + const range = max - min || 1; + + const points = chartData.map((val, i) => { + const x = padding + (i / (chartData.length - 1)) * (width - padding * 2); + const y = height - padding - ((val - min) / range) * (height - padding * 2); + return `${x},${y}`; + }); + + return `M ${points.join(' L ')}`; + }, [chartData]); + + // 生成填充区域 + const areaPath = useMemo(() => { + if (!chartPath) return ''; + const width = 120; + const height = 40; + return `${chartPath} L ${width - 4},${height - 4} L 4,${height - 4} Z`; + }, [chartPath]); + + return ( + + + {/* 标题 */} + + {name} + + + {/* 数值和涨跌幅 */} + + + {typeof value === 'number' ? value.toLocaleString(undefined, { minimumFractionDigits: 2 }) : value} + + + {changeText} + + + + {/* 迷你图表 */} + {chartData.length > 0 && ( + + + {/* 填充区域 */} + + {/* 线条 */} + + + + )} + + + ); +}; + +export default IndexChartCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/MiniTrendLine.js b/src/views/Profile/components/MarketDashboard/components/atoms/MiniTrendLine.js new file mode 100644 index 00000000..68192657 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/MiniTrendLine.js @@ -0,0 +1,50 @@ +// 迷你趋势线组件 - 基于 SVG +import React, { useMemo } from 'react'; +import { Box } from '@chakra-ui/react'; +import { THEME } from '../../constants'; + +const MiniTrendLine = ({ + data = [], + color = 'green', + width = 60, + height = 24, +}) => { + const pathD = useMemo(() => { + if (!data || data.length < 2) return ''; + + const min = Math.min(...data); + const max = Math.max(...data); + const range = max - min || 1; + + const points = data.map((value, index) => { + const x = (index / (data.length - 1)) * width; + const y = height - ((value - min) / range) * height * 0.8 - height * 0.1; + return `${x},${y}`; + }); + + return `M ${points.join(' L ')}`; + }, [data, width, height]); + + const strokeColor = color === 'red' ? THEME.status.up : THEME.status.down; + + if (!data || data.length < 2) { + return ; + } + + return ( + + + + + + ); +}; + +export default MiniTrendLine; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallChart.js b/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallChart.js new file mode 100644 index 00000000..207d93ba --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/RiseFallChart.js @@ -0,0 +1,92 @@ +// 涨跌分布图组件 +import React from 'react'; +import { Box, Text, VStack, HStack } from '@chakra-ui/react'; + +const RiseFallChart = ({ + riseCount = 2156, + fallCount = 2034, + flatCount = 312, + title = '涨跌分布' +}) => { + const total = riseCount + fallCount + flatCount; + const risePercent = ((riseCount / total) * 100).toFixed(1); + const fallPercent = ((fallCount / total) * 100).toFixed(1); + const flatPercent = ((flatCount / total) * 100).toFixed(1); + + // 分布数据 - 模拟不同涨跌幅区间 + const distribution = [ + { range: '>7%', rise: 86, fall: 12, label: '涨停' }, + { range: '3-7%', rise: 420, fall: 180 }, + { range: '0-3%', rise: 1650, fall: 0 }, + { range: '-3-0%', rise: 0, fall: 1542 }, + { range: '-7--3%', rise: 0, fall: 280 }, + { range: '<-7%', rise: 0, fall: 20, label: '跌停' }, + ]; + + const maxCount = Math.max(...distribution.map(d => Math.max(d.rise, d.fall))); + + return ( + + + + {title} + + + {/* 水平柱状图 */} + + {distribution.map((item, index) => ( + + {/* 涨(红色,向右) */} + + {item.rise > 0 && ( + + )} + + {/* 中心线 */} + + {/* 跌(绿色,向左显示但实际向右) */} + + {item.fall > 0 && ( + + )} + + + ))} + + + {/* 统计数字 */} + + + 涨 {riseCount} + + + 平 {flatCount} + + + 跌 {fallCount} + + + + + ); +}; + +export default RiseFallChart; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/StatCard.js b/src/views/Profile/components/MarketDashboard/components/atoms/StatCard.js new file mode 100644 index 00000000..dbfbcc59 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/StatCard.js @@ -0,0 +1,44 @@ +// 统计卡片组件(涨停/跌停/成交额) +import React from 'react'; +import { Box, Text, VStack } from '@chakra-ui/react'; +import { THEME } from '../../constants'; + +const StatCard = ({ label, value, subLabel, valueColor }) => { + return ( + + + + {label} + + + {value} + + {subLabel && ( + + {subLabel} + + )} + + + ); +}; + +export default StatCard; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/StatItem.js b/src/views/Profile/components/MarketDashboard/components/atoms/StatItem.js new file mode 100644 index 00000000..f234982c --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/StatItem.js @@ -0,0 +1,47 @@ +// AI平台能力统计项组件 +import React from 'react'; +import { Box, Text, VStack, Icon } from '@chakra-ui/react'; +import { + Building2, + BarChart3, + Calendar, + Bot, + TrendingUp, + Database, +} from 'lucide-react'; +import { THEME } from '../../constants'; + +// 图标映射 +const iconMap = { + building: Building2, + chart: BarChart3, + calendar: Calendar, + robot: Bot, + trending: TrendingUp, + database: Database, +}; + +const StatItem = ({ icon, value, label }) => { + const IconComponent = iconMap[icon] || Database; + + return ( + + + + + {value} + + + {label} + + + + ); +}; + +export default StatItem; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js b/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js new file mode 100644 index 00000000..5b779893 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/TurnoverChart.js @@ -0,0 +1,56 @@ +// 成交额柱状图组件 +import React from 'react'; +import { Box, Text, VStack, HStack } from '@chakra-ui/react'; + +const TurnoverChart = ({ data = [], title = '成交额' }) => { + // 默认数据 + const chartData = data.length > 0 ? data : [ + { time: '10:30', value: 0.85 }, + { time: '11:00', value: 0.92 }, + { time: '11:15', value: 0.78 }, + { time: '13:00', value: 1.05 }, + { time: '13:30', value: 1.12 }, + { time: '14:00', value: 0.95 }, + ]; + + const maxValue = Math.max(...chartData.map(d => d.value)); + + return ( + + + + {title} + + + {/* 柱状图 */} + + {chartData.map((item, index) => ( + + ))} + + + {/* 当前值 */} + + 1.25亿 + + + + ); +}; + +export default TurnoverChart; diff --git a/src/views/Profile/components/MarketDashboard/components/atoms/index.js b/src/views/Profile/components/MarketDashboard/components/atoms/index.js new file mode 100644 index 00000000..988447c5 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/atoms/index.js @@ -0,0 +1,11 @@ +// 原子组件导出 +export { default as MiniTrendLine } from './MiniTrendLine'; +export { default as IndexCard } from './IndexCard'; +export { default as StatCard } from './StatCard'; +export { default as ConceptItem } from './ConceptItem'; +export { default as DayCell } from './DayCell'; +export { default as StatItem } from './StatItem'; +export { default as IndexChartCard } from './IndexChartCard'; +export { default as TurnoverChart } from './TurnoverChart'; +export { default as RiseFallChart } from './RiseFallChart'; +export { default as HotSectorsRanking } from './HotSectorsRanking'; diff --git a/src/views/Profile/components/MarketDashboard/components/index.js b/src/views/Profile/components/MarketDashboard/components/index.js new file mode 100644 index 00000000..b4883089 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/components/index.js @@ -0,0 +1,6 @@ +// 组件导出 +export { default as MarketOverview } from './MarketOverview'; +export { default as HotConcepts } from './HotConcepts'; +export { default as TradingCalendar } from './TradingCalendar'; +export { default as PlatformStats } from './PlatformStats'; +export * from './atoms'; diff --git a/src/views/Profile/components/MarketDashboard/constants.ts b/src/views/Profile/components/MarketDashboard/constants.ts new file mode 100644 index 00000000..b634a484 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/constants.ts @@ -0,0 +1,108 @@ +// MarketDashboard 常量定义 + +// 黑金主题配色 +export const THEME = { + bg: { + primary: '#0A0A0A', // 纯黑背景 + card: '#141414', // 卡片背景 + cardHover: '#1A1A1A', // 卡片悬停 + gradient: 'linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%)', + }, + text: { + primary: '#FFFFFF', // 主文字(白色) + secondary: '#8B8B8B', // 次要文字(灰色) + accent: '#D4AF37', // 强调色(金色) + gold: '#D4AF37', // 金色 + goldLight: '#F0D78C', // 浅金色 + }, + status: { + up: '#EF4444', // 上涨(红色) + down: '#22C55E', // 下跌(绿色) + }, + border: 'rgba(212, 175, 55, 0.2)', // 金色边框 + borderGold: 'rgba(212, 175, 55, 0.4)', + shadow: '0 4px 20px rgba(212, 175, 55, 0.1)', +}; + +// 模拟数据(后续替换为真实 API) +export const MOCK_INDICES: Array<{ + name: string; + code: string; + value: number; + change: number; + chartData: number[]; +}> = [ + { + name: '上证指数', + code: '000001.SH', + value: 3391.88, + change: 0.52, + chartData: [3350, 3360, 3355, 3370, 3365, 3380, 3375, 3390, 3385, 3392], + }, + { + name: '深证成指', + code: '399001.SZ', + value: 10723.49, + change: 0.68, + chartData: [10650, 10680, 10660, 10700, 10690, 10720, 10710, 10730, 10720, 10723], + }, + { + name: '创业板指', + code: '399006.SZ', + value: 2156.78, + change: 1.23, + chartData: [2130, 2140, 2135, 2150, 2145, 2155, 2150, 2160, 2155, 2157], + }, +]; + +export const MOCK_MARKET_STATS = { + limitUp: 86, + limitDown: 12, + turnover: '1.2万亿', + riseCount: 2156, + fallCount: 2034, + flatCount: 312, + turnoverData: [ + { time: '10:30', value: 0.85 }, + { time: '11:00', value: 0.92 }, + { time: '11:15', value: 0.78 }, + { time: '13:00', value: 1.05 }, + { time: '13:30', value: 1.12 }, + { time: '14:00', value: 0.95 }, + ], + hotSectors: [ + { rank: 1, name: '人工智能', change: 3.2, trend: [100, 102, 101, 104, 103, 106] }, + { rank: 2, name: '新能源车', change: 1.8, trend: [100, 99, 101, 102, 101, 103] }, + { rank: 3, name: '生物医药', change: 1.3, trend: [100, 101, 100, 102, 101, 102] }, + { rank: 4, name: '消费科技', change: 1.2, trend: [100, 100, 101, 100, 102, 102] }, + { rank: 5, name: '半导体', change: 0.9, trend: [100, 100, 100, 101, 100, 101] }, + ], +}; + +export const MOCK_HOT_CONCEPTS: Array<{ + id: string; + name: string; + change: number; + trend: number[]; +}> = [ + { id: '1', name: '人工智能', change: 3.25, trend: [100, 102, 101, 103, 105, 108] }, + { id: '2', name: '芯片概念', change: 2.87, trend: [100, 99, 101, 102, 104, 106] }, + { id: '3', name: '新能源车', change: 2.15, trend: [100, 101, 100, 102, 103, 105] }, + { id: '4', name: '光伏', change: 1.92, trend: [100, 99, 100, 101, 102, 104] }, + { id: '5', name: '医药生物', change: 1.56, trend: [100, 100, 101, 101, 102, 103] }, + { id: '6', name: '消费电子', change: 1.33, trend: [100, 101, 100, 101, 102, 103] }, +]; + +export const MOCK_PLATFORM_STATS: Array<{ + icon: string; + value: string; + label: string; +}> = [ + { icon: 'building', value: '4300+', label: '上市公司' }, + { icon: 'chart', value: '500+', label: '概念板块' }, + { icon: 'calendar', value: '10年+', label: '历史数据' }, + { icon: 'robot', value: '24/7', label: 'AI分析' }, +]; + +// 星期标题 +export const WEEKDAY_LABELS = ['日', '一', '二', '三', '四', '五', '六']; diff --git a/src/views/Profile/components/MarketDashboard/index.js b/src/views/Profile/components/MarketDashboard/index.js new file mode 100644 index 00000000..d480e06f --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/index.js @@ -0,0 +1,3 @@ +// MarketDashboard 组件导出 +export { default } from './MarketDashboard'; +export { default as MarketDashboard } from './MarketDashboard'; diff --git a/src/views/Profile/components/MarketDashboard/types.ts b/src/views/Profile/components/MarketDashboard/types.ts new file mode 100644 index 00000000..37baba47 --- /dev/null +++ b/src/views/Profile/components/MarketDashboard/types.ts @@ -0,0 +1,88 @@ +// MarketDashboard 类型定义 + +// 迷你趋势线 +export interface MiniTrendLineProps { + data: number[]; + color?: 'green' | 'red'; + width?: number; + height?: number; +} + +// 指数卡片 +export interface IndexCardProps { + name: string; + value: number; + change: number; + trend?: number[]; +} + +// 统计卡片(涨停/成交额) +export interface StatCardProps { + label: string; + value: string | number; + subLabel?: string; + icon?: React.ReactNode; +} + +// 概念项 +export interface ConceptItemProps { + name: string; + change: number; + trend?: number[]; + onClick?: () => void; +} + +// 日期单元格 +export interface DayCellProps { + day: number; + isTrading: boolean; + isToday: boolean; + isWeekend: boolean; + isCurrentMonth?: boolean; +} + +// 统计项(AI平台能力) +export interface StatItemProps { + icon: React.ReactNode; + value: string; + label: string; +} + +// 指数数据 +export interface IndexData { + name: string; + code: string; + value: number; + change: number; + trend: number[]; +} + +// 概念数据 +export interface ConceptData { + id: string; + name: string; + change: number; + trend: number[]; +} + +// 市场统计数据 +export interface MarketStats { + limitUp: number; + limitDown: number; + turnover: string; +} + +// 平台能力数据 +export interface PlatformStat { + icon: string; + value: string; + label: string; +} + +// 完整仪表盘数据 +export interface MarketDashboardData { + indices: IndexData[]; + marketStats: MarketStats; + hotConcepts: ConceptData[]; + platformStats: PlatformStat[]; +} From 10e34d911fc3686d1db70d2029122fb61970c9bc Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:51:01 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(StrategyCenter):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8A=95=E8=B5=84=E8=A7=84=E5=88=92=E4=B8=AD=E5=BF=83=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Q1计划卡片(进度条+要点列表) - 银行股防守卡片(仓位+策略) - AI算力交易卡片(浮盈数据) - 消费复盘卡片(趋势图+心得) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/AITradingCard.js | 84 ++++++++++ .../components/DefenseStrategyCard.js | 81 ++++++++++ .../components/QuarterPlanCard.js | 126 +++++++++++++++ .../StrategyCenter/components/ReviewCard.js | 143 ++++++++++++++++++ .../StrategyCenter/components/index.js | 5 + .../components/StrategyCenter/index.js | 63 ++++++++ 6 files changed, 502 insertions(+) create mode 100644 src/views/Profile/components/StrategyCenter/components/AITradingCard.js create mode 100644 src/views/Profile/components/StrategyCenter/components/DefenseStrategyCard.js create mode 100644 src/views/Profile/components/StrategyCenter/components/QuarterPlanCard.js create mode 100644 src/views/Profile/components/StrategyCenter/components/ReviewCard.js create mode 100644 src/views/Profile/components/StrategyCenter/components/index.js create mode 100644 src/views/Profile/components/StrategyCenter/index.js diff --git a/src/views/Profile/components/StrategyCenter/components/AITradingCard.js b/src/views/Profile/components/StrategyCenter/components/AITradingCard.js new file mode 100644 index 00000000..ae535d12 --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/components/AITradingCard.js @@ -0,0 +1,84 @@ +// AI 算力交易卡片 +import React from 'react'; +import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; +import { Cpu, TrendingUp, Lightbulb } from 'lucide-react'; + +const AITradingCard = ({ + title = 'AI 算力交易', + currentProfit = 8.5, + targetProfit = 8.5, + strategies = [ + 'AI:界缓充提镂', + '筱略:高源分析', + ], +}) => { + const profitColor = currentProfit >= 0 ? '#EF4444' : '#22C55E'; + const profitSign = currentProfit >= 0 ? '+' : ''; + + return ( + + + {/* 标题 */} + + + + [{title}] + + + + {/* 浮盈数据 */} + + + + + 当前浮盈: + + + {profitSign}{currentProfit}% + + + + + + + 当前浮盈: + + + {profitSign}{targetProfit}% + + + + + {/* 策略列表 */} + + + + + 策略: + + + {strategies.map((item, index) => ( + + + {item} + + ))} + + + + ); +}; + +export default AITradingCard; diff --git a/src/views/Profile/components/StrategyCenter/components/DefenseStrategyCard.js b/src/views/Profile/components/StrategyCenter/components/DefenseStrategyCard.js new file mode 100644 index 00000000..5e52581a --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/components/DefenseStrategyCard.js @@ -0,0 +1,81 @@ +// 银行股防守卡片 +import React from 'react'; +import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; +import { Building2, Lock, TrendingUp, Lightbulb } from 'lucide-react'; + +const DefenseStrategyCard = ({ + title = '银行股防守', + position = '30%', + strategy = '高股息', + strategies = [ + 'AI辅AEI银行化分析', + 'AI:筏跌股鬈股', + ], +}) => { + return ( + + + {/* 标题 */} + + + + [{title}] + + + + {/* 仓位和策略 */} + + + + + 仓位: + + + {position} + + + + + + + 策略: + + + {strategy} + + + + + {/* 策略列表 */} + + + + + 策略: + + + {strategies.map((item, index) => ( + + + {item} + + ))} + + + + ); +}; + +export default DefenseStrategyCard; diff --git a/src/views/Profile/components/StrategyCenter/components/QuarterPlanCard.js b/src/views/Profile/components/StrategyCenter/components/QuarterPlanCard.js new file mode 100644 index 00000000..9a03fa69 --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/components/QuarterPlanCard.js @@ -0,0 +1,126 @@ +// 季度计划卡片 - 2025 Q1 计划 +import React from 'react'; +import { Box, Text, VStack, HStack, Progress, Icon } from '@chakra-ui/react'; +import { Calendar } from 'lucide-react'; + +const QuarterPlanCard = ({ + title = '2025 Q1 计划', + progress = { execute: 70, strategy: 100, target: 15 }, + keyPoints = [ + { label: '重点', value: 'AI、数字经济' }, + { label: '重点', value: 'AI、数字经济' }, + { label: '目标', value: '收益率+15%' }, + ], +}) => { + return ( + + + {/* 标题 */} + + + + [{title}] + + + + {/* 进度条区域 */} + + {/* 进行 */} + + + 进行 + + + div': { + bg: 'linear-gradient(90deg, #3B82F6 0%, #60A5FA 100%)', + }, + }} + /> + + + {progress.execute}% + + + + {/* 缓略 */} + + + 缓略 + + + div': { + bg: 'linear-gradient(90deg, #3B82F6 0%, #60A5FA 100%)', + }, + }} + /> + + + {progress.strategy}% + + + + {/* 目标 */} + + + 目标 + + + div': { + bg: 'linear-gradient(90deg, #3B82F6 0%, #60A5FA 100%)', + }, + }} + /> + + + +{progress.target}% + + + + + {/* 要点列表 */} + + {keyPoints.map((point, index) => ( + + + {point.label}: + {point.value} + + ))} + + + + ); +}; + +export default QuarterPlanCard; diff --git a/src/views/Profile/components/StrategyCenter/components/ReviewCard.js b/src/views/Profile/components/StrategyCenter/components/ReviewCard.js new file mode 100644 index 00000000..9571ef28 --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/components/ReviewCard.js @@ -0,0 +1,143 @@ +// 消费复盘卡片(带图表) +import React, { useMemo } from 'react'; +import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; +import { ShoppingBag, Heart } from 'lucide-react'; + +const ReviewCard = ({ + title = '消费复盘', + chartData = [8000, 10000, 9500, 11000, 12000, 11500, 10500, 9000, 10000, 11000, 12500, 11000], + insight = '关注复苏节奏', +}) => { + // 生成图表路径 + const { linePath, areaPath } = useMemo(() => { + if (!chartData || chartData.length < 2) return { linePath: '', areaPath: '' }; + + const width = 140; + const height = 60; + const padding = { top: 8, right: 4, bottom: 16, left: 4 }; + + const chartWidth = width - padding.left - padding.right; + const chartHeight = height - padding.top - padding.bottom; + + const min = Math.min(...chartData); + const max = Math.max(...chartData); + const range = max - min || 1; + + const points = chartData.map((val, i) => { + const x = padding.left + (i / (chartData.length - 1)) * chartWidth; + const y = padding.top + chartHeight - ((val - min) / range) * chartHeight; + return { x, y }; + }); + + const linePathStr = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' '); + + const areaPathStr = `${linePathStr} L ${points[points.length - 1].x},${height - padding.bottom} L ${padding.left},${height - padding.bottom} Z`; + + return { linePath: linePathStr, areaPath: areaPathStr }; + }, [chartData]); + + // Y轴刻度 + const yTicks = useMemo(() => { + const max = Math.max(...chartData); + return [max, Math.round(max / 2), 0]; + }, [chartData]); + + // X轴刻度 + const xTicks = ['10:05', '11:13', '12:15', '13:05']; + + return ( + + + {/* 标题 */} + + + + [{title}] + + + + {/* 图表区域 */} + + + {/* Y轴标签 */} + + {yTicks[0]} + + + {yTicks[1]} + + + {yTicks[2]} + + + {/* 网格线 */} + + + + + {/* 填充区域 */} + + + {/* 线条 */} + + + {/* 渐变定义 */} + + + + + + + + + {/* X轴标签 */} + + {xTicks.map((tick, i) => ( + {tick} + ))} + + + + {/* 心得 */} + + + + 心得: + {insight} + + + + ); +}; + +export default ReviewCard; diff --git a/src/views/Profile/components/StrategyCenter/components/index.js b/src/views/Profile/components/StrategyCenter/components/index.js new file mode 100644 index 00000000..844af436 --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/components/index.js @@ -0,0 +1,5 @@ +// 投资规划中心子组件导出 +export { default as QuarterPlanCard } from './QuarterPlanCard'; +export { default as DefenseStrategyCard } from './DefenseStrategyCard'; +export { default as AITradingCard } from './AITradingCard'; +export { default as ReviewCard } from './ReviewCard'; diff --git a/src/views/Profile/components/StrategyCenter/index.js b/src/views/Profile/components/StrategyCenter/index.js new file mode 100644 index 00000000..0aaa3769 --- /dev/null +++ b/src/views/Profile/components/StrategyCenter/index.js @@ -0,0 +1,63 @@ +// 投资规划中心组件 (Strategy Center) +import React from 'react'; +import { Box, Text, HStack, SimpleGrid, Icon } from '@chakra-ui/react'; +import { Target } from 'lucide-react'; +import GlassCard from '@components/GlassCard'; +import { + QuarterPlanCard, + DefenseStrategyCard, + AITradingCard, + ReviewCard, +} from './components'; + +const StrategyCenter = () => { + return ( + + {/* 标题栏 */} + + + + 投资规划中心 + + + (Strategy Center) + + + + + {/* 4列卡片布局 */} + + + + + + + + ); +}; + +export default StrategyCenter; From bc844bb4dc298d17f084a804c00df85657c035e5 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:51:08 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat(ForumCenter):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BB=B7=E5=80=BC=E8=AE=BA=E5=9D=9B/=E4=BA=92=E5=8A=A8?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 我的预测卡片(看涨/看跌投票) - 社区动态卡片(我发布的/我参与的) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/CommunityFeedCard.js | 121 +++++++++++++ .../ForumCenter/components/PredictionCard.js | 169 ++++++++++++++++++ .../ForumCenter/components/index.js | 3 + .../Profile/components/ForumCenter/index.js | 49 +++++ 4 files changed, 342 insertions(+) create mode 100644 src/views/Profile/components/ForumCenter/components/CommunityFeedCard.js create mode 100644 src/views/Profile/components/ForumCenter/components/PredictionCard.js create mode 100644 src/views/Profile/components/ForumCenter/components/index.js create mode 100644 src/views/Profile/components/ForumCenter/index.js diff --git a/src/views/Profile/components/ForumCenter/components/CommunityFeedCard.js b/src/views/Profile/components/ForumCenter/components/CommunityFeedCard.js new file mode 100644 index 00000000..bae63725 --- /dev/null +++ b/src/views/Profile/components/ForumCenter/components/CommunityFeedCard.js @@ -0,0 +1,121 @@ +// 社区动态卡片 +import React, { useState } from 'react'; +import { Box, Text, VStack, HStack, Icon, Button } from '@chakra-ui/react'; +import { Newspaper, Flame, MessageCircle } from 'lucide-react'; + +const CommunityFeedCard = ({ + myPosts = [ + { id: 1, title: '关于新能源车下半场的思考', date: '2025/12/18', replies: 32, isHot: true }, + { id: 2, title: '半导体行业深度分析', date: '2025/12/15', replies: 18, isHot: false }, + ], + participatedPosts = [ + { id: 3, title: 'AI产业链投资机会分析', date: '2025/12/17', replies: 45, isHot: true }, + { id: 4, title: '消费板块复苏节奏讨论', date: '2025/12/14', replies: 12, isHot: false }, + ], + onPostClick, +}) => { + const [activeTab, setActiveTab] = useState('my'); // 'my' | 'participated' + + const currentPosts = activeTab === 'my' ? myPosts : participatedPosts; + + return ( + + {/* 标题栏 */} + + + + 社区动态 + + + + {/* 内容区 */} + + + {/* Tab 切换 */} + + + + + + {/* 帖子列表 */} + + {currentPosts.map((post) => ( + onPostClick?.(post)} + > + + {post.title} + + + {post.date} + · + + {post.isHot ? ( + + ) : ( + + )} + + {post.replies}回复 + + + + + ))} + + + + + ); +}; + +export default CommunityFeedCard; diff --git a/src/views/Profile/components/ForumCenter/components/PredictionCard.js b/src/views/Profile/components/ForumCenter/components/PredictionCard.js new file mode 100644 index 00000000..f82f8a22 --- /dev/null +++ b/src/views/Profile/components/ForumCenter/components/PredictionCard.js @@ -0,0 +1,169 @@ +// 我的预测卡片 +import React from 'react'; +import { Box, Text, VStack, HStack, Button, Icon } from '@chakra-ui/react'; +import { Zap, History, TrendingUp, TrendingDown } from 'lucide-react'; + +const PredictionCard = ({ + question = '大A 2025年收盘价?', + myBet = { type: '看涨', points: 500 }, + winRate = 58, + odds = 1.8, + onBullish, + onBearish, + onViewHistory, +}) => { + return ( + + {/* 标题栏 */} + + + + 我的预测 + + + + {/* 内容区 */} + + + {/* 预测问题 - 带渐变背景 */} + + {/* 装饰性弧线 */} + + + + {question} + + + {/* 看涨/看跌按钮 */} + + + + + + + {/* 底部信息 */} + + + + 我的下注: + + {myBet.type} + + + {myBet.points}积分 + + + + + + + + + 当前胜率: + + {winRate}% + + + + 赔率: + + {odds} + + + + + + + + + + ); +}; + +export default PredictionCard; diff --git a/src/views/Profile/components/ForumCenter/components/index.js b/src/views/Profile/components/ForumCenter/components/index.js new file mode 100644 index 00000000..fdc5e756 --- /dev/null +++ b/src/views/Profile/components/ForumCenter/components/index.js @@ -0,0 +1,3 @@ +// 价值论坛子组件导出 +export { default as PredictionCard } from './PredictionCard'; +export { default as CommunityFeedCard } from './CommunityFeedCard'; diff --git a/src/views/Profile/components/ForumCenter/index.js b/src/views/Profile/components/ForumCenter/index.js new file mode 100644 index 00000000..03861567 --- /dev/null +++ b/src/views/Profile/components/ForumCenter/index.js @@ -0,0 +1,49 @@ +// 价值论坛 / 互动中心组件 (Forum Center) +import React from 'react'; +import { Box, Text, HStack, SimpleGrid, Icon } from '@chakra-ui/react'; +import { MessageCircle } from 'lucide-react'; +import GlassCard from '@components/GlassCard'; +import { PredictionCard, CommunityFeedCard } from './components'; + +const ForumCenter = () => { + return ( + + {/* 标题栏 */} + + + + 价值论坛 / 互动中心 + + + + + {/* 两列布局:预测卡片 + 社区动态 */} + + + + + + ); +}; + +export default ForumCenter; From 712090accb141ee0e256e61f49978de0b8be84e1 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:51:16 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat(WatchSidebar):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8F=B3=E4=BE=A7=E8=BE=B9=E6=A0=8F=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 关注股票面板(独立模块) - 关注事件面板(独立模块) - 固定200px宽度,粘性定位 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/FollowingEventsPanel.js | 110 +++++++++++++++++ .../WatchSidebar/components/WatchlistPanel.js | 114 ++++++++++++++++++ .../WatchSidebar/components/index.js | 3 + .../Profile/components/WatchSidebar/index.js | 50 ++++++++ 4 files changed, 277 insertions(+) create mode 100644 src/views/Profile/components/WatchSidebar/components/FollowingEventsPanel.js create mode 100644 src/views/Profile/components/WatchSidebar/components/WatchlistPanel.js create mode 100644 src/views/Profile/components/WatchSidebar/components/index.js create mode 100644 src/views/Profile/components/WatchSidebar/index.js diff --git a/src/views/Profile/components/WatchSidebar/components/FollowingEventsPanel.js b/src/views/Profile/components/WatchSidebar/components/FollowingEventsPanel.js new file mode 100644 index 00000000..5964a677 --- /dev/null +++ b/src/views/Profile/components/WatchSidebar/components/FollowingEventsPanel.js @@ -0,0 +1,110 @@ +// 关注事件面板 - 紧凑版 +import React from 'react'; +import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; +import { Star, Plus, Users } from 'lucide-react'; + +const FollowingEventsPanel = ({ + events = [], + onEventClick, + onAddEvent, +}) => { + return ( + + {/* 标题 */} + + + + + 关注事件 + + + ({events.length}) + + + + + + {/* 事件列表 */} + + {events.length === 0 ? ( + + + + 关注事件 + + + ) : ( + events.slice(0, 6).map((event) => { + const avgChg = event.related_avg_chg; + const isUp = avgChg > 0; + const changeColor = isUp ? '#EF4444' : avgChg < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)'; + + return ( + onEventClick?.(event)} + > + + {event.title} + + + + + {event.follower_count || 0} + + {avgChg !== undefined && avgChg !== null && ( + + {isUp ? '+' : ''}{Number(avgChg).toFixed(2)}% + + )} + + + ); + }) + )} + {events.length > 6 && ( + + 查看全部 ({events.length}) + + )} + + + ); +}; + +export default FollowingEventsPanel; diff --git a/src/views/Profile/components/WatchSidebar/components/WatchlistPanel.js b/src/views/Profile/components/WatchSidebar/components/WatchlistPanel.js new file mode 100644 index 00000000..8f252d71 --- /dev/null +++ b/src/views/Profile/components/WatchSidebar/components/WatchlistPanel.js @@ -0,0 +1,114 @@ +// 关注股票面板 - 紧凑版 +import React from 'react'; +import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; +import { BarChart2, Plus } from 'lucide-react'; + +const WatchlistPanel = ({ + watchlist = [], + realtimeQuotes = {}, + onStockClick, + onAddStock, +}) => { + return ( + + {/* 标题 */} + + + + + 关注股票 + + + ({watchlist.length}) + + + + + + {/* 股票列表 */} + + {watchlist.length === 0 ? ( + + + + 添加自选股 + + + ) : ( + watchlist.slice(0, 8).map((stock) => { + const quote = realtimeQuotes[stock.stock_code]; + const changePercent = quote?.change_percent ?? stock.change_percent; + const isUp = changePercent > 0; + const changeColor = isUp ? '#EF4444' : changePercent < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)'; + + return ( + onStockClick?.(stock)} + > + + + {stock.stock_name || stock.stock_code} + + + {stock.stock_code} + + + + + {quote?.current_price?.toFixed(2) || stock.current_price || '--'} + + + {changePercent !== undefined && changePercent !== null + ? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%` + : '--'} + + + + ); + }) + )} + {watchlist.length > 8 && ( + + 查看全部 ({watchlist.length}) + + )} + + + ); +}; + +export default WatchlistPanel; diff --git a/src/views/Profile/components/WatchSidebar/components/index.js b/src/views/Profile/components/WatchSidebar/components/index.js new file mode 100644 index 00000000..b0bc052f --- /dev/null +++ b/src/views/Profile/components/WatchSidebar/components/index.js @@ -0,0 +1,3 @@ +// 侧边栏子组件导出 +export { default as WatchlistPanel } from './WatchlistPanel'; +export { default as FollowingEventsPanel } from './FollowingEventsPanel'; diff --git a/src/views/Profile/components/WatchSidebar/index.js b/src/views/Profile/components/WatchSidebar/index.js new file mode 100644 index 00000000..422c8071 --- /dev/null +++ b/src/views/Profile/components/WatchSidebar/index.js @@ -0,0 +1,50 @@ +// 右侧边栏 - 关注股票和关注事件(两个独立模块) +import React from 'react'; +import { VStack } from '@chakra-ui/react'; +import GlassCard from '@components/GlassCard'; +import { WatchlistPanel, FollowingEventsPanel } from './components'; + +const WatchSidebar = ({ + watchlist = [], + realtimeQuotes = {}, + followingEvents = [], + onStockClick, + onEventClick, + onAddStock, + onAddEvent, +}) => { + return ( + + {/* 关注股票 - 独立模块 */} + + + + + {/* 关注事件 - 独立模块 */} + + + + + ); +}; + +export default WatchSidebar; From c639b418f0196f2500d6db211f3b617b72d79e70 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 16:51:27 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor(Center):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83=E4=B8=BA=E5=B7=A6=E5=8F=B3?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 左侧自适应:投资仪表盘、规划中心、论坛 - 右侧固定200px:关注股票、关注事件 - 使用 THEME 黑金配色 - 宽度与导航栏保持一致 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/views/Dashboard/Center.js | 384 +++++----------------------------- 1 file changed, 58 insertions(+), 326 deletions(-) diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js index 11386c2f..4e855d82 100644 --- a/src/views/Dashboard/Center.js +++ b/src/views/Dashboard/Center.js @@ -61,9 +61,13 @@ import { FiAlertCircle, FiUsers, } from 'react-icons/fi'; -import MyFutureEvents from './components/MyFutureEvents'; import InvestmentPlanningCenter from './components/InvestmentPlanningCenter'; import { getEventDetailUrl } from '@/utils/idEncoder'; +import MarketDashboard from '@views/Profile/components/MarketDashboard'; +import StrategyCenter from '@views/Profile/components/StrategyCenter'; +import ForumCenter from '@views/Profile/components/ForumCenter'; +import WatchSidebar from '@views/Profile/components/WatchSidebar'; +import { THEME } from '@views/Profile/components/MarketDashboard/constants'; export default function CenterDashboard() { const { user } = useAuth(); @@ -273,335 +277,63 @@ export default function CenterDashboard() { } return ( - - - {/* 主要内容区域 */} - - {/* 左列:自选股票 */} - - - - - - - 自选股票 - - {watchlist.length} - - {quotesLoading && } - - } - variant="ghost" - size="sm" - onClick={() => navigate('/stocks')} - aria-label="添加自选股" - /> - - - - {watchlist.length === 0 ? ( -
- - - - 暂无自选股 - - - -
- ) : ( - - {watchlist.slice(0, 10).map((stock) => ( - - - - - - {stock.stock_name || stock.stock_code} - - - - - {stock.stock_code} - - {realtimeQuotes[stock.stock_code] ? ( - 0 ? 'red' : 'green'} - fontSize="xs" - > - {realtimeQuotes[stock.stock_code].change_percent > 0 ? '+' : ''} - {realtimeQuotes[stock.stock_code].change_percent.toFixed(2)}% - - ) : stock.change_percent ? ( - 0 ? 'red' : 'green'} - fontSize="xs" - > - {stock.change_percent > 0 ? '+' : ''} - {stock.change_percent}% - - ) : null} - - - - - {realtimeQuotes[stock.stock_code]?.current_price?.toFixed(2) || stock.current_price || '--'} - - - {realtimeQuotes[stock.stock_code]?.update_time || stock.industry || '未分类'} - - - - - ))} - {watchlist.length > 10 && ( - - )} - - )} -
-
-
+ + + {/* 左右布局:左侧自适应,右侧固定200px */} + + {/* 左侧主内容区 */} + + {/* 市场概览仪表盘 */} + + + - {/* 中列:关注事件 */} - - {/* 关注事件 */} - - - - - - 关注事件 - - {followingEvents.length} - - - } - variant="ghost" - size="sm" - onClick={() => navigate('/community')} - aria-label="添加关注事件" - /> - - - - {followingEvents.length === 0 ? ( -
- - - - 暂无关注事件 - - - -
- ) : ( - - {followingEvents.slice(0, 5).map((event) => ( - - - - - {event.title} - - + {/* 投资规划中心 */} + + + - {/* 事件标签 */} - {event.tags && event.tags.length > 0 && ( - - {event.tags.slice(0, 3).map((tag, idx) => ( - - - {tag} - - - ))} - - )} + {/* 价值论坛 / 互动中心 */} + + + - {/* 事件统计 */} - - {event.related_avg_chg !== undefined && event.related_avg_chg !== null && ( - 0 ? 'red' : 'green'} - variant="subtle" - > - 平均超额 {event.related_avg_chg > 0 ? '+' : ''}{Number(event.related_avg_chg).toFixed(2)}% - - )} - - - {event.follower_count || 0} 关注 - - + {/* 投资规划中心(整合了日历、计划、复盘) */} + + + +
- {/* 事件信息 */} - - - - {event.creator?.username || '系统'} - · - {formatDate(event.created_at)} - - {event.exceed_expectation_score && ( - 70 ? 'red' : 'orange'} - variant="solid" - fontSize="xs" - > - 超预期 {event.exceed_expectation_score} - - )} - - - - ))} - {followingEvents.length > 5 && ( - - )} - - )} - - - - - - {/* 右列:我的评论 */} - - {/* 我的评论 */} - - - - - - 我的评论 - - {eventComments.length} - - - - - - {eventComments.length === 0 ? ( -
- - - - 暂无评论记录 - - - 参与事件讨论,分享您的观点 - - -
- ) : ( - - {eventComments.slice(0, 5).map((comment) => ( - - - - {comment.content} - - - - - {formatDate(comment.created_at)} - - {comment.event_title && ( - - - {comment.event_title} - - - )} - - - - ))} - {eventComments.length > 5 && ( - - 共 {eventComments.length} 条评论 - - )} - - )} -
-
-
-
- - {/* 投资规划中心(整合了日历、计划、复盘) */} - - - + {/* 右侧固定宽度侧边栏 */} + + navigate(`/company/${stock.stock_code}`)} + onEventClick={(event) => navigate(getEventDetailUrl(event.id))} + onAddStock={() => navigate('/stocks')} + onAddEvent={() => navigate('/community')} + /> + +
); From 18ba36a539d11700417e45d059c168f3fffe89e4 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 22 Dec 2025 18:57:28 +0800 Subject: [PATCH 7/7] =?UTF-8?q?refactor(Center):=20=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 目录重命名:Dashboard → Center(匹配路由 /home/center) - 删除遗留代码:Default.js、InvestmentPlansAndReviews.js、InvestmentCalendarChakra.js(共 2596 行) - 创建 src/types/center.ts 类型定义(15+ 接口) - 性能优化: - 创建 useCenterColors Hook 封装 7 个 useColorModeValue - 创建 utils/formatters.ts 提取纯函数 - 修复 loadRealtimeQuotes 的 useCallback 依赖项 - InvestmentPlanningCenter 添加 useMemo 缓存 - TypeScript 迁移:Center.js → Center.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/routes/lazy-components.js | 2 +- src/types/center.ts | 349 ++++ src/types/index.ts | 20 + .../Center.js => Center/Center.tsx} | 260 ++- .../components/CalendarPanel.tsx | 0 .../components/EventCard.tsx | 0 .../components/EventDetailModal.tsx | 0 .../components/EventEmptyState.tsx | 0 .../components/EventFormModal.less | 0 .../components/EventFormModal.tsx | 0 .../components/EventPanel.tsx | 0 .../components/InvestmentCalendar.less | 0 .../components/InvestmentPlanningCenter.tsx | 61 +- .../components/MyFutureEvents.js | 0 .../components/PlanningContext.tsx | 0 src/views/Center/hooks/index.ts | 5 + src/views/Center/hooks/useCenterColors.ts | 41 + src/views/Center/index.js | 4 + src/views/Center/utils/formatters.ts | 87 + src/views/Center/utils/index.ts | 11 + src/views/Dashboard/Default.js | 1421 ----------------- .../components/InvestmentCalendarChakra.js | 587 ------- .../components/InvestmentPlansAndReviews.js | 588 ------- 23 files changed, 658 insertions(+), 2778 deletions(-) create mode 100644 src/types/center.ts rename src/views/{Dashboard/Center.js => Center/Center.tsx} (61%) rename src/views/{Dashboard => Center}/components/CalendarPanel.tsx (100%) rename src/views/{Dashboard => Center}/components/EventCard.tsx (100%) rename src/views/{Dashboard => Center}/components/EventDetailModal.tsx (100%) rename src/views/{Dashboard => Center}/components/EventEmptyState.tsx (100%) rename src/views/{Dashboard => Center}/components/EventFormModal.less (100%) rename src/views/{Dashboard => Center}/components/EventFormModal.tsx (100%) rename src/views/{Dashboard => Center}/components/EventPanel.tsx (100%) rename src/views/{Dashboard => Center}/components/InvestmentCalendar.less (100%) rename src/views/{Dashboard => Center}/components/InvestmentPlanningCenter.tsx (87%) rename src/views/{Dashboard => Center}/components/MyFutureEvents.js (100%) rename src/views/{Dashboard => Center}/components/PlanningContext.tsx (100%) create mode 100644 src/views/Center/hooks/index.ts create mode 100644 src/views/Center/hooks/useCenterColors.ts create mode 100644 src/views/Center/index.js create mode 100644 src/views/Center/utils/formatters.ts create mode 100644 src/views/Center/utils/index.ts delete mode 100755 src/views/Dashboard/Default.js delete mode 100644 src/views/Dashboard/components/InvestmentCalendarChakra.js delete mode 100644 src/views/Dashboard/components/InvestmentPlansAndReviews.js diff --git a/src/routes/lazy-components.js b/src/routes/lazy-components.js index 37fed848..b383e89a 100644 --- a/src/routes/lazy-components.js +++ b/src/routes/lazy-components.js @@ -11,7 +11,7 @@ export const lazyComponents = { // Home 模块 // ⚡ 直接引用 HomePage,无需中间层(静态页面不需要骨架屏) HomePage: React.lazy(() => import('@views/Home/HomePage')), - CenterDashboard: React.lazy(() => import('@views/Dashboard/Center')), + CenterDashboard: React.lazy(() => import('@views/Center')), ProfilePage: React.lazy(() => import('@views/Profile/ProfilePage')), // 价值论坛 - 我的积分页面 ForumMyPoints: React.lazy(() => import('@views/Profile')), diff --git a/src/types/center.ts b/src/types/center.ts new file mode 100644 index 00000000..a0e377a4 --- /dev/null +++ b/src/types/center.ts @@ -0,0 +1,349 @@ +/** + * Center(个人中心)模块类型定义 + * + * 包含自选股、实时行情、关注事件等类型 + */ + +import type { NavigateFunction } from 'react-router-dom'; + +// ============================================================ +// Dashboard Events Hook 类型定义 +// ============================================================ + +/** + * useDashboardEvents Hook 配置选项 + */ +export interface DashboardEventsOptions { + /** 页面类型 */ + pageType?: 'center' | 'profile' | 'settings'; + /** 路由导航函数 */ + navigate?: NavigateFunction; +} + +/** + * useDashboardEvents Hook 返回值 + */ +export interface DashboardEventsResult { + /** 追踪功能卡片点击 */ + trackFunctionCardClicked: (cardName: string, cardData?: { count?: number }) => void; + /** 追踪自选股列表查看 */ + trackWatchlistViewed: (stockCount?: number, hasRealtime?: boolean) => void; + /** 追踪自选股点击 */ + trackWatchlistStockClicked: (stock: { code: string; name?: string }, position?: number) => void; + /** 追踪自选股添加 */ + trackWatchlistStockAdded: (stock: { code: string; name?: string }, source?: string) => void; + /** 追踪自选股移除 */ + trackWatchlistStockRemoved: (stock: { code: string; name?: string }) => void; + /** 追踪关注事件列表查看 */ + trackFollowingEventsViewed: (eventCount?: number) => void; + /** 追踪关注事件点击 */ + trackFollowingEventClicked: (event: { id: number; title?: string }, position?: number) => void; + /** 追踪评论列表查看 */ + trackCommentsViewed: (commentCount?: number) => void; + /** 追踪订阅信息查看 */ + trackSubscriptionViewed: (subscription?: { plan?: string; status?: string }) => void; + /** 追踪升级按钮点击 */ + trackUpgradePlanClicked: (currentPlan?: string, targetPlan?: string, source?: string) => void; + /** 追踪个人资料更新 */ + trackProfileUpdated: (updatedFields?: string[]) => void; + /** 追踪设置更改 */ + trackSettingChanged: (settingName: string, oldValue: unknown, newValue: unknown) => void; +} + +/** + * 自选股项目 + * 来自 /api/account/watchlist 接口 + */ +export interface WatchlistItem { + /** 股票代码(如 '600000.SH') */ + stock_code: string; + + /** 股票名称 */ + stock_name: string; + + /** 当前价格 */ + current_price?: number; + + /** 涨跌幅(百分比) */ + change_percent?: number; + + /** 添加时间 */ + created_at?: string; + + /** 备注 */ + note?: string; +} + +/** + * 实时行情数据 + * 来自 /api/account/watchlist/realtime 接口 + */ +export interface RealtimeQuote { + /** 股票代码 */ + stock_code: string; + + /** 当前价格 */ + current_price: number; + + /** 涨跌幅(百分比) */ + change_percent: number; + + /** 涨跌额 */ + change_amount?: number; + + /** 成交量 */ + volume?: number; + + /** 成交额 */ + amount?: number; + + /** 最高价 */ + high?: number; + + /** 最低价 */ + low?: number; + + /** 开盘价 */ + open?: number; + + /** 昨收价 */ + pre_close?: number; + + /** 更新时间戳 */ + timestamp?: number; +} + +/** + * 实时行情映射表 + * key 为股票代码,value 为行情数据 + */ +export type RealtimeQuotesMap = Record; + +/** + * 关注的事件 + * 来自 /api/account/events/following 接口 + */ +export interface FollowingEvent { + /** 事件 ID */ + id: number; + + /** 事件标题 */ + title: string; + + /** 关注人数 */ + follower_count?: number; + + /** 相关股票平均涨跌幅(百分比) */ + related_avg_chg?: number; + + /** 事件类型 */ + event_type?: string; + + /** 发生日期 */ + event_date?: string; + + /** 事件描述 */ + description?: string; + + /** 相关股票列表 */ + related_stocks?: Array<{ + code: string; + name: string; + change_percent?: number; + }>; + + /** 创建时间 */ + created_at?: string; +} + +/** + * 用户评论记录 + * 来自 /api/account/events/posts 接口 + */ +export interface EventComment { + /** 评论 ID */ + id: number; + + /** 评论内容 */ + content: string; + + /** 关联事件 ID */ + event_id: number; + + /** 关联事件标题 */ + event_title?: string; + + /** 点赞数 */ + like_count?: number; + + /** 回复数 */ + reply_count?: number; + + /** 创建时间 */ + created_at: string; + + /** 更新时间 */ + updated_at?: string; +} + +// ============================================================ +// 组件 Props 类型定义 +// ============================================================ + +/** + * WatchSidebar 组件 Props + */ +export interface WatchSidebarProps { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据(按股票代码索引) */ + realtimeQuotes: RealtimeQuotesMap; + + /** 关注的事件列表 */ + followingEvents: FollowingEvent[]; + + /** 点击股票回调 */ + onStockClick?: (stock: WatchlistItem) => void; + + /** 点击事件回调 */ + onEventClick?: (event: FollowingEvent) => void; + + /** 添加股票回调 */ + onAddStock?: () => void; + + /** 添加事件回调 */ + onAddEvent?: () => void; +} + +/** + * WatchlistPanel 组件 Props + */ +export interface WatchlistPanelProps { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据 */ + realtimeQuotes: RealtimeQuotesMap; + + /** 点击股票回调 */ + onStockClick?: (stock: WatchlistItem) => void; + + /** 添加股票回调 */ + onAddStock?: () => void; +} + +/** + * FollowingEventsPanel 组件 Props + */ +export interface FollowingEventsPanelProps { + /** 事件列表 */ + events: FollowingEvent[]; + + /** 点击事件回调 */ + onEventClick?: (event: FollowingEvent) => void; + + /** 添加事件回调 */ + onAddEvent?: () => void; +} + +// ============================================================ +// Hooks 返回值类型定义 +// ============================================================ + +/** + * useCenterColors Hook 返回值 + * 封装 Center 模块的所有颜色变量 + */ +export interface CenterColors { + /** 主要文本颜色 */ + textColor: string; + + /** 边框颜色 */ + borderColor: string; + + /** 背景颜色 */ + bgColor: string; + + /** 悬停背景色 */ + hoverBg: string; + + /** 次要文本颜色 */ + secondaryText: string; + + /** 卡片背景色 */ + cardBg: string; + + /** 区块背景色 */ + sectionBg: string; +} + +/** + * useCenterData Hook 返回值 + * 封装 Center 页面的数据加载逻辑 + */ +export interface UseCenterDataResult { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据 */ + realtimeQuotes: RealtimeQuotesMap; + + /** 关注的事件列表 */ + followingEvents: FollowingEvent[]; + + /** 用户评论列表 */ + eventComments: EventComment[]; + + /** 加载状态 */ + loading: boolean; + + /** 行情加载状态 */ + quotesLoading: boolean; + + /** 刷新数据 */ + refresh: () => Promise; + + /** 刷新实时行情 */ + refreshQuotes: () => Promise; +} + +// ============================================================ +// API 响应类型定义 +// ============================================================ + +/** + * 自选股列表 API 响应 + */ +export interface WatchlistApiResponse { + success: boolean; + data: WatchlistItem[]; + message?: string; +} + +/** + * 实时行情 API 响应 + */ +export interface RealtimeQuotesApiResponse { + success: boolean; + data: RealtimeQuote[]; + message?: string; +} + +/** + * 关注事件 API 响应 + */ +export interface FollowingEventsApiResponse { + success: boolean; + data: FollowingEvent[]; + message?: string; +} + +/** + * 用户评论 API 响应 + */ +export interface EventCommentsApiResponse { + success: boolean; + data: EventComment[]; + message?: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index 4c1540e9..0ea713f8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -63,3 +63,23 @@ export type { PlanFormData, PlanningContextValue, } from './investment'; + +// Center(个人中心)相关类型 +export type { + DashboardEventsOptions, + DashboardEventsResult, + WatchlistItem, + RealtimeQuote, + RealtimeQuotesMap, + FollowingEvent, + EventComment, + WatchSidebarProps, + WatchlistPanelProps, + FollowingEventsPanelProps, + CenterColors, + UseCenterDataResult, + WatchlistApiResponse, + RealtimeQuotesApiResponse, + FollowingEventsApiResponse, + EventCommentsApiResponse, +} from './center'; diff --git a/src/views/Dashboard/Center.js b/src/views/Center/Center.tsx similarity index 61% rename from src/views/Dashboard/Center.js rename to src/views/Center/Center.tsx index 4e855d82..345c87c4 100644 --- a/src/views/Dashboard/Center.js +++ b/src/views/Center/Center.tsx @@ -1,66 +1,26 @@ -// src/views/Dashboard/Center.js -import React, { useEffect, useState, useCallback } from 'react'; -import { logger } from '../../utils/logger'; -import { getApiBase } from '../../utils/apiConfig'; -import { useDashboardEvents } from '../../hooks/useDashboardEvents'; +/** + * Center - 个人中心仪表板主页面 + * + * 对应路由:/home/center + * 功能:自选股监控、关注事件、投资规划等 + */ + +import React, { useEffect, useState, useCallback, useRef } from 'react'; +import { logger } from '@/utils/logger'; +import { getApiBase } from '@/utils/apiConfig'; +import { useDashboardEvents } from '@/hooks/useDashboardEvents'; import { Box, Flex, - Grid, - SimpleGrid, - Stack, Text, - Badge, - Button, VStack, - HStack, - Card, - CardHeader, - CardBody, - Heading, - useColorModeValue, - Icon, - IconButton, - Stat, - StatLabel, - StatNumber, - StatHelpText, - StatArrow, - Divider, - Tag, - TagLabel, - TagLeftIcon, - Wrap, - WrapItem, - Avatar, - Tooltip, - Progress, useToast, - LinkBox, - LinkOverlay, Spinner, Center, - Image, } from '@chakra-ui/react'; -import { useAuth } from '../../contexts/AuthContext'; -import { useLocation, useNavigate, Link } from 'react-router-dom'; -import { - FiTrendingUp, - FiEye, - FiMessageSquare, - FiThumbsUp, - FiClock, - FiCalendar, - FiRefreshCw, - FiTrash2, - FiExternalLink, - FiPlus, - FiBarChart2, - FiStar, - FiActivity, - FiAlertCircle, - FiUsers, -} from 'react-icons/fi'; +import { useCenterColors } from './hooks'; +import { useAuth } from '@/contexts/AuthContext'; +import { useLocation, useNavigate } from 'react-router-dom'; import InvestmentPlanningCenter from './components/InvestmentPlanningCenter'; import { getEventDetailUrl } from '@/utils/idEncoder'; import MarketDashboard from '@views/Profile/components/MarketDashboard'; @@ -69,38 +29,87 @@ import ForumCenter from '@views/Profile/components/ForumCenter'; import WatchSidebar from '@views/Profile/components/WatchSidebar'; import { THEME } from '@views/Profile/components/MarketDashboard/constants'; -export default function CenterDashboard() { +import type { + WatchlistItem, + RealtimeQuotesMap, + FollowingEvent, + EventComment, + WatchlistApiResponse, + RealtimeQuotesApiResponse, + FollowingEventsApiResponse, + EventCommentsApiResponse, + DashboardEventsResult, +} from '@/types'; + +/** + * CenterDashboard 组件 + * 个人中心仪表板主页面 + */ +const CenterDashboard: React.FC = () => { const { user } = useAuth(); const location = useLocation(); const navigate = useNavigate(); const toast = useToast(); - // ⚡ 提取 userId 为独立变量 + // 提取 userId 为独立变量(优化依赖项) const userId = user?.id; - // 🎯 初始化Dashboard埋点Hook + // 初始化 Dashboard 埋点 Hook(类型断言为 DashboardEventsResult) const dashboardEvents = useDashboardEvents({ pageType: 'center', navigate - }); + }) as DashboardEventsResult; - // 颜色主题 - const textColor = useColorModeValue('gray.700', 'white'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const bgColor = useColorModeValue('white', 'gray.800'); - const hoverBg = useColorModeValue('gray.50', 'gray.700'); - const secondaryText = useColorModeValue('gray.600', 'gray.400'); - const cardBg = useColorModeValue('white', 'gray.800'); - const sectionBg = useColorModeValue('gray.50', 'gray.900'); + // 颜色主题(使用 useCenterColors 封装,避免 7 次 useColorModeValue 调用) + const { secondaryText } = useCenterColors(); - const [watchlist, setWatchlist] = useState([]); - const [realtimeQuotes, setRealtimeQuotes] = useState({}); - const [followingEvents, setFollowingEvents] = useState([]); - const [eventComments, setEventComments] = useState([]); - const [loading, setLoading] = useState(true); - const [quotesLoading, setQuotesLoading] = useState(false); + // 数据状态 + const [watchlist, setWatchlist] = useState([]); + const [realtimeQuotes, setRealtimeQuotes] = useState({}); + const [followingEvents, setFollowingEvents] = useState([]); + const [eventComments, setEventComments] = useState([]); + const [loading, setLoading] = useState(true); + const [quotesLoading, setQuotesLoading] = useState(false); - const loadData = useCallback(async () => { + // 使用 ref 跟踪是否已经加载过数据(首次加载标记) + const hasLoadedRef = useRef(false); + + /** + * 加载实时行情 + */ + const loadRealtimeQuotes = useCallback(async (): Promise => { + try { + setQuotesLoading(true); + const base = getApiBase(); + const response = await fetch(base + '/api/account/watchlist/realtime', { + credentials: 'include', + cache: 'no-store' + }); + + if (response.ok) { + const data: RealtimeQuotesApiResponse = await response.json(); + if (data.success) { + const quotesMap: RealtimeQuotesMap = {}; + data.data.forEach(item => { + quotesMap[item.stock_code] = item; + }); + setRealtimeQuotes(quotesMap); + } + } + } catch (error) { + logger.error('Center', 'loadRealtimeQuotes', error, { + userId, + timestamp: new Date().toISOString() + }); + } finally { + setQuotesLoading(false); + } + }, [userId]); + + /** + * 加载所有数据(自选股、关注事件、评论) + */ + const loadData = useCallback(async (): Promise => { try { const base = getApiBase(); const ts = Date.now(); @@ -111,14 +120,15 @@ export default function CenterDashboard() { fetch(base + `/api/account/events/posts?_=${ts}`, { credentials: 'include', cache: 'no-store' }), ]); - const jw = await w.json(); - const je = await e.json(); - const jc = await c.json(); + const jw: WatchlistApiResponse = await w.json(); + const je: FollowingEventsApiResponse = await e.json(); + const jc: EventCommentsApiResponse = await c.json(); + if (jw.success) { const watchlistData = Array.isArray(jw.data) ? jw.data : []; setWatchlist(watchlistData); - // 🎯 追踪自选股列表查看 + // 追踪自选股列表查看 if (watchlistData.length > 0) { dashboardEvents.trackWatchlistViewed(watchlistData.length, true); } @@ -128,18 +138,20 @@ export default function CenterDashboard() { loadRealtimeQuotes(); } } + if (je.success) { const eventsData = Array.isArray(je.data) ? je.data : []; setFollowingEvents(eventsData); - // 🎯 追踪关注的事件列表查看 + // 追踪关注的事件列表查看 dashboardEvents.trackFollowingEventsViewed(eventsData.length); } + if (jc.success) { const commentsData = Array.isArray(jc.data) ? jc.data : []; setEventComments(commentsData); - // 🎯 追踪评论列表查看 + // 追踪评论列表查看 dashboardEvents.trackCommentsViewed(commentsData.length); } } catch (err) { @@ -150,82 +162,9 @@ export default function CenterDashboard() { } finally { setLoading(false); } - }, [userId]); // ⚡ 使用 userId 而不是 user?.id - - // 加载实时行情 - const loadRealtimeQuotes = useCallback(async () => { - try { - setQuotesLoading(true); - const base = getApiBase(); - const response = await fetch(base + '/api/account/watchlist/realtime', { - credentials: 'include', - cache: 'no-store' - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - const quotesMap = {}; - data.data.forEach(item => { - quotesMap[item.stock_code] = item; - }); - setRealtimeQuotes(quotesMap); - } - } - } catch (error) { - logger.error('Center', 'loadRealtimeQuotes', error, { - userId: user?.id, - watchlistLength: watchlist.length - }); - } finally { - setQuotesLoading(false); - } - }, []); - - // 格式化日期 - const formatDate = (dateString) => { - if (!dateString) return ''; - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now - date); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays < 1) { - const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); - if (diffHours < 1) { - const diffMinutes = Math.ceil(diffTime / (1000 * 60)); - return `${diffMinutes}分钟前`; - } - return `${diffHours}小时前`; - } else if (diffDays < 7) { - return `${diffDays}天前`; - } else { - return date.toLocaleDateString('zh-CN'); - } - }; - - // 格式化数字 - const formatNumber = (num) => { - if (!num) return '0'; - if (num >= 10000) { - return (num / 10000).toFixed(1) + 'w'; - } else if (num >= 1000) { - return (num / 1000).toFixed(1) + 'k'; - } - return num.toString(); - }; - - // 获取事件热度颜色 - const getHeatColor = (score) => { - if (score >= 80) return 'red'; - if (score >= 60) return 'orange'; - if (score >= 40) return 'yellow'; - return 'green'; - }; - - // 🔧 使用 ref 跟踪是否已经加载过数据(首次加载标记) - const hasLoadedRef = React.useRef(false); + }, [userId, loadRealtimeQuotes, dashboardEvents]); + // 首次加载和页面可见性变化时加载数据 useEffect(() => { const isOnCenterPage = location.pathname.includes('/home/center'); @@ -236,12 +175,13 @@ export default function CenterDashboard() { loadData(); } - const onVis = () => { + const onVis = (): void => { if (document.visibilityState === 'visible' && location.pathname.includes('/home/center')) { console.log('[Center] 👁️ visibilitychange 触发 loadData'); loadData(); } }; + document.addEventListener('visibilitychange', onVis); return () => document.removeEventListener('visibilitychange', onVis); }, [userId, location.pathname, loadData, user]); @@ -259,7 +199,7 @@ export default function CenterDashboard() { const interval = setInterval(() => { loadRealtimeQuotes(); }, 60000); // 60秒刷新一次 - + return () => clearInterval(interval); } }, [watchlist.length, loadRealtimeQuotes]); @@ -327,8 +267,8 @@ export default function CenterDashboard() { watchlist={watchlist} realtimeQuotes={realtimeQuotes} followingEvents={followingEvents} - onStockClick={(stock) => navigate(`/company/${stock.stock_code}`)} - onEventClick={(event) => navigate(getEventDetailUrl(event.id))} + onStockClick={(stock: WatchlistItem) => navigate(`/company/${stock.stock_code}`)} + onEventClick={(event: FollowingEvent) => navigate(getEventDetailUrl(event.id))} onAddStock={() => navigate('/stocks')} onAddEvent={() => navigate('/community')} /> @@ -337,6 +277,6 @@ export default function CenterDashboard() {
); -} - +}; +export default CenterDashboard; diff --git a/src/views/Dashboard/components/CalendarPanel.tsx b/src/views/Center/components/CalendarPanel.tsx similarity index 100% rename from src/views/Dashboard/components/CalendarPanel.tsx rename to src/views/Center/components/CalendarPanel.tsx diff --git a/src/views/Dashboard/components/EventCard.tsx b/src/views/Center/components/EventCard.tsx similarity index 100% rename from src/views/Dashboard/components/EventCard.tsx rename to src/views/Center/components/EventCard.tsx diff --git a/src/views/Dashboard/components/EventDetailModal.tsx b/src/views/Center/components/EventDetailModal.tsx similarity index 100% rename from src/views/Dashboard/components/EventDetailModal.tsx rename to src/views/Center/components/EventDetailModal.tsx diff --git a/src/views/Dashboard/components/EventEmptyState.tsx b/src/views/Center/components/EventEmptyState.tsx similarity index 100% rename from src/views/Dashboard/components/EventEmptyState.tsx rename to src/views/Center/components/EventEmptyState.tsx diff --git a/src/views/Dashboard/components/EventFormModal.less b/src/views/Center/components/EventFormModal.less similarity index 100% rename from src/views/Dashboard/components/EventFormModal.less rename to src/views/Center/components/EventFormModal.less diff --git a/src/views/Dashboard/components/EventFormModal.tsx b/src/views/Center/components/EventFormModal.tsx similarity index 100% rename from src/views/Dashboard/components/EventFormModal.tsx rename to src/views/Center/components/EventFormModal.tsx diff --git a/src/views/Dashboard/components/EventPanel.tsx b/src/views/Center/components/EventPanel.tsx similarity index 100% rename from src/views/Dashboard/components/EventPanel.tsx rename to src/views/Center/components/EventPanel.tsx diff --git a/src/views/Dashboard/components/InvestmentCalendar.less b/src/views/Center/components/InvestmentCalendar.less similarity index 100% rename from src/views/Dashboard/components/InvestmentCalendar.less rename to src/views/Center/components/InvestmentCalendar.less diff --git a/src/views/Dashboard/components/InvestmentPlanningCenter.tsx b/src/views/Center/components/InvestmentPlanningCenter.tsx similarity index 87% rename from src/views/Dashboard/components/InvestmentPlanningCenter.tsx rename to src/views/Center/components/InvestmentPlanningCenter.tsx index 06899ff7..92402ac1 100644 --- a/src/views/Dashboard/components/InvestmentPlanningCenter.tsx +++ b/src/views/Center/components/InvestmentPlanningCenter.tsx @@ -12,7 +12,7 @@ * - PlanningContext (数据共享层) */ -import React, { useState, useEffect, useCallback, Suspense, lazy } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react'; import { Box, Card, @@ -119,27 +119,46 @@ const InvestmentPlanningCenter: React.FC = () => { loadAllData(); }, [loadAllData]); - // 提供给子组件的 Context 值 - const contextValue: PlanningContextValue = { - allEvents, - setAllEvents, - loadAllData, - loading, - setLoading, - openPlanModalTrigger, - openReviewModalTrigger, - toast, - borderColor, - textColor, - secondaryText, - cardBg, - setViewMode, - setListTab, - }; + // 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染) + const contextValue: PlanningContextValue = useMemo( + () => ({ + allEvents, + setAllEvents, + loadAllData, + loading, + setLoading, + openPlanModalTrigger, + openReviewModalTrigger, + toast, + borderColor, + textColor, + secondaryText, + cardBg, + setViewMode, + setListTab, + }), + [ + allEvents, + loadAllData, + loading, + openPlanModalTrigger, + openReviewModalTrigger, + toast, + borderColor, + textColor, + secondaryText, + cardBg, + ] + ); - // 计算各类型事件数量 - const planCount = allEvents.filter(e => e.type === 'plan').length; - const reviewCount = allEvents.filter(e => e.type === 'review').length; + // 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组) + const { planCount, reviewCount } = useMemo( + () => ({ + planCount: allEvents.filter(e => e.type === 'plan').length, + reviewCount: allEvents.filter(e => e.type === 'review').length, + }), + [allEvents] + ); return ( diff --git a/src/views/Dashboard/components/MyFutureEvents.js b/src/views/Center/components/MyFutureEvents.js similarity index 100% rename from src/views/Dashboard/components/MyFutureEvents.js rename to src/views/Center/components/MyFutureEvents.js diff --git a/src/views/Dashboard/components/PlanningContext.tsx b/src/views/Center/components/PlanningContext.tsx similarity index 100% rename from src/views/Dashboard/components/PlanningContext.tsx rename to src/views/Center/components/PlanningContext.tsx diff --git a/src/views/Center/hooks/index.ts b/src/views/Center/hooks/index.ts new file mode 100644 index 00000000..7dfe76df --- /dev/null +++ b/src/views/Center/hooks/index.ts @@ -0,0 +1,5 @@ +/** + * Center 模块 Hooks 导出 + */ + +export { useCenterColors, default as useCenterColorsDefault } from './useCenterColors'; diff --git a/src/views/Center/hooks/useCenterColors.ts b/src/views/Center/hooks/useCenterColors.ts new file mode 100644 index 00000000..db2d0edf --- /dev/null +++ b/src/views/Center/hooks/useCenterColors.ts @@ -0,0 +1,41 @@ +/** + * useCenterColors Hook + * + * 封装 Center 模块的所有颜色变量,避免每次渲染重复调用 useColorModeValue + * 将 7 次 hook 调用合并为 1 次 useMemo 计算 + */ + +import { useMemo } from 'react'; +import { useColorModeValue } from '@chakra-ui/react'; +import type { CenterColors } from '@/types/center'; + +/** + * 获取 Center 模块的颜色配置 + * 使用 useMemo 缓存结果,避免每次渲染重新计算 + */ +export function useCenterColors(): CenterColors { + // 获取当前主题模式下的基础颜色 + const textColor = useColorModeValue('gray.700', 'white'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + const bgColor = useColorModeValue('white', 'gray.800'); + const hoverBg = useColorModeValue('gray.50', 'gray.700'); + const secondaryText = useColorModeValue('gray.600', 'gray.400'); + const cardBg = useColorModeValue('white', 'gray.800'); + const sectionBg = useColorModeValue('gray.50', 'gray.900'); + + // 使用 useMemo 缓存颜色对象,只在颜色值变化时重新创建 + return useMemo( + () => ({ + textColor, + borderColor, + bgColor, + hoverBg, + secondaryText, + cardBg, + sectionBg, + }), + [textColor, borderColor, bgColor, hoverBg, secondaryText, cardBg, sectionBg] + ); +} + +export default useCenterColors; diff --git a/src/views/Center/index.js b/src/views/Center/index.js new file mode 100644 index 00000000..37dca6a2 --- /dev/null +++ b/src/views/Center/index.js @@ -0,0 +1,4 @@ +// src/views/Center/index.js +// 入口文件,导出 Center 组件 + +export { default } from './Center'; diff --git a/src/views/Center/utils/formatters.ts b/src/views/Center/utils/formatters.ts new file mode 100644 index 00000000..19c2af30 --- /dev/null +++ b/src/views/Center/utils/formatters.ts @@ -0,0 +1,87 @@ +/** + * Center 模块格式化工具函数 + * + * 这些是纯函数,提取到组件外部避免每次渲染重建 + */ + +/** + * 格式化相对时间(如 "5分钟前"、"3天前") + * @param dateString 日期字符串 + * @returns 格式化后的相对时间字符串 + */ +export function formatRelativeTime(dateString: string | null | undefined): string { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays < 1) { + const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); + if (diffHours < 1) { + const diffMinutes = Math.ceil(diffTime / (1000 * 60)); + return `${diffMinutes}分钟前`; + } + return `${diffHours}小时前`; + } else if (diffDays < 7) { + return `${diffDays}天前`; + } else { + return date.toLocaleDateString('zh-CN'); + } +} + +/** + * 格式化数字(如 10000 → "1w",1500 → "1.5k") + * @param num 数字 + * @returns 格式化后的字符串 + */ +export function formatCompactNumber(num: number | null | undefined): string { + if (!num) return '0'; + + if (num >= 10000) { + return (num / 10000).toFixed(1) + 'w'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'k'; + } + return num.toString(); +} + +/** + * 根据热度分数获取颜色 + * @param score 热度分数 (0-100) + * @returns Chakra UI 颜色名称 + */ +export function getHeatColor(score: number): string { + if (score >= 80) return 'red'; + if (score >= 60) return 'orange'; + if (score >= 40) return 'yellow'; + return 'green'; +} + +/** + * 根据涨跌幅获取颜色 + * @param changePercent 涨跌幅百分比 + * @returns 颜色值 + */ +export function getChangeColor(changePercent: number | null | undefined): string { + if (changePercent === null || changePercent === undefined) { + return 'rgba(255, 255, 255, 0.6)'; + } + if (changePercent > 0) return '#EF4444'; // 红色(涨) + if (changePercent < 0) return '#22C55E'; // 绿色(跌) + return 'rgba(255, 255, 255, 0.6)'; // 灰色(平) +} + +/** + * 格式化涨跌幅显示 + * @param changePercent 涨跌幅百分比 + * @returns 格式化后的字符串(如 "+5.23%") + */ +export function formatChangePercent(changePercent: number | null | undefined): string { + if (changePercent === null || changePercent === undefined) { + return '--'; + } + const prefix = changePercent > 0 ? '+' : ''; + return `${prefix}${Number(changePercent).toFixed(2)}%`; +} diff --git a/src/views/Center/utils/index.ts b/src/views/Center/utils/index.ts new file mode 100644 index 00000000..ebecc059 --- /dev/null +++ b/src/views/Center/utils/index.ts @@ -0,0 +1,11 @@ +/** + * Center 模块工具函数导出 + */ + +export { + formatRelativeTime, + formatCompactNumber, + getHeatColor, + getChangeColor, + formatChangePercent, +} from './formatters'; diff --git a/src/views/Dashboard/Default.js b/src/views/Dashboard/Default.js deleted file mode 100755 index 81353909..00000000 --- a/src/views/Dashboard/Default.js +++ /dev/null @@ -1,1421 +0,0 @@ -/*! - -========================================================= -* Argon Dashboard Chakra PRO - v1.0.0 -========================================================= - -* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro -* Copyright 2022 Creative Tim (https://www.creative-tim.com/) - -* Designed and Coded by Simmmple & Creative Tim - -========================================================= - -* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -*/ - -// Chakra imports -import { - Avatar, - AvatarGroup, - Badge, - Box, - Button, - Checkbox, - Flex, - Grid, - Icon, - Image, - Input, - Progress, - SimpleGrid, - Spacer, - Stack, - Stat, - StatLabel, - StatNumber, - Table, - Tbody, - Td, - Text, - Th, - Thead, - Tr, - useColorMode, - useColorModeValue, -} from '@chakra-ui/react'; -import avatar10 from 'assets/img/avatars/avatar10.png'; -import avatar2 from 'assets/img/avatars/avatar2.png'; -import avatar3 from 'assets/img/avatars/avatar3.png'; -import avatar4 from 'assets/img/avatars/avatar4.png'; -import avatar5 from 'assets/img/avatars/avatar5.png'; -import handBg from 'assets/img/hand-background.png'; -import teamsImage from 'assets/img/teams-image.png'; -// Custom components -import Card from 'components/Card/Card.js'; -import CardBody from 'components/Card/CardBody.js'; -import CardHeader from 'components/Card/CardHeader.js'; -import LineChart from 'components/Charts/LineChart'; -import IconBox from 'components/Icons/IconBox'; -import { BitcoinLogo } from 'components/Icons/Icons'; -// Custom icons -import { - AdobexdLogo, - CartIcon, - DocumentIcon, - GlobeIcon, - JiraLogo, - RocketIcon, - SettingsIcon, - SlackLogo, - SpotifyLogo, - WalletIcon, -} from 'components/Icons/Icons.js'; -import { HSeparator } from 'components/Separator/Separator'; -import TablesReportsRow from 'components/Tables/TablesReportsRow'; -import TablesTableRow from 'components/Tables/TablesTableRow'; -import React from 'react'; -import { AiFillLike, AiOutlinePlus } from 'react-icons/ai'; -import { - FaChevronDown, - FaChevronUp, - FaCommentDots, - FaUser, -} from 'react-icons/fa'; -import { IoMdShareAlt } from 'react-icons/io'; -import { IoBulb } from 'react-icons/io5'; -import { RiArrowDropRightLine } from 'react-icons/ri'; -import { - lineChartDataDefault, - lineChartOptionsDefault, -} from 'variables/charts'; -import { tablesReportsData, tablesTableData } from 'variables/general'; - -export default function Default() { - // Chakra Color Mode - const iconBlue = useColorModeValue('blue.500', 'blue.500'); - const iconBoxInside = useColorModeValue('white', 'white'); - const textColor = useColorModeValue('gray.700', 'white'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const cardColor = useColorModeValue('gray.800', 'navy.800'); - const bgBox = useColorModeValue('gray.800', 'blue.500'); - - const { colorMode } = useColorMode(); - - return ( - - - - - - - - Today's Money - - - - $53,897 - - - - - - - - - - +3.48%{' '} - - Since last month - - - - - - - - - Today's Users - - - - $3,200 - - - - - - - - - - +5.2%{' '} - - Since last month - - - - - - - - - New Clients - - - - +2,503 - - - - - - - - - - -2.82%{' '} - - Since last month - - - - - - - - - Total Sales - - - - $173,000 - - - - - - - - - - +8.12%{' '} - - Since last month - - - - - - - - - - - - Get started with Argon - - - Start your development process with an innovative admin dashboard! - - - - - - - Sales Overview - - - - (+5%) more - {' '} - in 2022 - - - - - - - - - - Team Members - - - - - - - - Esthera Jackson - - - Online - - - - - - - - - - - Esthera Jackson - - - In meeting - - - - - - - - - - - Esthera Jackson - - - Offline - - - - - - - - - - - Esthera Jackson - - - Online - - - - - - - - - - - - - To Do List - - - - - - Call with Dave - - - 09:30 AM - - - - - - - - Brunch Meeting - - - 11:00 AM - - - - - - - - Argon Dashboard Launch - - - 02:00 PM - - - - - - - - Winter Hackaton - - - 11:30 AM - - - - - - - - - - - Progress Track - - - - - - - React Material Dashboard - - - - - - - - - Argon Design System - - - - - - - - - VueJs Now UI Kit PRO - - - - - - - - - Soft UI Dashboard - - - - - - - - - - - - - - - - - - - Esthera Jackson - - - 3 days ago - - - - - - - - - Personal profiles are the perfect way for you to grab their - attention and persuade recruiters to continue reading your CV - because you’re telling them from the off exactly why they should - hire you. - - - - - - - 1502 - - - - 36 - - - - 12 - - - - - - - - - - - and 30+ more - - - - - - - - - - - Michael Lewis - - - I always felt like I could do anything. That’s the main thing - people are controlled by! Thoughts- their perception of - themselves! - - - - - 3 likes - - - - 2 shares - - - - - - - - - - - Jessica Stones - - - Society has put up so many boundaries, so many limitations on - what’s right and wrong that it’s almost impossible to get a - pure thought out. It’s like a little kid, a little boy. - - - - - 10 likes - - - - 1 share - - - - - - - - - - - Anthony Joshua - - - It's all about work ! Great ideas mean nothing if they aren't - realised by hungry, desiring people. - - - - - 42 likes - - - - 6 shares - - - - - - - - - - - - - - - - - - - - - - - - - - - - {tablesReportsData - .filter((_, idx) => idx < 4) - .map((row, index, arr) => { - return ( - - ); - })} - -
- Author - - Function - - Review - - Employed - - Date - - Id -
-
-
- - - - - - ${' '} - - 3,300 - - - Your current balance - - - - +15%{' '} - - ($250) - - - - - - Orders: 60% - - - - - - Sales: 40% - - - - - - - - - - - - Active - - - - Address - - - 0yx8Wkasd8uWpa083Jj81qZhs923K21 - - - - - Name - - - John Snow - - - - - - - - - - - - - - - - - - - - - Sales by Country - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Country: - - Sales: - - Value: - - Bounce: -
- - 🇺🇸 - - United States - - - - - 2500 - - - - $214,000 - - - - 40,22% - -
- - 🇩🇪 - - Germany - - - - - 3900 - - - - $446,700 - - - - 19,22% - -
- - 🇬🇧 - - Great Britain - - - - - 1300 - - - - $121,900 - - - - 39,22% - -
- - 🇧🇷 - - Brasil - - - - - 920 - - - - $52,100 - - - {' '} - - 29,9% - -
-
-
-
-
- - - - - - - - - - - - - - - {tablesTableData.map((row, index, arr) => { - return ( - - - - ); - })} - -
- Author - - Function - - Status - - Employed -
-
-
- - - - Categories - - - - - - - - - - - - Devices - - - 250 in stock,{' '} - - 346+ sold - - - - - - - - - - - - - - - Tickets - - - 123 closed,{' '} - - 15 open - - - - - - - - - - - - - - - Error logs - - - 1 is active,{' '} - - 40 closed - - - - - - - - - - - - - - - Happy Users - - - - +430 - - - - - - - - - - - - - - - Tickets - - - 123 closed,{' '} - - 15 open - - - - - - - - - - -
-
- ); -} diff --git a/src/views/Dashboard/components/InvestmentCalendarChakra.js b/src/views/Dashboard/components/InvestmentCalendarChakra.js deleted file mode 100644 index 4f1a72e9..00000000 --- a/src/views/Dashboard/components/InvestmentCalendarChakra.js +++ /dev/null @@ -1,587 +0,0 @@ -// src/views/Dashboard/components/InvestmentCalendarChakra.js -import React, { useState, useEffect, useCallback } from 'react'; -import { - Box, - Card, - CardHeader, - CardBody, - Heading, - VStack, - HStack, - Text, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, - useDisclosure, - Badge, - IconButton, - Flex, - Grid, - useColorModeValue, - Divider, - Tooltip, - Icon, - Input, - FormControl, - FormLabel, - Textarea, - Select, - useToast, - Spinner, - Center, - Tag, - TagLabel, - TagLeftIcon, -} from '@chakra-ui/react'; -import { - FiCalendar, - FiClock, - FiStar, - FiTrendingUp, - FiPlus, - FiEdit2, - FiTrash2, - FiSave, - FiX, -} from 'react-icons/fi'; -import FullCalendar from '@fullcalendar/react'; -import dayGridPlugin from '@fullcalendar/daygrid'; -import interactionPlugin from '@fullcalendar/interaction'; -import dayjs from 'dayjs'; -import 'dayjs/locale/zh-cn'; -import { logger } from '../../../utils/logger'; -import { getApiBase } from '../../../utils/apiConfig'; -import TimelineChartModal from '../../../components/StockChart/TimelineChartModal'; -import KLineChartModal from '../../../components/StockChart/KLineChartModal'; -import './InvestmentCalendar.less'; - -dayjs.locale('zh-cn'); - -export default function InvestmentCalendarChakra() { - const { isOpen, onOpen, onClose } = useDisclosure(); - const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure(); - const { isOpen: isTimelineModalOpen, onOpen: onTimelineModalOpen, onClose: onTimelineModalClose } = useDisclosure(); - const { isOpen: isKLineModalOpen, onOpen: onKLineModalOpen, onClose: onKLineModalClose } = useDisclosure(); - const toast = useToast(); - - // 颜色主题 - const bgColor = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const textColor = useColorModeValue('gray.700', 'white'); - const secondaryText = useColorModeValue('gray.600', 'gray.400'); - - const [events, setEvents] = useState([]); - const [selectedDate, setSelectedDate] = useState(null); - const [selectedDateEvents, setSelectedDateEvents] = useState([]); - const [selectedStock, setSelectedStock] = useState(null); - const [loading, setLoading] = useState(false); - const [newEvent, setNewEvent] = useState({ - title: '', - description: '', - type: 'plan', - importance: 3, - stocks: '', - }); - - // 加载事件数据 - const loadEvents = useCallback(async () => { - try { - setLoading(true); - const base = getApiBase(); - - // 直接加载用户相关的事件(投资计划 + 关注的未来事件) - const userResponse = await fetch(base + '/api/account/calendar/events', { - credentials: 'include' - }); - - if (userResponse.ok) { - const userData = await userResponse.json(); - if (userData.success) { - const allEvents = (userData.data || []).map(event => ({ - ...event, - id: `${event.source || 'user'}-${event.id}`, - title: event.title, - start: event.event_date, - date: event.event_date, - backgroundColor: event.source === 'future' ? '#3182CE' : '#8B5CF6', - borderColor: event.source === 'future' ? '#3182CE' : '#8B5CF6', - extendedProps: { - ...event, - isSystem: event.source === 'future', - } - })); - - setEvents(allEvents); - logger.debug('InvestmentCalendar', '日历事件加载成功', { - count: allEvents.length - }); - } - } - } catch (error) { - logger.error('InvestmentCalendar', 'loadEvents', error); - // ❌ 移除数据加载失败 toast(非关键操作) - } finally { - setLoading(false); - } - }, []); // ✅ 移除 toast 依赖 - - useEffect(() => { - loadEvents(); - }, [loadEvents]); - - // 根据重要性获取颜色 - const getEventColor = (importance) => { - if (importance >= 5) return '#E53E3E'; // 红色 - if (importance >= 4) return '#ED8936'; // 橙色 - if (importance >= 3) return '#ECC94B'; // 黄色 - if (importance >= 2) return '#48BB78'; // 绿色 - return '#3182CE'; // 蓝色 - }; - - // 处理日期点击 - const handleDateClick = (info) => { - const clickedDate = dayjs(info.date); - setSelectedDate(clickedDate); - - // 筛选当天的事件 - const dayEvents = events.filter(event => - dayjs(event.start).isSame(clickedDate, 'day') - ); - setSelectedDateEvents(dayEvents); - onOpen(); - }; - - // 处理事件点击 - const handleEventClick = (info) => { - const event = info.event; - const clickedDate = dayjs(event.start); - setSelectedDate(clickedDate); - setSelectedDateEvents([{ - title: event.title, - start: event.start, - extendedProps: { - ...event.extendedProps, - }, - }]); - onOpen(); - }; - - // 添加新事件 - const handleAddEvent = async () => { - try { - const base = getApiBase(); - - const eventData = { - ...newEvent, - event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')), - stocks: newEvent.stocks.split(',').map(s => s.trim()).filter(s => s), - }; - - const response = await fetch(base + '/api/account/calendar/events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(eventData), - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - logger.info('InvestmentCalendar', '添加事件成功', { - eventTitle: eventData.title, - eventDate: eventData.event_date - }); - toast({ - title: '添加成功', - description: '投资计划已添加', - status: 'success', - duration: 3000, - }); - onAddClose(); - loadEvents(); - setNewEvent({ - title: '', - description: '', - type: 'plan', - importance: 3, - stocks: '', - }); - } - } - } catch (error) { - logger.error('InvestmentCalendar', 'handleAddEvent', error, { - eventTitle: newEvent?.title - }); - toast({ - title: '添加失败', - description: '无法添加投资计划', - status: 'error', - duration: 3000, - }); - } - }; - - // 删除用户事件 - const handleDeleteEvent = async (eventId) => { - if (!eventId) { - logger.warn('InvestmentCalendar', '删除事件失败', '缺少事件 ID', { eventId }); - toast({ - title: '无法删除', - description: '缺少事件 ID', - status: 'error', - duration: 3000, - }); - return; - } - try { - const base = getApiBase(); - - const response = await fetch(base + `/api/account/calendar/events/${eventId}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (response.ok) { - logger.info('InvestmentCalendar', '删除事件成功', { eventId }); - toast({ - title: '删除成功', - status: 'success', - duration: 2000, - }); - loadEvents(); - } - } catch (error) { - logger.error('InvestmentCalendar', 'handleDeleteEvent', error, { eventId }); - toast({ - title: '删除失败', - status: 'error', - duration: 3000, - }); - } - }; - - // 处理股票点击 - 打开图表弹窗 - const handleStockClick = (stockCodeOrName, eventDate) => { - // 解析股票代码(可能是 "600000" 或 "600000 平安银行" 格式) - let stockCode = stockCodeOrName; - let stockName = ''; - - if (typeof stockCodeOrName === 'string') { - const parts = stockCodeOrName.trim().split(/\s+/); - stockCode = parts[0]; - stockName = parts.slice(1).join(' '); - } - - // 添加交易所后缀(如果没有) - if (!stockCode.includes('.')) { - if (stockCode.startsWith('6')) { - stockCode = `${stockCode}.SH`; - } else if (stockCode.startsWith('0') || stockCode.startsWith('3')) { - stockCode = `${stockCode}.SZ`; - } else if (stockCode.startsWith('8') || stockCode.startsWith('9') || stockCode.startsWith('4')) { - // 北交所股票 - stockCode = `${stockCode}.BJ`; - } - } - - setSelectedStock({ - stock_code: stockCode, - stock_name: stockName || stockCode, - }); - }; - - return ( - - - - - - 投资日历 - - - - - - {loading ? ( -
- -
- ) : ( - - - - )} -
- - {/* 查看事件详情 Modal - 条件渲染 */} - {isOpen && ( - - - - - {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件 - - - - {selectedDateEvents.length === 0 ? ( -
- - 当天没有事件 - - -
- ) : ( - - {selectedDateEvents.map((event, idx) => ( - - - - - - {event.title} - - {event.extendedProps?.isSystem ? ( - 系统事件 - ) : ( - 我的计划 - )} - - - - - 重要度: {event.extendedProps?.importance || 3}/5 - - - - {!event.extendedProps?.isSystem && ( - } - size="sm" - variant="ghost" - colorScheme="red" - onClick={() => handleDeleteEvent(event.extendedProps?.id)} - /> - )} - - - {event.extendedProps?.description && ( - - {event.extendedProps.description} - - )} - - {event.extendedProps?.stocks && event.extendedProps.stocks.length > 0 && ( - - - 相关股票: - {event.extendedProps.stocks.map((stock, i) => ( - handleStockClick(stock, event.start)} - _hover={{ transform: 'scale(1.05)', shadow: 'md' }} - transition="all 0.2s" - > - - {stock} - - ))} - - {selectedStock && ( - - - - - )} - - )} - - ))} - - )} -
- - - -
-
- )} - - {/* 添加投资计划 Modal - 条件渲染 */} - {isAddOpen && ( - - - - - 添加投资计划 - - - - - - 标题 - setNewEvent({ ...newEvent, title: e.target.value })} - placeholder="例如:关注半导体板块" - /> - - - - 描述 -