// 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="例如:关注半导体板块" /> 描述