返回 AiToEarn
index.tsx
1 /**
2 * FavoriteButton - 收藏按钮组件
3 * 功能:支持收藏/取消收藏操作,带 loading 状态和红心动画
4 */
5
6 'use client'
7
8 import { Heart, Loader2 } from 'lucide-react'
9 import { useTransClient } from '@/app/i18n/client'
10 import { Button } from '@/components/ui/button'
11 import { cn } from '@/utils/className'
12
13 export interface IFavoriteButtonProps {
14 /** 是否已收藏 */
15 isFavorited: boolean
16 /** 是否加载中 */
17 isLoading?: boolean
18 /** 点击回调 */
19 onClick?: (e: React.MouseEvent) => void
20 /** 是否只显示图标(不显示文字) */
21 iconOnly?: boolean
22 /** 按钮变体 */
23 variant?: 'default' | 'ghost' | 'outline'
24 /** 自定义类名 */
25 className?: string
26 /** 图标大小 */
27 iconSize?: 'sm' | 'md' | 'lg'
28 }
29
30 /**
31 * FavoriteButton - 可复用的收藏按钮
32 */
33 export function FavoriteButton({
34 isFavorited,
35 isLoading = false,
36 onClick,
37 iconOnly = false,
38 variant = 'ghost',
39 className,
40 iconSize = 'md',
41 }: IFavoriteButtonProps) {
42 const { t } = useTransClient('chat')
43
44 const iconSizeClass = {
45 sm: 'w-3 h-3',
46 md: 'w-4 h-4',
47 lg: 'w-5 h-5',
48 }[iconSize]
49
50 const handleClick = (e: React.MouseEvent) => {
51 e.stopPropagation()
52 e.preventDefault()
53 if (isLoading)
54 return
55 onClick?.(e)
56 }
57
58 return (
59 <Button
60 variant={variant}
61 size={iconOnly ? 'icon' : 'sm'}
62 onClick={handleClick}
63 disabled={isLoading}
64 className={cn(
65 'cursor-pointer transition-colors',
66 iconOnly ? 'h-8 w-8' : 'h-8 px-2 gap-1',
67 className,
68 )}
69 aria-label={isFavorited ? t('task.unfavorite') : t('task.favorite')}
70 >
71 {isLoading ? (
72 <Loader2 className={cn(iconSizeClass, 'animate-spin')} />
73 ) : (
74 <Heart
75 className={cn(
76 iconSizeClass,
77 'transition-colors',
78 isFavorited && 'text-red-500 fill-red-500',
79 )}
80 />
81 )}
82 {!iconOnly && (
83 <span className="text-xs">{isFavorited ? t('task.unfavorite') : t('task.favorite')}</span>
84 )}
85 </Button>
86 )
87 }
88
89 export default FavoriteButton
90
90 lines Plain Text