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