Designer
@easyink/designer 提供的是一个完整的 Vue 组件,而不是一组零散拼装件。你把它嵌进页面,传入模板、数据源和宿主能力,它就能给你一个可以工作的编辑工作台。
最小用法
<script setup lang="ts">
import { ref } from 'vue'
import { builtinDesignerMaterialBundle } from '@easyink/builtin/all'
import { EasyInkDesigner, createLocalStoragePreferenceProvider } from '@easyink/designer'
import { zhCN } from '@easyink/designer/locale'
import '@easyink/designer/index.css'
const schema = ref({})
const preferenceProvider = createLocalStoragePreferenceProvider()
const runtimeConfig = {
materials: {
bundles: [builtinDesignerMaterialBundle],
},
}
</script>
<template>
<EasyInkDesigner
v-model:schema="schema"
:locale="zhCN"
:preference-provider="preferenceProvider"
:runtime-config="runtimeConfig"
/>
</template>这段代码已经能让 Designer 跑起来,并注册全部内置设计态物料。schema 可以先传空对象,组件内部会在进入 DesignerStore 前把它归一化成完整模板。
常用输入
当前组件实现里,你最常碰到的是这些输入:
| 属性 | 作用 |
|---|---|
schema | 模板输入,支持 v-model:schema |
dataSources | 设计时字段树 |
locale | 语言包 |
preferenceProvider | 工作台偏好持久化 |
autoSave | 模板自动保存 |
fontProvider | 字体目录和字体加载器 |
runtimeConfig | 配置物料包、纸张预设和新建模板默认值 |
setupStore | 初始化 store 后做自定义注册 |
contributions | 注册面板、工具栏动作和命令扩展 |
interactionProvider | 接管确认、资产选择、上传和文件读取这类交互 |
如果你只是做业务接入,schema、dataSources、locale、preferenceProvider 和 autoSave 最常用。剩下几项通常在你开始做二次开发时才会用到。
运行时配置
如果你想注册物料包,或者给纸张下拉框加企业纸张,用 runtimeConfig。
先看一个常见配置:
import type { DesignerRuntimeConfig } from '@easyink/designer'
import { builtinDesignerMaterialBundle } from '@easyink/builtin/basic'
const runtimeConfig: DesignerRuntimeConfig = {
materials: {
bundles: [builtinDesignerMaterialBundle],
},
paper: {
mode: 'append',
presets: [
{ name: '企业标签 80x50', width: 80, height: 50 },
{ name: '热敏票据 76x130', width: 76, height: 130 },
],
defaultPreset: '企业标签 80x50',
},
}<EasyInkDesigner
v-model:schema="schema"
:runtime-config="runtimeConfig"
/>上面这段配置做了两件事:
- 物料栏注册内置基础物料。
- 在页面属性的纸张预设里追加两种企业纸张,并把新建空模板的默认纸张设为
企业标签 80x50。
你可能已经有疑问了:defaultPreset 会不会覆盖已有模板?不会。它只在输入 schema 没有显式 page.width / page.height 时生效。如果你打开的是已有模板,模板自己的页面尺寸优先。
内置物料范围
内置物料由 @easyink/builtin 提供。Designer 不会自动内置它;推荐从公开子路径选择一个集合,再把它作为普通 DesignerMaterialBundle 传给 runtimeConfig.materials.bundles。根入口 @easyink/builtin 保留 all 集合兼容导出,也提供 all/basic/none 的显式别名,但业务接入通常直接用下面的子路径更清晰。
import { builtinDesignerMaterialBundle } from '@easyink/builtin/all'
const runtimeConfig = {
materials: {
bundles: [builtinDesignerMaterialBundle],
},
}可选子路径有三个:
| 子路径 | 注册内容 | 适合场景 |
|---|---|---|
@easyink/builtin/all | 全部内置物料 | 需要完整设计能力 |
@easyink/builtin/basic | 内置基础集合:排除图表和签名,保留文本、图片、线条、矩形、数据表格、SVG 等常用物料 | 需要常用编辑能力,但不想引入图表和签名 |
@easyink/builtin/none | 空集合 | 你要完全使用自己的物料包,同时保留统一接入形态 |
这里有两个概念容易混在一起:
@easyink/builtin/basic控制的是“注册哪些内置物料能力”。- 物料栏里的“基础”“数据”“图表”等分组,来自物料 bundle 里的
catalogs。
也就是说,basic 不是物料面板分类 API。它只是一个内置物料范围枚举;真正决定物料出现在哪个分类、分类标题怎么翻译、以及分类顺序的,是 catalogs。
如果你不传内置 bundle,画布仍然能打开,但没有可拖拽的内置物料。这个模式通常要配合自定义物料 bundle 使用:
const runtimeConfig = {
materials: {
bundles: [enterpriseMaterialBundle],
},
}关于自定义物料 bundle 怎么写,可以继续看 自定义物料开发。
如果你要新增企业自己的分类,也是在自定义 bundle 里追加 catalogs:
const enterpriseMaterialBundle = {
materials: [
// 你的物料定义
],
catalogs: [
{
id: 'enterprise',
label: 'materials.catalog.enterprise',
order: 60,
items: [{ type: 'price-tag' }],
},
],
localeMessages: {
messages: {
materials: {
catalog: {
enterprise: '企业物料',
},
},
},
},
}id 是分类的稳定标识,label 是翻译 key,order 决定分类顺序。你可以复用内置的 basic、data、chart、svg、utility 来扩展已有分类,也可以用自己的 enterprise、label、ticket 这类业务分类。
纸张预设
纸张预设控制的是页面属性面板里的“纸张”下拉框。
默认情况下,Designer 会带上 A3、A4、A5、Letter、Legal 等常用纸张。如果你只是想加企业自己的纸张,用 append:
const runtimeConfig = {
paper: {
mode: 'append',
presets: [
{ name: '物流面单 100x150', width: 100, height: 150 },
],
},
}如果你的业务不想展示 EasyInk 默认纸张,只想展示自己的纸张,用 replace:
const runtimeConfig = {
paper: {
mode: 'replace',
presets: [
{ name: '价签 40x30', width: 40, height: 30 },
{ name: '吊牌 55x90', width: 55, height: 90 },
],
},
}纸张尺寸目前按模板单位里的毫米语义填写。用户选中预设时,Designer 会同步更新 page.width、page.height 和 page.pageModel.paper,所以 Viewer 和打印链路看到的是同一份页面尺寸。
提示
name 同时用作下拉显示和值匹配。我们建议你给企业纸张取稳定名称,比如 物流面单 100x150,不要用会频繁变化的营销文案。
schema 自动补齐
先看一个例子:
const schema = ref({
page: { width: 80, height: 120 },
})这份输入不完整,但 Designer 仍然能工作。原因很简单:它内部会把输入补成完整 DocumentSchema,再交给 store 和后续流程。
这也是为什么:
update:schema回传给你的会是完整模板。- 自动保存拿到的也是完整模板。
- 你不需要自己先手写所有默认字段。
自动保存与偏好持久化
很多业务项目第一次接入时会把这两件事写成一个接口,后面就会越来越乱。
先看两者各自负责什么:
| 能力 | 存什么 |
|---|---|
autoSave | 模板内容,也就是 DocumentSchema |
preferenceProvider | 窗口布局、缩放、面板开关、吸附设置 |
先看代码:
const autoSave = {
enabled: true,
delay: 1000,
save: async (schemaSnapshot) => {
await saveTemplate(schemaSnapshot)
},
}
const preferenceProvider = createLocalStoragePreferenceProvider()如果你的目标是“模板别丢”和“用户习惯别丢”,这两条能力都应该保留,但别让它们共用一套存储语义。
数据源接入
Designer 不负责请求业务数据,它只消费一份字段树。
const dataSources = [
{
id: 'order',
name: '订单',
fields: [
{ name: 'orderNo', path: 'orderNo', title: '订单号', use: 'text' },
],
},
]<EasyInkDesigner
v-model:schema="schema"
:data-sources="dataSources"
/>这时 Designer 会把字段树注册进内部数据源注册表,并在左侧面板里展示出来。用户后续做的是拖拽绑定,不是直接传运行时数据。
字体接入
如果你的模板要用业务字体,先给 fontProvider。
import type { FontProvider } from '@easyink/designer'
const fontProvider: FontProvider = {
async listFonts() {
return [
{
family: 'SourceHanSans',
displayName: '思源黑体',
weights: ['400', '700'],
styles: ['normal'],
preview: 'EasyInk 字体预览',
},
]
},
async loadFont(family, weight, style) {
return `/fonts/${encodeURIComponent(family)}-${weight ?? '400'}-${style ?? 'normal'}.woff2`
},
}<EasyInkDesigner
v-model:schema="schema"
:font-provider="fontProvider"
/>Designer 会自己负责加载字体和注入 @font-face。宿主不用再手写一套重复的注入逻辑。
setupStore 用途
当你需要在初始化时做一次注册或定制,就可以用 setupStore。
最常见的场景是注册自定义物料,或者在 store 初始化后接入自己的扩展逻辑。这个回调会在内置物料注册、fontProvider 设置之后执行。
import type { DesignerStore } from '@easyink/designer'
function setupStore(store: DesignerStore) {
console.log(store.schema.unit)
}<EasyInkDesigner
v-model:schema="schema"
:setup-store="setupStore"
/>这已经属于进阶能力了。如果你还没到这一层,先把 Designer 跑顺就好。
Contribution 扩展
如果你要挂面板、工具栏按钮或命令,用 contributions。
<EasyInkDesigner
v-model:schema="schema"
:contributions="contributions"
/>contributions 会交给内部的 ContributionRegistry 激活。它和 setupStore 的边界不一样:setupStore 适合直接操作 store 做注册,contributions 适合把面板、工具栏动作和命令作为一组扩展交给 Designer。
完整写法可以继续看 贡献扩展开发。
宿主交互接管
Designer 支持把确认、资产选择、上传、文本文件读取这类交互交给宿主控制。
先看一个完整一点的例子:
const interactionProvider = {
async confirm(request) {
return openBusinessConfirmDialog(request)
},
async pickAsset(request) {
const asset = await openAssetLibrary({
accept: request.accept,
currentUrl: request.currentUrl,
source: request.source,
payload: request.payload,
})
if (!asset)
return null
return {
url: asset.url,
assetId: asset.id,
alt: asset.alt,
}
},
async uploadAsset(request) {
const uploaded = await uploadToYourServer(request.file)
return {
url: uploaded.url,
assetId: uploaded.id,
name: request.picked.name,
}
},
async pickFileText(request) {
const file = await openTextFileDialog(request.accept)
if (!file)
return null
return {
text: await file.text(),
name: file.name,
type: file.type,
size: file.size,
}
},
}<EasyInkDesigner
v-model:schema="schema"
:interaction-provider="interactionProvider"
/>这很适合接你的业务弹窗、权限策略、审计流程和素材库。
pickAsset 面向图片这类需要稳定 URL 的资源。如果你的素材库已经返回 URL,直接返回 { url } 就行;如果用户从本地选了文件,可以让 pickAsset 返回 { file },Designer 会继续调用 uploadAsset,并把上传后的 URL 写回属性。
const interactionProvider = {
async pickAsset(request) {
const file = await openImageFileDialog(request.accept)
return file ? { file, name: file.name } : null
},
async uploadAsset(request) {
const uploaded = await uploadToYourServer(request.file)
return { url: uploaded.url }
},
}pickFileText 面向 SVG/JSON/CSS 这类需要把文件文本写入属性值的场景,不会走上传或 data URL 链路。自定义物料想触发这些能力时,不需要自己写文件输入框,而是在属性声明里加 editorOptions.valueInput:
const propSchemas = [
{
key: 'src',
label: 'materials.logo.property.src',
type: 'image',
editorOptions: {
valueInput: {
kind: 'asset-url',
id: 'designer.logo.pickImage',
source: 'logo-material',
accept: ['image/*'],
},
},
},
]这条声明和宿主回调的对应关系是固定的:
valueInput.kind: 'asset-url':调用pickAsset,如果返回{ file },继续调用uploadAsset。valueInput.kind: 'text-file':调用pickFileText,把返回的text写入属性。
关于 asset-url 和 text-file 的物料侧声明,可以继续看 自定义物料开发:属性值输入增强。
Store 访问
如果你写的是 Designer 内部子组件或贡献面板,可以直接通过 useDesignerStore() 取到 store。
import { useDesignerStore } from '@easyink/designer'
const store = useDesignerStore()这个 hook 依赖 Vue 注入,所以必须在 EasyInkDesigner 组件树内使用。脱离组件树单独调用会直接报错。
顶栏插槽与扩展入口
Designer 还提供一个顶栏插槽:
<EasyInkDesigner v-model:schema="schema">
<template #topbar>
<div>My Header</div>
</template>
</EasyInkDesigner>如果你需要的不是简单插槽,而是按钮、面板、命令和诊断订阅,那就继续往 贡献扩展开发 看。
样式导入
最后一个容易漏掉的点,是样式入口:
import '@easyink/designer/index.css'如果你看到组件能挂载,但界面布局明显不对,先检查这里。
关于 Designer,目前知道这些就够用了。接下来最适合继续读的是: