前端 + AI 进阶 Day 12:可视化工作流编辑器

当然可以!以下是 严格按照你指定模板风格(含文件结构、学习目标、原理说明、完整代码、验证步骤等)编写的 Day 12 完整版


前端 + AI 进阶学习路线|Week 11-12:智能工作流前端

Day 12:可视化工作流编辑器

学习时间:2026年1月5日(星期一)
关键词:可视化工作流、React Flow、拖拽节点、连线、节点配置、DAG


📁 项目文件结构

day12-workflow-editor/
├── src/
│   ├── components/
│   │   ├── WorkflowCanvas.jsx        # 工作流画布(React Flow)
│   │   ├── NodeTypes/                # 自定义节点类型
│   │   │   ├── AINode.jsx            # AI 节点
│   │   │   ├── InputNode.jsx         # 输入节点
│   │   │   └── OutputNode.jsx        # 输出节点
│   │   └── ControlsPanel.jsx         # 控制面板(节点库 + 操作)
│   ├── lib/
│   │   └── workflowTemplates.js      # 工作流模板
│   └── App.jsx                       # 主应用集成
└── public/

✅ 本日核心:构建可拖拽、可配置、可连线的可视化 AI 工作流编辑器


🎯 今日学习目标

  1. 使用 React Flow 实现 拖拽式工作流画布
  2. 创建 自定义节点类型(输入 / AI / 输出)
  3. 支持 节点连线配置面板
  4. 实现 工作流模板加载/保存 基础能力

💡 为什么需要可视化工作流?

复杂 AI 任务常需多步协同:

  • 数据预处理 → AI 分析 → 结果后处理
  • 多模型串联(如:OCR → 翻译 → 摘要)
  • 条件分支(如:检测到错误 → 触发修复流程)

可视化编排 降低使用门槛,让非程序员也能构建 AI 自动化流程


📚 核心技术选型

功能 技术
可视化画布 react-flow(v11+,轻量、高性能)
节点自定义 React 组件 + useNodeId Hook
状态管理 React Flow 内置 useReactFlow
持久化 JSON 序列化(后续可对接 IndexedDB)
npm install reactflow

⚠️ 注意:react-flow 已重命名为 reactflow(v11+)


🔧 动手实践:构建可视化工作流编辑器

步骤 1:创建项目并安装依赖

npx create-react-app day12-workflow-editor
cd day12-workflow-editor
npm install reactflow

步骤 2:创建自定义节点类型

// src/components/NodeTypes/InputNode.jsx
import { Handle, Position } from 'reactflow';

export default function InputNode({ data }) {
  return (
    <div style={{
      background: '#e6f7ff',
      border: '2px solid #1890ff',
      borderRadius: '8px',
      padding: '12px 16px',
      minWidth: '160px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
    }}>
      <div style={{ fontWeight: '600', color: '#1890ff', marginBottom: '4px' }}>
        📥 输入
      </div>
      <div style={{ fontSize: '12px', color: '#666' }}>
        {data.label || '用户输入'}
      </div>
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
// src/components/NodeTypes/AINode.jsx
import { Handle, Position } from 'reactflow';

export default function AINode({ data }) {
  return (
    <div style={{
      background: '#f6ffed',
      border: '2px solid #52c41a',
      borderRadius: '8px',
      padding: '12px 16px',
      minWidth: '160px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
    }}>
      <div style={{ fontWeight: '600', color: '#52c41a', marginBottom: '4px' }}>
        🤖 AI 模型
      </div>
      <div style={{ fontSize: '12px', color: '#666', wordBreak: 'break-word' }}>
        {data.label || 'LLM 分析'}
      </div>
      <Handle type="target" position={Position.Left} />
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
// src/components/NodeTypes/OutputNode.jsx
import { Handle, Position } from 'reactflow';

export default function OutputNode({ data }) {
  return (
    <div style={{
      background: '#fff2e8',
      border: '2px solid #fa8c16',
      borderRadius: '8px',
      padding: '12px 16px',
      minWidth: '160px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
    }}>
      <div style={{ fontWeight: '600', color: '#fa8c16', marginBottom: '4px' }}>
        📤 输出
      </div>
      <div style={{ fontSize: '12px', color: '#666' }}>
        {data.label || '最终结果'}
      </div>
      <Handle type="target" position={Position.Left} />
    </div>
  );
}

步骤 3:创建工作流画布

// src/components/WorkflowCanvas.jsx
import React, { useState, useCallback } from 'react';
import ReactFlow, {
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
  MiniMap,
} from 'reactflow';
import 'reactflow/dist/style.css';

import InputNode from './NodeTypes/InputNode';
import AINode from './NodeTypes/AINode';
import OutputNode from './NodeTypes/OutputNode';

const nodeTypes = {
  input: InputNode,
  ai: AINode,
  output: OutputNode,
};

const initialNodes = [
  { id: '1', type: 'input', position: { x: 50, y: 150 }, data: { label: '用户问题' } },
  { id: '2', type: 'ai', position: { x: 300, y: 150 }, data: { label: 'AI 回答生成' } },
  { id: '3', type: 'output', position: { x: 550, y: 150 }, data: { label: '聊天界面' } },
];

const initialEdges = [
  { id: 'e1-2', source: '1', target: '2', animated: true },
  { id: 'e2-3', source: '2', target: '3', animated: true },
];

export default function WorkflowCanvas({ onNodesChange, onEdgesChange }) {
  const [nodes, setNodes, onNodesChangeHandler] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChangeHandler] = useEdgesState(initialEdges);

  // 同步外部状态(用于保存)
  React.useEffect(() => {
    onNodesChange?.(nodes);
    onEdgesChange?.(edges);
  }, [nodes, edges, onNodesChange, onEdgesChange]);

  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
  );

  return (
    <div style={{ width: '100%', height: '100%' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChangeHandler}
        onEdgesChange={onEdgesChangeHandler}
        onConnect={onConnect}
        nodeTypes={nodeTypes}
        fitView
        attributionPosition="top-right"
      >
        <Controls />
        <Background color="#aaa" gap={16} />
        <MiniMap
          nodeColor={(n) => {
            if (n.type === 'input') return '#1890ff';
            if (n.type === 'ai') return '#52c41a';
            if (n.type === 'output') return '#fa8c16';
            return '#666';
          }}
        />
      </ReactFlow>
    </div>
  );
}

步骤 4:创建控制面板(节点库)

// src/components/ControlsPanel.jsx
import { useReactFlow } from 'reactflow';
import { useCallback } from 'react';

export default function ControlsPanel() {
  const { addNodes } = useReactFlow();

  const onAddNode = useCallback((type, label) => {
    const newNode = {
      id: `${type}-${Date.now()}`,
      type,
      position: { x: 200, y: 200 }, // 后续可改进为鼠标位置
      data: { label },
    };
    addNodes([newNode]);
  }, [addNodes]);

  return (
    <div style={{
      padding: '16px',
      borderRight: '1px solid #e8e8e8',
      backgroundColor: '#fafafa',
      width: '200px',
      display: 'flex',
      flexDirection: 'column',
      gap: '12px'
    }}>
      <h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>🧩 节点库</h3>
      
      <button
        onClick={() => onAddNode('input', '新输入')}
        style={{
          padding: '8px',
          backgroundColor: '#e6f7ff',
          border: '1px solid #1890ff',
          borderRadius: '4px',
          cursor: 'pointer',
          textAlign: 'left',
          fontSize: '14px',
        }}
      >
        📥 输入节点
      </button>
      
      <button
        onClick={() => onAddNode('ai', 'AI 分析')}
        style={{
          padding: '8px',
          backgroundColor: '#f6ffed',
          border: '1px solid #52c41a',
          borderRadius: '4px',
          cursor: 'pointer',
          textAlign: 'left',
          fontSize: '14px',
        }}
      >
        🤖 AI 节点
      </button>
      
      <button
        onClick={() => onAddNode('output', '结果输出')}
        style={{
          padding: '8px',
          backgroundColor: '#fff2e8',
          border: '1px solid #fa8c16',
          borderRadius: '4px',
          cursor: 'pointer',
          textAlign: 'left',
          fontSize: '14px',
        }}
      >
        📤 输出节点
      </button>

      <div style={{ marginTop: '20px' }}>
        <h4 style={{ margin: '12px 0 8px 0', fontSize: '14px' }}>🛠️ 操作</h4>
        <button
          onClick={() => {
            const workflow = {
              nodes: window.__rf?.getNodes?.() || [],
              edges: window.__rf?.getEdges?.() || [],
            };
            localStorage.setItem('workflow', JSON.stringify(workflow));
            alert('✅ 工作流已保存到本地!');
          }}
          style={{
            padding: '8px',
            backgroundColor: '#52c41a',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            width: '100%',
            fontSize: '14px',
          }}
        >
          💾 保存工作流
        </button>
        
        <button
          onClick={() => {
            const saved = localStorage.getItem('workflow');
            if (saved) {
              const { nodes, edges } = JSON.parse(saved);
              window.__rf?.setNodes?.(nodes);
              window.__rf?.setEdges?.(edges);
              alert('✅ 工作流已加载!');
            } else {
              alert('⚠️ 无已保存的工作流');
            }
          }}
          style={{
            padding: '8px',
            backgroundColor: '#1890ff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            width: '100%',
            fontSize: '14px',
            marginTop: '8px',
          }}
        >
          📂 加载工作流
        </button>
      </div>
    </div>
  );
}

💡 临时全局访问:在 WorkflowCanvas.jsx 中添加:

// 用于 ControlsPanel 临时访问(生产环境应使用 Context)
useEffect(() => {
  window.__rf = { getNodes: () => nodes, getEdges: () => edges, setNodes, setEdges };
  return () => { delete window.__rf; };
}, [nodes, edges, setNodes, setEdges]);

步骤 5:在 App.jsx 中集成

// src/App.jsx
import React from 'react';
import WorkflowCanvas from './components/WorkflowCanvas';
import ControlsPanel from './components/ControlsPanel';

function App() {
  return (
    <div style={{ 
      fontFamily: 'Inter, -apple-system, sans-serif',
      width: '100vw',
      height: '100vh',
      display: 'flex'
    }}>
      <ControlsPanel />
      <WorkflowCanvas />
    </div>
  );
}

export default App;

✅ 效果验证

  • ✅ 画布显示默认工作流(输入 → AI → 输出)
  • ✅ 从左侧“节点库”拖拽新节点到画布
  • ✅ 拖拽连线(从输出句柄到输入句柄)
  • ✅ 点击“保存工作流” → 存入 localStorage
  • ✅ 刷新页面 → 点击“加载工作流” → 恢复状态
  • ✅ 支持缩放、平移、小地图(MiniMap)

🤔 思考与延伸

  1. 节点配置:如何为 AI 节点添加模型选择/提示词配置?
    → 点击节点弹出配置面板(使用 useReactFlowsetNodes 更新)

  2. 执行引擎:如何运行工作流?
    → 遍历 DAG,按拓扑排序执行节点(Day 13 实现)

  3. 模板市场:如何分享工作流?
    → 将 JSON 转为短链接(如 workflow.to/abc123

💡 为 Day 13 做准备:今日的 nodes/edges 结构可直接用于工作流执行


📅 明日预告

Day 13:工作流执行监控

  • 实现工作流执行引擎(DAG 遍历)
  • 实时显示节点状态(运行中/成功/失败)
  • 日志面板 + 进度条

✍️ 小结

今天,我们迈出了构建 AI 自动化工作流 的第一步!通过可视化编排,用户可像搭积木一样组合 AI 能力,无需写代码。低代码 + AI,是普惠智能的关键路径。

💬 实践提示:React Flow 支持自定义边、背景、快捷键等,可进一步优化体验。欢迎分享你的工作流设计!

posted @ 2025-12-30 16:08  XiaoZhengTou  阅读(21)  评论(0)    收藏  举报