feat(textbooks): 教材模块审计重构 — 跨模块解耦 + 权限 + i18n + 错误边界 + 纯函数抽取
P0 修复: - 解耦跨模块 UI 依赖:knowledge-point-dialogs 不再直接 import questions, 改为 renderQuestionCreator render prop 由页面注入 - 接入 usePermission Hook 替换 canEdit 硬编码 - 全模块 i18n 改造:新增 en/zh-CN 翻译文件,替换所有硬编码文案 - Server Action 资源归属校验:新增 verifyChapterBelongsToTextbook/ verifyKnowledgePointBelongsToTextbook,在 reorder/update/delete/create 中校验 P1 改进: - 补齐 Error Boundary:4 个 error.tsx + TextbookSectionErrorBoundary 区块包裹 - 抽取纯函数到 utils.ts/graph-layout.ts/constants.ts 并补单测(26 用例全通过) - 消除重复组件:删除 knowledge-point-panel/create-knowledge-point-dialog - 修复类型断言:chapter.children! → 守卫式访问 - 图谱 a11y:添加 role/aria-label/aria-pressed - 统一删除确认:confirm() → AlertDialog - 数据范围过滤:getTextbooksWithScope 支持学生端按年级过滤 P2 预留: - TextbookAnalytics 埋点接口 + Provider + Hook 同步 005 架构数据 JSON:补充 getTextbooksWithScope/verify*/ChapterTreeNode 等
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { PlusCircle, Pencil, Trash2 } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import type { KnowledgePoint } from "../types"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
@@ -11,6 +12,8 @@ import { Button } from "@/shared/components/ui/button"
|
||||
interface KnowledgePointListProps {
|
||||
knowledgePoints: KnowledgePoint[]
|
||||
canEdit: boolean
|
||||
/** 是否有创建题目权限(控制"创建相关题目"按钮可见性) */
|
||||
canCreateQuestion?: boolean
|
||||
highlightedKpId: string | null
|
||||
onHighlight: (id: string) => void
|
||||
onEdit: (kp: KnowledgePoint) => void
|
||||
@@ -21,16 +24,19 @@ interface KnowledgePointListProps {
|
||||
export function KnowledgePointList({
|
||||
knowledgePoints,
|
||||
canEdit,
|
||||
canCreateQuestion = false,
|
||||
highlightedKpId,
|
||||
onHighlight,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCreateQuestion,
|
||||
}: KnowledgePointListProps) {
|
||||
const t = useTranslations("textbooks")
|
||||
|
||||
if (knowledgePoints.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground p-4 text-center text-sm">
|
||||
该章节暂无知识点。
|
||||
{t("reader.emptyKnowledge")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -51,22 +57,27 @@ export function KnowledgePointList({
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h4 className="text-sm font-medium leading-none">{kp.name}</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-5 px-1">Lv.{kp.level}</Badge>
|
||||
<Badge variant="outline" className="text-[10px] h-5 px-1">
|
||||
{t("panel.level")}
|
||||
{kp.level}
|
||||
</Badge>
|
||||
{canEdit && (
|
||||
<div className="flex items-center gap-1 ml-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCreateQuestion(kp)
|
||||
}}
|
||||
title="创建相关题目"
|
||||
aria-label="创建相关题目"
|
||||
>
|
||||
<PlusCircle className="h-3 w-3" />
|
||||
</Button>
|
||||
{canCreateQuestion && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCreateQuestion(kp)
|
||||
}}
|
||||
title={t("dialog.knowledge.createQuestion")}
|
||||
aria-label={t("dialog.knowledge.createQuestion")}
|
||||
>
|
||||
<PlusCircle className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -75,8 +86,8 @@ export function KnowledgePointList({
|
||||
e.stopPropagation()
|
||||
onEdit(kp)
|
||||
}}
|
||||
title="编辑知识点"
|
||||
aria-label="编辑知识点"
|
||||
title={t("dialog.knowledge.editKp")}
|
||||
aria-label={t("dialog.knowledge.editKp")}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -85,8 +96,8 @@ export function KnowledgePointList({
|
||||
size="icon"
|
||||
className="h-5 w-5 text-muted-foreground hover:text-destructive"
|
||||
onClick={(e) => onDelete(kp.id, e)}
|
||||
title="删除知识点"
|
||||
aria-label="删除知识点"
|
||||
title={t("dialog.knowledge.deleteKp")}
|
||||
aria-label={t("dialog.knowledge.deleteKp")}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user