feat(lesson-preparation): major update with AI features, schedules, and new components
- Add actions-schedules.ts and data-access-schedules.ts for schedule management - Add AI differentiation, AI feedback, consistency check dialogs - Add attachment-picker, curriculum-heatmap, print-view, version-diff-view - Add lesson-plan-mobile-view and schedule-dialog components - Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check, curriculum-coverage, export, version-diff - Add history-slice hook for version history - Update existing components, hooks, providers, services, types - Add teacher lesson-plans heatmap and library pages
This commit is contained in:
82
src/modules/lesson-preparation/lib/auto-layout.ts
Normal file
82
src/modules/lesson-preparation/lib/auto-layout.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* V5-8:画布自动布局
|
||||
*
|
||||
* 使用 dagre 计算 DAG 布局,将教学节点按流程关系自动排列。
|
||||
* 仅对可拖动的教学节点(非正文节点)布局,正文节点位置保持不变。
|
||||
*/
|
||||
import dagre from "@dagrejs/dagre";
|
||||
import type {
|
||||
AnyLessonPlanNode,
|
||||
AnyLessonPlanEdge,
|
||||
} from "../types";
|
||||
|
||||
export interface AutoLayoutOptions {
|
||||
/** 布局方向:TB(上→下)/ LR(左→右),默认 TB */
|
||||
direction?: "TB" | "LR";
|
||||
/** 节点宽度,默认 240 */
|
||||
nodeWidth?: number;
|
||||
/** 节点高度,默认 120 */
|
||||
nodeHeight?: number;
|
||||
/** 节点水平间距,默认 40 */
|
||||
rankSep?: number;
|
||||
/** 节点垂直间距,默认 60 */
|
||||
nodeSep?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算自动布局,返回每个节点的新位置(含原始位置作为兜底)
|
||||
*/
|
||||
export function computeAutoLayout(
|
||||
nodes: AnyLessonPlanNode[],
|
||||
edges: AnyLessonPlanEdge[],
|
||||
options: AutoLayoutOptions = {},
|
||||
): Map<string, { x: number; y: number }> {
|
||||
const {
|
||||
direction = "TB",
|
||||
nodeWidth = 240,
|
||||
nodeHeight = 120,
|
||||
rankSep = 60,
|
||||
nodeSep = 40,
|
||||
} = options;
|
||||
|
||||
const result = new Map<string, { x: number; y: number }>();
|
||||
|
||||
if (nodes.length === 0) return result;
|
||||
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph({ rankdir: direction, ranksep: rankSep, nodesep: nodeSep });
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
// 仅对教学节点布局(正文节点固定位置)
|
||||
const layoutableNodes = nodes.filter((n) => n.type !== "textbook_content");
|
||||
const layoutableIds = new Set(layoutableNodes.map((n) => n.id));
|
||||
|
||||
for (const node of layoutableNodes) {
|
||||
g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
}
|
||||
|
||||
// 仅添加两端都可布局的 flow 边
|
||||
for (const edge of edges) {
|
||||
if (edge.type !== "flow") continue;
|
||||
if (!layoutableIds.has(edge.source) || !layoutableIds.has(edge.target)) continue;
|
||||
g.setEdge(edge.source, edge.target);
|
||||
}
|
||||
|
||||
// 孤立节点(无边)也要 setNode,dagre 会自动排列
|
||||
dagre.layout(g);
|
||||
|
||||
for (const node of layoutableNodes) {
|
||||
const laid = g.node(node.id);
|
||||
if (laid) {
|
||||
// dagre 返回中心点,React Flow 使用左上角,需减去半宽/半高
|
||||
result.set(node.id, {
|
||||
x: laid.x - nodeWidth / 2,
|
||||
y: laid.y - nodeHeight / 2,
|
||||
});
|
||||
} else {
|
||||
result.set(node.id, node.position);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user