Files
NextEdu/src/modules/error-book/components/add-error-book-dialog.tsx
SpecialX 2dd8c2197c feat(error-book): update components, actions, data-access, and add analytics
- Add data-access-analytics for error book analytics queries

- Update actions, data-access for improved error book operations

- Update components: add-error-book-dialog, analytics-stats-cards, chapter-weakness-chart, class-error-bar-chart, class-filter, error-book-detail-dialog, error-book-filters, error-book-item-card, error-book-list, error-book-stats-cards, grouped-student-error-table, knowledge-point-weakness-chart, review-buttons, subject-distribution-chart, subject-tabs, top-wrong-questions

- Remove class-error-overview (replaced by new components)
2026-07-03 10:30:41 +08:00

170 lines
5.1 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 { useTranslations } from "next-intl"
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 { createErrorBookItemAction } from "../actions"
import { COMMON_ERROR_TAGS } from "../types"
interface QuestionOption {
id: string
preview: string
}
interface AddErrorBookDialogProps {
/** 预加载的题目选项(若提供则直接使用,不再调用 onLoadQuestions */
questionOptions?: QuestionOption[]
/** 懒加载题目选项的回调(对话框打开时调用) */
onLoadQuestions?: () => Promise<QuestionOption[]>
}
export function AddErrorBookDialog({
questionOptions: questionOptionsProp,
onLoadQuestions,
}: AddErrorBookDialogProps) {
const t = useTranslations("error-book")
const [open, setOpen] = useState(false)
const [isPending, startTransition] = useTransition()
const [questionId, setQuestionId] = useState("")
const [note, setNote] = useState("")
const [errorTags, setErrorTags] = useState<string[]>([])
const [loadedOptions, setLoadedOptions] = useState<QuestionOption[]>([])
const questionOptions = questionOptionsProp ?? loadedOptions
useEffect(() => {
if (open && onLoadQuestions && loadedOptions.length === 0 && !questionOptionsProp) {
onLoadQuestions()
.then((options) => {
setLoadedOptions(options)
})
.catch(() => {})
}
}, [open, onLoadQuestions, loadedOptions.length, questionOptionsProp])
function toggleTag(tag: string) {
setErrorTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
}
function handleSubmit() {
if (!questionId) {
toast.error(t("messages.selectQuestion"))
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 ?? t("messages.addedShort"))
setOpen(false)
setQuestionId("")
setNote("")
setErrorTags([])
} else {
toast.error(res.message ?? t("messages.addFailed"))
}
})
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4" data-icon="inline-start" />
{t("actions.add")}
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{t("addDialog.title")}</DialogTitle>
<DialogDescription>
{t("addDialog.description")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="question">{t("addDialog.selectQuestion")}</Label>
<Select value={questionId} onValueChange={setQuestionId}>
<SelectTrigger id="question">
<SelectValue placeholder={t("addDialog.selectPlaceholder")} />
</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">{t("addDialog.noteLabel")}</Label>
<Textarea
id="note"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder={t("addDialog.notePlaceholder")}
maxLength={2000}
/>
</div>
<div className="space-y-2">
<Label>{t("addDialog.errorTagsLabel")}</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)}>
{t("actions.cancel")}
</Button>
<Button disabled={isPending || !questionId} onClick={handleSubmit}>
{isPending ? t("actions.adding") : t("actions.add")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}