1.微信公众平台隐私协议添加蓝牙协议,审核通过即可2.蓝牙打印流程2.1 搜索蓝牙首先我们需要先初始化蓝牙模块,在进行搜索蓝牙。在监听到附近蓝牙设备时,记录他的名称和deviceId。初始化蓝牙(openBluetoothAdapter):查看蓝牙是否可用,若初始化失败,则是蓝牙未打开,提示用户打开蓝牙。若已经打开蓝牙,则准备进行蓝牙搜索。2.2 连接蓝牙搜索蓝牙(startBluetoothDevicesDiscovery):开始搜索蓝牙设备。这一步需要和关闭搜索蓝牙(stopBluetoothDevicesDiscovery)成对使用,长时间占用搜索设备,浪费资源,在查找到需要的设备连接之后需要主动去停止搜索设备。搜索出附近蓝牙设备后,获取蓝牙设备的deviceId传入createBLEConnection方法中。在连接蓝牙设备时,我们需要注意的是保证尽量成对的调用 createBLEConnection 和 closeBLEConnection 接口。安卓如果多次调用 createBLEConnection 创建连接,有可能导致系统持有同一设备多个连接的实例,导致调用 closeBLEConnection 的时候并不能真正的断开与设备的连接。我们将连接成功的蓝牙信息存到currDev中,以便直接连接,无需进行搜索操作。获取已搜索到的蓝牙列表(getBluetoothDevices):查看所有已经发现的蓝牙设备getBluetoothDevices。在这一步可以查看到以前已经获取到的蓝牙设备deviceId。可以在这一步中查看以前已经连接到的设备,主动去尝试连接这个设备。3.连接蓝牙设备连接蓝牙设备(createBLEConnection):通过设备的deviceId来连接设备。在这里如果APP若是已经连接过此低功耗蓝牙设备,可以直接传入之前设备ID进行尝试连接。这一步的连接操作需要和关闭连接closeBLEConnection成对操作。如果多次调用创建连接,有可能会导致系统持有一个设备的多个连接实例,导致在调用关闭连接的时候不能真正关闭连接。4.获取服务获取设备所有服务(getBLEDeviceServices):在连接设备之后,APP需要主动去获取设备的所有服务(services),设备会返回给APP设备的服务列表(services)包含设备服务的UUID,该服务是否为主服务。获取服务特征值(getBLEDeviceCharacteristic):在获取设备的服务列表之后,根据自己设备的蓝牙协议接口文档,根据指定的服务ID(serviceId)使用获取服务特征值方法传入两个参数设备ID(deviceId)和服务ID(serviceId)向设备获取该服务中的所有的特征值(characteristic),设备会向APP返回该服务中的所有特征值列表,列表包含设备特征值的UUID,该特征值支持的操作类型。5.写入命令向设备写入控制命令(writeBLECharacteristicValue):可以向设备写入(发送)控制命令writeBLECharacteristicValue,此方法是向低功耗蓝牙设备特征值写入二进制数据。需要注意只有该特征值的属性支持write才可以调用此方法。在此方法调用成功后,设备特征值发生改变,就会触发onBLECharacteristicValueChange回调,主动返回特征值数据。普通蓝牙则需要根据打印机相对指令进行设置,一般使用tsc.js,传递蓝牙指令.template view view style="text-align: center; margin-bottom: 20rpx" view 说明:首次使用请先打开手机蓝牙,点击下方“连接”按钮绑定蓝牙打印机。/view /view view style="padding-bottom: 3px" view view产品名称:/view /view view { { jdcername }} /view /view view style="padding-bottom: 3px" view view打印机名称:/view /view view { { devicename }} text v-show="devicename" style="color: #09be4f"(已连接)/text text v-show="!devicename" style="color: #dc4e41"未连接打印机/text text @click="showPrinterList" style="color: #007aff; margin-left: 20px" 连接/text /view view style="padding: 0px 10px" button type="primary" plain size="mini" style="margin-top: 5px" @click="handlePrint" 打印 /button /view /view uni-popup ref="popupnew" :type="type" :animation="false" :maskClick="true" @change="change" view style=" background-color: #fff; padding: 15px; width: 600rpx; font-size: 38rpx; " view v-for="(item, index) in deviceList" :key="index" view style=" display: flex; justify-content: space-between; margin-bottom: 20rpx; " text名称:{ { item.name }}/text view style="color: #007aff; cursor: pointer" @click="connectDevice(item)" 连接/view /view /view view v-if="deviceList.length === 0 scanning" style="text-align: center" 扫描中.../view view v-if="deviceList.length === 0 !scanning" style="text-align: center" 未发现设备,请下拉刷新/view /view /uni-popup /view /template script setup lang="ts" import { $URL } from "../../../api/gbk.js"; import { ref, onMounted, onUnmounted } from "vue"; // ========== 全局数据 ========== let Globalindex = { ret: 0, statmessage: "", jdcercode: "http://distss.com/12342474241150839802", jdcerbasename: "家庭农场", stockbillid: 200288, jdcername: "产品名称:白菜 数量(重量):5公斤", jdcerorigin: "上海斜沟崖", // 没有标点,无需修改 jdcertel: "联系方式:4006257518", // 冒号改为全角 jdcerdate: "开具日期:2026年4月22日", isinternalqc: true, // 内部质量控制 isselfexamed: true, // 自我检测合格 isrequexamed: true, // 委托检测合格 }; // import { Globalindex } from "../../../global/globalindex.ts"; // ========== 蓝牙相关状态 ========== const deviceList = ref([]); const devicename = ref(""); const deviceitems = ref({}); let connectedDeviceId = ""; let writeCharacteristic = null; let serviceId = ""; const type = ref("center"); const popupnew = ref(null); const jdcername = Globalindex.jdcername; let scanning = ref(false); // 打印指令缓冲区 let command = ref([]); // ========== 工具函数(编码转换)========== // ASCII 字符串转字节数组(用于 TSPL 命令) function asciiToBytes(str: string): Uint8Array { const bytes = []; for (let i = 0; i str.length; i++) { bytes.push(str.charCodeAt(i)); } return new Uint8Array(bytes); } // 中文字符串转 GBK 字节数组(利用 $URL.encode 返回的十六进制串) function chineseToBytes(str: string): Uint8Array { const hex = $URL.encode(str); // 例如 "C4E3BAC3" const bytes = []; for (let i = 0; i hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } return new Uint8Array(bytes); } // 通用 GBK 转字节(与 chineseToBytes 相同,用于 ESC/POS 分支兼容) function gbkToBytes(str: string): Uint8Array { return chineseToBytes(str); } // 添加纯 ASCII 命令到缓冲区 function addCommand(content: string) { const bytes = asciiToBytes(content); for (let i = 0; i bytes.length; i++) { command.value.push(bytes[i]); } } // ========== TSPL 指令函数 ========== function setSize(pageWidth: number, pageHeight: number) { addCommand(`SIZE ${pageWidth} mm, ${pageHeight} mm\r\n`); } function setGap(printGap: number) { addCommand(`GAP ${printGap} mm,0 mm\r\n`); } function setDirection(n: number) { addCommand(`DIRECTION ${n}\r\n`); } function setDensity(n: number) { addCommand(`DENSITY ${n}\r\n`); } function setCls() { addCommand("CLS\r\n"); } function setPagePrint() { addCommand("PRINT 1,1\r\n"); } function setQR( x: number, y: number, level: string, width: number, mode: string, content: string, ) { addCommand(`QRCODE ${x},${y},${level},${width},${mode},0,"${content}"\r\n`); } function setBar(x: number, y: number, width: number, height: number) { addCommand(`BAR ${x},${y},${width},${height}\r\n`); } // 支持中文的 setText function setText( x: number, y: number, font: string | number, x_: number, y_: number, str: string, ) { // 字符宽度(点),可根据实际字体微调 const CHINESE_WIDTH = 24; // 中文字符宽度(TSS24.BF2 通常为24点) const ASCII_WIDTH = 12; // 半角字符宽度(数字、字母、空格、标点) // 1. 将字符串拆分为连续的 ASCII 段和非 ASCII 段 const segments: { text: string; isAscii: boolean }[] = []; let current = ""; let currentIsAscii: boolean | null = null; for (const ch of str) { const isAscii = ch.charCodeAt(0) 128; if (currentIsAscii === null) { currentIsAscii = isAscii; current = ch; } else if (currentIsAscii === isAscii) { current += ch; } else { segments.push({ text: current, isAscii: currentIsAscii }); current = ch; currentIsAscii = isAscii; } } if (current) segments.push({ text: current, isAscii: currentIsAscii }); // 2. 依次发送每个片段,累加 X 坐标 let currentX = x; for (const seg of segments) { // 转义双引号和反斜杠(TSPL 协议要求) let escaped = seg.text.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); if (!seg.isAscii) { // 非 ASCII 片段(中文、全角符号)使用 GBK 编码 const prefix = `TEXT ${currentX},${y},"${font}",0,${x_},${y_},"`; const prefixBytes = asciiToBytes(prefix); const gbkBytes = chineseToBytes(escaped); const suffixBytes = asciiToBytes('"\r\n'); const total = new Uint8Array( prefixBytes.length + gbkBytes.length + suffixBytes.length, ); total.set(prefixBytes, 0); total.set(gbkBytes, prefixBytes.length); total.set(suffixBytes, prefixBytes.length + gbkBytes.length); for (let i = 0; i total.length; i++) command.value.push(total[i]); currentX += seg.text.length * CHINESE_WIDTH; } else { // ASCII 片段(数字、字母、半角标点、空格)直接使用 ASCII 命令 addCommand( `TEXT ${currentX},${y},"${font}",0,${x_},${y_},"${escaped}"\r\n`, ); currentX += seg.text.length * ASCII_WIDTH; } } } // ========== 构建 TSPL 打印数据(佳博/通用)========== function buildGPAOrPBAData(): Uint8Array { console.log("buildGPAOrPBAData"); command.value = []; // 设置中文代码页(必须放在最前面) addCommand("CODEPAGE 936\r\n"); setSize(50, 50); setGap(5); setDirection(1); setDensity(0); setCls(); // 二维码 setQR(280, 260, "L", 3, "A", Globalindex.jdcercode); // isinternalqc: false, // 内部质量控制 // isselfexamed: false, // 自我检测合格 // isrequexamed: false, // 委托检测合格 if (Globalindex.isinternalqc) { setText(-5, 105, "TSS24.BF2", 1, 1, "√"); } if (Globalindex.isselfexamed) { setText(150, 105, "TSS24.BF2", 1, 1, "√"); } if (Globalindex.isrequexamed) { setText(270, 105, "TSS24.BF2", 1, 1, "√"); } setText(0, 145, "TSS24.BF2", 1, 1, Globalindex.jdcername); setText(0, 180, "TSS24.BF2", 1, 1, "产地:"); setText(0, 210, "TSS24.BF2", 1, 1, Globalindex.jdcerorigin); setText(0, 240, "TSS24.BF2", 1, 1, Globalindex.jdcertel); setText(0, 270, "TSS24.BF2", 1, 1, Globalindex.jdcerdate); setText(0, 300, "TSS24.BF2", 1, 1, "生产者盖章或签名:"); setText(0, 330, "TSS24.BF2", 1, 1, Globalindex.jdcerbasename); setPagePrint(); return new Uint8Array(command.value); } // ========== 构建 ESC/POS 打印数据(VB 打印机)========== // 辅助函数:字符串转十六进制(用于二维码) function stringtoHex(str: string): string { let val = ""; for (let i = 0; i str.length; i++) { if (val == "") val = str.charCodeAt(i).toString(16); else val += str.charCodeAt(i).toString(16); } return val; } // 字节数组转十六进制字符串 function bufToHex(buffer: Uint8Array): string { return Array.prototype.map .call(buffer, (x) = ("00" + x.toString(16)).slice(-2)) .join(""); } // 十六进制字符串转字节数组 function hexToBytes(hex: string): Uint8Array { hex = hex.replace(/\s/g, "").toLowerCase(); let bytes = []; for (let i = 0; i hex.length; i += 2) { let byte = parseInt(hex.substr(i, 2), 16); if (byte 127) byte = byte - 256; bytes.push(byte); } return new Uint8Array(bytes); } function buildVBData(): Uint8Array { let samplecode = stringtoHex(Globalindex.jdcercode); let qrcode = "1D 51 55 00"; // 居左55点距 qrcode += "1D57061D6B200101"; qrcode += samplecode; qrcode += "00"; qrcode += "0A"; qrcode += "0A"; let str_a = Globalindex.jdcername; let arrayBuffer_a = gbkToBytes(str_a); let hex_a = "1B40"; // 打印机初始化 hex_a = "1B 24 12 00"; // 居左12点距 hex_a += bufToHex(arrayBuffer_a); hex_a += "00"; hex_a += "0D"; let str_c = Globalindex.jdcerbasename; let arrayBuffer_c = gbkToBytes(str_c); let hex_c = "1B 24 06 00"; // 居左06点距 hex_c += bufToHex(arrayBuffer_c); hex_c += "00"; hex_c += "1B69"; let finalHex = hex_a + qrcode + hex_c; return hexToBytes(finalHex); } // ========== 根据打印机类型构建数据 ========== function buildPrintData(): Uint8Array { let reg = /Printer_/i; if (devicename.value == "V2B3_639544B") { return buildVBData(); } else if ( devicename.value == "GP-D320FX_A7ED" || reg.test(devicename.value) ) { return buildGPAOrPBAData(); } else { return buildGPAOrPBAData(); } } // ========== 蓝牙操作函数 ========== function initBluetooth() { wx.openBluetoothAdapter({ success: () = { console.log("蓝牙适配器初始化成功"); wx.onBluetoothAdapterStateChange((res) = { console.log("蓝牙状态变化", res); if (!res.available) { devicename.value = ""; connectedDeviceId = ""; uni.showToast({ title: "蓝牙已断开", icon: "none" }); } }); }, fail: (err) = { console.error("蓝牙适配器初始化失败", err); uni.showModal({ title: "提示", content: "请打开手机蓝牙并授权位置权限", showCancel: false, }); }, }); } function showPrinterList() { type.value = "center"; popupnew.value.open(); startScan(); } function startScan() { if (scanning.value) return; scanning.value = true; deviceList.value = []; wx.stopBluetoothDevicesDiscovery({ complete: () = { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: () = { console.log("开始扫描设备"); wx.onBluetoothDeviceFound((res) = { const devices = res.devices; devices.forEach((device) = { if ( device.name !deviceList.value.some((d) = d.deviceId === device.deviceId) ) { deviceList.value.push({ name: device.name, deviceId: device.deviceId, RSSI: device.RSSI, }); } }); }); setTimeout(() = { if (scanning.value) stopScan(); }, 10000); }, fail: (err) = { console.error("开始扫描失败", err); scanning.value = false; uni.showToast({ title: "扫描失败,请重试", icon: "none" }); }, }); }, }); } function stopScan() { wx.stopBluetoothDevicesDiscovery({ success: () = { scanning.value = false; console.log("停止扫描"); }, }); } async function connectDevice(item) { uni.showLoading({ title: "连接中...", mask: true }); try { stopScan(); if (connectedDeviceId) await closeBluetoothConnection(); await new Promise((resolve, reject) = { wx.createBLEConnection({ deviceId: item.deviceId, success: resolve, fail: reject, }); }); connectedDeviceId = item.deviceId; devicename.value = item.name; deviceitems.value = item; const servicesRes = await new Promise((resolve, reject) = { wx.getBLEDeviceServices({ deviceId: co