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 2f04293c68
commit 6272e50348
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 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 handleOpenAddModal = (): void => {
onClose(); // 先关闭详情弹窗
setIsAddModalOpen(true);
};
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
{/* 使用通用弹窗组件 - 添加事件 */}
<EventFormModal
isOpen={isAddModalOpen}
onClose={handleCloseAddModal}
mode="create"
eventType="plan"
initialDate={selectedDate?.format('YYYY-MM-DD')}
onSuccess={loadAllData}
colorScheme="purple"
onClick={handleAddEvent}
isDisabled={!newEvent.title}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
label="事件"
showDatePicker={false}
showTypeSelect={true}
showStatusSelect={false}
showImportance={true}
showTags={false}
stockInputMode="text"
apiEndpoint="calendar/events"
/>
</Box>
);
};

View File

@@ -0,0 +1,496 @@
/**
* EventFormModal - 通用事件表单弹窗组件
* 用于新建/编辑投资计划、复盘、日历事件等
*
* 通过 props 配置差异化行为:
* - 字段显示控制(日期选择器、类型、状态、重要度、标签等)
* - API 端点配置investment-plans 或 calendar/events
* - 主题颜色和标签文案
*/
import React, { useState, useEffect } from 'react';
import {
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
VStack,
HStack,
Icon,
Input,
InputGroup,
InputLeftElement,
FormControl,
FormLabel,
Textarea,
Select,
Tag,
TagLabel,
TagLeftIcon,
TagCloseButton,
} from '@chakra-ui/react';
import {
FiSave,
FiCalendar,
FiTrendingUp,
FiHash,
} from 'react-icons/fi';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { usePlanningData } from './PlanningContext';
import type { InvestmentEvent, EventType, EventStatus } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
dayjs.locale('zh-cn');
/**
* 表单数据接口
*/
interface FormData {
date: string;
title: string;
content: string;
type: EventType;
stocks: string[];
tags: string[];
status: EventStatus;
importance: number;
}
/**
* EventFormModal Props
*/
export interface EventFormModalProps {
/** 弹窗是否打开 */
isOpen: boolean;
/** 关闭弹窗回调 */
onClose: () => void;
/** 模式:新建或编辑 */
mode: 'create' | 'edit';
/** 事件类型(新建时使用) */
eventType?: EventType;
/** 初始日期(新建时使用,如从日历点击) */
initialDate?: string;
/** 编辑时的原始事件数据 */
editingEvent?: InvestmentEvent | null;
/** 保存成功回调 */
onSuccess: () => void;
/** 主题颜色 */
colorScheme?: string;
/** 显示标签(如 "计划"、"复盘"、"事件" */
label?: string;
/** 是否显示日期选择器 */
showDatePicker?: boolean;
/** 是否显示类型选择 */
showTypeSelect?: boolean;
/** 是否显示状态选择 */
showStatusSelect?: boolean;
/** 是否显示重要度选择 */
showImportance?: boolean;
/** 是否显示标签输入 */
showTags?: boolean;
/** 股票输入方式:'tag' 为标签形式,'text' 为逗号分隔文本 */
stockInputMode?: 'tag' | 'text';
/** API 端点 */
apiEndpoint?: 'investment-plans' | 'calendar/events';
}
/**
* EventFormModal 组件
*/
export const EventFormModal: React.FC<EventFormModalProps> = ({
isOpen,
onClose,
mode,
eventType = 'plan',
initialDate,
editingEvent,
onSuccess,
colorScheme = 'purple',
label = '事件',
showDatePicker = true,
showTypeSelect = false,
showStatusSelect = true,
showImportance = false,
showTags = true,
stockInputMode = 'tag',
apiEndpoint = 'investment-plans',
}) => {
const { toast, secondaryText } = usePlanningData();
// 表单数据
const [formData, setFormData] = useState<FormData>({
date: initialDate || dayjs().format('YYYY-MM-DD'),
title: '',
content: '',
type: eventType,
stocks: [],
tags: [],
status: 'active',
importance: 3,
});
// 股票和标签输入框
const [stockInput, setStockInput] = useState<string>('');
const [tagInput, setTagInput] = useState<string>('');
// 保存中状态
const [saving, setSaving] = useState<boolean>(false);
// 初始化表单数据
useEffect(() => {
if (mode === 'edit' && editingEvent) {
setFormData({
date: dayjs(editingEvent.event_date || editingEvent.date).format('YYYY-MM-DD'),
title: editingEvent.title,
content: editingEvent.description || editingEvent.content || '',
type: editingEvent.type || eventType,
stocks: editingEvent.stocks || [],
tags: editingEvent.tags || [],
status: editingEvent.status || 'active',
importance: editingEvent.importance || 3,
});
// 如果是文本模式,将股票数组转为逗号分隔
if (stockInputMode === 'text' && editingEvent.stocks) {
setStockInput(editingEvent.stocks.join(','));
}
} else {
// 新建模式,重置表单
setFormData({
date: initialDate || dayjs().format('YYYY-MM-DD'),
title: '',
content: '',
type: eventType,
stocks: [],
tags: [],
status: 'active',
importance: 3,
});
setStockInput('');
setTagInput('');
}
}, [mode, editingEvent, eventType, initialDate, stockInputMode]);
// 保存数据
const handleSave = async (): Promise<void> => {
try {
setSaving(true);
const base = getApiBase();
// 构建请求数据
let requestData: Record<string, unknown> = { ...formData };
// 如果是文本模式,解析股票输入
if (stockInputMode === 'text' && stockInput) {
requestData.stocks = stockInput.split(',').map(s => s.trim()).filter(s => s);
}
// 根据 API 端点调整字段名
if (apiEndpoint === 'calendar/events') {
requestData = {
title: formData.title,
description: formData.content,
type: formData.type,
importance: formData.importance,
stocks: requestData.stocks,
event_date: formData.date,
};
}
const url = mode === 'edit' && editingEvent
? `${base}/api/account/${apiEndpoint}/${editingEvent.id}`
: `${base}/api/account/${apiEndpoint}`;
const method = mode === 'edit' ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestData),
});
if (response.ok) {
logger.info('EventFormModal', `${mode === 'edit' ? '更新' : '创建'}${label}成功`, {
itemId: editingEvent?.id,
title: formData.title,
});
toast({
title: mode === 'edit' ? '更新成功' : '创建成功',
status: 'success',
duration: 2000,
});
onClose();
onSuccess();
} else {
throw new Error('保存失败');
}
} catch (error) {
logger.error('EventFormModal', 'handleSave', error, {
itemId: editingEvent?.id,
title: formData?.title
});
toast({
title: '保存失败',
description: '无法保存数据',
status: 'error',
duration: 3000,
});
} finally {
setSaving(false);
}
};
// 添加股票Tag 模式)
const handleAddStock = (): void => {
if (stockInput.trim() && !formData.stocks.includes(stockInput.trim())) {
setFormData({
...formData,
stocks: [...formData.stocks, stockInput.trim()],
});
setStockInput('');
}
};
// 移除股票Tag 模式)
const handleRemoveStock = (index: number): void => {
setFormData({
...formData,
stocks: formData.stocks.filter((_, i) => i !== index),
});
};
// 添加标签
const handleAddTag = (): void => {
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
setFormData({
...formData,
tags: [...formData.tags, tagInput.trim()],
});
setTagInput('');
}
};
// 移除标签
const handleRemoveTag = (index: number): void => {
setFormData({
...formData,
tags: formData.tags.filter((_, i) => i !== index),
});
};
// 获取标题 placeholder
const getTitlePlaceholder = (): string => {
switch (formData.type) {
case 'plan':
return '例如:布局新能源板块';
case 'review':
return '例如:本周操作复盘';
case 'reminder':
return '例如:关注半导体板块';
case 'analysis':
return '例如:行业分析任务';
default:
return '请输入标题';
}
};
// 获取内容 placeholder
const getContentPlaceholder = (): string => {
switch (formData.type) {
case 'plan':
return '详细描述您的投资计划...';
case 'review':
return '详细记录您的投资复盘...';
default:
return '详细描述...';
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{mode === 'edit' ? '编辑' : '新建'}{label}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
{/* 日期选择器 */}
{showDatePicker && (
<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={getTitlePlaceholder()}
/>
</FormControl>
{/* 内容/描述 */}
<FormControl>
<FormLabel></FormLabel>
<Textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder={getContentPlaceholder()}
rows={6}
/>
</FormControl>
{/* 类型选择 */}
{showTypeSelect && (
<FormControl>
<FormLabel></FormLabel>
<Select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as EventType })}
>
<option value="plan"></option>
<option value="review"></option>
<option value="reminder"></option>
<option value="analysis"></option>
</Select>
</FormControl>
)}
{/* 状态选择 */}
{showStatusSelect && (
<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>
)}
{/* 重要度选择 */}
{showImportance && (
<FormControl>
<FormLabel></FormLabel>
<Select
value={formData.importance}
onChange={(e) => setFormData({ ...formData, 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>
)}
{/* 股票输入 - Tag 模式 */}
{stockInputMode === 'tag' && (
<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={() => handleRemoveStock(idx)} />
</Tag>
))}
</HStack>
</FormControl>
)}
{/* 股票输入 - 文本模式 */}
{stockInputMode === 'text' && (
<FormControl>
<FormLabel></FormLabel>
<Input
value={stockInput}
onChange={(e) => setStockInput(e.target.value)}
placeholder="例如600519,000858,002415"
/>
</FormControl>
)}
{/* 标签输入 */}
{showTags && (
<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={colorScheme}>
<TagLeftIcon as={FiHash} />
<TagLabel>{tag}</TagLabel>
<TagCloseButton onClick={() => handleRemoveTag(idx)} />
</Tag>
))}
</HStack>
</FormControl>
)}
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onClose}>
</Button>
<Button
colorScheme={colorScheme}
onClick={handleSave}
isDisabled={!formData.title || (showDatePicker && !formData.date)}
isLoading={saving}
leftIcon={<FiSave />}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default EventFormModal;

View File

@@ -8,46 +8,28 @@
* - label: 显示文案
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useRef } 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,
@@ -60,7 +42,8 @@ import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { usePlanningData } from './PlanningContext';
import type { InvestmentEvent, PlanFormData, EventStatus } from '@/types';
import { EventFormModal } from './EventFormModal';
import type { InvestmentEvent, EventStatus } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
@@ -109,105 +92,42 @@ export const EventPanel: React.FC<EventPanelProps> = ({
cardBg,
} = usePlanningData();
const { isOpen, onOpen, onClose } = useDisclosure();
// 弹窗状态
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
const [formData, setFormData] = useState<PlanFormData>({
date: dayjs().format('YYYY-MM-DD'),
title: '',
content: '',
type,
stocks: [],
tags: [],
status: 'active',
});
const [stockInput, setStockInput] = useState<string>('');
const [tagInput, setTagInput] = useState<string>('');
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
// 筛选事件列表(按类型过滤,排除系统事件)
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]);
// 监听外部触发打开新建模态框
// 监听外部触发打开新建模态框(修复 bug只在值变化时触发
useEffect(() => {
if (openModalTrigger && openModalTrigger > 0) {
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
// 只有当 trigger 值增加时才打开弹窗
handleOpenModal(null);
}
}, [openModalTrigger, handleOpenModal]);
prevTriggerRef.current = openModalTrigger || 0;
}, [openModalTrigger]);
// 保存数据
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('EventPanel', `${editingItem ? '更新' : '创建'}${label}成功`, {
itemId: editingItem?.id,
title: formData.title,
});
toast({
title: editingItem ? '更新成功' : '创建成功',
status: 'success',
duration: 2000,
});
onClose();
loadAllData();
// 打开编辑/新建模态框
const handleOpenModal = (item: InvestmentEvent | null = null): void => {
if (item) {
setEditingItem(item);
setModalMode('edit');
} else {
throw new Error('保存失败');
}
} catch (error) {
logger.error('EventPanel', 'handleSave', error, {
itemId: editingItem?.id,
title: formData?.title
});
toast({
title: '保存失败',
description: '无法保存数据',
status: 'error',
duration: 3000,
});
setEditingItem(null);
setModalMode('create');
}
setIsModalOpen(true);
};
// 关闭弹窗
const handleCloseModal = (): void => {
setIsModalOpen(false);
setEditingItem(null);
};
// 删除数据
@@ -241,28 +161,6 @@ export const EventPanel: React.FC<EventPanelProps> = ({
}
};
// 添加股票
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) {
@@ -384,133 +282,24 @@ export const EventPanel: React.FC<EventPanelProps> = ({
)}
</VStack>
{/* 编辑/新建模态框 */}
{isOpen && (
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{editingItem ? '编辑' : '新建'}{label}
</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={type === 'plan' ? '例如:布局新能源板块' : '例如:本周操作复盘'}
/>
</FormControl>
<FormControl>
<FormLabel></FormLabel>
<Textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder={type === 'plan' ? '详细描述您的投资计划...' : '详细记录您的投资复盘...'}
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={colorScheme}>
<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
{/* 使用通用弹窗组件 */}
<EventFormModal
isOpen={isModalOpen}
onClose={handleCloseModal}
mode={modalMode}
eventType={type}
editingEvent={editingItem}
onSuccess={loadAllData}
colorScheme={colorScheme}
onClick={handleSave}
isDisabled={!formData.title || !formData.date}
leftIcon={<FiSave />}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
label={label}
showDatePicker={true}
showTypeSelect={false}
showStatusSelect={true}
showImportance={false}
showTags={true}
stockInputMode="tag"
apiEndpoint="investment-plans"
/>
</Box>
);
};