189 lines
6.7 KiB
TypeScript
189 lines
6.7 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import { startTransition, useState } from "react";
|
||
|
||
type AccountAuthFormProps = {
|
||
redirectTo: string;
|
||
};
|
||
|
||
type AuthMode = "login" | "register";
|
||
|
||
export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
|
||
const router = useRouter();
|
||
const [mode, setMode] = useState<AuthMode>("login");
|
||
const [username, setUsername] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [notice, setNotice] = useState<string | null>(null);
|
||
|
||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
|
||
if (!username.trim()) {
|
||
setNotice("请先输入用户名。");
|
||
return;
|
||
}
|
||
|
||
if (!password) {
|
||
setNotice("请先输入密码。");
|
||
return;
|
||
}
|
||
|
||
if (mode === "register" && password !== confirmPassword) {
|
||
setNotice("两次输入的密码还不一致,请再检查一次。");
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
setNotice(mode === "login" ? "正在登录账号……" : "正在创建账号……");
|
||
|
||
try {
|
||
const response = await fetch(
|
||
mode === "login" ? "/api/account/login" : "/api/account/register",
|
||
{
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
username: username.trim(),
|
||
password,
|
||
redirectTo,
|
||
}),
|
||
},
|
||
);
|
||
|
||
const payload = (await response.json().catch(() => null)) as
|
||
| { error?: string; redirectTo?: string }
|
||
| null;
|
||
|
||
if (!response.ok) {
|
||
throw new Error(
|
||
payload?.error ||
|
||
(mode === "login"
|
||
? "登录失败,请稍后再试。"
|
||
: "创建账号失败,请稍后再试。"),
|
||
);
|
||
}
|
||
|
||
setNotice(mode === "login" ? "登录成功,正在进入……" : "账号已创建,正在进入……");
|
||
startTransition(() => {
|
||
router.replace(payload?.redirectTo || redirectTo);
|
||
router.refresh();
|
||
});
|
||
} catch (error) {
|
||
setNotice(
|
||
error instanceof Error ? error.message : "操作失败,请稍后再试。",
|
||
);
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="rounded-[32px] border border-white/75 bg-white/84 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur sm:p-7">
|
||
<div className="flex flex-wrap gap-3">
|
||
{[
|
||
{ id: "login", label: "已有账号,直接登录" },
|
||
{ id: "register", label: "第一次使用,创建账号" },
|
||
].map((item) => {
|
||
const active = mode === item.id;
|
||
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
onClick={() => {
|
||
setMode(item.id as AuthMode);
|
||
setNotice(null);
|
||
}}
|
||
className={`inline-flex h-11 items-center justify-center rounded-full px-4 text-sm font-semibold transition ${
|
||
active
|
||
? "bg-[var(--ink)] text-white shadow-[0_12px_30px_rgba(36,27,20,0.18)]"
|
||
: "border border-[var(--line)] bg-white text-[var(--ink)] hover:bg-[var(--paper-soft)]"
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||
<div>
|
||
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-username">
|
||
用户名
|
||
</label>
|
||
<input
|
||
id="caregiver-username"
|
||
value={username}
|
||
onChange={(event) => setUsername(event.target.value)}
|
||
autoComplete="username"
|
||
placeholder="例如:family_guardian"
|
||
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
|
||
/>
|
||
<p className="mt-2 text-xs leading-6 text-[var(--muted)]">
|
||
建议使用 3 到 24 位字母、数字、下划线或横线,方便全家统一登录。
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-password">
|
||
密码
|
||
</label>
|
||
<input
|
||
id="caregiver-password"
|
||
type="password"
|
||
value={password}
|
||
onChange={(event) => setPassword(event.target.value)}
|
||
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
||
placeholder="请输入密码"
|
||
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
|
||
/>
|
||
<p className="mt-2 text-xs leading-6 text-[var(--muted)]">
|
||
密码至少 8 位。同一账号可以在多台家属设备登录,消息提醒会同步到所有已登录设备。
|
||
</p>
|
||
</div>
|
||
|
||
{mode === "register" ? (
|
||
<div>
|
||
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-password-confirm">
|
||
再输入一次密码
|
||
</label>
|
||
<input
|
||
id="caregiver-password-confirm"
|
||
type="password"
|
||
value={confirmPassword}
|
||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||
autoComplete="new-password"
|
||
placeholder="再次输入密码"
|
||
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
|
||
/>
|
||
</div>
|
||
) : null}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={submitting}
|
||
className="inline-flex h-12 w-full items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)] disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{submitting
|
||
? mode === "login"
|
||
? "正在登录"
|
||
: "正在创建账号"
|
||
: mode === "login"
|
||
? "登录并继续"
|
||
: "创建账号并继续"}
|
||
</button>
|
||
</form>
|
||
|
||
{notice ? (
|
||
<p className="mt-4 rounded-[20px] bg-[var(--paper-soft)] px-4 py-3 text-sm leading-7 text-[var(--muted)]">
|
||
{notice}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
);
|
||
} |