feat(Center): 新增 FUIEventCard 毛玻璃风格事件卡片
- 融合 ReviewCard 的 UI 风格(毛玻璃 + 金色主题) - 支持编辑、删除、展开描述等功能 - 使用 FUI_THEME 常量统一管理主题色 - 用于复盘列表的高级视觉呈现 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
255
src/views/Center/components/FUIEventCard.tsx
Normal file
255
src/views/Center/components/FUIEventCard.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* FUIEventCard - 毛玻璃风格投资事件卡片组件
|
||||
*
|
||||
* 融合 ReviewCard 的 UI 风格(毛玻璃 + 金色主题)
|
||||
* 与 EventCard 的功能(编辑、删除、展开描述)
|
||||
*
|
||||
* 用于复盘列表的高级视觉呈现
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagLeftIcon,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FiEdit2,
|
||||
FiTrash2,
|
||||
FiCalendar,
|
||||
FiTrendingUp,
|
||||
FiChevronDown,
|
||||
FiChevronUp,
|
||||
} from 'react-icons/fi';
|
||||
import { FileText, Heart, Target } from 'lucide-react';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
import type { InvestmentEvent } from '@/types';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
// 主题颜色常量(与 ReviewCard 保持一致)
|
||||
const FUI_THEME = {
|
||||
bg: 'rgba(26, 26, 46, 0.7)',
|
||||
border: 'rgba(212, 175, 55, 0.15)',
|
||||
borderHover: 'rgba(212, 175, 55, 0.3)',
|
||||
text: {
|
||||
primary: 'rgba(255, 255, 255, 0.95)',
|
||||
secondary: 'rgba(255, 255, 255, 0.6)',
|
||||
muted: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
accent: '#D4AF37', // 金色
|
||||
icon: '#F59E0B', // 橙色图标
|
||||
};
|
||||
|
||||
/**
|
||||
* FUIEventCard Props
|
||||
*/
|
||||
export interface FUIEventCardProps {
|
||||
/** 事件数据 */
|
||||
event: InvestmentEvent;
|
||||
/** 主题颜色 */
|
||||
colorScheme?: string;
|
||||
/** 显示标签(用于 aria-label) */
|
||||
label?: string;
|
||||
/** 编辑回调 */
|
||||
onEdit?: (event: InvestmentEvent) => void;
|
||||
/** 删除回调 */
|
||||
onDelete?: (id: number) => void;
|
||||
}
|
||||
|
||||
/** 描述最大显示行数 */
|
||||
const MAX_LINES = 3;
|
||||
|
||||
/**
|
||||
* FUIEventCard 组件
|
||||
*/
|
||||
export const FUIEventCard = memo<FUIEventCardProps>(({
|
||||
event,
|
||||
colorScheme = 'orange',
|
||||
label = '复盘',
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
// 展开/收起状态
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isOverflow, setIsOverflow] = useState(false);
|
||||
const descriptionRef = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
// 获取描述内容
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={FUI_THEME.bg}
|
||||
borderRadius="lg"
|
||||
p={4}
|
||||
border="1px solid"
|
||||
borderColor={FUI_THEME.border}
|
||||
backdropFilter="blur(8px)"
|
||||
transition="all 0.3s ease"
|
||||
_hover={{
|
||||
borderColor: FUI_THEME.borderHover,
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(212, 175, 55, 0.15)',
|
||||
}}
|
||||
>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* 头部区域:图标 + 标题 + 操作按钮 */}
|
||||
<Flex justify="space-between" align="start" gap={2}>
|
||||
<HStack spacing={2} flex={1}>
|
||||
<Box
|
||||
as={FileText}
|
||||
boxSize={4}
|
||||
color={FUI_THEME.icon}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={FUI_THEME.text.primary}
|
||||
noOfLines={1}
|
||||
>
|
||||
[{event.title}]
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 编辑/删除按钮 */}
|
||||
{(onEdit || onDelete) && (
|
||||
<HStack spacing={0}>
|
||||
{onEdit && (
|
||||
<IconButton
|
||||
icon={<FiEdit2 size={14} />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color={FUI_THEME.text.secondary}
|
||||
_hover={{ color: FUI_THEME.accent, bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||
onClick={() => onEdit(event)}
|
||||
aria-label={`编辑${label}`}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconButton
|
||||
icon={<FiTrash2 size={14} />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color={FUI_THEME.text.secondary}
|
||||
_hover={{ color: '#EF4444', bg: 'rgba(239, 68, 68, 0.1)' }}
|
||||
onClick={() => onDelete(event.id)}
|
||||
aria-label={`删除${label}`}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* 日期行 */}
|
||||
<HStack spacing={2}>
|
||||
<FiCalendar size={12} color={FUI_THEME.text.muted} />
|
||||
<Text fontSize="xs" color={FUI_THEME.text.secondary}>
|
||||
{dayjs(event.event_date || event.date).format('YYYY年MM月DD日')}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 描述内容(可展开/收起) */}
|
||||
{description && (
|
||||
<Box>
|
||||
<HStack spacing={1} align="start">
|
||||
<Text color={FUI_THEME.text.muted} fontSize="xs">•</Text>
|
||||
{event.type === 'plan' ? (
|
||||
<>
|
||||
<Box as={Target} boxSize={3} color={FUI_THEME.accent} flexShrink={0} mt="2px" />
|
||||
<Text color={FUI_THEME.text.secondary} fontSize="xs" flexShrink={0}>
|
||||
计划内容:
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box as={Heart} boxSize={3} color="#EF4444" flexShrink={0} mt="2px" />
|
||||
<Text color={FUI_THEME.text.secondary} fontSize="xs" flexShrink={0}>
|
||||
心得:
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
<Text
|
||||
ref={descriptionRef}
|
||||
fontSize="xs"
|
||||
color={FUI_THEME.text.primary}
|
||||
noOfLines={isExpanded ? undefined : MAX_LINES}
|
||||
whiteSpace="pre-wrap"
|
||||
mt={1}
|
||||
pl={5}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
{isOverflow && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color={FUI_THEME.accent}
|
||||
mt={1}
|
||||
ml={4}
|
||||
_hover={{ bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||
rightIcon={isExpanded ? <FiChevronUp /> : <FiChevronDown />}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? '收起' : '展开'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 股票标签 */}
|
||||
{event.stocks && event.stocks.length > 0 && (
|
||||
<HStack spacing={2} flexWrap="wrap" gap={1}>
|
||||
<Text fontSize="xs" color={FUI_THEME.text.secondary}>
|
||||
相关股票:
|
||||
</Text>
|
||||
{event.stocks.map((stock, idx) => {
|
||||
const stockCode = typeof stock === 'string' ? stock : stock.code;
|
||||
const displayText = typeof stock === 'string' ? stock : `${stock.name}(${stock.code})`;
|
||||
return (
|
||||
<Tag
|
||||
key={stockCode || idx}
|
||||
size="sm"
|
||||
bg="rgba(212, 175, 55, 0.1)"
|
||||
color={FUI_THEME.accent}
|
||||
border="1px solid"
|
||||
borderColor="rgba(212, 175, 55, 0.2)"
|
||||
>
|
||||
<TagLeftIcon as={FiTrendingUp} boxSize={3} />
|
||||
<TagLabel fontSize="xs">{displayText}</TagLabel>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
FUIEventCard.displayName = 'FUIEventCard';
|
||||
|
||||
export default FUIEventCard;
|
||||
Reference in New Issue
Block a user