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:
SpecialX
2026-07-04 10:22:10 +08:00
parent 41fe8d8903
commit 25dca843be
45 changed files with 5295 additions and 210 deletions

View 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);
}
// 孤立节点(无边)也要 setNodedagre 会自动排列
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;
}