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

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

一、前言

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

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

最开始看起来非常简单:

bash 复制代码
dymo.label.framework.printLabel(...)

调用一下就可以。

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

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

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


二、最终架构

整个打印流程设计成:

bash 复制代码
┌────────────────────────────┐
│        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 的细节。

业务页面只需要:

bash 复制代码
const result = await printVisitorTicket(response.data);

三、项目目录

最终项目结构:

bash 复制代码
src/
├── services/
│   ├── dymoService.js
│   └── dymoTemplateBinding.js
│
└── templates/
    └── visitor-ticket.label

其中:

visitor-ticket.label

负责:

标签长什么样。

dymoTemplateBinding.js

负责:

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

dymoService.js

负责:

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

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


四、Vue 项目引入 DYMO Label 模板

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

bash 复制代码
import visitorLabelTemplate from "@/templates/visitor-ticket.label?raw";

这里的:

bash 复制代码
?raw

非常重要。

它的作用是:

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

因此:

bash 复制代码
console.log(visitorLabelTemplate);

拿到的就是完整 XML。


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

假设 Label 模板里面有

bash 复制代码
<TextObject>
    <Name>Company</Name>
    ...
</TextObject>

后端返回:

bash 复制代码
{
  Company: "上海某某科技有限公司"
}

我们需要把:

bash 复制代码
Company

和:

bash 复制代码
上海某某科技有限公司

建立关系。

因此定义:

bash 复制代码
const bindingMap = {
  Company: { type: "text" },
  UserName: { type: "text" },
  Date: { type: "date" },
  QrCode: { type: "image" },
  Text1: { type: "text" },
  Text2: { type: "text" },
  Text3: { type: "text" },
};

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

例如增加:

bash 复制代码
Department: {
  type: "text",
}

就可以继续扩展。


六、文本对象绑定

最基础的就是:

bash 复制代码
function setTextObjectValue(labelXml, objectName, value) {
  const safeValue = escapeXmlText(value);

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

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

            return `${prefix}${safeValue}${suffix}`;
          }

          return `${prefix}${suffix}`;
        }
      );
    }
  );
}

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

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

例如:

bash 复制代码
`${value}`

如果 value 里面存在:

bash 复制代码
&
<
>

就可能破坏 XML。

因此需要:

bash 复制代码
function escapeXmlText(value) {
  if (value === null || value === undefined) {
    return "";
  }

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

例如:

bash 复制代码
A & B

最终会变成:

bash 复制代码
A &amp; B

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

后端可能返回:

bash 复制代码
data:image/png;base64,iVBORw0KGgo...

但是 DYMO Label XML 的:

bash 复制代码
<Data>
    ...
</Data>

里面需要的是:

bash 复制代码
iVBORw0KGgo...

而不是:

bash 复制代码
data:image/png;base64,iVBORw0KGgo...

所以需要进行处理。

bash 复制代码
function normalizeBase64Image(value) {
  if (value === null || value === undefined) {
    return "";
  }

  let base64 = String(value).trim();

  if (!base64) {
    return "";
  }

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

  while (dataUrlRegex.test(base64)) {
    base64 = base64.replace(dataUrlRegex, "");
  }

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

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

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

  return base64;
}

然后:

bash 复制代码
function setImageObjectValue(
  labelXml,
  objectName,
  value
) {
  const base64 = normalizeBase64Image(value);

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

      if (!dataRegex.test(block)) {
        return block;
      }

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

八、dymoService.js

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

首先获取 DYMO Framework:

bash 复制代码
function getDymoFramework() {
  if (
    typeof window === "undefined" ||
    !window.dymo ||
    !window.dymo.label ||
    !window.dymo.label.framework
  ) {
    return null;
  }

  return window.dymo.label.framework;
}

为什么不直接:

bash 复制代码
window.dymo.label.framework

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

所以需要先判断:

bash 复制代码
window.dymo

然后:

bash 复制代码
window.dymo.label

最后:

bash 复制代码
window.dymo.label.framework

九、检测 DYMO 服务

bash 复制代码
function check() {
  const framework = getDymoFramework();

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

  let printers = [];

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

  ...
}

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

bash 复制代码
window.dymo

也不需要处理:

bash 复制代码
framework.getPrinters()

十、打印机自动选择

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

bash 复制代码
function resolvePrinter(options = {}) {
  const printers = getPrinters();

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

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

  if (printerName) {
    const printer =
      findPrinter(
        printers,
        printerName
      );

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

    return printer;
  }

  const printer =
    getDefaultPrinter(printers);

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

  return printer;
}

这样可以支持两种模式:

指定打印机

bash 复制代码
await printVisitorTicket(data, {
  printerName: "DYMO LabelWriter 450",
});

自动选择

bash 复制代码
await printVisitorTicket(data);

十一、为什么需要打印锁

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

用户连续点击:

bash 复制代码
打印
打印
打印
打印

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

因此:

bash 复制代码
let printing = false;

开始打印:

bash 复制代码
function acquirePrintLock() {
  if (printing) {
    throw new Error(
      "DYMO 正在打印,请勿重复提交。"
    );
  }

  printing = true;
}

结束后:

bash 复制代码
function releasePrintLock() {
  printing = false;
}

最终:

bash 复制代码
async function printLabel(
  labelXml,
  options = {}
) {
  acquirePrintLock();

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

这里 finally 非常重要。

因为无论打印:

bash 复制代码
成功

还是:

bash 复制代码
失败

都必须释放锁。


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

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

真正执行打印的是:

bash 复制代码
framework.printLabel(
  printerName,
  "",
  labelXml,
  ""
);

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

bash 复制代码
function printLabelAsync(
  framework,
  printerName,
  labelXml,
  options = {}
) {
  const timeout =
    Number(options.timeout) ||
    DYMO_CONFIG.printTimeout;

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

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

        completed = true;

        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()

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

bash 复制代码
printLabel()

而是再封装一层:

bash 复制代码
async function printVisitorTicket(
  data = {},
  options = {}
) {
  const checkResult = check();

  if (!checkResult.success) {
    return checkResult;
  }

  let templateXml;

  try {
    templateXml =
      getVisitorLabelTemplate();

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

  let labelXml;

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

  return printLabel(
    labelXml,
    options
  );
}

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

业务入口。


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

后端接口返回:

bash 复制代码
const response = await createVisitor();

假设:

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

页面只需要:

bash 复制代码
const result =
  await printVisitorTicket(
    response.data
  );

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

  return;
}

ElMessage.success(
  "打印成功"
);

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

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

bash 复制代码
DYMO Framework
Label XML
Base64
打印机名称
打印锁
XML 转义
模板对象

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

不推荐:

bash 复制代码
if (
  error.message.includes("paper")
) {
  ...
}

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

应该让服务层统一转换。

例如:

bash 复制代码
DYMO_NOT_AVAILABLE
NO_PRINTER
PRINTER_NOT_FOUND
PRINTER_OFFLINE
PRINTER_NO_PAPER
PRINT_TIMEOUT
PRINT_FAILED
TEMPLATE_ERROR
BIND_DATA_FAILED

然后页面只负责:

bash 复制代码
switch (result.code) {
  case "PRINTER_NO_PAPER":
    ElMessage.error(
      "打印机缺纸,请联系管理员"
    );
    break;

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

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

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

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

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

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

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

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

而不是提前猜一个:

bash 复制代码
NO_PAPER

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

开发过程中还遇到过:

bash 复制代码
ERR_CONNECTION_REFUSED

例如:

bash 复制代码
https://127.0.0.1:41952/DYMO/DLS/Printing/PrintLabel

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

它代表:

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

所以排查应该分成两层:

bash 复制代码
第一层
浏览器
   ↓
DYMO Web Service

确认:

bash 复制代码
DYMO Connect 是否运行

然后:

bash 复制代码
第二层
DYMO Framework
   ↓
打印机

确认:

bash 复制代码
打印机是否存在
打印机是否在线
打印机是否正常

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


十七、最终形成三层结构

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

第一层:业务层

bash 复制代码
await printVisitorTicket(data);

负责:

我要打印访客标签。


第二层:服务层

bash 复制代码
dymoService.js

负责:

bash 复制代码
DYMO Framework
打印机
模板
打印
异常

第三层:模板绑定层

bash 复制代码
dymoTemplateBinding.js

负责:

bash 复制代码
Company
UserName
Date
QrCode
Text1
Text2
Text3

与 XML 对象之间的映射。


十八、最终调用关系

最终完整调用链:

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

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

最终业务代码从原来的:

bash 复制代码
业务页面
 ↓
DYMO Framework
 ↓
打印机

变成:

bash 复制代码
业务页面
 ↓
printVisitorTicket(data)
 ↓
完成打印

以后如果需要增加:

bash 复制代码
访客标签
员工标签
车辆标签
会议标签
临时卡标签

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

可以继续:

bash 复制代码
printVisitorTicket(data);

printEmployeeTicket(data);

printVehicleTicket(data);

printMeetingTicket(data);

底层依然共用:

bash 复制代码
dymoService.js
dymoTemplateBinding.js
DYMO Framework

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


二十、完整核心代码

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

dymoTemplateBinding.js

bash 复制代码
const OBJECT_TYPES =
  "TextObject|ImageObject|DateTimeObject";

function escapeXmlText(value) {
  if (value === null || value === undefined) {
    return "";
  }

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

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

function normalizeBase64Image(value) {
  if (value === null || value === undefined) {
    return "";
  }

  let base64 = String(value).trim();

  if (!base64) {
    return "";
  }

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

  while (dataUrlRegex.test(base64)) {
    base64 = base64.replace(
      dataUrlRegex,
      ""
    );
  }

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

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

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

  return base64;
}

function createNameRegex(objectName) {
  const escapedName =
    escapeRegExp(objectName);

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

function findObjectBlock(
  labelXml,
  objectName
) {
  if (!labelXml || !objectName) {
    return null;
  }

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

  const nameRegex =
    createNameRegex(objectName);

  let match;

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

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

  return null;
}

function replaceObjectBlock(
  labelXml,
  objectName,
  replacer
) {
  const object =
    findObjectBlock(
      labelXml,
      objectName
    );

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

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

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

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

function setTextObjectValue(
  labelXml,
  objectName,
  value
) {
  const safeValue =
    escapeXmlText(value);

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

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

            return `${prefix}${safeValue}${suffix}`;
          }

          return `${prefix}${suffix}`;
        }
      );
    }
  );
}

function setImageObjectValue(
  labelXml,
  objectName,
  value
) {
  const base64 =
    normalizeBase64Image(value);

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

      if (!dataRegex.test(block)) {
        return block;
      }

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

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

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

    case "date":
    case "datetime":
    case "DateTimeObject":
      return setDateTimeObjectValue(
        labelXml,
        objectName,
        value
      );

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

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

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

  let resultXml = labelXml;

  const bindingMap = {
    Company: {
      type: "text",
    },

    UserName: {
      type: "text",
    },

    Date: {
      type: "date",
    },

    QrCode: {
      type: "image",
    },

    Text1: {
      type: "text",
    },

    Text2: {
      type: "text",
    },

    Text3: {
      type: "text",
    },
  };

  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 集成最终不是简单地完成:

bash 复制代码
printLabel()

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

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

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

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

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

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

第四,DateTimeObject 不要随意修改 StaticDateTime

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

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

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

bash 复制代码
const result =
  await printVisitorTicket(
    response.data
  );

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

  return;
}
相关推荐
两只羊ovo1 小时前
Vue 3 + DeepSeek 流式输出实战:从“干等”到“打字机”体验
前端·vue.js
Yeyu1 小时前
车载多 App 同屏渲染(二):SurfaceControlViewHost 跨进程
前端
樊小肆1 小时前
离谱,每轮请求 25% 的 token,竟在重发模型想完就扔的内心独白
前端·人工智能·agent
小妖现世1 小时前
前端面试复习笔记:React 高频题 + JS 手写题
前端
holidaypenguin1 小时前
Windows 本地 HTTPS 证书生成指南
前端
酷酷的逗逗乐1 小时前
前端转 Agent 开发 · 第六节
前端·程序员
回家吃饭去吧1 小时前
WebAssembly 深度解析:前端性能的最后一块拼图
前端
爱勇宝1 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
尤小小1 小时前
Vite-SSG 实践:Vue项目预渲染落地完整方案
前端·vue.js