diff --git a/src/app/parent/actions.ts b/src/app/parent/actions.ts index ba1d4cd..9892874 100644 --- a/src/app/parent/actions.ts +++ b/src/app/parent/actions.ts @@ -5,7 +5,7 @@ import { and, eq, sql } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { db } from "@/db"; -import { activationCodes, sessions, taskInstances, taskTemplates, users } from "@/db/schema"; +import { activationCodes, familyInvites, sessions, taskInstances, taskTemplates, users } from "@/db/schema"; import { SUBJECT_VALUES } from "@/db/schema"; import { todayStr } from "@/lib/dates"; import { ensureDay } from "@/lib/tasks"; @@ -51,6 +51,21 @@ export async function createActivationCode( return { code, childName: child.displayName }; } +// xuchao: 生成家庭邀请码(6 位,15 分钟一次性),对方凭码在 /register 注册加入 +export async function generateFamilyInvite(): Promise<{ code?: string; error?: string }> { + const parent = await getSessionUser(); + if (!parent || parent.role !== "parent") return { error: "请先以家长身份登录" }; + + const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0"); + await db.insert(familyInvites).values({ + familyId: parent.familyId, + codeHash: hashCode(code), + createdBy: parent.id, + expiresAt: new Date(Date.now() + 15 * 60_000), + }); + return { code }; +} + // 家长补卡 / 撤销补卡:仅限过去的日期,补卡记录永久带标记 export async function makeupTask(instanceId: number, done: boolean) { const parent = await getSessionUser(); diff --git a/src/app/parent/dashboard.tsx b/src/app/parent/dashboard.tsx index 1e20cd6..3540b3c 100644 --- a/src/app/parent/dashboard.tsx +++ b/src/app/parent/dashboard.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ChildManage } from "./child-manage"; import { DevicesTab } from "./devices-tab"; +import { FamilyManage } from "./family-manage"; import { OverviewTab } from "./overview-tab"; import { PointsManage } from "./points-manage"; import { TemplatesTab } from "./templates-tab"; @@ -69,6 +70,13 @@ export type DevicesData = { }; export type WeeklyRow = { childName: string; weekKey: string; score: number; passed: boolean }; +// xuchao: 家庭管理数据——家庭名 + 家长成员 + 孩子成员 +export type FamilyData = { + familyName: string; + parents: { id: number; name: string }[]; + kids: { id: number; name: string }[]; +}; + const tabCls = "data-[state=active]:bg-white data-[state=active]:text-indigo-600 data-[state=active]:shadow-soft"; function Section({ title, desc, children }: { title: string; desc?: string; children: React.ReactNode }) { @@ -87,12 +95,14 @@ export function ParentDashboard({ devices, weekly, points, + family, }: { overview: OverviewData; templates: TemplatesData; devices: DevicesData; weekly: WeeklyRow[]; points: PointsData; + family: FamilyData; }) { return (
@@ -165,6 +175,14 @@ export function ParentDashboard({ +
+ +
+
({ id: k.id, name: k.name }))} />
diff --git a/src/app/parent/family-manage.tsx b/src/app/parent/family-manage.tsx new file mode 100644 index 0000000..ecb48de --- /dev/null +++ b/src/app/parent/family-manage.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { UserPlus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { generateFamilyInvite } from "./actions"; + +// xuchao: 家庭管理——展示成员列表,生成一次性邀请码让其他家长注册加入 +export function FamilyManage({ + familyName, + parents, + kids, +}: { + familyName: string; + parents: { id: number; name: string }[]; + kids: { id: number; name: string }[]; +}) { + const [invite, setInvite] = useState<{ code?: string; error?: string } | null>(null); + const [pending, startTransition] = useTransition(); + + const gen = () => { + setInvite(null); + startTransition(async () => { + setInvite(await generateFamilyInvite()); + }); + }; + + return ( +
+

+ {familyName} · {parents.length} 位家长 · {kids.length} 个孩子 +

+
+ {parents.map((p) => ( +
+ {p.name} + 家长 +
+ ))} + {kids.map((k) => ( +
+ {k.name} + 孩子 +
+ ))} +
+
+ + {invite?.error &&

{invite.error}

} + {invite?.code && ( +

+ 邀请码{" "} + {invite.code} + 15 分钟有效,一次性 +

+ )} +
+
+ ); +} diff --git a/src/app/parent/page.tsx b/src/app/parent/page.tsx index 863e4d5..1b3c1bb 100644 --- a/src/app/parent/page.tsx +++ b/src/app/parent/page.tsx @@ -13,6 +13,7 @@ import { weeklyTest, wrongBook, wordRecording, + families, } from "@/db/schema"; import { SESSION_COOKIE, getSessionUser } from "@/lib/auth"; import { addDays, dowOf, todayStr } from "@/lib/dates"; @@ -27,6 +28,7 @@ import { viewUrl } from "@/lib/oss"; import { ParentDashboard, type DevicesData, + type FamilyData, type OverviewData, type PointsData, type TemplatesData, @@ -219,6 +221,23 @@ export default async function ParentPage() { }); } + // ===== 家庭管理(家庭名 + 全部成员) ===== + const [familyRow] = await db + .select({ name: families.name }) + .from(families) + .where(eq(families.id, parent.familyId)) + .limit(1); + const parentRows = await db + .select({ id: users.id, displayName: users.displayName }) + .from(users) + .where(and(eq(users.familyId, parent.familyId), eq(users.role, "parent"))) + .orderBy(asc(users.id)); + const family: FamilyData = { + familyName: familyRow?.name ?? "我的家庭", + parents: parentRows.map((p) => ({ id: p.id, name: p.displayName })), + kids: kids.map((k) => ({ id: k.id, name: k.name })), + }; + return ( ); }