diff --git a/src/store/slices/planningSlice.ts b/src/store/slices/planningSlice.ts index e8d9b01f..94653192 100644 --- a/src/store/slices/planningSlice.ts +++ b/src/store/slices/planningSlice.ts @@ -215,6 +215,14 @@ const planningSlice = createSlice({ state.allEvents = state.allEvents.filter(e => e.id !== action.payload); state.lastUpdated = Date.now(); }, + /** 乐观更新事件(编辑时使用) */ + optimisticUpdateEvent: (state, action: PayloadAction) => { + const index = state.allEvents.findIndex(e => e.id === action.payload.id); + if (index !== -1) { + state.allEvents[index] = action.payload; + state.lastUpdated = Date.now(); + } + }, }, extraReducers: (builder) => { builder @@ -277,6 +285,7 @@ export const { optimisticAddEvent, replaceEvent, removeEvent, + optimisticUpdateEvent, } = planningSlice.actions; export default planningSlice.reducer; diff --git a/src/views/Center/components/CalendarPanel.tsx b/src/views/Center/components/CalendarPanel.tsx index dbac0962..77ea19b2 100644 --- a/src/views/Center/components/CalendarPanel.tsx +++ b/src/views/Center/components/CalendarPanel.tsx @@ -1,6 +1,8 @@ /** * CalendarPanel - 投资日历面板组件 - * 使用 FullCalendar 展示投资计划、复盘等事件 + * 使用 Ant Design Calendar 展示投资计划、复盘等事件 + * + * 聚合展示模式:每个日期格子显示各类型事件的汇总信息 */ import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react'; @@ -14,12 +16,8 @@ import { ModalCloseButton, Spinner, Center, + VStack, } from '@chakra-ui/react'; -import FullCalendar from '@fullcalendar/react'; -import dayGridPlugin from '@fullcalendar/daygrid'; -import interactionPlugin from '@fullcalendar/interaction'; -import type { DateClickArg } from '@fullcalendar/interaction'; -import type { EventClickArg } from '@fullcalendar/core'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/zh-cn'; @@ -28,7 +26,15 @@ import { selectAllEvents } from '@/store/slices/planningSlice'; import { usePlanningData } from './PlanningContext'; import { EventDetailModal } from './EventDetailModal'; import type { InvestmentEvent } from '@/types'; -import './InvestmentCalendar.less'; + +// 使用新的公共日历组件 +import { + BaseCalendar, + CalendarEventBlock, + type CellRenderInfo, + type CalendarEvent, + CALENDAR_COLORS, +} from '@components/Calendar'; // 懒加载投资日历组件 const InvestmentCalendar = lazy(() => import('@components/InvestmentCalendar')); @@ -36,18 +42,12 @@ const InvestmentCalendar = lazy(() => import('@components/InvestmentCalendar')); dayjs.locale('zh-cn'); /** - * FullCalendar 事件类型 + * 事件聚合信息(用于日历格子显示) */ -interface CalendarEvent { - id: string; - title: string; - start: string; - date: string; - backgroundColor: string; - borderColor: string; - extendedProps: InvestmentEvent & { - isSystem: boolean; - }; +interface EventSummary { + plans: InvestmentEvent[]; + reviews: InvestmentEvent[]; + systems: InvestmentEvent[]; } /** @@ -72,140 +72,123 @@ export const CalendarPanel: React.FC = () => { const [selectedDate, setSelectedDate] = useState(null); const [selectedDateEvents, setSelectedDateEvents] = useState([]); + const [initialFilter, setInitialFilter] = useState<'all' | 'plan' | 'review' | 'system'>('all'); - // 转换数据为 FullCalendar 格式(使用 useMemo 缓存) - // 黑金主题色:计划用金色,复盘用青金色,系统事件用蓝色 - const calendarEvents: CalendarEvent[] = useMemo(() => - allEvents.map(event => ({ - ...event, - id: `${event.source || 'user'}-${event.id}`, - title: event.title, - start: event.event_date, - date: event.event_date, - backgroundColor: event.source === 'future' ? '#3B82F6' : event.type === 'plan' ? '#D4AF37' : '#10B981', - borderColor: event.source === 'future' ? '#3B82F6' : event.type === 'plan' ? '#D4AF37' : '#10B981', - extendedProps: { - ...event, - isSystem: event.source === 'future', + // 按日期分组事件(用于聚合展示) + const eventsByDate = useMemo(() => { + const map: Record = {}; + allEvents.forEach(event => { + const dateStr = event.event_date; + if (!map[dateStr]) { + map[dateStr] = { plans: [], reviews: [], systems: [] }; } - })), [allEvents]); + if (event.source === 'future') { + map[dateStr].systems.push(event); + } else if (event.type === 'plan') { + map[dateStr].plans.push(event); + } else if (event.type === 'review') { + map[dateStr].reviews.push(event); + } + }); + return map; + }, [allEvents]); - // 抽取公共的打开事件详情函数 - const openEventDetail = useCallback((date: Date | null): void => { - if (!date) return; - const clickedDate = dayjs(date); - setSelectedDate(clickedDate); + // 将事件摘要转换为 CalendarEvent 格式 + const getCalendarEvents = useCallback((dateStr: string): CalendarEvent[] => { + const summary = eventsByDate[dateStr]; + if (!summary) return []; + + const events: CalendarEvent[] = []; + + // 系统事件 + if (summary.systems.length > 0) { + events.push({ + id: `${dateStr}-system`, + type: 'system', + title: summary.systems[0]?.title || '', + date: dateStr, + count: summary.systems.length, + }); + } + + // 计划 + if (summary.plans.length > 0) { + events.push({ + id: `${dateStr}-plan`, + type: 'plan', + title: summary.plans[0]?.title || '', + date: dateStr, + count: summary.plans.length, + }); + } + + // 复盘 + if (summary.reviews.length > 0) { + events.push({ + id: `${dateStr}-review`, + type: 'review', + title: summary.reviews[0]?.title || '', + date: dateStr, + count: summary.reviews.length, + }); + } + + return events; + }, [eventsByDate]); + + // 处理日期选择(点击日期空白区域) + const handleDateSelect = useCallback((date: Dayjs): void => { + setSelectedDate(date); const dayEvents = allEvents.filter(event => - dayjs(event.event_date).isSame(clickedDate, 'day') + dayjs(event.event_date).isSame(date, 'day') ); setSelectedDateEvents(dayEvents); + setInitialFilter('all'); // 点击日期时显示全部 setIsDetailModalOpen(true); }, [allEvents]); - // 处理日期点击 - const handleDateClick = useCallback((info: DateClickArg): void => { - openEventDetail(info.date); - }, [openEventDetail]); + // 处理事件点击(点击具体事件类型) + const handleEventClick = useCallback((event: CalendarEvent): void => { + const date = dayjs(event.date); + setSelectedDate(date); - // 处理事件点击 - const handleEventClick = useCallback((info: EventClickArg): void => { - openEventDetail(info.event.start); - }, [openEventDetail]); + const dayEvents = allEvents.filter(e => + dayjs(e.event_date).isSame(date, 'day') + ); + setSelectedDateEvents(dayEvents); + setInitialFilter(event.type as 'plan' | 'review' | 'system'); // 定位到对应 Tab + setIsDetailModalOpen(true); + }, [allEvents]); + + // 自定义日期格子内容渲染 + const renderCellContent = useCallback((date: Dayjs, _info: CellRenderInfo) => { + const dateStr = date.format('YYYY-MM-DD'); + const events = getCalendarEvents(dateStr); + + if (events.length === 0) return null; + + return ( + + + + ); + }, [getCalendarEvents, handleEventClick]); return ( - - + @@ -217,6 +200,7 @@ export const CalendarPanel: React.FC = () => { events={selectedDateEvents} borderColor={borderColor} secondaryText={secondaryText} + initialFilter={initialFilter} onNavigateToPlan={() => { setViewMode('list'); setListTab(0); @@ -249,7 +233,6 @@ export const CalendarPanel: React.FC = () => { )} - ); }; diff --git a/src/views/Center/components/EventDetailModal.less b/src/views/Center/components/EventDetailModal.less index ee6719ec..c376b31f 100644 --- a/src/views/Center/components/EventDetailModal.less +++ b/src/views/Center/components/EventDetailModal.less @@ -1,7 +1,8 @@ /* EventDetailModal.less - 事件详情弹窗黑金主题样式 */ // ==================== 变量定义 ==================== -@color-bg-deep: #0A0A14; +// 与 GlassCard transparent 变体保持一致 +@color-bg-deep: rgba(15, 15, 26, 0.95); @color-bg-primary: #0F0F1A; @color-bg-elevated: #1A1A2E; @@ -27,11 +28,10 @@ .event-detail-modal { // Modal 整体 .ant-modal-content { - background: linear-gradient(135deg, @color-bg-elevated 0%, @color-bg-primary 100%) !important; + background: @color-bg-deep !important; border: 1px solid @color-line-default; border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 24px rgba(212, 175, 55, 0.1); - backdrop-filter: blur(16px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 24px rgba(212, 175, 55, 0.08); } // Modal 头部 @@ -68,3 +68,61 @@ background: transparent; } } + +// ==================== 响应式适配 ==================== +@media (max-width: 768px) { + .event-detail-modal { + .ant-modal-content { + border-radius: 0; + min-height: 100vh; + } + + .ant-modal-header { + padding: 12px 16px; + } + + .ant-modal-title { + font-size: 16px; + } + + .ant-modal-body { + padding: 12px 16px 20px; + } + } + + .event-detail-modal-root { + .ant-modal { + max-width: 100vw !important; + margin: 0 !important; + top: 0 !important; + padding: 0 !important; + } + + .ant-modal-centered .ant-modal { + top: 0 !important; + } + } +} + +// ==================== 滚动条样式(备用,已在组件内定义) ==================== +.event-detail-modal { + // 自定义滚动条 + ::-webkit-scrollbar { + width: 6px; + } + + ::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; + } + + ::-webkit-scrollbar-thumb { + background: rgba(212, 175, 55, 0.3); + border-radius: 3px; + transition: background 0.2s ease; + + &:hover { + background: rgba(212, 175, 55, 0.5); + } + } +} diff --git a/src/views/Center/components/EventDetailModal.tsx b/src/views/Center/components/EventDetailModal.tsx index 2a417e79..952c1294 100644 --- a/src/views/Center/components/EventDetailModal.tsx +++ b/src/views/Center/components/EventDetailModal.tsx @@ -1,18 +1,94 @@ /** * EventDetailModal - 事件详情弹窗组件 * 用于展示某一天的所有投资事件 - * 使用 Ant Design 实现 + * + * 功能: + * - Tab 筛选(全部/计划/复盘/系统) + * - 两列网格布局 + * - 响应式宽度 */ -import React from 'react'; -import { Modal, Space } from 'antd'; +import React, { useState, useMemo } from 'react'; +import { Modal } from 'antd'; +import { + Box, + Grid, + HStack, + Button, + Text, + Badge, + Icon, + useBreakpointValue, +} from '@chakra-ui/react'; +import { Target, Heart, Calendar, LayoutGrid } from 'lucide-react'; import type { Dayjs } from 'dayjs'; -import { EventCard } from './EventCard'; +import { FUIEventCard } from './FUIEventCard'; import { EventEmptyState } from './EventEmptyState'; import type { InvestmentEvent } from '@/types'; import './EventDetailModal.less'; +/** + * 筛选类型 + */ +type FilterType = 'all' | 'plan' | 'review' | 'system'; + +/** + * 筛选器配置 + */ +interface FilterOption { + key: FilterType; + label: string; + icon?: React.ElementType; + color: string; +} + +const FILTER_OPTIONS: FilterOption[] = [ + { + key: 'all', + label: '全部', + icon: LayoutGrid, + color: 'rgba(255, 255, 255, 0.8)', + }, + { + key: 'plan', + label: '计划', + icon: Target, + color: '#D4AF37', + }, + { + key: 'review', + label: '复盘', + icon: Heart, + color: '#10B981', + }, + { + key: 'system', + label: '系统', + icon: Calendar, + color: '#3B82F6', + }, +]; + +/** + * 根据 hex 颜色生成 rgba 颜色 + */ +const hexToRgba = (hex: string, alpha: number): string => { + // 处理 rgba 格式 + if (hex.startsWith('rgba')) { + return hex; + } + // 处理 hex 格式 + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result) { + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + return hex; +}; + /** * EventDetailModal Props */ @@ -35,8 +111,27 @@ export interface EventDetailModalProps { onNavigateToReview?: () => void; /** 打开投资日历 */ onOpenInvestmentCalendar?: () => void; + /** 初始筛选类型(点击事件时定位到对应 Tab) */ + initialFilter?: FilterType; } +/** + * 获取各类型事件数量 + */ +const getEventCounts = (events: InvestmentEvent[]) => { + const counts = { all: events.length, plan: 0, review: 0, system: 0 }; + events.forEach(event => { + if (event.source === 'future') { + counts.system++; + } else if (event.type === 'plan') { + counts.plan++; + } else if (event.type === 'review') { + counts.review++; + } + }); + return counts; +}; + /** * EventDetailModal 组件 */ @@ -45,26 +140,64 @@ export const EventDetailModal: React.FC = ({ onClose, selectedDate, events, - borderColor, - secondaryText, onNavigateToPlan, onNavigateToReview, onOpenInvestmentCalendar, + initialFilter, }) => { + // 筛选状态 + const [activeFilter, setActiveFilter] = useState('all'); + + // 响应式弹窗宽度 + const modalWidth = useBreakpointValue({ base: '100%', md: 600, lg: 800 }) || 600; + + // 响应式网格列数 + const gridColumns = useBreakpointValue({ base: 1, md: 2 }) || 1; + + // 各类型事件数量 + const eventCounts = useMemo(() => getEventCounts(events), [events]); + + // 筛选后的事件 + const filteredEvents = useMemo(() => { + if (activeFilter === 'all') return events; + if (activeFilter === 'system') return events.filter(e => e.source === 'future'); + return events.filter(e => e.type === activeFilter && e.source !== 'future'); + }, [events, activeFilter]); + + // 弹窗打开时设置筛选(使用 initialFilter 或默认 'all') + React.useEffect(() => { + if (isOpen) { + setActiveFilter(initialFilter || 'all'); + } + }, [isOpen, initialFilter]); + return ( + {selectedDate?.format('YYYY年MM月DD日') || ''} 的事件 + + {events.length} + + + } footer={null} - width={600} + width={modalWidth} maskClosable={false} keyboard={true} centered className="event-detail-modal" rootClassName="event-detail-modal-root" styles={{ - body: { paddingTop: 16, paddingBottom: 24 }, + body: { paddingTop: 8, paddingBottom: 24 }, }} > {events.length === 0 ? ( @@ -83,17 +216,86 @@ export const EventDetailModal: React.FC = ({ }} /> ) : ( - - {events.map((event, idx) => ( - - ))} - + + {/* Tab 筛选器 */} + + {FILTER_OPTIONS.map(option => { + const count = eventCounts[option.key]; + const isActive = activeFilter === option.key; + + return ( + + ); + })} + + + {/* 事件网格 */} + + {filteredEvents.length === 0 ? ( + + + 该分类下暂无事件 + + + ) : ( + + {filteredEvents.map((event, idx) => ( + + ))} + + )} + + )} ); diff --git a/src/views/Center/components/EventFormModal.tsx b/src/views/Center/components/EventFormModal.tsx index ed379ae0..f4866792 100644 --- a/src/views/Center/components/EventFormModal.tsx +++ b/src/views/Center/components/EventFormModal.tsx @@ -41,6 +41,7 @@ import { optimisticAddEvent, replaceEvent, removeEvent, + optimisticUpdateEvent, } from '@/store/slices/planningSlice'; import './EventFormModal.less'; import type { InvestmentEvent, EventType } from '@/types'; @@ -373,26 +374,51 @@ export const EventFormModal: React.FC = ({ return; } - // ===== 编辑模式:传统更新 ===== - const response = await fetch(url, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify(requestData), - }); - - if (response.ok) { - logger.info('EventFormModal', `更新${label}成功`, { - itemId: editingEvent?.id, + // ===== 编辑模式:乐观更新 ===== + if (editingEvent) { + // 构建更新后的事件对象 + const updatedEvent: InvestmentEvent = { + ...editingEvent, title: values.title, - }); - message.success('修改成功'); - onClose(); - onSuccess(); - // 使用 Redux 刷新数据,确保列表和日历同步 - dispatch(fetchAllEvents()); - } else { - throw new Error('保存失败'); + content: values.content || '', + description: values.content || '', + date: values.date.format('YYYY-MM-DD'), + event_date: values.date.format('YYYY-MM-DD'), + stocks: stocksWithNames, + updated_at: new Date().toISOString(), + }; + + // ① 立即更新 UI + dispatch(optimisticUpdateEvent(updatedEvent)); + setSaving(false); + onClose(); // 立即关闭弹窗 + + // ② 后台发送 API 请求 + try { + const response = await fetch(url, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(requestData), + }); + + if (response.ok) { + logger.info('EventFormModal', `更新${label}成功`, { + itemId: editingEvent.id, + title: values.title, + }); + message.success('修改成功'); + onSuccess(); + } else { + throw new Error('保存失败'); + } + } catch (error) { + // ③ 失败回滚 - 重新加载数据 + dispatch(fetchAllEvents()); + logger.error('EventFormModal', 'handleSave edit rollback', error); + message.error('修改失败,请重试'); + } + return; } } catch (error) { if (error instanceof Error && error.message !== '保存失败') { diff --git a/src/views/Center/components/EventPanel.tsx b/src/views/Center/components/EventPanel.tsx index b8609b70..40a0eed8 100644 --- a/src/views/Center/components/EventPanel.tsx +++ b/src/views/Center/components/EventPanel.tsx @@ -24,11 +24,12 @@ import { FiFileText } from 'react-icons/fi'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { fetchAllEvents, - deleteEvent, + removeEvent, selectPlans, selectReviews, selectPlanningLoading, } from '@/store/slices/planningSlice'; +import { getApiBase } from '@/utils/apiConfig'; import { EventFormModal } from './EventFormModal'; import { FUIEventCard } from './FUIEventCard'; import type { InvestmentEvent } from '@/types'; @@ -104,22 +105,37 @@ export const EventPanel: React.FC = ({ setEditingItem(null); }; - // 删除数据 - 使用 Redux action + // 删除数据 - 乐观更新模式 const handleDelete = async (id: number): Promise => { if (!window.confirm('确定要删除吗?')) return; + // ① 立即从 UI 移除 + dispatch(removeEvent(id)); + + // ② 后台发送 API 请求 try { - await dispatch(deleteEvent(id)).unwrap(); - logger.info('EventPanel', `删除${label}成功`, { itemId: id }); - toast({ - title: '删除成功', - status: 'success', - duration: 2000, + const base = getApiBase(); + const response = await fetch(`${base}/api/account/investment-plans/${id}`, { + method: 'DELETE', + credentials: 'include', }); + + if (response.ok) { + logger.info('EventPanel', `删除${label}成功`, { itemId: id }); + toast({ + title: '删除成功', + status: 'success', + duration: 2000, + }); + } else { + throw new Error('删除失败'); + } } catch (error) { - logger.error('EventPanel', 'handleDelete', error, { itemId: id }); + // ③ 失败回滚 - 重新加载数据 + dispatch(fetchAllEvents()); + logger.error('EventPanel', 'handleDelete rollback', error, { itemId: id }); toast({ - title: '删除失败', + title: '删除失败,请重试', status: 'error', duration: 3000, }); diff --git a/src/views/Center/components/FUIEventCard.tsx b/src/views/Center/components/FUIEventCard.tsx index 765bb6c6..4452bb6e 100644 --- a/src/views/Center/components/FUIEventCard.tsx +++ b/src/views/Center/components/FUIEventCard.tsx @@ -56,29 +56,47 @@ const FUI_THEME = { export interface FUIEventCardProps { /** 事件数据 */ event: InvestmentEvent; + /** 显示变体: list(列表视图) | modal(弹窗只读) */ + variant?: 'list' | 'modal'; /** 主题颜色 */ colorScheme?: string; /** 显示标签(用于 aria-label) */ label?: string; - /** 编辑回调 */ + /** 编辑回调 (modal 模式不显示) */ onEdit?: (event: InvestmentEvent) => void; - /** 删除回调 */ + /** 删除回调 (modal 模式不显示) */ onDelete?: (id: number) => void; } /** 描述最大显示行数 */ const MAX_LINES = 3; +/** + * 获取事件类型徽章配置 + */ +const getTypeBadge = (event: InvestmentEvent) => { + if (event.source === 'future') { + return { label: '系统事件', color: '#3B82F6', bg: 'rgba(59, 130, 246, 0.15)' }; + } + if (event.type === 'plan') { + return { label: '我的计划', color: '#D4AF37', bg: 'rgba(212, 175, 55, 0.15)' }; + } + return { label: '我的复盘', color: '#10B981', bg: 'rgba(16, 185, 129, 0.15)' }; +}; + /** * FUIEventCard 组件 */ export const FUIEventCard = memo(({ event, + variant = 'list', colorScheme = 'orange', label = '复盘', onEdit, onDelete, }) => { + const isModalVariant = variant === 'modal'; + const typeBadge = getTypeBadge(event); // 展开/收起状态 const [isExpanded, setIsExpanded] = useState(false); const [isOverflow, setIsOverflow] = useState(false); @@ -115,7 +133,7 @@ export const FUIEventCard = memo(({ }} > - {/* 头部区域:图标 + 标题 + 操作按钮 */} + {/* 头部区域:图标 + 标题 + 操作按钮/类型徽章 */} (({ - {/* 编辑/删除按钮 */} - {(onEdit || onDelete) && ( - - {onEdit && ( - } - size="xs" - variant="ghost" - color={FUI_THEME.text.secondary} - _hover={{ color: FUI_THEME.accent, bg: 'rgba(212, 175, 55, 0.1)' }} - onClick={() => onEdit(event)} - aria-label={`编辑${label}`} - /> - )} - {onDelete && ( - } - size="xs" - variant="ghost" - color={FUI_THEME.text.secondary} - _hover={{ color: '#EF4444', bg: 'rgba(239, 68, 68, 0.1)' }} - onClick={() => onDelete(event.id)} - aria-label={`删除${label}`} - /> - )} - + {/* modal 模式: 显示类型徽章 */} + {isModalVariant ? ( + + {typeBadge.label} + + ) : ( + /* list 模式: 显示编辑/删除按钮 */ + (onEdit || onDelete) && ( + + {onEdit && ( + } + size="xs" + variant="ghost" + color={FUI_THEME.text.secondary} + _hover={{ color: FUI_THEME.accent, bg: 'rgba(212, 175, 55, 0.1)' }} + onClick={() => onEdit(event)} + aria-label={`编辑${label}`} + /> + )} + {onDelete && ( + } + size="xs" + variant="ghost" + color={FUI_THEME.text.secondary} + _hover={{ color: '#EF4444', bg: 'rgba(239, 68, 68, 0.1)' }} + onClick={() => onDelete(event.id)} + aria-label={`删除${label}`} + /> + )} + + ) )} diff --git a/src/views/Center/components/InvestmentPlanningCenter.tsx b/src/views/Center/components/InvestmentPlanningCenter.tsx index e2b93c34..86c4d8d9 100644 --- a/src/views/Center/components/InvestmentPlanningCenter.tsx +++ b/src/views/Center/components/InvestmentPlanningCenter.tsx @@ -24,8 +24,6 @@ import { TabPanels, Tab, TabPanel, - Spinner, - Center, Button, ButtonGroup, } from '@chakra-ui/react'; @@ -39,6 +37,7 @@ import { Target } from 'lucide-react'; import GlassCard from '@components/GlassCard'; import { PlanningDataProvider } from './PlanningContext'; +import { EventPanelSkeleton, CalendarPanelSkeleton } from './skeletons'; import type { PlanningContextValue } from '@/types'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { @@ -48,7 +47,6 @@ import { selectPlans, selectReviews, } from '@/store/slices/planningSlice'; -import './InvestmentCalendar.less'; // 懒加载子面板组件(实现代码分割) const CalendarPanel = lazy(() => @@ -58,14 +56,6 @@ const EventPanel = lazy(() => import('./EventPanel').then(module => ({ default: module.EventPanel })) ); -/** - * 面板加载占位符 - */ -const PanelLoadingFallback: React.FC = () => ( -
- -
-); /** * InvestmentPlanningCenter 主组件 @@ -193,7 +183,7 @@ const InvestmentPlanningCenter: React.FC = () => { {viewMode === 'calendar' ? ( /* 日历视图 */ - }> + }> ) : ( @@ -262,7 +252,7 @@ const InvestmentPlanningCenter: React.FC = () => { {/* 计划列表面板 */} - }> + }> { {/* 复盘列表面板 */} - }> + }> { + return ( + + {/* 工具栏骨架 */} + + {/* 左侧:导航按钮 */} + + + + + + + {/* 中间:月份标题 */} + + + {/* 右侧占位(保持对称) */} + + + + {/* 日历网格骨架 */} + + {/* 星期头(周日 - 周六) */} + {[...Array(7)].map((_, i) => ( + + ))} + + {/* 日期格子(5行 x 7列 = 35个) */} + {[...Array(35)].map((_, i) => ( + + ))} + + + ); +}); + +CalendarPanelSkeleton.displayName = 'CalendarPanelSkeleton'; + +export default CalendarPanelSkeleton; diff --git a/src/views/Center/components/skeletons/EventPanelSkeleton.tsx b/src/views/Center/components/skeletons/EventPanelSkeleton.tsx new file mode 100644 index 00000000..571d84e8 --- /dev/null +++ b/src/views/Center/components/skeletons/EventPanelSkeleton.tsx @@ -0,0 +1,112 @@ +/** + * EventPanelSkeleton - 事件列表骨架屏组件 + * 用于视图切换时的加载占位 + */ + +import React, { memo } from 'react'; +import { + Box, + VStack, + HStack, + Skeleton, + SkeletonText, +} from '@chakra-ui/react'; + +// 骨架屏主题配色(黑金主题) +const SKELETON_THEME = { + startColor: 'rgba(26, 32, 44, 0.6)', + endColor: 'rgba(212, 175, 55, 0.2)', + cardBg: 'rgba(26, 26, 46, 0.7)', + cardBorder: 'rgba(212, 175, 55, 0.15)', +}; + +/** + * 单个事件卡片骨架 + */ +const EventCardSkeleton: React.FC = () => ( + + {/* 头部:图标 + 标题 */} + + + + + + {/* 日期行 */} + + + + + + {/* 描述内容 */} + + + {/* 股票标签行 */} + + + + + +); + +/** + * EventPanelSkeleton 组件 + * 显示多个卡片骨架屏 + */ +export const EventPanelSkeleton: React.FC = memo(() => { + return ( + + {[1, 2, 3, 4].map(i => ( + + ))} + + ); +}); + +EventPanelSkeleton.displayName = 'EventPanelSkeleton'; + +export default EventPanelSkeleton; diff --git a/src/views/Center/components/skeletons/index.ts b/src/views/Center/components/skeletons/index.ts new file mode 100644 index 00000000..cff722be --- /dev/null +++ b/src/views/Center/components/skeletons/index.ts @@ -0,0 +1,6 @@ +/** + * Center 模块骨架屏组件统一导出 + */ + +export { EventPanelSkeleton } from './EventPanelSkeleton'; +export { CalendarPanelSkeleton } from './CalendarPanelSkeleton';