Files
NextEdu/src/modules/textbooks/hooks/use-graph-data.ts
SpecialX 58656da983 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 一致性。
2026-06-23 00:13:03 +08:00

70 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState, useEffect, useRef, useCallback } from "react"
import { getKnowledgeGraphDataAction } from "../actions"
import type { GraphViewMode, KnowledgeGraphData } from "../types"
interface UseGraphDataResult {
data: KnowledgeGraphData | null
isLoading: boolean
error: string | null
reload: () => void
}
/**
* 图谱数据加载 Hook。
*
* 按 textbookId + viewMode 加载,切换 viewMode 时重新加载。
* 使用派生值模式isLoading 从 data.viewMode 派生),避免 effect 中同步 setState。
*/
export function useGraphData(
textbookId: string,
viewMode: GraphViewMode,
): UseGraphDataResult {
const [data, setData] = useState<KnowledgeGraphData | null>(null)
const [error, setError] = useState<string | null>(null)
const [reloadTrigger, setReloadTrigger] = useState(0)
const lastRequestKey = useRef<string>("")
const reload = useCallback(() => {
setReloadTrigger((n) => n + 1)
}, [])
// 派生 loading 状态:无数据或当前数据不匹配请求的 viewMode
const isLoading = data === null || data.viewMode !== viewMode
useEffect(() => {
if (!textbookId) return
const requestKey = `${textbookId}:${viewMode}:${reloadTrigger}`
if (lastRequestKey.current === requestKey) return
lastRequestKey.current = requestKey
let cancelled = false
getKnowledgeGraphDataAction(textbookId, viewMode)
.then((result) => {
if (cancelled) return
if (result.success && result.data) {
setData(result.data)
setError(null)
} else {
setData(null)
setError(result.message ?? "Unknown error")
}
})
.catch((e: unknown) => {
if (!cancelled) {
setData(null)
setError(e instanceof Error ? e.message : "Unknown error")
}
})
return () => {
cancelled = true
}
}, [textbookId, viewMode, reloadTrigger])
return { data, isLoading, error, reload }
}