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)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useTransition, useEffect } from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,53 +24,44 @@ import {
|
||||
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() {
|
||||
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 [questionOptions, setQuestionOptions] = useState<Array<{
|
||||
id: string
|
||||
preview: string
|
||||
}>>([])
|
||||
const [loadedOptions, setLoadedOptions] = useState<QuestionOption[]>([])
|
||||
|
||||
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 "题目"
|
||||
}
|
||||
const questionOptions = questionOptionsProp ?? loadedOptions
|
||||
|
||||
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),
|
||||
}))
|
||||
)
|
||||
}
|
||||
if (open && onLoadQuestions && loadedOptions.length === 0 && !questionOptionsProp) {
|
||||
onLoadQuestions()
|
||||
.then((options) => {
|
||||
setLoadedOptions(options)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}, [open, questionOptions.length])
|
||||
}, [open, onLoadQuestions, loadedOptions.length, questionOptionsProp])
|
||||
|
||||
function toggleTag(tag: string) {
|
||||
setErrorTags((prev) =>
|
||||
@@ -79,7 +71,7 @@ export function AddErrorBookDialog() {
|
||||
|
||||
function handleSubmit() {
|
||||
if (!questionId) {
|
||||
toast.error("请选择题目")
|
||||
toast.error(t("messages.selectQuestion"))
|
||||
return
|
||||
}
|
||||
startTransition(async () => {
|
||||
@@ -90,13 +82,13 @@ export function AddErrorBookDialog() {
|
||||
)
|
||||
const res = await createErrorBookItemAction(undefined, formData)
|
||||
if (res.success) {
|
||||
toast.success(res.message ?? "已添加")
|
||||
toast.success(res.message ?? t("messages.addedShort"))
|
||||
setOpen(false)
|
||||
setQuestionId("")
|
||||
setNote("")
|
||||
setErrorTags([])
|
||||
} else {
|
||||
toast.error(res.message ?? "添加失败")
|
||||
toast.error(res.message ?? t("messages.addFailed"))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -106,23 +98,23 @@ export function AddErrorBookDialog() {
|
||||
<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>添加错题</DialogTitle>
|
||||
<DialogTitle>{t("addDialog.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
从题库中选择题目,添加到你的错题本。你也可以在完成作业/考试后自动采集。
|
||||
{t("addDialog.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="question">选择题目</Label>
|
||||
<Label htmlFor="question">{t("addDialog.selectQuestion")}</Label>
|
||||
<Select value={questionId} onValueChange={setQuestionId}>
|
||||
<SelectTrigger id="question">
|
||||
<SelectValue placeholder="从题库中选择..." />
|
||||
<SelectValue placeholder={t("addDialog.selectPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{questionOptions.map((q) => (
|
||||
@@ -135,18 +127,18 @@ export function AddErrorBookDialog() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="note">学习笔记(可选)</Label>
|
||||
<Label htmlFor="note">{t("addDialog.noteLabel")}</Label>
|
||||
<Textarea
|
||||
id="note"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="记录错误原因、解题思路..."
|
||||
placeholder={t("addDialog.notePlaceholder")}
|
||||
maxLength={2000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>错误原因标签</Label>
|
||||
<Label>{t("addDialog.errorTagsLabel")}</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{COMMON_ERROR_TAGS.map((tag) => (
|
||||
<Button
|
||||
@@ -165,10 +157,10 @@ export function AddErrorBookDialog() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
取消
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
<Button disabled={isPending || !questionId} onClick={handleSubmit}>
|
||||
{isPending ? "添加中..." : "添加"}
|
||||
{isPending ? t("actions.adding") : t("actions.add")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user