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,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import type { KnowledgePoint } from "../types"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
@@ -13,9 +15,20 @@ import {
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { Label } from "@/shared/components/ui/label"
|
||||
import { Textarea } from "@/shared/components/ui/textarea"
|
||||
import { CreateQuestionDialog } from "@/modules/questions/components/create-question-dialog"
|
||||
|
||||
interface KnowledgePointDialogsProps {
|
||||
/**
|
||||
* 题目创建器的渲染接口。
|
||||
*
|
||||
* 通过 render prop 注入,避免 textbooks 模块直接 import questions 模块组件(P0-1 解耦)。
|
||||
* 页面层负责传入实际的 CreateQuestionDialog 实现。
|
||||
*/
|
||||
export interface QuestionCreatorRenderProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
targetKp: KnowledgePoint | null
|
||||
}
|
||||
|
||||
export interface KnowledgePointDialogsProps {
|
||||
// Create KP dialog
|
||||
createDialogOpen: boolean
|
||||
setCreateDialogOpen: (open: boolean) => void
|
||||
@@ -30,10 +43,16 @@ interface KnowledgePointDialogsProps {
|
||||
isUpdatingKp: boolean
|
||||
onUpdateKnowledgePoint: (formData: FormData) => Promise<void>
|
||||
|
||||
// Question dialog
|
||||
// Question dialog(通过 render prop 注入,解耦 questions 模块)
|
||||
questionDialogOpen: boolean
|
||||
setQuestionDialogOpen: (open: boolean) => void
|
||||
targetKpForQuestion: KnowledgePoint | null
|
||||
/**
|
||||
* 题目创建器渲染函数。
|
||||
* 由页面层注入实际的 questions 模块组件,模块内部不直接 import questions。
|
||||
* 若不传则不渲染题目创建入口(学生端等无权限场景)。
|
||||
*/
|
||||
renderQuestionCreator?: (props: QuestionCreatorRenderProps) => ReactNode
|
||||
}
|
||||
|
||||
export function KnowledgePointDialogs({
|
||||
@@ -50,34 +69,35 @@ export function KnowledgePointDialogs({
|
||||
questionDialogOpen,
|
||||
setQuestionDialogOpen,
|
||||
targetKpForQuestion,
|
||||
renderQuestionCreator,
|
||||
}: KnowledgePointDialogsProps) {
|
||||
const t = useTranslations("textbooks.dialog.knowledge")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加知识点</DialogTitle>
|
||||
<DialogDescription>
|
||||
从选中的文本创建知识点。
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("createTitle")}</DialogTitle>
|
||||
<DialogDescription>{t("createDesc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action={onCreateKnowledgePoint as (formData: FormData) => void}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">名称</Label>
|
||||
<Label htmlFor="name">{t("name")}</Label>
|
||||
<Input id="name" name="name" defaultValue={selectedText} required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">描述(可选)</Label>
|
||||
<Textarea id="description" name="description" placeholder="请输入描述..." />
|
||||
<Label htmlFor="description">{t("description")}</Label>
|
||||
<Textarea id="description" name="description" placeholder={t("descriptionPlaceholder")} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setCreateDialogOpen(false)} disabled={isCreating}>
|
||||
取消
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isCreating}>
|
||||
{isCreating ? "创建中..." : "创建"}
|
||||
{isCreating ? t("creating") : t("create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -87,26 +107,32 @@ export function KnowledgePointDialogs({
|
||||
<Dialog open={editKpDialogOpen} onOpenChange={setEditKpDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑知识点</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改知识点的名称和描述。
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("editTitle")}</DialogTitle>
|
||||
<DialogDescription>{t("editDesc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action={onUpdateKnowledgePoint}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">显示名称</Label>
|
||||
<Label htmlFor="edit-name">{t("displayName")}</Label>
|
||||
<Input id="edit-name" name="name" defaultValue={editingKp?.name} required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-description">描述(可选)</Label>
|
||||
<Textarea id="edit-description" name="description" defaultValue={editingKp?.description || ""} placeholder="请输入描述..." />
|
||||
<Label htmlFor="edit-description">{t("description")}</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
name="description"
|
||||
defaultValue={editingKp?.description || ""}
|
||||
placeholder={t("descriptionPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 border rounded-md p-3 bg-muted/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="edit-anchorText" className="text-muted-foreground text-xs flex items-center gap-1">
|
||||
高级:关联文本 (影响文中高亮)
|
||||
<Label
|
||||
htmlFor="edit-anchorText"
|
||||
className="text-muted-foreground text-xs flex items-center gap-1"
|
||||
>
|
||||
{t("anchorText")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
@@ -118,31 +144,32 @@ export function KnowledgePointDialogs({
|
||||
className="text-sm font-mono"
|
||||
required
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
修改此字段会改变文中被高亮匹配的文字。通常保持与原文一致。
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">{t("anchorTextHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setEditKpDialogOpen(false)} disabled={isUpdatingKp}>
|
||||
取消
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setEditKpDialogOpen(false)}
|
||||
disabled={isUpdatingKp}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isUpdatingKp}>
|
||||
{isUpdatingKp ? "保存中..." : "保存"}
|
||||
{isUpdatingKp ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<CreateQuestionDialog
|
||||
open={questionDialogOpen}
|
||||
onOpenChange={setQuestionDialogOpen}
|
||||
defaultKnowledgePointIds={targetKpForQuestion ? [targetKpForQuestion.id] : []}
|
||||
defaultContent={targetKpForQuestion ? `Please explain the knowledge point: ${targetKpForQuestion.name}` : ""}
|
||||
defaultType="text"
|
||||
/>
|
||||
{renderQuestionCreator?.({
|
||||
open: questionDialogOpen,
|
||||
onOpenChange: setQuestionDialogOpen,
|
||||
targetKp: targetKpForQuestion,
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user