Files
vf_react/src/views/Center/components/InvestmentPlanningCenter.tsx
zdl 43e5e8b6fa 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>
2025-12-23 14:15:49 +08:00

296 lines
9.4 KiB
TypeScript

/**
* InvestmentPlanningCenter - 投资规划中心主组件 (Redux 版本)
*
* 使用 Redux 管理数据,确保列表和日历视图数据同步
*
* 组件架构:
* - InvestmentPlanningCenter (主组件)
* - CalendarPanel (日历面板,懒加载)
* - EventPanel (通用事件面板,用于计划和复盘)
* - PlanningContext (UI 状态共享)
*/
import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react';
import {
Box,
Heading,
HStack,
Flex,
Icon,
useColorModeValue,
useToast,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Spinner,
Center,
Button,
ButtonGroup,
} from '@chakra-ui/react';
import {
FiCalendar,
FiFileText,
FiList,
FiPlus,
} from 'react-icons/fi';
import { Target } from 'lucide-react';
import GlassCard from '@components/GlassCard';
import { PlanningDataProvider } from './PlanningContext';
import type { PlanningContextValue } from '@/types';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
fetchAllEvents,
selectAllEvents,
selectPlanningLoading,
selectPlans,
selectReviews,
} from '@/store/slices/planningSlice';
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 dispatch = useAppDispatch();
const toast = useToast();
// Redux 状态
const allEvents = useAppSelector(selectAllEvents);
const loading = useAppSelector(selectPlanningLoading);
const plans = useAppSelector(selectPlans);
const reviews = useAppSelector(selectReviews);
// 颜色主题
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');
// 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);
// 组件挂载时加载数据
useEffect(() => {
dispatch(fetchAllEvents());
}, [dispatch]);
// 刷新数据的方法(供子组件调用)
const loadAllData = async (): Promise<void> => {
await dispatch(fetchAllEvents());
};
// 提供给子组件的 Context 值
const contextValue: PlanningContextValue = useMemo(
() => ({
allEvents,
setAllEvents: () => {}, // Redux 管理,不需要 setter
loadAllData,
loading,
setLoading: () => {}, // Redux 管理,不需要 setter
openPlanModalTrigger,
openReviewModalTrigger,
toast,
borderColor,
textColor,
secondaryText,
cardBg,
setViewMode,
setListTab,
}),
[
allEvents,
loading,
openPlanModalTrigger,
openReviewModalTrigger,
toast,
borderColor,
textColor,
secondaryText,
cardBg,
]
);
// 金色主题色
const goldAccent = 'rgba(212, 175, 55, 0.9)';
return (
<PlanningDataProvider value={contextValue}>
<GlassCard variant="transparent" cornerDecor padding="lg">
{/* 标题区域 */}
<Flex justify="space-between" align="center" wrap="wrap" gap={2} mb={{ base: 3, md: 4 }}>
<HStack spacing={{ base: 2, md: 3 }}>
<Box
as={Target}
boxSize={{ base: 5, md: 6 }}
color={goldAccent}
/>
<Heading
size={{ base: 'sm', md: 'md' }}
bgGradient="linear(to-r, #D4AF37, #F5E6A3)"
bgClip="text"
>
</Heading>
</HStack>
{/* 视图切换按钮组 - H5隐藏 */}
<ButtonGroup size="sm" isAttached display={{ base: 'none', md: 'flex' }}>
<Button
leftIcon={<Icon as={FiList} boxSize={4} />}
bg={viewMode === 'list' ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
color={viewMode === 'list' ? goldAccent : 'rgba(255, 255, 255, 0.6)'}
border="1px solid"
borderColor={viewMode === 'list' ? 'rgba(212, 175, 55, 0.4)' : 'rgba(212, 175, 55, 0.2)'}
_hover={{ bg: 'rgba(212, 175, 55, 0.15)', color: goldAccent }}
onClick={() => setViewMode('list')}
>
</Button>
<Button
leftIcon={<Icon as={FiCalendar} boxSize={4} />}
bg={viewMode === 'calendar' ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
color={viewMode === 'calendar' ? goldAccent : 'rgba(255, 255, 255, 0.6)'}
border="1px solid"
borderColor={viewMode === 'calendar' ? 'rgba(212, 175, 55, 0.4)' : 'rgba(212, 175, 55, 0.2)'}
_hover={{ bg: 'rgba(212, 175, 55, 0.15)', color: goldAccent }}
onClick={() => setViewMode('calendar')}
>
</Button>
</ButtonGroup>
</Flex>
{/* 渐变分割线 */}
<Box
h="1px"
bgGradient="linear(to-r, transparent, rgba(212, 175, 55, 0.4), transparent)"
mb={{ base: 3, md: 4 }}
/>
{/* 内容区域 */}
<Box>
{viewMode === 'calendar' ? (
/* 日历视图 */
<Suspense fallback={<PanelLoadingFallback />}>
<CalendarPanel />
</Suspense>
) : (
/* 列表视图:我的计划 / 我的复盘 切换 */
<Tabs
index={listTab}
onChange={setListTab}
variant="unstyled"
size={{ base: 'sm', md: 'md' }}
>
<Flex justify="space-between" align="center" mb={{ base: 2, md: 4 }} flexWrap="nowrap" gap={1}>
<TabList mb={0} flex="1" minW={0}>
<Tab
fontSize={{ base: '11px', md: 'sm' }}
px={{ base: 2, md: 4 }}
py={2}
whiteSpace="nowrap"
color="rgba(255, 255, 255, 0.6)"
_selected={{
color: goldAccent,
borderBottom: '2px solid',
borderColor: goldAccent,
}}
>
<Box as={Target} boxSize={{ base: 3, md: 4 }} mr={1} />
({plans.length})
</Tab>
<Tab
fontSize={{ base: '11px', md: 'sm' }}
px={{ base: 2, md: 4 }}
py={2}
whiteSpace="nowrap"
color="rgba(255, 255, 255, 0.6)"
_selected={{
color: goldAccent,
borderBottom: '2px solid',
borderColor: goldAccent,
}}
>
<Icon as={FiFileText} mr={1} boxSize={{ base: 3, md: 4 }} />
({reviews.length})
</Tab>
</TabList>
<Button
size="xs"
bg="rgba(212, 175, 55, 0.2)"
color={goldAccent}
border="1px solid"
borderColor="rgba(212, 175, 55, 0.3)"
leftIcon={<Icon as={FiPlus} boxSize={3} />}
fontSize={{ base: '11px', md: 'sm' }}
flexShrink={0}
_hover={{ bg: 'rgba(212, 175, 55, 0.3)' }}
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="orange"
label="计划"
openModalTrigger={openPlanModalTrigger}
/>
</Suspense>
</TabPanel>
{/* 复盘列表面板 */}
<TabPanel px={0}>
<Suspense fallback={<PanelLoadingFallback />}>
<EventPanel
type="review"
colorScheme="orange"
label="复盘"
openModalTrigger={openReviewModalTrigger}
/>
</Suspense>
</TabPanel>
</TabPanels>
</Tabs>
)}
</Box>
</GlassCard>
</PlanningDataProvider>
);
};
export default InvestmentPlanningCenter;