label-template-designer
v0.1.8
Published
Vue 3 visual label template designer with template library
Downloads
909
Readme
label-template-designer
可直接安装到 Vue 3 项目的标签模板设计器组件。组件内部使用 Shadow DOM 隔离样式,设计器常驻,模板库以 Dialog 风格弹窗显示,不监听 location.hash,也不要求宿主项目使用路由。
安装
npm install label-template-designer vue包内置 TypeScript 声明文件,包含 props、事件和模板 ref 方法的类型,TypeScript 项目不需要另行安装 @types/label-template-designer:
import LabelTemplateDesigner, {
type DataSource,
type LabelTemplateDesignerInstance,
type TemplateList,
} from 'label-template-designer'默认导出是 Vue 组件,工作区会在内部的 Shadow DOM 中运行,不需要额外引入全局 CSS。组件根节点使用 width: 100%; height: 100%,宿主项目需要给组件本身或其父级提供明确高度(例如父容器 height: 600px,或父级一路继承 height: 100%)。设计器内部样式不会修改宿主页面;宿主页面的普通 CSS 也不会穿透到设计器内部。
默认行为:
show-tmpl控制模板库弹窗是否显示;默认不显示,宿主可以使用v-model:show-tmpl双向接管;- 模板列表和模板内容由宿主传入,组件不在浏览器本地缓存模板内容;
- 点击设计器中的“模板库”打开弹窗;如果已有当前模板,弹窗顶部显示模板名称和纸张大小;
- 弹窗没有已选模板时,点击遮罩、关闭按钮或按 Esc 都不能关闭,并会触发
template-library-close-blocked; - 点击模板库中的列表项会选择模板、关闭弹窗并通过
template-request请求对应模板详情; - 点击模板库中的“预览”会以同样的模板选择流程加载详情,但进入只读预览模式;此模式仅允许修改右侧示例值、导出 HTML/JSON、打开模板库、使用该模板以及实时预览全屏/退出全屏;
- 点击模板库列表项会触发
open-template,参数为模板 ID,可用于埋点或宿主的附加处理; - 模板库支持按模板名称搜索和重置;按钮会向宿主抛出查询事件,列表区域占用弹窗剩余空间,超出后独立滚动;
- 模板加载完成后实时预览保持干净;模板再次编辑且未获宿主确认保存时显示“未保存”水印;
- 预览模式点击“使用该模板”会触发
use-template(templateData),参数结构与save的第一个参数一致;组件保持预览状态,不会自动发起保存请求。
独立库接入
label-template-designer 是一个只负责界面和编辑状态的 Vue 3 组件包。模板列表、模板详情、数据源以及所有增删改查接口都由宿主项目管理,组件通过 props 接收数据,通过事件把用户操作抛给宿主。组件不依赖宿主路由,也不会修改宿主的 URL 或 location.hash。
组件内部通过 Vue Custom Element 使用 Shadow DOM 隔离样式,但这是实现细节,Vue 项目不需要手动注册 Custom Element,也不需要额外引入 CSS。需要传数组或对象时,必须使用 Vue 的动态绑定语法,例如 :template-list、:template-detail 和 :data-source,不要写成普通字符串属性。
Props
所有 props 都是可选的。Vue 模板中推荐使用下表的短横线写法,JavaScript 渲染函数或 JSX 中使用驼峰写法。
| Vue 属性 | 模板写法 | 类型 | 默认值 | 详细说明 |
| --- | --- | --- | --- | --- |
| templateList | template-list | Array \| Object | null | 模板库数据。可以传模板数组,也可以传服务端分页对象。优先级高于 list 和 templates。 |
| list | list | Array \| Object | null | templateList 的别名,适合宿主接口直接使用 list 作为属性名;当 template-list 已传入时忽略。 |
| templates | templates | Array \| Object | null | templateList 的语义别名;当 template-list、list 已传入时忽略。 |
| showTmpl | show-tmpl | Boolean | false | 是否显示模板库弹窗。推荐使用 v-model:show-tmpl;弹窗内部交互会通过 update:showTmpl 通知宿主,同时保留 update:show-tmpl 兼容监听。 |
| pageSize | page-size | Number \| String | null | 每页数量。未传时读取分页对象中的 pageSize、limit 或 perPage,都没有时默认 10。 |
| dataSource | data-source | Array | [] | 设计器左侧数据源面板及实时预览的模拟数据。组件不会请求或修改真实业务数据。 |
| templateDetail | template-detail | Object | null | 当前模板的完整详情,至少应包含与当前选中 ID 相同的 id。列表项只用于展示和选中,设计器实际使用此对象。 |
template-list 的数据格式
直接传数组时,组件在模板库内做前端分页:
const templateList = [
{
id: 1025,
name: '商品标签',
type: 0,
paper: { width: 100, height: 150 },
elements: [],
},
]传入数组时不需要宿主处理 page-change 才能翻页,但如果需要从接口查询、搜索或删除,仍然应该监听对应事件并更新数组。
服务端分页时传入对象,并且至少包含 list 和 total;page、limit 用于让组件同步当前页和每页数量:
const templateList = {
list: [
{ id: 1025, name: '商品标签', paper: { width: 100, height: 150 }, elements: [] },
],
total: 35,
page: 1,
limit: 10,
}列表项至少需要 id。常用字段包括 name、type、paper、elements、html、is_del 以及后端时间字段。列表项只承担两项职责:在模板库中显示摘要,以及确定需要请求详情的模板 ID;不要把列表摘要当作设计器完整模板使用。
template-detail 的数据格式
宿主收到 template-request 后请求详情,再把结果传回 template-detail:
const templateDetail = {
id: 1025,
name: '商品标签',
type: 0,
paper: { width: 100, height: 150 },
elements: [
{
id: 'text-1',
type: 'text',
x: 10,
y: 10,
w: 40,
h: 8,
content: '{{name}}',
fontFamily: 'Arial',
fontSize: 10,
},
],
sampleValues: { name: '示例商品' },
}template-detail.id 必须和当前模板 ID 一致,否则工作区不会把这份详情应用到当前设计器。模板中的几何值使用毫米;sampleValues 只用于设计器实时预览,不会写入最终导出的 HTML。宿主可以额外保留后端的 html、时间字段等字段,保存事件会把模板数据快照交给宿主。
data-source 的数据格式
data-source 是分组数组,外层 title 是分组标题,内层 value 是字段数组:
const dataSource = [
{
title: '基础信息',
value: [
{ field: 'name', title: '商品名称', value: '标签示例' },
{ field: 'price', title: '价格', value: 12.5 },
],
},
{
title: '动态列表',
value: [
{ field: 'serialNumbers', title: '序列号', value: ['SN001', 'SN002'] },
],
},
]field 用于模板绑定,title 用于面板显示和搜索,value 用作示例值。条码/二维码也可在属性面板直接填写静态 content,不产生字段依赖。数组值用于动态列表;组件不会从接口自动加载数据源,也不会把数据源自动保存到模板接口。
完整接入示例
下面示例演示服务端分页、接口搜索、详情加载、保存确认以及模板库增删改查。接口地址和响应结构需要替换为宿主项目自己的实现;示例假定接口返回 { data: ... },其中列表数据为 { list, total },详情数据为模板对象。
<script setup>
import { ref } from 'vue'
import LabelTemplateDesigner from 'label-template-designer'
const showTmpl = ref(true)
const templateList = ref({ list: [], total: 0, page: 1, limit: 10 })
const templateDetail = ref(null)
const dataSource = ref([
{
title: '基础信息',
value: [{ field: 'name', title: '名称', value: '示例商品' }],
},
])
async function loadTemplateDetail(id) {
}
function onTemplateListRequest({ page, pageSize, keyword }) {
// 弹窗打开时由宿主请求列表并更新 templateList
}
function onTemplateOpen(id) {
// 模板库点击列表项时收到模板 ID,可用于埋点或附加处理。
}
function onUseTemplate(templateData) {
// 预览模式点击“使用该模板”时收到与 save 第一个参数相同的完整模板数据。
}
function onPageChange({ page, pageSize }) {
// 按 page/pageSize 重新请求,并更新 templateList
}
function onSearch({ keyword, page, pageSize }) {
// 按 keyword 从第 1 页重新请求,并更新 templateList
}
function onReset({ page, pageSize }) {
// 清空查询条件,从第 1 页重新请求,并更新 templateList
}
function onShowTmplChange(visible) {
showTmpl.value = visible
}
async function onSave(templateData, confirmSave) {
// 将 templateData(包含 templateData.html)提交给宿主接口
// 接口确认成功后必须调用 confirmSave()
}
function onJsonImportSuccess(message) {
// 导入成功;组件不会在工具栏内显示提示
console.log(message)
}
function onJsonImportError(message) {
// 导入失败;组件不会在工具栏内显示提示
console.error(message)
}
</script>
<template>
<LabelTemplateDesigner
v-model:show-tmpl="showTmpl"
:template-list="templateList"
:template-detail="templateDetail"
:data-source="dataSource"
@template-list-request="onTemplateListRequest"
@show-tmpl-change="onShowTmplChange"
@open-template="onTemplateOpen"
@template-request="loadTemplateDetail"
@page-change="onPageChange"
@search="onSearch"
@reset="onReset"
@save="onSave"
@use-template="onUseTemplate"
@json-import-success="onJsonImportSuccess"
@json-import-error="onJsonImportError"
/>
</template>组件不渲染详情加载状态、错误提示或重试弹窗;宿主可以在自己的页面中按业务处理请求状态、错误反馈、请求取消和过期响应。
Props 与事件的配合顺序
- 宿主把
show-tmpl设为true,组件触发template-list-request,宿主请求列表并把结果传给template-list;服务端分页时应包含total、当前page和limit。 - 用户在模板库点击列表项,组件更新当前模板 ID,触发
open-template、template-change、update:showTmpl(false)(并同步发出 kebab-case 兼容事件),随后触发template-request;点击“预览”时还会触发preview-template并进入只读预览模式。 - 预览模式点击“使用该模板”会触发
use-template(templateData),不会退出只读预览或触发save;普通编辑模式点击“保存”才会触发save。其中templateData与save的第一个参数结构一致。 - 宿主收到
template-request后请求完整模板详情,成功后传入匹配的template-detail;组件不会替宿主显示加载或错误 UI。 - 弹窗没有当前模板时,关闭按钮、遮罩和 Esc 会触发
template-library-close-blocked,不会关闭弹窗;已有当前模板时可正常关闭。删除当前模板后同样回到未选择状态,必须重新选择模板才能关闭。 - 用户修改模板时,设计器内部管理编辑状态;宿主不需要为了每次拖动重新请求详情。
- 用户点击保存时,宿主接收
save(templateData, confirmSave, isTemporaryTemplate);第三个参数为true时表示当前是没有真实详情的临时模板,接口成功后调用confirmSave();如果接口失败,不要调用该回调,设计器会继续保持未保存状态。 - 模板库的查询、重置、翻页、新建、重命名和删除都只抛事件,宿主完成接口调用后重新更新对应 props。
事件
| 事件 | 参数 | 触发时机和宿主处理 |
| --- | --- | --- |
| update:showTmpl | visible | 用户打开或关闭弹窗时触发,供 v-model:show-tmpl 自动同步。 |
| update:show-tmpl | visible | 与 update:showTmpl 同步发出的 DOM 事件兼容别名。 |
| show-tmpl-change | visible, reason | 弹窗由组件内部交互改变时触发,reason 说明是设计器按钮、选择模板或关闭按钮。 |
| template-library-open | { page, pageSize, keyword } | 弹窗打开时触发,宿主可据此请求初始列表。 |
| template-library-close | reason | 弹窗成功关闭时触发;没有当前模板时不会触发。 |
| template-library-close-blocked | { reason, action } | 没有选择模板却尝试关闭时触发;组件会保持弹窗打开。 |
| template-list-request | { page, pageSize, keyword } | 弹窗打开后请求初始列表;组件不发 HTTP,宿主返回数据后更新 template-list。 |
| template-change | id | 用户在模板库打开一个模板时触发,表示当前模板 ID 已切换。 |
| open-template | id | 用户点击模板库列表项时触发,只表示选择动作;详情请求仍通过 template-request 处理。 |
| preview-template | id | 用户点击模板库“预览”按钮时触发;模板选择、详情请求和弹窗关闭流程相同,但设计器进入只读预览模式。 |
| preview-mode-change | previewOnly, id | 预览模式切换时触发;true 表示只读预览,false 表示普通编辑。 |
| use-template | templateData | 预览模式点击“使用该模板”时触发;参数结构与 save 的第一个参数一致,包含模板数据和 html。组件保持只读预览,不会触发 save。 |
| template-request | id | 组件需要完整详情时触发。宿主应按 ID 请求接口并回传 template-detail;组件不会发 HTTP 或显示请求错误 UI。 |
| save | templateData, confirmSave, isTemporaryTemplate | 普通编辑模式点击“保存”时触发。第三个参数为 true 时表示当前是临时模板;templateData.html 是当前模板生成的完整 HTML;接口成功后调用 confirmSave()。预览模式的“使用该模板”不会触发此事件。 |
| create-template | { name, type, paper, elements } | 点击“新建模板”时触发,默认纸型为 100×150mm。宿主创建成功后刷新列表。 |
| rename-template | 模板参数对象 | 确认重命名时触发,原对象字段会保留,只覆盖新的 name。 |
| delete-template | id | 确认删除时触发,宿主调用删除接口后刷新列表。 |
| page-change | { page, pageSize } | 翻页时触发。服务端分页时按这两个参数重新请求列表。 |
| search | { keyword, page: 1, pageSize } | 点击“查询”或按 Enter 时触发。宿主把 keyword 作为接口查询条件,并从第 1 页开始请求。 |
| reset | { page: 1, pageSize } | 点击“重置”或按 Esc 时触发。宿主清空接口查询条件并重新请求第 1 页。 |
| json-import-success | message | JSON 导入成功时触发;消息由组件传出,组件不显示内置提示。 |
| json-import-error | message | JSON 导入失败时触发;消息由组件传出,组件不显示内置提示。 |
模板库不直接写入后端;模板 JSON 的导入/导出由设计器负责,模板持久化仍由宿主完成。导入成功或失败只触发上述事件,不会在组件内显示提示。
Ref 方法
<script setup>
import { ref } from 'vue'
import LabelTemplateDesigner from 'label-template-designer'
const designer = ref(null)
function showLibrary() {
designer.value?.openLibrary()
}
function closeLibrary() {
designer.value?.closeLibrary()
}
function openTemplate(id) {
designer.value?.openTemplate(id)
}
function previewTemplate(id) {
designer.value?.previewTemplate(id)
}
function reloadDetail() {
designer.value?.requestTemplateDetail(true)
}
function save() {
// 会触发 save 事件,并返回本次保存的 templateData 快照
const templateData = designer.value?.saveTemplate()
return templateData
}
function markSaved() {
// 仅在宿主保存接口确认成功后调用
return designer.value?.confirmSave()
}
function print() {
// 打印当前实时预览(示例值);返回 false 表示当前环境不支持打印
return designer.value?.printPreview()
}
function notifyJsonImportSuccess(message) {
return designer.value?.jsonImportSuccess(message)
}
function notifyJsonImportError(message) {
return designer.value?.jsonImportError(message)
}
</script>
<template>
<LabelTemplateDesigner ref="designer" />
<button type="button" @click="showLibrary">打开模板库</button>
<button type="button" @click="closeLibrary">关闭模板库</button>
<button type="button" @click="save">保存模板</button>
<button type="button" @click="print">打印预览</button>
</template>暴露的方法和属性如下:
| 名称 | 类型/参数 | 说明 |
| --- | --- | --- |
| showTmpl | ComputedRef<Boolean> | 模板库弹窗是否显示,只读读取状态。 |
| activeTemplateId | ComputedRef<String \| null> | 当前模板 ID,只读读取状态。 |
| previewOnly | ComputedRef<Boolean> | 是否处于模板库“预览”进入的只读模式,只读读取状态。 |
| openLibrary() | 无 | 打开模板库弹窗。 |
| closeLibrary() | reason | 关闭模板库弹窗;没有当前模板时返回 false 并触发 template-library-close-blocked。 |
| openTemplate(id) | id | 打开指定模板;模板摘要列表为空或不包含该 ID 时也可以直接打开。随后会触发详情请求事件。 |
| previewTemplate(id) | id | 以只读预览模式打开指定模板;流程与 openTemplate(id) 相同。 |
| exitPreviewMode() | 无 | 退出只读预览模式并恢复普通编辑能力;模板库中的列表项选择也会自动退出该模式。 |
| requestTemplateDetail(force = false) | force | 重新请求当前模板详情;传 true 可忽略同一 ID 的去重限制。 |
| saveTemplate() | 无 | 生成当前模板快照,触发 save 事件并返回 templateData。 |
| confirmSave() | 无 | 宿主接口成功后确认保存,清除未保存状态并显示已保存。 |
| printPreview() | 无 | 使用隐藏 iframe 调用浏览器打印,只打印当前实时预览的示例值内容,不打开新标签页;返回 false 表示当前环境不支持打印。 |
| jsonImportSuccess(message) | message | 转发一次 JSON 导入成功事件;组件不显示内置提示。设计器内部导入成功时会自动调用。 |
| jsonImportError(message) | message | 转发一次 JSON 导入失败事件;组件不显示内置提示。设计器内部导入失败时会自动调用。 |
