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;
|
||||
onSuccess: (token: string) => void;
|
||||
onError: (error: string) => void;
|
||||
onAbort?: () => void;
|
||||
};
|
||||
|
||||
export function ReCaptchaWidget(props: CaptchaProps) {
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user