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:
@@ -5,7 +5,8 @@ import { and, asc, count, eq, inArray, like, or, sql, isNull, type SQL } from "d
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import { chapters, knowledgePoints, textbooks } from "@/shared/db/schema"
|
||||
import { chapters, knowledgePoints, knowledgePointPrerequisites, textbooks } from "@/shared/db/schema"
|
||||
import { escapeLikePattern } from "@/shared/lib/action-utils"
|
||||
import type {
|
||||
Chapter,
|
||||
KnowledgePoint,
|
||||
@@ -14,7 +15,9 @@ import type {
|
||||
import type {
|
||||
CreateChapterInput,
|
||||
CreateKnowledgePointInput,
|
||||
CreatePrerequisiteInput,
|
||||
CreateTextbookInput,
|
||||
DeletePrerequisiteInput,
|
||||
UpdateChapterContentInput,
|
||||
UpdateKnowledgePointInput,
|
||||
UpdateTextbookInput,
|
||||
@@ -41,7 +44,7 @@ export const getTextbooks = cache(async (query?: string, subject?: string, grade
|
||||
|
||||
const q = query?.trim()
|
||||
if (q) {
|
||||
const needle = `%${q}%`
|
||||
const needle = `%${escapeLikePattern(q)}%`
|
||||
const nameCond = or(
|
||||
like(textbooks.title, needle),
|
||||
like(textbooks.subject, needle),
|
||||
@@ -288,8 +291,10 @@ export async function deleteChapter(id: string): Promise<void> {
|
||||
if (kids) stack.push(...kids)
|
||||
}
|
||||
|
||||
await db.delete(knowledgePoints).where(inArray(knowledgePoints.chapterId, idsToDelete))
|
||||
await db.delete(chapters).where(inArray(chapters.id, idsToDelete))
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(knowledgePoints).where(inArray(knowledgePoints.chapterId, idsToDelete))
|
||||
await tx.delete(chapters).where(inArray(chapters.id, idsToDelete))
|
||||
})
|
||||
}
|
||||
|
||||
export const getKnowledgePointsByChapterId = cache(async (chapterId: string): Promise<KnowledgePoint[]> => {
|
||||
@@ -505,7 +510,7 @@ export const getTextbooksWithScope = cache(
|
||||
|
||||
const q = query?.trim()
|
||||
if (q) {
|
||||
const needle = `%${q}%`
|
||||
const needle = `%${escapeLikePattern(q)}%`
|
||||
const nameCond = or(
|
||||
like(textbooks.title, needle),
|
||||
like(textbooks.subject, needle),
|
||||
@@ -617,3 +622,41 @@ export const getKnowledgePointOptions = cache(async (): Promise<KnowledgePointOp
|
||||
grade: row.grade ?? null,
|
||||
}))
|
||||
})
|
||||
|
||||
// ===== Prerequisite CRUD =====
|
||||
|
||||
export async function createPrerequisite(data: CreatePrerequisiteInput): Promise<void> {
|
||||
await db.insert(knowledgePointPrerequisites).values({
|
||||
knowledgePointId: data.knowledgePointId,
|
||||
prerequisiteKpId: data.prerequisiteKpId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deletePrerequisite(data: DeletePrerequisiteInput): Promise<void> {
|
||||
await db
|
||||
.delete(knowledgePointPrerequisites)
|
||||
.where(and(
|
||||
eq(knowledgePointPrerequisites.knowledgePointId, data.knowledgePointId),
|
||||
eq(knowledgePointPrerequisites.prerequisiteKpId, data.prerequisiteKpId),
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教材下所有知识点的前置依赖边列表。
|
||||
* 用于循环检测。
|
||||
*/
|
||||
export async function getPrerequisiteEdgesForTextbook(
|
||||
textbookId: string,
|
||||
): Promise<Array<[string, string]>> {
|
||||
const rows = await db
|
||||
.select({
|
||||
knowledgePointId: knowledgePointPrerequisites.knowledgePointId,
|
||||
prerequisiteKpId: knowledgePointPrerequisites.prerequisiteKpId,
|
||||
})
|
||||
.from(knowledgePointPrerequisites)
|
||||
.innerJoin(knowledgePoints, eq(knowledgePoints.id, knowledgePointPrerequisites.knowledgePointId))
|
||||
.innerJoin(chapters, eq(chapters.id, knowledgePoints.chapterId))
|
||||
.where(eq(chapters.textbookId, textbookId))
|
||||
|
||||
return rows.map((r) => [r.knowledgePointId, r.prerequisiteKpId] as [string, string])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user