diff --git a/src/views/Dashboard/components/EventCard.tsx b/src/views/Dashboard/components/EventCard.tsx new file mode 100644 index 00000000..e17cb40a --- /dev/null +++ b/src/views/Dashboard/components/EventCard.tsx @@ -0,0 +1,296 @@ +/** + * 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; diff --git a/src/views/Dashboard/components/EventDetailCard.tsx b/src/views/Dashboard/components/EventDetailCard.tsx deleted file mode 100644 index 823d1bc7..00000000 --- a/src/views/Dashboard/components/EventDetailCard.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/** - * EventDetailCard - 事件详情卡片组件 - * 用于日历视图中展示单个事件的详细信息 - */ - -import React, { useState, useRef, useEffect } from 'react'; -import { - Box, - Badge, - Flex, - HStack, - Text, - Tag, - TagLabel, - TagLeftIcon, - Button, - useColorModeValue, -} from '@chakra-ui/react'; -import { - FiTrendingUp, - FiChevronDown, - FiChevronUp, -} from 'react-icons/fi'; -import type { InvestmentEvent } from '@/types'; - -/** - * EventDetailCard Props - */ -export interface EventDetailCardProps { - /** 事件数据 */ - event: InvestmentEvent; - /** 边框颜色 */ - borderColor?: string; - /** 次要文字颜色 */ - secondaryText?: string; -} - -/** - * 最大显示行数 - */ -const MAX_LINES = 3; - -/** - * EventDetailCard 组件 - */ -export const EventDetailCard: React.FC = ({ - event, - borderColor: borderColorProp, - secondaryText: secondaryTextProp, -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const [isOverflow, setIsOverflow] = useState(false); - const descriptionRef = useRef(null); - - // 默认颜色 - const defaultBorderColor = useColorModeValue('gray.200', 'gray.600'); - const defaultSecondaryText = useColorModeValue('gray.600', 'gray.400'); - - const borderColor = borderColorProp || defaultBorderColor; - const secondaryText = secondaryTextProp || defaultSecondaryText; - - // 检测内容是否溢出 - useEffect(() => { - const el = descriptionRef.current; - if (el) { - // 计算行高和最大高度 - const lineHeight = parseInt(getComputedStyle(el).lineHeight) || 20; - const maxHeight = lineHeight * MAX_LINES; - setIsOverflow(el.scrollHeight > maxHeight + 5); // 5px 容差 - } - }, [event.description]); - - // 获取事件类型标签 - const getEventBadge = () => { - if (event.source === 'future') { - return 系统事件; - } else if (event.type === 'plan') { - return 我的计划; - } else if (event.type === 'review') { - return 我的复盘; - } - return null; - }; - - return ( - - {/* 标题和标签 */} - - - - {event.title} - - {getEventBadge()} - - - - {/* 描述内容 - 支持展开/收起 */} - {event.description && ( - - - {event.description} - - {isOverflow && ( - - )} - - )} - - {/* 相关股票 */} - {event.stocks && event.stocks.length > 0 && ( - - 相关股票: - {event.stocks.map((stock, i) => ( - - - {stock} - - ))} - - )} - - ); -}; - -export default EventDetailCard; diff --git a/src/views/Dashboard/components/EventDetailModal.tsx b/src/views/Dashboard/components/EventDetailModal.tsx index a9c6b0a0..398ee0f8 100644 --- a/src/views/Dashboard/components/EventDetailModal.tsx +++ b/src/views/Dashboard/components/EventDetailModal.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Modal, Space } from 'antd'; import type { Dayjs } from 'dayjs'; -import { EventDetailCard } from './EventDetailCard'; +import { EventCard } from './EventCard'; import { EventEmptyState } from './EventEmptyState'; import type { InvestmentEvent } from '@/types'; @@ -82,9 +82,10 @@ export const EventDetailModal: React.FC = ({ ) : ( {events.map((event, idx) => ( -