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;
onSuccess: (token: string) => void;
onError: (error: string) => void;
onAbort?: () => void;
};
export function ReCaptchaWidget(props: CaptchaProps) {

View File

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

View File

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

View File

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

View File

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