返回 AiToEarn
AuthLoadingPage.tsx
1 /**
2 * AuthLoadingPage - 授权Loading页
3 * 显示授权等待状态、倒计时和操作按钮
4 */
5
6 'use client'
7
8 import { AlertCircle, ExternalLink, Loader2, RefreshCw, X } from 'lucide-react'
9 import { useMemo } from 'react'
10 import { useShallow } from 'zustand/react/shallow'
11 import { useTransClient } from '@/app/i18n/client'
12 import { PlatformIcon } from '@/components/common/PlatformIcon'
13 import { Button } from '@/components/ui/button'
14 import { usePlatformInfo } from '@/hooks/usePlatformMetadata'
15 import { useChannelManagerStore } from '../channelManagerStore'
16
17 export function AuthLoadingPage() {
18 const { t } = useTransClient('account')
19
20 const { authState, targetSpaceId, setCurrentView, reopenAuthWindow, stopAuth, startAuth }
21 = useChannelManagerStore(
22 useShallow(state => ({
23 authState: state.authState,
24 targetSpaceId: state.targetSpaceId,
25 setCurrentView: state.setCurrentView,
26 reopenAuthWindow: state.reopenAuthWindow,
27 stopAuth: state.stopAuth,
28 startAuth: state.startAuth,
29 })),
30 )
31
32 const { platform, countdown, isPolling, error, isTimeout, authMode, qrCodeDataUrl } = authState
33
34 // 获取平台信息
35 const platformInfo = usePlatformInfo(platform)
36
37 const hasQrAuthInstruction = Boolean(platformInfo?.authInstruction)
38 const qrAuthHeaderText = platformInfo?.authInstruction
39 || t('channelManager.qrAuthTitle', { platform: platformInfo?.name ?? platform ?? '' })
40 const qrAuthTip = hasQrAuthInstruction
41 ? null
42 : t('channelManager.qrAuthTip', { platform: platformInfo?.name ?? platform ?? '' })
43
44 // 格式化倒计时
45 const formattedCountdown = useMemo(() => {
46 const minutes = Math.floor(countdown / 60)
47 const seconds = countdown % 60
48 return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
49 }, [countdown])
50
51 // 处理取消
52 const handleCancel = () => {
53 stopAuth()
54 setCurrentView('connect-list')
55 }
56
57 // 处理重试
58 const handleRetry = () => {
59 if (platform) {
60 startAuth(platform, targetSpaceId || undefined)
61 }
62 }
63
64 // 如果没有平台信息,显示错误
65 if (!platformInfo || !platform) {
66 return (
67 <div data-testid="cm-auth-error" className="flex h-full flex-col items-center justify-center p-8">
68 <AlertCircle className="mb-4 h-16 w-16 text-destructive" />
69 <p className="text-lg text-muted-foreground">{t('channelManager.authError')}</p>
70 <Button className="mt-4 cursor-pointer" onClick={handleCancel}>
71 {t('common.back')}
72 </Button>
73 </div>
74 )
75 }
76
77 // 超时或错误状态
78 if (isTimeout || error) {
79 return (
80 <div className="flex h-full flex-col items-center justify-center p-8">
81 <div className="mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-muted">
82 <PlatformIcon
83 platform={platform}
84 width={56}
85 height={56}
86 className="h-14 w-14 object-contain opacity-50 grayscale"
87 />
88 </div>
89
90 <div className="mb-2 flex items-center gap-2 text-destructive">
91 <AlertCircle className="h-5 w-5" />
92 <span className="text-lg font-medium">
93 {isTimeout ? t('channelManager.authTimeout') : t('channelManager.authFailed')}
94 </span>
95 </div>
96
97 <p className="mb-6 text-center text-sm text-muted-foreground">
98 {isTimeout
99 ? t('channelManager.authTimeoutTip')
100 : error || t('channelManager.authFailedTip')}
101 </p>
102
103 <div className="flex gap-3">
104 <Button data-testid="cm-auth-cancel-btn" variant="outline" className="cursor-pointer" onClick={handleCancel}>
105 {t('cancel')}
106 </Button>
107 <Button data-testid="cm-auth-retry-btn" className="cursor-pointer" onClick={handleRetry}>
108 <RefreshCw className="mr-2 h-4 w-4" />
109 {t('channelManager.retry')}
110 </Button>
111 </div>
112 </div>
113 )
114 }
115
116 // 正在授权状态
117 if (authMode === 'miniappQr') {
118 return (
119 <div data-testid="cm-auth-douyin-miniapp-qr" className="flex h-full flex-col items-center justify-center p-8">
120 <div className="mb-5 flex max-w-md items-center justify-center gap-3">
121 <PlatformIcon
122 platform={platform}
123 width={40}
124 height={40}
125 className="h-10 w-10 shrink-0 object-contain"
126 />
127 {hasQrAuthInstruction
128 ? (
129 <p className="text-left text-sm leading-6 text-muted-foreground">
130 {qrAuthHeaderText}
131 </p>
132 )
133 : (
134 <h2 className="text-center text-xl font-semibold">
135 {qrAuthHeaderText}
136 </h2>
137 )}
138 </div>
139
140 <div className="mb-5 flex h-56 w-56 items-center justify-center rounded-lg border border-border bg-card p-3">
141 {qrCodeDataUrl
142 ? (
143 <img
144 src={qrCodeDataUrl}
145 alt={t('channelManager.qrAuthAlt', { platform: platformInfo.name })}
146 className="h-full w-full object-contain"
147 />
148 )
149 : (
150 <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
151 )}
152 </div>
153
154 {qrAuthTip && (
155 <p className="mb-5 max-w-sm text-center text-sm text-muted-foreground">
156 {qrAuthTip}
157 </p>
158 )}
159
160 <div className="mb-7 flex items-center gap-2">
161 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
162 <span className="text-sm text-muted-foreground">{t('channelManager.remainingTime')}</span>
163 <span data-testid="cm-auth-countdown" className="font-mono text-2xl font-bold text-primary">{formattedCountdown}</span>
164 </div>
165
166 <div className="flex gap-3">
167 <Button data-testid="cm-auth-cancel-btn" variant="outline" className="cursor-pointer" onClick={handleCancel}>
168 <X className="mr-2 h-4 w-4" />
169 {t('cancel')}
170 </Button>
171 <Button data-testid="cm-auth-refresh-qr-btn" className="cursor-pointer" onClick={reopenAuthWindow}>
172 <RefreshCw className="mr-2 h-4 w-4" />
173 {t('channelManager.refreshQrCode')}
174 </Button>
175 </div>
176
177 <p className="mt-8 text-center text-xs text-muted-foreground">
178 {t('channelManager.autoRedirectTip')}
179 </p>
180 </div>
181 )
182 }
183
184 return (
185 <div data-testid="cm-auth-loading" className="flex h-full flex-col items-center justify-center p-8">
186 {/* 平台图标和loading动画 */}
187 <div className="relative mb-8">
188 <div className="flex h-28 w-28 items-center justify-center rounded-full bg-gradient-to-br from-primary/10 to-primary/5">
189 <PlatformIcon
190 platform={platform}
191 width={64}
192 height={64}
193 className="h-16 w-16 object-contain"
194 />
195 </div>
196 {/* 旋转边框动画 */}
197 {isPolling && (
198 <div
199 className="absolute inset-0 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
200 style={{ animationDuration: '2s' }}
201 />
202 )}
203 </div>
204
205 {/* 标题 */}
206 <h2 className="mb-2 text-xl font-semibold">
207 {t('channelManager.connecting', { platform: platformInfo.name })}
208 </h2>
209
210 {/* 提示文案 */}
211 <p className="mb-6 text-center text-sm text-muted-foreground">
212 {t('channelManager.authWaitingTip')}
213 </p>
214
215 {/* 倒计时 */}
216 <div className="mb-8 flex items-center gap-2">
217 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
218 <span className="text-sm text-muted-foreground">{t('channelManager.remainingTime')}</span>
219 <span data-testid="cm-auth-countdown" className="font-mono text-2xl font-bold text-primary">{formattedCountdown}</span>
220 </div>
221
222 {/* 操作按钮 */}
223 <div className="flex gap-3">
224 <Button data-testid="cm-auth-cancel-btn" variant="outline" className="cursor-pointer" onClick={handleCancel}>
225 <X className="mr-2 h-4 w-4" />
226 {t('cancel')}
227 </Button>
228 <Button data-testid="cm-auth-reopen-btn" className="cursor-pointer" onClick={reopenAuthWindow}>
229 <ExternalLink className="mr-2 h-4 w-4" />
230 {t('channelManager.reopenAuthPage')}
231 </Button>
232 </div>
233
234 {/* 底部提示 */}
235 <p className="mt-8 text-center text-xs text-muted-foreground">
236 {t('channelManager.autoRedirectTip')}
237 </p>
238 </div>
239 )
240 }
241
241 lines Plain Text