diff --git a/src/views/Dashboard/components/ReviewsPanel.tsx b/src/views/Dashboard/components/ReviewsPanel.tsx new file mode 100644 index 00000000..851151be --- /dev/null +++ b/src/views/Dashboard/components/ReviewsPanel.tsx @@ -0,0 +1,506 @@ +/** + * ReviewsPanel - 投资复盘列表面板组件 + * 显示、编辑和管理投资复盘 + */ + +import React, { useState } 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 { + FiPlus, + 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; +} + +/** + * ReviewsPanel 组件 + * 复盘列表面板,显示所有投资复盘 + */ +export const ReviewsPanel: React.FC = () => { + const { + allEvents, + loadAllData, + loading, + toast, + textColor, + secondaryText, + cardBg, + borderColor, + } = usePlanningData(); + + const { isOpen, onOpen, onClose } = useDisclosure(); + const [editingItem, setEditingItem] = useState(null); + const [formData, setFormData] = useState({ + date: dayjs().format('YYYY-MM-DD'), + title: '', + content: '', + type: 'review', + stocks: [], + tags: [], + status: 'active', + }); + const [stockInput, setStockInput] = useState(''); + const [tagInput, setTagInput] = useState(''); + + // 筛选复盘列表(排除系统事件) + const reviews = allEvents.filter(event => event.type === 'review' && event.source !== 'future'); + + // 打开编辑/新建模态框 + const handleOpenModal = (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: 'review', + stocks: item.stocks || [], + tags: item.tags || [], + status: item.status || 'active', + }); + } else { + setEditingItem(null); + setFormData({ + date: dayjs().format('YYYY-MM-DD'), + title: '', + content: '', + type: 'review', + stocks: [], + tags: [], + status: 'active', + }); + } + onOpen(); + }; + + // 保存数据 + 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('ReviewsPanel', `${editingItem ? '更新' : '创建'}成功`, { + itemId: editingItem?.id, + title: formData.title, + }); + toast({ + title: editingItem ? '更新成功' : '创建成功', + status: 'success', + duration: 2000, + }); + onClose(); + loadAllData(); + } else { + throw new Error('保存失败'); + } + } catch (error) { + logger.error('ReviewsPanel', '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('ReviewsPanel', '删除成功', { itemId: id }); + toast({ + title: '删除成功', + status: 'success', + duration: 2000, + }); + loadAllData(); + } + } catch (error) { + logger.error('ReviewsPanel', '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): JSX.Element => { + 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="编辑复盘" + /> + } + size="sm" + variant="ghost" + colorScheme="red" + onClick={() => handleDelete(item.id)} + aria-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 ? ( +
+ +
+ ) : reviews.length === 0 ? ( +
+ + + 暂无投资复盘 + + +
+ ) : ( + + {reviews.map(renderCard)} + + )} +
+ + {/* 编辑/新建模态框 */} + {isOpen && ( + + + + + {editingItem ? '编辑' : '新建'}投资复盘 + + + + + + 日期 + + + + + setFormData({ ...formData, date: e.target.value })} + /> + + + + + 标题 + setFormData({ ...formData, title: e.target.value })} + placeholder="例如:本周操作复盘" + /> + + + + 内容 +