- 新建 KnowledgeCardFields 组件:基础版/深度版编辑、字符计数器、来源参考、预览面板 - 提取 QuestionForm 中内联的知识卡字段为独立组件 - 新增 sourceRef 字段到类型定义和表单 schema
246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
import { useEffect, useState } from "react"
|
|
import { useForm } from "react-hook-form"
|
|
import { z } from "zod/v4"
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
import { useNavigate } from "react-router"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { DistractorEditor } from "@/components/question/DistractorEditor"
|
|
import { KnowledgeCardFields } from "@/components/question/KnowledgeCardFields"
|
|
import { fetchCategories } from "@/lib/api/category-api"
|
|
import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants"
|
|
import type { Question, Difficulty, QuestionStatus } from "@/types/question"
|
|
import type { Category } from "@/types/category"
|
|
|
|
const questionSchema = z.object({
|
|
stem: z.string().min(1, "请输入题干").max(500),
|
|
correctAnswer: z.string().min(1, "请输入正确答案"),
|
|
distractors: z
|
|
.array(z.string().min(1, "干扰项不能为空"))
|
|
.min(4, "至少 4 个干扰项")
|
|
.max(6, "最多 6 个干扰项"),
|
|
categoryId: z.string().min(1, "请选择分类"),
|
|
difficulty: z.number().min(1).max(5),
|
|
status: z.enum(["draft", "reviewing", "published", "archived"]),
|
|
knowledgeCardBasic: z.string().min(1, "请填写基础知识卡").max(100),
|
|
knowledgeCardDeep: z.string().max(300).optional(),
|
|
sourceRef: z.string().max(500).optional(),
|
|
})
|
|
|
|
type FormValues = z.infer<typeof questionSchema>
|
|
|
|
interface QuestionFormProps {
|
|
question?: Question
|
|
}
|
|
|
|
export function QuestionForm({ question }: QuestionFormProps) {
|
|
const navigate = useNavigate()
|
|
const isEditing = !!question
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
setValue,
|
|
watch,
|
|
} = useForm<FormValues>({
|
|
resolver: zodResolver(questionSchema),
|
|
defaultValues: question
|
|
? {
|
|
stem: question.stem,
|
|
correctAnswer: question.correctAnswer,
|
|
distractors: question.distractors,
|
|
categoryId: question.categoryId,
|
|
difficulty: question.difficulty,
|
|
status: question.status,
|
|
knowledgeCardBasic: question.knowledgeCardBasic,
|
|
knowledgeCardDeep: question.knowledgeCardDeep ?? "",
|
|
sourceRef: question.sourceRef ?? "",
|
|
}
|
|
: {
|
|
stem: "",
|
|
correctAnswer: "",
|
|
distractors: ["", "", "", ""],
|
|
categoryId: "",
|
|
difficulty: 3,
|
|
status: "draft",
|
|
knowledgeCardBasic: "",
|
|
knowledgeCardDeep: "",
|
|
sourceRef: "",
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data))
|
|
}, [])
|
|
|
|
async function onSubmit(data: FormValues) {
|
|
setSubmitting(true)
|
|
try {
|
|
// TODO: 接入 API
|
|
console.log("submit", isEditing ? "update" : "create", data)
|
|
navigate("/questions")
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const distractorsValue = watch("distractors")
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl space-y-6">
|
|
{/* 题干 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="stem">题干</Label>
|
|
<Textarea
|
|
id="stem"
|
|
placeholder="输入题目文字"
|
|
rows={3}
|
|
{...register("stem")}
|
|
/>
|
|
{errors.stem && (
|
|
<p className="text-sm text-destructive">{errors.stem.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 正确答案 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="correctAnswer">正确答案</Label>
|
|
<Input
|
|
id="correctAnswer"
|
|
placeholder="正确选项文本"
|
|
{...register("correctAnswer")}
|
|
/>
|
|
{errors.correctAnswer && (
|
|
<p className="text-sm text-destructive">
|
|
{errors.correctAnswer.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 干扰项 */}
|
|
<DistractorEditor
|
|
value={distractorsValue}
|
|
onChange={(items) =>
|
|
setValue("distractors", items, { shouldValidate: true })
|
|
}
|
|
errors={
|
|
errors.distractors?.message ? [errors.distractors.message] : undefined
|
|
}
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
{/* 分类 + 难度 + 状态 */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>分类</Label>
|
|
<Select
|
|
value={watch("categoryId")}
|
|
onValueChange={(val) => setValue("categoryId", val)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="选择分类" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categories.map((cat) => (
|
|
<SelectItem key={cat.id} value={cat.id}>
|
|
{cat.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.categoryId && (
|
|
<p className="text-sm text-destructive">
|
|
{errors.categoryId.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>难度</Label>
|
|
<Select
|
|
value={String(watch("difficulty"))}
|
|
onValueChange={(val) =>
|
|
setValue("difficulty", Number(val) as Difficulty)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(DIFFICULTY_LABELS).map(([value, label]) => (
|
|
<SelectItem key={value} value={value}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>状态</Label>
|
|
<Select
|
|
value={watch("status")}
|
|
onValueChange={(val) => setValue("status", val as QuestionStatus)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(QUESTION_STATUSES).map(([value, label]) => (
|
|
<SelectItem key={value} value={value}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 知识卡 */}
|
|
<KnowledgeCardFields
|
|
basicRegister={register("knowledgeCardBasic")}
|
|
deepRegister={register("knowledgeCardDeep")}
|
|
sourceRefRegister={register("sourceRef")}
|
|
basicError={errors.knowledgeCardBasic?.message}
|
|
deepError={errors.knowledgeCardDeep?.message}
|
|
watchBasic={watch("knowledgeCardBasic") ?? ""}
|
|
watchDeep={watch("knowledgeCardDeep") ?? ""}
|
|
/>
|
|
|
|
{/* 提交 */}
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => navigate("/questions")}
|
|
>
|
|
取消
|
|
</Button>
|
|
<Button type="submit" disabled={submitting}>
|
|
{submitting
|
|
? "保存中..."
|
|
: isEditing
|
|
? "更新题目"
|
|
: "创建题目"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|