pref: PlansPanel 和 ReviewsPanel 代码高度重复,提取公共组件

创建通用 EventPanel 组件
新建 EventPanel.tsx (~420 行) - 通用事件面板组件
  - 删除 PlansPanel.tsx (495 行 → 27 行,减少 94%)
  - 删除 ReviewsPanel.tsx (496 行 → 27 行,减少 94%)
  - 修复 CalendarPanel.tsx 中的 setActiveTab 引用
This commit is contained in:
zdl
2025-12-05 11:29:16 +08:00
parent 90a59e031c
commit 1351d2626a
4 changed files with 82 additions and 553 deletions

View File

@@ -91,7 +91,6 @@ export const CalendarPanel: React.FC = () => {
allEvents, allEvents,
loadAllData, loadAllData,
loading, loading,
setActiveTab,
toast, toast,
borderColor, borderColor,
secondaryText, secondaryText,
@@ -246,13 +245,8 @@ export const CalendarPanel: React.FC = () => {
} }
}; };
// 跳转到计划或复盘标签页 // 查看事件详情(关闭弹窗)
const handleViewDetails = (event: InvestmentEvent): void => { const handleViewDetails = (): void => {
if (event.type === 'plan') {
setActiveTab(1); // 跳转到"我的计划"标签页
} else if (event.type === 'review') {
setActiveTab(2); // 跳转到"我的复盘"标签页
}
onClose(); onClose();
}; };
@@ -357,7 +351,7 @@ export const CalendarPanel: React.FC = () => {
size="sm" size="sm"
variant="ghost" variant="ghost"
colorScheme="blue" colorScheme="blue"
onClick={() => handleViewDetails(event)} onClick={() => handleViewDetails()}
aria-label="查看详情" aria-label="查看详情"
/> />
</Tooltip> </Tooltip>

View File

@@ -1,9 +1,14 @@
/** /**
* ReviewsPanel - * EventPanel -
* *
*
* props
* - type: 'plan' | 'review'
* - colorScheme: 主题色
* - label: 显示文案
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { import {
Box, Box,
Button, Button,
@@ -71,10 +76,29 @@ interface StatusInfo {
} }
/** /**
* ReviewsPanel * EventPanel Props
*
*/ */
export const ReviewsPanel: React.FC = () => { export interface EventPanelProps {
/** 事件类型 */
type: 'plan' | 'review';
/** 主题颜色 */
colorScheme: string;
/** 显示标签(如 "计划" 或 "复盘" */
label: string;
/** 外部触发打开模态框的计数器 */
openModalTrigger?: number;
}
/**
* EventPanel
*
*/
export const EventPanel: React.FC<EventPanelProps> = ({
type,
colorScheme,
label,
openModalTrigger,
}) => {
const { const {
allEvents, allEvents,
loadAllData, loadAllData,
@@ -83,8 +107,6 @@ export const ReviewsPanel: React.FC = () => {
textColor, textColor,
secondaryText, secondaryText,
cardBg, cardBg,
borderColor,
openReviewModalTrigger,
} = usePlanningData(); } = usePlanningData();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
@@ -93,7 +115,7 @@ export const ReviewsPanel: React.FC = () => {
date: dayjs().format('YYYY-MM-DD'), date: dayjs().format('YYYY-MM-DD'),
title: '', title: '',
content: '', content: '',
type: 'review', type,
stocks: [], stocks: [],
tags: [], tags: [],
status: 'active', status: 'active',
@@ -101,25 +123,18 @@ export const ReviewsPanel: React.FC = () => {
const [stockInput, setStockInput] = useState<string>(''); const [stockInput, setStockInput] = useState<string>('');
const [tagInput, setTagInput] = useState<string>(''); const [tagInput, setTagInput] = useState<string>('');
// 筛选复盘列表(排除系统事件) // 筛选事件列表(按类型过滤,排除系统事件)
const reviews = allEvents.filter(event => event.type === 'review' && event.source !== 'future'); const events = allEvents.filter(event => event.type === type && event.source !== 'future');
// 监听外部触发打开新建模态框
useEffect(() => {
if (openReviewModalTrigger && openReviewModalTrigger > 0) {
handleOpenModal(null);
}
}, [openReviewModalTrigger]);
// 打开编辑/新建模态框 // 打开编辑/新建模态框
const handleOpenModal = (item: InvestmentEvent | null = null): void => { const handleOpenModal = useCallback((item: InvestmentEvent | null = null): void => {
if (item) { if (item) {
setEditingItem(item); setEditingItem(item);
setFormData({ setFormData({
date: dayjs(item.event_date || item.date).format('YYYY-MM-DD'), date: dayjs(item.event_date || item.date).format('YYYY-MM-DD'),
title: item.title, title: item.title,
content: item.description || item.content || '', content: item.description || item.content || '',
type: 'review', type,
stocks: item.stocks || [], stocks: item.stocks || [],
tags: item.tags || [], tags: item.tags || [],
status: item.status || 'active', status: item.status || 'active',
@@ -130,14 +145,21 @@ export const ReviewsPanel: React.FC = () => {
date: dayjs().format('YYYY-MM-DD'), date: dayjs().format('YYYY-MM-DD'),
title: '', title: '',
content: '', content: '',
type: 'review', type,
stocks: [], stocks: [],
tags: [], tags: [],
status: 'active', status: 'active',
}); });
} }
onOpen(); onOpen();
}; }, [type, onOpen]);
// 监听外部触发打开新建模态框
useEffect(() => {
if (openModalTrigger && openModalTrigger > 0) {
handleOpenModal(null);
}
}, [openModalTrigger, handleOpenModal]);
// 保存数据 // 保存数据
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
@@ -160,7 +182,7 @@ export const ReviewsPanel: React.FC = () => {
}); });
if (response.ok) { if (response.ok) {
logger.info('ReviewsPanel', `${editingItem ? '更新' : '创建'}成功`, { logger.info('EventPanel', `${editingItem ? '更新' : '创建'}${label}成功`, {
itemId: editingItem?.id, itemId: editingItem?.id,
title: formData.title, title: formData.title,
}); });
@@ -175,7 +197,7 @@ export const ReviewsPanel: React.FC = () => {
throw new Error('保存失败'); throw new Error('保存失败');
} }
} catch (error) { } catch (error) {
logger.error('ReviewsPanel', 'handleSave', error, { logger.error('EventPanel', 'handleSave', error, {
itemId: editingItem?.id, itemId: editingItem?.id,
title: formData?.title title: formData?.title
}); });
@@ -201,7 +223,7 @@ export const ReviewsPanel: React.FC = () => {
}); });
if (response.ok) { if (response.ok) {
logger.info('ReviewsPanel', '删除成功', { itemId: id }); logger.info('EventPanel', `删除${label}成功`, { itemId: id });
toast({ toast({
title: '删除成功', title: '删除成功',
status: 'success', status: 'success',
@@ -210,7 +232,7 @@ export const ReviewsPanel: React.FC = () => {
loadAllData(); loadAllData();
} }
} catch (error) { } catch (error) {
logger.error('ReviewsPanel', 'handleDelete', error, { itemId: id }); logger.error('EventPanel', 'handleDelete', error, { itemId: id });
toast({ toast({
title: '删除失败', title: '删除失败',
status: 'error', status: 'error',
@@ -270,7 +292,7 @@ export const ReviewsPanel: React.FC = () => {
<Flex justify="space-between" align="start"> <Flex justify="space-between" align="start">
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
<HStack> <HStack>
<Icon as={FiFileText} color="green.500" /> <Icon as={FiFileText} color={`${colorScheme}.500`} />
<Text fontWeight="bold" fontSize="lg"> <Text fontWeight="bold" fontSize="lg">
{item.title} {item.title}
</Text> </Text>
@@ -294,7 +316,7 @@ export const ReviewsPanel: React.FC = () => {
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => handleOpenModal(item)} onClick={() => handleOpenModal(item)}
aria-label="编辑复盘" aria-label={`编辑${label}`}
/> />
<IconButton <IconButton
icon={<FiTrash2 />} icon={<FiTrash2 />}
@@ -302,7 +324,7 @@ export const ReviewsPanel: React.FC = () => {
variant="ghost" variant="ghost"
colorScheme="red" colorScheme="red"
onClick={() => handleDelete(item.id)} onClick={() => handleDelete(item.id)}
aria-label="删除复盘" aria-label={`删除${label}`}
/> />
</HStack> </HStack>
</Flex> </Flex>
@@ -327,7 +349,7 @@ export const ReviewsPanel: React.FC = () => {
{item.tags && item.tags.length > 0 && ( {item.tags && item.tags.length > 0 && (
<> <>
{item.tags.map((tag, idx) => ( {item.tags.map((tag, idx) => (
<Tag key={idx} size="sm" colorScheme="green" variant="subtle"> <Tag key={idx} size="sm" colorScheme={colorScheme} variant="subtle">
<TagLeftIcon as={FiHash} /> <TagLeftIcon as={FiHash} />
<TagLabel>{tag}</TagLabel> <TagLabel>{tag}</TagLabel>
</Tag> </Tag>
@@ -346,18 +368,18 @@ export const ReviewsPanel: React.FC = () => {
<VStack align="stretch" spacing={4}> <VStack align="stretch" spacing={4}>
{loading ? ( {loading ? (
<Center py={8}> <Center py={8}>
<Spinner size="xl" color="green.500" /> <Spinner size="xl" color={`${colorScheme}.500`} />
</Center> </Center>
) : reviews.length === 0 ? ( ) : events.length === 0 ? (
<Center py={8}> <Center py={8}>
<VStack spacing={3}> <VStack spacing={3}>
<Icon as={FiFileText} boxSize={12} color="gray.300" /> <Icon as={FiFileText} boxSize={12} color="gray.300" />
<Text color={secondaryText}></Text> <Text color={secondaryText}>{label}</Text>
</VStack> </VStack>
</Center> </Center>
) : ( ) : (
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}> <Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
{reviews.map(renderCard)} {events.map(renderCard)}
</Grid> </Grid>
)} )}
</VStack> </VStack>
@@ -368,7 +390,7 @@ export const ReviewsPanel: React.FC = () => {
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
{editingItem ? '编辑' : '新建'} {editingItem ? '编辑' : '新建'}{label}
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
@@ -392,7 +414,7 @@ export const ReviewsPanel: React.FC = () => {
<Input <Input
value={formData.title} value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })} onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="例如:本周操作复盘" placeholder={type === 'plan' ? '例如:布局新能源板块' : '例如:本周操作复盘'}
/> />
</FormControl> </FormControl>
@@ -401,7 +423,7 @@ export const ReviewsPanel: React.FC = () => {
<Textarea <Textarea
value={formData.content} value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })} onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder="详细记录您的投资复盘..." placeholder={type === 'plan' ? '详细描述您的投资计划...' : '详细记录您的投资复盘...'}
rows={6} rows={6}
/> />
</FormControl> </FormControl>
@@ -446,7 +468,7 @@ export const ReviewsPanel: React.FC = () => {
</HStack> </HStack>
<HStack mt={2} spacing={2} flexWrap="wrap"> <HStack mt={2} spacing={2} flexWrap="wrap">
{(formData.tags || []).map((tag, idx) => ( {(formData.tags || []).map((tag, idx) => (
<Tag key={idx} size="sm" colorScheme="green"> <Tag key={idx} size="sm" colorScheme={colorScheme}>
<TagLeftIcon as={FiHash} /> <TagLeftIcon as={FiHash} />
<TagLabel>{tag}</TagLabel> <TagLabel>{tag}</TagLabel>
<TagCloseButton <TagCloseButton
@@ -478,7 +500,7 @@ export const ReviewsPanel: React.FC = () => {
</Button> </Button>
<Button <Button
colorScheme="green" colorScheme={colorScheme}
onClick={handleSave} onClick={handleSave}
isDisabled={!formData.title || !formData.date} isDisabled={!formData.title || !formData.date}
leftIcon={<FiSave />} leftIcon={<FiSave />}
@@ -492,3 +514,5 @@ export const ReviewsPanel: React.FC = () => {
</Box> </Box>
); );
}; };
export default EventPanel;

View File

@@ -3,14 +3,12 @@
* *
* 性能优化: * 性能优化:
* - 使用 React.lazy() 懒加载子面板,减少初始加载时间 * - 使用 React.lazy() 懒加载子面板,减少初始加载时间
* - 从 1421 行拆分为 5 个独立模块,提升可维护性
* - 使用 TypeScript 提供类型安全 * - 使用 TypeScript 提供类型安全
* *
* 组件架构: * 组件架构:
* - InvestmentPlanningCenter (主组件~200 行) * - InvestmentPlanningCenter (主组件)
* - CalendarPanel (日历面板,懒加载) * - CalendarPanel (日历面板,懒加载)
* - PlansPanel (计划面板,懒加载) * - EventPanel (通用事件面板,用于计划和复盘)
* - ReviewsPanel (复盘面板,懒加载)
* - PlanningContext (数据共享层) * - PlanningContext (数据共享层)
*/ */
@@ -54,11 +52,8 @@ import './InvestmentCalendar.css';
const CalendarPanel = lazy(() => const CalendarPanel = lazy(() =>
import('./CalendarPanel').then(module => ({ default: module.CalendarPanel })) import('./CalendarPanel').then(module => ({ default: module.CalendarPanel }))
); );
const PlansPanel = lazy(() => const EventPanel = lazy(() =>
import('./PlansPanel').then(module => ({ default: module.PlansPanel })) import('./EventPanel').then(module => ({ default: module.EventPanel }))
);
const ReviewsPanel = lazy(() =>
import('./ReviewsPanel').then(module => ({ default: module.ReviewsPanel }))
); );
/** /**
@@ -220,14 +215,24 @@ const InvestmentPlanningCenter: React.FC = () => {
{/* 计划列表面板 */} {/* 计划列表面板 */}
<TabPanel px={0}> <TabPanel px={0}>
<Suspense fallback={<PanelLoadingFallback />}> <Suspense fallback={<PanelLoadingFallback />}>
<PlansPanel /> <EventPanel
type="plan"
colorScheme="purple"
label="计划"
openModalTrigger={openPlanModalTrigger}
/>
</Suspense> </Suspense>
</TabPanel> </TabPanel>
{/* 复盘列表面板 */} {/* 复盘列表面板 */}
<TabPanel px={0}> <TabPanel px={0}>
<Suspense fallback={<PanelLoadingFallback />}> <Suspense fallback={<PanelLoadingFallback />}>
<ReviewsPanel /> <EventPanel
type="review"
colorScheme="green"
label="复盘"
openModalTrigger={openReviewModalTrigger}
/>
</Suspense> </Suspense>
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>

View File

@@ -1,494 +0,0 @@
/**
* PlansPanel - 投资计划列表面板组件
* 显示、编辑和管理投资计划
*/
import React, { useState, useEffect } 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;
}
/**
* PlansPanel 组件
* 计划列表面板,显示所有投资计划
*/
export const PlansPanel: React.FC = () => {
const {
allEvents,
loadAllData,
loading,
toast,
textColor,
secondaryText,
cardBg,
borderColor,
openPlanModalTrigger,
} = usePlanningData();
const { isOpen, onOpen, onClose } = useDisclosure();
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
const [formData, setFormData] = useState<PlanFormData>({
date: dayjs().format('YYYY-MM-DD'),
title: '',
content: '',
type: 'plan',
stocks: [],
tags: [],
status: 'active',
});
const [stockInput, setStockInput] = useState<string>('');
const [tagInput, setTagInput] = useState<string>('');
// 筛选计划列表(排除系统事件)
const plans = allEvents.filter(event => event.type === 'plan' && event.source !== 'future');
// 监听外部触发打开新建模态框
useEffect(() => {
if (openPlanModalTrigger && openPlanModalTrigger > 0) {
handleOpenModal(null);
}
}, [openPlanModalTrigger]);
// 打开编辑/新建模态框
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<void> => {
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<void> => {
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): React.ReactElement => {
const statusInfo = getStatusInfo(item.status);
return (
<Card
key={item.id}
bg={cardBg}
shadow="sm"
_hover={{ shadow: 'md' }}
transition="all 0.2s"
>
<CardBody>
<VStack align="stretch" spacing={3}>
<Flex justify="space-between" align="start">
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Icon as={FiFileText} color="purple.500" />
<Text fontWeight="bold" fontSize="lg">
{item.title}
</Text>
</HStack>
<HStack spacing={2}>
<Icon as={FiCalendar} boxSize={3} color={secondaryText} />
<Text fontSize="sm" color={secondaryText}>
{dayjs(item.event_date || item.date).format('YYYY年MM月DD日')}
</Text>
<Badge
colorScheme={statusInfo.color}
variant="subtle"
>
{statusInfo.text}
</Badge>
</HStack>
</VStack>
<HStack>
<IconButton
icon={<FiEdit2 />}
size="sm"
variant="ghost"
onClick={() => handleOpenModal(item)}
aria-label="编辑计划"
/>
<IconButton
icon={<FiTrash2 />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => handleDelete(item.id)}
aria-label="删除计划"
/>
</HStack>
</Flex>
{(item.content || item.description) && (
<Text fontSize="sm" color={textColor} noOfLines={3}>
{item.content || item.description}
</Text>
)}
<HStack spacing={2} flexWrap="wrap">
{item.stocks && item.stocks.length > 0 && (
<>
{item.stocks.map((stock, idx) => (
<Tag key={idx} size="sm" colorScheme="blue" variant="subtle">
<TagLeftIcon as={FiTrendingUp} />
<TagLabel>{stock}</TagLabel>
</Tag>
))}
</>
)}
{item.tags && item.tags.length > 0 && (
<>
{item.tags.map((tag, idx) => (
<Tag key={idx} size="sm" colorScheme="purple" variant="subtle">
<TagLeftIcon as={FiHash} />
<TagLabel>{tag}</TagLabel>
</Tag>
))}
</>
)}
</HStack>
</VStack>
</CardBody>
</Card>
);
};
return (
<Box>
<VStack align="stretch" spacing={4}>
{loading ? (
<Center py={8}>
<Spinner size="xl" color="purple.500" />
</Center>
) : plans.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiTarget} boxSize={12} color="gray.300" />
<Text color={secondaryText}></Text>
</VStack>
</Center>
) : (
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
{plans.map(renderCard)}
</Grid>
)}
</VStack>
{/* 编辑/新建模态框 */}
{isOpen && (
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{editingItem ? '编辑' : '新建'}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel></FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<Icon as={FiCalendar} color={secondaryText} />
</InputLeftElement>
<Input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
/>
</InputGroup>
</FormControl>
<FormControl isRequired>
<FormLabel></FormLabel>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="例如:布局新能源板块"
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder="详细描述您的投资计划..."
rows={6}
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<HStack>
<Input
value={stockInput}
onChange={(e) => setStockInput(e.target.value)}
placeholder="输入股票代码"
onKeyPress={(e) => e.key === 'Enter' && handleAddStock()}
/>
<Button onClick={handleAddStock}></Button>
</HStack>
<HStack mt={2} spacing={2} flexWrap="wrap">
{(formData.stocks || []).map((stock, idx) => (
<Tag key={idx} size="sm" colorScheme="blue">
<TagLeftIcon as={FiTrendingUp} />
<TagLabel>{stock}</TagLabel>
<TagCloseButton
onClick={() => setFormData({
...formData,
stocks: formData.stocks.filter((_, i) => i !== idx)
})}
/>
</Tag>
))}
</HStack>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<HStack>
<Input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
placeholder="输入标签"
onKeyPress={(e) => e.key === 'Enter' && handleAddTag()}
/>
<Button onClick={handleAddTag}></Button>
</HStack>
<HStack mt={2} spacing={2} flexWrap="wrap">
{(formData.tags || []).map((tag, idx) => (
<Tag key={idx} size="sm" colorScheme="purple">
<TagLeftIcon as={FiHash} />
<TagLabel>{tag}</TagLabel>
<TagCloseButton
onClick={() => setFormData({
...formData,
tags: formData.tags.filter((_, i) => i !== idx)
})}
/>
</Tag>
))}
</HStack>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as EventStatus })}
>
<option value="active"></option>
<option value="completed"></option>
<option value="cancelled"></option>
</Select>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onClose}>
</Button>
<Button
colorScheme="purple"
onClick={handleSave}
isDisabled={!formData.title || !formData.date}
leftIcon={<FiSave />}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
</Box>
);
};