移动端蓝牙打印
移动端蓝牙打印功能开发指南
# 概述
本指南介绍如何在 uni-app 移动端项目中集成和使用蓝牙打印功能。蓝牙打印功能基于 SPP(Serial Port Profile) 协议实现,适用于支持 SPP 的蓝牙打印机(如芝柯 XT453 等)。
# 核心依赖
// 主要依赖的工具类
import SPPPrintHelper from "@/utils/bluetooth/spp-print-helper.js"; // 蓝牙打印辅助类
import CPCLBuilder from "@/utils/bluetooth/builder/cpcl-builder.js"; // CPCL 指令构建器
import { renderCpclTemplate } from "@/utils/bluetooth/utils.js"; // 模板渲染工具
1
2
3
4
2
3
4
# 基本使用流程
# 1. 进入页面时注册
在页面的 created 生命周期中初始化蓝牙打印机实例:
export default {
name: "your-page",
mixins: [mixin], // 必须引入 mixin
data() {
return {
sppHelper: null, // SPP 蓝牙打印辅助实例
printTemplate: "", // 打印模板内容
};
},
created() {
console.log("初始化 SPP 蓝牙打印辅助实例");
// 初始化 SPP 蓝牙打印辅助实例
this.sppHelper = new SPPPrintHelper();
this.sppHelper.init();
// 在 created 中只做初始化,不立即连接
// 连接放到 mounted 中后台执行,避免阻塞页面首帧渲染
// 获取打印模板(如果需要)
this.getPrintTemplate();
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2. 页面挂载时连接打印机
在 mounted 生命周期中后台静默连接默认打印机:
mounted() {
// 页面挂载完成后再后台静默连接默认打印机,避免阻塞页面进入
if (this.sppHelper) {
console.log("后台静默连接默认打印机");
this.sppHelper.connectInBackground({
timeout: [`<nowiki>`](http://<nowiki>)5000,
onError: (err) => {
$toast.toast("自动连接默认打印机失败:" + err.message);
console.error("[your-page] auto connect default err :>> ", err);
},
});
// 注册打印机断开回调,收到系统 ACL 断开广播时提示用户
if (this.sppHelper.printer) {
this.sppHelper.printer.onDisconnect = (address) => {
console.warn("[sppTool] printer disconnected:", address);
$toast.toast("蓝牙打印机已断开连接");
};
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. 使用时调用打印
在业务逻辑中调用打印功能。以下是几种常见的使用场景示例:
# Demo 1: 使用 CPCL 构建器直接构建
// 顶部导入依赖
import CPCLBuilder from "@/utils/bluetooth/builder/cpcl-builder.js";
export default {
// ... 其他配置
methods: {
/**
* 使用 CPCL 构建器直接构建打印内容
*/
async printWithBuilder() {
try {
// 1. 创建构建器实例
const builder = new CPCLBuilder();
// 2. 初始化指令(必须放在开头)
builder.init(420); // 设置标签高度为 420 点
// 3. 添加页面元素
builder.addBox(0, 0, 520, 350); // 添加矩形框
builder.addText("物料编码: " + this.partCode, 55, 0, 10, 10);
builder.setAlignment("center");
builder.addBarcode("128", this.barcodeNo, 10, 50);
// 4. 添加打印指令(必须放在最后)
builder.print();
// 5. 构建 CPCL 指令字符串
const cpclText = builder.build();
// 6. 发送到打印机
await this.sppHelper.printCPCL(cpclText);
$toast.success("打印成功");
} catch (err) {
console.error("[your-page] print error :>> ", err);
$toast.toast("打印失败:" + (err.message || err));
}
},
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Demo 2: 使用模板渲染(后端模板)
// 顶部导入依赖
import { renderCpclTemplate } from "@/utils/bluetooth/utils.js";
import printTemplateInfoBluetoothApi from './api/printTemplateInfoBluetooth.js';
export default {
// ... 其他配置
methods: {
/**
* 使用后端模板进行渲染打印
*/
async printWithBackendTemplate() {
try {
// 1. 准备打印数据
const data = {
barcodeNo: this.barcodeNo,
partCode: this.partCode,
partName: this.partName,
qty: this.qty,
// ... 其他业务字段
};
// 2. 渲染模板(template 从后端 API 获取)
const cpclText = renderCpclTemplate(this.printTemplate, data);
console.log('渲染后的 CPCL:', cpclText);
// 3. 发送到打印机
await this.sppHelper.printCPCL(cpclText);
$toast.success("打印成功");
} catch (err) {
console.error("[your-page] print error :>> ", err);
$toast.toast("打印失败:" + (err.message || err));
}
},
/**
* 获取后端模板(通常在 created/mounted 中调用)
*/
async getPrintTemplate() {
try {
const res = await printTemplateInfoBluetoothApi.getByCode("your-template-code");
if (res?.code === 0) {
this.printTemplate = res?.data?.content || "";
}
} catch (err) {
console.error("getPrintTemplate err >>> ", err);
this.catchErr(err);
}
},
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Demo 3: 使用页面级工具函数
// 顶部导入依赖
import { barcodePrintTemplate } from "./utils/print.js";
export default {
// ... 其他配置
methods: {
/**
* 使用页面级工具函数打印
*/
async printWithPageUtils() {
try {
// 1. 准备打印数据
const data = {
barcodeNo: this.barcodeNo,
partCode: this.partCode,
partName: this.partName,
};
// 2. 调用工具函数生成 CPCL 指令
const cpclText = barcodePrintTemplate(data);
// 3. 发送到打印机
await this.sppHelper.printCPCL(cpclText);
$toast.success("打印成功");
} catch (err) {
console.error("[your-page] print error :>> ", err);
this.catchErr(err);
}
},
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Demo 4: 获取模板 API 文件示例
// api/printTemplateInfoBluetooth.js
import request from "@/api/index";
const basePath = "/sys/v1/print-template-info-bluetooth";
export default {
/**
* 根据编码获取打印模板信息
* @param {string} code 模板编码
* @returns {Promise<{code: number, msg: string, data: {content: string}}>}
*/
getByCode(code) {
return request({
url: `${basePath}/get-by-code?code=${code}`,
method: "GET",
});
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Demo 5: 页面级打印工具函数示例
// utils/print.js
import CPCLBuilder from "@/utils/bluetooth/builder/cpcl-builder.js";
/**
* 条码打印模板
* @param {object} data 参数
* @param {string} data.barcodeNo 条码
* @param {string} data.partCode 物料编码
* @param {string} data.partName 物料名称
* @param {number} opts.height - 打印高度,默认420
* @returns {string} CPCL 指令字符串
*/
export function barcodePrintTemplate(data, opts) {
const builder = new CPCLBuilder();
const totalWidth = 520;
const totalHeight = 350;
const row1Height = 116;
// 初始化命令(必须放在开头)
builder.init(opts?.height || 420);
// 方框
builder.addBox(0, 0, totalWidth, totalHeight);
// 添加内容
builder.addText(`物料编码: ${data.partCode}`, 55, 0, 4, 4);
builder.addText(`物料名称: ${data.partName}`, 55, 0, 260, 4);
builder.setAlignment("center");
builder.addBarcode("128", data.barcodeNo, 4, row1Height + 4);
builder.addText(data.barcodeNo, 55, 4, row1Height + 50 + 8);
// 打印指令(必须放在最后)
builder.print();
return builder.build();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
重要提示:所有导入语句(import)都应该放在 Vue 组件的顶部,而不是在方法内部。
# 4. 离开页面时注销
在页面的 $onUnload 生命周期中断开蓝牙连接:
export default {
name: "your-page",
mixins: [mixin], // 必须引入 mixin
methods: {
/**
* 页面卸载生命周期,同 uni-app 的 onUnload,兼容 H5 和 APK
*/
$onUnload() {
// 页面离开时断开 SPP 蓝牙连接,释放资源
if (this.sppHelper) {
console.log("断开 SPP 蓝牙连接");
this.sppHelper.disconnect().catch((err) => {
console.error("SPP disconnect err :>> ", err);
});
}
},
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 常见问题处理
# 1. 连接失败处理
// 连接时的错误处理
this.sppHelper.connectInBackground({
timeout: 5000,
onError: (err) => {
// 可以提示用户或记录日志
console.warn("连接打印机失败:", err.message);
$toast.toast("打印机连接失败,请检查蓝牙和设备");
},
onSuccess: () => {
console.log("打印机连接成功");
},
});
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 2. 打印失败处理
try {
await this.sppHelper.printCPCL(cpclText);
$toast.success("打印成功");
} catch (err) {
console.error("打印失败:", err);
// 根据错误类型给予用户提示
if (err.message.includes("蓝牙未开启")) {
$toast.toast("请开启手机蓝牙");
} else if (err.message.includes("未配对")) {
$toast.toast("打印机未配对,请先配对设备");
} else if (err.message.includes("连接超时")) {
$toast.toast("打印机连接超时,请检查设备是否开启");
} else if (err.message.includes("断开连接")) {
$toast.toast("打印机已断开连接");
} else {
$toast.toast("打印失败:" + err.message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3. 重试机制
/**
* 带重试的打印方法
* @param {string} cpclText CPCL 指令
* @param {number} maxRetries 最大重试次数,默认 2
*/
async printWithRetry(cpclText, maxRetries = 2) {
let retries = 0;
while (retries <= maxRetries) {
try {
await this.sppHelper.printCPCL(cpclText);
return true; // 成功
} catch (err) {
retries++;
console.warn(`打印失败,第 ${retries} 次重试:`, err.message);
if (retries > maxRetries) {
throw err; // 超出重试次数,抛出错误
}
// 短暂延迟后重试
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 开发注意事项
- 权限要求:确保 App 具有蓝牙权限
- 环境限制:蓝牙打印功能仅支持 APP-PLUS 环境(打包后的 APK)
- 默认设备:需要先通过系统设置添加并设置默认打印机
- 连接管理:遵循 "进入注册 → 使用时连接 → 离开注销" 的生命周期管理
- 错误处理:所有打印操作都应包含 try-catch 错误处理
- 性能优化:避免在页面进入时立即连接,使用
connectInBackground后台连接 - 资源释放:页面离开时必须调用
disconnect()释放蓝牙连接
# 参考示例项目
- LES条码拆分:
src/pages/barcode/barcode-split/ - LES标准拣货:
src/pages/outbound/pick-goods/
这两个项目展示了完整的蓝牙打印功能实现,包括模板管理、错误处理和生命周期管理。
在线编辑 (opens new window)