跳到主要内容

Store

store 是 Ydesign 编辑器的响应式数据中心。所有 UI 状态(画布尺寸、缩放、选中、字体、背景等)和画布引用(editor)都收敛在一个 MobX-State-Tree(简称 MST)实例上。

你可以理解为:

React / Vue 组件 ──读──►  store  ◄──写──  用户交互

└──► store.editor.xxxHandler ──► Fabric 画布

组件只读 / 写 store,所有画布操作都通过 store.editor 下面的 Handler 下发;反过来,画布事件会自动同步回 store,视图自动更新。

💡 与 Polotno 的 Store 不同,Ydesign 的数据模型里没有 Page、没有 Element。对象即 Fabric.js 的原生 FabricObject。多页能力后续通过 SceneHandler 扩展,详见该页。


创建 Store

import { createStore } from '@ydesign/react-editor';

const store = createStore({
key: 'YOUR_API_KEY', // 调用后端能力(字体、模板、AI)的凭据
token: 'user-token', // 可选:业务登录态
workspaceId: 'ws-001', // 可选:多租户隔离
});

返回的是 MST 实例。所有状态字段和动作方法都挂在它上面。

<DesignEditorApp /> 的关系

一次性启动完整编辑器时,createDesignEditorApp自动创建并返回 store

import { createDesignEditorApp } from '@ydesign/react-editor';

const { store } = createDesignEditorApp({
container: document.getElementById('root'),
key: 'YOUR_API_KEY',
});

// store 就是同一个实例,后续所有读写都基于它
store.setSize({ width: 1200, height: 800 });

基本状态字段

字段类型说明
widthnumber画布宽度(单位:像素)
heightnumber画布高度(单位:像素)
scalenumber当前缩放比,1 表示 100%
scaleToFitnumber"适配屏幕"下的基础缩放值
unit'px' | 'mm' | 'cm' | 'in'画布单位
dpinumber打印分辨率,默认 72
bleednumber出血位(印刷场景)
bleedVisibleboolean是否显示出血线
rulesVisibleboolean是否显示标尺
backgroundColorstring | object背景色,支持纯色或渐变对象
selectedElementsIdsstring[]当前选中的元素 ID 列表
fontsFont[]当前设计用到的字体
openedSidePanelstring当前打开的侧边面板名(templates / text / ...)
editorEditor | null@ydesign/coreEditor 实例,持有所有 Handler
objectsArray<any>画布上所有元素的快照(只读镜像)

editor 字段是"面向画布的遥控器":所有画布操作(添加元素、缩放、裁剪、历史等)都通过 store.editor.xxxHandler.xxx() 下发。详见 元素操作场景操作


常用派生视图

MST 的 views 让你可以拿到"从源状态计算出"的派生值:

// 当前选中的元素(Fabric 实例)
store.selectedElements;
// => FabricObject[]

// 递归展开选中(组里的子元素一并返回)
store.selectedShapes;
// => FabricObject[](不含 group 自身)

// 按 ID 拿到元素
store.getElementById('abc123');

// 按回调条件查找第一个元素(递归搜索 group)
store.find(e => e.type === 'textbox');

常用 Actions(UI 层)

这些 actions 是用户交互时常用的入口:

画布尺寸 / 单位 / 背景

// 画布尺寸(会同步到画布)
store.setSize({ width: 1200, height: 800 });

// 单位 / DPI
store.setUnit({ unit: 'mm', dpi: 300 });

// 背景色(支持纯色 / 渐变)
store.setBackgroundColor('#f0f0f0');
store.setBackgroundColor({
type: 'linear',
coords: { x1: 0, y1: 0, x2: 1, y2: 1 },
colorStops: [
{ offset: 0, color: '#ff6a00' },
{ offset: 1, color: '#2253eb' },
],
});

// 缩放(仅改 UI 字段;缩放画布请用 store.editor.zoomHandler)
store.setScale(1);

选中 / 批量操作

// 选中一批元素
store.selectElements(['id-1', 'id-2']);

// 添加新元素(来自 JSON)
store.addElement({
type: 'textbox',
text: 'Hello Ydesign',
left: 100,
top: 100,
fontSize: 48,
fill: '#111',
});

// 批量修改属性(作用在当前选中,或指定 element)
store.set({ opacity: 0.8 });
store.set({ fill: '#ff6a00' }, someElement);

// 批量删除
store.deleteElements(['id-1', 'id-2']);

// 克隆当前选中
store.clone();

图层

store.moveElementsUp(['id-1']); // 向上一层
store.moveElementsDown(['id-1']); // 向下一层
store.moveElementsTop(['id-1']); // 置顶
store.moveElementsBottom(['id-1']); // 置底

// 按钮态判断
store.canMoveElementsUp(['id-1']); // => boolean
store.canMoveElementsTop(['id-1']);

详见 元素操作 · 图层

字体

store.addFont({
fontFamily: 'Noto Sans SC',
url: 'https://cdn.example.com/noto-sans-sc.woff2',
});

store.removeFont('Noto Sans SC');

// 按需预加载某个字体(返回 Promise,等待字体可用)
await store.loadFont('Noto Sans SC');

字体有 用户字体(存在 store.fonts,随设计一起导出)与 全局字体(进程级注册,不进入 JSON)两种。详见 编辑器配置 · 字体管理

撤销 / 重做

历史栈由 HistoryHandler 管理,暴露在 editor.historyHandler

store.editor?.historyHandler.undo();
store.editor?.historyHandler.redo();

// 查询是否可以继续撤销 / 重做
const { hasUndo, hasRedo } = store.editor!.historyHandler.getStatus();

导入 / 导出

// 导出 JSON(完整设计稿)
const json = store.toJSON();

// 从 JSON 加载设计稿
await store.loadJSON(json);

// 导出图片(base64 / Blob / 下载到本地)
const dataUrl = await store.toDataURL({ multiplier: 2, format: 'png' });
const blob = await store.toBlob({ multiplier: 2, format: 'jpeg', quality: 0.9 });
await store.saveAsImage({ multiplier: 2, format: 'png', fileName: 'my-design.png' });

完整导出选项见 场景操作 · 导入 / 导出


响应式:订阅状态变化

方式 A:React observer

@ydesign/react-editor 已经把 observer 用在所有内置组件上。你自己的组件也可以:

import { observer } from 'mobx-react-lite';

const MyStatusBar = observer(({ store }) => {
return (
<div>
画布:{store.width} × {store.height} px|选中 {store.selectedElementsIds.length} 个元素
</div>
);
});

方式 B:MobX reaction / autorun

适合和 React 无关的业务逻辑(自动保存、脏标记、统计等):

import { reaction } from 'mobx';

reaction(
() => store.toJSON(),
json => {
localStorage.setItem('design-draft', JSON.stringify(json));
},
{ delay: 1000 } // 1s 节流
);

方式 C:store.on('change', cb)

store.objects(画布对象快照)发生变化时触发,专门用于"设计稿改动检测":

const dispose = store.on('change', objects => {
console.log('画布对象变动:', objects.length, '个元素');
});

// 取消订阅
dispose();

序列化

直接用 MST 的 getSnapshot

import { getSnapshot } from 'mobx-state-tree';

const snapshot = getSnapshot(store);
// => 纯 JSON:{ width, height, scale, backgroundColor, fonts, ... }

如果你只关心画布内容(元素 + 工作区),用 store.toJSON() 更直接,它只包含 Fabric 可再序列化的对象数据。


完整字段 / 方法参考


FAQ

Q:为什么没有 Page / Element 类? A:Ydesign 底层直接用 Fabric.js 的 FabricObject,"元素"就是 Fabric 对象。好处是无额外抽象层、生态兼容性好、上手成本低。Polotno 的 Element 类更像一个适配层,Ydesign 认为多此一举。

Q:我的设计有多页,怎么办? A:当前版本只支持单页。多页能力将通过 SceneHandler 扩展,每一页对应一份独立的画布 JSON,切换时通过 importFromJSON 热加载。详见场景文档。

Q:能直接 mutate store.width = 1000 吗? A:不能。MST 的顶层字段只接受 action 写入,请用 store.setSize({ width: 1000, height: ... })

Q:store.editor 为什么可能是 null A:Editor 是在挂载 <Workspace /> 时异步创建的。使用前记得判空(或者用 ?. 调用):store.editor?.zoomHandler.zoomToFit()