shenlan-designer
shenlan-designer 组件包使用文档
shenlan-designer 是一个基于 Konva.js 的 POD 商品设计器组件包。组件包只负责设计器核心能力:画布、产品模板、设计元素、选择变换、图层、撤销重做、导出、生产校验和事件通知。商品价格、购物车、订单、素材库、页面布局等业务能力应放在业务项目中实现。
安装
pnpm add shenlan-designer
也支持 npm / yarn:
npm install shenlan-designer
yarn add shenlan-designer
本地联调未发布版本时,可以使用 file: 或打包文件:
pnpm add "file:C:/Work/Front/shenlan-designer/packages/designer"
快速开始
import { createDesigner, createTshirtTemplate, type Designer } from "shenlan-designer";
const container = document.querySelector<HTMLDivElement>("#designer");
if (!container) {
throw new Error("缺少设计器容器");
}
const designer: Designer = createDesigner(container, {
product: createTshirtTemplate(),
ui: {
title: "基础按需印制定制短袖",
assets: [{ id: "badge", name: "徽章", src: "/badge.svg" }]
}
});
Vue 3 中常见写法:
<template>
<div ref="designerRef" class="h-full min-h-[600px]"></div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
import { createDesigner, createTshirtTemplate, type Designer } from "shenlan-designer";
const designerRef = ref<HTMLDivElement>();
const designer = shallowRef<Designer>();
const unsubscribers: Array<() => void> = [];
onMounted(() => {
if (!designerRef.value) return;
designer.value = createDesigner(designerRef.value, {
product: createTshirtTemplate(),
ui: {
title: "基础按需印制定制短袖"
}
});
unsubscribers.push(
designer.value.on("selection:change", ({ ids }) => {
console.log("当前选中元素", ids);
})
);
});
onBeforeUnmount(() => {
unsubscribers.forEach((unsubscribe) => unsubscribe());
designer.value?.destroy();
});
</script>
产品模板说明与常见品类示例
createTshirtTemplate() 是组件包内置的 T 恤默认模板。它的作用是让你快速启动设计器,不用一开始就手写完整的 ProductTemplate。
它不是必须使用的。正式业务中,如果商品是杯子、帆布袋、手机壳或其他 POD 商品,可以自己从后端商品数据组装 ProductTemplate,再传给 createDesigner。
设计器真正依赖的是产品模板里的这些信息:
interface ProductTemplate {
id: string;
name: string;
category: string;
previewMode?: "2d" | "3d";
colors: ProductColor[];
sizes: ProductSize[];
views: ProductViewTemplate[];
}
其中最重要的是 views。每个 view 表示一个可设计面,例如正面、背面、杯身展开面、手机壳背面等。
previewMode 告诉预览组件当前商品用 2D 实拍贴图还是 3D 模型预览。省略时默认为 "2d"。某一面也可以单独写 views[].previewMode 覆盖产品默认值。设计稿始终是 2D 展开面,这个字段只影响效果图,不影响印刷文件。
杯子模板示例
import { createDesigner, type ProductTemplate } from "shenlan-designer";
const mugProduct: ProductTemplate = {
id: "mug-basic",
name: "马克杯",
category: "mug",
previewMode: "3d",
colors: [
{ id: "white", name: "白色", value: "#ffffff" }
],
sizes: [
{ id: "default", name: "默认" }
],
views: [
{
id: "wrap",
name: "杯身展开面",
canvas: { width: 1200, height: 500 },
printArea: {
x: 80,
y: 60,
width: 1040,
height: 380,
safeInset: 24,
bleed: 16
},
model3d: {
src: "https://cdn.example.com/models/mug-11oz.glb",
wrapTarget: "PrintArea",
colorTarget: "Body",
primitive: "cylinder"
}
}
]
};
const designer = createDesigner(container, {
product: mugProduct,
mockup: previewEl,
toolbar: false
});
designer.getPreviewMode(); // "3d"
帆布袋模板示例
import type { ProductTemplate } from "shenlan-designer";
const bagProduct: ProductTemplate = {
id: "canvas-bag",
name: "帆布袋",
category: "bag",
colors: [
{ id: "natural", name: "本白", value: "#f5f0e6" },
{ id: "black", name: "黑色", value: "#111111" }
],
sizes: [
{ id: "default", name: "默认" }
],
views: [
{
id: "front",
name: "正面",
canvas: { width: 900, height: 1100 },
printArea: {
x: 180,
y: 260,
width: 540,
height: 620,
safeInset: 24,
bleed: 16
}
},
{
id: "back",
name: "背面",
canvas: { width: 900, height: 1100 },
printArea: {
x: 180,
y: 260,
width: 540,
height: 620,
safeInset: 24,
bleed: 16
}
}
]
};
手机壳模板示例
import type { ProductTemplate } from "shenlan-designer";
const phoneCaseProduct: ProductTemplate = {
id: "phone-case-15",
name: "手机壳",
category: "phone-case",
colors: [
{ id: "clear", name: "透明", value: "#ffffff" }
],
sizes: [
{ id: "iphone-15", name: "iPhone 15" }
],
views: [
{
id: "back",
name: "背面",
canvas: { width: 600, height: 1200 },
printArea: {
x: 60,
y: 80,
width: 480,
height: 1040,
safeInset: 30,
bleed: 20
}
}
]
};
后续如果某些品类会频繁使用,也可以在组件包中继续增加快捷方法,例如 createMugTemplate()、createCanvasBagTemplate()、createPhoneCaseTemplate()。但业务项目始终可以直接传自己的 ProductTemplate,不需要被内置模板限制。
初始化选项
createDesigner(container, options);
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
product |
ProductTemplate |
createTshirtTemplate() |
产品模板。 |
document |
DesignerDocument |
自动创建 | 已保存的设计文档。传入后优先使用文档。 |
branding |
string | DesignerBranding |
无 | SaaS 租户水印文本。可直接传字符串,斜向铺在红线框外的灰色工作区,不进入设计稿和印刷图。 |
brandingVisible |
boolean |
true |
是否显示企业水印。可在运行时用 setBrandingVisible 开关。 |
width |
number |
容器宽度 | 画布显示宽度。 |
height |
number |
容器高度 | 画布显示高度。 |
keyboard |
boolean |
整页编辑器下默认 true,仅画布模式下默认 false |
是否启用撤销/复制/删除等快捷键。按住空格拖动画布平移不依赖此选项。 |
keyboardTarget |
EventTarget |
window |
快捷键监听目标。 |
toolbar |
boolean |
仅画布模式下默认 true |
旧版画布小工具条。整页编辑器开启时不会出现。 |
ui |
boolean | DesignerUiOptions |
true |
整页编辑器壳。默认开启,安装后即可得到和示例相同的工作台。设为 false 则只挂画布。 |
guides |
boolean | CanvasGuideOptions |
整页编辑器下为示例辅助线,仅画布模式下为 true |
是否显示辅助线,支持细分控制。 |
默认会挂载整页编辑器。容器需要有明确高度,例如 height: 100vh。
createDesigner(container, {
product: createTshirtTemplate(),
branding: "创客工作室",
ui: {
topbar: true,
sidebar: true,
inspector: true,
canvasToolbar: true,
floatingToolbar: true,
floatingPreview: true,
branding: true,
zoomControl: true,
viewSwitcher: true,
commerce: true,
productTab: true,
assetsTab: true,
uploadTab: true,
backgroundTab: true,
layersTab: true,
title: "基础按需印制定制短袖",
assets: [{ id: "badge", name: "徽章", src: "/badge.svg" }]
}
});
某个功能设为 false 即隐藏,省略则显示。运行时也可以:
designer.setUiFeature("sidebar", false);
designer.getUiFeature("inspector"); // true
只想要中间画布时:
const designer = createDesigner(container, {
product: createTshirtTemplate(),
ui: false,
keyboard: true
});
Designer 实例 API
文档与视图
getDocument()
获取当前设计文档的 JSON 克隆。
const document = designer.getDocument();
loadDocument(document)
加载设计文档。加载后会清空撤销重做历史,并清空选区。
designer.loadDocument(savedDocument);
setProduct(product)
切换产品模板,并创建新的空设计文档。
designer.setProduct(createTshirtTemplate());
getViews()
获取当前产品的所有设计视图,例如正面、背面、袖子等。
const views = designer.getViews();
getActiveView()
获取当前正在编辑的视图。
const activeView = designer.getActiveView();
setActiveView(viewId)
切换当前编辑视图。
designer.setActiveView("front");
添加元素
addText(options)
添加文字元素,返回元素 id。
const id = designer.addText({
text: "我的定制文字",
x: 120,
y: 160,
fontSize: 48,
fill: "#111827"
});
支持指定视图:
designer.addText({
viewId: "back",
text: "背面文字"
});
addImage(options)
添加图片元素,返回元素 id。图片会异步加载自然尺寸。
const id = await designer.addImage({
src: "https://example.com/image.png",
x: 100,
y: 120,
width: 300,
height: 300
});
支持指定视图:
await designer.addImage({
viewId: "front",
src: imageUrl
});
更新与删除元素
updateElement(id, patch)
更新元素属性。
designer.updateElement(id, {
x: 180,
y: 220,
fill: "#2563eb",
opacity: 0.8
});
removeElements(ids?)
删除指定元素。不传 ids 时删除当前选中的元素。
designer.removeElements(["text-1"]);
designer.removeElements();
选择
designer.select(["text-1"]);
designer.selectAll();
designer.clearSelection();
const ids = designer.getSelection();
微移与图层
designer.nudgeSelection({ x: 1, y: 0 });
designer.bringForward();
designer.sendBackward();
designer.bringToFront();
designer.sendToBack();
这些方法默认作用于当前选区,也可以传入元素 id:
designer.bringToFront(["image-1"]);
复制、剪切、粘贴、复制一份
const copiedIds = designer.copy();
const cutIds = designer.cut();
const pastedIds = designer.paste({ offset: { x: 20, y: 20 } });
const duplicatedIds = designer.duplicate();
撤销重做
designer.undo();
designer.redo();
const canUndo = designer.canUndo();
const canRedo = designer.canRedo();
辅助线与网格线
辅助线用于 POD 生产提示和位置对齐:
- 网格线:画布中的细小位置辅助线,用于对齐、估算间距和观察素材位置。
- 灰色边框:画布边界。
- 蓝色实线:打印区域。
- 绿色虚线:安全区域,重要内容建议放在内部。
- 橙色虚线:出血/风险区域,靠近或超出可能被裁切或印偏。
- 出血区域外阴影:提示该区域不建议操作,橙色出血范围内才是设计风险可控区域。
关闭全部辅助线:
designer.setGuides(false);
只控制网格线:
designer.setGridVisible(false);
designer.setGridVisible(true);
const visible = designer.getGridVisible();
细分控制显示项和网格样式:
designer.setGuides({
canvasBorder: true,
grid: {
visible: true,
size: 100,
color: "#94a3b8",
opacity: 0.32
},
printArea: true,
safeArea: false,
bleedArea: false,
outsideBleedMask: true
});
业务项目也可以通过 API 传入对应矩形范围,覆盖产品模板里的默认打印范围:
designer.setGuides({
printArea: {
visible: true,
area: { x: 120, y: 0, width: 583, height: 1200 }
},
safeArea: {
visible: true,
area: { x: 150, y: 30, width: 523, height: 1140 }
},
bleedArea: {
visible: true,
area: { x: 108, y: 0, width: 607, height: 1200 }
},
outsideBleedMask: {
visible: true,
fill: "#111827",
opacity: 0.34
}
});
读取当前配置:
const guides = designer.getGuides();
初始化时也可以配置:
createDesigner(container, {
product: createTshirtTemplate(),
guides: {
grid: true,
printArea: true,
safeArea: true,
bleedArea: false,
outsideBleedMask: true
}
});
CanvasGuideOptions 常用结构:
interface CanvasGuideOptions {
canvasBorder?: boolean;
grid?: boolean | {
visible?: boolean;
size?: number;
color?: string;
opacity?: number;
};
printArea?: boolean | { visible?: boolean; area?: Rect };
safeArea?: boolean | { visible?: boolean; area?: Rect };
bleedArea?: boolean | { visible?: boolean; area?: Rect };
outsideBleedMask?: boolean | {
visible?: boolean;
fill?: string;
opacity?: number;
};
}
导出
exportJSON()
导出设计 JSON。
const json = designer.exportJSON();
exportImage(options?)
导出当前视图或指定视图图片。
const result = await designer.exportImage({
viewId: "front",
area: "print",
pixelRatio: 2,
transparent: true
});
console.log(result.dataUrl);
常用选项:
| 选项 | 类型 | 说明 |
|---|---|---|
viewId |
string |
指定导出视图,不传则导出当前视图。 |
area |
"canvas" | "print" |
导出整个画布或打印区域。 |
pixelRatio |
number |
导出倍率。 |
transparent |
boolean |
是否透明背景。 |
backgroundColor |
string |
非透明背景时的背景色。 |
mimeType |
"image/png" | "image/jpeg" | "image/webp" |
图片格式。 |
quality |
number |
JPEG/WebP 质量。 |
返回值:
interface ExportResult {
dataUrl: string;
width: number;
height: number;
mimeType: string;
}
exportAllViews(options?)
导出所有视图,适合做组合预览或提交整套设计图。
const results = await designer.exportAllViews({
area: "print",
pixelRatio: 2,
transparent: true
});
for (const item of results) {
console.log(item.viewId, item.viewName, item.result.dataUrl);
}
返回值:
interface ExportAllViewsResult {
viewId: string;
viewName: string;
result: ExportResult;
}
生产校验
const warnings = designer.validateForPrint();
可能返回:
- 空设计。
- 元素超出打印区域。
- 元素超出安全区域。
- 图片自然尺寸不足。
事件
on(type, listener)
订阅事件,返回取消订阅函数。
const unsubscribe = designer.on("selection:change", ({ ids }) => {
console.log(ids);
});
unsubscribe();
事件列表:
| 事件 | 数据 | 说明 |
|---|---|---|
document:change |
{ document } |
文档变化。 |
selection:change |
{ ids } |
选区变化。 |
element:change |
{ ids } |
元素变化。 |
viewport:change |
{ zoom, pan } |
画布缩放或平移。 |
warning |
{ code, message, details } |
警告。 |
export:done |
{ type, result } |
导出完成。 |
销毁
页面卸载时必须销毁实例。
designer.destroy();
多视图、设计稿与实拍贴合
设计稿和效果图是两个空间:
- 设计稿:Konva 编辑器,使用印刷坐标系,是生产数据。
- 效果图:Pixi 把当前打印区贴到带透视/皱褶的商品实拍上,只用于预览。
const designer = createDesigner(editorEl, {
product: createTshirtTemplate(),
mockup: mockupEl,
mockupQuality: "live",
toolbar: false,
keyboard: true
});
designer.setProductColor("navy");
designer.setActiveView("front");
await designer.addImage({ src: uploadedUrl });
const printFile = await designer.exportImage({
area: "print",
transparent: true,
pixelRatio: 2
});
const mockupPreview = await designer.exportMockupImage({
mimeType: "image/jpeg"
});
const mockupSheet = await designer.exportMockupImage({
layout: "sheet",
mimeType: "image/jpeg"
});
产品模板可以提供 Smart Mockup 资源:preview.base、mask、displacement、lighting,以及 mapping.dst 四点透视。没有位移图时会降级为四点贴图。印刷文件仍然只使用 printArea 导出,不要把效果图交给工厂。
预览后端由产品上的 previewMode 决定,不要用 category 猜测:
"2d"(默认):Pixi Smart Mockup,适合 T 恤、帆布袋等实拍贴图。"3d":Three.js 加载model3d.src的 glTF / glb,把当前印刷图贴到wrapTarget网格上,可拖转。没有src或加载失败时,降级为圆柱 / 盒子 / 平面。- 某一面可写
views[].previewMode覆盖产品默认值。 designer.getPreviewMode()读取当前生效模式;setProduct()换商品时预览组件会跟着切换。- 事件
preview:modechange会在 2D / 3D 后端切换时通知宿主。
后端 3D 模型数据契约
运营在后台上传的是 glTF 场景文件(推荐单个 .glb),接口把它的地址和贴图目标发给设计器。设计器 不会 再向你们的上传接口发请求,只消费下面这份 JSON。
商品初始化(可直接作为 DesignerBootstrap,或组装成 ProductTemplate):
{
"product": {
"id": "mug-11oz",
"name": "11oz 马克杯",
"category": "mug",
"previewMode": "3d",
"colors": [
{ "id": "white", "name": "白色", "value": "#ffffff" },
{ "id": "black", "name": "黑色", "value": "#111111" }
],
"sizes": [{ "id": "default", "name": "默认" }]
},
"views": [
{
"id": "wrap",
"name": "杯身展开面",
"dpi": 300,
"print": { "widthCm": 21.0, "heightCm": 9.5 },
"previewMode": "3d",
"model3d": {
"src": "https://cdn.example.com/models/mug-11oz.glb",
"wrapTarget": "PrintArea",
"colorTarget": "Body",
"wrapFlipY": true,
"primitive": "cylinder",
"camera": {
"fov": 35,
"position": [0.18, 0.12, 0.32],
"target": [0, 0.05, 0]
}
}
}
]
}
views[].model3d 字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
src |
3D 商品必填 | 模型文件的 HTTPS 公网 URL。推荐 .glb(单文件)。CDN 必须允许浏览器跨域读取(Access-Control-Allow-Origin,且能 GET 二进制)。签名 URL 也可以,只要设计器打开时仍有效。 |
wrapTarget |
强烈建议 | 承接印刷图的 网格名或材质名,需与 glb 里的名字一致,例如 PrintArea。大小写不敏感。找不到时会警告 PREVIEW_3D_WRAP_TARGET_NOT_FOUND,并尝试贴到第一块带 UV 的网格。 |
colorTarget |
建议 | 换商品颜色时要染色的网格/材质名,例如 Body。应和印刷面分开。找不到时警告 PREVIEW_3D_COLOR_TARGET_NOT_FOUND。 |
wrapFlipY |
否 | 印刷图 UV 是否翻转 Y,默认 true。贴反了就改成 false。 |
primitive |
否 | src 缺失或加载失败时的降级几何:cylinder / box / plane。杯子建议 cylinder。 |
color |
否 | 没有选中商品色时的默认杯体颜色。 |
camera.fov |
否 | 透视相机视角,默认 35。 |
camera.position |
否 | [x, y, z]。不传则按模型包围盒自动框住。 |
camera.target |
否 | 相机看向的点,默认 [0, 0, 0]。 |
exposure |
否 | 曝光,默认 1。 |
模型文件规范(给运营 / 3D 制作):
- 每个 SKU 一份
.glb,不要多商品共用对不上 UV 的模型。 - 印刷面做成独立网格或独立材质,名字固定,例如
PrintArea。杯体另叫Body。 PrintArea的 UV 必须是杯身展开:U 沿圆周,V 沿高度,且和接口里的print.widthCm / heightCm是同一张展开图。- 杯体颜色不要烤进贴图,用可染色材质,方便
colors[].value换色。 - Y 轴向上,模型尽量放在原点附近。
- 不要用
.gltf + .bin + 多张贴图的拆分目录,除非这些附属文件也能用 HTTPS 且跨域可读。
宿主接入:
const template = productTemplateFromBootstrap(apiPayload);
const designer = createDesigner(editorEl, {
product: template,
mockup: previewEl,
toolbar: false
});
加载失败会发出 PREVIEW_3D_MODEL_LOAD_FAILED 并暂时显示圆柱占位。印刷导出仍然只使用 2D printArea,不要把 3D 截图交给工厂。
企业水印与默认图案
两类资源不要混用:
mockup:商品实拍底图,只用于效果图预览,不进入印刷文件。branding.text:SaaS 租户水印文本,斜向铺在红线框以外的灰色工作区,不写入印刷元素,也不会出现在印刷图 / 效果图导出里。views[].defaultArtwork(bootstrap 里是artwork):真正要印到商品上的默认图案,会进入设计稿和印刷文件。
SaaS 租户在运行时传入水印文本:
const designer = createDesigner(container, {
product: productTemplateFromBootstrap(apiPayload),
branding: `${tenant.name} - ${user.name}`,
toolbar: false
});
也可以写进后端配置:
const template = productTemplateFromBootstrap({
product: {
id: api.product.id,
name: api.product.name,
branding: {
text: `${tenant.name} - ${user.name}`
}
},
views: api.views.map((view) => ({
id: view.id,
name: view.name,
dpi: view.dpi,
print: { widthCm: view.widthCm, heightCm: view.heightCm },
mockup: view.mockup,
artwork: view.patternUrl
? [{ src: view.patternUrl, coverPrintArea: true, imageMode: "fill" }]
: undefined
}))
});
规则:
- 水印文本斜向铺在画布红线框外的灰色 pasteboard 上,红线以内保持干净;使用设计坐标尺寸,随画布缩放/平移一起变化,并按当前视口重新铺满。
- 默认约
-30°、浅灰色。需要更淡、更实或换颜色可传branding.opacity/branding.color/branding.rotation。 - 页面可用
designer.setBrandingVisible(false)/designer.getBrandingVisible()开关显示,不影响设计稿和印刷导出。 createDesigner({ branding })会覆盖产品模板里的product.branding.text,方便同一个商品模板服务多个租户。可直接传字符串,例如branding: "创客工作室"。views[].artwork才是可印刷默认图案;不要把企业水印写进artwork。
后端返回厘米尺寸和 DPI 时,在业务项目里转换,不要让设计器去调接口:
import { productTemplateFromBootstrap, cmToPx } from "shenlan-designer";
const template = productTemplateFromBootstrap({
product: api.product,
views: api.views.map((view) => ({
id: view.id,
name: view.name,
designAreaId: view.designAreaId,
dpi: view.dpi,
print: { widthCm: view.widthCm, heightCm: view.heightCm },
mockup: {
src: view.mockup.src,
imageSize: view.mockup.imageSize,
quad: view.mockup.quad
}
}))
});
const printFiles = await designer.exportPrintFiles();
15cm @ 300DPI 会生成约 1772px 的印刷画布。四个点只用于效果图贴合。
VbenAdmin / Element Plus 接入建议
默认 createDesigner(container) 已经带整页编辑器,业务项目可以直接嵌入。如果要继续用 Element Plus 自己做壳,关闭整页 UI,只保留画布:
const designer = createDesigner(container, {
product: createTshirtTemplate(),
ui: false,
keyboard: true
});
然后用 Element Plus 和 TailwindCSS 实现业务 UI:
- 顶部按钮:
el-button - 填色:
el-popover+el-color-picker - 属性面板:
el-form+el-input-number+el-slider - 历史记录:
el-drawer或el-popover - 预览区:业务项目自定义 mockup / 轮播 / 缩略图
示例:
function addText() {
designer.addText({
text: "我的定制文字",
fontSize: 48,
fill: "#111827"
});
}
async function exportPreview() {
const result = await designer.exportImage({
area: "print",
transparent: true
});
previewUrl.value = result.dataUrl;
}
数据边界
设计文档是纯 JSON 数据:
- 不保存 DOM 节点。
- 不保存 Konva 节点。
- 不保存函数和 Promise。
- 图片元素保存
src,不默认把二进制写入 JSON。 - 坐标使用设计坐标系,不使用 DOM 像素。
- 旋转使用角度。
- 图层顺序通过
zIndex表达。