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,18 +1,16 @@
/**
* InvestmentPlanningCenter - 投资规划中心主组件 (TypeScript 重构版)
* InvestmentPlanningCenter - 投资规划中心主组件 (Redux 版本)
*
* 性能优化:
* - 使用 React.lazy() 懒加载子面板,减少初始加载时间
* - 使用 TypeScript 提供类型安全
* 使用 Redux 管理数据,确保列表和日历视图数据同步
*
* 组件架构:
* - InvestmentPlanningCenter (主组件)
* - CalendarPanel (日历面板,懒加载)
* - EventPanel (通用事件面板,用于计划和复盘)
* - PlanningContext (数据共享)
* - PlanningContext (UI 状态共享)
*/
import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react';
import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react';
import {
Box,
Heading,
@@ -30,7 +28,6 @@ import {
Center,
Button,
ButtonGroup,
Text,
} from '@chakra-ui/react';
import {
FiCalendar,
@@ -42,9 +39,15 @@ import { Target } from 'lucide-react';
import GlassCard from '@components/GlassCard';
import { PlanningDataProvider } from './PlanningContext';
import type { InvestmentEvent, PlanningContextValue } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
import type { PlanningContextValue } from '@/types';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
fetchAllEvents,
selectAllEvents,
selectPlanningLoading,
selectPlans,
selectReviews,
} from '@/store/slices/planningSlice';
import './InvestmentCalendar.less';
// 懒加载子面板组件(实现代码分割)
@@ -68,64 +71,45 @@ const PanelLoadingFallback: React.FC = () => (
* InvestmentPlanningCenter 主组件
*/
const InvestmentPlanningCenter: React.FC = () => {
const dispatch = useAppDispatch();
const toast = useToast();
// Redux 状态
const allEvents = useAppSelector(selectAllEvents);
const loading = useAppSelector(selectPlanningLoading);
const plans = useAppSelector(selectPlans);
const reviews = useAppSelector(selectReviews);
// 颜色主题
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const textColor = useColorModeValue('gray.700', 'white');
const secondaryText = useColorModeValue('gray.600', 'gray.400');
const cardBg = useColorModeValue('gray.50', 'gray.700');
// 全局数据状态
const [allEvents, setAllEvents] = useState<InvestmentEvent[]>([]);
const [loading, setLoading] = useState<boolean>(false);
// UI 状态
const [viewMode, setViewMode] = useState<'calendar' | 'list'>('list');
const [listTab, setListTab] = useState<number>(0); // 0: 我的计划, 1: 我的复盘
const [openPlanModalTrigger, setOpenPlanModalTrigger] = useState<number>(0);
const [openReviewModalTrigger, setOpenReviewModalTrigger] = useState<number>(0);
/**
* 加载所有事件数据(日历事件 + 计划 + 复盘)
*/
const loadAllData = useCallback(async (): Promise<void> => {
try {
setLoading(true);
const base = getApiBase();
const response = await fetch(base + '/api/account/calendar/events', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setAllEvents(data.data || []);
logger.debug('InvestmentPlanningCenter', '数据加载成功', {
count: data.data?.length || 0
});
}
}
} catch (error) {
logger.error('InvestmentPlanningCenter', 'loadAllData', error);
} finally {
setLoading(false);
}
}, []);
// 组件挂载时加载数据
useEffect(() => {
loadAllData();
}, [loadAllData]);
dispatch(fetchAllEvents());
}, [dispatch]);
// 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染
// 刷新数据的方法(供子组件调用
const loadAllData = async (): Promise<void> => {
await dispatch(fetchAllEvents());
};
// 提供给子组件的 Context 值
const contextValue: PlanningContextValue = useMemo(
() => ({
allEvents,
setAllEvents,
setAllEvents: () => {}, // Redux 管理,不需要 setter
loadAllData,
loading,
setLoading,
setLoading: () => {}, // Redux 管理,不需要 setter
openPlanModalTrigger,
openReviewModalTrigger,
toast,
@@ -138,7 +122,6 @@ const InvestmentPlanningCenter: React.FC = () => {
}),
[
allEvents,
loadAllData,
loading,
openPlanModalTrigger,
openReviewModalTrigger,
@@ -150,15 +133,6 @@ const InvestmentPlanningCenter: React.FC = () => {
]
);
// 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组)
const { planCount, reviewCount } = useMemo(
() => ({
planCount: allEvents.filter(e => e.type === 'plan').length,
reviewCount: allEvents.filter(e => e.type === 'review').length,
}),
[allEvents]
);
// 金色主题色
const goldAccent = 'rgba(212, 175, 55, 0.9)';
@@ -245,7 +219,7 @@ const InvestmentPlanningCenter: React.FC = () => {
}}
>
<Box as={Target} boxSize={{ base: 3, md: 4 }} mr={1} />
({planCount})
({plans.length})
</Tab>
<Tab
fontSize={{ base: '11px', md: 'sm' }}
@@ -260,7 +234,7 @@ const InvestmentPlanningCenter: React.FC = () => {
}}
>
<Icon as={FiFileText} mr={1} boxSize={{ base: 3, md: 4 }} />
({reviewCount})
({reviews.length})
</Tab>
</TabList>
<Button