更新记录
1.0.0(2026-01-25)
阿华VUE3蓝牙小程序打印
平台兼容性
uni-app(3.6.15)
| Vue2 |
Vue3 |
Chrome |
Safari |
app-vue |
app-nvue |
Android |
iOS |
鸿蒙 |
| √ |
√ |
- |
- |
√ |
- |
- |
- |
- |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
小红书小程序 |
快应用-华为 |
快应用-联盟 |
| √ |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
# 阿华蓝牙打印插件,支持二维码,支持二次开发
适合支持ESC/POS协议的蓝牙打印机,比如佳博等等
支持vue3\小程序,APP端待测试。封装了简单文字排版,比如换行、居中、加粗,超行省略等。
本示例使用步骤:
-
需要安装iconv-lite插件,为了解决中文乱码问题
npm install iconv-lite
pnpm add iconv-lite
- 把组件导入到项目的uni_modules目录下
- 在需要的页面引入组件
<template>
<hua-print></hua-print>
</template>
如果不想用默认的页面,可以使用插槽替代,然后用ref调用组件暴露的方法打印
例子
<template>
<hua-print ref="huaPrintRef">
<template #listBox>
<view>
中间内容
<view @click="handlePrintInit">打开蓝牙搜索设备</view>
</view>
</template>
<template #footBox>
<view>
尾部内容
</view>
</template>
</hua-print>
</template>
方法属性
<script setup>
const huaPrintRef = ref(null)
// 搜索出来的设备列表
const devices = computed(() => huaPrintRef.value?.devices || [])
// 是否已经连接
const isConnected = computed(() => huaPrintRef.value?.isConnected || false)
// 当前连接的设备id
const currentDeviceId = computed(() => huaPrintRef.value?.currentDeviceId || '')
// 把文字转成ESC/POS编码,发送内容给打印的时候,先在内容前面加上escPos.value.initChinese()
const escPos = computed(() => huaPrintRef.value?.escPos || {})
// 二维码对象 qrCode.value.print('阿华打印', 5), // 二维码大小5
const qrCode = computed(() => huaPrintRef.value?.qrCode || {})
// 初始化打印,里面内容根据需要自定义
async function handlePrintInit() {
if (!huaPrintRef.value.currentDeviceId && !huaPrintRef.value.isConnected) {
try {
await huaPrintRef.value.initBluetooth()
uni.showLoading({ title: '搜索中...' })
try {
await huaPrintRef.value.searchDevices()
uni.hideLoading()
}
catch (err) {
uni.hideLoading()
uni.showToast({ title: '搜索失败', icon: 'error' })
}
}
catch (err) {
uni.showToast({ title: '打开蓝牙失败', icon: 'error' })
}
}
}
// 连接或断开设备 huaPrintRef.value?.disconnect(false),disconnect(false)默认是true,true是全部关闭设备,要重新初始化蓝牙;false是断开当前连接的设备
async function handleConnectDevice(item) {
console.log(item, currentDeviceId.value, isConnected.value)
if (item.deviceId === currentDeviceId.value && isConnected.value) {
uni.showLoading({ title: '正在断开中...' })
await huaPrintRef.value?.disconnect(false)
uni.hideLoading()
uni.showToast({ title: '断开成功' })
return
}
if (item.deviceId !== currentDeviceId.value && isConnected.value) {
uni.showLoading({ title: '正在断开中...' })
await huaPrintRef.value?.disconnect(false)
uni.hideLoading()
uni.showToast({ title: '断开成功' })
}
uni.showLoading({ title: '正在连接中...' })
try {
await huaPrintRef.value?.connectDevice(item.deviceId, item.name)
uni.hideLoading()
uni.showToast({ title: '连接成功' })
}
catch (err) {
uni.hideLoading()
uni.showToast({ title: '连接失败', icon: 'error' })
}
}
// 打印内容,测试小票
function handlePrintTest() {
const printData = [
// 重点!重点!重点!重点!
escPos.value.initChinese(), // 核心:初始化中文模式
escPos.value.align(1), // 居中
escPos.value.bold(1), // 加粗
escPos.value.fontSize(2, 2), // 大字体
escPos.value.text('阿华打印\n\n'),
escPos.value.bold(0), // 取消加粗
escPos.value.fontSize(1, 1), // 正常字体
escPos.value.text(`订单\n`),
escPos.value.lineFeed(),
escPos.value.align(0), // 左对齐
escPos.value.line(), // 分隔线
escPos.value.text(`门店:阿华外卖店\n`),
escPos.value.text(`门店地址:有你的地方\n`),
escPos.value.line(),
escPos.value.bold(1),
escPos.value.text('商品 数量\n'),
escPos.value.bold(0),
escPos.value.line(),
]
// 商品列表
const items = [{
spuName: '湛江白切鸭',
productSpec: `大份`,
quantity: 2,
}]
const goods = ['云南过桥米线', '程序员无BUG套餐', '程序员996套餐', '程序员无BUG套餐程序员无BUG套餐程序员无BUG套餐程序员无BUG套餐']
for (let i = 0; i < 4; i++) {
items.push({
spuName: goods[i],
productSpec: `大份`,
quantity: i + 2,
})
}
// 58mm纸固定配置,其他纸张大小可以更改这里
const NAME_COL_FIX_WIDTH = 22; // 名称列强制固定打印宽度(核心)
const QTY_COL_TOTAL_WIDTH = 6; // 数量列固定宽度(右对齐,数字+半角空格)
const FULL_SPACE = ' '; // 全角空格(占2,和中文匹配)
const HALF_SPACE = ' '; // 半角空格(占1,和英文/数字匹配)
items.forEach((item) => {
const wrappedName = huaPrintRef.value?.wrapLongText(item.spuName, 22) // 原有换行逻辑不变,保证不超行
const wrappedSpec = huaPrintRef.value?.wrapLongText(item.productSpec, 22)
const qtyStr = item.quantity.toString(); // 数量转字符串(兼容数字)
const nameLines = wrappedName.split('\n')
for (let i = 0; i < nameLines.length; i++) {
let nameLine = nameLines[i] || '';
// 1. 精准计算当前名称行的打印宽度差值
const currentWidth = huaPrintRef.value?.getPrintWidth(nameLine) || 0;
let widthDiff = NAME_COL_FIX_WIDTH - currentWidth;
// 2. 动态补空格:差≥2补全角、差1补半角,精准凑到固定宽度(核心修复)
while (widthDiff > 0) {
if (widthDiff >= 2) {
nameLine += FULL_SPACE;
widthDiff -= 2;
} else {
nameLine += HALF_SPACE;
widthDiff -= 1;
}
}
// 3. 数量列处理:仅第一行显示,半角空格右对齐,非第一行留空(和第一行对齐)
let currentQty = '';
if (i === 0) {
// 数字/英文用半角空格右对齐,精准卡在数量列固定宽度
currentQty = qtyStr.padStart(QTY_COL_TOTAL_WIDTH, HALF_SPACE);
} else {
// 换行后的名称行,数量列填等宽半角空格,保持列对齐
currentQty = HALF_SPACE.repeat(QTY_COL_TOTAL_WIDTH);
}
// 4. 拼接:名称列(精准固定宽)+ 数量列(固定宽),绝对对齐
printData.push(escPos.value.text(`${nameLine}${currentQty}\n`));
}
// 5. 规格行处理:和名称列对齐后,数量列留空,避免规格行错位
let specLine = wrappedSpec || '';
const specWidth = huaPrintRef.value?.getPrintWidth(specLine) || 0;
let specWidthDiff = NAME_COL_FIX_WIDTH - specWidth;
// 规格行也按相同规则补空格,和名称列完全对齐
while (specWidthDiff > 0) {
if (specWidthDiff >= 2) {
specLine += FULL_SPACE;
specWidthDiff -= 2;
} else {
specLine += HALF_SPACE;
specWidthDiff -= 1;
}
}
// 规格行数量列留空,保持整列垂直对齐
printData.push(escPos.value.text(`${specLine}${HALF_SPACE.repeat(QTY_COL_TOTAL_WIDTH)}\n\n`));
})
// 合计+二维码
printData.push(
escPos.value.line(),
escPos.value.bold(1),
escPos.value.fontSize(1, 2),
escPos.value.text(`总计:88件\n`), // 重点:¥直接显示
escPos.value.bold(0),
escPos.value.fontSize(1, 1),
escPos.value.lineFeed(2),
escPos.value.align(1), // 二维码居中
escPos.value.lineFeed(),
qrCode.value.print('阿华打印', 5), // 二维码大小5
escPos.value.lineFeed(3),
escPos.value.lineFeed(4),
escPos.value.align(0), // 左对齐
escPos.value.text(`打印时间: 2025-01-01 12:00:00\n\n`),
escPos.value.text(' \n'),
escPos.value.line(),
escPos.value.cut(), // 切纸
)
handlePrintContent(printData)
}
// 发送给打印机内容,核心方法是printReceipt,可以根据自己内容直接调用这个方法,printContent是个数组
function handlePrintContent(printContent) {
return new Promise((resolve, reject) => {
try {
huaPrintRef.value?.printReceipt(printContent).then(() => {
resolve(true)
})
uni.showToast({ title: '打印任务已发送' })
}
catch (err) {
uni.showToast({ title: '打印失败', icon: 'error' })
reject(err)
}
})
}
</script>
属性说明
| 名称 |
类型 |
默认值 |
描述 |
| deviceNames |
Array |
['GP', 'GB', '佳博'] |
匹配设备的名称,符合的添加到devices参数里面,5秒停止搜索设备 |
<hua-print ref="huaPrintRef" :deviceNames="['GP']"></hua-print>
组件暴露的参数
| 名称 |
类型 |
默认值 |
描述 |
| devices |
Array |
[] |
设备列表,[{deviceId:'', name: '', RSSI: ''}] |
| connectedDevice |
Object 或 null |
null |
当前连接的设备信息,{deviceId:'', name: ''} |
| isConnected |
Boolean |
false |
是否已经连接设备 |
| isScanning |
Boolean |
false |
是否正在搜索设备 |
| currentDeviceId |
String |
'' |
当前连接的设备ID,可以用connectedDevice这个参数 |
| writeServiceId |
String |
'' |
当前连接的设备ServiceId |
| writeCharacteristicId |
String |
'' |
当前连接的设备CharacteristicId |
| escPos |
Object |
|
文字设置对象 |
| qrCode |
Object |
|
二维码设置对象 |
escPos参数说明
| 名称 |
类型 |
默认值 |
描述 |
| initChinese |
Function |
|
中文打印机初始化,打印内容的时候和它并在一起,参考上面的例子 |
| lineFeed |
Function |
|
换行 |
| align |
Function |
0 |
对齐:0左 1中 2右 |
| bold |
Function |
0 |
是否加粗:0关 1开 |
| fontSize |
Function(w = 1, h = 1) |
|
字体大小:width/height 1-8 |
| text |
Function |
|
打印文本(强制GBK),中文的都需要调用这个方法 |
| line |
Function |
|
分隔线 |
| cut |
Function |
|
切纸(佳博兼容) |
| beep |
Function |
|
蜂鸣 |
qrCode参数说明
| 名称 |
类型 |
默认值 |
描述 |
| print |
Function(data, size = 5) |
|
二维码,data:内容,size:二维码大小 |
方法说明-initBluetooth
| 名称 |
类型 |
默认值 |
描述 |
| initBluetooth |
Function() |
|
打开蓝牙,蓝牙初始化,返回值new Promise |
const res = await initBluetooth()
console.log(res) res = true
方法说明-searchDevices
| 名称 |
类型 |
默认值 |
描述 |
| searchDevices |
Function() |
|
搜索设备,返回值new Promise,有匹配数据返回devices |
const res = await searchDevices()
console.log(res)
res = [{
deviceId: '',
name: '',
RSSI: '',
}]
方法说明-stopSearch
| 名称 |
类型 |
默认值 |
描述 |
| stopSearch |
Function() |
|
停止搜索设备 |
方法说明-connectDevice
| 名称 |
类型 |
默认值 |
描述 |
| connectDevice |
Function(deviceId, deviceName) |
|
连接设备,返回值new Promise |
const res = await connectDevice(deviceId, name)
console.log(res)
if (res) {
uni.showToast({ title: '连接成功' })
}
方法说明-disconnect
| 名称 |
类型 |
默认值 |
描述 |
| disconnect |
Function(isCloseAll = true) |
|
断开设备,如果只需关闭某个设备isCloseAll=false,默认是全部断开。全部断开要重新初始化蓝牙initBluetooth() |
await disconnect(false)
方法说明-printReceipt
| 名称 |
类型 |
默认值 |
描述 |
| printReceipt |
Function(arr) |
|
发送内容打印,内容是个数组,返回是new Promise |
const printData = [
escPos.initChinese(), // 核心:初始化中文模式
escPos.align(1), // 居中
escPos.bold(1), // 加粗
escPos.fontSize(2, 2), // 大字体
escPos.text('阿华打印\n\n'),
escPos.bold(0), // 取消加粗
escPos.fontSize(1, 1), // 正常字体
escPos.text(`订单\n`),
escPos.lineFeed(),
escPos.align(0), // 左对齐
escPos.line(), // 分隔线
escPos.text(`门店:阿华外卖店\n`),
escPos.text(`门店地址:有你的地方\n`),
escPos.line(),
escPos.bold(1),
escPos.text('商品 数量\n'),
escPos.bold(0),
escPos.line(),
]
handlePrintContent(printData)
function handlePrintContent(arr) {
return new Promise((resolve, reject) => {
try {
printReceipt(arr).then(() => {
resolve(true)
})
uni.showToast({ title: '打印任务已发送' })
}
catch (err) {
uni.showToast({ title: '打印失败', icon: 'error' })
reject(err)
}
})
}
方法说明-cleanup
| 名称 |
类型 |
默认值 |
描述 |
| cleanup |
Function() |
|
清理函数,包括停止搜索设备、断开连接,卸载蓝牙监听等等 |
cleanup()
方法说明-wrapLongText
| 名称 |
类型 |
默认值 |
描述 |
| wrapLongText |
Function(str, maxWidth = 32, maxLines = 2, ellipsis = true) |
|
长文本按打印宽度自动换行,str 要处理的文本;maxWidth 一行最大字符数(58mm=32,80mm=48;中文占2个字符位);maxLines 最大显示行数,默认2行,超过截断;ellipsis 是否加省略号,默认true(截断后最后一行加…) |
const str = wrapLongText('阿华打印')
方法说明-getPrintWidth
| 名称 |
类型 |
默认值 |
描述 |
| getPrintWidth |
Function(str) |
|
计算字符串的打印宽度(中文/全角=2,英文/半角=1);return number 打印宽度 |
const number = getPrintWidth('阿华打印')