返回 AiToEarn
ConnectChannelList.tsx
根目录 / project / aitoearn-web / src / components / ChannelManager / components / ConnectChannelList.tsx
1 /**
2 * ConnectChannelList - 连接频道列表页
3 * 显示所有可连接的平台,点击进入授权流程
4 */
5
6 'use client'
7
8 import { ArrowLeft, Sparkles } from 'lucide-react'
9 import { toast } from 'sonner'
10 import { useShallow } from 'zustand/react/shallow'
11 import { PlatType } from '@/app/config/platConfig'
12 import { useTransClient } from '@/app/i18n/client'
13 import { PlatformIcon } from '@/components/common/PlatformIcon'
14 import { Button } from '@/components/ui/button'
15 import { ScrollArea } from '@/components/ui/scroll-area'
16 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
17 import { isChina } from '@/constant'
18 import { useIsMobile } from '@/hooks/useIsMobile'
19 import { useRegionSortedPlatforms } from '@/hooks/usePlatformMetadata'
20 import { useAccountStore } from '@/store/account'
21 import { isPlatformComingSoon, isPlatformRegionLimited } from '@/store/platformMetadata/utils'
22 import { useUserStore } from '@/store/user'
23 import { navigateToLogin } from '@/utils/auth'
24 import { confirmPlatformRegionRedirect } from '@/utils/region/redirect'
25 import { confirm } from '@/utils/ui/confirm'
26 import { useChannelManagerStore } from '../channelManagerStore'
27
28 export function ConnectChannelList() {
29 const { t } = useTransClient('account')
30 const isMobile = useIsMobile()
31 const platforms = useRegionSortedPlatforms()
32
33 const { isNewUser, setCurrentView, startAuth, targetSpaceId, setTargetSpaceId, closeModal }
34 = useChannelManagerStore(
35 useShallow(state => ({
36 isNewUser: state.isNewUser,
37 setCurrentView: state.setCurrentView,
38 startAuth: state.startAuth,
39 targetSpaceId: state.targetSpaceId,
40 setTargetSpaceId: state.setTargetSpaceId,
41 closeModal: state.closeModal,
42 })),
43 )
44
45 const { accountGroupList } = useAccountStore(
46 useShallow(state => ({
47 accountGroupList: state.accountGroupList,
48 })),
49 )
50
51 const { token } = useUserStore(
52 useShallow(state => ({
53 token: state.token,
54 })),
55 )
56
57 // 返回主页
58 const handleBack = () => {
59 setCurrentView('main')
60 }
61
62 // 处理平台点击
63 const handlePlatformClick = async (platform: PlatType) => {
64 const platInfo = platforms.find(([key]) => key === platform)?.[1]
65 const platformName = platInfo?.name || platform
66
67 if (isPlatformComingSoon(platInfo)) {
68 toast.warning(t('channelManager.platformComingSoon', { platform: platformName }))
69 return
70 }
71
72 if (isPlatformRegionLimited(platInfo)) {
73 await confirmPlatformRegionRedirect(platformName)
74 return
75 }
76
77 // 移动端点击小红书时显示提示
78 if (isMobile && platform === PlatType.Xhs) {
79 toast.warning(t('channelManager.xhsMobileNotSupported'))
80 return
81 }
82
83 // 未登录时关闭频道弹框并跳转登录页
84 if (!token) {
85 closeModal()
86 navigateToLogin()
87 return
88 }
89
90 if (platform === PlatType.WxGzh) {
91 const confirmed = await confirm({
92 title: t('channelManager.wxGzhAuthNoticeTitle'),
93 content: t('channelManager.wxGzhAuthNoticeContent'),
94 okText: t('channelManager.continueAuth'),
95 })
96
97 if (!confirmed) {
98 return
99 }
100 }
101
102 // 如果没有设置目标空间,使用默认空间
103 let spaceId = targetSpaceId
104 if (!spaceId) {
105 const defaultSpace = accountGroupList.find(g => g.isDefault)
106 spaceId = defaultSpace?.id || null
107 if (spaceId) {
108 setTargetSpaceId(spaceId)
109 }
110 }
111
112 // 开始授权流程
113 startAuth(platform, spaceId || undefined)
114 }
115
116 return (
117 <div className="flex h-full flex-col">
118 {/* 头部 - 返回按钮 */}
119 <div className="flex items-center gap-3 border-b px-4 py-3">
120 <Button data-testid="cm-connect-back-btn" variant="ghost" size="sm" className="cursor-pointer" onClick={handleBack}>
121 <ArrowLeft className="mr-1 h-4 w-4" />
122 {t('channelManager.backToChannels')}
123 </Button>
124 </div>
125
126 {/* 新用户提示 */}
127 {isNewUser && (
128 <div data-testid="cm-connect-new-user-tip" className="mx-4 mt-4 flex items-center gap-3 rounded-lg border bg-muted/30 p-4">
129 <Sparkles className="h-6 w-6 shrink-0 text-muted-foreground" />
130 <p className="text-sm text-muted-foreground">{t('channelManager.newUserTip')}</p>
131 </div>
132 )}
133
134 {/* 平台网格 */}
135 <ScrollArea className="flex-1 p-4">
136 <div
137 data-testid="cm-connect-list"
138 className="
139 grid w-full gap-2 sm:gap-3
140 pt-3
141 grid-cols-3
142 sm:grid-cols-4
143 md:grid-cols-5
144 lg:grid-cols-6
145 "
146 >
147 <TooltipProvider>
148 {platforms.map(([key, value]) => {
149 const isDisabled = isPlatformComingSoon(value)
150 const isRegionLimited = isPlatformRegionLimited(value)
151 const isBrowserPlugin = value.authType === 'plugin'
152 const isQrAuth = value.authType === 'qrcode'
153 const regionLimitedLabel = isRegionLimited
154 ? t(isChina ? 'channelManager.internationalSiteOnly' : 'channelManager.chinaSiteOnly')
155 : undefined
156 const disabledTip = isDisabled
157 ? t('channelManager.platformComingSoon', { platform: value.name })
158 : regionLimitedLabel
159
160 return (
161 <Tooltip key={key}>
162 <TooltipTrigger asChild>
163 <Button
164 data-testid="cm-connect-platform-card"
165 variant="ghost"
166 className={`
167 group relative flex h-[90px] min-w-0
168 w-full cursor-pointer flex-col items-center
169 justify-center overflow-hidden whitespace-normal
170 rounded-lg border border-border bg-card
171 p-2 transition-all duration-200
172 hover:-translate-y-0.5 hover:border-foreground/30 hover:bg-accent
173 hover:shadow-md
174 active:translate-y-0 active:scale-[0.98]
175 sm:h-[100px] sm:rounded-xl sm:p-3
176 md:h-[110px]
177 ${isDisabled ? 'opacity-50 grayscale' : ''}
178 ${isRegionLimited ? 'opacity-45 grayscale' : ''}
179 `}
180 onClick={() => handlePlatformClick(key as PlatType)}
181 >
182 {regionLimitedLabel && (
183 <span className="absolute -left-px -top-px z-20 rounded-br rounded-tl-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
184 {regionLimitedLabel}
185 </span>
186 )}
187 {/* 浏览器插件标签 */}
188 {isBrowserPlugin && (
189 <span className="absolute -right-px -top-px z-20 rounded-bl rounded-tr-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
190 {t('channelManager.browserPlugin')}
191 </span>
192 )}
193 {isQrAuth && (
194 <span className="absolute -right-px -top-px z-20 rounded-bl rounded-tr-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
195 {t('channelManager.qrCodeAuth')}
196 </span>
197 )}
198
199 {/* 光泽效果 */}
200 <div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full" />
201
202 <div className="relative z-10 flex w-full flex-col items-center gap-1.5 sm:gap-2">
203 <PlatformIcon
204 platform={key}
205 className="
206 h-9 w-9 object-contain
207 drop-shadow-md filter transition-all
208 duration-300 group-hover:rotate-[5deg]
209 group-hover:scale-110
210 sm:h-10 sm:w-10
211 md:h-11 md:w-11
212 "
213 width={44}
214 height={44}
215 />
216 <span
217 className="
218 line-clamp-2 w-full text-center
219 text-[11px] font-medium leading-tight
220 text-foreground transition-all duration-300
221 group-hover:font-semibold
222 sm:text-xs
223 "
224 >
225 {value.name}
226 </span>
227 </div>
228 </Button>
229 </TooltipTrigger>
230 {disabledTip && (
231 <TooltipContent className="max-w-[200px] text-xs">
232 <p>{disabledTip}</p>
233 </TooltipContent>
234 )}
235 </Tooltip>
236 )
237 })}
238 </TooltipProvider>
239 </div>
240 </ScrollArea>
241 </div>
242 )
243 }
244
244 lines Plain Text