AI 助力搭建 Ant Design 6 可视化 UI 设计器:从零到一的开发实战

在 AI 时代,前端开发的效率边界正在被重新定义。这篇文章记录了我如何借助 AI 协作,从零开始搭建一套基于 Ant Design 6 的可视化 UI 设计器,踩过的坑、解决的问题、以及对未来前端开发趋势的思考。


一、为什么要自己造轮子?

市面上已有不少低代码平台,但我遇到了几个痛点:

  • 私有格式锁定:很多平台生成的是 JSON DSL,难以二次开发
  • 组件覆盖不全:想用 Ant Design 6 的新组件,往往要等平台更新
  • 定制能力有限:业务需要的特殊交互,平台往往不支持

于是决定:自己动手,丰衣足食


在这里插入图片描述

二、整体架构设计

2.1 双内核混合渲染

┌─────────────────────────────────────────────────┐
│                 用户操作层                       │
└─────────────────┬───────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────┐
│          GrapesJS 编辑内核                       │
│  - 拖拽物理引擎、DOM 选区管理、CSS 样式生成        │
└─────────────────┬───────────────────────────────┘
                  ↓ Model 变更事件
┌─────────────────────────────────────────────────┐
│              Runtime Bridge                      │
│  - 监听 Model → 转换 React Props → 挂载组件       │
└─────────────────┬───────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────┐
│        React + Ant Design 渲染层                 │
└─────────────────────────────────────────────────┘

2.2 代码结构规划

src/
├── editor/
│   ├── components/          # 组件定义
│   │   ├── AndButton.ts     # 按钮组件
│   │   ├── AndTable.ts      # 表格组件
│   │   ├── AndIterator.ts   # 迭代器组件
│   │   └── ...
│   ├── traits/              # 属性编辑器
│   │   ├── TableTrait.ts    # 表格类型属性
│   │   ├── JsonEditorTrait.ts
│   │   └── ...
│   ├── utils/               # 工具函数
│   │   ├── slotInjection.ts # Slot 注入
│   │   └── propParser.ts    # 属性解析
│   └── leiwoReactPlugin.ts  # GrapesJS 插件入口
├── react-components/        # React 组件封装
│   ├── AndButton.tsx
│   ├── AndTable.tsx
│   └── ...
└── services/
    ├── storage.ts           # 存储服务
    ├── history.ts           # 历史版本
    └── codeGenerator.ts     # 代码生成

三、代码生成策略:可重复、可覆盖、不影响业务

3.1 核心原则:UI 归设计器,逻辑归业务

生成的代码采用分层隔离策略:

project/
├── generated/               # 设计器生成,可覆盖
│   ├── pages/
│   │   ├── HomePage.tsx
│   │   └── UserList.tsx
│   └── components/
│       └── UserCard.tsx
├── business/                # 业务代码,手动维护
│   ├── hooks/
│   │   └── useUserData.ts
│   ├── services/
│   │   └── api.ts
│   └── handlers/
│       └── userHandlers.ts
└── app.tsx                  # 入口,手动维护

3.2 生成代码的结构

// generated/pages/UserList.tsx
// ⚠️ 此文件由设计器生成,请勿手动修改
// 重新生成将覆盖此文件

import { useUserListHandlers } from '@/business/handlers/userListHandlers';

export const UserList: React.FC = () => {
  // 业务逻辑通过 Hook 注入
  const handlers = useUserListHandlers();
  
  return (
    <Card title="用户列表">
      <Table 
        columns={...}
        dataSource={handlers.data}
        loading={handlers.loading}
        onRow={(record) => ({
          onClick: () => handlers.onRowClick(record)
        })}
      />
      <Button onClick={handlers.onAdd}>新增用户</Button>
    </Card>
  );
};

3.3 业务代码的结构

// business/handlers/userListHandlers.ts
// ✅ 此文件由开发者维护,设计器不会覆盖

export const useUserListHandlers = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  
  const onRowClick = (record) => {
    // 业务逻辑...
  };
  
  const onAdd = () => {
    // 业务逻辑...
  };
  
  return { data, loading, onRowClick, onAdd };
};

3.4 代码生成器实现

class CodeGenerator {
  generate(page: PageModel): string {
    const imports = this.collectImports(page);
    const handlers = this.extractHandlerRefs(page);
    const jsx = this.generateJSX(page.rootComponent);
    
    return `
// ⚠️ 此文件由设计器生成
${imports}

export const ${page.name}: React.FC = () => {
  ${handlers.map(h => `const ${h.name} = ${h.hook}();`).join('\n  ')}
  
  return (
    ${jsx}
  );
};
    `;
  }
  
  private generateJSX(component: ComponentModel): string {
    const props = this.serializeProps(component);
    const children = component.children
      .map(c => this.generateJSX(c))
      .join('\n');
    
    return `<${component.type} ${props}>${children}</${component.type}>`;
  }
}

关键设计

  • 生成的文件有明确标识,重新生成时直接覆盖
  • 业务逻辑通过「约定接口」注入,不写在生成代码中
  • 事件处理器引用 handlers.xxx,设计器只管绑定,不管实现

四、历史版本管理

4.1 版本存储结构

interface VersionRecord {
  id: string;
  pageId: string;
  timestamp: Date;
  snapshot: string;        // GrapesJS 序列化的完整状态
  thumbnail?: string;      // 缩略图
  description?: string;    // 版本描述
  author: string;
}

4.2 自动保存与手动快照

class HistoryManager {
  private autoSaveInterval = 30000; // 30秒自动保存
  
  constructor(private editor: Editor) {
    // 自动保存
    setInterval(() => this.autoSave(), this.autoSaveInterval);
    
    // 监听重要操作,创建快照
    editor.on('component:add', () => this.markDirty());
    editor.on('component:remove', () => this.markDirty());
    editor.on('component:update', () => this.markDirty());
  }
  
  // 手动创建版本
  async createVersion(description: string) {
    const snapshot = this.editor.getProjectData();
    const thumbnail = await this.captureThumbnail();
    
    return this.api.saveVersion({
      pageId: this.currentPageId,
      snapshot: JSON.stringify(snapshot),
      thumbnail,
      description,
    });
  }
  
  // 恢复版本
  async restoreVersion(versionId: string) {
    const version = await this.api.getVersion(versionId);
    const data = JSON.parse(version.snapshot);
    
    this.editor.loadProjectData(data);
  }
  
  // 版本对比(可视化 Diff)
  async compareVersions(v1: string, v2: string) {
    // 加载两个版本的组件树,生成差异报告
  }
}

4.3 版本面板 UI

┌─────────────────────────────────────┐
│  版本历史                    [新建] │
├─────────────────────────────────────┤
│  ┌─────┐ v3.0 - 添加用户表格       │
│  │ 📷  │ 2024-01-12 11:30          │
│  └─────┘ [恢复] [对比] [删除]       │
├─────────────────────────────────────┤
│  ┌─────┐ v2.0 - 调整布局           │
│  │ 📷  │ 2024-01-12 10:15          │
│  └─────┘ [恢复] [对比] [删除]       │
└─────────────────────────────────────┘

在这里插入图片描述

五、全局复制粘贴

5.1 跨页面复制的挑战

普通的复制粘贴只能在当前编辑器内使用。要实现跨页面跨项目复制,需要:

  1. 序列化组件为独立的 JSON
  2. 处理组件 ID 冲突
  3. 处理样式和资源引用

5.2 实现方案

class ClipboardManager {
  private storageKey = 'ui-designer-clipboard';
  
  async copy(components: Component[]) {
    const data = components.map(c => ({
      type: c.get('type'),
      attributes: c.getAttributes(),
      style: c.getStyle(),
      children: this.serializeChildren(c),
      // 不复制 ID,粘贴时重新生成
    }));
    
    // 存到 localStorage,支持跨 Tab 页
    localStorage.setItem(this.storageKey, JSON.stringify({
      timestamp: Date.now(),
      components: data,
    }));
    
    // 同时写入系统剪贴板(可选)
    await navigator.clipboard.writeText(JSON.stringify(data));
  }
  
  async paste(targetContainer: Component) {
    const raw = localStorage.getItem(this.storageKey);
    if (!raw) return;
    
    const { components } = JSON.parse(raw);
    
    components.forEach(compData => {
      // 递归创建组件,ID 自动生成
      targetContainer.components().add(compData);
    });
  }
}

5.3 快捷键绑定

// 注册快捷键
editor.Commands.add('clipboard:copy', {
  run: (editor) => {
    const selected = editor.getSelected();
    if (selected) {
      clipboardManager.copy([selected]);
    }
  }
});

document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.key === 'c') {
    editor.runCommand('clipboard:copy');
  }
  if (e.ctrlKey && e.key === 'v') {
    editor.runCommand('clipboard:paste');
  }
});

六、多页面 (Multi-Page) 管理

6.1 页面模型设计

interface PageModel {
  id: string;
  name: string;
  path: string;           // 路由路径
  icon?: string;
  order: number;
  parentId?: string;      // 支持页面分组
  content: string;        // GrapesJS 序列化内容
  meta: {
    title: string;
    layout: 'default' | 'full' | 'blank';
  };
}

6.2 页面切换实现

class PageManager {
  private pages: Map<string, PageModel> = new Map();
  private currentPageId: string | null = null;
  
  async switchPage(pageId: string) {
    // 1. 保存当前页面
    if (this.currentPageId) {
      await this.savePage(this.currentPageId);
    }
    
    // 2. 加载目标页面
    const page = this.pages.get(pageId);
    if (!page) throw new Error('Page not found');
    
    const data = JSON.parse(page.content);
    this.editor.loadProjectData(data);
    
    this.currentPageId = pageId;
    this.emit('page:change', page);
  }
  
  async createPage(name: string): Promise<PageModel> {
    const page: PageModel = {
      id: generateId(),
      name,
      path: `/${slugify(name)}`,
      order: this.pages.size,
      content: JSON.stringify(this.getEmptyPageData()),
      meta: { title: name, layout: 'default' },
    };
    
    await this.api.savePage(page);
    this.pages.set(page.id, page);
    
    return page;
  }
}

6.3 页面导航面板

┌─────────────────────────────┐
│  页面管理           [+ 新建] │
├─────────────────────────────┤
│  📄 首页           /home     │
│  📄 用户管理       /users    │
│  📁 系统设置                 │
│    ├─ 📄 基础设置  /settings │
│    └─ 📄 权限管理  /auth     │
└─────────────────────────────┘

七、高级组件实现

7.1 动态容器 (DynamicContainer)

动态容器支持 Flex 布局的可视化配置:

editor.Components.addType('and-dynamic-container', {
  model: {
    defaults: {
      tagName: 'div',
      droppable: true,
      traits: [
        { name: 'direction', type: 'select', 
          options: [
            { id: 'row', label: '水平' },
            { id: 'column', label: '垂直' }
          ]},
        { name: 'justify', type: 'select',
          options: [
            { id: 'flex-start', label: '起始' },
            { id: 'center', label: '居中' },
            { id: 'flex-end', label: '末尾' },
            { id: 'space-between', label: '两端对齐' },
          ]},
        { name: 'align', type: 'select', options: [...] },
        { name: 'gap', type: 'number', label: '间距' },
        { name: 'wrap', type: 'checkbox', label: '换行' },
      ],
    },
  },
  view: {
    onRender({ el, model }) {
      const style = {
        display: 'flex',
        flexDirection: model.get('direction') || 'row',
        justifyContent: model.get('justify') || 'flex-start',
        alignItems: model.get('align') || 'stretch',
        gap: `${model.get('gap') || 0}px`,
        flexWrap: model.get('wrap') ? 'wrap' : 'nowrap',
      };
      Object.assign(el.style, style);
    }
  }
});

7.2 迭代器 (Iterator) 组件

迭代器是最强大的高级组件——根据数据源动态渲染子组件:

editor.Components.addType('and-iterator', {
  model: {
    defaults: {
      tagName: 'div',
      droppable: true,  // 设计时可拖入模板组件
      traits: [
        { name: 'dataSource', type: 'text', label: '数据源' },
        { name: 'itemKey', type: 'text', label: '唯一键', default: 'id' },
        { name: 'emptyText', type: 'text', label: '空状态文案' },
      ],
    },
  },
  view: {
    onRender({ el, model }) {
      // 设计模式:显示模板 + 预览数据
      if (el.hasAttribute('editing')) {
        this.renderDesignMode(el, model);
      }
    },
    
    renderDesignMode(el, model) {
      // 获取模板(第一个子组件)
      const template = model.components().at(0);
      if (!template) {
        el.innerHTML = '<div class="empty-tip">拖入组件作为循环模板</div>';
        return;
      }
      
      // 用示例数据预览
      const sampleData = [
        { id: 1, name: '示例1' },
        { id: 2, name: '示例2' },
      ];
      
      // 渲染预览
      el.innerHTML = '';
      sampleData.forEach((item, index) => {
        const clone = template.clone();
        // 绑定数据到模板
        this.bindDataToTemplate(clone, item, index);
        el.appendChild(clone.getEl());
      });
    },
  },
});

运行时渲染

// react-components/AndIterator.tsx
export const AndIterator: React.FC<{
  dataSource: any[];
  itemKey: string;
  template: React.ReactNode;
  emptyText?: string;
}> = ({ dataSource, itemKey, template, emptyText }) => {
  if (!dataSource?.length) {
    return <Empty description={emptyText || '暂无数据'} />;
  }
  
  return (
    <>
      {dataSource.map((item, index) => (
        <IteratorContext.Provider 
          key={item[itemKey] || index}
          value={{ item, index, total: dataSource.length }}
        >
          {template}
        </IteratorContext.Provider>
      ))}
    </>
  );
};

7.3 条件渲染组件

editor.Components.addType('and-condition', {
  model: {
    defaults: {
      traits: [
        { name: 'condition', type: 'text', label: '条件表达式' },
        { name: 'showElse', type: 'checkbox', label: '显示 else 分支' },
      ],
    },
  },
});

设计时展示

┌────────────────────────────────┐
│  IF: user.role === 'admin'     │
│  ┌──────────────────────────┐  │
│  │ [管理员才能看到的内容]    │  │
│  └──────────────────────────┘  │
│  ELSE:                         │
│  ┌──────────────────────────┐  │
│  │ [普通用户看到的内容]      │  │
│  └──────────────────────────┘  │
└────────────────────────────────┘

八、AI 在开发过程中的价值

8.1 问题诊断加速

场景:页面刷新后组件不渲染

AI 分析链路

  1. 检查 [onRender]是否执行 ✓
  2. 检查 React 挂载是否完成 ✓
  3. 检查 Slot 注入是否成功 ✗ → 发现 slot 元素不存在
  4. 结论:React 18 并发渲染时序问题

解决方案:实现重试机制等待 slot 出现

8.2 模式识别与抽象

AI 发现:多个组件有相似的 slot 注入逻辑

建议的抽象

// 公共工具函数
export function injectChildrenToSlot(options) { ... }
export function waitForSlots(options) { ... }

8.3 边界情况覆盖

AI 主动提问

  • “这个组件在 iframe 预览模式下能正常工作吗?”
  • “如果用户快速连续点击会怎样?”
  • “移动端触摸事件考虑了吗?”

九、AI 时代的前端开发趋势

趋势 1:人机分工明确化

任务 谁更擅长
UI 布局 人类(视觉判断)
业务逻辑 AI(模式化)
Bug 诊断 AI 定位 + 人类决策
代码重构 AI(模式识别)

趋势 2:可视化 + AI 协作

  1. 人类 用设计器搭建 UI 骨架
  2. AI 填充业务逻辑和数据绑定
  3. 人类 微调和验收

趋势 3:开发者技能转移

  • 从「写 CSS」→「描述需求」
  • 从「记 API」→「理解架构」
  • 从「独立开发」→「人机协作」

十、总结

核心收获:

  1. GrapesJS + React 混合架构可行,但要处理 DOM 主权冲突
  2. 代码生成要分层,UI 可覆盖,业务不受影响
  3. 版本管理是必须,给用户「后悔药」
  4. 高级组件如迭代器,是提升设计器能力的关键
  5. AI 是效率倍增器,尤其在调试和重构环节

💡 AI 时代造轮子的成本已大大降低。想清楚架构,剩下的有 AI 帮你填空。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐