Illuminix_nextjs/src/ui/login/login-form.tsx
2024-09-21 22:30:45 +08:00

310 lines
14 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-[full] sm: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-200 rounded-lg"
disabled={
captchaTimer > 0 ||
captchaLoading
}
>
{captchaLoading ? (
<span className="loading loading-spinner loading-md text-gray-200"></span>
) : captchaTimer > 0 ? (
<span className="text-gray-200">
{captchaTimer}s
</span>
) : (
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 text-gray-200 w-full rounded-lg hover:text-green-500 hover:bg-secondary/20"
>
{loginLoading ? (
<span className="loading loading-spinner loading-md text-gray-200"></span>
) : (
<span>{t("login")}</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-200 hover:bg-gray-700"
>
{t("register")}
</button>
</div>
</div>
);
}