Files
vf_react/src/views/Center/components/InvestmentPlanningCenter.tsx
zdl 7912caf2c2 refactor(Center): 全面优化个人中心模块
- 目录重命名:Dashboard → Center(匹配路由 /home/center)
- 删除遗留代码:Default.js、InvestmentPlansAndReviews.js、InvestmentCalendarChakra.js(共 2596 行)
- 创建 src/types/center.ts 类型定义(15+ 接口)
- 性能优化:
  - 创建 useCenterColors Hook 封装 7 个 useColorModeValue
  - 创建 utils/formatters.ts 提取纯函数
  - 修复 loadRealtimeQuotes 的 useCallback 依赖项
  - InvestmentPlanningCenter 添加 useMemo 缓存
- TypeScript 迁移:Center.js → Center.tsx

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 18:59:09 +08:00

271 lines
8.5 KiB
TypeScript

/**
* InvestmentPlanningCenter - 投资规划中心主组件 (TypeScript 重构版)
*
* 性能优化:
* - 使用 React.lazy() 懒加载子面板,减少初始加载时间
* - 使用 TypeScript 提供类型安全
*
* 组件架构:
* - InvestmentPlanningCenter (主组件)
* - CalendarPanel (日历面板,懒加载)
* - EventPanel (通用事件面板,用于计划和复盘)
* - PlanningContext (数据共享层)
*/
import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react';
import {
Box,
Card,
CardHeader,
CardBody,
Heading,
HStack,
Flex,
Icon,
useColorModeValue,
useToast,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Spinner,
Center,
Button,
ButtonGroup,
} from '@chakra-ui/react';
import {
FiCalendar,
FiTarget,
FiFileText,
FiList,
FiPlus,
} from 'react-icons/fi';
import { PlanningDataProvider } from './PlanningContext';
import type { InvestmentEvent, PlanningContextValue } from '@/types';
import { logger } from '@/utils/logger';
import { getApiBase } from '@/utils/apiConfig';
import './InvestmentCalendar.less';
// 懒加载子面板组件(实现代码分割)
const CalendarPanel = lazy(() =>
import('./CalendarPanel').then(module => ({ default: module.CalendarPanel }))
);
const EventPanel = lazy(() =>
import('./EventPanel').then(module => ({ default: module.EventPanel }))
);
/**
* 面板加载占位符
*/
const PanelLoadingFallback: React.FC = () => (
<Center py={12}>
<Spinner size="xl" color="purple.500" thickness="4px" />
</Center>
);
/**
* InvestmentPlanningCenter 主组件
*/
const InvestmentPlanningCenter: React.FC = () => {
const toast = useToast();
// 颜色主题
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);
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]);
// 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染)
const contextValue: PlanningContextValue = useMemo(
() => ({
allEvents,
setAllEvents,
loadAllData,
loading,
setLoading,
openPlanModalTrigger,
openReviewModalTrigger,
toast,
borderColor,
textColor,
secondaryText,
cardBg,
setViewMode,
setListTab,
}),
[
allEvents,
loadAllData,
loading,
openPlanModalTrigger,
openReviewModalTrigger,
toast,
borderColor,
textColor,
secondaryText,
cardBg,
]
);
// 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组)
const { planCount, reviewCount } = useMemo(
() => ({
planCount: allEvents.filter(e => e.type === 'plan').length,
reviewCount: allEvents.filter(e => e.type === 'review').length,
}),
[allEvents]
);
return (
<PlanningDataProvider value={contextValue}>
<Card bg={bgColor} shadow="md">
<CardHeader pb={{ base: 2, md: 4 }} px={{ base: 3, md: 5 }}>
<Flex justify="space-between" align="center" wrap="wrap" gap={2}>
<HStack spacing={{ base: 1, md: 2 }}>
<Icon as={FiTarget} color="purple.500" boxSize={{ base: 4, md: 5 }} />
<Heading size={{ base: 'sm', md: 'md' }}></Heading>
</HStack>
{/* 视图切换按钮组 - H5隐藏 */}
<ButtonGroup size="sm" isAttached variant="outline" display={{ base: 'none', md: 'flex' }}>
<Button
leftIcon={<Icon as={FiList} boxSize={4} />}
colorScheme={viewMode === 'list' ? 'purple' : 'gray'}
variant={viewMode === 'list' ? 'solid' : 'outline'}
onClick={() => setViewMode('list')}
>
</Button>
<Button
leftIcon={<Icon as={FiCalendar} boxSize={4} />}
colorScheme={viewMode === 'calendar' ? 'purple' : 'gray'}
variant={viewMode === 'calendar' ? 'solid' : 'outline'}
onClick={() => setViewMode('calendar')}
>
</Button>
</ButtonGroup>
</Flex>
</CardHeader>
<CardBody pt={0} px={{ base: 3, md: 5 }}>
{viewMode === 'calendar' ? (
/* 日历视图 */
<Suspense fallback={<PanelLoadingFallback />}>
<CalendarPanel />
</Suspense>
) : (
/* 列表视图:我的计划 / 我的复盘 切换 */
<Tabs
index={listTab}
onChange={setListTab}
variant="enclosed"
colorScheme="purple"
size={{ base: 'sm', md: 'md' }}
>
<Flex justify="space-between" align="center" mb={{ base: 2, md: 4 }} flexWrap="nowrap" gap={1}>
<TabList mb={0} borderBottom="none" flex="1" minW={0}>
<Tab fontSize={{ base: '11px', md: 'sm' }} px={{ base: 1, md: 4 }} whiteSpace="nowrap">
<Icon as={FiTarget} mr={1} boxSize={{ base: 3, md: 4 }} />
({planCount})
</Tab>
<Tab fontSize={{ base: '11px', md: 'sm' }} px={{ base: 1, md: 4 }} whiteSpace="nowrap">
<Icon as={FiFileText} mr={1} boxSize={{ base: 3, md: 4 }} />
({reviewCount})
</Tab>
</TabList>
<Button
size="xs"
colorScheme="purple"
leftIcon={<Icon as={FiPlus} boxSize={3} />}
fontSize={{ base: '11px', md: 'sm' }}
flexShrink={0}
onClick={() => {
if (listTab === 0) {
setOpenPlanModalTrigger(prev => prev + 1);
} else {
setOpenReviewModalTrigger(prev => prev + 1);
}
}}
>
{listTab === 0 ? '新建计划' : '新建复盘'}
</Button>
</Flex>
<TabPanels>
{/* 计划列表面板 */}
<TabPanel px={0}>
<Suspense fallback={<PanelLoadingFallback />}>
<EventPanel
type="plan"
colorScheme="purple"
label="计划"
openModalTrigger={openPlanModalTrigger}
/>
</Suspense>
</TabPanel>
{/* 复盘列表面板 */}
<TabPanel px={0}>
<Suspense fallback={<PanelLoadingFallback />}>
<EventPanel
type="review"
colorScheme="green"
label="复盘"
openModalTrigger={openReviewModalTrigger}
/>
</Suspense>
</TabPanel>
</TabPanels>
</Tabs>
)}
</CardBody>
</Card>
</PlanningDataProvider>
);
};
export default InvestmentPlanningCenter;