Files
NextEdu/src/modules/error-book/components/add-error-book-dialog.tsx
SpecialX bf056399c6 feat(error-book): implement error book module with SM2 spaced repetition
- Add SM2 algorithm implementation with tests for spaced repetition review scheduling

- Add data-access, schema, types, and server actions for error book CRUD

- Add components: add dialog, class overview, filters, item card, stats cards, review buttons, top wrong questions

- Add error-book routes for admin, teacher, parent, and student roles

- Add i18n messages (en, zh-CN) for error book module
2026-06-23 17:36:42 +08:00

178 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState, useTransition, useEffect } from "react"
import { Plus } from "lucide-react"
import { toast } from "sonner"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/shared/components/ui/dialog"
import { Button } from "@/shared/components/ui/button"
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 { getQuestionsAction } from "@/modules/questions/actions"
import { createErrorBookItemAction } from "../actions"
import { COMMON_ERROR_TAGS } from "../types"
export function AddErrorBookDialog() {
const [open, setOpen] = useState(false)
const [isPending, startTransition] = useTransition()
const [questionId, setQuestionId] = useState("")
const [note, setNote] = useState("")
const [errorTags, setErrorTags] = useState<string[]>([])
const [questionOptions, setQuestionOptions] = useState<Array<{
id: string
preview: string
}>>([])
function extractPreview(content: unknown): string {
if (typeof content === "string") return content.slice(0, 60)
if (Array.isArray(content)) {
const texts: string[] = []
for (const node of content) {
if (typeof node === "string") texts.push(node)
else if (typeof node === "object" && node !== null) {
const n = node as Record<string, unknown>
if (typeof n.text === "string") texts.push(n.text)
}
}
return texts.join("").slice(0, 60)
}
return "题目"
}
useEffect(() => {
if (open && questionOptions.length === 0) {
getQuestionsAction({ pageSize: 100 })
.then((res) => {
if (res.success && res.data) {
setQuestionOptions(
res.data.data.map((q) => ({
id: q.id,
preview: extractPreview(q.content),
}))
)
}
})
.catch(() => {})
}
}, [open, questionOptions.length])
function toggleTag(tag: string) {
setErrorTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
}
function handleSubmit() {
if (!questionId) {
toast.error("请选择题目")
return
}
startTransition(async () => {
const formData = new FormData()
formData.append(
"json",
JSON.stringify({ questionId, note, errorTags })
)
const res = await createErrorBookItemAction(undefined, formData)
if (res.success) {
toast.success(res.message ?? "已添加")
setOpen(false)
setQuestionId("")
setNote("")
setErrorTags([])
} else {
toast.error(res.message ?? "添加失败")
}
})
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4" data-icon="inline-start" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
/
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="question"></Label>
<Select value={questionId} onValueChange={setQuestionId}>
<SelectTrigger id="question">
<SelectValue placeholder="从题库中选择..." />
</SelectTrigger>
<SelectContent>
{questionOptions.map((q) => (
<SelectItem key={q.id} value={q.id}>
{q.preview}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="note"></Label>
<Textarea
id="note"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="记录错误原因、解题思路..."
maxLength={2000}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-1">
{COMMON_ERROR_TAGS.map((tag) => (
<Button
key={tag}
type="button"
variant={errorTags.includes(tag) ? "default" : "outline"}
size="sm"
onClick={() => toggleTag(tag)}
>
{tag}
</Button>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
</Button>
<Button disabled={isPending || !questionId} onClick={handleSubmit}>
{isPending ? "添加中..." : "添加"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}