feat(textbooks): 知识图谱功能全面重构 — 前置依赖 + dagre 布局 + React Flow 可视化 + 师生双视角
将教材模块图谱从基本无用状态升级为完整知识图谱可视化系统。 数据层:新增 knowledgePointPrerequisites 表(复合主键+双外键 cascade);新增 data-access-graph.ts(server-only)知识点关联聚合、学生/班级掌握度查询;utils.ts 新增 hasCycleAfterAddingEdge(DFS 循环依赖检测)。 业务层:3 个新 Server Action(getKnowledgeGraphDataAction 三视图模式、createPrerequisiteAction 含循环检测、deletePrerequisiteAction);graph-layout.ts 重写为 dagre 分层有向图布局。 视图层:knowledge-graph.tsx 重写为 React Flow 主组件(全书视图+搜索高亮+关联节点高亮+章节着色);4 个新组件(graph-kp-node/graph-prerequisite-edge/graph-toolbar/graph-node-detail-panel);use-graph-data.ts 派生值模式避免 effect 中 setState。 架构:严格三层架构,客户端通过 Server Action 间接访问 server-only 数据层;权限校验+ i18n 全覆盖;架构文档 004/005 同步。 测试:utils.test.ts 新增 5 个循环检测测试,graph-layout.test.ts 重写 5 个 dagre 布局测试,全部 30 个教材模块单元测试通过。 附带提交 drizzle/0005 error-book 迁移文件以保持 journal 一致性。
This commit is contained in:
86
src/modules/textbooks/components/graph-toolbar.tsx
Normal file
86
src/modules/textbooks/components/graph-toolbar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Search, RotateCcw } from "lucide-react"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import type { GraphViewMode } from "../types"
|
||||
|
||||
interface GraphToolbarProps {
|
||||
viewMode: GraphViewMode
|
||||
onViewModeChange: (mode: GraphViewMode) => void
|
||||
availableViewModes: GraphViewMode[]
|
||||
searchText: string
|
||||
onSearchChange: (text: string) => void
|
||||
onResetView: () => void
|
||||
}
|
||||
|
||||
const ALL_VIEW_MODES: readonly GraphViewMode[] = [
|
||||
"structure",
|
||||
"student-mastery",
|
||||
"class-mastery",
|
||||
]
|
||||
|
||||
function isGraphViewMode(value: string): value is GraphViewMode {
|
||||
return ALL_VIEW_MODES.some((mode) => mode === value)
|
||||
}
|
||||
|
||||
export function GraphToolbar({
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
availableViewModes,
|
||||
searchText,
|
||||
onSearchChange,
|
||||
onResetView,
|
||||
}: GraphToolbarProps) {
|
||||
const t = useTranslations("textbooks")
|
||||
|
||||
const handleValueChange = (v: string): void => {
|
||||
if (isGraphViewMode(v)) {
|
||||
onViewModeChange(v)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 p-2 border-b bg-background/95 shrink-0">
|
||||
<Select value={viewMode} onValueChange={handleValueChange}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableViewModes.includes("structure") && (
|
||||
<SelectItem value="structure">{t("graph.viewMode.structure")}</SelectItem>
|
||||
)}
|
||||
{availableViewModes.includes("student-mastery") && (
|
||||
<SelectItem value="student-mastery">{t("graph.viewMode.studentMastery")}</SelectItem>
|
||||
)}
|
||||
{availableViewModes.includes("class-mastery") && (
|
||||
<SelectItem value="class-mastery">{t("graph.viewMode.classMastery")}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="relative flex-1 min-w-[120px]">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
||||
<Input
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={t("graph.toolbar.search")}
|
||||
className="h-8 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" className="h-8 px-2" onClick={onResetView}>
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
<span className="text-xs">{t("graph.toolbar.resetView")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user