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:
@@ -25,11 +25,6 @@ import {
|
|||||||
Center,
|
Center,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
Tag,
|
Tag,
|
||||||
TagLabel,
|
TagLabel,
|
||||||
TagLeftIcon,
|
TagLeftIcon,
|
||||||
@@ -50,23 +45,13 @@ import dayjs, { Dayjs } from 'dayjs';
|
|||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
import { usePlanningData } from './PlanningContext';
|
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 { logger } from '@/utils/logger';
|
||||||
import { getApiBase } from '@/utils/apiConfig';
|
import { getApiBase } from '@/utils/apiConfig';
|
||||||
|
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
/**
|
|
||||||
* 新事件表单数据类型
|
|
||||||
*/
|
|
||||||
interface NewEventForm {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
type: EventType;
|
|
||||||
importance: number;
|
|
||||||
stocks: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FullCalendar 事件类型
|
* FullCalendar 事件类型
|
||||||
*/
|
*/
|
||||||
@@ -96,18 +81,13 @@ export const CalendarPanel: React.FC = () => {
|
|||||||
secondaryText,
|
secondaryText,
|
||||||
} = usePlanningData();
|
} = usePlanningData();
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
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 [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
|
||||||
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
|
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
|
||||||
const [newEvent, setNewEvent] = useState<NewEventForm>({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
type: 'plan',
|
|
||||||
importance: 3,
|
|
||||||
stocks: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 转换数据为 FullCalendar 格式
|
// 转换数据为 FullCalendar 格式
|
||||||
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({
|
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({
|
||||||
@@ -149,61 +129,15 @@ export const CalendarPanel: React.FC = () => {
|
|||||||
onOpen();
|
onOpen();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新事件
|
// 打开添加弹窗
|
||||||
const handleAddEvent = async (): Promise<void> => {
|
const handleOpenAddModal = (): void => {
|
||||||
try {
|
onClose(); // 先关闭详情弹窗
|
||||||
const base = getApiBase();
|
setIsAddModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const eventData = {
|
// 关闭添加弹窗
|
||||||
...newEvent,
|
const handleCloseAddModal = (): void => {
|
||||||
event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')),
|
setIsAddModalOpen(false);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除事件
|
// 删除事件
|
||||||
@@ -300,10 +234,7 @@ export const CalendarPanel: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="purple"
|
colorScheme="purple"
|
||||||
leftIcon={<FiPlus />}
|
leftIcon={<FiPlus />}
|
||||||
onClick={() => {
|
onClick={handleOpenAddModal}
|
||||||
onClose();
|
|
||||||
onAddOpen();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
添加投资计划
|
添加投资计划
|
||||||
</Button>
|
</Button>
|
||||||
@@ -397,88 +328,24 @@ export const CalendarPanel: React.FC = () => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 添加投资计划 Modal */}
|
{/* 使用通用弹窗组件 - 添加事件 */}
|
||||||
{isAddOpen && (
|
<EventFormModal
|
||||||
<Modal isOpen={isAddOpen} onClose={onAddClose} size="lg" closeOnOverlayClick={false} closeOnEsc={true}>
|
isOpen={isAddModalOpen}
|
||||||
<ModalOverlay />
|
onClose={handleCloseAddModal}
|
||||||
<ModalContent>
|
mode="create"
|
||||||
<ModalHeader>
|
eventType="plan"
|
||||||
添加投资计划
|
initialDate={selectedDate?.format('YYYY-MM-DD')}
|
||||||
</ModalHeader>
|
onSuccess={loadAllData}
|
||||||
<ModalCloseButton />
|
colorScheme="purple"
|
||||||
<ModalBody>
|
label="事件"
|
||||||
<VStack spacing={4}>
|
showDatePicker={false}
|
||||||
<FormControl isRequired>
|
showTypeSelect={true}
|
||||||
<FormLabel>标题</FormLabel>
|
showStatusSelect={false}
|
||||||
<Input
|
showImportance={true}
|
||||||
value={newEvent.title}
|
showTags={false}
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, title: e.target.value })}
|
stockInputMode="text"
|
||||||
placeholder="例如:关注半导体板块"
|
apiEndpoint="calendar/events"
|
||||||
/>
|
/>
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
496
src/views/Dashboard/components/EventFormModal.tsx
Normal file
496
src/views/Dashboard/components/EventFormModal.tsx
Normal 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;
|
||||||
@@ -8,46 +8,28 @@
|
|||||||
* - label: 显示文案
|
* - label: 显示文案
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Badge,
|
Badge,
|
||||||
IconButton,
|
IconButton,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalFooter,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
useDisclosure,
|
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
HStack,
|
||||||
Text,
|
Text,
|
||||||
Spinner,
|
Spinner,
|
||||||
Center,
|
Center,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
Tag,
|
Tag,
|
||||||
TagLabel,
|
TagLabel,
|
||||||
TagLeftIcon,
|
TagLeftIcon,
|
||||||
TagCloseButton,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FiEdit2,
|
FiEdit2,
|
||||||
FiTrash2,
|
FiTrash2,
|
||||||
FiSave,
|
|
||||||
FiFileText,
|
FiFileText,
|
||||||
FiCalendar,
|
FiCalendar,
|
||||||
FiTrendingUp,
|
FiTrendingUp,
|
||||||
@@ -60,7 +42,8 @@ import dayjs from 'dayjs';
|
|||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
import { usePlanningData } from './PlanningContext';
|
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 { logger } from '@/utils/logger';
|
||||||
import { getApiBase } from '@/utils/apiConfig';
|
import { getApiBase } from '@/utils/apiConfig';
|
||||||
|
|
||||||
@@ -109,105 +92,42 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
cardBg,
|
cardBg,
|
||||||
} = usePlanningData();
|
} = usePlanningData();
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
// 弹窗状态
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
|
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
|
||||||
const [formData, setFormData] = useState<PlanFormData>({
|
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
|
||||||
date: dayjs().format('YYYY-MM-DD'),
|
|
||||||
title: '',
|
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
|
||||||
content: '',
|
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
|
||||||
type,
|
|
||||||
stocks: [],
|
|
||||||
tags: [],
|
|
||||||
status: 'active',
|
|
||||||
});
|
|
||||||
const [stockInput, setStockInput] = useState<string>('');
|
|
||||||
const [tagInput, setTagInput] = useState<string>('');
|
|
||||||
|
|
||||||
// 筛选事件列表(按类型过滤,排除系统事件)
|
// 筛选事件列表(按类型过滤,排除系统事件)
|
||||||
const events = allEvents.filter(event => event.type === type && event.source !== 'future');
|
const events = allEvents.filter(event => event.type === type && event.source !== 'future');
|
||||||
|
|
||||||
// 打开编辑/新建模态框
|
// 监听外部触发打开新建模态框(修复 bug:只在值变化时触发)
|
||||||
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]);
|
|
||||||
|
|
||||||
// 监听外部触发打开新建模态框
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openModalTrigger && openModalTrigger > 0) {
|
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
|
||||||
|
// 只有当 trigger 值增加时才打开弹窗
|
||||||
handleOpenModal(null);
|
handleOpenModal(null);
|
||||||
}
|
}
|
||||||
}, [openModalTrigger, handleOpenModal]);
|
prevTriggerRef.current = openModalTrigger || 0;
|
||||||
|
}, [openModalTrigger]);
|
||||||
|
|
||||||
// 保存数据
|
// 打开编辑/新建模态框
|
||||||
const handleSave = async (): Promise<void> => {
|
const handleOpenModal = (item: InvestmentEvent | null = null): void => {
|
||||||
try {
|
if (item) {
|
||||||
const base = getApiBase();
|
setEditingItem(item);
|
||||||
|
setModalMode('edit');
|
||||||
const url = editingItem
|
} else {
|
||||||
? base + `/api/account/investment-plans/${editingItem.id}`
|
setEditingItem(null);
|
||||||
: base + '/api/account/investment-plans';
|
setModalMode('create');
|
||||||
|
|
||||||
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();
|
|
||||||
} else {
|
|
||||||
throw new Error('保存失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('EventPanel', 'handleSave', error, {
|
|
||||||
itemId: editingItem?.id,
|
|
||||||
title: formData?.title
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: '保存失败',
|
|
||||||
description: '无法保存数据',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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 => {
|
const getStatusInfo = (status?: EventStatus): StatusInfo => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -384,133 +282,24 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* 编辑/新建模态框 */}
|
{/* 使用通用弹窗组件 */}
|
||||||
{isOpen && (
|
<EventFormModal
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
|
isOpen={isModalOpen}
|
||||||
<ModalOverlay />
|
onClose={handleCloseModal}
|
||||||
<ModalContent>
|
mode={modalMode}
|
||||||
<ModalHeader>
|
eventType={type}
|
||||||
{editingItem ? '编辑' : '新建'}投资{label}
|
editingEvent={editingItem}
|
||||||
</ModalHeader>
|
onSuccess={loadAllData}
|
||||||
<ModalCloseButton />
|
colorScheme={colorScheme}
|
||||||
<ModalBody>
|
label={label}
|
||||||
<VStack spacing={4}>
|
showDatePicker={true}
|
||||||
<FormControl isRequired>
|
showTypeSelect={false}
|
||||||
<FormLabel>日期</FormLabel>
|
showStatusSelect={true}
|
||||||
<InputGroup>
|
showImportance={false}
|
||||||
<InputLeftElement pointerEvents="none">
|
showTags={true}
|
||||||
<Icon as={FiCalendar} color={secondaryText} />
|
stockInputMode="tag"
|
||||||
</InputLeftElement>
|
apiEndpoint="investment-plans"
|
||||||
<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
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
onClick={handleSave}
|
|
||||||
isDisabled={!formData.title || !formData.date}
|
|
||||||
leftIcon={<FiSave />}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user