refactor(Planning): 投资规划中心重构为 Redux 状态管理

- 新增 planningSlice 管理计划/复盘数据
- InvestmentPlanningCenter 改用 Redux 而非本地 state
- 列表和日历视图共享同一数据源,保持同步
- 优化 Mock handlers,改进事件 ID 生成和调试日志

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-23 14:15:49 +08:00
parent dc617fb659
commit 43e5e8b6fa
9 changed files with 554 additions and 104 deletions

View File

@@ -1,5 +1,5 @@
/**
* EventPanel - 通用事件面板组件
* EventPanel - 通用事件面板组件 (Redux 版本)
* 用于显示、编辑和管理投资计划或复盘
*
* 通过 props 配置差异化行为:
@@ -17,15 +17,22 @@ import {
Spinner,
Center,
Icon,
useToast,
} from '@chakra-ui/react';
import { FiFileText } from 'react-icons/fi';
import { usePlanningData } from './PlanningContext';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
fetchAllEvents,
deleteEvent,
selectPlans,
selectReviews,
selectPlanningLoading,
} from '@/store/slices/planningSlice';
import { EventFormModal } from './EventFormModal';
import { FUIEventCard } from './FUIEventCard';
import type { InvestmentEvent } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
/**
* EventPanel Props
@@ -51,15 +58,16 @@ export const EventPanel: React.FC<EventPanelProps> = ({
label,
openModalTrigger,
}) => {
const {
allEvents,
loadAllData,
loading,
toast,
textColor,
secondaryText,
cardBg,
} = usePlanningData();
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);
@@ -69,9 +77,6 @@ export const EventPanel: React.FC<EventPanelProps> = ({
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
// 筛选事件列表(按类型过滤,排除系统事件)
const events = allEvents.filter(event => event.type === type && event.source !== 'future');
// 监听外部触发打开新建模态框(修复 bug只在值变化时触发
useEffect(() => {
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
@@ -99,27 +104,18 @@ export const EventPanel: React.FC<EventPanelProps> = ({
setEditingItem(null);
};
// 删除数据
// 删除数据 - 使用 Redux action
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',
await dispatch(deleteEvent(id)).unwrap();
logger.info('EventPanel', `删除${label}成功`, { itemId: id });
toast({
title: '删除成功',
status: 'success',
duration: 2000,
});
if (response.ok) {
logger.info('EventPanel', `删除${label}成功`, { itemId: id });
toast({
title: '删除成功',
status: 'success',
duration: 2000,
});
loadAllData();
}
} catch (error) {
logger.error('EventPanel', 'handleDelete', error, { itemId: id });
toast({
@@ -130,11 +126,19 @@ export const EventPanel: React.FC<EventPanelProps> = ({
}
};
// 刷新数据
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}>
@@ -172,7 +176,7 @@ export const EventPanel: React.FC<EventPanelProps> = ({
mode={modalMode}
eventType={type}
editingEvent={editingItem}
onSuccess={loadAllData}
onSuccess={handleRefresh}
label={label}
apiEndpoint="investment-plans"
/>