返回 AiToEarn
ChannelItem.tsx
1 /**
2 * ChannelItem - 频道项组件
3 *
4 * 功能:
5 * - 显示频道信息(头像、名称、平台、粉丝数等)
6 * - 处理频道删除操作
7 * - 离线频道显示灰色样式和重新授权按钮
8 */
9
10 'use client'
11
12 import type { SocialAccount } from '@/api/accounts/account.types'
13 import { Loader2, RefreshCw, Trash2 } from 'lucide-react'
14
15 import { useState } from 'react'
16 import AccountStatusView from '@/app/[lng]/accounts/components/AccountsTopNav/components/AccountStatusView'
17 import { AccountStatus } from '@/app/config/accountConfig'
18 import { PlatType } from '@/app/config/platConfig'
19 import { useTransClient } from '@/app/i18n/client'
20 import { PlatformIcon } from '@/components/common/PlatformIcon'
21 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
22 import { Button } from '@/components/ui/button'
23 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
24 import { usePlatformInfo } from '@/hooks/usePlatformMetadata'
25 import { cn } from '@/utils/className'
26 import { getOssUrl } from '@/utils/oss'
27 import { shouldShowFansSyncNotice } from '@/utils/social/account'
28 import { useChannelManagerStore } from '../channelManagerStore'
29 import { isPlatformFansRefreshSupported } from '../utils/fansRefreshSupport'
30
31 interface ChannelItemProps {
32 channel: SocialAccount
33 onDelete: (channel: SocialAccount) => void
34 deleteLoading?: string | null
35 fansRefreshTarget?: 'all' | PlatType | null
36 onRefreshFans?: (platform: PlatType) => void
37 }
38
39 export function ChannelItem({
40 channel,
41 onDelete,
42 deleteLoading,
43 fansRefreshTarget,
44 onRefreshFans,
45 }: ChannelItemProps) {
46 const platInfo = usePlatformInfo(channel.type)
47 const isDeleting = deleteLoading === channel.id
48 const isOffline = channel.status !== AccountStatus.USABLE
49 const isFansRefreshing = fansRefreshTarget === 'all' || fansRefreshTarget === channel.type
50 const supportsFansRefresh = isPlatformFansRefreshSupported(channel.type, platInfo)
51 const showFansSyncNotice = channel.type === PlatType.Douyin && shouldShowFansSyncNotice(channel.type, channel.fansCount)
52 const showXhsFansAsyncNotice = channel.type === PlatType.Xhs && channel.fansCount === 0
53 const { t } = useTransClient('account')
54 const openAndAuth = useChannelManagerStore(state => state.openAndAuth)
55 const [refreshTooltipOpen, setRefreshTooltipOpen] = useState(false)
56
57 // 处理重新授权
58 const handleReauth = () => {
59 openAndAuth(channel.type, channel.groupId)
60 }
61
62 return (
63 <div
64 data-testid="cm-channel-item"
65 className={cn(
66 'group relative flex min-w-0 items-center gap-3 px-3 py-4 transition-colors hover:bg-muted/20 sm:px-5',
67 isDeleting && 'cursor-not-allowed opacity-50',
68 isOffline && 'opacity-70',
69 )}
70 >
71 <Avatar className={cn('h-12 w-12 shrink-0 border border-border/70 shadow-sm', isOffline && 'grayscale')}>
72 <AvatarImage src={getOssUrl(channel.avatar)} alt={channel.nickname} />
73 <AvatarFallback className="bg-muted text-sm font-semibold text-foreground">
74 {channel.nickname?.[0]}
75 </AvatarFallback>
76 </Avatar>
77
78 <div className="min-w-0 flex-1">
79 <div data-testid="cm-channel-name" className={cn('truncate text-base font-semibold leading-5 text-foreground', isOffline && 'text-muted-foreground')}>
80 {channel.nickname}
81 </div>
82 <div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5">
83 {platInfo?.icon && (
84 <span className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
85 <PlatformIcon
86 platform={channel.type}
87 width={16}
88 height={16}
89 className={cn('shrink-0 rounded-sm', isOffline && 'grayscale')}
90 />
91 <span className="shrink-0">{platInfo.name}</span>
92 </span>
93 )}
94 <span data-testid="cm-channel-status"><AccountStatusView account={channel} /></span>
95 {showFansSyncNotice ? (
96 <span className="shrink-0 text-xs text-muted-foreground">
97 {t('channelManager.fansSyncNotice')}
98 </span>
99 ) : showXhsFansAsyncNotice ? (
100 <span className="shrink-0 text-xs text-muted-foreground">
101 {t('channelManager.xhsFansAsyncNotice')}
102 </span>
103 ) : channel.fansCount !== undefined && channel.fansCount !== null && (
104 <span className="shrink-0 text-xs text-muted-foreground">
105 {t('channelManager.fans', { count: channel.fansCount })}
106 </span>
107 )}
108 </div>
109 </div>
110
111 {/* 离线状态显示重新授权按钮 */}
112 {isOffline && !isDeleting && (
113 <Button
114 data-testid="cm-channel-reauth-btn"
115 variant="outline"
116 size="sm"
117 className="h-9 shrink-0 cursor-pointer rounded-lg px-3 sm:px-4"
118 onClick={handleReauth}
119 >
120 <RefreshCw className="h-3 w-3 sm:mr-1" />
121 <span className="hidden sm:inline">{t('channelManager.reauth')}</span>
122 </Button>
123 )}
124
125 {!isOffline && !isDeleting && onRefreshFans && supportsFansRefresh && (
126 <TooltipProvider delayDuration={0}>
127 <Tooltip open={refreshTooltipOpen} onOpenChange={setRefreshTooltipOpen}>
128 <TooltipTrigger asChild>
129 <Button
130 data-testid="cm-channel-refresh-fans-btn"
131 type="button"
132 variant="outline"
133 size="sm"
134 aria-label={t('channelManager.refreshFansAction')}
135 disabled={!!fansRefreshTarget}
136 className="h-8 w-8 shrink-0 cursor-pointer rounded-lg border-primary/35 bg-background p-0 text-primary hover:border-primary/65 hover:bg-primary/5 hover:text-primary disabled:cursor-not-allowed"
137 onClick={() => {
138 setRefreshTooltipOpen(false)
139 onRefreshFans(channel.type)
140 }}
141 >
142 {isFansRefreshing ? (
143 <Loader2 className="h-4 w-4 animate-spin" />
144 ) : (
145 <RefreshCw className="h-4 w-4" />
146 )}
147 </Button>
148 </TooltipTrigger>
149 <TooltipContent side="top">
150 {t('channelManager.refreshFansAction')}
151 </TooltipContent>
152 </Tooltip>
153 </TooltipProvider>
154 )}
155
156 {isDeleting ? (
157 <div className="p-1">
158 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
159 </div>
160 ) : (
161 <Button
162 data-testid="cm-channel-delete-btn"
163 variant="ghost"
164 size="sm"
165 onClick={() => onDelete(channel)}
166 className="h-8 w-8 shrink-0 rounded-lg p-0 text-muted-foreground opacity-100 hover:bg-destructive/10 hover:text-destructive"
167 >
168 <Trash2 className="h-4 w-4" />
169 </Button>
170 )}
171 </div>
172 )
173 }
174
174 lines Plain Text