287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
|
"use client";
|
||
|
|
||
|
import { useState, useEffect } from "react";
|
||
|
import { useToast } from "@/contexts/ToastContext";
|
||
|
import * as Form from "@radix-ui/react-form";
|
||
|
import useFetch from "@/hooks/useFetch";
|
||
|
import GoogleLogin from "./google-login";
|
||
|
import { RadioGroup, Radio } from "@headlessui/react";
|
||
|
import { useRouter } from "next/navigation";
|
||
|
import useUserStore from "@/store/userStore";
|
||
|
import { useTranslations } from "next-intl";
|
||
|
import Link from "next/link";
|
||
|
|
||
|
interface LoginFormProps {
|
||
|
toRegister: () => void;
|
||
|
}
|
||
|
|
||
|
type LoginMethod = "password" | "code";
|
||
|
|
||
|
export default function LoginForm({ toRegister }: LoginFormProps) {
|
||
|
const t = useTranslations("loginForm");
|
||
|
const tTerms = useTranslations("terms"); // 用于获取用户协议的标题
|
||
|
const tGlobal = useTranslations(); // 用于获取全局翻译,如 "agreeTo" 和 "agreeToTerms"
|
||
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("password");
|
||
|
const [username, setUsername] = useState("");
|
||
|
const [phone, setPhone] = useState("");
|
||
|
const [password, setPassword] = useState("");
|
||
|
const [captcha, setCaptcha] = useState("");
|
||
|
const [captchaTimer, setCaptchaTimer] = useState(0);
|
||
|
const [agreed, setAgreed] = useState(true); // 默认勾选
|
||
|
|
||
|
const router = useRouter();
|
||
|
const { addToast } = useToast();
|
||
|
|
||
|
const setUser = useUserStore((state) => state.setUser);
|
||
|
|
||
|
const {
|
||
|
fetchData: login,
|
||
|
loading: loginLoading,
|
||
|
error: loginError,
|
||
|
data: loginData,
|
||
|
} = useFetch({
|
||
|
url: "/api/login/",
|
||
|
method: "POST",
|
||
|
});
|
||
|
|
||
|
const {
|
||
|
fetchData: fetchCaptcha,
|
||
|
loading: captchaLoading,
|
||
|
error: captchaError,
|
||
|
data: captchaData,
|
||
|
} = useFetch({
|
||
|
url: "/api/send-verification-sms/",
|
||
|
method: "POST",
|
||
|
});
|
||
|
const {
|
||
|
fetchData: GetUserinfo,
|
||
|
loading: userinfoLoading,
|
||
|
data: userinfoData,
|
||
|
} = useFetch({
|
||
|
url: "/api/profile/",
|
||
|
method: "POST",
|
||
|
});
|
||
|
|
||
|
useEffect(() => {
|
||
|
let timer: NodeJS.Timeout;
|
||
|
if (captchaTimer > 0) {
|
||
|
timer = setInterval(() => {
|
||
|
setCaptchaTimer((prev) => prev - 1);
|
||
|
}, 1000);
|
||
|
}
|
||
|
return () => clearInterval(timer);
|
||
|
}, [captchaTimer]);
|
||
|
|
||
|
const handleGetCaptcha = async () => {
|
||
|
if (!phone) {
|
||
|
addToast(t("enterPhoneNumber"), "error");
|
||
|
return;
|
||
|
}
|
||
|
await fetchCaptcha({ phone_number: phone });
|
||
|
setCaptchaTimer(60);
|
||
|
};
|
||
|
|
||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||
|
event.preventDefault();
|
||
|
|
||
|
if (!agreed) {
|
||
|
addToast(tGlobal("agreeToTerms"), "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// 校验非空字段
|
||
|
if (loginMethod === "password" && !username) {
|
||
|
addToast(t("enterUsername"), "error");
|
||
|
return;
|
||
|
}
|
||
|
if (loginMethod === "code" && !phone) {
|
||
|
addToast(t("enterPhone"), "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (loginMethod === "password" && !password) {
|
||
|
addToast(t("enterPassword"), "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (loginMethod === "code" && !captcha) {
|
||
|
addToast(t("enterCaptcha"), "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const loginData = {
|
||
|
login_type: loginMethod,
|
||
|
...(loginMethod === "password" && { username, password }),
|
||
|
...(loginMethod === "code" && { phone, code: captcha }),
|
||
|
};
|
||
|
|
||
|
// 调用登录函数发送登录请求
|
||
|
login(loginData).then(() => {
|
||
|
addToast(t("loginSuccess"), "success");
|
||
|
|
||
|
GetUserinfo().then((res) => {
|
||
|
console.log("userinfo", res);
|
||
|
setUser(res);
|
||
|
});
|
||
|
router.replace("/create");
|
||
|
});
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<div className="flex flex-col items-center gap-[25px] py-8 bg-black/80 rounded-lg shadow-lg px-8 w-[500px]">
|
||
|
<div className="mt-auto w-full items-center justify-stretch flex flex-col gap-1.5">
|
||
|
<GoogleLogin />
|
||
|
<div className="z-0 w-[80%] relative flex justify-center items-center my-3">
|
||
|
<small className="font-normal text-xs bg-transparent text-gray-300 px-2.5 py-1 tracking-wide border-0 border-t border-gray-600 leading-none">
|
||
|
{t("orUseLoginMethod")}
|
||
|
</small>
|
||
|
<div className="z-[-1] absolute h-[1px] w-full bg-info opacity-50"></div>
|
||
|
</div>
|
||
|
<Form.Root className="w-full" onSubmit={handleSubmit}>
|
||
|
<div className="w-full flex flex-col gap-3">
|
||
|
<Form.Field name="loginMethod">
|
||
|
<RadioGroup
|
||
|
value={loginMethod}
|
||
|
onChange={setLoginMethod}
|
||
|
aria-label="Login method"
|
||
|
className="flex items-center gap-3"
|
||
|
>
|
||
|
<div className="flex items-center gap-2">
|
||
|
<Radio
|
||
|
id="password"
|
||
|
value="password"
|
||
|
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||
|
>
|
||
|
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||
|
</Radio>
|
||
|
<Form.Label htmlFor="password" className="text-gray-300">
|
||
|
{t("passwordLogin")}
|
||
|
</Form.Label>
|
||
|
</div>
|
||
|
<div className="flex items-center gap-2">
|
||
|
<Radio
|
||
|
id="code"
|
||
|
value="code"
|
||
|
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||
|
>
|
||
|
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||
|
</Radio>
|
||
|
<Form.Label htmlFor="code" className="text-gray-300">
|
||
|
{t("codeLogin")}
|
||
|
</Form.Label>
|
||
|
</div>
|
||
|
</RadioGroup>
|
||
|
</Form.Field>
|
||
|
|
||
|
{loginMethod === "password" && (
|
||
|
<>
|
||
|
<Form.Field name="username">
|
||
|
<input
|
||
|
type="text"
|
||
|
id="username"
|
||
|
value={username}
|
||
|
onChange={(e) => setUsername(e.target.value)}
|
||
|
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||
|
placeholder={t("emailOrPhone")}
|
||
|
/>
|
||
|
</Form.Field>
|
||
|
|
||
|
<Form.Field name="password">
|
||
|
<input
|
||
|
type="password"
|
||
|
id="password"
|
||
|
value={password}
|
||
|
onChange={(e) => setPassword(e.target.value)}
|
||
|
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||
|
placeholder={t("yourPassword")}
|
||
|
/>
|
||
|
</Form.Field>
|
||
|
</>
|
||
|
)}
|
||
|
|
||
|
{loginMethod === "code" && (
|
||
|
<>
|
||
|
<Form.Field name="phone">
|
||
|
<input
|
||
|
type="tel"
|
||
|
id="phone"
|
||
|
value={phone}
|
||
|
onChange={(e) => setPhone(e.target.value)}
|
||
|
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||
|
placeholder={t("phone")}
|
||
|
/>
|
||
|
</Form.Field>
|
||
|
<Form.Field name="captcha">
|
||
|
<div className="flex items-center">
|
||
|
<input
|
||
|
type="text"
|
||
|
id="captcha"
|
||
|
value={captcha}
|
||
|
onChange={(e) => setCaptcha(e.target.value)}
|
||
|
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||
|
placeholder={t("captcha")}
|
||
|
/>
|
||
|
<button
|
||
|
type="button"
|
||
|
onClick={handleGetCaptcha}
|
||
|
className="btn h-10 btn-sm w-32 ml-2 bg-transparent hover:bg-green-500 hover:text-white text-gray-500 rounded-lg"
|
||
|
disabled={captchaTimer > 0 || captchaLoading}
|
||
|
>
|
||
|
{captchaLoading ? (
|
||
|
<span className="loading loading-spinner loading-md"></span>
|
||
|
) : captchaTimer > 0 ? (
|
||
|
`${captchaTimer}s`
|
||
|
) : (
|
||
|
t("getCaptcha")
|
||
|
)}
|
||
|
</button>
|
||
|
</div>
|
||
|
</Form.Field>
|
||
|
</>
|
||
|
)}
|
||
|
|
||
|
{/* 用户协议复选框 */}
|
||
|
<Form.Field name="agreement">
|
||
|
<div className="flex items-center justify-between">
|
||
|
<label className="flex items-center text-gray-300 text-sm">
|
||
|
<input
|
||
|
type="checkbox"
|
||
|
checked={agreed}
|
||
|
onChange={(e) => setAgreed(e.target.checked)}
|
||
|
className="h-4 w-4 text-green-500 bg-gray-700 border-gray-600 rounded focus:ring-green-500"
|
||
|
/>
|
||
|
<span className="ml-2">
|
||
|
{tGlobal("agreeTo")}{" "}
|
||
|
<Link href="/terms" className="text-green-500 underline">
|
||
|
{tTerms("title")}
|
||
|
</Link>
|
||
|
</span>
|
||
|
</label>
|
||
|
</div>
|
||
|
</Form.Field>
|
||
|
|
||
|
<Form.Submit
|
||
|
disabled={loginLoading}
|
||
|
className="btn bg-transparent hover:bg-transparent text-gray-500 w-full rounded-lg hover:text-green-500"
|
||
|
>
|
||
|
{t("login")}
|
||
|
{loginLoading ? (
|
||
|
<span className="loading loading-spinner loading-md"></span>
|
||
|
) : (
|
||
|
""
|
||
|
)}
|
||
|
</Form.Submit>
|
||
|
</div>
|
||
|
</Form.Root>
|
||
|
|
||
|
<button
|
||
|
onClick={toRegister}
|
||
|
className="btn btn-outline btn-neutral whitespace-nowrap z-0 text-sm font-medium w-full mt-3 bg-transparent border-gray-500 text-gray-300 hover:bg-gray-700"
|
||
|
>
|
||
|
{t("register")}
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|