refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
Some checks failed
CI / build-deploy (push) Has been cancelled
Some checks failed
CI / build-deploy (push) Has been cancelled
- RBAC: 新增30个权限点、DataScope行级权限、requirePermission守卫,所有57+ Server Action接入权限校验 - UI拆分: exam-form(1623行→11文件)、textbook-reader(744行→7文件),均降至300行以内 - 测试: 新增5个单元测试文件(19用例),修复4个集成测试文件(38用例全部通过) - 架构文档: 新增架构影响地图(004/005)、标准功能清单(006)、差距审计报告(007) - 项目规则: 架构图优先规则,改码必同步图 - 安全: rehype-sanitize净化、AES加密API Key、权限路由守卫 - 无障碍: skip-link、aria-label、prefers-reduced-motion - 性能: next/font优化、next/image、代码分割
This commit is contained in:
130
src/modules/exams/components/question-options-editor.tsx
Normal file
130
src/modules/exams/components/question-options-editor.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import { Plus, Trash2 } from "lucide-react"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { Label } from "@/shared/components/ui/label"
|
||||
import { Checkbox } from "@/shared/components/ui/checkbox"
|
||||
import type { ExamNode } from "./assembly/selected-question-list"
|
||||
import type { EditableQuestionContent } from "./exam-form-types"
|
||||
|
||||
type QuestionOptionsEditorProps = {
|
||||
selectedQuestionId: string
|
||||
selectedContent: EditableQuestionContent
|
||||
questionType: string
|
||||
updatePreviewQuestionNode: (questionId: string, updater: (node: ExamNode) => ExamNode) => void
|
||||
parseEditableContent: (raw: unknown) => EditableQuestionContent
|
||||
}
|
||||
|
||||
export function QuestionOptionsEditor({
|
||||
selectedQuestionId,
|
||||
selectedContent,
|
||||
questionType,
|
||||
updatePreviewQuestionNode,
|
||||
parseEditableContent,
|
||||
}: QuestionOptionsEditorProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>选项</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
updatePreviewQuestionNode(selectedQuestionId, (node) => {
|
||||
if (!node.question) return node
|
||||
const current = parseEditableContent(node.question.content)
|
||||
const nextId = String.fromCharCode(65 + current.options.length)
|
||||
return {
|
||||
...node,
|
||||
question: {
|
||||
...node.question,
|
||||
content: {
|
||||
...current,
|
||||
options: [...current.options, { id: nextId, text: "", isCorrect: false }],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||
新增选项
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{selectedContent.options.map((option, optionIndex) => (
|
||||
<div key={`${option.id}-${optionIndex}`} className="flex items-center gap-2 rounded-md border p-2">
|
||||
<Input
|
||||
className="w-16"
|
||||
value={option.id}
|
||||
onChange={(event) => {
|
||||
const nextId = event.target.value
|
||||
updatePreviewQuestionNode(selectedQuestionId, (node) => {
|
||||
if (!node.question) return node
|
||||
const current = parseEditableContent(node.question.content)
|
||||
const options = current.options.map((item, idx) => idx === optionIndex ? { ...item, id: nextId } : item)
|
||||
return { ...node, question: { ...node.question, content: { ...current, options } } }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
className="flex-1"
|
||||
value={option.text}
|
||||
onChange={(event) => {
|
||||
const text = event.target.value
|
||||
updatePreviewQuestionNode(selectedQuestionId, (node) => {
|
||||
if (!node.question) return node
|
||||
const current = parseEditableContent(node.question.content)
|
||||
const options = current.options.map((item, idx) => idx === optionIndex ? { ...item, text } : item)
|
||||
return { ...node, question: { ...node.question, content: { ...current, options } } }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<Checkbox
|
||||
aria-label={`标记选项 ${option.id} 为正确答案`}
|
||||
checked={option.isCorrect}
|
||||
onCheckedChange={(checked) => {
|
||||
const isCorrect = Boolean(checked)
|
||||
updatePreviewQuestionNode(selectedQuestionId, (node) => {
|
||||
if (!node.question) return node
|
||||
const current = parseEditableContent(node.question.content)
|
||||
const options = current.options.map((item, idx) => {
|
||||
if (idx !== optionIndex) {
|
||||
if (questionType === "single_choice") {
|
||||
return { ...item, isCorrect: false }
|
||||
}
|
||||
return item
|
||||
}
|
||||
return { ...item, isCorrect }
|
||||
})
|
||||
return { ...node, question: { ...node.question, content: { ...current, options } } }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">正确</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="删除选项"
|
||||
onClick={() => {
|
||||
updatePreviewQuestionNode(selectedQuestionId, (node) => {
|
||||
if (!node.question) return node
|
||||
const current = parseEditableContent(node.question.content)
|
||||
const options = current.options.filter((_, idx) => idx !== optionIndex)
|
||||
return { ...node, question: { ...node.question, content: { ...current, options } } }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user