[开源] 从零到一打造在线 PPT 编辑器:React + Zustand + Zundo

技术栈

  • React 框架
  • TS 语言
  • Umi 脚手架
  • Zustand 状态管理
  • Zundo 状态回滚记录
  • Svg 图形
  • pptxgenjs导出为PPT,还没做
  • Ant Design UI框架

核心设计与实现

这个在线 PPT 编辑器的核心可以拆解为几个关键模块:数据模型、画布渲染器、状态管理器和属性面板

1. 数据模型:JSON

首先,我们需要定义数据结构。整个演示文稿、每一页幻灯片以及幻灯片上的每一个元素(文本、形状、图片等)都应该被结构化、序列化为 JSON 对象

typescript 复制代码
// 单个元素(文本、形状、图片等)
export interface PPTElement {
  id: string;
  type: 'text' | 'image' | 'shape' | 'line';
  left: number;
  top: number;
  width: number;
  height: number;
  rotate?: number;
  content: string; // 文本内容、图片 URL、或形状类型
  style?: {
    // ... 样式属性
  };
}

// 单张幻灯片
export interface Slide {
  id: string;
  elements: PPTElement[];
  background?: {
    color?: string;
    image?: string;
  };
}

// 整个演示文稿
export interface Presentation {
  title: string;
  slides: Slide[];
}

这种设计使得状态的读取、更新和持久化都变得非常直观

2. 状态管理:Zustand + Zundo

状态管理是编辑器的灵魂。我使用 Zustand 创建了一个 presentationStore 来统一管理所有的状态和操作。

Zundo 的集成异常简单,只需将你的状态创建函数包裹在 zundo 中间件里即可。

typescript 复制代码
import { create } from 'zustand';
import { temporal } from 'zundo'; // 引入 zundo

export const usePresentationStore = create(
  temporal( // 使用 temporal 中间件包裹
    (set, get) => ({
      slides: initialSlides, // 初始幻灯片数据
      selectedSlideId: initialSlides[0].id,
      selectedElementIds: [],

      // 添加元素
      addElement: (element) => {
        // ...
      },

      // 更新元素
      updateElement: (id, patch) => {
        // ...
      },

      // ... 其他所有操作 slides 的方法
    }),
    {
      // Zundo 配置项
      limit: 50, // 最多记录 50 步历史
    }
  )
);

现在,presentationStore 自动拥有了撤销 (undo) 和重做 (redo) 的能力。我们只需要在组件中调用它们:

tsx 复制代码
import { useTemporalStore } from '@/stores/presentationStore';

const CanvasHeader = () => {
  const { undo, redo } = useTemporalStore.temporal.getState();

  return (
    <div>
      <Tooltip title="撤销">
        <Button icon={<UndoOutlined />} onClick={() => undo()} />
      </Tooltip>
      <Tooltip title="重做">
        <Button icon={<RedoOutlined />} onClick={() => redo()} />
      </Tooltip>
      {/* ... */}
    </div>
  );
};

复杂的状态历史追溯功能被 Zundo 优雅地解决了,真香!

3. 画布与渲染器

画布是用户与 PPT 交互的核心区域。它负责根据当前 Slide 的数据,渲染出所有元素。

我创建了一个 ElementRenderer 组件,它会根据元素的 type 字段,动态地渲染出不同的子组件(如 TextElement、ImageElement 等)。

tsx 复制代码
const ElementRenderer = ({ element }: { element: PPTElement }) => {
  switch (element.type) {
    case 'text':
      return <div style={...}>{element.content}</div>;
    case 'image':
      return <img src={element.content} style={...} />;
    case 'shape':
      return <ShapeElement type={element.content} style={...} />;
    default:
      return null;
  }
};

缩略图列表的实现 :我没有为缩略图单独编写一套渲染逻辑,而是直接复用了 渲染器组件,只是通过 props 传入一个缩放比例 scale,并禁用其交互

tsx 复制代码
const SlideThumbnail = ({ slide, size }) => {
  const scale = size.width / 1920; // 假设画布标准宽度为 1920

  return (
    <div style={{ width: size.width, height: size.height }}>
      <Canvas
        slide={slide}
        scale={scale}
        interactive={false} // 禁用交互
        embedded={true}     // 嵌入模式
      />
    </div>
  );
};

4. 属性面板:响应式交互

当用户选中一个元素时,右侧的属性面板会显示其对应的可编辑属性(如颜色、字体大小、位置等)

这里的逻辑是:

  1. 监听 presentationStore 中的 selectedElementIds
  2. 当选中元素变化时,从 slides 数据中找到该元素的详细信息
  3. 将元素属性绑定到属性面板的输入框中
  4. 当用户修改输入框时,调用 store 中的 updateElement 方法来更新状态

数据驱动视图的理念

未来路线图 (Roadmap)

这个项目还有很大的想象空间,我计划在未来加入更多的功能:

  • PPT 导出 :支持将编辑好的内容导出为 .pptx 文件
  • 完备的快捷键:增加更多快捷键
  • 更多的属性配置:支持配置多种多样的样式
  • 元素对齐与分布:提供辅助线、元素吸附、水平/垂直分布等高级编辑功能
  • 动画效果:为元素添加入场、退场动画
  • 主题与模板:内置更多精美的设计模板
  • 多人实时协作:这是最具挑战性的功能,也是在线文档的终极形态

写在最后

这个项目目前还处于早期阶段,有很多不完善之处。非常欢迎大家提出宝贵的建议、报告 Bug,甚至参与到开发中来。勿喷! Star!!!!

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万4 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋4 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学4 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端