- 新增 planningSlice 管理计划/复盘数据 - InvestmentPlanningCenter 改用 Redux 而非本地 state - 列表和日历视图共享同一数据源,保持同步 - 优化 Mock handlers,改进事件 ID 生成和调试日志 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
296 lines
9.4 KiB
TypeScript
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;
|