- 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)
170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
"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>
|
||
)
|
||
}
|