// src/views/Dashboard/components/InvestmentPlanningCenter.js import React, { useState, useEffect, useCallback, createContext, useContext } 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, TagCloseButton, Tabs, TabList, TabPanels, Tab, TabPanel, InputGroup, InputLeftElement, } from '@chakra-ui/react'; import { FiCalendar, FiClock, FiStar, FiTrendingUp, FiPlus, FiEdit2, FiTrash2, FiSave, FiX, FiTarget, FiFileText, FiHash, FiCheckCircle, FiXCircle, FiAlertCircle, } 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 '../components/InvestmentCalendar.css'; dayjs.locale('zh-cn'); // 创建 Context 用于跨标签页共享数据 const PlanningDataContext = createContext(); export default function InvestmentPlanningCenter() { 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 cardBg = useColorModeValue('gray.50', 'gray.700'); // 全局数据状态 const [allEvents, setAllEvents] = useState([]); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState(0); // 加载所有事件数据(日历事件 + 计划 + 复盘) const loadAllData = useCallback(async () => { try { setLoading(true); const base = getApiBase(); const response = await fetch(base + '/api/account/calendar/events', { credentials: 'include' }); if (response.ok) { const data = await response.json(); if (data.success) { setAllEvents(data.data || []); logger.debug('InvestmentPlanningCenter', '数据加载成功', { count: data.data?.length || 0 }); } } } catch (error) { logger.error('InvestmentPlanningCenter', 'loadAllData', error); } finally { setLoading(false); } }, []); useEffect(() => { loadAllData(); }, [loadAllData]); // 提供给子组件的 Context 值 const contextValue = { allEvents, setAllEvents, loadAllData, loading, setLoading, activeTab, setActiveTab, toast, bgColor, borderColor, textColor, secondaryText, cardBg, }; return ( 投资规划中心 日历视图 我的计划 ({allEvents.filter(e => e.type === 'plan').length}) 我的复盘 ({allEvents.filter(e => e.type === 'review').length}) ); } // 日历视图面板 function CalendarPanel() { const { allEvents, loadAllData, loading, setActiveTab, toast, borderColor, secondaryText, } = useContext(PlanningDataContext); const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure(); const [selectedDate, setSelectedDate] = useState(null); const [selectedDateEvents, setSelectedDateEvents] = useState([]); const [newEvent, setNewEvent] = useState({ title: '', description: '', type: 'plan', importance: 3, stocks: '', }); // 转换数据为 FullCalendar 格式 const calendarEvents = 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' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169', borderColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169', extendedProps: { ...event, isSystem: event.source === 'future', } })); // 处理日期点击 const handleDateClick = (info) => { const clickedDate = dayjs(info.date); setSelectedDate(clickedDate); const dayEvents = allEvents.filter(event => dayjs(event.event_date).isSame(clickedDate, 'day') ); setSelectedDateEvents(dayEvents); onOpen(); }; // 处理事件点击 const handleEventClick = (info) => { const event = info.event; const clickedDate = dayjs(event.start); setSelectedDate(clickedDate); const dayEvents = allEvents.filter(ev => dayjs(ev.event_date).isSame(clickedDate, 'day') ); setSelectedDateEvents(dayEvents); 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('CalendarPanel', '添加事件成功', { eventTitle: eventData.title, eventDate: eventData.event_date }); toast({ title: '添加成功', description: '投资计划已添加', status: 'success', duration: 3000, }); onAddClose(); loadAllData(); setNewEvent({ title: '', description: '', type: 'plan', importance: 3, stocks: '', }); } } } catch (error) { logger.error('CalendarPanel', 'handleAddEvent', error, { eventTitle: newEvent?.title }); toast({ title: '添加失败', description: '无法添加投资计划', status: 'error', duration: 3000, }); } }; // 删除事件 const handleDeleteEvent = async (eventId) => { if (!eventId) { logger.warn('CalendarPanel', '删除事件失败', '缺少事件 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('CalendarPanel', '删除事件成功', { eventId }); toast({ title: '删除成功', status: 'success', duration: 2000, }); loadAllData(); } } catch (error) { logger.error('CalendarPanel', 'handleDeleteEvent', error, { eventId }); toast({ title: '删除失败', status: 'error', duration: 3000, }); } }; // 跳转到计划或复盘标签页 const handleViewDetails = (event) => { if (event.type === 'plan') { setActiveTab(1); // 跳转到"我的计划"标签页 } else if (event.type === 'review') { setActiveTab(2); // 跳转到"我的复盘"标签页 } onClose(); }; return ( {loading ? (
) : ( )} {/* 查看事件详情 Modal */} {isOpen && ( {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件 {selectedDateEvents.length === 0 ? (
当天没有事件
) : ( {selectedDateEvents.map((event, idx) => ( {event.title} {event.source === 'future' ? ( 系统事件 ) : event.type === 'plan' ? ( 我的计划 ) : ( 我的复盘 )} {event.importance && ( 重要度: {event.importance}/5 )} {!event.source || event.source === 'user' ? ( <> } size="sm" variant="ghost" colorScheme="blue" onClick={() => handleViewDetails(event)} /> } size="sm" variant="ghost" colorScheme="red" onClick={() => handleDeleteEvent(event.id)} /> ) : null} {event.description && ( {event.description} )} {event.stocks && event.stocks.length > 0 && ( 相关股票: {event.stocks.map((stock, i) => ( {stock} ))} )} ))} )}
)} {/* 添加投资计划 Modal */} {isAddOpen && ( 添加投资计划 标题 setNewEvent({ ...newEvent, title: e.target.value })} placeholder="例如:关注半导体板块" /> 描述