- 目录重命名: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>
188 lines
5.0 KiB
TypeScript
188 lines
5.0 KiB
TypeScript
/**
|
||
* EventPanel - 通用事件面板组件
|
||
* 用于显示、编辑和管理投资计划或复盘
|
||
*
|
||
* 通过 props 配置差异化行为:
|
||
* - type: 'plan' | 'review'
|
||
* - colorScheme: 主题色
|
||
* - label: 显示文案
|
||
*/
|
||
|
||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||
import {
|
||
Box,
|
||
Grid,
|
||
VStack,
|
||
Text,
|
||
Spinner,
|
||
Center,
|
||
Icon,
|
||
} from '@chakra-ui/react';
|
||
import { FiFileText } from 'react-icons/fi';
|
||
|
||
import { usePlanningData } from './PlanningContext';
|
||
import { EventFormModal } from './EventFormModal';
|
||
import { EventCard } from './EventCard';
|
||
import type { InvestmentEvent } from '@/types';
|
||
import { logger } from '@/utils/logger';
|
||
import { getApiBase } from '@/utils/apiConfig';
|
||
|
||
/**
|
||
* EventPanel Props
|
||
*/
|
||
export interface EventPanelProps {
|
||
/** 事件类型 */
|
||
type: 'plan' | 'review';
|
||
/** 主题颜色 */
|
||
colorScheme: string;
|
||
/** 显示标签(如 "计划" 或 "复盘") */
|
||
label: string;
|
||
/** 外部触发打开模态框的计数器 */
|
||
openModalTrigger?: number;
|
||
}
|
||
|
||
/**
|
||
* EventPanel 组件
|
||
* 通用事件列表面板,显示投资计划或复盘
|
||
*/
|
||
export const EventPanel: React.FC<EventPanelProps> = ({
|
||
type,
|
||
colorScheme,
|
||
label,
|
||
openModalTrigger,
|
||
}) => {
|
||
const {
|
||
allEvents,
|
||
loadAllData,
|
||
loading,
|
||
toast,
|
||
textColor,
|
||
secondaryText,
|
||
cardBg,
|
||
} = usePlanningData();
|
||
|
||
// 弹窗状态
|
||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||
const [editingItem, setEditingItem] = useState<InvestmentEvent | null>(null);
|
||
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
|
||
|
||
// 使用 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) {
|
||
// 只有当 trigger 值增加时才打开弹窗
|
||
handleOpenModal(null);
|
||
}
|
||
prevTriggerRef.current = openModalTrigger || 0;
|
||
}, [openModalTrigger]);
|
||
|
||
// 打开编辑/新建模态框
|
||
const handleOpenModal = (item: InvestmentEvent | null = null): void => {
|
||
if (item) {
|
||
setEditingItem(item);
|
||
setModalMode('edit');
|
||
} else {
|
||
setEditingItem(null);
|
||
setModalMode('create');
|
||
}
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
// 关闭弹窗
|
||
const handleCloseModal = (): void => {
|
||
setIsModalOpen(false);
|
||
setEditingItem(null);
|
||
};
|
||
|
||
// 删除数据
|
||
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',
|
||
});
|
||
|
||
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({
|
||
title: '删除失败',
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
}
|
||
};
|
||
|
||
// 使用 useCallback 优化回调函数
|
||
const handleEdit = useCallback((item: InvestmentEvent) => {
|
||
handleOpenModal(item);
|
||
}, []);
|
||
|
||
return (
|
||
<Box>
|
||
<VStack align="stretch" spacing={4}>
|
||
{loading ? (
|
||
<Center py={8}>
|
||
<Spinner size="xl" color={`${colorScheme}.500`} />
|
||
</Center>
|
||
) : events.length === 0 ? (
|
||
<Center py={{ base: 6, md: 8 }}>
|
||
<VStack spacing={{ base: 2, md: 3 }}>
|
||
<Icon as={FiFileText} boxSize={{ base: 8, md: 12 }} color="gray.300" />
|
||
<Text color={secondaryText} fontSize={{ base: 'sm', md: 'md' }}>暂无投资{label}</Text>
|
||
</VStack>
|
||
</Center>
|
||
) : (
|
||
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={{ base: 3, md: 4 }}>
|
||
{events.map(event => (
|
||
<EventCard
|
||
key={event.id}
|
||
event={event}
|
||
variant="list"
|
||
colorScheme={colorScheme}
|
||
label={label}
|
||
textColor={textColor}
|
||
secondaryText={secondaryText}
|
||
cardBg={cardBg}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
/>
|
||
))}
|
||
</Grid>
|
||
)}
|
||
</VStack>
|
||
|
||
{/* 使用通用弹窗组件 */}
|
||
<EventFormModal
|
||
isOpen={isModalOpen}
|
||
onClose={handleCloseModal}
|
||
mode={modalMode}
|
||
eventType={type}
|
||
editingEvent={editingItem}
|
||
onSuccess={loadAllData}
|
||
label={label}
|
||
apiEndpoint="investment-plans"
|
||
/>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default EventPanel;
|