前端 + AI 进阶 Day 14:工作流模板市场

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

Day 14:工作流模板市场

学习时间:2026年1月7日(星期三)
关键词:工作流模板、模板保存/导入、模板分享、模板市场、JSON 序列化


📁 项目文件结构

day14-workflow-marketplace/
├── src/
│   ├── components/
│   │   ├── WorkflowCanvas.jsx        # 复用 Day 13 画布
│   │   ├── ExecutionPanel.jsx        # 复用 Day 13 执行面板
│   │   ├── ControlsPanel.jsx         # 扩展:模板管理
│   │   └── TemplateGallery.jsx        # 新增:模板画廊
│   ├── lib/
│   │   └── workflowTemplates.js      # 模板存储与管理
│   └── App.jsx                       # 主应用集成
└── public/

✅ 本日核心:让工作流可复用、可分享、可发现——构建前端工作流模板市场


🎯 今日学习目标

  1. 实现 工作流模板保存/导入(本地 + 在线)
  2. 构建 模板画廊(展示预设模板)
  3. 支持 一键应用模板到画布
  4. 实现 模板分享链接(URL 编码)

💡 为什么需要模板市场?

用户重复造轮子:

  • 每次都需从零搭建“图片分析”工作流
  • 优秀工作流无法被团队复用
  • 新用户不知如何开始

模板即资产,让工作流从“一次性脚本”变为“可复用解决方案”


📚 核心设计思路

功能 实现方式
模板存储 本地:localStorage;在线:URL Hash / 后端 API(本日用 URL)
模板结构 { id, name, description, nodes, edges, createdAt }
分享链接 encodeURIComponent(JSON.stringify(workflow))?template=...
预设模板 内置常用工作流(如“多模态分析”、“代码生成”)

⚠️ 注意:URL 长度有限(~2000 字符),复杂模板需压缩或后端存储


🔧 动手实践:构建工作流模板市场

步骤 1:创建项目并复用前期组件

npx create-react-app day14-workflow-marketplace
cd day14-workflow-marketplace
npm install reactflow
# 复制 Day 12-13 的 components/ 和 lib/

步骤 2:创建模板管理库

// src/lib/workflowTemplates.js
// 预设模板
export const PRESET_TEMPLATES = [
  {
    id: 'image-analysis',
    name: '🖼️ 多模态图片分析',
    description: '上传图片 → AI 标注 → 生成报告',
    nodes: [
      { id: '1', type: 'input', position: { x: 50, y: 150 },  { label: '上传图片' } },
      { id: '2', type: 'ai', position: { x: 300, y: 100 },  { label: 'LLaVA 视觉分析' } },
      { id: '3', type: 'ai', position: { x: 300, y: 200 },  { label: '生成结构化报告' } },
      { id: '4', type: 'output', position: { x: 550, y: 150 }, data: { label: '分析报告' } },
    ],
    edges: [
      { id: 'e1-2', source: '1', target: '2', animated: true },
      { id: 'e1-3', source: '1', target: '3', animated: true },
      { id: 'e2-4', source: '2', target: '4', animated: true },
      { id: 'e3-4', source: '3', target: '4', animated: true },
    ]
  },
  {
    id: 'code-generation',
    name: '💻 代码生成与测试',
    description: '描述需求 → 生成代码 → 单元测试',
    nodes: [
      { id: '1', type: 'input', position: { x: 50, y: 150 },  { label: '需求描述' } },
      { id: '2', type: 'ai', position: { x: 300, y: 150 },  { label: '生成代码' } },
      { id: '3', type: 'ai', position: { x: 550, y: 150 },  { label: '生成测试用例' } },
      { id: '4', type: 'output', position: { x: 800, y: 150 }, data: { label: '完整代码包' } },
    ],
    edges: [
      { id: 'e1-2', source: '1', target: '2', animated: true },
      { id: 'e2-3', source: '2', target: '3', animated: true },
      { id: 'e3-4', source: '3', target: '4', animated: true },
    ]
  },
  {
    id: 'data-summarization',
    name: '📊 文本摘要流水线',
    description: '输入长文本 → 关键信息提取 → 生成摘要',
    nodes: [
      { id: '1', type: 'input', position: { x: 50, y: 150 },  { label: '长文本输入' } },
      { id: '2', type: 'ai', position: { x: 300, y: 150 },  { label: '实体识别' } },
      { id: '3', type: 'ai', position: { x: 550, y: 150 },  { label: '生成摘要' } },
      { id: '4', type: 'output', position: { x: 800, y: 150 }, data: { label: '摘要结果' } },
    ],
    edges: [
      { id: 'e1-2', source: '1', target: '2', animated: true },
      { id: 'e2-3', source: '2', target: '3', animated: true },
      { id: 'e3-4', source: '3', target: '4', animated: true },
    ]
  }
];

// 保存模板到 URL
export const saveTemplateToUrl = (workflow) => {
  try {
    const serialized = JSON.stringify({
      nodes: workflow.nodes,
      edges: workflow.edges
    });
    const encoded = encodeURIComponent(serialized);
    const url = `${window.location.origin}${window.location.pathname}?template=${encoded}`;
    return url;
  } catch (e) {
    console.error('Failed to serialize workflow', e);
    return null;
  }
};

// 从 URL 加载模板
export const loadTemplateFromUrl = () => {
  const urlParams = new URLSearchParams(window.location.search);
  const templateParam = urlParams.get('template');
  if (templateParam) {
    try {
      const decoded = decodeURIComponent(templateParam);
      const workflow = JSON.parse(decoded);
      return workflow;
    } catch (e) {
      console.error('Failed to parse template from URL', e);
      return null;
    }
  }
  return null;
};

// 保存到本地模板库
export const saveTemplateLocally = (template) => {
  const existing = JSON.parse(localStorage.getItem('custom_templates') || '[]');
  const newTemplate = {
    ...template,
    id: `custom-${Date.now()}`,
    name: template.name || '自定义模板',
    createdAt: Date.now()
  };
  localStorage.setItem('custom_templates', JSON.stringify([...existing, newTemplate]));
  return newTemplate;
};

// 加载本地模板
export const loadLocalTemplates = () => {
  return JSON.parse(localStorage.getItem('custom_templates') || '[]');
};

步骤 3:创建模板画廊组件

// src/components/TemplateGallery.jsx
import { PRESET_TEMPLATES, loadLocalTemplates, saveTemplateToUrl } from '../lib/workflowTemplates';

export default function TemplateGallery({ onApplyTemplate }) {
  const localTemplates = loadLocalTemplates();
  const allTemplates = [...PRESET_TEMPLATES, ...localTemplates];

  const handleShare = (template) => {
    const url = saveTemplateToUrl(template);
    if (url) {
      navigator.clipboard.writeText(url).then(() => {
        alert('📋 模板分享链接已复制到剪贴板!');
      });
    }
  };

  return (
    <div style={{
      padding: '16px',
      backgroundColor: '#fafafa',
      width: '300px',
      display: 'flex',
      flexDirection: 'column',
      gap: '16px'
    }}>
      <h3 style={{ margin: 0, fontSize: '18px', fontWeight: '600' }}>🧩 工作流模板市场</h3>
      
      {allTemplates.length === 0 ? (
        <div style={{ color: '#888', fontSize: '14px' }}>
          暂无自定义模板,保存当前工作流以创建模板。
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
          {allTemplates.map((template) => (
            <div
              key={template.id}
              style={{
                border: '1px solid #e8e8e8',
                borderRadius: '8px',
                padding: '12px',
                backgroundColor: '#fff',
                position: 'relative'
              }}
            >
              <div style={{ fontWeight: '600', marginBottom: '4px' }}>
                {template.name}
              </div>
              <div style={{ fontSize: '12px', color: '#666', marginBottom: '8px' }}>
                {template.description}
              </div>
              <div style={{ display: 'flex', gap: '8px' }}>
                <button
                  onClick={() => onApplyTemplate(template)}
                  style={{
                    padding: '4px 8px',
                    fontSize: '12px',
                    backgroundColor: '#1890ff',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer'
                  }}
                >
                  应用
                </button>
                <button
                  onClick={() => handleShare(template)}
                  style={{
                    padding: '4px 8px',
                    fontSize: '12px',
                    backgroundColor: '#52c41a',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer'
                  }}
                >
                  分享
                </button>
              </div>
              {template.createdAt && (
                <div style={{ 
                  position: 'absolute', 
                  bottom: '4px', 
                  right: '8px', 
                  fontSize: '10px', 
                  color: '#999' 
                }}>
                  自定义
                </div>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

步骤 4:扩展控制面板(保存/导入)

// src/components/ControlsPanel.jsx(更新版)
import { saveTemplateLocally, loadTemplateFromUrl } from '../lib/workflowTemplates';

export default function ControlsPanel({ 
  nodes, 
  edges, 
  onApplyTemplate,
  onResetCanvas 
}) {
  // 检查 URL 模板
  React.useEffect(() => {
    const urlTemplate = loadTemplateFromUrl();
    if (urlTemplate) {
      onApplyTemplate(urlTemplate);
      // 清除 URL 参数(可选)
      window.history.replaceState({}, document.title, window.location.pathname);
    }
  }, [onApplyTemplate]);

  const handleSaveTemplate = () => {
    if (nodes.length === 0) {
      alert('请先创建工作流!');
      return;
    }
    const templateName = prompt('模板名称:', '我的工作流');
    if (templateName) {
      const newTemplate = saveTemplateLocally({
        name: templateName,
        nodes,
        edges
      });
      alert(`✅ 模板 "${templateName}" 已保存!`);
    }
  };

  const handleImportFromFile = () => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';
    input.onchange = (e) => {
      const file = e.target.files?.[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (event) => {
        try {
          const workflow = JSON.parse(event.target.result);
          onApplyTemplate(workflow);
        } catch (e) {
          alert('❌ 无效的模板文件');
        }
      };
      reader.readAsText(file);
    };
    input.click();
  };

  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={handleSaveTemplate}
        style={{
          padding: '8px',
          backgroundColor: '#52c41a',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          width: '100%',
          fontSize: '14px',
        }}
      >
        💾 保存为模板
      </button>
      
      <button
        onClick={handleImportFromFile}
        style={{
          padding: '8px',
          backgroundColor: '#1890ff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          width: '100%',
          fontSize: '14px',
          marginTop: '8px'
        }}
      >
        📂 从文件导入
      </button>

      <button
        onClick={onResetCanvas}
        style={{
          padding: '8px',
          backgroundColor: '#f5222d',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          width: '100%',
          fontSize: '14px',
          marginTop: '8px'
        }}
      >
        🗑️ 清空画布
      </button>
    </div>
  );
}

步骤 5:在 App.jsx 中集成模板市场

// src/App.jsx
import React, { useState, useEffect } from 'react';
import WorkflowCanvas from './components/WorkflowCanvas';
import ExecutionPanel from './components/ExecutionPanel';
import ControlsPanel from './components/ControlsPanel';
import TemplateGallery from './components/TemplateGallery';
import { PRESET_TEMPLATES } from './lib/workflowTemplates';

function App() {
  const [nodes, setNodes] = useState(PRESET_TEMPLATES[0].nodes); // 默认加载第一个模板
  const [edges, setEdges] = useState(PRESET_TEMPLATES[0].edges);

  const applyTemplate = (template) => {
    setNodes(template.nodes || []);
    setEdges(template.edges || []);
  };

  const resetCanvas = () => {
    setNodes([]);
    setEdges([]);
  };

  return (
    <div style={{ 
      fontFamily: 'Inter, -apple-system, sans-serif',
      width: '100vw',
      height: '100vh',
      display: 'flex',
      flexDirection: 'column'
    }}>
      <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
        <ControlsPanel 
          nodes={nodes}
          edges={edges}
          onApplyTemplate={applyTemplate}
          onResetCanvas={resetCanvas}
        />
        <WorkflowCanvas 
          nodes={nodes}
          edges={edges}
          onNodesChange={setNodes}
          onEdgesChange={setEdges}
        />
        <TemplateGallery onApplyTemplate={applyTemplate} />
      </div>
      <ExecutionPanel 
        nodes={nodes}
        edges={edges}
        onUpdateNode={(id, status) => {
          setNodes(prev => prev.map(n => 
            n.id === id ? { ...n,  { ...n.data, status } } : n
          ));
        }}
        onResetNodes={() => {
          setNodes(prev => prev.map(n => ({ ...n,  { ...n.data, status: 'idle' } })));
        }}
      />
    </div>
  );
}

export default App;

✅ 效果验证

  • ✅ 画布默认加载“多模态图片分析”模板
  • ✅ 点击模板画廊中的“应用” → 画布更新为该模板
  • ✅ 点击“💾 保存为模板” → 输入名称 → 模板出现在画廊
  • ✅ 点击“分享” → 链接复制到剪贴板(格式:?template=...
  • ✅ 打开分享链接 → 自动加载模板
  • ✅ “从文件导入” → 选择 JSON 文件 → 加载工作流

🤔 思考与延伸

  1. 模板压缩:如何缩短 URL 长度?
    → 使用 lz-string 压缩 JSON:npm install lz-string

  2. 在线模板库:如何实现真正的模板市场?
    → 后端存储模板 + 搜索/评分/分类 API

  3. 版本管理:如何支持模板更新?
    → 添加 version 字段,提供“升级”功能

💡 终极目标:将 ai-frontend-kit 的工作流能力封装为可嵌入的 <WorkflowBuilder /> 组件


📅 项目收尾

3 个月学习完成!

  • 你已掌握 前端 AI 体验优化、多模态交互、AI SDK、调试工具、对话设计、工作流编排 全栈能力
  • 所有代码可整合为 ai-frontend-kit 开源项目
  • 部署到 Vercel,全球可访问!

✍️ 小结

今天,我们为工作流系统画上完美句号!通过模板市场,用户可轻松复用、分享、发现优秀工作流,真正实现 AI 能力的民主化可复用性,是工程价值的终极体现。

💬 最后建议:将 14 天的成果整合为 monorepo,发布 ai-frontend-kit 到 npm,开源到 GitHub!你已具备构建下一代 AI 原生应用的全部能力。🎉

posted @ 2025-12-31 10:20  XiaoZhengTou  阅读(9)  评论(0)    收藏  举报