Files
NextEdu/src/modules/homework/components/homework-assignment-form.tsx
SpecialX 4f0ef217a0 refactor(modules): update existing module implementations across attendance, audit, auth, classes, course-plans, exams, files, homework, layout, proctoring, questions, scheduling, textbooks, users
- Update attendance components and data-access for record management

- Update audit log views, filters, and data-access

- Update auth login and register forms

- Update classes actions, components, and data-access (admin, schedule, stats)

- Update course-plans actions, form, list, progress, and schema

- Update exams actions, AI pipeline, preview components, and hooks

- Update files components (icon, list, preview, upload) and data-access

- Update homework assignment form, review view, auto-save hook, and stats-service

- Update layout sidebar, header, and navigation config

- Update proctoring actions, anti-cheat monitor, and data-access

- Update questions actions, components (dialog, actions, columns, filters), and data-access

- Update scheduling actions, auto-scheduler, components, and schema

- Update textbooks constants and text-selection hook

- Update users class-registration, import-dialog, data-access, and user-service
2026-06-23 17:38:56 +08:00

241 lines
9.3 KiB
TypeScript

"use client"
import { useMemo, useState } from "react"
import { useFormStatus } from "react-dom"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { useSearchParams } from "next/navigation"
import { useTranslations } from "next-intl"
import { FileText, FileQuestion } from "lucide-react"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Button } from "@/shared/components/ui/button"
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 { createHomeworkAssignmentAction } from "../actions"
import type { TeacherClass } from "@/modules/classes/types"
type ExamOption = { id: string; title: string }
function SubmitButton() {
const { pending } = useFormStatus()
const t = useTranslations("examHomework")
return (
<Button type="submit" disabled={pending}>
{pending ? t("homework.form.submitting") : t("homework.form.submit")}
</Button>
)
}
export function HomeworkAssignmentForm({ exams, classes }: { exams: ExamOption[]; classes: TeacherClass[] }) {
const router = useRouter()
const searchParams = useSearchParams()
const t = useTranslations("examHomework")
const [mode, setMode] = useState<"exam" | "quick">(exams.length > 0 ? "exam" : "quick")
const initialExamId = useMemo(() => exams[0]?.id ?? "", [exams])
const [examId, setExamId] = useState<string>(initialExamId)
const initialClassId = useMemo(() => {
const fromQuery = searchParams.get("classId") || ""
if (fromQuery && classes.some((c) => c.id === fromQuery)) return fromQuery
return classes[0]?.id ?? ""
}, [classes, searchParams])
const [classId, setClassId] = useState<string>(initialClassId)
const [allowLate, setAllowLate] = useState<boolean>(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (formData: FormData) => {
if (mode === "exam" && !examId) {
toast.error(t("homework.form.selectExamRequired"))
return
}
if (mode === "quick" && !formData.get("title")) {
toast.error(t("homework.form.titleRequired"))
return
}
if (!classId) {
toast.error(t("homework.form.selectClassRequired"))
return
}
if (mode === "exam") {
formData.set("sourceExamId", examId)
} else {
formData.delete("sourceExamId")
}
formData.set("classId", classId)
formData.set("allowLate", allowLate ? "true" : "false")
formData.set("publish", "true")
setIsSubmitting(true)
try {
const result = await createHomeworkAssignmentAction(null, formData)
if (result.success) {
toast.success(result.message)
router.push("/teacher/homework/assignments")
} else {
toast.error(result.message || t("homework.form.createFailed"))
}
} catch {
toast.error(t("homework.form.createFailed"))
} finally {
setIsSubmitting(false)
}
}
return (
<Card className="relative">
{isSubmitting && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/60 backdrop-blur-sm">
<div className="flex items-center gap-2 text-sm font-medium">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
{t("homework.form.creating")}
</div>
</div>
)}
<CardHeader>
<CardTitle>{t("homework.form.createTitle")}</CardTitle>
</CardHeader>
<CardContent>
<form action={handleSubmit} className="space-y-6">
{/* 模式切换 */}
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setMode("quick")}
className={cn(
"flex items-center gap-2 rounded-md border p-3 text-left transition-colors",
mode === "quick"
? "border-primary bg-primary/5 text-primary"
: "border-border hover:bg-muted/50"
)}
>
<FileText className="h-4 w-4 shrink-0" aria-hidden="true" />
<div>
<div className="text-sm font-medium">{t("homework.form.quickMode")}</div>
<div className="text-xs text-muted-foreground">{t("homework.form.quickModeDescription")}</div>
</div>
</button>
<button
type="button"
onClick={() => setMode("exam")}
disabled={exams.length === 0}
className={cn(
"flex items-center gap-2 rounded-md border p-3 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
mode === "exam"
? "border-primary bg-primary/5 text-primary"
: "border-border hover:bg-muted/50"
)}
>
<FileQuestion className="h-4 w-4 shrink-0" aria-hidden="true" />
<div>
<div className="text-sm font-medium">{t("homework.form.examMode")}</div>
<div className="text-xs text-muted-foreground">{t("homework.form.examModeDescription")}</div>
</div>
</button>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2 md:col-span-2">
<Label>{t("homework.form.class")}</Label>
<Select value={classId} onValueChange={setClassId}>
<SelectTrigger>
<SelectValue placeholder={t("homework.form.selectClass")} />
</SelectTrigger>
<SelectContent>
{classes.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input type="hidden" name="classId" value={classId} />
</div>
{mode === "exam" && (
<div className="grid gap-2 md:col-span-2">
<Label>{t("homework.form.sourceExam")}</Label>
<Select value={examId} onValueChange={setExamId}>
<SelectTrigger>
<SelectValue placeholder={t("homework.form.selectExam")} />
</SelectTrigger>
<SelectContent>
{exams.map((e) => (
<SelectItem key={e.id} value={e.id}>
{e.title}
</SelectItem>
))}
</SelectContent>
</Select>
<input type="hidden" name="sourceExamId" value={examId} />
</div>
)}
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="title">
{t("homework.form.assignmentTitle")} {mode === "quick" && <span className="text-destructive">*</span>}
</Label>
<Input
id="title"
name="title"
placeholder={mode === "exam" ? t("homework.form.titlePlaceholderExam") : t("homework.form.titlePlaceholderQuick")}
required={mode === "quick"}
/>
</div>
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="description">{t("homework.form.description")}</Label>
<Textarea
id="description"
name="description"
className="min-h-[80px]"
placeholder={mode === "quick" ? t("homework.form.descriptionPlaceholderQuick") : undefined}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="availableAt">{t("homework.form.availableAt")}</Label>
<Input id="availableAt" name="availableAt" type="datetime-local" />
</div>
<div className="grid gap-2">
<Label htmlFor="dueAt">{t("homework.form.dueAt")}</Label>
<Input id="dueAt" name="dueAt" type="datetime-local" />
</div>
<div className="flex items-center gap-2 md:col-span-2">
<input
id="allowLate"
type="checkbox"
checked={allowLate}
onChange={(e) => setAllowLate(e.target.checked)}
/>
<Label htmlFor="allowLate">{t("homework.form.allowLate")}</Label>
<input type="hidden" name="allowLate" value={allowLate ? "true" : "false"} />
</div>
<div className="grid gap-2">
<Label htmlFor="lateDueAt">{t("homework.form.lateDueAt")}</Label>
<Input id="lateDueAt" name="lateDueAt" type="datetime-local" />
</div>
<div className="grid gap-2">
<Label htmlFor="maxAttempts">{t("homework.form.maxAttempts")}</Label>
<Input id="maxAttempts" name="maxAttempts" type="number" min={1} max={20} defaultValue={1} />
</div>
</div>
<CardFooter className="justify-end">
<SubmitButton />
</CardFooter>
</form>
</CardContent>
</Card>
)
}