feat: 更新验证码组件,添加 onAbort 处理,优化错误状态显示

This commit is contained in:
2025-09-10 21:36:35 +08:00
parent c17100ed3c
commit 40cbda117d
5 changed files with 33 additions and 13 deletions

View File

@ -15,6 +15,7 @@ export type CaptchaProps = {
url?: string; url?: string;
onSuccess: (token: string) => void; onSuccess: (token: string) => void;
onError: (error: string) => void; onError: (error: string) => void;
onAbort?: () => void;
}; };
export function ReCaptchaWidget(props: CaptchaProps) { export function ReCaptchaWidget(props: CaptchaProps) {

View File

@ -1,7 +1,9 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { CaptchaProps } from "."; import { CaptchaProps } from ".";
import { Turnstile } from "@marsidev/react-turnstile"; import { Turnstile } from "@marsidev/react-turnstile";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const TURNSTILE_TIMEOUT = 15
// 简单的转圈圈动画 // 简单的转圈圈动画
function Spinner() { function Spinner() {
return ( return (
@ -39,14 +41,14 @@ function ErrorMark() {
export function OfficialTurnstileWidget(props: CaptchaProps) { export function OfficialTurnstileWidget(props: CaptchaProps) {
return <div> return <div>
<Turnstile className="w-full" options={{ size: "invisible" }} siteKey={props.siteKey} onSuccess={props.onSuccess} /> <Turnstile className="w-full" options={{ size: "invisible" }} siteKey={props.siteKey} onSuccess={props.onSuccess} onError={props.onError} onAbort={props.onAbort} />
</div>; </div>;
} }
// 自定义包装组件
export function TurnstileWidget(props: CaptchaProps) { export function TurnstileWidget(props: CaptchaProps) {
const t = useTranslations("Captcha"); const t = useTranslations("Captcha");
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [error, setError] = useState<string | null>(null);
// 只在验证通过时才显示勾 // 只在验证通过时才显示勾
const handleSuccess = (token: string) => { const handleSuccess = (token: string) => {
@ -56,15 +58,27 @@ export function TurnstileWidget(props: CaptchaProps) {
const handleError = (error: string) => { const handleError = (error: string) => {
setStatus('error'); setStatus('error');
setError(error);
props.onError && props.onError(error); props.onError && props.onError(error);
}; };
useEffect(() => {
const timer = setTimeout(() => {
if (status === 'loading') {
setStatus('error');
setError('timeout');
props.onError && props.onError('timeout');
}
}, TURNSTILE_TIMEOUT * 1000);
return () => clearTimeout(timer);
})
return ( return (
<div className="flex items-center justify-evenly w-full border border-gray-300 rounded-md px-4 py-2 relative"> <div className="flex items-center justify-evenly w-full border border-gray-300 rounded-md px-4 py-2 relative">
{status === 'loading' && <Spinner />} {status === 'loading' && <Spinner />}
{status === 'success' && <CheckMark />} {status === 'success' && <CheckMark />}
{status === 'error' && <ErrorMark />} {status === 'error' && <ErrorMark />}
<div className="flex-1 text-center">{status === 'success' ? t("success") : (status === 'error' ? t("error") : t("doing"))}</div> <div className="flex-1 text-center">{status === 'success' ? t("success") : (status === 'error' ? t("error") : t("doing"))} {error && t(error)}</div>
<div className="absolute inset-0 opacity-0 pointer-events-none"> <div className="absolute inset-0 opacity-0 pointer-events-none">
<OfficialTurnstileWidget {...props} onSuccess={handleSuccess} onError={handleError} /> <OfficialTurnstileWidget {...props} onSuccess={handleSuccess} onError={handleError} />
</div> </div>

View File

@ -34,6 +34,7 @@ export function LoginForm({
} | null>(null) } | null>(null)
const [captchaToken, setCaptchaToken] = useState<string | null>(null) const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const [captchaError, setCaptchaError] = useState<string | null>(null) const [captchaError, setCaptchaError] = useState<string | null>(null)
const [refreshCaptchaKey, setRefreshCaptchaKey] = useState(0)
const [{ username, password }, setCredentials] = useState({ username: '', password: '' }) const [{ username, password }, setCredentials] = useState({ username: '', password: '' })
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
@ -58,7 +59,7 @@ export function LoginForm({
.catch((error) => { .catch((error) => {
console.error("Error fetching captcha config:", error) console.error("Error fetching captcha config:", error)
}) })
}, []) }, [refreshCaptchaKey])
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@ -71,6 +72,14 @@ export function LoginForm({
} }
} }
const handleCaptchaError = (error: string) => {
setCaptchaError(error);
// 刷新验证码
setTimeout(() => {
setRefreshCaptchaKey(k => k + 1);
}, 1500);
}
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-6", className)} {...props}>
<Card> <Card>
@ -148,11 +157,9 @@ export function LoginForm({
</div> </div>
{captchaProps && {captchaProps &&
<div className="flex justify-center items-center w-full"> <div className="flex justify-center items-center w-full">
<Captcha {...captchaProps} onSuccess={setCaptchaToken} onError={setCaptchaError} /> <Captcha {...captchaProps} onSuccess={setCaptchaToken} onError={handleCaptchaError} key={refreshCaptchaKey} />
</div> </div>
} }
{captchaError && <div>
{t("captcha_error")}</div>}
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"

View File

@ -1,13 +1,11 @@
"use client" "use client"
import { getUserByUsername } from "@/api/user";
import { User } from "@/models/user"; import { User } from "@/models/user";
import { useEffect, useState } from "react"; import GravatarAvatar from "@/components/common/gravatar";
import { getGravatarByUser } from "../common/gravatar";
export function UserProfile({ user }: { user: User }) { export function UserProfile({ user }: { user: User }) {
return ( return (
<div className="flex"> <div className="flex">
{getGravatarByUser({user,className: "rounded-full mr-4"})} <GravatarAvatar email={user.email} size={120}/>
</div> </div>
); );
} }

View File

@ -4,7 +4,7 @@
}, },
"Captcha": { "Captcha": {
"doing": "正在检测你是不是机器人...", "doing": "正在检测你是不是机器人...",
"error": "验证失败,请重试。", "error": "验证失败",
"success": "恭喜,你是人类!" "success": "恭喜,你是人类!"
}, },
"Comment": { "Comment": {