/** * EventPanel - 通用事件面板组件 * 用于显示、编辑和管理投资计划或复盘 * * 通过 props 配置差异化行为: * - type: 'plan' | 'review' * - colorScheme: 主题色 * - label: 显示文案 */ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Button, Badge, IconButton, Flex, Grid, Card, CardBody, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, useDisclosure, VStack, HStack, Text, Spinner, Center, Icon, Input, InputGroup, InputLeftElement, FormControl, FormLabel, Textarea, Select, Tag, TagLabel, TagLeftIcon, TagCloseButton, } from '@chakra-ui/react'; import { FiEdit2, FiTrash2, FiSave, FiFileText, FiCalendar, FiTrendingUp, FiHash, FiCheckCircle, FiXCircle, FiAlertCircle, } from 'react-icons/fi'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { usePlanningData } from './PlanningContext'; import type { InvestmentEvent, PlanFormData, EventStatus } from '@/types'; import { logger } from '@/utils/logger'; import { getApiBase } from '@/utils/apiConfig'; dayjs.locale('zh-cn'); /** * 状态信息接口 */ interface StatusInfo { icon: React.ComponentType; color: string; text: string; } /** * EventPanel Props */ export interface EventPanelProps { /** 事件类型 */ type: 'plan' | 'review'; /** 主题颜色 */ colorScheme: string; /** 显示标签(如 "计划" 或 "复盘") */ label: string; /** 外部触发打开模态框的计数器 */ openModalTrigger?: number; } /** * EventPanel 组件 * 通用事件列表面板,显示投资计划或复盘 */ export const EventPanel: React.FC = ({ type, colorScheme, label, openModalTrigger, }) => { const { allEvents, loadAllData, loading, toast, textColor, secondaryText, cardBg, } = usePlanningData(); const { isOpen, onOpen, onClose } = useDisclosure(); const [editingItem, setEditingItem] = useState(null); const [formData, setFormData] = useState({ date: dayjs().format('YYYY-MM-DD'), title: '', content: '', type, stocks: [], tags: [], status: 'active', }); const [stockInput, setStockInput] = useState(''); const [tagInput, setTagInput] = useState(''); // 筛选事件列表(按类型过滤,排除系统事件) const events = allEvents.filter(event => event.type === type && event.source !== 'future'); // 打开编辑/新建模态框 const handleOpenModal = useCallback((item: InvestmentEvent | null = null): void => { if (item) { setEditingItem(item); setFormData({ date: dayjs(item.event_date || item.date).format('YYYY-MM-DD'), title: item.title, content: item.description || item.content || '', type, stocks: item.stocks || [], tags: item.tags || [], status: item.status || 'active', }); } else { setEditingItem(null); setFormData({ date: dayjs().format('YYYY-MM-DD'), title: '', content: '', type, stocks: [], tags: [], status: 'active', }); } onOpen(); }, [type, onOpen]); // 监听外部触发打开新建模态框 useEffect(() => { if (openModalTrigger && openModalTrigger > 0) { handleOpenModal(null); } }, [openModalTrigger, handleOpenModal]); // 保存数据 const handleSave = async (): Promise => { try { const base = getApiBase(); const url = editingItem ? base + `/api/account/investment-plans/${editingItem.id}` : base + '/api/account/investment-plans'; const method = editingItem ? 'PUT' : 'POST'; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(formData), }); if (response.ok) { logger.info('EventPanel', `${editingItem ? '更新' : '创建'}${label}成功`, { itemId: editingItem?.id, title: formData.title, }); toast({ title: editingItem ? '更新成功' : '创建成功', status: 'success', duration: 2000, }); onClose(); loadAllData(); } else { throw new Error('保存失败'); } } catch (error) { logger.error('EventPanel', 'handleSave', error, { itemId: editingItem?.id, title: formData?.title }); toast({ title: '保存失败', description: '无法保存数据', status: 'error', duration: 3000, }); } }; // 删除数据 const handleDelete = async (id: number): Promise => { if (!window.confirm('确定要删除吗?')) return; try { 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, }); loadAllData(); } } catch (error) { logger.error('EventPanel', 'handleDelete', error, { itemId: id }); toast({ title: '删除失败', status: 'error', duration: 3000, }); } }; // 添加股票 const handleAddStock = (): void => { if (stockInput.trim() && !formData.stocks.includes(stockInput.trim())) { setFormData({ ...formData, stocks: [...formData.stocks, stockInput.trim()], }); setStockInput(''); } }; // 添加标签 const handleAddTag = (): void => { if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) { setFormData({ ...formData, tags: [...formData.tags, tagInput.trim()], }); setTagInput(''); } }; // 获取状态信息 const getStatusInfo = (status?: EventStatus): StatusInfo => { switch (status) { case 'completed': return { icon: FiCheckCircle, color: 'green', text: '已完成' }; case 'cancelled': return { icon: FiXCircle, color: 'red', text: '已取消' }; default: return { icon: FiAlertCircle, color: 'blue', text: '进行中' }; } }; // 渲染单个卡片 const renderCard = (item: InvestmentEvent): React.ReactElement => { const statusInfo = getStatusInfo(item.status); return ( {item.title} {dayjs(item.event_date || item.date).format('YYYY年MM月DD日')} {statusInfo.text} } size="sm" variant="ghost" onClick={() => handleOpenModal(item)} aria-label={`编辑${label}`} /> } size="sm" variant="ghost" colorScheme="red" onClick={() => handleDelete(item.id)} aria-label={`删除${label}`} /> {(item.content || item.description) && ( {item.content || item.description} )} {item.stocks && item.stocks.length > 0 && ( <> {item.stocks.map((stock, idx) => ( {stock} ))} )} {item.tags && item.tags.length > 0 && ( <> {item.tags.map((tag, idx) => ( {tag} ))} )} ); }; return ( {loading ? (
) : events.length === 0 ? (
暂无投资{label}
) : ( {events.map(renderCard)} )}
{/* 编辑/新建模态框 */} {isOpen && ( {editingItem ? '编辑' : '新建'}投资{label} 日期 setFormData({ ...formData, date: e.target.value })} /> 标题 setFormData({ ...formData, title: e.target.value })} placeholder={type === 'plan' ? '例如:布局新能源板块' : '例如:本周操作复盘'} /> 内容