Files
APP/pages/common/addBLE/addEquip.vue
2026-04-17 09:45:02 +08:00

1184 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="content">
<view class="topAnimate">
<view class="animate center">
<view class="animateContent">
<view class="circle"></view>
<view class="circle"></view>
<view class="circle"></view>
</view>
<view class="imgContent center">
<view class="img center">
<image src="/static/images/BLEAdd/bluetooth.png" class="titleIco" mode="aspectFit">
</image>
</view>
</view>
</view>
</view>
<view class="mainContent">
<view class="p100">
<view class="lblTitle">配对设备</view>
<view class="list" style="margin-bottom: 30rpx;">
<view class="item " @click.stop="disConnect(item,index)" v-for="item, index in PairEquip" v-show="PairEquip.length>0">
<view class="leftImg ">
<image src="/static/images/common/bluetooth.png" class="titleIco filterNone"
mode="heightFix">
</image>
</view>
<view class="centertxt ">
<view class="name" v-text="item.name"></view>
</view>
<view class="rightIco center">
<image src="/static/images/BLEAdd/linked.png" class="img" mode="aspectFit">
</image>
</view>
</view>
<view v-show="PairEquip.length==0" class="item center">
<view class="noLink">无已配对设备</view>
</view>
</view>
<view class="lblTitle">
<text>发现设备:{{deviceCnt}}</text>
<view @click="refreshBleList()">刷新</view>
</view>
<view class="lblTitle">
<input class="uni-input" v-model="search" placeholder="名称筛选" />
<!-- <uni-easyinput :styles="{color:'#ffffffde',borderColor:'#cbcbcba8'}" :clearable="true" class="uni-mt-5" :trim="'both'"
prefixIcon="search" v-model="search" placeholder="名称筛选"></uni-easyinput> -->
</view>
<view class="list searchList">
<view class="item" v-on:click="Link(item,index)" v-for="item, index in EquipMents"
v-show="item.name.indexOf(search)>-1">
<view class="before" v-if="item.isTarget"></view>
<view class="leftImg ">
<image src="/static/images/BLEAdd/bluetooth.png" class="titleIco" mode="heightFix">
</image>
</view>
<view class="centertxt ">
<view class="name">
<text>{{item.name?item.name:'Unnamed'}}</text>
</view>
<view class="id">
<text>信号:{{item.RSSI}}dBm</text>
</view>
</view>
<view class="rightIco center">
<image :src="isItemLink(item,index)" class="img" mode="aspectFit">
</image>
</view>
</view>
</view>
</view>
</view>
<BottomSlideMenuPlus :config="Status.BottomMenu">
<view class="openBlue">
<view class="txt">
当前手机蓝牙关闭,是否打开以扫描附近的蓝牙设备?
</view>
<view class="btns">
<view class="btn cancel" @click="Status.BottomMenu.show=false">
<view>取消</view>
</view>
<view class="ok btn" @click="gotoSetting">
<view>开启</view>
</view>
</view>
</view>
</BottomSlideMenuPlus>
<MsgBox ref="msgPop" />
<global-loading ref="loading" />
</view>
</template>
<script>
import bleTool from '@/utils/BleHelper.js';
import request from '@/utils/request.js';
import {
showLoading,
hideLoading,
updateLoading
} from '@/utils/loading.js';
import {
MsgSuccess,
MsgError,
MsgClose,
MsgWarning,
showPop,
MsgClear
} from '@/utils/MsgPops.js';
const pagePath = "pages/common/addBLE/addEquip";
var ble = null;
var these = null;
var eventChannel = null;
export default {
data() {
return {
Status: {
navigateTO: false,
isPageHidden: false,
intval: null,
time: null,
BottomMenu: {
show: false,
showHeader: false,
menuItems: [],
activeIndex: -1,
bgColor: '#2a2a2a',
itemBgColor: '#3a3a3a',
textColor: '#ffffffde',
textAlign: 'flex-start',
title: '主灯模式',
showDivider: false,
dividerColor: '#00000000',
dividerThickness: '0rpx',
dividerMargin: '10rpx',
itemHeight: '80rpx',
type: '',
showBtn: false,
btnBgColor: "#bbe600",
btnText: "确定",
btnTextColor: "#232323de",
showMask: true,
maskBgColor: '#00000066',
showClose: false
}
},
search: '', //筛选
PairEquip: [], //已配对设备
tmpLink:[],//本次已配对
EquipMents: [], //搜索出来的设备
device: null,
item: {
deviceId: ''
},
}
},
computed: {
deviceCnt: function() {
let arr = this.EquipMents.filter(item => {
return item.name.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
return arr.length;
}
},
onHide: function() {
this.Status.isPageHidden = true;
if (ble) ble.StopSearch();
},
onUnload() {
if (ble) {
ble.StopSearch();
ble.removeAllCallback(pagePath);
if (!this.device && !this.Status.navigateTO) {
if (this.tmpLink && this.tmpLink.length && this.tmpLink.length > 0) {
console.error("页面卸载时,断开所有连接")
let f = this.tmpLink.forEach((v) => {
ble.disconnectDevice(v.deviceId).catch(ex => {
console.error("无法断开设备连接", ex);
});
});
}
}
}
},
onLoad(option) {
eventChannel = this.getOpenerEventChannel();
eventChannel.on('detailData', function(rec) {
console.log("接收到父页面的参数:", rec);
these.device = rec.data;
if (rec.data.bluetoothName) {
these.search = rec.data.bluetoothName;
} else if (rec.data.deviceName) {
these.search = rec.data.deviceName;
}
startValidDevice();
});
let search = option.search;
these = this;
const systemInfo = uni.getSystemInfoSync();
// Ensure ble is initialized
if (systemInfo.uniPlatform == 'web') {
this.EquipMents = [{
"RSSI": -55,
"advertisData": "",
"advertisServiceUUIDs": [
"0000FFE0-0000-1000-8000-00805F9B34FB"
],
"deviceId": "EBDA4E6F-3A28-FF65-A845-AE8CC7B78375",
"name": "HBY670-BF74EA",
"linkStatu": false
},
{
"RSSI": -61,
"advertisData": "",
"advertisServiceUUIDs": [
"0000FFE0-0000-1000-8000-00805F9B34FB"
],
"deviceId": "469FB381-B47E-1E40-8073-EF50B5704AAB",
"name": "EF4651",
"linkStatu": false,
"isTarget": true
}
];
console.error("1111111111")
this.PairEquip=[this.EquipMents[0]];
return;
}
ble = bleTool.getBleTool();
this.refreshLinked();
let StartSubsrib = () => {
these.EquipMents = [];
if (!ble) {
ble = bleTool.getBleTool();
}
//蓝牙不可用的回调
ble.addStateBreakCallback(res => {
if (these.Status.isPageHidden) {
return;
}
console.log("处理蓝牙不可用");
hideLoading(these);
console.error("1111111111")
these.PairEquip = [];
these.EquipMents = [];
uni.showToast({
icon: 'fail',
title: '蓝牙已不可用'
});
these.showOpenSetting();
}, pagePath);
//蓝牙恢复可用的回调
ble.addStateRecoveryCallback(res => {
if (these.Status.isPageHidden) {
return;
}
these.Status.BottomMenu.show = false;
console.error("1111111111")
these.PairEquip = [];
these.EquipMents = [];
uni.showToast({
icon: 'fail',
title: '蓝牙恢复可用'
});
these.refreshBleList();
}), pagePath;
//蓝牙断开连接的回调
ble.addDisposeCallback(res => {
if (these.Status.isPageHidden) {
return;
}
// console.log("处理蓝牙断开连接");
these.refreshLinked();
setTimeout(() => {
hideLoading(these);
}, 1500);
}, pagePath);
//搜索到新设备的回调 (Always active)
ble.addDeviceFound((arr) => {
// console.log("--- 收到原始扫描数据 ---", JSON.stringify(arr));
if (these.Status.isPageHidden) {
return;
}
if (!arr || !arr.devices) {
return;
}
arr = arr.devices;
// console.log(`本次扫描批次发现 ${arr.length} 个设备`);
for (var i = 0; i < arr.length; i++) {
let device = arr[i];
device.linkStatu = false;
let f = these.EquipMents.find((v, index) => {
if (v.deviceId == device.deviceId) {
// console.log(
// `更新设备信号: ${device.name || device.deviceId}, RSSI: ${device.RSSI}`
// );
these.$set(these.EquipMents[index], 'RSSI', device.RSSI);
return true;
}
return false;
});
if (!f) {
// console.log("+++ 发现新设备,准备添加到列表:", JSON.stringify(device));
if (these.device && these.device.bluetoothName && device.name) {
const bn = these.device.bluetoothName;
if (these.device.bluetoothName === device.name ||
(device.name.indexOf(bn) > -1) ||
(bn.indexOf(device.name) > -1)) {
device.isTarget = true;
}
}
// 102J与 100J 同 AE30 芯片;协议不同。广播名一般为 HBY102J-xxxxxx见协议
if (device.name && /^HBY102J/i.test(device.name)) {
device.isTarget = true;
}
if (device.name) {
device.name = device.name.replace(/^JQZM-/i, '').replace(/^HBY102J-/i, '');
}
these.EquipMents.push(device);
}
these.EquipMents.sort((a, b) => b.RSSI - a.RSSI); //信号好的排前面,一般信号好的是目标设备
}
}, pagePath);
//蓝牙连接已恢复的回调
ble.addRecoveryCallback(res => {
if (these.Status.isPageHidden) {
return;
}
these.refreshLinked();
// hideLoading(these);
if (!these.device) {
hideLoading(these);
}else{
clearInterval(this.Status.intval);
these.DeviceVerdict(res.deviceId);
}
}, pagePath);
}
let startValidDevice = () => {
if (these.device) {
console.log("进入配对模式,启用连接恢复和验证逻辑。");
//收到设备的消息回调
ble.addReceiveCallback((receivData, f, path, arr) => {
console.log("000000", receivData);
if (these.Status.isPageHidden) {
return;
}
if ((receivData.str && (receivData.str.indexOf('mac address:') > -1 || receivData.str.indexOf(
'sta_address') > -1)) ||
(receivData.bytes && receivData.bytes[0] === 0xFC && receivData.bytes.length >= 7)) {
if (f.macAddress && these.device) {
clearInterval(this.Status.intval);
this.Status.intval = null;
this.Status.time = null;
showLoading(these, {
text: '正在验证设备'
});
setTimeout(() => {
these.DeviceVerdict(f.deviceId);
}, 0);
}
}
}, pagePath);
}
}
StartSubsrib();
},
onShow: function() {
debugger;
this.Status.isPageHidden = false;
this.Status.navigateTO = false;
this.refreshBleList();
this.refreshLinked();
},
methods: {
refreshLinked(){
if(ble){
let arr=[];
arr=ble.data.LinkedList.filter(v=>{
return v.Linked;
});
this.PairEquip=arr;
}
},
checkAndRequestLocationPermission() {
return new Promise((resolve) => {
if (uni.getSystemInfoSync().platform !== 'android') {
return resolve(true);
}
plus.android.requestPermissions(
['android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN',
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION'
],
(result) => {
if (result.granted && result.granted.length > 0) {
console.log('定位权限已授予');
resolve(true);
} else {
// console.warn('定位权限被拒绝');
MsgClear(these);
showPop({
headerTxt: '权限提醒',
message: '扫描蓝牙设备,需要您开启定位权限',
buttonText: '去开启',
okCallback: uni.openSetting
}, these, true);
resolve(false);
}
},
(error) => {
MsgError('请求定位权限失败:' + error.code, '确定', these);
resolve(false);
}
);
resolve(true);
});
},
async refreshBleList() {
const systemInfo = uni.getSystemInfoSync();
if (systemInfo.uniPlatform == 'web') {
return;
}
const hasPermission = await this.checkAndRequestLocationPermission();
if (!hasPermission) {
console.log("缺少定位权限,已中止蓝牙扫描。");
return;
}
if (!ble) {
ble = bleTool.getBleTool();
if (!ble) {
console.error("BLE helper not initialized!");
return;
}
}
showLoading(these, {
text: '正在扫描蓝牙设备'
})
let time = null;
let startSearch = () => {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
these.EquipMents = [];
ble.StartSearch().then(result => {
// console.log("开始搜索成功", result);
these.Status.BottomMenu.show=false;
}).catch(err => {
console.error("开始搜索失败:", err);
if (err.code === 10001) {
these.showOpenSetting();
} else {
MsgClear(these);
MsgError('出现错误:' + err.msg, '确定', these);
}
}).finally(() => {
hideLoading(these);
});
}, 200);
}
ble.StopSearch().catch(err => {
console.error("err=", err);
}).finally(startSearch);
},
isItemLink: function(item, index) {
let src = '/static/images/BLEAdd/noLink.png';
if (this.PairEquip && this.PairEquip.length) {
if (this.PairEquip.length > 0) {
let f = this.PairEquip.find(function(v) {
return v.deviceId == item.deviceId;
});
if (f) {
src = '/static/images/BLEAdd/linked.png';
}
}
}
return src;
},
showOpenSetting: function() {
this.Status.BottomMenu.show = true;
},
gotoSetting: function() {
this.Status.BottomMenu.show = false;
ble.showBlueSetting(false);
},
DeviceVerdict(deviceId) { //判断是否是目标设备
if (these.Status.isPageHidden) {
return;
}
console.log("deviceid=", deviceId);
console.log("these.device=", these.device)
if (these.device) { //从设备详情过来的,回设备详情去
let f = ble.data.LinkedList.find(v => {
if (v.deviceId == deviceId) {
v.device = these.device;
return true;
}
return false;
});
let removeLink = () => {
ble.subScribe(deviceId, false); //取消订阅消息
ble.DropDevice(deviceId, null); //从缓存中删除该设备并断开连接
these.device.deviceMac = "";
let index = these.PairEquip.findIndex(function(v) {
return v.deviceId == deviceId;
});
if (index > -1) {
this.PairEquip.splice(index, 1);
}
}
//不是目标设备的处理方法
let deviceInvalid = () => {
console.error("连接的设备不是目标设备");
removeLink();
updateLoading(these, {
text: "设备Mac地址错误,请重选设备连接"
});
setTimeout(() => {
hideLoading(these);
}, 1500)
return;
}
//找到目标设备的处理方法
let deviceOK = () => {
if (these.Status.isPageHidden) {
return;
}
hideLoading(these);
eventChannel.emit('BindOver', these.device);
ble.updateCache();
uni.navigateBack();
}
if (f && f.macAddress) {
if (!these.device || !these.device.deviceMac) { //走服务端验证
console.error("走服务端验证")
request({
url: '/app/device/getDeviceInfoByDeviceMac',
method: 'GET',
data: {
deviceMac: f.macAddress
}
}).then(res => {
if (res && res.code == 200) {
let data = res.data;
//服务端验证要验证id而不是mac地址了
if (data.id != these.device.id) {
deviceInvalid();
return;
} else {
deviceOK();
}
} else {
this.serverDevice = null;
deviceInvalid();
}
}).catch((ex) => {
deviceInvalid();
});
return;
} else if (f.macAddress != these.device.deviceMac) { //直接验证上层传过来的数据
console.log("客户端验证失败");
deviceInvalid();
return;
} else if (f.macAddress == these.device.deviceMac) {
console.log("客户端验证成功");
deviceOK();
}
return true;
} else {
this.Status.time = 30;
showLoading(these, {
text: "等待设备上报Mac地址," + these.Status.time + 's'
});
clearInterval(this.Status.intval);
this.Status.intval = null;
this.Status.intval = setInterval(() => {
this.Status.time = this.Status.time - 1;
if (this.Status.time < 0) {
hideLoading(these);
console.log("停止倒计时", this.Status.time);
clearInterval(this.Status.intval)
this.Status.intval = null;
this.Status.time = null;
f = ble.data.LinkedList.find(v => {
if (v.deviceId == deviceId) {
v.device = these.device;
return true;
}
return false;
});
if (!(f && f.macAddress)) {
deviceInvalid()
return;
}
return;
}
updateLoading(these, {
text: "等待设备上报Mac地址," + these.Status.time + 's'
});
}, 1000);
return undefined;
}
}
return false;
},
Link: function(item) {
this.item.deviceId = item.deviceId;
showLoading(this, {
text: "正在连接:第1次"
});
let index = 1;
let total = 5;
let linkCallback = (res) => {
console.log("连接成功", these.device);
if (!these.device) {
console.log("跳转到绑定")
hideLoading(these);
uni.navigateTo({
url: "/pages/common/addBLE/LinkBle",
events: {
},
success(res) {
these.Status.navigateTO = true;
res.eventChannel.emit('LinkItem', item);
}
});
return;
}
}
let execLink = () => {
return new Promise((resolve, reject) => {
if (index > total) {
reject({
msg: "连接超时"
});
return;
}
ble.LinkBlue(item.deviceId).then((res) => {
this.tmpLink=[item];
console.log("连接成功");
ble.StopSearch();
resolve(res);
}).catch((ex) => {
if (index == total) {
console.log("连接了N次都没连上");
reject(ex);
updateLoading(this, {
text: ex.msg
})
return;
}
index++;
updateLoading(this, {
text: ex.msg + ",正在重试第" + index + "次"
})
execLink().then(resolve).catch(reject);
})
});
}
execLink().then((res) => {
console.log("res=", res);
if(this.Status.isPageHidden){
return;
}
linkCallback(res);
}).catch(ex => {
console.error("ex=", ex)
MsgClear(these);
MsgError("连接失败:" + ex.msg, '确定', these);
hideLoading(these);
});
},
disConnect:function(item,index){
// #ifdef H5|WEB
this.PairEquip.splice(index,1);
// #endif
// #ifdef APP|APP-PLUS
if(ble){
ble.disconnectDevice(item.deviceId).catch(ex=>{
console.error("无法断开连接",ex);
});
}
// #endif
}
}
}
</script>
<style>
.noLink {
text-align: center;
width: 100%;
font-size: 28rpx;
}
.content {
background-color: #121212;
color: #ffffffde;
box-sizing: border-box;
overflow: hidden;
width: 100%;
min-height: 100vh;
height: auto;
}
.p100 {
width: 100%;
height: 100%;
}
.fleft {
float: left;
}
.fright {
float: right;
}
.clear {
clear: both;
}
.center {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: center;
align-items: center;
}
.topAnimate {
position: absolute;
left: 0rpx;
width: 100%;
height: 400rpx;
top: 20px;
/* 距离视口顶部 20px 时固定 */
z-index: 100;
/* 确保元素显示在最上层 */
}
.animate {
width: 100%;
height: 400rpx;
position: relative;
}
.animate .animateContent {
position: relative;
z-index: 0;
}
.animate .imgContent {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
}
.animate .imgContent .img {
height: 100rpx;
width: 100rpx;
background: #bbe600;
border-radius: 50%;
}
.animate .titleIco {
width: 36rpx;
height: 36rpx;
}
.circle {
position: absolute;
border-radius: 50%;
background-color: #bbe60033;
display: inline-block;
transform: translate(-50%, -50%);
border: 1rpx solid #bbe6003d;
animation: expand 4s infinite ease-in-out;
}
.circle:nth-child(2) {
animation-delay: 1s;
}
.circle:nth-child(3) {
animation-delay: 2s;
}
@keyframes expand {
0% {
width: 0;
height: 0;
opacity: 0.8;
}
90% {
width: 18rem;
height: 18rem;
opacity: 0;
}
100% {
width: 0rem;
height: 0rem;
opacity: 0;
}
}
.mainContent {
padding: 30rpx;
box-sizing: border-box;
width: 100%;
height: calc(100% - 240rpx);
overflow-y: scroll;
position: absolute;
top: 240rpx;
left: 0rpx;
z-index: 101;
}
.mainContent .p100 {}
.mainContent .lblTitle {
color: #ffffffde;
font-family: "PingFang SC";
font-size: 28rpx;
font-weight: 700;
text-align: left;
width: 100%;
height: 28rpx;
line-height: 28rpx;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.list {
min-height: 120rpx;
}
.searchList {
width: 100%;
height: calc(100% - 300rpx);
overflow-y: scroll;
box-sizing: border-box;
/* border: 1px solid red; */
}
.list .item {
width: 100%;
min-height: 120rpx;
height: auto;
box-sizing: border-box;
padding: 20rpx;
border-radius: 8px;
background: #1a1a1a9e;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
margin-top: 20rpx;
position: relative;
overflow: hidden;
}
.list .item .leftImg {
width: 60rpx;
height: 60rpx;
}
.list .item .centertxt {
width: calc(100% - 100rpx);
height: 100%;
padding-left: 10rpx;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: flex-start;
align-content: flex-start;
}
.list .item .before {
position: absolute;
bottom: 0px;
right: 0px;
content: "";
width: 0;
height: 0;
border-right: 30rpx solid #bbe600d4;
border-top: 30rpx solid transparent;
animation-delay: 1s;
animation: fade 1.5s infinite ease-in-out;
}
@keyframes fade {
0% {
opacity: 0.3;
}
50% {
opacity: 1;
}
100% {
opacity: 0.3;
}
}
.list .item .rightIco {
width: 40rpx;
height: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: space-between;
align-items: center;
}
.list .item .leftImg .titleIco {
width: 100%;
height: 100%;
filter: invert(100%);
}
.list .item .name {
color: #ffffffde;
font-family: "PingFang SC";
font-size: 26rpx;
font-weight: 400;
line-height: 36rpx;
text-align: left;
}
.list .item .id {
color: #ffffff99;
font-family: "PingFang SC";
font-size: 26rpx;
font-weight: 400;
line-height: 36rpx;
text-align: left;
}
.list .item .rightIco .img {
width: 40rpx;
height: 40rpx;
}
.openBlue {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
height: auto;
}
.openBlue .txt {
color: rgba(255, 255, 255, 0.87);
font-family: "PingFang SC";
font-size: 28rpx;
font-weight: 400;
letter-spacing: 0.14rpx;
text-align: center;
}
.openBlue .btns {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
align-items: center;
justify-content: space-evenly;
margin-top: 50rpx;
width: 100%;
height: auto;
}
.openBlue .btn {
border-radius: 91rpx;
box-sizing: border-box;
width: 25%;
height: 60rpx;
text-align: center;
font-family: "PingFang SC";
font-size: 28rpx;
letter-spacing: 12rpx;
display: flex !important;
align-items: center;
letter-spacing: 0.375rem;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: center;
}
.openBlue .cancel {
border: 1px solid rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 1);
}
.openBlue .ok {
background-color: #BBE600;
color: #232323;
}
.filterNone {
filter: none !important;
-webkit-filter: none !important;
}
.uni-input {
background-color: #121212;
width: 100%;
height: 60rpx;
color: #ffffffde;
border: 1rpx solid #cbcbcbde;
border-radius: 8rpx;
font-size: 26rpx;
text-indent: 8rpx;
font-family: "PingFang SC";
line-height: 60rpx;
caret-color: #BBE600;
font-weight: 200;
}
</style>