

技术栈
- 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. 属性面板:响应式交互
当用户选中一个元素时,右侧的属性面板会显示其对应的可编辑属性(如颜色、字体大小、位置等)
这里的逻辑是:
- 监听
presentationStore中的selectedElementIds - 当选中元素变化时,从
slides数据中找到该元素的详细信息 - 将元素属性绑定到属性面板的输入框中
- 当用户修改输入框时,调用
store中的updateElement方法来更新状态
数据驱动视图的理念
未来路线图 (Roadmap)
这个项目还有很大的想象空间,我计划在未来加入更多的功能:
- PPT 导出 :支持将编辑好的内容导出为
.pptx文件 - 完备的快捷键:增加更多快捷键
- 更多的属性配置:支持配置多种多样的样式
- 元素对齐与分布:提供辅助线、元素吸附、水平/垂直分布等高级编辑功能
- 动画效果:为元素添加入场、退场动画
- 主题与模板:内置更多精美的设计模板
- 多人实时协作:这是最具挑战性的功能,也是在线文档的终极形态
写在最后
这个项目目前还处于早期阶段,有很多不完善之处。非常欢迎大家提出宝贵的建议、报告 Bug,甚至参与到开发中来。勿喷! Star!!!!