From 21ada3b52b7a3af68df9221fea9dd1c70c21f1df Mon Sep 17 00:00:00 2001 From: cybaka520 Date: Sun, 18 Oct 2026 14:21:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E5=90=8E=E7=BD=91=E9=A1=B5=E7=AB=AF=E6=9C=AA=E6=94=B6=E5=88=B0?= =?UTF-8?q?=E5=B7=B2=E6=89=AB=E7=A0=81=E7=8A=B6=E6=80=81=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hub/android/data/remote/AuthRepository.kt | 5 ++ .../hub/android/ui/screen/ScanViewModel.kt | 3 + frontend/src/components/AuthDialog.tsx | 78 ++++++++++--------- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt index 8d83469..6aa67f6 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt @@ -46,6 +46,11 @@ class AuthRepository @Inject constructor( // ---- 扫码登录 ---- + /** 上报「已扫码、等待确认」,让网页端立刻提示扫码成功 */ + suspend fun markQrScanned(ticket: String) { + request { api.markQrScanned(TicketRequest(ticket)) } + } + /** 确认把当前账号授权给该票据 */ suspend fun confirmQrLogin(ticket: String) { request { api.confirmQr(TicketRequest(ticket)) } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt index f7202ab..e5da614 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt @@ -52,6 +52,9 @@ class ScanViewModel @Inject constructor( return } _state.value = ScanState.AwaitingConfirm(ticket) + // 通知网页端「已扫码」,让它从二维码切到「扫码成功,请在手机上确认」。 + // 失败不影响本地确认流程,网页端到确认时仍能拿到 token。 + viewModelScope.launch { runCatching { repo.markQrScanned(ticket) } } } /** 用户确认授权 */ diff --git a/frontend/src/components/AuthDialog.tsx b/frontend/src/components/AuthDialog.tsx index 4c27098..0403909 100644 --- a/frontend/src/components/AuthDialog.tsx +++ b/frontend/src/components/AuthDialog.tsx @@ -12,13 +12,14 @@ import { import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowLeft, Eye, EyeOff, Github, Loader2, Mail, X } from 'lucide-react'; +import { ArrowLeft, Check, Eye, EyeOff, Github, Loader2, Mail, X } from 'lucide-react'; import { z } from 'zod'; import { useAuth } from '@/hooks/useAuth'; import { useAliyunCaptcha } from '@/hooks/useAliyunCaptcha'; import { useCountdown } from '@/hooks/useCountdown'; import { api } from '@/lib/api'; import { queryKeys } from '@/lib/query'; +import { cn } from '@/lib/utils'; import { buttonTap } from '@/lib/motion'; import type { CaptchaConfig, LoginResult } from '@/lib/auth'; import { Dialog, DialogContent } from '@/components/ui/dialog'; @@ -56,7 +57,6 @@ type CodeFormValues = z.infer>; /** 扫码登录面板:申请票据 → 渲染二维码 → 轮询状态 → 确认后写入登录态 */ function QrLoginPanel({ onSuccess }: { onSuccess: (r: LoginResult) => void }) { const [dataUrl, setDataUrl] = useState(''); - const [countdown, setCountdown] = useState(0); // 弹窗打开时申请票据;关闭后作废(refetchInterval 内会停) const ticketQuery = useQuery({ @@ -69,6 +69,7 @@ function QrLoginPanel({ onSuccess }: { onSuccess: (r: LoginResult) => void }) { }); const ticket = ticketQuery.data; + const { refetch: refetchTicket } = ticketQuery; // 二维码渲染:qrContent 变化或票据过期重建时重新生成 useEffect(() => { @@ -94,27 +95,24 @@ function QrLoginPanel({ onSuccess }: { onSuccess: (r: LoginResult) => void }) { }; }, [ticket?.qrContent]); - // 过期倒计时 + // 票据到期自动换新,用户无需手动刷新 useEffect(() => { if (!ticket) return; - setCountdown(ticket.expiresIn); - const timer = setInterval(() => { - setCountdown((c) => { - if (c <= 1) { - clearInterval(timer); - return 0; - } - return c - 1; - }); - }, 1000); - return () => clearInterval(timer); - }, [ticket]); + const timer = setTimeout( + () => { + setDataUrl(''); + void refetchTicket(); + }, + Math.max(ticket.expiresIn, 1) * 1000 + ); + return () => clearTimeout(timer); + }, [ticket, refetchTicket]); // 轮询票据状态,confirmed 时把 token 交给外层写入登录态 const statusQuery = useQuery({ queryKey: [...queryKeys.qrTicket, ticket?.ticket], queryFn: () => api.getQrTicketStatus(ticket!.ticket), - enabled: !!ticket?.ticket && countdown > 0, + enabled: !!ticket?.ticket, refetchInterval: (query) => { const status = query.state.data?.status; // 已确认 / 已过期就停轮询 @@ -131,33 +129,39 @@ function QrLoginPanel({ onSuccess }: { onSuccess: (r: LoginResult) => void }) { if (token && user) onSuccess({ token, user }); }, [status, statusQuery.data, onSuccess]); - const expired = countdown <= 0 || status === 'expired'; + // 服务端判定票据失效时同样自动换新 + useEffect(() => { + if (status !== 'expired') return; + setDataUrl(''); + void refetchTicket(); + }, [status, refetchTicket]); + const scanned = status === 'scanned'; return (
- {dataUrl && !expired && 登录二维码} - {expired && ( -
-

二维码已过期

- + {dataUrl && ( + 登录二维码 + )} + {!dataUrl && !scanned && ( +
+
)} - {scanned && !expired && ( -
- -

- 已扫码 -
- 请在手机上确认 -

+ {scanned && ( +
+ + + +
+

扫码成功

+

请在手机上确认

+
)}
@@ -167,8 +171,6 @@ function QrLoginPanel({ onSuccess }: { onSuccess: (r: LoginResult) => void }) {
扫码登录或扫码下载 APP

- - {!expired &&

{countdown}s 后失效

}
); }