更新记录

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端待测试。封装了简单文字排版,比如换行、居中、加粗,超行省略等。

本示例使用步骤:

  1. 需要安装iconv-lite插件,为了解决中文乱码问题

    npm install iconv-lite
    pnpm add iconv-lite
  2. 把组件导入到项目的uni_modules目录下
  3. 在需要的页面引入组件
    <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('阿华打印')

隐私、权限声明

1. 本插件需要申请的系统权限列表:

蓝牙

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。