背景:
已完成的h5项目(移动端网页、基于vue框架+vant组件)现在要求加入蓝牙打印功能。
问题描述:
Native.js for Android封装一条通过JS语法直接调用Native Java接口通道,通过plus.android可调用几乎所有的系统。这里通过 Native.js获取监听连接等操作Android蓝牙设备。
需求描述:
1. 点击“切换打印机”按钮可以搜索周围蓝牙设备加入未配对列表,点击未配对蓝牙设备进行配对,点击已配对的蓝牙设置进行连接打印
2. 点击“打印”按钮,直接自动调用上次连接的打印机进行打印
解决方法:
1.新增一个搜索蓝牙,蓝牙配对连接并打印的页面
BluetoothMatch.vue
<template>
<div class="print-wrap" v-if="showPrint">
<div class="print-main">
<div class="btn-wrap">
<van-button
type="info"
@click="searchDevices('')"
:disabled="btnDisabled"
>{{ btnText }}</van-button
>
<van-button type="info" plain @click="close">
取消</van-button
>
</div>
<div style="margin: 15px 0">
已配对蓝牙:
<ul id="matchDevice">
<van-cell
v-for="(item, index) in matchDeviceList"
:key="index"
:title="item.name"
@click="handlePrint(item.id, index)"
>
<template #right-icon>
<van-loading type="spinner" v-if="item.isConnecting" />
<van-icon name="success" v-if="item.isConnected" />
<span
v-if="!item.isConnected && !item.isConnecting"
style="color: #ccc"
>未连接</span
>
</template>
</van-cell>
</ul>
</div>
<div>
未配对蓝牙:
<ul id="unmatchDevice">
<van-cell
v-for="(item, index) in unmatchDeviceList"
:key="index"
:title="item.name"
@click="searchDevices(item.id, index)"
>
<template #right-icon>
<van-loading type="spinner" v-if="item.isConnecting" />
</template>
</van-cell>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: "BluetoothMatch",
data() {
return {
device: null,
BAdapterL: null,
BluetoothAdapter: null,
uuid: null,
main: null,
bluetoothSocket: null,
BleDevice: null,
matchDeviceList: [],
unmatchDeviceList: [],
btnText: "搜索设备",
btnDisabled: false,
lastIndex: -1, //上一个链接的设备
matchAddress: [],
unmatchAddress: [],
isMatching: false, //配对设备
isConnecting: false, //连接设备打印
isArray: false,
};
},
props: {
printValue: {
type: Array,
defalut() {
return [];
},
},
address: {
type: String,
default: "",
},
showPrint: {
type: Boolean,
defalut: false,
},
},
watch: {
"matchDeviceList.length": {
handler(newVal) {
localStorage.setItem(
"matchDeviceList",
JSON.stringify(this.matchDeviceList)
);
},
},
"matchAddress.length": {
handler(newVal) {
localStorage.setItem("matchAddress", JSON.stringify(this.matchAddress));
},
},
},
mounted() {
this.openBluetoothAdapter();
console.log("print mounted---------------");
if (JSON.parse(localStorage.getItem("matchDeviceList"))) {
this.matchDeviceList = JSON.parse(
localStorage.getItem("matchDeviceList")
);
}
if (JSON.parse(localStorage.getItem("matchAddress"))) {
this.matchAddress = JSON.parse(localStorage.getItem("matchAddress"));
}
console.log(this.matchDeviceList);
},
methods: {
// 监听蓝牙设备连接状态
listenerConnection() {
plus.bluetooth.onBLEConnectionStateChange(function (e) {
console.log("connection state changed: " + JSON.stringify(e));
});
},
openBluetoothAdapter() {
console.log("初始化蓝牙模块--------");
var _this = this;
plus.bluetooth.openBluetoothAdapter({
success: function (e) {
console.log("open success: " + JSON.stringify(e));
},
fail: function (e) {
console.log("open failed: " + JSON.stringify(e));
},
complete(e) {
console.log(e);
if (!e.errCode) {
console.log("初始化完成");
} else if (e.errCode == 10001) {
uni.showToast({
icon: "none",
title:"请打开手机蓝牙",
});
} else {
uni.showToast({
icon: "none",
title: e.errMsg,
});
}
},
});
},
close() {
this.btnDisabled = false;
this.btnText = "搜索设备";
this.unmatchDeviceList = [];
this.unmatchAddress = [];
this.isConnecting = false;
this.isMatching = false;
if (this.bluetoothSocket) {
this.bluetoothSocket.close();
}
this.$emit("closePrint");
},
// Android权限查询
requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
function (resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log("已获取的权限:" + grantedPermission);
result = 1;
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log("拒绝本次申请的权限:" + deniedPresentPermission);
result = 0;
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log("永久拒绝申请的权限:" + deniedAlwaysPermission);
result = -1;
}
resolve(result);
// // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
// if (result != 1) {
// console.log("跳转到设置")
// // Toast('打印需要位置权限,正在跳转到设置界面...');
// this.gotoAppPermissionSetting()
// }
},
function (error) {
console.log("申请权限错误:" + error.code + " = " + error.message);
resolve({
code: error.code,
message: error.message,
});
}
);
});
},
gotoAppPermissionSetting() {
// console.log(plus.device.vendor);
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
},
handlePrint(mac_address, index) {
console.log("打印:", this.isConnecting);
if (this.isConnecting) {
return;
}
this.isConnecting = true;
console.log("准备连接设备:", mac_address);
if (!mac_address) {
this.$toast("请选择打印机");
return;
}
this.main = plus.android.runtimeMainActivity();
this.BluetoothAdapter = plus.android.importClass(
"android.bluetooth.BluetoothAdapter"
);
let UUID = plus.android.importClass("java.util.UUID");
this.uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
this.BAdapter = this.BluetoothAdapter.getDefaultAdapter();
this.device = this.BAdapter.getRemoteDevice(mac_address);
plus.android.importClass(this.device);
this.bluetoothSocket =
this.device.createInsecureRfcommSocketToServiceRecord(this.uuid);
plus.android.importClass(this.bluetoothSocket);
if (!this.bluetoothSocket.isConnected()) {
console.log("检测到设备未连接,尝试连接....");
this.matchDeviceList[index].isConnecting = true;
this.matchDeviceList[index].isConnected = false;
console.log(this.matchDeviceList[index]);
console.log("连接中...");
this.bluetoothSocket.connect();
}
if (this.lastIndex != -1) {
//关闭上个连接设备
this.matchDeviceList[this.lastIndex].isConnecting = false;
this.matchDeviceList[this.lastIndex].isConnected = false;
}
this.isConnecting = false;
this.matchDeviceList[index].isConnecting = false;
this.matchDeviceList[index].isConnected = true;
this.lastIndex = index;
console.log("设备已连接");
localStorage.setItem("lastConnectAddress", mac_address);
if (this.bluetoothSocket.isConnected()) {
console.log("test begin");
// TestDemo = plus.android.importClass("com.topband.TestDemo");
let outputStream = this.bluetoothSocket.getOutputStream();
console.log("outputStream:%o", outputStream);
plus.android.importClass(outputStream);
this.printValue.forEach((item) => {
if (this.device) {
console.log("this.BleDevice.getName():%o", this.device.getName());
}
let bytes = plus.android.invoke(item, "getBytes", "GB2312");
outputStream.write(bytes);
outputStream.flush();
});
this.device = null; //这里关键
this.bluetoothSocket.close(); //必须关闭蓝牙连接否则意外断开的话打印错误
}
},
/**
*
* @param {string} address 设备地址
* @param {string} index 设备索引
*/
async searchDevices(address, index) {
console.log("address:" + address);
if (!address) {
//点击搜索设备按钮 清空 重新搜索
this.matchAddress = [];
this.unmatchAddress = [];
this.unmatchDeviceList = []; //清空容器
this.matchDeviceList = []; //清空容器
this.isConnecting = false;
this.isMatching = false;
this.btnDisabled = true;
this.btnText = "正在搜索请稍候...";
} else if (this.isMatching) {
//正在配对
return;
} else {
//开始配对设备
this.isMatching = true;
this.unmatchDeviceList[index].isConnecting = true;
}
//注册类
let _this = this;
let main = plus.android.runtimeMainActivity();
//check location permission
let promiseV = await this.requestAndroidPermission(
"android.permission.ACCESS_COARSE_LOCATION"
);
console.log("promiseV:%o", promiseV);
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
if (promiseV != 1) {
console.log("跳转到设置");
// Toast('打印需要位置权限,正在跳转到设置界面...');
plus.nativeUI.toast("打印需要位置权限,正在跳转到设置界面");
this.gotoAppPermissionSetting();
_this.btnDisabled = false;
_this.btnText = "搜索设备"
return;
}
let IntentFilter = plus.android.importClass(
"android.content.IntentFilter"
);
let BluetoothAdapter = plus.android.importClass(
"android.bluetooth.BluetoothAdapter"
);
let BluetoothDevice = plus.android.importClass(
"android.bluetooth.BluetoothDevice"
);
let BAdapter = BluetoothAdapter.getDefaultAdapter();
let filter = new IntentFilter();
let bdevice = new BluetoothDevice();
let on = null;
let un = null;
BAdapter.startDiscovery(); //开启搜索
let receiver;
receiver = plus.android.implements(
"io.dcloud.android.content.BroadcastReceiver",
{
onReceive: function (context, intent) {
//实现onReceiver回调函数
plus.android.importClass(intent); //通过intent实例引入intent类,方便以后的‘.’操作
console.log(intent.getAction()); //获取action
if (
intent.getAction() ==
"android.bluetooth.adapter.action.DISCOVERY_FINISHED"
) {
main.unregisterReceiver(receiver); //取消监听
_this.btnDisabled = false;
_this.btnText ="搜索设备"
console.log("搜索结束");
} else {
_this.BleDevice = intent.getParcelableExtra(
BluetoothDevice.EXTRA_DEVICE
);
//判断是否配对
console.log("bledevice:" + _this.BleDevice);
if (
_this.BleDevice &&
_this.BleDevice.getBondState() == bdevice.BOND_NONE
) {
console.log(
"蓝牙设备:" +
_this.BleDevice.getName() +
" " +
_this.BleDevice.getAddress()
);
//参数如果跟取得的mac地址一样就配对
if (address == _this.BleDevice.getAddress()) {
console.log("开始配对-------------");
if (_this.BleDevice.createBond()) {
//配对命令.createBond()
console.log("配对成功:", _this.BleDevice.createBond());
let obj = {
id: _this.BleDevice.getAddress(),
name: _this.BleDevice.getName(),
isConnecting: false,
isConnected: false,
};
_this.isMatching = false;
_this.unmatchDeviceList.splice(index, 1);
if (_this.matchAddress.indexOf(obj.id) == -1) {
_this.matchAddress.push(obj.id);
_this.matchDeviceList.push(obj);
}
// this.listenerConnection()
} else {
console.log("配对失败:", _this.BleDevice.createBond());
_this.isMatching = false;
_this.unmatchDeviceList[index].isConnecting = false;
}
} else {
console.log(
"未配对蓝牙设备:" +
_this.BleDevice.getName() +
" " +
_this.BleDevice.getAddress()
);
if (_this.BleDevice.getName() != on) {
//判断防止重复添加
let obj = {
id: _this.BleDevice.getAddress(),
name: _this.BleDevice.getName(),
isConnecting: false,
isConnected: false,
};
if (_this.unmatchAddress.indexOf(obj.id) == -1) {
_this.unmatchAddress.push(obj.id);
_this.unmatchDeviceList.push(obj);
}
}
}
} else {
if (_this.BleDevice.getName() != un) {
//判断防止重复添加
console.log(
"已配对蓝牙设备:" +
_this.BleDevice.getName() +
" " +
_this.BleDevice.getAddress()
);
let obj = {
id: _this.BleDevice.getAddress(),
name: _this.BleDevice.getName(),
isConnecting: false,
isConnected: false,
};
if (_this.matchAddress.indexOf(obj.id) == -1) {
_this.matchAddress.push(obj.id);
_this.matchDeviceList.push(obj);
}
}
}
}
},
}
);
filter.addAction(bdevice.ACTION_FOUND);
filter.addAction(BAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BAdapter.ACTION_DISCOVERY_FINISHED);
filter.addAction(BAdapter.ACTION_STATE_CHANGED);
main.registerReceiver(receiver, filter); //注册监听
},
},
};
</script>
<style scoped>
.print-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
}
.print-main {
width: 80%;
padding: 20px 10px;
background-color: #fff;
border-radius: 10px;
}
.btn-wrap {
display: flex;
justify-content: space-between;
}
li {
margin: 8px 0;
}
button {
height: 32px;
}
#unmatchDevice,
#matchDevice {
max-height: 200px;
overflow: auto;
}
#matchDevice li.isConnected {
color: #1989fa;
}
.btn-info {
padding: 0 15px;
border-radius: 2px;
font-size: 14px;
color: #fff;
background-color: #1989fa;
border: 1px solid #1989fa;
}
.btn-info.plain {
color: #1989fa;
background-color: #fff;
}
.btn-info:disabled {
background-color: #005ea7;
}
#unmatchDevice .van-cell,
#matchDevice .van-cell {
padding: 8px 16px 8px 8px;
}
#matchDevice .van-loading__spinner,
#unmatchDevice .van-loading__spinner {
width: auto;
height: 100%;
}
/deep/ .van-icon-success {
color: #2753f5;
}
</style>
<style>
#unmatchDevice li,
#matchDevice li {
margin: 8px 0;
}
</style>
2.添加调用蓝牙打印的js函数
bluetoothPrint.js
export function bluetoothPrint(mac_address, printValue) {
console.log("进入print-----")
try {
let BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
let UUID = plus.android.importClass("java.util.UUID");
let uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
let BAdapter = BluetoothAdapter.getDefaultAdapter();
let device = BAdapter.getRemoteDevice(mac_address);
plus.android.importClass(device);
let bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid);
plus.android.importClass(bluetoothSocket);
if (!bluetoothSocket.isConnected()) {
console.log('检测到设备未连接,尝试连接....');
console.log("连接中...")
bluetoothSocket.connect();
}
console.log('设备已连接');
if (bluetoothSocket.isConnected()) {
console.log("test begin")
let outputStream = bluetoothSocket.getOutputStream();
console.log("outputStream:%o", outputStream);
plus.android.importClass(outputStream);
console.log("qrCodeArr:%o", printValue)
printValue.forEach(item => {
if (device) {
console.log("this.BleDevice.getName():%o", device.getName())
}
console.log("version log -------2021-999999")
console.log("print qr-----")
let bytes = plus.android.invoke(item, 'getBytes', 'GB2312');
outputStream.write(bytes);
outputStream.flush();
})
device = null //这里关键
bluetoothSocket.close(); //必须关闭蓝牙连接否则意外断开的话打印错误
localStorage.setItem("lastConnectAddress", mac_address)
}
} catch (error) {}
}
3.引入蓝牙配对连接页面和蓝牙打印功能函数。
蓝牙打印机为佳博打印机,打印指令可参考佳博标签打印机的编程手册
<template>
<div class="container">
<van-button
type="info"
size="small"
native-type="button"
@click="printLabel(1)"
><i class="fa fa-bluetooth" style="margin-right: 5px"></i
>蓝牙打印</van-button
>
<van-button
type="info"
size="small"
icon="exchange"
native-type="button"
@click="printLabel(2)"
>切换打印机</van-button
>
<print
:printValue="printValue"
:address="address"
:showPrint="showPrint"
@closePrint="closePrint"
></print>
<van-overlay :show="loading">
<van-loading color="#1989fa" v-if="loading" />
</van-overlay>
</div>
</template>
<script>
import Vue from "vue";
import Print from "@/components/BluetoothMatch";
import { bluetoothPrint } from "../../utils/bluetoothPrint";
import { Overlay } from "vant";
Vue.use(Overlay);
export default {
components: {
Print,
},
data() {
return {
show: false,
loading: false,
showPrint: false,
printValue: [],
address: ""
};
},
mounted() {},
methods: {
printLabel(type) {
let params = {
prodCode:"12345678",
workOrder:"11111111",
prodDate:"20231010",
number:100,
batch:"1"
}
let boxCode = ["12345678909876541","12345678909876542"]
this.printByBluetooth(type, params, boxCode);
},
closePrint() {
this.showPrint = false;
},
/**
* @param {String|Number} type 打印类型,1为直接用上次连接的打印机打印 2为切换打印机并打印
* @param {Object} params 打印的数据
* @param {Array} boxCode 打印的箱码
*/
printByBluetooth(type, params, boxCode) {
boxCode = boxCode.filter((item) => item);
if (!boxCode.length) {
this.$toast("无待打印物料");
return;
}
this.printValue = [];
boxCode.forEach((item, index) => {
batch = params.batch ? params.batch : "N/A";
let command = "SIZE 50 mm, 45 mm\r\n"; // 设置标签纸尺寸为 50mm x 40mm
command += "CLS\r\n"; // 清除打印缓冲区
command += "BAR 16,16,368,4\r\n"; // 绘制直线
command += "BAR 16,144,368,4\r\n"; // 绘制直线
command += "BAR 16,200,368,4\r\n"; // 绘制直线
command += "BAR 16,248,368,4\r\n"; // 绘制直线
command += "BAR 16,296,368,4\r\n"; // 绘制直线
command += "BAR 16,344,368,4\r\n"; // 绘制直线
command += "BAR 184,296,2,56\r\n"; // 绘制直线
command += `BARCODE 40,56,\"39\",64,1,0,1,2,\"${item}\"\r\n`;
command += 'TEXT 120,24,"TSS24.BF2",0,1,1,"物料标签"\r\n';
command += `TEXT 24,160,\"TSS24.BF2\",0,1,1,\"产品编码:${params.prodCode}\"\r\n`;
command += `TEXT 24,210,\"TSS24.BF2\",0,1,1,\"工单:${params.workOrder}\"\r\n`;
command += `TEXT 24,260,\"TSS24.BF2\",0,1,1,\"入仓日期:${params.prodDate}\"\r\n`;
command += `TEXT 24,306,\"TSS24.BF2\",0,1,1,\"数量:${params.number}\"\r\n`;
command += `TEXT 200,306,\"TSS24.BF2\",0,1,1,\"批次:${batch}\"\r\n`;
command += "PRINT 1, 1\r\n"; // 执行打印
this.printValue.push(command);
});
console.log(this.printValue);
if (type == 1) {
//蓝牙打印
let lastConnectAddress = "";
if (localStorage.getItem("lastConnectAddress")) {
//默认取上次连接的打印机
lastConnectAddress = localStorage.getItem("lastConnectAddress");
}
console.log(this.lastConnectAddress);
if (lastConnectAddress) {
//直接连接第一个
bluetoothPrint(lastConnectAddress, this.printValue);
} else {
this.$toast("请选择打印机");
}
} else {
//切换打印机
this.showPrint = true;
}
},
},
};
</script>
<style scoped></style>
4.效果展示
5. 添加完蓝牙打印功能后,如何在本地调试以及如何将Vue项目打包为apk,且项目可以不改变现有的模式部署在远程服务器上等,在后续的文章会进行分享
本文参考:安卓Native.js蓝牙连接票据打印机完整代码http://ask.dcloud.net.cn/article/643