mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
feat: 更新验证码组件,添加 onAbort 处理,优化错误状态显示
This commit is contained in:
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
},
|
},
|
||||||
"Captcha": {
|
"Captcha": {
|
||||||
"doing": "正在检测你是不是机器人...",
|
"doing": "正在检测你是不是机器人...",
|
||||||
"error": "验证失败,请重试。",
|
"error": "验证失败",
|
||||||
"success": "恭喜,你是人类!"
|
"success": "恭喜,你是人类!"
|
||||||
},
|
},
|
||||||
"Comment": {
|
"Comment": {
|
||||||
|
Reference in New Issue
Block a user