import { DragEvent, useCallback, useMemo, useState } from "react"; import { Background, Controls, Handle, MiniMap, Position, ReactFlow, ReactFlowProvider, SelectionMode, addEdge, type Connection, type Edge, type Node, type NodeProps, type NodeTypes, useEdgesState, useNodesState, useReactFlow, } from "@xyflow/react"; type ParameterValue = number | string; type ParameterDefinition = { label: string; unit?: string; default: ParameterValue; }; type ComponentDefinition = { type: string; label: string; modelType: string; ports: string[]; parameters: Record; }; type SimulationNodeData = { label: string; componentType: string; modelType: string; ports: string[]; parameters: Record; }; type SimulationNode = Node; type SimulationEdge = Edge; type SimulationConfig = { t_start: number; t_stop: number; step: number; max_step: number; method: string; }; type ProjectNodePayload = { id: string; type: "simulationComponent"; position: { x: number; y: number; }; data: SimulationNodeData; }; type ProjectEdgePayload = { id: string; source: string; target: string; sourceHandle: string | null; targetHandle: string | null; }; type ReactFlowProjectPayload = { name: string; nodes: ProjectNodePayload[]; edges: ProjectEdgePayload[]; simulation: SimulationConfig; }; type SaveProjectResult = { id: string; componentCount: number; connectionCount: number; }; type SimulationResult = { success: boolean; message: string; final: { time: number; tankPressure: number; tankTemperature: number; cylinderPressure: number; cylinderTemperature: number; }; series: Record; artifacts: Record; networkSummary: string; }; const defaultSimulationConfig: SimulationConfig = { t_start: 0, t_stop: 2, step: 0.1, max_step: 0.005, method: "BDF", }; const componentDefinitions: ComponentDefinition[] = [ { type: "cylinder", label: "气瓶", modelType: "cylinder", ports: ["port_b"], parameters: { volume: { label: "容积", unit: "m3", default: 0.01 }, p0: { label: "初始压力", unit: "Pa", default: 35000000 }, T0: { label: "初始温度", unit: "K", default: 300 }, }, }, { type: "tank", label: "贮箱", modelType: "tank", ports: ["port_a"], parameters: { volume: { label: "容积", unit: "m3", default: 0.1 }, p0: { label: "初始压力", unit: "Pa", default: 100000 }, T0: { label: "初始温度", unit: "K", default: 300 }, }, }, { type: "pipe", label: "管段", modelType: "pipe", ports: ["port_a", "port_b"], parameters: { length: { label: "长度", unit: "m", default: 5 }, diameter: { label: "直径", unit: "m", default: 0.02 }, lambda_darcy: { label: "摩阻系数", default: 0.02 }, p0: { label: "初始压力", unit: "Pa", default: 100000 }, T0: { label: "初始温度", unit: "K", default: 300 }, }, }, { type: "orifice", label: "孔板/阀门", modelType: "orifice", ports: ["port_a", "port_b"], parameters: { K: { label: "流量系数", default: 0.00001 }, opening: { label: "开度", default: 1 }, }, }, { type: "tee", label: "三通", modelType: "tee", ports: ["port_in", "port_out1", "port_out2"], parameters: {}, }, ]; const nextNodeNumberByType: Record = {}; function defaultParameters(definition: ComponentDefinition) { return Object.fromEntries( Object.entries(definition.parameters).map(([key, parameter]) => [ key, parameter.default, ]), ); } function buildNodeData(definition: ComponentDefinition): SimulationNodeData { return { label: definition.label, componentType: definition.type, modelType: definition.modelType, ports: definition.ports, parameters: defaultParameters(definition), }; } function SimulationComponentNode({ data, selected }: NodeProps) { const inputPorts = data.ports.filter((port) => port.includes("in") || port === "port_a"); const outputPorts = data.ports.filter((port) => !inputPorts.includes(port)); const singlePorts = data.ports.length === 1 ? data.ports : []; return (
{data.label}
{inputPorts.map((port, index) => ( ))} {outputPorts.map((port, index) => ( ))} {singlePorts.map((port) => ( ))}
); } function symbolForComponent(type: string) { if (type === "cylinder") { return
; } if (type === "tank") { return
; } if (type === "pipe") { return
; } if (type === "orifice") { return
; } return
; } function FlowWorkbench() { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedNodeId, setSelectedNodeId] = useState(null); const [projectName, setProjectName] = useState("demo-system"); const [simulationConfig, setSimulationConfig] = useState( defaultSimulationConfig, ); const [xmlText, setXmlText] = useState(""); const [statusText, setStatusText] = useState("后端待调用"); const [simulationResult, setSimulationResult] = useState(null); const [busy, setBusy] = useState(false); const reactFlow = useReactFlow(); const nodeTypes = useMemo( () => ({ simulationComponent: SimulationComponentNode }), [], ); const selectedNode = nodes.find((node) => node.id === selectedNodeId) ?? null; const selectedDefinition = selectedNode ? componentDefinitions.find((definition) => definition.type === selectedNode.data.componentType) : null; const buildCurrentProject = useCallback( () => buildProjectPayload(projectName, nodes, edges, simulationConfig), [edges, nodes, projectName, simulationConfig], ); const onConnect = useCallback( (connection: Connection) => { setEdges((currentEdges) => addEdge( { ...connection, id: `edge-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}-${Date.now()}`, }, currentEdges, ), ); }, [setEdges], ); const onDragStart = (event: DragEvent, type: string) => { event.dataTransfer.setData("application/system-component", type); event.dataTransfer.effectAllowed = "move"; }; const onDrop = useCallback( (event: DragEvent) => { event.preventDefault(); const type = event.dataTransfer.getData("application/system-component"); const definition = componentDefinitions.find((item) => item.type === type); if (!definition) { return; } const position = reactFlow.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); const id = takeNextNodeId(definition.type); const node: SimulationNode = { id, type: "simulationComponent", position, data: buildNodeData(definition), }; setNodes((currentNodes) => [...currentNodes, node]); setSelectedNodeId(id); }, [reactFlow, setNodes], ); const updateSelectedNode = (updater: (node: SimulationNode) => SimulationNode) => { if (!selectedNodeId) { return; } setNodes((currentNodes) => currentNodes.map((node) => (node.id === selectedNodeId ? updater(node) : node)), ); }; const updateSimulationConfig = (key: keyof SimulationConfig, value: string) => { setSimulationConfig((current) => ({ ...current, [key]: key === "method" ? value : Number(value), })); }; const generateXml = async () => { await runBusyAction("正在调用后端生成 XML...", async () => { const xml = await postJsonText("/api/reactflow/system-xml", buildCurrentProject()); setXmlText(xml); setStatusText(`XML 已生成:${nodes.length} 个组件,${edges.length} 条连接`); }); }; const downloadXml = async () => { await runBusyAction("正在生成并下载 XML...", async () => { const xml = await postJsonText("/api/reactflow/system-xml", buildCurrentProject()); setXmlText(xml); downloadText(`${safeFilename(projectName)}.xml`, xml); setStatusText("XML 已下载"); }); }; const saveProject = async () => { await runBusyAction("正在保存工程...", async () => { const project = buildCurrentProject(); const result = await postJson( `/api/reactflow/projects/${encodeURIComponent(project.name)}`, project, ); setStatusText( `工程已保存:${result.id},${result.componentCount} 个组件,${result.connectionCount} 条连接`, ); }); }; const loadProject = async () => { await runBusyAction("正在加载工程...", async () => { const projectId = projectName.trim() || "untitled"; const project = await getJson( `/api/reactflow/projects/${encodeURIComponent(projectId)}`, ); const loadedNodes = project.nodes.map(normalizeLoadedNode); setNodes(loadedNodes); setEdges(project.edges.map(normalizeLoadedEdge)); setProjectName(project.name || projectId); setSimulationConfig({ ...defaultSimulationConfig, ...project.simulation }); setSelectedNodeId(null); setSimulationResult(null); setXmlText(""); syncNextNodeNumbers(loadedNodes); setStatusText(`工程已加载:${loadedNodes.length} 个组件,${project.edges.length} 条连接`); window.requestAnimationFrame(() => reactFlow.fitView()); }); }; const runSimulation = async () => { await runBusyAction("正在调用 PythonModels 仿真...", async () => { const result = await postJson( "/api/reactflow/simulate-testmodel", buildCurrentProject(), ); setSimulationResult(result); setStatusText(result.success ? "仿真完成" : `仿真返回:${result.message}`); }); }; async function runBusyAction(message: string, action: () => Promise) { setBusy(true); setStatusText(message); try { await action(); } catch (error) { setStatusText(`操作失败:${formatError(error)}`); } finally { setBusy(false); } } return (
System Simulation Flow
React Flow 建模与仿真 MVP
{ event.preventDefault(); event.dataTransfer.dropEffect = "move"; }} onDrop={onDrop} > setSelectedNodeId(node.id)} onNodesChange={onNodesChange} onPaneClick={() => setSelectedNodeId(null)} onSelectionChange={({ nodes: selectedNodes }) => setSelectedNodeId(selectedNodes.length === 1 ? selectedNodes[0].id : null) } panActivationKeyCode="Space" panOnDrag={[1]} selectionMode={SelectionMode.Partial} selectionOnDrag zoomOnDoubleClick={false} >