npm.io
0.1.0 • Published yesterday

shenlan-designer

Licence
MIT
Version
0.1.0
Deps
3
Size
2.9 MB
Vulns
0
Weekly
0

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 制作):

  1. 每个 SKU 一份 .glb,不要多商品共用对不上 UV 的模型。
  2. 印刷面做成独立网格或独立材质,名字固定,例如 PrintArea。杯体另叫 Body。
  3. PrintArea 的 UV 必须是杯身展开:U 沿圆周,V 沿高度,且和接口里的 print.widthCm / heightCm 是同一张展开图。
  4. 杯体颜色不要烤进贴图,用可染色材质,方便 colors[].value 换色。
  5. Y 轴向上,模型尽量放在原点附近。
  6. 不要用 .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 表达。

Keywords