"use client"; import { useCallback, useMemo } from "react"; import { useTranslations } from "next-intl"; import { ReactFlow, Background, Controls, MiniMap, type Node, type Edge, type NodeChange, type EdgeChange, type Connection, applyEdgeChanges, BackgroundVariant, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { useLessonPlanEditor } from "../hooks/use-lesson-plan-editor"; import { LessonNode } from "./nodes/lesson-node"; import { toRfNodes, toRfEdges } from "../lib/rf-mappers"; import type { LessonPlanNode } from "../types"; const nodeTypes = { lesson: LessonNode }; // NodeEditor 只负责画布交互,内容编辑由 NodeEditPanel 处理 type Props = Record; export function NodeEditor({}: Props) { const t = useTranslations("lessonPreparation"); const { doc, selectedNodeId, updateNodePosition, removeNode, connect, selectNode, setEdges } = useLessonPlanEditor(); // 使用纯函数映射 nodes/edges const rfNodes: Node[] = useMemo( () => toRfNodes(doc.nodes, selectedNodeId), [doc.nodes, selectedNodeId], ); const rfEdges: Edge[] = useMemo( () => toRfEdges(doc.edges), [doc.edges], ); const onNodesChange = useCallback( (changes: NodeChange[]) => { changes.forEach((change) => { if (change.type === "position" && change.position) { // 拖拽结束时(dragging: false)才写入最终位置,避免中间状态污染(修复 P1-1) if (change.dragging === false) { updateNodePosition(change.id, change.position); } } else if (change.type === "remove") { removeNode(change.id); } else if (change.type === "select") { selectNode(change.selected ? change.id : null); } }); }, [updateNodePosition, removeNode, selectNode], ); const onConnect = useCallback( (conn: Connection) => { if (conn.source && conn.target) { connect(conn.source, conn.target); } }, [connect], ); // 同步 edges 变化(如拖拽重连) const onEdgesChangeSync = useCallback( (changes: EdgeChange[]) => { // 简单处理:删除时调用 disconnect const nextEdges = applyEdgeChanges(changes, rfEdges); const ourEdges = nextEdges.map((e) => ({ id: e.id, source: e.source, target: e.target, sourceHandle: e.sourceHandle ?? null, targetHandle: e.targetHandle ?? null, })); setEdges(ourEdges); }, [rfEdges, setEdges], ); return (
{doc.nodes.length === 0 && (

{t("editor.canvasEmpty")}

{t("editor.canvasEmptyHint")}

)} selectNode(node.id)} onPaneClick={() => selectNode(null)} fitView fitViewOptions={{ padding: 0.2, maxZoom: 1.2 }} defaultEdgeOptions={{ animated: true, style: { stroke: "#1976d2", strokeWidth: 2 }, }} proOptions={{ hideAttribution: true }} className="bg-surface-container-low" > { const nodeData = (n.data as { node?: LessonPlanNode }).node; if (!nodeData) return "#9e9e9e"; const colors: Record = { objective: "#4caf50", key_point: "#f44336", import: "#2196f3", new_teaching: "#9c27b0", consolidation: "#ff9800", summary: "#607d8b", homework: "#795548", blackboard: "#009688", text_study: "#3f51b5", exercise: "#e91e63", rich_text: "#9e9e9e", reflection: "#cddc39", }; return colors[nodeData.type] ?? "#9e9e9e"; }} />
); }