refactor: 抽取 EventFormModal 通用弹窗组件,修复视图切换弹窗自动打开 bug

- 新建 EventFormModal.tsx 通用弹窗组件(约 500 行)
  - 支持通过 props 配置字段显示(日期、类型、状态、重要度、标签)
  - 支持两种 API 端点(investment-plans / calendar/events)
  - 支持两种股票输入模式(tag 标签形式 / text 逗号分隔)

- 重构 EventPanel.tsx 使用 EventFormModal
  - 使用 useRef 修复弹窗自动打开 bug(视图切换时不再误触发)
  - 移除内联 Modal 代码,减少约 200 行

- 重构 CalendarPanel.tsx 使用 EventFormModal
  - 添加事件功能改用 EventFormModal
  - 保留详情弹窗(只读展示当日事件列表)
  - 移除内联表单代码,减少约 100 行

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-05 12:11:14 +08:00
parent 260602a408
commit 750547645d
3 changed files with 575 additions and 423 deletions

View File

@@ -25,11 +25,6 @@ import {
Center,
Tooltip,
Icon,
Input,
FormControl,
FormLabel,
Textarea,
Select,
Tag,
TagLabel,
TagLeftIcon,
@@ -50,23 +45,13 @@ import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';
import { usePlanningData } from './PlanningContext';
import type { InvestmentEvent, EventType } from '@/types';
import { EventFormModal } from './EventFormModal';
import type { InvestmentEvent } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
dayjs.locale('zh-cn');
/**
* 新事件表单数据类型
*/
interface NewEventForm {
title: string;
description: string;
type: EventType;
importance: number;
stocks: string;
}
/**
* FullCalendar 事件类型
*/
@@ -96,18 +81,13 @@ export const CalendarPanel: React.FC = () => {
secondaryText,
} = usePlanningData();
// 详情弹窗
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure();
// 添加弹窗状态
const [isAddModalOpen, setIsAddModalOpen] = useState<boolean>(false);
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
const [newEvent, setNewEvent] = useState<NewEventForm>({
title: '',
description: '',
type: 'plan',
importance: 3,
stocks: '',
});
// 转换数据为 FullCalendar 格式
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({
@@ -149,61 +129,15 @@ export const CalendarPanel: React.FC = () => {
onOpen();
};
// 添加新事件
const handleAddEvent = async (): Promise<void> => {
try {
const base = getApiBase();
// 打开添加弹窗
const handleOpenAddModal = (): void => {
onClose(); // 先关闭详情弹窗
setIsAddModalOpen(true);
};
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('CalendarPanel', '添加事件成功', {
eventTitle: eventData.title,
eventDate: eventData.event_date
});
toast({
title: '添加成功',
description: '投资计划已添加',
status: 'success',
duration: 3000,
});
onAddClose();
loadAllData();
setNewEvent({
title: '',
description: '',
type: 'plan',
importance: 3,
stocks: '',
});
}
}
} catch (error) {
logger.error('CalendarPanel', 'handleAddEvent', error, {
eventTitle: newEvent?.title
});
toast({
title: '添加失败',
description: '无法添加投资计划',
status: 'error',
duration: 3000,
});
}
// 关闭添加弹窗
const handleCloseAddModal = (): void => {
setIsAddModalOpen(false);
};
// 删除事件
@@ -300,10 +234,7 @@ export const CalendarPanel: React.FC = () => {
size="sm"
colorScheme="purple"
leftIcon={<FiPlus />}
onClick={() => {
onClose();
onAddOpen();
}}
onClick={handleOpenAddModal}
>
</Button>
@@ -397,88 +328,24 @@ export const CalendarPanel: React.FC = () => {
</Modal>
)}
{/* 添加投资计划 Modal */}
{isAddOpen && (
<Modal isOpen={isAddOpen} onClose={onAddClose} size="lg" closeOnOverlayClick={false} closeOnEsc={true}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel></FormLabel>
<Input
value={newEvent.title}
onChange={(e) => setNewEvent({ ...newEvent, title: e.target.value })}
placeholder="例如:关注半导体板块"
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Textarea
value={newEvent.description}
onChange={(e) => setNewEvent({ ...newEvent, description: e.target.value })}
placeholder="详细描述您的投资计划..."
rows={3}
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Select
value={newEvent.type}
onChange={(e) => setNewEvent({ ...newEvent, type: e.target.value as EventType })}
>
<option value="plan"></option>
<option value="review"></option>
<option value="reminder"></option>
<option value="analysis"></option>
</Select>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Select
value={newEvent.importance}
onChange={(e) => setNewEvent({ ...newEvent, importance: parseInt(e.target.value) })}
>
<option value={5}> </option>
<option value={4}> </option>
<option value={3}> </option>
<option value={2}> </option>
<option value={1}> </option>
</Select>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Input
value={newEvent.stocks}
onChange={(e) => setNewEvent({ ...newEvent, stocks: e.target.value })}
placeholder="例如600519,000858,002415"
/>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onAddClose}>
</Button>
<Button
colorScheme="purple"
onClick={handleAddEvent}
isDisabled={!newEvent.title}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
{/* 使用通用弹窗组件 - 添加事件 */}
<EventFormModal
isOpen={isAddModalOpen}
onClose={handleCloseAddModal}
mode="create"
eventType="plan"
initialDate={selectedDate?.format('YYYY-MM-DD')}
onSuccess={loadAllData}
colorScheme="purple"
label="事件"
showDatePicker={false}
showTypeSelect={true}
showStatusSelect={false}
showImportance={true}
showTags={false}
stockInputMode="text"
apiEndpoint="calendar/events"
/>
</Box>
);
};