refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
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:
SpecialX
2026-06-16 23:38:33 +08:00
parent 99f116cb64
commit 125f7ec54c
75 changed files with 9480 additions and 3289 deletions

View 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>
)
}