Skip to content

Designer

@easyink/designer 提供的是一个完整的 Vue 组件,而不是一组零散拼装件。你把它嵌进页面,传入模板、数据源和宿主能力,它就能给你一个可以工作的编辑工作台。

最小用法

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接管确认、资产选择、上传和文件读取这类交互

如果你只是做业务接入,schemadataSourceslocalepreferenceProviderautoSave 最常用。剩下几项通常在你开始做二次开发时才会用到。

运行时配置

如果你想注册物料包,或者给纸张下拉框加企业纸张,用 runtimeConfig

先看一个常见配置:

ts
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',
  },
}
vue
<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 的显式别名,但业务接入通常直接用下面的子路径更清晰。

ts
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 使用:

ts
const runtimeConfig = {
  materials: {
    bundles: [enterpriseMaterialBundle],
  },
}

关于自定义物料 bundle 怎么写,可以继续看 自定义物料开发

如果你要新增企业自己的分类,也是在自定义 bundle 里追加 catalogs

ts
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 决定分类顺序。你可以复用内置的 basicdatachartsvgutility 来扩展已有分类,也可以用自己的 enterpriselabelticket 这类业务分类。

纸张预设

纸张预设控制的是页面属性面板里的“纸张”下拉框。

默认情况下,Designer 会带上 A3、A4、A5、Letter、Legal 等常用纸张。如果你只是想加企业自己的纸张,用 append

ts
const runtimeConfig = {
  paper: {
    mode: 'append',
    presets: [
      { name: '物流面单 100x150', width: 100, height: 150 },
    ],
  },
}

如果你的业务不想展示 EasyInk 默认纸张,只想展示自己的纸张,用 replace

ts
const runtimeConfig = {
  paper: {
    mode: 'replace',
    presets: [
      { name: '价签 40x30', width: 40, height: 30 },
      { name: '吊牌 55x90', width: 55, height: 90 },
    ],
  },
}

纸张尺寸目前按模板单位里的毫米语义填写。用户选中预设时,Designer 会同步更新 page.widthpage.heightpage.pageModel.paper,所以 Viewer 和打印链路看到的是同一份页面尺寸。

提示

name 同时用作下拉显示和值匹配。我们建议你给企业纸张取稳定名称,比如 物流面单 100x150,不要用会频繁变化的营销文案。

schema 自动补齐

先看一个例子:

ts
const schema = ref({
  page: { width: 80, height: 120 },
})

这份输入不完整,但 Designer 仍然能工作。原因很简单:它内部会把输入补成完整 DocumentSchema,再交给 store 和后续流程。

这也是为什么:

  • update:schema 回传给你的会是完整模板。
  • 自动保存拿到的也是完整模板。
  • 你不需要自己先手写所有默认字段。

自动保存与偏好持久化

很多业务项目第一次接入时会把这两件事写成一个接口,后面就会越来越乱。

先看两者各自负责什么:

能力存什么
autoSave模板内容,也就是 DocumentSchema
preferenceProvider窗口布局、缩放、面板开关、吸附设置

先看代码:

ts
const autoSave = {
  enabled: true,
  delay: 1000,
  save: async (schemaSnapshot) => {
    await saveTemplate(schemaSnapshot)
  },
}

const preferenceProvider = createLocalStoragePreferenceProvider()

如果你的目标是“模板别丢”和“用户习惯别丢”,这两条能力都应该保留,但别让它们共用一套存储语义。

数据源接入

Designer 不负责请求业务数据,它只消费一份字段树。

ts
const dataSources = [
  {
    id: 'order',
    name: '订单',
    fields: [
      { name: 'orderNo', path: 'orderNo', title: '订单号', use: 'text' },
    ],
  },
]
vue
<EasyInkDesigner
  v-model:schema="schema"
  :data-sources="dataSources"
/>

这时 Designer 会把字段树注册进内部数据源注册表,并在左侧面板里展示出来。用户后续做的是拖拽绑定,不是直接传运行时数据。

字体接入

如果你的模板要用业务字体,先给 fontProvider

ts
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`
  },
}
vue
<EasyInkDesigner
  v-model:schema="schema"
  :font-provider="fontProvider"
/>

Designer 会自己负责加载字体和注入 @font-face。宿主不用再手写一套重复的注入逻辑。

setupStore 用途

当你需要在初始化时做一次注册或定制,就可以用 setupStore

最常见的场景是注册自定义物料,或者在 store 初始化后接入自己的扩展逻辑。这个回调会在内置物料注册、fontProvider 设置之后执行。

ts
import type { DesignerStore } from '@easyink/designer'

function setupStore(store: DesignerStore) {
  console.log(store.schema.unit)
}
vue
<EasyInkDesigner
  v-model:schema="schema"
  :setup-store="setupStore"
/>

这已经属于进阶能力了。如果你还没到这一层,先把 Designer 跑顺就好。

Contribution 扩展

如果你要挂面板、工具栏按钮或命令,用 contributions

vue
<EasyInkDesigner
  v-model:schema="schema"
  :contributions="contributions"
/>

contributions 会交给内部的 ContributionRegistry 激活。它和 setupStore 的边界不一样:setupStore 适合直接操作 store 做注册,contributions 适合把面板、工具栏动作和命令作为一组扩展交给 Designer。

完整写法可以继续看 贡献扩展开发

宿主交互接管

Designer 支持把确认、资产选择、上传、文本文件读取这类交互交给宿主控制。

先看一个完整一点的例子:

ts
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,
    }
  },
}
vue
<EasyInkDesigner
  v-model:schema="schema"
  :interaction-provider="interactionProvider"
/>

这很适合接你的业务弹窗、权限策略、审计流程和素材库。

pickAsset 面向图片这类需要稳定 URL 的资源。如果你的素材库已经返回 URL,直接返回 { url } 就行;如果用户从本地选了文件,可以让 pickAsset 返回 { file },Designer 会继续调用 uploadAsset,并把上传后的 URL 写回属性。

ts
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

ts
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-urltext-file 的物料侧声明,可以继续看 自定义物料开发:属性值输入增强

Store 访问

如果你写的是 Designer 内部子组件或贡献面板,可以直接通过 useDesignerStore() 取到 store。

ts
import { useDesignerStore } from '@easyink/designer'

const store = useDesignerStore()

这个 hook 依赖 Vue 注入,所以必须在 EasyInkDesigner 组件树内使用。脱离组件树单独调用会直接报错。

顶栏插槽与扩展入口

Designer 还提供一个顶栏插槽:

vue
<EasyInkDesigner v-model:schema="schema">
  <template #topbar>
    <div>My Header</div>
  </template>
</EasyInkDesigner>

如果你需要的不是简单插槽,而是按钮、面板、命令和诊断订阅,那就继续往 贡献扩展开发 看。

样式导入

最后一个容易漏掉的点,是样式入口:

ts
import '@easyink/designer/index.css'

如果你看到组件能挂载,但界面布局明显不对,先检查这里。

关于 Designer,目前知道这些就够用了。接下来最适合继续读的是: