/** * EventCard - 统一的投资事件卡片组件 * * 通过 variant 属性控制两种显示模式: * - list: 列表模式(EventPanel 中使用,带编辑/删除按钮) * - detail: 详情模式(日历弹窗中使用,显示类型徽章) * * 两种模式都支持: * - 标题显示 * - 描述内容展开/收起 * - 股票标签显示 */ import React, { useState, useEffect, useRef, memo } from 'react'; import { Box, Badge, IconButton, Flex, Card, CardBody, VStack, HStack, Text, Icon, Tag, TagLabel, TagLeftIcon, Button, useColorModeValue, } from '@chakra-ui/react'; import { FiEdit2, FiTrash2, FiFileText, FiCalendar, FiTrendingUp, FiChevronDown, FiChevronUp, } from 'react-icons/fi'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import type { InvestmentEvent, EventType, EventSource } from '@/types'; dayjs.locale('zh-cn'); /** * 卡片变体类型 */ export type EventCardVariant = 'list' | 'detail'; /** * 类型信息接口 */ interface TypeInfo { color: string; text: string; } /** * 获取类型信息 */ const getTypeInfo = (type?: EventType, source?: EventSource): TypeInfo => { if (source === 'future') { return { color: 'blue', text: '系统事件' }; } if (type === 'plan') { return { color: 'purple', text: '我的计划' }; } if (type === 'review') { return { color: 'green', text: '我的复盘' }; } return { color: 'gray', text: '未知类型' }; }; /** * EventCard Props */ export interface EventCardProps { /** 事件数据 */ event: InvestmentEvent; /** 卡片变体: list(列表模式) | detail(详情模式) */ variant?: EventCardVariant; /** 主题颜色(list 模式) */ colorScheme?: string; /** 显示标签(用于 aria-label) */ label?: string; /** 主要文本颜色 */ textColor?: string; /** 次要文本颜色 */ secondaryText?: string; /** 卡片背景色(list 模式) */ cardBg?: string; /** 边框颜色(detail 模式) */ borderColor?: string; /** 编辑回调(list 模式) */ onEdit?: (event: InvestmentEvent) => void; /** 删除回调(list 模式) */ onDelete?: (id: number) => void; } /** 描述最大显示行数 */ const MAX_LINES = 3; /** * EventCard 组件 */ export const EventCard = memo(({ event, variant = 'list', colorScheme = 'purple', label = '事件', textColor, secondaryText, cardBg, borderColor, onEdit, onDelete, }) => { // 展开/收起状态 const [isExpanded, setIsExpanded] = useState(false); const [isOverflow, setIsOverflow] = useState(false); const descriptionRef = useRef(null); // 默认颜色值(使用 hooks) const defaultTextColor = useColorModeValue('gray.700', 'white'); const defaultSecondaryText = useColorModeValue('gray.600', 'gray.400'); const defaultCardBg = useColorModeValue('gray.50', 'gray.700'); const defaultBorderColor = useColorModeValue('gray.200', 'gray.600'); // 使用传入的值或默认值 const finalTextColor = textColor || defaultTextColor; const finalSecondaryText = secondaryText || defaultSecondaryText; const finalCardBg = cardBg || defaultCardBg; const finalBorderColor = borderColor || defaultBorderColor; // 获取描述内容 const description = event.description || event.content || ''; // 检测描述是否溢出 useEffect(() => { const el = descriptionRef.current; if (el && description) { const lineHeight = parseInt(getComputedStyle(el).lineHeight) || 20; const maxHeight = lineHeight * MAX_LINES; setIsOverflow(el.scrollHeight > maxHeight + 5); } else { setIsOverflow(false); } }, [description]); // 获取类型信息 const typeInfo = getTypeInfo(event.type, event.source); // 是否为 list 模式 const isListMode = variant === 'list'; // 渲染容器 const renderContainer = (children: React.ReactNode) => { if (isListMode) { return ( {children} ); } return ( {children} ); }; return renderContainer( {/* 头部区域:标题 + 徽章 + 操作按钮 */} {/* 标题行 */} {event.title} {/* detail 模式显示类型徽章 */} {!isListMode && ( {typeInfo.text} )} {/* list 模式显示日期 */} {isListMode && ( {dayjs(event.event_date || event.date).format('YYYY年MM月DD日')} )} {/* list 模式显示编辑/删除按钮 */} {isListMode && (onEdit || onDelete) && ( {onEdit && ( } size="sm" variant="ghost" onClick={() => onEdit(event)} aria-label={`编辑${label}`} /> )} {onDelete && ( } size="sm" variant="ghost" colorScheme="red" onClick={() => onDelete(event.id)} aria-label={`删除${label}`} /> )} )} {/* 描述内容(可展开/收起) */} {description && ( {description} {isOverflow && ( )} )} {/* 股票标签 */} {event.stocks && event.stocks.length > 0 && ( 相关股票: {event.stocks.map((stock, idx) => { // 兼容两种格式:对象 {code, name} 或字符串 const stockCode = typeof stock === 'string' ? stock : stock.code; const stockName = typeof stock === 'string' ? stock : stock.name; const displayText = typeof stock === 'string' ? stock : `${stock.name}(${stock.code})`; return ( {displayText} ); })} )} ); }); EventCard.displayName = 'EventCard'; export default EventCard;