From 4d6da77aeb006b7c6f62afda82481528af364f98 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 18 Nov 2025 13:51:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=20PlansPanel.tsx=20?= =?UTF-8?q?=E6=96=B0=E5=BB=BA:=20src/views/Dashboard/components/PlansPanel?= =?UTF-8?q?.tsx=20=E5=A4=8D=E5=88=B6=E5=8E=9F=E6=96=87=E4=BB=B6=E7=AC=AC?= =?UTF-8?q?=20607-1030=20=E8=A1=8C=E4=BB=A3=E7=A0=81=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=20?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E7=8A=B6=E6=80=81=E4=BD=BF=E7=94=A8=20PlanFo?= =?UTF-8?q?rmData=20=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Dashboard/components/PlansPanel.tsx | 506 ++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 src/views/Dashboard/components/PlansPanel.tsx diff --git a/src/views/Dashboard/components/PlansPanel.tsx b/src/views/Dashboard/components/PlansPanel.tsx new file mode 100644 index 00000000..b4e4ffe7 --- /dev/null +++ b/src/views/Dashboard/components/PlansPanel.tsx @@ -0,0 +1,506 @@ +/** + * PlansPanel - 投资计划列表面板组件 + * 显示、编辑和管理投资计划 + */ + +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, + FiTarget, + 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; +} + +/** + * PlansPanel 组件 + * 计划列表面板,显示所有投资计划 + */ +export const PlansPanel: 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: 'plan', + stocks: [], + tags: [], + status: 'active', + }); + const [stockInput, setStockInput] = useState(''); + const [tagInput, setTagInput] = useState(''); + + // 筛选计划列表(排除系统事件) + const plans = allEvents.filter(event => event.type === 'plan' && 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: 'plan', + stocks: item.stocks || [], + tags: item.tags || [], + status: item.status || 'active', + }); + } else { + setEditingItem(null); + setFormData({ + date: dayjs().format('YYYY-MM-DD'), + title: '', + content: '', + type: 'plan', + 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('PlansPanel', `${editingItem ? '更新' : '创建'}成功`, { + itemId: editingItem?.id, + title: formData.title, + }); + toast({ + title: editingItem ? '更新成功' : '创建成功', + status: 'success', + duration: 2000, + }); + onClose(); + loadAllData(); + } else { + throw new Error('保存失败'); + } + } catch (error) { + logger.error('PlansPanel', '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('PlansPanel', '删除成功', { itemId: id }); + toast({ + title: '删除成功', + status: 'success', + duration: 2000, + }); + loadAllData(); + } + } catch (error) { + logger.error('PlansPanel', '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 ? ( +
+ +
+ ) : plans.length === 0 ? ( +
+ + + 暂无投资计划 + + +
+ ) : ( + + {plans.map(renderCard)} + + )} +
+ + {/* 编辑/新建模态框 */} + {isOpen && ( + + + + + {editingItem ? '编辑' : '新建'}投资计划 + + + + + + 日期 + + + + + setFormData({ ...formData, date: e.target.value })} + /> + + + + + 标题 + setFormData({ ...formData, title: e.target.value })} + placeholder="例如:布局新能源板块" + /> + + + + 内容 +