工作内容 - 新增 /teacher/questions 页面,支持 Suspense/空状态/加载态 - 题库 CRUD Server Actions:创建/更新/递归删除子题,变更后 revalidatePath - getQuestions 支持 q/type/difficulty/knowledgePointId 筛选与分页返回 meta - UI:表格列/筛选器/创建编辑弹窗,content JSON 兼容组卷 - 更新中文设计文档:docs/design/004_question_bank_implementation.md
163 lines
4.0 KiB
TypeScript
163 lines
4.0 KiB
TypeScript
"use client"
|
|
|
|
import { ColumnDef } from "@tanstack/react-table"
|
|
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { Checkbox } from "@/shared/components/ui/checkbox"
|
|
import { Question, QuestionType } from "../types"
|
|
import { QuestionActions } from "./question-actions"
|
|
|
|
const getTypeColor = (type: QuestionType) => {
|
|
switch (type) {
|
|
case "single_choice":
|
|
return "default"
|
|
case "multiple_choice":
|
|
return "secondary"
|
|
case "judgment":
|
|
return "outline"
|
|
case "text":
|
|
return "secondary"
|
|
default:
|
|
return "secondary"
|
|
}
|
|
}
|
|
|
|
const getTypeLabel = (type: QuestionType) => {
|
|
switch (type) {
|
|
case "single_choice":
|
|
return "Single Choice"
|
|
case "multiple_choice":
|
|
return "Multiple Choice"
|
|
case "judgment":
|
|
return "True/False"
|
|
case "text":
|
|
return "Short Answer"
|
|
case "composite":
|
|
return "Composite"
|
|
default:
|
|
return type
|
|
}
|
|
}
|
|
|
|
export const columns: ColumnDef<Question>[] = [
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => (
|
|
<Checkbox
|
|
checked={table.getIsAllPageRowsSelected()}
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
aria-label="Select all"
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
aria-label="Select row"
|
|
/>
|
|
),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "type",
|
|
header: "Type",
|
|
cell: ({ row }) => {
|
|
const type = row.getValue("type") as QuestionType
|
|
return (
|
|
<Badge variant={getTypeColor(type)} className="whitespace-nowrap">
|
|
{getTypeLabel(type)}
|
|
</Badge>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "content",
|
|
header: "Content",
|
|
cell: ({ row }) => {
|
|
const content = row.getValue("content") as unknown
|
|
let preview = ""
|
|
if (typeof content === "string") {
|
|
preview = content
|
|
} else if (content && typeof content === "object") {
|
|
const text = (content as { text?: unknown }).text
|
|
if (typeof text === "string") {
|
|
preview = text
|
|
} else {
|
|
preview = JSON.stringify(content)
|
|
}
|
|
}
|
|
preview = preview.slice(0, 80)
|
|
|
|
return (
|
|
<div className="max-w-[400px] truncate font-medium" title={preview}>
|
|
{preview}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "difficulty",
|
|
header: "Difficulty",
|
|
cell: ({ row }) => {
|
|
const diff = row.getValue("difficulty") as number
|
|
const label =
|
|
diff === 1
|
|
? "Easy"
|
|
: diff === 2
|
|
? "Easy-Med"
|
|
: diff === 3
|
|
? "Medium"
|
|
: diff === 4
|
|
? "Med-Hard"
|
|
: "Hard"
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline" className="tabular-nums">
|
|
{label}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground tabular-nums">({diff})</span>
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "knowledgePoints",
|
|
header: "Knowledge Points",
|
|
cell: ({ row }) => {
|
|
const kps = row.original.knowledgePoints
|
|
if (!kps || kps.length === 0) return <span className="text-muted-foreground">-</span>
|
|
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{kps.slice(0, 2).map((kp) => (
|
|
<Badge key={kp.id} variant="outline" className="text-xs">
|
|
{kp.name}
|
|
</Badge>
|
|
))}
|
|
{kps.length > 2 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
+{kps.length - 2}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: "Created",
|
|
cell: ({ row }) => {
|
|
return (
|
|
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
|
{new Date(row.getValue("createdAt")).toLocaleDateString()}
|
|
</span>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => <QuestionActions question={row.original} />,
|
|
},
|
|
]
|