fix(textbooks): 规范核查修复 — 安全漏洞+功能缺失+i18n+类型安全
安全:createPrerequisiteAction 补充 prerequisiteKpId 归属校验;deletePrerequisiteAction 补充双知识点归属校验,防止跨教材越权。 功能:实现图谱添加/删除前置依赖(Dialog + Select 选择知识点 + 调用 Server Action + 自动刷新图谱),替换原 no-op 回调。 i18n:修复 8 处硬编码英文字符串(textbook-reader/chapter-sidebar-list/textbook-card/textbook-form-dialog/textbook-settings-dialog/create-chapter-dialog/teacher-textbook-reader),新增 saveFailed/createFailed/updateFailed/deleteFailed/questionCreatorDefaultContent 等 key。 类型安全:graph-prerequisite-edge.tsx 使用 GraphEdgeData 类型经 unknown 安全转换,替代裸 as 断言。 规范:analytics.tsx 移动 use client 指令到文件第一行;同步架构文档 005 JSON 类型定义(GraphNodeData/GraphEdgeData/MasteryLevel)。 验证:教材模块 lint 零错误、tsc 零错误、193 个单元测试全部通过。
This commit is contained in:
@@ -14,13 +14,34 @@ import {
|
||||
} from "@xyflow/react"
|
||||
import "@xyflow/react/dist/style.css"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { toast } from "sonner"
|
||||
import { Share2 } from "lucide-react"
|
||||
import { usePermission } from "@/shared/hooks/use-permission"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import type { GraphViewMode, GraphNodeData } from "../types"
|
||||
import { computeGraphLayout } from "../graph-layout"
|
||||
import { useGraphData } from "../hooks/use-graph-data"
|
||||
import {
|
||||
createPrerequisiteAction,
|
||||
deletePrerequisiteAction,
|
||||
} from "../actions"
|
||||
import { GraphKpNode } from "./graph-kp-node"
|
||||
import { GraphPrerequisiteEdge } from "./graph-prerequisite-edge"
|
||||
import { GraphToolbar } from "./graph-toolbar"
|
||||
@@ -51,8 +72,12 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure" }: Know
|
||||
const [viewMode, setViewMode] = useState<GraphViewMode>(initialViewMode)
|
||||
const [searchText, setSearchText] = useState("")
|
||||
const [selectedKpId, setSelectedKpId] = useState<string | null>(null)
|
||||
// 添加前置依赖对话框状态
|
||||
const [addPrereqOpen, setAddPrereqOpen] = useState(false)
|
||||
const [newPrereqId, setNewPrereqId] = useState<string>("")
|
||||
const [isSavingPrereq, setIsSavingPrereq] = useState(false)
|
||||
|
||||
const { data, isLoading, error } = useGraphData(textbookId, viewMode)
|
||||
const { data, isLoading, error, reload } = useGraphData(textbookId, viewMode)
|
||||
|
||||
const availableViewModes: GraphViewMode[] = isTeacher
|
||||
? ["structure", "class-mastery"]
|
||||
@@ -173,6 +198,51 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure" }: Know
|
||||
reactFlow.fitView({ nodes: [{ id: kpId }], duration: 300 })
|
||||
}, [reactFlow])
|
||||
|
||||
// 添加前置依赖
|
||||
const handleAddPrerequisite = useCallback(async () => {
|
||||
if (!selectedKpId || !newPrereqId || !textbookId) return
|
||||
setIsSavingPrereq(true)
|
||||
const formData = new FormData()
|
||||
formData.set("knowledgePointId", selectedKpId)
|
||||
formData.set("prerequisiteKpId", newPrereqId)
|
||||
formData.set("textbookId", textbookId)
|
||||
const result = await createPrerequisiteAction(formData)
|
||||
setIsSavingPrereq(false)
|
||||
if (result.success) {
|
||||
toast.success(t("graph.detail.prerequisiteAdded"))
|
||||
setAddPrereqOpen(false)
|
||||
setNewPrereqId("")
|
||||
reload()
|
||||
} else {
|
||||
toast.error(result.message)
|
||||
}
|
||||
}, [selectedKpId, newPrereqId, textbookId, t, reload])
|
||||
|
||||
// 删除前置依赖
|
||||
const handleRemovePrerequisite = useCallback(async (prereqId: string) => {
|
||||
if (!selectedKpId || !textbookId) return
|
||||
const formData = new FormData()
|
||||
formData.set("knowledgePointId", selectedKpId)
|
||||
formData.set("prerequisiteKpId", prereqId)
|
||||
formData.set("textbookId", textbookId)
|
||||
const result = await deletePrerequisiteAction(formData)
|
||||
if (result.success) {
|
||||
toast.success(t("graph.detail.prerequisiteRemoved"))
|
||||
reload()
|
||||
} else {
|
||||
toast.error(result.message)
|
||||
}
|
||||
}, [selectedKpId, textbookId, t, reload])
|
||||
|
||||
// 可选的前置知识点(排除自身和已是前置的)
|
||||
const availablePrereqs = useMemo(() => {
|
||||
if (!data || !selectedKpId) return []
|
||||
const existing = new Set(data.knowledgePoints.find((kp) => kp.id === selectedKpId)?.prerequisiteIds ?? [])
|
||||
return data.knowledgePoints.filter((kp) =>
|
||||
kp.id !== selectedKpId && !existing.has(kp.id),
|
||||
)
|
||||
}, [data, selectedKpId])
|
||||
|
||||
if (isLoading && !data) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
|
||||
@@ -255,15 +325,44 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure" }: Know
|
||||
textbookId={textbookId}
|
||||
onClose={() => setSelectedKpId(null)}
|
||||
onJumpToKp={onJumpToKp}
|
||||
onAddPrerequisite={() => {
|
||||
// 后续迭代:打开添加前置对话框
|
||||
}}
|
||||
onRemovePrerequisite={(_prereqId: string) => {
|
||||
// 后续迭代:调用 deletePrerequisiteAction
|
||||
}}
|
||||
onAddPrerequisite={() => setAddPrereqOpen(true)}
|
||||
onRemovePrerequisite={handleRemovePrerequisite}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 添加前置依赖对话框 */}
|
||||
<Dialog open={addPrereqOpen} onOpenChange={setAddPrereqOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("graph.detail.addPrerequisiteTitle")}</DialogTitle>
|
||||
<DialogDescription>{t("graph.detail.addPrerequisiteDesc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Select value={newPrereqId} onValueChange={setNewPrereqId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("graph.detail.selectPrerequisite")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availablePrereqs.map((kp) => (
|
||||
<SelectItem key={kp.id} value={kp.id}>
|
||||
{kp.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setAddPrereqOpen(false)}>
|
||||
{t("graph.detail.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddPrerequisite}
|
||||
disabled={!newPrereqId || isSavingPrereq}
|
||||
>
|
||||
{isSavingPrereq ? t("graph.detail.saving") : t("graph.detail.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user