From df41f7713d9cfd55d6f5e31f3142131135afdf7b Mon Sep 17 00:00:00 2001
From: jacobxu666
Date: Wed, 19 Aug 2026 15:20:49 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=B6=E9=95=BF=E6=B3=A8?=
=?UTF-8?q?=E5=86=8C=E9=A1=B5=20/register:=E5=87=AD=E5=AE=B6=E5=BA=AD?=
=?UTF-8?q?=E9=82=80=E8=AF=B7=E7=A0=81=E6=B3=A8=E5=86=8C=E5=8A=A0=E5=85=A5?=
=?UTF-8?q?=E5=B9=B6=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/login/page.tsx | 9 ++++
src/app/register/actions.ts | 68 ++++++++++++++++++++++++++++++
src/app/register/page.tsx | 83 +++++++++++++++++++++++++++++++++++++
3 files changed, 160 insertions(+)
create mode 100644 src/app/register/actions.ts
create mode 100644 src/app/register/page.tsx
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 729e276..337f616 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -65,6 +65,15 @@ export default function LoginPage() {
去输入激活码
+
+ 家里另一位家长想加入?{" "}
+
+ 用邀请码加入家庭
+
+
diff --git a/src/app/register/actions.ts b/src/app/register/actions.ts
new file mode 100644
index 0000000..09f9fbd
--- /dev/null
+++ b/src/app/register/actions.ts
@@ -0,0 +1,68 @@
+"use server";
+
+import crypto from "node:crypto";
+import bcrypt from "bcryptjs";
+import { and, eq, gt, isNull } from "drizzle-orm";
+import { redirect } from "next/navigation";
+import { z } from "zod";
+import { db } from "@/db";
+import { familyInvites, users } from "@/db/schema";
+import { createSession } from "@/lib/auth";
+
+function hashCode(code: string) {
+ return crypto.createHash("sha256").update(code).digest("hex");
+}
+
+const registerSchema = z.object({
+ displayName: z.string().trim().min(1, "请输入称呼").max(20, "称呼最多 20 字"),
+ phone: z.string().regex(/^1\d{10}$/, "请输入 11 位手机号"),
+ password: z.string().min(6, "密码至少 6 位"),
+ code: z.string().regex(/^\d{6}$/, "邀请码是 6 位数字"),
+});
+
+export type RegisterState = { error?: string } | null;
+
+// xuchao: 家长凭邀请码注册:校验码→查重手机号→事务(码标记已用+建家长)→自动登录跳家长中心
+export async function registerWithInvite(
+ _prev: RegisterState,
+ formData: FormData,
+): Promise {
+ const parsed = registerSchema.safeParse({
+ displayName: formData.get("displayName"),
+ phone: formData.get("phone"),
+ password: formData.get("password"),
+ code: formData.get("code"),
+ });
+ if (!parsed.success) return { error: parsed.error.issues[0].message };
+
+ const { displayName, phone, password, code } = parsed.data;
+
+ const [invite] = await db
+ .select({ id: familyInvites.id, familyId: familyInvites.familyId })
+ .from(familyInvites)
+ .where(
+ and(
+ eq(familyInvites.codeHash, hashCode(code)),
+ isNull(familyInvites.usedAt),
+ gt(familyInvites.expiresAt, new Date()),
+ ),
+ )
+ .limit(1);
+ if (!invite) return { error: "邀请码无效或已过期,请让家长重新生成" };
+
+ const [dup] = await db.select({ id: users.id }).from(users).where(eq(users.phone, phone)).limit(1);
+ if (dup) return { error: "该手机号已注册" };
+
+ const passwordHash = await bcrypt.hash(password, 10);
+ const newUserId = await db.transaction(async (tx) => {
+ await tx.update(familyInvites).set({ usedAt: new Date() }).where(eq(familyInvites.id, invite.id));
+ const [created] = await tx
+ .insert(users)
+ .values({ familyId: invite.familyId, role: "parent", displayName, phone, passwordHash })
+ .$returningId();
+ return created.id;
+ });
+
+ await createSession(newUserId, "家长设备", 30);
+ redirect("/parent");
+}
\ No newline at end of file
diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx
new file mode 100644
index 0000000..8da58ac
--- /dev/null
+++ b/src/app/register/page.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useActionState } from "react";
+import Link from "next/link";
+import { UserPlus } from "lucide-react";
+import { BackgroundBlobs } from "@/components/background-blobs";
+import { registerWithInvite } from "./actions";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default function RegisterPage() {
+ const [state, formAction, pending] = useActionState(registerWithInvite, null);
+
+ return (
+
+
+
+
+
+
+
+ 加入家庭
+ 需要家里已有成员的邀请码
+
+
+
+
+
+
+ );
+}
\ No newline at end of file