feat(lesson-preparation): add readonly view, anchor node selector, and type guards
- Add lesson-plan-readonly-view for viewing published plans - Add anchor-node-selector and textbook-segments for canvas anchor positioning - Add i18n-errors and type-guards lib utilities - Add lesson-plan-provider-setup for provider initialization - Update actions, data-access (knowledge, versions, main), publish-service - Update blocks (blackboard, exercise, homework, import, key-point, objective, reflection) - Update editor, node-editor, node-edit-panel, pickers, and providers
This commit is contained in:
@@ -2,10 +2,9 @@ import type { Node, Edge } from "@xyflow/react";
|
||||
import type {
|
||||
AnyLessonPlanEdge,
|
||||
AnyLessonPlanNode,
|
||||
LessonPlanNode,
|
||||
NodeAnchor,
|
||||
TextbookContentNode,
|
||||
} from "../types";
|
||||
import { getNodeColor } from "./node-summary";
|
||||
|
||||
/**
|
||||
* 纯函数:将课案 nodes/edges 映射为 React Flow 格式。
|
||||
@@ -20,6 +19,8 @@ import type {
|
||||
export interface ToRfNodesContext {
|
||||
anchors: NodeAnchor[];
|
||||
selectedNodeId: string | null;
|
||||
/** 可锚定的教学节点列表(P1-1:用于节点选择器)*/
|
||||
anchorableNodes?: { id: string; title: string; type: string }[];
|
||||
onAddRangeAnchor?: (params: {
|
||||
nodeId: string;
|
||||
start: number;
|
||||
@@ -30,8 +31,14 @@ export interface ToRfNodesContext {
|
||||
nodeId: string;
|
||||
start: number;
|
||||
}) => void;
|
||||
/** 创建新节点并锚定(P1-1)*/
|
||||
onCreateNewNode?: (params: {
|
||||
anchorType: "range" | "point";
|
||||
start: number;
|
||||
end?: number;
|
||||
textPreview?: string;
|
||||
}) => void;
|
||||
onSelectNode?: (id: string | null) => void;
|
||||
onZoomChange?: (zoom: number) => void;
|
||||
}
|
||||
|
||||
export function toRfNodes(
|
||||
@@ -39,10 +46,25 @@ export function toRfNodes(
|
||||
selectedNodeId: string | null,
|
||||
ctx?: ToRfNodesContext,
|
||||
): Node[] {
|
||||
// 当有选中节点时,收集所有与选中节点相关的节点 ID(通过锚点关联)
|
||||
const relatedNodeIds = new Set<string>();
|
||||
if (selectedNodeId && ctx?.anchors) {
|
||||
for (const a of ctx.anchors) {
|
||||
if (a.nodeId === selectedNodeId) {
|
||||
relatedNodeIds.add(a.nodeId);
|
||||
}
|
||||
}
|
||||
// 正文节点始终相关(因为锚点在正文上)
|
||||
const textbookNode = nodes.find((n) => n.type === "textbook_content");
|
||||
if (textbookNode) relatedNodeIds.add(textbookNode.id);
|
||||
relatedNodeIds.add(selectedNodeId);
|
||||
}
|
||||
|
||||
return nodes.map((n) => {
|
||||
// 正文节点
|
||||
// 正文节点:n.type === "textbook_content" 已收窄为 TextbookContentNode
|
||||
if (n.type === "textbook_content") {
|
||||
const tbNode = n as TextbookContentNode;
|
||||
const tbNode = n;
|
||||
const isDimmed = selectedNodeId !== null && !relatedNodeIds.has(tbNode.id);
|
||||
return {
|
||||
id: tbNode.id,
|
||||
type: "textbook_content",
|
||||
@@ -51,24 +73,28 @@ export function toRfNodes(
|
||||
node: tbNode,
|
||||
anchors: ctx?.anchors ?? [],
|
||||
selectedNodeId,
|
||||
anchorableNodes: ctx?.anchorableNodes ?? [],
|
||||
onAddRangeAnchor: ctx?.onAddRangeAnchor,
|
||||
onAddPointAnchor: ctx?.onAddPointAnchor,
|
||||
onCreateNewNode: ctx?.onCreateNewNode,
|
||||
onSelectNode: ctx?.onSelectNode,
|
||||
onZoomChange: ctx?.onZoomChange,
|
||||
} as Record<string, unknown>,
|
||||
selected: tbNode.id === selectedNodeId,
|
||||
draggable: false,
|
||||
style: isDimmed ? { opacity: 0.3 } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 教学节点
|
||||
const lessonNode = n as LessonPlanNode;
|
||||
// 教学节点:textbook_content 分支已上方 return,此处 n 已收窄为 LessonPlanNode
|
||||
const lessonNode = n;
|
||||
const isDimmed = selectedNodeId !== null && !relatedNodeIds.has(lessonNode.id);
|
||||
return {
|
||||
id: lessonNode.id,
|
||||
type: "lesson",
|
||||
position: lessonNode.position,
|
||||
data: { node: lessonNode } as Record<string, unknown>,
|
||||
selected: lessonNode.id === selectedNodeId,
|
||||
style: isDimmed ? { opacity: 0.3 } : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -80,36 +106,39 @@ export function toRfEdges(
|
||||
): Edge[] {
|
||||
return edges.map((e) => {
|
||||
if (e.type === "anchor") {
|
||||
// 锚点边:默认 10% 透明度,选中关联节点时 100%
|
||||
// 锚点边:默认 40% 透明度,选中关联节点时 100%
|
||||
const anchor = anchors.find((a) => a.id === e.anchorId);
|
||||
const isActive = anchor && anchor.nodeId === selectedNodeId;
|
||||
// P1-4 修复:使用锚点关联节点的颜色,而非硬编码蓝色
|
||||
const strokeColor = anchor ? getNodeColor(anchor.nodeId) : "#9e9e9e";
|
||||
return {
|
||||
...e,
|
||||
animated: false,
|
||||
animated: isActive,
|
||||
className: isActive ? "anchor-edge active" : "anchor-edge",
|
||||
// P1-3 修复:将 anchorId 存入 data,fromRfEdges 从 data 读取
|
||||
data: { anchorId: e.anchorId },
|
||||
style: {
|
||||
stroke: anchor ? getNodeColorForAnchor(anchor.nodeId) : "#9e9e9e",
|
||||
strokeWidth: 2,
|
||||
opacity: isActive ? 1 : 0.1,
|
||||
stroke: strokeColor,
|
||||
strokeWidth: isActive ? 3 : 2,
|
||||
opacity: isActive ? 1 : 0.4,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 流程边
|
||||
const isDimmed = selectedNodeId !== null && e.source !== selectedNodeId && e.target !== selectedNodeId;
|
||||
return {
|
||||
...e,
|
||||
animated: true,
|
||||
style: { stroke: "#1976d2", strokeWidth: 2 },
|
||||
animated: !isDimmed,
|
||||
style: {
|
||||
stroke: "#1976d2",
|
||||
strokeWidth: 2,
|
||||
opacity: isDimmed ? 0.2 : 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 简单的颜色查找(避免循环依赖 node-summary)
|
||||
function getNodeColorForAnchor(_nodeId: string): string {
|
||||
// 实际颜色由 CSS 类 .anchor-edge 设置,这里返回默认值
|
||||
return "#1976d2";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 React Flow edges 转回课案 edges 格式。
|
||||
*/
|
||||
@@ -125,12 +154,13 @@ export function fromRfEdges(
|
||||
targetHandle: e.targetHandle ?? null,
|
||||
};
|
||||
|
||||
// 保留原有的 type 信息(通过 className 判断或默认为 flow)
|
||||
if (e.className?.includes("anchor-edge")) {
|
||||
// P1-3 修复:优先从 data.anchorId 读取,回退到 className 判断
|
||||
const dataAnchorId = (e.data as { anchorId?: string } | undefined)?.anchorId;
|
||||
if (dataAnchorId || e.className?.includes("anchor-edge")) {
|
||||
return {
|
||||
...base,
|
||||
type: "anchor" as const,
|
||||
anchorId: e.id, // 简化:用 edge id 作为 anchorId(实际应从 data 读取)
|
||||
anchorId: dataAnchorId ?? e.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user