Vue 3 + DYMO Connect Framework 实战:从标签模板到打印服务的完整实现

文章来源声明: 原文作者:申行; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684097616185884712; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

三层分层设计把 DYMO 底层细节隔离干净,业务侧只剩一行调用;Base64 前缀、打印锁、超时等踩坑点均给出可复用方案,适合中后台打印集成参考。

**以访客标签打印为例,详解 DYMO Connect Framework、Label XML、动态数据绑定、Base64 图片、打印机选择与异常处理**

一、前言

在访客管理、门禁系统、前台登记等场景中,经常会遇到这样的需求:

用户完成访客登记后,系统自动将访客信息打印到 DYMO 标签打印机上。

最开始看起来非常简单:

dymo.label.framework.printLabel(...)

调用一下就可以。

但真正做到生产环境以后,会发现问题远比想象中复杂:

  • DYMO Connect 是否安装?
  • DYMO Framework 是否正常运行?
  • 当前电脑有没有打印机?
  • 使用哪一台打印机?
  • Label XML 模板从哪里来?
  • 后端返回的数据如何绑定到 Label XML?
  • 二维码是 URL 还是 Base64?
  • Base64 前面的 data:image/png;base64, 要不要保留?
  • 日期应该由前端生成还是 DYMO 自己生成?
  • 用户连续点击打印怎么办?
  • 打印机没纸怎么办?
  • 打印机离线怎么办?
  • DYMO 服务挂掉怎么办?
  • 如何让业务页面不关心这些底层细节?

因此,我最终采用了一个比较清晰的分层设计。


二、最终架构

整个打印流程设计成:

┌────────────────────────────┐
│        Vue 业务页面         │
│                            │
│ printVisitorTicket(data)   │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│       dymoService.js        │
│                            │
│  Framework 检测             │
│  打印机选择                 │
│  模板加载                   │
│  数据绑定                   │
│  打印锁                     │
│  异常处理                   │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│   dymoTemplateBinding.js    │
│                            │
│  Label XML                 │
│  TextObject                │
│  ImageObject               │
│  DateTimeObject             │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│       Label XML             │
│                            │
│  Company                   │
│  UserName                  │
│  Date                      │
│  QrCode                    │
│  Text1                     │
│  Text2                     │
│  Text3                     │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│ DYMO Connect Framework      │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│       DYMO 打印机           │
└────────────────────────────┘

这样设计以后,Vue 页面完全不需要知道 DYMO Framework 的细节。

业务页面只需要:

const result = await printVisitorTicket(response.data);


三、项目目录

最终项目结构:

src/
├── services/
│   ├── dymoService.js
│   └── dymoTemplateBinding.js
│
└── templates/
    └── visitor-ticket.label

其中:

visitor-ticket.label

负责:

标签长什么样。

dymoTemplateBinding.js

负责:

数据应该填到标签的哪个对象里面。

dymoService.js

负责:

怎么检测 DYMO、怎么找打印机、怎么执行打印。

这三个职责不要混在一起。


四、Vue 项目引入 DYMO Label 模板

因为 .label 本质上是 XML 文件,所以 Vite 可以直接使用:

import visitorLabelTemplate from <span>"@/templates/visitor-ticket.label?raw"</span>;

这里的:

?raw

非常重要。

它的作用是:

不让 Vite 把 .label 当普通资源 URL,而是直接把文件内容作为字符串加载。

因此:

console.log(visitorLabelTemplate);

拿到的就是完整 XML。


五、为什么要单独设计 dymoTemplateBinding.js

假设 Label 模板里面有

<TextObject>
    <Name>Company</Name>
    ...
</TextObject>

后端返回:

{
  Company: <span>"上海某某科技有限公司"</span>
}

我们需要把:

Company

和:

上海某某科技有限公司

建立关系。

因此定义:

const bindingMap = {
  Company: { <span>type</span>: <span>"text"</span> },
  UserName: { <span>type</span>: <span>"text"</span> },
  Date: { <span>type</span>: <span>"date"</span> },
  QrCode: { <span>type</span>: <span>"image"</span> },
  Text1: { <span>type</span>: <span>"text"</span> },
  Text2: { <span>type</span>: <span>"text"</span> },
  Text3: { <span>type</span>: <span>"text"</span> },
};

这样以后新增打印字段非常简单。

例如增加:

Department: {
  <span>type</span>: <span>"text"</span>,
}

就可以继续扩展。


六、文本对象绑定

最基础的就是:

<span>function</span> setTextObjectValue(labelXml, objectName, value) {
  const safeValue = escapeXmlText(value);

  <span>return</span> replaceObjectBlock(
    labelXml,
    objectName,
    (block) => {
      <span>let</span> replaced = <span>false</span>;

      <span>return</span> block.replace(
        /(<TextSpan\b[^>]*>[\s\S]*?<Text\b[^>]*>)([\s\S]*?)(<\/Text>)/gi,
        (match, prefix, oldValue, suffix) => {
          <span>if</span> (!replaced) {
            replaced = <span>true</span>;

            <span>return</span> `<span>${prefix}</span><span>${safeValue}</span><span>${suffix}</span>`;
          }

          <span>return</span> `<span>${prefix}</span><span>${suffix}</span>`;
        }
      );
    }
  );
}

这里有一个容易忽略的问题:

不能直接把用户数据拼接到 XML 中。

例如:

`<span>${value}</span>`

如果 value 里面存在:

&
<
>

就可能破坏 XML。

因此需要:

<span>function</span> escapeXmlText(value) {
  <span>if</span> (value === null || value === undefined) {
    <span>return</span> <span>""</span>;
  }

  <span>return</span> String(value)
    .replace(/&/g, <span>"&amp;"</span>)
    .replace(/</g, <span>"&lt;"</span>)
    .replace(/>/g, <span>"&gt;"</span>);
}

例如:

A & B

最终会变成:

A &amp; B


七、二维码 Base64 是这次踩坑比较严重的地方

后端可能返回:

data:image/png;<span>base64</span>,iVBORw0KGgo...

但是 DYMO Label XML 的:

<Data>
    ...
</Data>

里面需要的是:

iVBORw0KGgo...

而不是:

data:image/png;<span>base64</span>,iVBORw0KGgo...

所以需要进行处理。

<span>function</span> normalizeBase64Image(value) {
  <span>if</span> (value === null || value === undefined) {
    <span>return</span> <span>""</span>;
  }

  <span>let</span> <span>base64</span> = String(value).trim();

  <span>if</span> (!<span>base64</span>) {
    <span>return</span> <span>""</span>;
  }

  const dataUrlRegex =
    /^data:image\/[a-zA-Z0-9.+-]+;<span>base64</span>,/i;

  <span>while</span> (dataUrlRegex.<span>test</span>(<span>base64</span>)) {
    <span>base64</span> = base64.replace(dataUrlRegex, <span>""</span>);
  }

  <span>base64</span> = base64.replace(/\s+/g, <span>""</span>);

  <span>if</span> (!/^[A-Za-z0-9+/]*={0,2}$/.test(<span>base64</span>)) {
    throw new Error(
      <span>"QrCode 图片数据不是合法的 Base64。"</span>
    );
  }

  <span>if</span> (base64.length % 4 !== 0) {
    throw new Error(
      <span>"QrCode Base64 数据长度无效。"</span>
    );
  }

  <span>return</span> <span>base64</span>;
}

然后:

<span>function</span> setImageObjectValue(
  labelXml,
  objectName,
  value
) {
  const <span>base64</span> = normalizeBase64Image(value);

  <span>return</span> replaceObjectBlock(
    labelXml,
    objectName,
    (block) => {
      const dataRegex =
        /(<Data\b[^>]*>)[\s\S]*?(<\/Data>)/i;

      <span>if</span> (!dataRegex.<span>test</span>(block)) {
        <span>return</span> block;
      }

      <span>return</span> block.replace(
        dataRegex,
        `$1<span>${base64}</span><span>$2</span>`
      );
    }
  );
}


八、dymoService.js

模板数据绑定完成以后,进入真正的打印服务。

首先获取 DYMO Framework:

<span>function</span> <span><span>getDymoFramework</span></span>() {
  <span>if</span> (
    typeof window === <span>"undefined"</span> ||
    !window.dymo ||
    !window.dymo.label ||
    !window.dymo.label.framework
  ) {
    <span>return</span> null;
  }

  <span>return</span> window.dymo.label.framework;
}

为什么不直接:

window.dymo.label.framework

因为 Vue 页面可能在 Framework 尚未加载的时候执行。

所以需要先判断:

window.dymo

然后:

window.dymo.label

最后:

window.dymo.label.framework


九、检测 DYMO 服务

<span>function</span> <span><span>check</span></span>() {
  const framework = getDymoFramework();

  <span>if</span> (!framework) {
    <span>return</span> {
      success: <span>false</span>,
      code: <span>"DYMO_NOT_AVAILABLE"</span>,
      message:
        <span>"未检测到 DYMO Connect Framework,请确认 DYMO Connect 已安装并正在运行。"</span>,
      printers: [],
    };
  }

  <span>let</span> printers = [];

  try {
    printers = framework.getPrinters() || [];
  } catch (error) {
    <span>return</span> {
      success: <span>false</span>,
      code: <span>"GET_PRINTERS_FAILED"</span>,
      message:
        <span>"无法获取 DYMO 打印机列表,请确认 DYMO Connect 服务正常运行。"</span>,
      error,
      printers: [],
    };
  }

  ...
}

这样业务页面就不需要处理:

window.dymo

也不需要处理:

framework.getPrinters()


十、打印机自动选择

打印机选择也放在服务层。

<span>function</span> resolvePrinter(options = {}) {
  const printers = getPrinters();

  <span>if</span> (!printers.length) {
    throw new Error(
      <span>"未检测到 DYMO 打印机。"</span>
    );
  }

  const printerName =
    options.printerName ||
    DYMO_CONFIG.printerName;

  <span>if</span> (printerName) {
    const printer =
      findPrinter(
        printers,
        printerName
      );

    <span>if</span> (!printer) {
      throw new Error(
        `未找到指定的 DYMO 打印机:<span>${printerName}</span>`
      );
    }

    <span>return</span> printer;
  }

  const printer =
    getDefaultPrinter(printers);

  <span>if</span> (!printer) {
    throw new Error(
      <span>"无法确定 DYMO 默认打印机。"</span>
    );
  }

  <span>return</span> printer;
}

这样可以支持两种模式:

指定打印机

await printVisitorTicket(data, {
  printerName: <span>"DYMO LabelWriter 450"</span>,
});

自动选择

await printVisitorTicket(data);


十一、为什么需要打印锁

访客登记页面很容易出现这种情况:

用户连续点击:

打印
打印
打印
打印

如果不限制,就可能连续发送多个打印任务。

因此:

<span>let</span> printing = <span>false</span>;

开始打印:

<span>function</span> <span><span>acquirePrintLock</span></span>() {
  <span>if</span> (printing) {
    throw new Error(
      <span>"DYMO 正在打印,请勿重复提交。"</span>
    );
  }

  printing = <span>true</span>;
}

结束后:

<span>function</span> <span><span>releasePrintLock</span></span>() {
  printing = <span>false</span>;
}

最终:

async <span>function</span> printLabel(
  labelXml,
  options = {}
) {
  acquirePrintLock();

  try {
    ...
  } finally {
    releasePrintLock();
  }
}

这里 finally 非常重要。

因为无论打印:

成功

还是:

失败

都必须释放锁。


十二、真正执行打印的地方

前面的所有代码其实都在做准备。

真正执行打印的是:

framework.printLabel(
  printerName,
  <span>""</span>,
  labelXml,
  <span>""</span>
);

我又在外面包装了一层 Promise:

<span>function</span> printLabelAsync(
  framework,
  printerName,
  labelXml,
  options = {}
) {
  const <span>timeout</span> =
    Number(options.timeout) ||
    DYMO_CONFIG.printTimeout;

  <span>return</span> new Promise(
    (resolve, reject) => {
      <span>let</span> completed = <span>false</span>;

      const finish = (
        callback,
        value
      ) => {
        <span>if</span> (completed) <span>return</span>;

        completed = <span>true</span>;

        clearTimeout(timer);

        callback(value);
      };

      const timer = setTimeout(() => {
        finish(
          reject,
          new Error(
            "DYMO 打印请求超时,请检查 DYMO Connect 和打印机状态。"
          )
        );
      }, timeout);

      try {
        const result =
          framework.printLabel(
            printerName,
            "",
            labelXml,
            ""
          );

        finish(resolve, result);
      } catch (error) {
        finish(reject, error);
      }
    }
  );
}

这样可以避免打印请求一直挂着。


十三、为什么要设计 printVisitorTicket()

最终我没有让业务页面直接调用:

printLabel()

而是再封装一层:

async <span>function</span> printVisitorTicket(
  data = {},
  options = {}
) {
  const checkResult = check();

  <span>if</span> (!checkResult.success) {
    <span>return</span> checkResult;
  }

  <span>let</span> templateXml;

  try {
    templateXml =
      getVisitorLabelTemplate();

    validateTemplate(templateXml);
  } catch (error) {
    <span>return</span> {
      success: <span>false</span>,
      code: <span>"TEMPLATE_ERROR"</span>,
      message:
        error?.message ||
        <span>"DYMO 标签模板加载失败。"</span>,
      error,
    };
  }

  <span>let</span> labelXml;

  try {
    labelXml =
      createVisitorLabel(
        templateXml,
        data
      );
  } catch (error) {
    <span>return</span> {
      success: <span>false</span>,
      code: <span>"BIND_DATA_FAILED"</span>,
      message:
        error?.message ||
        <span>"DYMO 标签数据绑定失败。"</span>,
      error,
    };
  }

  <span>return</span> printLabel(
    labelXml,
    options
  );
}

它实际上就是整个系统的:

业务入口。


十四、业务页面最终有多简单?

后端接口返回:

const response = await createVisitor();

假设:

response.data = {
  Company: <span>"上海某某科技有限公司"</span>,
  UserName: <span>"张三"</span>,
  Date: <span>"2026-09-11"</span>,
  QrCode: <span>"data:image/png;base64,iVBORw0KGgo..."</span>,
  Text1: <span>"访客"</span>,
  Text2: <span>"A栋"</span>,
  Text3: <span>"欢迎光临"</span>,
};

页面只需要:

const result =
  await printVisitorTicket(
    response.data
  );

<span>if</span> (!result.success) {
  ElMessage.error(
    result.message ||
    <span>"打印失败,请联系管理员"</span>
  );

  <span>return</span>;
}

ElMessage.success(
  <span>"打印成功"</span>
);

这就是整个封装最大的价值。

业务页面完全不需要知道:

DYMO Framework
Label XML
Base64
打印机名称
打印锁
XML 转义
模板对象


十五、为什么错误不要在页面里面判断

不推荐:

<span>if</span> (
  error.message.includes(<span>"paper"</span>)
) {
  ...
}

因为页面不应该关心 DYMO 的底层错误信息。

应该让服务层统一转换。

例如:

DYMO_NOT_AVAILABLE
NO_PRINTER
PRINTER_NOT_FOUND
PRINTER_OFFLINE
PRINTER_NO_PAPER
PRINT_TIMEOUT
PRINT_FAILED
TEMPLATE_ERROR
BIND_DATA_FAILED

然后页面只负责:

switch (result.code) {
  <span>case</span> <span>"PRINTER_NO_PAPER"</span>:
    ElMessage.error(
      <span>"打印机缺纸,请联系管理员"</span>
    );
    <span>break</span>;

  <span>case</span> <span>"PRINTER_OFFLINE"</span>:
    ElMessage.error(
      <span>"打印机离线,请联系管理员"</span>
    );
    <span>break</span>;

  <span>case</span> <span>"DYMO_NOT_AVAILABLE"</span>:
    ElMessage.error(
      <span>"打印服务异常,请联系管理员"</span>
    );
    <span>break</span>;

  default:
    ElMessage.error(
      <span>"打印失败,请联系管理员"</span>
    );
}

不过这里有一个需要特别说明的地方:

目前不能仅凭 getPrinters() 就断言一定能够提前判断“缺纸”。

DYMO Framework 是否会提供明确的纸张状态,以及无纸时返回什么错误,需要结合实际打印机和 Framework 的错误信息确认。

所以正确的开发顺序应该是:

真实模拟缺纸
      ↓
获取 DYMO 实际错误
      ↓
分析 error / message
      ↓
统一转换 code
      ↓
页面显示中文提示

而不是提前猜一个:

NO_PAPER


十六、这次遇到的 DYMO 服务连接问题

开发过程中还遇到过:

ERR_CONNECTION_REFUSED

例如:

https://127.0.0.1:41952/DYMO/DLS/Printing/PrintLabel

这个错误和 Label XML 本身没有关系。

它代表:

浏览器尝试访问本机 DYMO Web Service,但是对应服务没有正常监听。

所以排查应该分成两层:

第一层
浏览器
   ↓
DYMO Web Service

确认:

DYMO Connect 是否运行

然后:

第二层
DYMO Framework
   ↓
打印机

确认:

打印机是否存在
打印机是否在线
打印机是否正常

不要把这两类问题混在一起。


十七、最终形成三层结构

到这里整个设计就比较清晰了。

第一层:业务层

await printVisitorTicket(data);

负责:

我要打印访客标签。


第二层:服务层

dymoService.js

负责:

DYMO Framework
打印机
模板
打印
异常


第三层:模板绑定层

dymoTemplateBinding.js

负责:

Company
UserName
Date
QrCode
Text1
Text2
Text3

与 XML 对象之间的映射。


十八、最终调用关系

最终完整调用链:

Vue 页面
    │
    │ printVisitorTicket(response.data)
    ↓
dymoService.js
    │
    ├── check()
    │
    ├── getVisitorLabelTemplate()
    │
    ├── validateTemplate()
    │
    ├── createVisitorLabel()
    │       │
    │       ↓
    │   dymoTemplateBinding.js
    │       │
    │       ↓
    │   Label XML
    │
    └── printLabel()
            │
            ├── acquirePrintLock()
            │
            ├── resolvePrinter()
            │
            ├── printLabelAsync()
            │
            └── framework.printLabel()
                    │
                    ↓
             DYMO Connect
                    │
                    ↓
               DYMO Printer


十九、这种封装最大的意义

最终业务代码从原来的:

业务页面
 ↓
DYMO Framework
 ↓
打印机

变成:

业务页面
 ↓
printVisitorTicket(data)
 ↓
完成打印

以后如果需要增加:

访客标签
员工标签
车辆标签
会议标签
临时卡标签

也不需要让业务页面直接操作 DYMO。

可以继续:

printVisitorTicket(data);

printEmployeeTicket(data);

printVehicleTicket(data);

printMeetingTicket(data);

底层依然共用:

dymoService.js
dymoTemplateBinding.js
DYMO Framework

这就从一个简单的“调用打印机”,变成了一个真正可以维护和扩展的打印服务层


二十、完整核心代码

文章最后可以放一个完整的核心代码版本,方便读者直接复制使用。

dymoTemplateBinding.js

const OBJECT_TYPES =
  <span>"TextObject|ImageObject|DateTimeObject"</span>;

<span>function</span> escapeXmlText(value) {
  <span>if</span> (value === null || value === undefined) {
    <span>return</span> <span>""</span>;
  }

  <span>return</span> String(value)
    .replace(/&/g, <span>"&amp;"</span>)
    .replace(/</g, <span>"&lt;"</span>)
    .replace(/>/g, <span>"&gt;"</span>);
}

<span>function</span> escapeRegExp(value) {
  <span>return</span> String(value).replace(
    /[.*+?^<span>${}</span>()|[\]\\]/g,
    <span>"\\$&"</span>
  );
}

<span>function</span> normalizeBase64Image(value) {
  <span>if</span> (value === null || value === undefined) {
    <span>return</span> <span>""</span>;
  }

  <span>let</span> <span>base64</span> = String(value).trim();

  <span>if</span> (!<span>base64</span>) {
    <span>return</span> <span>""</span>;
  }

  const dataUrlRegex =
    /^data:image\/[a-zA-Z0-9.+-]+;<span>base64</span>,/i;

  <span>while</span> (dataUrlRegex.<span>test</span>(<span>base64</span>)) {
    <span>base64</span> = base64.replace(
      dataUrlRegex,
      <span>""</span>
    );
  }

  <span>base64</span> = base64.replace(/\s+/g, <span>""</span>);

  <span>if</span> (
    !/^[A-Za-z0-9+/]*={0,2}$/.test(<span>base64</span>)
  ) {
    throw new Error(
      <span>"QrCode 图片数据不是合法的 Base64。"</span>
    );
  }

  <span>if</span> (base64.length % 4 !== 0) {
    throw new Error(
      <span>"QrCode Base64 数据长度无效。"</span>
    );
  }

  <span>return</span> <span>base64</span>;
}

<span>function</span> createNameRegex(objectName) {
  const escapedName =
    escapeRegExp(objectName);

  <span>return</span> new RegExp(
    `<Name\\s*>\\s*<span>${escapedName}</span>\\s*<\\/Name\\s*>`,
    <span>"i"</span>
  );
}

<span>function</span> findObjectBlock(
  labelXml,
  objectName
) {
  <span>if</span> (!labelXml || !objectName) {
    <span>return</span> null;
  }

  const objectRegex =
    new RegExp(
      `<(<span>${OBJECT_TYPES}</span>)\\b[^>]*>[\\s\\S]*?<\\/\\1>`,
      <span>"gi"</span>
    );

  const nameRegex =
    createNameRegex(objectName);

  <span>let</span> match;

  <span>while</span> (
    (match = objectRegex.<span>exec</span>(labelXml)) !== null
  ) {
    const block = match[0];

    <span>if</span> (nameRegex.<span>test</span>(block)) {
      <span>return</span> {
        <span>type</span>: match[1],
        start: match.index,
        end:
          match.index + block.length,
        block,
      };
    }
  }

  <span>return</span> null;
}

<span>function</span> replaceObjectBlock(
  labelXml,
  objectName,
  replacer
) {
  const object =
    findObjectBlock(
      labelXml,
      objectName
    );

  <span>if</span> (!object) {
    <span>return</span> {
      success: <span>false</span>,
      xml: labelXml,
      message:
        `DYMO 模板中不存在对象:<span>${objectName}</span>`,
    };
  }

  const newBlock =
    replacer(
      object.block,
      object.type
    );

  <span>if</span> (typeof newBlock !== <span>"string"</span>) {
    <span>return</span> {
      success: <span>false</span>,
      xml: labelXml,
      message:
        `DYMO 模板对象替换失败:<span>${objectName}</span>`,
    };
  }

  <span>return</span> {
    success: <span>true</span>,
    xml:
      labelXml.slice(
        0,
        object.start
      ) +
      newBlock +
      labelXml.slice(object.end),
  };
}

<span>function</span> setTextObjectValue(
  labelXml,
  objectName,
  value
) {
  const safeValue =
    escapeXmlText(value);

  <span>return</span> replaceObjectBlock(
    labelXml,
    objectName,
    (block) => {
      <span>let</span> replaced = <span>false</span>;

      <span>return</span> block.replace(
        /(<TextSpan\b[^>]*>[\s\S]*?<Text\b[^>]*>)([\s\S]*?)(<\/Text>)/gi,
        (
          match,
          prefix,
          oldValue,
          suffix
        ) => {
          <span>if</span> (!replaced) {
            replaced = <span>true</span>;

            <span>return</span> `<span>${prefix}</span><span>${safeValue}</span><span>${suffix}</span>`;
          }

          <span>return</span> `<span>${prefix}</span><span>${suffix}</span>`;
        }
      );
    }
  );
}

<span>function</span> setImageObjectValue(
  labelXml,
  objectName,
  value
) {
  const <span>base64</span> =
    normalizeBase64Image(value);

  <span>return</span> replaceObjectBlock(
    labelXml,
    objectName,
    (block) => {
      const dataRegex =
        /(<Data\b[^>]*>)[\s\S]*?(<\/Data>)/i;

      <span>if</span> (!dataRegex.<span>test</span>(block)) {
        <span>return</span> block;
      }

      <span>return</span> block.replace(
        dataRegex,
        `$1<span>${base64}</span><span>$2</span>`
      );
    }
  );
}

<span>function</span> setDateTimeObjectValue(
  labelXml,
  objectName,
  value
) {
  <span>return</span> replaceObjectBlock(
    labelXml,
    objectName,
    (block) => block
  );
}

<span>function</span> setObjectValue(
  labelXml,
  objectName,
  value,
  <span>type</span>
) {
  switch (<span>type</span>) {
    <span>case</span> <span>"image"</span>:
    <span>case</span> <span>"ImageObject"</span>:
      <span>return</span> setImageObjectValue(
        labelXml,
        objectName,
        value
      );

    <span>case</span> <span>"date"</span>:
    <span>case</span> <span>"datetime"</span>:
    <span>case</span> <span>"DateTimeObject"</span>:
      <span>return</span> setDateTimeObjectValue(
        labelXml,
        objectName,
        value
      );

    <span>case</span> <span>"text"</span>:
    <span>case</span> <span>"TextObject"</span>:
    default:
      <span>return</span> setTextObjectValue(
        labelXml,
        objectName,
        value
      );
  }
}

<span>function</span> bindData(
  labelXml,
  data = {}
) {
  <span>if</span> (
    !labelXml ||
    typeof labelXml !== <span>"string"</span>
  ) {
    throw new Error(
      <span>"DYMO Label XML 模板不能为空"</span>
    );
  }

  <span>if</span> (
    !data ||
    typeof data !== <span>"object"</span>
  ) {
    throw new Error(
      <span>"DYMO 打印数据必须是对象"</span>
    );
  }

  <span>let</span> resultXml = labelXml;

  const bindingMap = {
    Company: {
      <span>type</span>: <span>"text"</span>,
    },

    UserName: {
      <span>type</span>: <span>"text"</span>,
    },

    Date: {
      <span>type</span>: <span>"date"</span>,
    },

    QrCode: {
      <span>type</span>: <span>"image"</span>,
    },

    Text1: {
      <span>type</span>: <span>"text"</span>,
    },

    Text2: {
      <span>type</span>: <span>"text"</span>,
    },

    Text3: {
      <span>type</span>: <span>"text"</span>,
    },
  };

  Object.keys(bindingMap)
    .forEach((objectName) => {
      if (
        !Object.prototype.hasOwnProperty.call(
          data,
          objectName
        )
      ) {
        return;
      }

      const config =
        bindingMap[objectName];

      const result =
        setObjectValue(
          resultXml,
          objectName,
          data[objectName],
          config.type
        );

      if (result.success) {
        resultXml = result.xml;
      }
    });

  return resultXml;
}

function createVisitorLabel(
  templateXml,
  data = {}
) {
  return bindData(
    templateXml,
    data
  );
}

export {
  bindData,
  createVisitorLabel,
  setTextObjectValue,
  setImageObjectValue,
  setDateTimeObjectValue,
  setObjectValue,
  normalizeBase64Image,
};


二十一、总结

这次 DYMO 集成最终不是简单地完成:

printLabel()

而是建立了一套完整的打印链路:

业务数据
   ↓
模板
   ↓
数据绑定
   ↓
Label XML
   ↓
打印机选择
   ↓
DYMO Framework
   ↓
打印
   ↓
统一错误处理

其中最重要的几个经验是:

第一,不要让业务页面直接操作 DYMO Framework。

第二,把 Label XML 数据绑定独立出来。

第三,Base64 图片一定要处理 Data URL 前缀。

第四,DateTimeObject 不要随意修改 StaticDateTime

第五,打印过程需要防重复提交和超时控制。

第六,打印异常应该在服务层统一转换,页面只负责显示业务提示。

最终让业务代码保持简单:

const result =
  await printVisitorTicket(
    response.data
  );

<span>if</span> (!result.success) {
  ElMessage.error(
    result.message ||
    <span>"打印失败,请联系管理员"</span>
  );

  <span>return</span>;
}