添加账号系统
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function ActivityChart({ data, color = "var(--copper)" }: ActivityChartPr
|
||||
fontSize: 13,
|
||||
}}
|
||||
labelStyle={{ color: "#241b14", fontWeight: 600 }}
|
||||
formatter={(value: number) => [`${value} 次`, "次数"]}
|
||||
formatter={(value) => [`${Number(value ?? 0)} 次`, "次数"]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="count"
|
||||
|
||||
@@ -19,37 +19,11 @@ export function BindDeviceForm() {
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setNotice("正在建立连接,请稍候……");
|
||||
setNotice("正在前往绑定页面,请稍候……");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/family/bind", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ deviceUuid: rawCode }),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| { error?: string }
|
||||
| null;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || "连接失败,请稍后再试。");
|
||||
}
|
||||
|
||||
setNotice("连接成功!现在可以查看长辈的留言和动态了。\n");
|
||||
setRawCode("");
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
} catch (error) {
|
||||
setNotice(
|
||||
error instanceof Error ? error.message : "连接失败,请稍后再试。",
|
||||
);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
startTransition(() => {
|
||||
router.push(`/bind?deviceUuid=${encodeURIComponent(rawCode.trim())}`);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,7 @@ type PanelShellProps = {
|
||||
currentPath: string;
|
||||
title: string;
|
||||
description: string;
|
||||
caregiverToken: string;
|
||||
caregiverLabel: string;
|
||||
children: ReactNode;
|
||||
actions?: ReactNode;
|
||||
eyebrow?: string;
|
||||
@@ -32,7 +32,7 @@ export function PanelShell({
|
||||
currentPath,
|
||||
title,
|
||||
description,
|
||||
caregiverToken,
|
||||
caregiverLabel,
|
||||
children,
|
||||
actions,
|
||||
eyebrow = "家人连线",
|
||||
@@ -81,8 +81,16 @@ export function PanelShell({
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||
<span className="rounded-full border border-[var(--line)] bg-white/76 px-4 py-2">
|
||||
账号:{caregiverToken.slice(0, 8).toUpperCase()}
|
||||
账号:{caregiverLabel}
|
||||
</span>
|
||||
<form action="/api/account/logout" method="post">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex h-10 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -45,38 +45,13 @@ export function ScanClient() {
|
||||
|
||||
scannerRef.current.stop();
|
||||
setErrorText(null);
|
||||
setStatusText("识别到了设备,正在连接……");
|
||||
setStatusText("识别到了设备,正在打开绑定页面……");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/family/bind", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ deviceUuid: scannedValue }),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
| { error?: string; deviceUuid?: string }
|
||||
| null;
|
||||
const deviceUuid = payload?.deviceUuid;
|
||||
|
||||
if (!response.ok || !deviceUuid) {
|
||||
throw new Error(payload?.error || "连接失败,请重新扫描。");
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(
|
||||
`/bind?deviceUuid=${encodeURIComponent(deviceUuid)}&source=scan`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
setErrorText(
|
||||
error instanceof Error ? error.message : "连接失败,请重新扫描。",
|
||||
startTransition(() => {
|
||||
router.replace(
|
||||
`/bind?deviceUuid=${encodeURIComponent(scannedValue)}&source=scan`,
|
||||
);
|
||||
setStatusText("请再次将二维码对准镜头。");
|
||||
await scannerRef.current.start().catch(() => null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user