"use client" import { useEffect, useMemo, useState } from "react" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" import { toast } from "sonner" import { Button } from "@/shared/components/ui/button" import { Checkbox } from "@/shared/components/ui/checkbox" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog" import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" import { Textarea } from "@/shared/components/ui/textarea" import { cn } from "@/shared/lib/utils" import { Permissions } from "@/shared/types/permissions" type Role = "student" | "teacher" | "parent" | "admin" const TEACHER_SUBJECTS = ["语文", "数学", "英语", "美术", "体育", "科学", "社会", "音乐"] as const type TeacherSubject = (typeof TEACHER_SUBJECTS)[number] function isRecord(v: unknown): v is Record { return typeof v === "object" && v !== null } export function OnboardingGate() { const router = useRouter() const { status, data: session, update } = useSession() const [required, setRequired] = useState(false) const [open, setOpen] = useState(false) const [step, setStep] = useState(0) const [isSubmitting, setIsSubmitting] = useState(false) const [role, setRole] = useState("student") const [name, setName] = useState("") const [phone, setPhone] = useState("") const [address, setAddress] = useState("") const [classCodes, setClassCodes] = useState("") const [teacherSubjects, setTeacherSubjects] = useState([]) const canClose = useMemo(() => !required, [required]) useEffect(() => { if (status !== "authenticated") return let cancelled = false ;(async () => { const res = await fetch("/api/onboarding/status", { cache: "no-store" }).catch(() => null) const json = res ? await res.json().catch(() => null) : null if (cancelled) return if (isRecord(json)) { const required = Boolean(json.required) const role = String(json.role ?? "student") as Role setRequired(required) setRole(role === "admin" ? "admin" : role) setName(String(session?.user?.name ?? "").trim()) if (required) { setOpen(true) setStep(0) } } })() return () => { cancelled = true } }, [status, session?.user?.name]) useEffect(() => { if (!open) return if (!required) return setOpen(true) }, [open, required]) const title = step === 0 ? "角色选择" : step === 1 ? "通用信息" : step === 2 ? "角色信息(可跳过)" : "完成" const description = step === 0 ? "请选择你在系统中的角色" : step === 1 ? "填写姓名、电话、住址等信息" : step === 2 ? "不同角色可配置班级代码、教学科目等" : "配置完成,可以进入系统" const canNextFromStep0 = role.length > 0 const canNextFromStep1 = name.trim().length > 0 && phone.trim().length > 0 const permissions = (session?.user?.permissions ?? []) as string[] const isAdmin = permissions.includes(Permissions.SETTINGS_ADMIN) const isTeacher = permissions.includes(Permissions.EXAM_CREATE) const isStudent = permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE) const isParent = !permissions.includes(Permissions.EXAM_CREATE) && !permissions.includes(Permissions.HOMEWORK_SUBMIT) && permissions.includes(Permissions.EXAM_READ) const onNext = async () => { if (step === 0) { if (!canNextFromStep0) return setStep(1) return } if (step === 1) { if (!canNextFromStep1) { toast.error("请填写姓名与电话") return } if (isAdmin) { setStep(3) } else { setStep(2) } return } if (step === 2) { setStep(3) return } } const onBack = () => { if (step === 0) return setStep((s) => Math.max(0, s - 1)) } const toggleSubject = (subject: TeacherSubject) => { setTeacherSubjects((prev) => (prev.includes(subject) ? prev.filter((s) => s !== subject) : [...prev, subject])) } const onFinish = async () => { setIsSubmitting(true) try { const res = await fetch("/api/onboarding/complete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, name, phone, address, classCodes, teacherSubjects, }), }) const json = await res.json().catch(() => null) if (!res.ok || !isRecord(json) || json.success !== true) { const msg = isRecord(json) ? String(json.message ?? "") : "" throw new Error(msg || "提交失败") } await update?.() toast.success("配置完成") setRequired(false) setOpen(false) router.push("/dashboard") router.refresh() } catch (e) { const msg = e instanceof Error ? e.message : "提交失败" toast.error(msg) } finally { setIsSubmitting(false) } } return ( { if (canClose) setOpen(v) else setOpen(true) }} > {title} {description}
= 0 ? "bg-primary" : "bg-muted")} />
= 1 ? "bg-primary" : "bg-muted")} />
= 2 ? "bg-primary" : "bg-muted")} />
= 3 ? "bg-primary" : "bg-muted")} />
{step === 0 ? (
{isAdmin ? (
admin
) : ( )}
) : null} {step === 1 ? (
setName(e.target.value)} />
setPhone(e.target.value)} />
setAddress(e.target.value)} />
) : null} {step === 2 ? (
{isTeacher ? ( <>