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