Files
vf_react/src/views/Center/components/EventPanel.tsx

204 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* EventPanel - 通用事件面板组件 (Redux 版本)
* 用于显示、编辑和管理投资计划或复盘
*
* 通过 props 配置差异化行为:
* - type: 'plan' | 'review'
* - colorScheme: 主题色
* - label: 显示文案
*/
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Box,
Grid,
VStack,
Text,
Spinner,
Center,
Icon,
useToast,
} from '@chakra-ui/react';
import { FiFileText } from 'react-icons/fi';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
fetchAllEvents,
removeEvent,
selectPlans,
selectReviews,
selectPlanningLoading,
} from '@/store/slices/planningSlice';
import { getApiBase } from '@/utils/apiConfig';
import { EventFormModal } from './EventFormModal';
import { FUIEventCard } from './FUIEventCard';
import type { InvestmentEvent } from '@/types';
import { logger } from '@/utils/logger';
/**
* EventPanel Props
*/
export interface EventPanelProps {
/** 事件类型 */
type: 'plan' | 'review';
/** 主题颜色 */
colorScheme: string;
/** 显示标签(如 "计划" 或 "复盘" */
label: string;
/** 外部触发打开模态框的计数器 */
openModalTrigger?: number;
}
/**
* EventPanel 组件
* 通用事件列表面板,显示投资计划或复盘
*/
export const EventPanel: React.FC<EventPanelProps> = ({
type,
colorScheme,
label,
openModalTrigger,
}) => {
const dispatch = useAppDispatch();
const toast = useToast();
// Redux 状态
const plans = useAppSelector(selectPlans);
const reviews = useAppSelector(selectReviews);
const loading = useAppSelector(selectPlanningLoading);
// 根据类型选择事件列表
const events = type === 'plan' ? plans : reviews;
// 弹窗状态
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
// 监听外部触发打开新建模态框(修复 bug只在值变化时触发
useEffect(() => {
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
// 只有当 trigger 值增加时才打开弹窗
handleOpenModal(null);
}
prevTriggerRef.current = openModalTrigger || 0;
}, [openModalTrigger]);
// 打开编辑/新建模态框
const handleOpenModal = (item: InvestmentEvent | null = null): void => {
if (item) {
setEditingItem(item);
setModalMode('edit');
} else {
setEditingItem(null);
setModalMode('create');
}
setIsModalOpen(true);
};
// 关闭弹窗
const handleCloseModal = (): void => {
setIsModalOpen(false);
setEditingItem(null);
};
// 删除数据 - 乐观更新模式
const handleDelete = async (id: number): Promise<void> => {
if (!window.confirm('确定要删除吗?')) return;
// ① 立即从 UI 移除
dispatch(removeEvent(id));
// ② 后台发送 API 请求
try {
const base = getApiBase();
const response = await fetch(`${base}/api/account/investment-plans/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (response.ok) {
logger.info('EventPanel', `删除${label}成功`, { itemId: id });
toast({
title: '删除成功',
status: 'success',
duration: 2000,
});
} else {
throw new Error('删除失败');
}
} catch (error) {
// ③ 失败回滚 - 重新加载数据
dispatch(fetchAllEvents());
logger.error('EventPanel', 'handleDelete rollback', error, { itemId: id });
toast({
title: '删除失败,请重试',
status: 'error',
duration: 3000,
});
}
};
// 刷新数据
const handleRefresh = useCallback(() => {
dispatch(fetchAllEvents());
}, [dispatch]);
// 使用 useCallback 优化回调函数
const handleEdit = useCallback((item: InvestmentEvent) => {
handleOpenModal(item);
}, []);
// 颜色主题
const secondaryText = 'rgba(255, 255, 255, 0.6)';
return (
<Box>
<VStack align="stretch" spacing={4}>
{loading ? (
<Center py={8}>
<Spinner size="xl" color={`${colorScheme}.500`} />
</Center>
) : events.length === 0 ? (
<Center py={{ base: 6, md: 8 }}>
<VStack spacing={{ base: 2, md: 3 }}>
<Icon as={FiFileText} boxSize={{ base: 8, md: 12 }} color="gray.300" />
<Text color={secondaryText} fontSize={{ base: 'sm', md: 'md' }}>{label}</Text>
</VStack>
</Center>
) : (
<Grid templateColumns={{ base: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} gap={{ base: 3, md: 4 }}>
{events.map(event => (
<FUIEventCard
key={event.id}
event={event}
colorScheme={colorScheme}
label={label}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</Grid>
)}
</VStack>
{/* 使用通用弹窗组件 */}
<EventFormModal
isOpen={isModalOpen}
onClose={handleCloseModal}
mode={modalMode}
eventType={type}
editingEvent={editingItem}
onSuccess={handleRefresh}
label={label}
apiEndpoint="investment-plans"
/>
</Box>
);
};
export default EventPanel;