2024-10-17 18:00:05 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view>
|
2025-09-22 15:24:01 +08:00
|
|
|
|
<u-navbar :is-back="true" title=' ' title-color="#000" :border-bottom="false" :background="bgc"
|
|
|
|
|
|
id="navbar">
|
2024-10-17 18:00:05 +08:00
|
|
|
|
</u-navbar>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
<view class="page">
|
|
|
|
|
|
<!-- 有搜索到设备 -->
|
|
|
|
|
|
<view class="you" v-if="flags">
|
|
|
|
|
|
<view class="topone">
|
|
|
|
|
|
<image src="https://api.ccttiot.com/smartmeter/img/static/ubrPcpGQEXTadkBa1gKh" mode=""></image>
|
2026-06-18 11:31:43 +08:00
|
|
|
|
扫描到以下设备,点击添加!
|
2025-04-18 13:42:53 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="toptwo">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
如未找到想添加的设备,点击重新搜索
|
2025-04-18 13:42:53 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="list">
|
2025-09-22 15:24:01 +08:00
|
|
|
|
<view class="list_item" v-for="(item,index) in sortedJiaohuaqi" :key="index" :class="{ show: item.show }">
|
2025-04-18 13:42:53 +08:00
|
|
|
|
<image :src="item.modelPicture" mode=""></image>
|
|
|
|
|
|
<view class="cen">
|
|
|
|
|
|
<view class="name" style="color: #ccc;" v-if="item.userId && item.userId != userid">
|
|
|
|
|
|
{{item.modelName == undefined ? '' : item.modelName}}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="name" v-else>
|
|
|
|
|
|
{{item.modelName == undefined ? '' : item.modelName}}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="devmac">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
MAC:{{item.mac == undefined ? item.name.slice(-12) : item.mac}}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
</view>
|
2025-10-25 15:53:10 +08:00
|
|
|
|
<view class="devmac" style="display: flex;align-items: center;">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
信号:
|
2025-10-25 15:53:10 +08:00
|
|
|
|
<!-- 最强信号:-50 及以上(-1 到 -50) -->
|
|
|
|
|
|
<image
|
|
|
|
|
|
style="width: 30rpx;height: 20rpx;"
|
|
|
|
|
|
v-if="item.ssid >= -50"
|
|
|
|
|
|
src="https://api.ccttiot.com/smartmeter/img/static/ueeDGk0mVPLUd0DHxrWj"
|
|
|
|
|
|
mode=""
|
|
|
|
|
|
></image>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 较强信号:-51 到 -60 -->
|
|
|
|
|
|
<image
|
|
|
|
|
|
style="width: 30rpx;height: 20rpx;"
|
|
|
|
|
|
v-else-if="item.ssid >= -60 && item.ssid <= -51"
|
|
|
|
|
|
src="https://api.ccttiot.com/smartmeter/img/static/uM1obZ76ittglMRKXWLq"
|
|
|
|
|
|
mode=""
|
|
|
|
|
|
></image>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中等信号:-61 到 -70 -->
|
|
|
|
|
|
<image
|
|
|
|
|
|
style="width: 30rpx;height: 20rpx;"
|
|
|
|
|
|
v-else-if="item.ssid >= -70 && item.ssid <= -61"
|
|
|
|
|
|
src="https://api.ccttiot.com/smartmeter/img/static/ujO9AZIuUSQvHcCBKqc4"
|
|
|
|
|
|
mode=""
|
|
|
|
|
|
></image>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 较弱信号:-71 到 -80 -->
|
|
|
|
|
|
<image
|
|
|
|
|
|
style="width: 30rpx;height: 20rpx;"
|
|
|
|
|
|
v-else-if="item.ssid >= -80 && item.ssid <= -71"
|
|
|
|
|
|
src="https://api.ccttiot.com/smartmeter/img/static/uCSlbXZvho808NMCkIQP"
|
|
|
|
|
|
mode=""
|
|
|
|
|
|
></image>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 最弱信号:-81 到 -100 -->
|
|
|
|
|
|
<image
|
|
|
|
|
|
style="width: 30rpx;height: 20rpx;"
|
|
|
|
|
|
v-else-if="item.ssid >= -100 && item.ssid <= -81"
|
|
|
|
|
|
src="https://api.ccttiot.com/smartmeter/img/static/u8bj3ZNi8Zssunk69HWc"
|
|
|
|
|
|
mode=""
|
|
|
|
|
|
></image>
|
2025-04-26 17:47:36 +08:00
|
|
|
|
</view>
|
2024-10-18 17:59:38 +08:00
|
|
|
|
</view>
|
2026-03-26 17:48:21 +08:00
|
|
|
|
<!-- 绑定状态:优先显示“已添加”,未查询完显示“查询中”,其余显示“添加” -->
|
|
|
|
|
|
<view class="add" style="color: #ccc;border: 1px solid #ccc;" v-if="item.isBound === true">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
已添加
|
2026-03-26 17:48:21 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="add" style="color: #ccc;border: 1px solid #ccc;" v-else-if="item.hasCheckBind !== true">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
查询中…
|
2024-10-18 17:59:38 +08:00
|
|
|
|
</view>
|
2025-04-21 17:59:16 +08:00
|
|
|
|
<view class="add" @click="btnadd(item)" v-else>
|
2026-06-18 11:31:43 +08:00
|
|
|
|
添加
|
2025-12-26 16:53:58 +08:00
|
|
|
|
</view>
|
2024-10-18 17:59:38 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 未搜索到设备 -->
|
|
|
|
|
|
<view class="wei" v-else>
|
|
|
|
|
|
<image src="https://api.ccttiot.com/smartmeter/img/static/uQ4g6A27FGtF34ebOtea" mode=""></image>
|
|
|
|
|
|
<view class="sbname">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
{{ lastBleIssue && lastBleIssue.title ? lastBleIssue.title : '搜索附近的设备失败' }}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="sbwz">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
<text v-if="lastBleIssue && lastBleIssue.kind === 'WECHAT_AUTH'">检测到「微信未授权本小程序使用蓝牙」。{{ wechatBleAuthGuideBrief }}</text>
|
|
|
|
|
|
<text v-else-if="lastBleIssue && lastBleIssue.kind === 'PHONE_BT_OFF'">检测到「手机系统蓝牙未开启」:请先到系统设置打开手机蓝牙,也可在弹窗中尝试「打开系统蓝牙设置」。</text>
|
|
|
|
|
|
<text v-else-if="lastBleIssue && lastBleIssue.kind === 'LOCATION'">检测到「定位权限未开启」:部分安卓手机扫描蓝牙需开启定位。请在系统设置或弹窗中允许定位/附近设备权限后再试。</text>
|
|
|
|
|
|
<text v-else-if="lastBleIssue && lastBleIssue.kind === 'GENERIC_INIT_FAIL'">「蓝牙初始化失败」在不少机型上是系统/微信偶发或权限组合问题,不一定只是蓝牙没开。请按弹窗里的顺序逐项排查,并可复制错误信息给客服。</text>
|
|
|
|
|
|
<text v-else>未扫描到设备。请先确认手机系统蓝牙已打开;若反复提示初始化失败,请按弹窗「排查步骤」操作,并尝试重启微信或手机。{{ wechatBleAuthGuideBrief }}</text>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
</view>
|
2024-10-18 17:59:38 +08:00
|
|
|
|
</view>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 点击搜索 -->
|
2026-06-18 11:31:43 +08:00
|
|
|
|
<view class="btnss" v-if="showSearchButton" @click="handleSearch">
|
|
|
|
|
|
重新搜索
|
2024-10-18 17:59:38 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
2025-04-21 17:59:16 +08:00
|
|
|
|
<!-- 自定义名称弹框 -->
|
|
|
|
|
|
<u-popup v-model="showNameDialog" mode="center" border-radius="14" width="600rpx">
|
|
|
|
|
|
<view class="custom-name-dialog">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
<view class="dialog-title">设备名称</view>
|
|
|
|
|
|
<u-input v-model="customDeviceName" placeholder="请输入设备名称" />
|
2025-04-21 17:59:16 +08:00
|
|
|
|
<view class="dialog-btns">
|
2026-06-18 11:31:43 +08:00
|
|
|
|
<view class="btn cancel" @click="showNameDialog = false">取消</view>
|
|
|
|
|
|
<view class="btn confirm" @click="confirmAddDevice">确定</view>
|
2025-04-21 17:59:16 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</u-popup>
|
|
|
|
|
|
|
2024-10-17 18:00:05 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-04-18 13:42:53 +08:00
|
|
|
|
var xBlufi = require("@/components/blufi/xBlufi.js");
|
2024-10-17 18:00:05 +08:00
|
|
|
|
export default {
|
2025-04-18 13:42:53 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
bgc: {
|
|
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
|
|
},
|
|
|
|
|
|
active: 1,
|
|
|
|
|
|
flag: true,
|
|
|
|
|
|
devicesList: [],
|
2026-01-17 17:37:00 +08:00
|
|
|
|
newlist:[],
|
2025-04-18 13:42:53 +08:00
|
|
|
|
deviceId: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
mac: '',
|
|
|
|
|
|
flags: true,
|
|
|
|
|
|
userid: '',
|
2026-01-17 17:37:00 +08:00
|
|
|
|
arr: [],
|
2025-04-18 13:42:53 +08:00
|
|
|
|
jiaohuaqi: [],
|
2025-04-21 17:59:16 +08:00
|
|
|
|
getpre: [],
|
|
|
|
|
|
showNameDialog: false,
|
|
|
|
|
|
customDeviceName: '',
|
|
|
|
|
|
currentDevice: null,
|
2025-05-19 16:32:27 +08:00
|
|
|
|
searchTimer: null,
|
|
|
|
|
|
checkTimer: null,
|
|
|
|
|
|
isSearching: false,
|
|
|
|
|
|
searchTimeout: null,
|
|
|
|
|
|
throttleTimer: null,
|
|
|
|
|
|
lastSearchTime: 0,
|
2026-06-18 11:31:43 +08:00
|
|
|
|
searchInterval: 800, // 搜索节流间隔(更细致)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
displayQueue: [], // 显示队列
|
|
|
|
|
|
processingQueue: false, // 是否正在处理队列
|
2026-03-26 17:48:21 +08:00
|
|
|
|
isBatchRequesting: false, // 是否有批量请求进行中
|
|
|
|
|
|
rssiMap: {}, // 记录每个 mac 的信号强度
|
2026-06-18 11:31:43 +08:00
|
|
|
|
discoveredDeviceMap: {}, // 扫描过程中累计发现的设备(按 mac 去重)
|
|
|
|
|
|
discoverStartRetryCount: 0, // 扫描启动重试次数
|
|
|
|
|
|
maxDiscoverStartRetry: 5, // 扫描启动最大重试次数(部分机型需多试几次)
|
|
|
|
|
|
maxBluetoothInitRetry: 6, // openAdapter 重试次数
|
|
|
|
|
|
scanRestartTimer: null, // 周期重启扫描,避免系统扫描卡死
|
|
|
|
|
|
showSearchButton: true, // 重新搜索按钮是否显示
|
|
|
|
|
|
searchButtonCooldownTimer: null, // 重新搜索按钮冷却计时器
|
|
|
|
|
|
// 最近一次蓝牙失败:WECHAT_AUTH | PHONE_BT_OFF | LOCATION | GENERIC_INIT_FAIL | UNKNOWN
|
|
|
|
|
|
lastBleIssue: null,
|
2025-04-18 13:42:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-09-22 15:24:01 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
sortedJiaohuaqi() {
|
|
|
|
|
|
return this.jiaohuaqi.slice().sort((a, b) => {
|
|
|
|
|
|
if (a.ssid === undefined) return 1;
|
|
|
|
|
|
if (b.ssid === undefined) return -1;
|
|
|
|
|
|
return Math.abs(a.ssid) - Math.abs(b.ssid);
|
|
|
|
|
|
});
|
2026-06-18 11:31:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 与弹窗一致:指微信小程序标题栏右侧「⋯」路径,避免说「左上角」与真实界面不符
|
|
|
|
|
|
wechatBleAuthGuideBrief() {
|
|
|
|
|
|
return '看本页最顶部:标题栏最右侧点「⋯」→ 弹出层里点「设置」→ 再点「蓝牙」并打开,然后点下面「重新搜索」。'
|
2025-09-22 15:24:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-04-18 13:42:53 +08:00
|
|
|
|
// 分享到好友(会话)
|
|
|
|
|
|
onShareAppMessage: function() {
|
|
|
|
|
|
return {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
title: '绿小能',
|
2025-04-18 13:42:53 +08:00
|
|
|
|
path: '/pages/index/index'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 分享到朋友圈
|
|
|
|
|
|
onShareTimeline: function() {
|
|
|
|
|
|
return {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
title: '绿小能',
|
2025-04-18 13:42:53 +08:00
|
|
|
|
query: '',
|
|
|
|
|
|
path: '/pages/index/index'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
this.getmodel()
|
|
|
|
|
|
this.getinfo()
|
2026-06-18 11:31:43 +08:00
|
|
|
|
// 预热蓝牙适配器,减少首次进入时初始化失败
|
|
|
|
|
|
this.ensureBluetoothReady(false)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
this.startSearch()
|
|
|
|
|
|
},
|
|
|
|
|
|
onHide() {
|
|
|
|
|
|
this.stopSearch()
|
|
|
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
if (this.searchButtonCooldownTimer) {
|
|
|
|
|
|
clearTimeout(this.searchButtonCooldownTimer)
|
|
|
|
|
|
this.searchButtonCooldownTimer = null
|
|
|
|
|
|
}
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.stopSearch()
|
2025-04-18 13:42:53 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
sleep(ms) {
|
|
|
|
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeMac(text) {
|
|
|
|
|
|
const raw = String(text || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
|
|
|
|
|
if (raw.length < 12) return ''
|
|
|
|
|
|
return raw.slice(-12)
|
|
|
|
|
|
},
|
|
|
|
|
|
parseBleIdentity(device) {
|
|
|
|
|
|
const prefixes = ['WATER', 'SMSJ:', 'DATER', 'TATER', 'SATER']
|
|
|
|
|
|
const candidates = [device.localName, device.name, device.deviceId]
|
|
|
|
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
|
|
|
|
const value = String(candidates[i] || '')
|
|
|
|
|
|
if (!value) continue
|
|
|
|
|
|
const upper = value.toUpperCase()
|
|
|
|
|
|
const pre = prefixes.find(p => upper.startsWith(p))
|
|
|
|
|
|
const mac = this.normalizeMac(value)
|
|
|
|
|
|
if (pre && mac) {
|
|
|
|
|
|
return { pre, mac }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
// 是否在微信内被关闭「蓝牙」授权(与手机系统蓝牙开关不同)
|
|
|
|
|
|
isWechatBluetoothUnauthorized() {
|
|
|
|
|
|
if (typeof uni.getAppAuthorizeSetting !== 'function') return false
|
|
|
|
|
|
try {
|
|
|
|
|
|
const as = uni.getAppAuthorizeSetting()
|
|
|
|
|
|
return !!(as && typeof as.bluetoothAuthorized === 'boolean' && as.bluetoothAuthorized === false)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 根据 uni/wx 返回的 err 归类(各厂商 errMsg 差异大,无法识别时走 GENERIC_INIT_FAIL)
|
|
|
|
|
|
classifyBluetoothError(err) {
|
|
|
|
|
|
if (!err) return { kind: 'UNKNOWN' }
|
|
|
|
|
|
const msg = String(err.errMsg || err.message || '').toLowerCase()
|
|
|
|
|
|
const code = err.errCode
|
|
|
|
|
|
if (
|
|
|
|
|
|
msg.includes('auth deny') ||
|
|
|
|
|
|
msg.includes('authoriz') ||
|
|
|
|
|
|
(msg.includes('permission') && (msg.includes('bluetooth') || msg.includes('nearby') || msg.includes('device'))) ||
|
|
|
|
|
|
msg.includes('not authorized') ||
|
|
|
|
|
|
msg.includes('system permission denied') ||
|
|
|
|
|
|
msg.includes('user denied') ||
|
|
|
|
|
|
msg.includes('privacy') && msg.includes('bluetooth')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return { kind: 'WECHAT_AUTH' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
msg.includes('location') ||
|
|
|
|
|
|
msg.includes('定位') ||
|
|
|
|
|
|
msg.includes('location services') ||
|
|
|
|
|
|
msg.includes('need location') ||
|
|
|
|
|
|
msg.includes('gps') && msg.includes('enable')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return { kind: 'LOCATION' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
code === 10001 ||
|
|
|
|
|
|
msg.includes('not available') ||
|
|
|
|
|
|
msg.includes('unavailable') ||
|
|
|
|
|
|
msg.includes('powered off') ||
|
|
|
|
|
|
msg.includes('bluetooth is closed') ||
|
|
|
|
|
|
msg.includes('蓝牙未打开') ||
|
|
|
|
|
|
msg.includes('请打开蓝牙') ||
|
|
|
|
|
|
msg.includes('蓝牙不可用') ||
|
|
|
|
|
|
msg.includes('adapter not') ||
|
|
|
|
|
|
msg.includes('bluetooth off') ||
|
|
|
|
|
|
msg.includes('turn on bluetooth')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return { kind: 'PHONE_BT_OFF' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
msg.includes('timeout') ||
|
|
|
|
|
|
msg.includes('time out') ||
|
|
|
|
|
|
msg.includes('超时') ||
|
|
|
|
|
|
msg.includes('busy') ||
|
|
|
|
|
|
msg.includes('正在使用') ||
|
|
|
|
|
|
msg.includes('already') && msg.includes('open')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return { kind: 'GENERIC_INIT_FAIL' }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { kind: 'UNKNOWN' }
|
|
|
|
|
|
},
|
|
|
|
|
|
formatBleDiagnostic(err) {
|
|
|
|
|
|
if (!err) return '(无详细错误码)'
|
|
|
|
|
|
const code = err.errCode != null ? String(err.errCode) : ''
|
|
|
|
|
|
const msg = String(err.errMsg || err.message || '').trim()
|
|
|
|
|
|
const line = [code && `errCode=${code}`, msg && `errMsg=${msg}`].filter(Boolean).join('\n')
|
|
|
|
|
|
return line || '(无详细错误码)'
|
|
|
|
|
|
},
|
|
|
|
|
|
setBleIssueFromKind(kind, extra = {}) {
|
|
|
|
|
|
const titles = {
|
|
|
|
|
|
WECHAT_AUTH: '微信未授权蓝牙',
|
|
|
|
|
|
PHONE_BT_OFF: '手机蓝牙未开启',
|
|
|
|
|
|
LOCATION: '需开启定位权限',
|
|
|
|
|
|
GENERIC_INIT_FAIL: '蓝牙初始化失败',
|
|
|
|
|
|
UNKNOWN: '蓝牙不可用'
|
|
|
|
|
|
}
|
|
|
|
|
|
this.lastBleIssue = { kind, title: titles[kind] || titles.UNKNOWN, ...extra }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 弹窗说明 + 可跳转:微信授权页 / 系统蓝牙设置;无法归类时用「综合排查」避免误导
|
|
|
|
|
|
showBluetoothIssueModal(parsed, rawErr) {
|
|
|
|
|
|
const kindRaw = parsed && parsed.kind ? parsed.kind : 'UNKNOWN'
|
|
|
|
|
|
const diag = this.formatBleDiagnostic(rawErr)
|
|
|
|
|
|
const mergeInit = kindRaw === 'UNKNOWN' || kindRaw === 'GENERIC_INIT_FAIL'
|
|
|
|
|
|
const kindUi = mergeInit ? 'GENERIC_INIT_FAIL' : kindRaw
|
|
|
|
|
|
this.setBleIssueFromKind(kindUi, { diagnostic: diag })
|
|
|
|
|
|
if (kindRaw === 'WECHAT_AUTH') {
|
|
|
|
|
|
const content = [
|
|
|
|
|
|
'【原因】当前是「微信不让本小程序用蓝牙」,和手机「设置→蓝牙」总开关不是同一项。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'【点哪里】看屏幕最上方小程序那一横条:',
|
|
|
|
|
|
'· 最左边一般是「‹」返回聊天,不要点它。',
|
|
|
|
|
|
'· 中间是本页标题。',
|
|
|
|
|
|
'· 最右边有一小块区域,里面有「⋯」或「···」(更多),和「关小程序」的圆圈在同一行——点这个「⋯」。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'【接下来】',
|
|
|
|
|
|
'① 点「⋯」后会从下往上弹出一块面板;',
|
|
|
|
|
|
'② 在面板里点「设置」;',
|
|
|
|
|
|
'③ 进入后找到「蓝牙」这一行,把开关打开;',
|
|
|
|
|
|
'④ 若没看到「蓝牙」,在「设置」页里往上滑找一下。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'做完后回到本页,点「重新搜索」。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'(若下方按钮能打开微信权限页,也可作辅助;以「⋯→设置→蓝牙」为准。)'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '请打开:小程序的蓝牙权限',
|
|
|
|
|
|
content,
|
|
|
|
|
|
confirmText: '打开权限页(辅助)',
|
|
|
|
|
|
cancelText: '我先按步骤点',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm && typeof uni.openAppAuthorizeSetting === 'function') {
|
|
|
|
|
|
uni.openAppAuthorizeSetting({})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kindRaw === 'PHONE_BT_OFF') {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '手机蓝牙未开启',
|
|
|
|
|
|
content: '系统蓝牙处于关闭状态,微信无法扫描设备。\n\n请先打开手机「设置」→「蓝牙」并开启;也可点「打开系统蓝牙设置」尝试跳转(若系统支持)。',
|
|
|
|
|
|
confirmText: '打开系统蓝牙设置',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm && typeof uni.openSystemBluetoothSetting === 'function') {
|
|
|
|
|
|
uni.openSystemBluetoothSetting({
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showToast({ title: '请手动在系统设置中打开蓝牙', icon: 'none', duration: 2500 })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kindRaw === 'LOCATION') {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '需开启定位权限',
|
|
|
|
|
|
content: '部分安卓机型扫描蓝牙需开启「定位」或「附近设备」权限。\n\n请点「去设置」,在权限页中允许定位;或到系统设置 → 应用 → 微信 → 权限中开启。',
|
|
|
|
|
|
confirmText: '去设置',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm && typeof uni.openAppAuthorizeSetting === 'function') {
|
|
|
|
|
|
uni.openAppAuthorizeSetting({})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// UNKNOWN / GENERIC_INIT_FAIL:不猜测单一原因,给完整排查 + 可复制诊断
|
|
|
|
|
|
let plat = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
plat = String((uni.getSystemInfoSync() || {}).platform || '').toLowerCase()
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
const androidLine = plat === 'android'
|
|
|
|
|
|
? '· 【安卓】系统「定位」总开关建议打开;并在 设置→应用→微信→权限 里允许「附近设备/蓝牙/定位」相关项。\n'
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const content = [
|
|
|
|
|
|
'不少机型会反复提示「蓝牙初始化失败」,常见原因包括:系统蓝牙栈卡住、微信权限未开全、安卓定位限制等,不一定是硬件损坏。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'请按顺序尝试(每完成一两步即可点「重新搜索」试一次):',
|
|
|
|
|
|
'① 手机系统设置里关闭蓝牙,等 2~3 秒再打开',
|
|
|
|
|
|
'② 微信:标题栏右侧「⋯」→「设置」→「蓝牙」→ 允许本小程序',
|
|
|
|
|
|
androidLine + '③ 从多任务里划掉微信,重新打开后再进本页',
|
|
|
|
|
|
'④ 仍失败可重启手机,或将微信更新到最新版本',
|
|
|
|
|
|
'⑤ 若偶发成功、偶发失败,多为系统资源占用,可多试几次',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'【本机返回】复制后发给客服更易排查:',
|
|
|
|
|
|
diag
|
|
|
|
|
|
].filter(Boolean).join('\n')
|
|
|
|
|
|
const safeContent = content.length > 1750 ? content.slice(0, 1747) + '…' : content
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '蓝牙初始化失败',
|
|
|
|
|
|
content: safeContent,
|
|
|
|
|
|
confirmText: '复制诊断信息',
|
|
|
|
|
|
cancelText: '关闭',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
const clip = `绿小能-蓝牙诊断\n${diag}\nplatform=${plat || 'unknown'}\nraw=${String((rawErr && rawErr.errMsg) || '').slice(0, 400)}`
|
|
|
|
|
|
uni.setClipboardData({
|
|
|
|
|
|
data: clip.slice(0, 800),
|
|
|
|
|
|
success: () => uni.showToast({ title: '已复制', icon: 'none' })
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
async ensureBluetoothReady(showErrorModal = true) {
|
|
|
|
|
|
if (this.isWechatBluetoothUnauthorized()) {
|
|
|
|
|
|
if (showErrorModal) this.showBluetoothIssueModal({ kind: 'WECHAT_AUTH' }, {})
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
let lastErr = null
|
|
|
|
|
|
let lastStateUnavailable = false
|
|
|
|
|
|
for (let i = 0; i < this.maxBluetoothInitRetry; i++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
|
|
uni.openBluetoothAdapter({
|
|
|
|
|
|
success: resolve,
|
|
|
|
|
|
fail: reject
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
const state = await new Promise((resolve, reject) => {
|
|
|
|
|
|
uni.getBluetoothAdapterState({
|
|
|
|
|
|
success: resolve,
|
|
|
|
|
|
fail: reject
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
if (state && state.available) {
|
|
|
|
|
|
if (showErrorModal) this.lastBleIssue = null
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
lastStateUnavailable = true
|
|
|
|
|
|
lastErr = { errMsg: 'getBluetoothAdapterState: unavailable', errCode: 10001 }
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
lastErr = err
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
uni.closeBluetoothAdapter({})
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
await this.sleep(350 + Math.min(i, 5) * 200)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showErrorModal) {
|
|
|
|
|
|
const parsed = this.classifyBluetoothError(lastErr)
|
|
|
|
|
|
// 适配器 unavailable 也可能是权限/系统卡住,不再强行归类为「仅手机蓝牙未开」
|
|
|
|
|
|
const hint = lastStateUnavailable && parsed.kind === 'UNKNOWN'
|
|
|
|
|
|
? { errMsg: `${String(lastErr && lastErr.errMsg || '')} (adapter state: unavailable)`.trim(), errCode: lastErr && lastErr.errCode }
|
|
|
|
|
|
: lastErr
|
|
|
|
|
|
this.showBluetoothIssueModal(parsed, hint)
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
},
|
|
|
|
|
|
startDiscoverWithRetry() {
|
|
|
|
|
|
xBlufi.notifyStartDiscoverBle({ isStart: true })
|
|
|
|
|
|
},
|
2025-04-18 13:42:53 +08:00
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
getinfo() {
|
2025-12-26 16:53:58 +08:00
|
|
|
|
this.$u.get(`/system/user/profile`).then((res) => {
|
2025-04-18 13:42:53 +08:00
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
|
this.userid = res.data.userId
|
2025-12-26 16:53:58 +08:00
|
|
|
|
uni.setStorageSync('user',res.data)
|
|
|
|
|
|
uni.setStorageSync('userId',res.data.userId)
|
2025-04-18 13:42:53 +08:00
|
|
|
|
} else if (res.code == 401) {
|
|
|
|
|
|
uni.showModal({
|
2026-06-18 11:31:43 +08:00
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '您还未登录,是否前去登录?',
|
2026-03-26 17:48:21 +08:00
|
|
|
|
success: (r) => {
|
|
|
|
|
|
if (r.confirm) {
|
2025-04-18 13:42:53 +08:00
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: '/pages/login/login'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-04-21 17:59:16 +08:00
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
})
|
2024-10-18 17:59:38 +08:00
|
|
|
|
},
|
2025-04-21 17:59:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 点击添加按钮
|
2025-04-18 13:42:53 +08:00
|
|
|
|
btnadd(e) {
|
2025-04-21 17:59:16 +08:00
|
|
|
|
this.currentDevice = e;
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.customDeviceName = e.modelName || '未知设备';
|
2025-04-21 17:59:16 +08:00
|
|
|
|
this.showNameDialog = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
// 确认添加设备
|
|
|
|
|
|
confirmAddDevice() {
|
|
|
|
|
|
if (!this.customDeviceName.trim()) {
|
|
|
|
|
|
uni.showToast({
|
2026-06-18 11:31:43 +08:00
|
|
|
|
title: '请输入设备名称',
|
2025-04-21 17:59:16 +08:00
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mac = this.currentDevice.name.slice(-12);
|
2025-04-18 13:42:53 +08:00
|
|
|
|
let data = {
|
|
|
|
|
|
mac: mac,
|
2025-12-26 16:53:58 +08:00
|
|
|
|
// userId: this.userid,
|
2025-04-21 17:59:16 +08:00
|
|
|
|
pre: this.currentDevice.pre,
|
|
|
|
|
|
deviceName: this.customDeviceName
|
2024-10-17 18:00:05 +08:00
|
|
|
|
}
|
2025-04-26 17:47:36 +08:00
|
|
|
|
console.log(data,'参数');
|
2026-01-17 17:37:00 +08:00
|
|
|
|
this.$u.post(`/app/device/bindDeviceByBlueTooth`, data).then((res) => {
|
2025-04-18 13:42:53 +08:00
|
|
|
|
if (res.code == 200) {
|
2026-01-17 17:37:00 +08:00
|
|
|
|
uni.showToast({
|
2026-06-18 11:31:43 +08:00
|
|
|
|
title: '绑定成功',
|
2026-01-17 17:37:00 +08:00
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 3000
|
2025-04-18 13:42:53 +08:00
|
|
|
|
})
|
2026-01-17 17:37:00 +08:00
|
|
|
|
this.showNameDialog = false;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
// let datas = {
|
|
|
|
|
|
// mac:mac
|
|
|
|
|
|
// }
|
|
|
|
|
|
// this.$u.post(`/app/device/bindDevice`, datas).then(resp =>{
|
|
|
|
|
|
// if(resp.code == 200){
|
|
|
|
|
|
// uni.showToast({
|
|
|
|
|
|
// title:'绑定成功',
|
|
|
|
|
|
// icon: 'none',
|
|
|
|
|
|
// duration: 3000
|
|
|
|
|
|
// })
|
|
|
|
|
|
// this.showNameDialog = false;
|
|
|
|
|
|
// setTimeout(() => {
|
|
|
|
|
|
// uni.navigateBack()
|
|
|
|
|
|
// }, 2000)
|
|
|
|
|
|
// }else{
|
|
|
|
|
|
// uni.showToast({
|
|
|
|
|
|
// title: resp.msg,
|
|
|
|
|
|
// icon: 'none',
|
|
|
|
|
|
// duration: 3000
|
|
|
|
|
|
// })
|
|
|
|
|
|
// }
|
|
|
|
|
|
// })
|
2025-04-18 13:42:53 +08:00
|
|
|
|
} else {
|
2025-04-26 17:47:36 +08:00
|
|
|
|
console.log(res,'报错');
|
2025-04-18 13:42:53 +08:00
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: res.msg,
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 3000
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2024-10-17 18:00:05 +08:00
|
|
|
|
},
|
2025-04-21 17:59:16 +08:00
|
|
|
|
getmodel() {
|
|
|
|
|
|
this.$u.get(`/app/getAllModelList`).then(res => {
|
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
|
this.getpre = res.data
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getpipei(pre) {
|
|
|
|
|
|
// 添加默认返回值防止undefined
|
|
|
|
|
|
return this.getpre.find(item => item.pre == pre) || {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
modelName: '未知型号',
|
2025-04-21 17:59:16 +08:00
|
|
|
|
picture: ''
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-05-19 16:32:27 +08:00
|
|
|
|
// 开始搜索
|
2026-06-18 11:31:43 +08:00
|
|
|
|
async startSearch() {
|
2025-05-19 16:32:27 +08:00
|
|
|
|
if (this.isSearching) return
|
2026-06-18 11:31:43 +08:00
|
|
|
|
const ready = await this.ensureBluetoothReady(true)
|
|
|
|
|
|
if (!ready) {
|
|
|
|
|
|
this.flags = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.lastBleIssue = null
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.isSearching = true
|
|
|
|
|
|
this.jiaohuaqi = []
|
|
|
|
|
|
this.displayQueue = []
|
|
|
|
|
|
this.processingQueue = false
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.discoveredDeviceMap = {}
|
|
|
|
|
|
this.rssiMap = {}
|
|
|
|
|
|
this.arr = []
|
|
|
|
|
|
this.lastSearchTime = 0
|
|
|
|
|
|
this.discoverStartRetryCount = 0
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.flag = false
|
|
|
|
|
|
|
|
|
|
|
|
// 开始蓝牙搜索
|
|
|
|
|
|
xBlufi.listenDeviceMsgEvent(true, this.funListenDeviceMsgEvent)
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.startDiscoverWithRetry()
|
|
|
|
|
|
// 周期重启扫描,降低系统层扫描卡住导致搜不到的概率
|
|
|
|
|
|
this.scanRestartTimer = setInterval(() => {
|
|
|
|
|
|
if (!this.isSearching) return
|
|
|
|
|
|
xBlufi.notifyStartDiscoverBle({ isStart: false })
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!this.isSearching) return
|
|
|
|
|
|
this.startDiscoverWithRetry()
|
|
|
|
|
|
}, 250)
|
|
|
|
|
|
}, 8000)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-06-18 11:31:43 +08:00
|
|
|
|
// 持续慢速扫描,不自动停止;仅在离开页面或手动重新搜索时停止
|
2025-05-19 16:32:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 停止搜索
|
|
|
|
|
|
stopSearch() {
|
|
|
|
|
|
this.isSearching = false
|
|
|
|
|
|
if (this.checkTimer) {
|
|
|
|
|
|
clearInterval(this.checkTimer)
|
|
|
|
|
|
this.checkTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.searchTimeout) {
|
|
|
|
|
|
clearTimeout(this.searchTimeout)
|
|
|
|
|
|
this.searchTimeout = null
|
|
|
|
|
|
}
|
2026-06-18 11:31:43 +08:00
|
|
|
|
if (this.scanRestartTimer) {
|
|
|
|
|
|
clearInterval(this.scanRestartTimer)
|
|
|
|
|
|
this.scanRestartTimer = null
|
|
|
|
|
|
}
|
2025-05-19 16:32:27 +08:00
|
|
|
|
if (this.throttleTimer) {
|
|
|
|
|
|
clearTimeout(this.throttleTimer)
|
|
|
|
|
|
this.throttleTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
xBlufi.notifyStartDiscoverBle({ 'isStart': false })
|
2026-06-18 11:31:43 +08:00
|
|
|
|
xBlufi.listenDeviceMsgEvent(false, this.funListenDeviceMsgEvent)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.flag = true
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 处理显示队列
|
|
|
|
|
|
processDisplayQueue() {
|
|
|
|
|
|
if (this.processingQueue || this.displayQueue.length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
this.processingQueue = true
|
|
|
|
|
|
const device = this.displayQueue.shift()
|
|
|
|
|
|
|
|
|
|
|
|
// 检查设备是否已存在
|
|
|
|
|
|
if (!this.jiaohuaqi.some(item => item.name === device.name)) {
|
|
|
|
|
|
this.jiaohuaqi.push(device)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟处理下一个设备
|
2025-04-21 17:59:16 +08:00
|
|
|
|
setTimeout(() => {
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.processingQueue = false
|
|
|
|
|
|
this.processDisplayQueue()
|
2026-06-18 11:31:43 +08:00
|
|
|
|
}, 700) // 每个设备显示间隔700ms,慢速渐进展示
|
2025-05-19 16:32:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 添加设备到显示队列
|
|
|
|
|
|
addToDisplayQueue(device) {
|
|
|
|
|
|
this.displayQueue.push(device)
|
|
|
|
|
|
if (!this.processingQueue) {
|
|
|
|
|
|
this.processDisplayQueue()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-18 11:31:43 +08:00
|
|
|
|
// 扫描到设备后先直接展示,再由接口补充绑定状态
|
|
|
|
|
|
upsertScannedDevice(item, parsed) {
|
|
|
|
|
|
const mac = parsed.mac
|
|
|
|
|
|
const pre = parsed.pre || ''
|
|
|
|
|
|
const rssi = item && item.RSSI != null ? item.RSSI : undefined
|
|
|
|
|
|
const matched = this.getpipei(pre)
|
|
|
|
|
|
const exist = this.jiaohuaqi.find(dev => dev.mac === mac)
|
|
|
|
|
|
if (exist) {
|
|
|
|
|
|
if (pre) exist.pre = pre
|
|
|
|
|
|
if (!exist.modelName) exist.modelName = matched.modelName
|
|
|
|
|
|
if (!exist.modelPicture) exist.modelPicture = matched.picture
|
|
|
|
|
|
if (rssi !== undefined) exist.ssid = rssi
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.addToDisplayQueue({
|
|
|
|
|
|
name: mac,
|
|
|
|
|
|
mac,
|
|
|
|
|
|
pre,
|
|
|
|
|
|
modelName: matched.modelName || '未知型号',
|
|
|
|
|
|
modelPicture: matched.picture || '',
|
|
|
|
|
|
userId: null,
|
|
|
|
|
|
isBound: false,
|
|
|
|
|
|
hasCheckBind: false,
|
|
|
|
|
|
ssid: rssi
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-03-26 17:48:21 +08:00
|
|
|
|
// 更新设备列表(只展示接口返回的数据,累加去重)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
updateDeviceList(existList) {
|
2026-03-26 17:48:21 +08:00
|
|
|
|
if (!Array.isArray(existList) || existList.length === 0) return
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-03-26 17:48:21 +08:00
|
|
|
|
existList.forEach(item => {
|
|
|
|
|
|
// 后端返回的设备信息中 mac 为唯一标识
|
|
|
|
|
|
const mac = item.mac
|
|
|
|
|
|
if (!mac) return
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-03-26 17:48:21 +08:00
|
|
|
|
// 计算信号强度:优先取本地扫描到的 rssiMap
|
|
|
|
|
|
const ssid = this.rssiMap[mac] != null ? this.rssiMap[mac] : undefined
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-03-26 17:48:21 +08:00
|
|
|
|
// 设备是否已绑定
|
|
|
|
|
|
const isBound = item.userId != null
|
|
|
|
|
|
|
|
|
|
|
|
// 如果列表中已存在该设备:更新状态
|
|
|
|
|
|
const exist = this.jiaohuaqi.find(dev => dev.mac === mac)
|
|
|
|
|
|
if (exist) {
|
|
|
|
|
|
exist.userId = item.userId
|
|
|
|
|
|
exist.isBound = isBound
|
|
|
|
|
|
exist.hasCheckBind = true
|
|
|
|
|
|
exist.modelName = item.modelName || exist.modelName
|
|
|
|
|
|
exist.modelPicture = item.modelPicture || exist.modelPicture
|
|
|
|
|
|
if (ssid !== undefined) {
|
|
|
|
|
|
exist.ssid = ssid
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 不存在则新增(只用接口返回的数据来展示)
|
|
|
|
|
|
const pre = item.pre || ''
|
|
|
|
|
|
const matched = this.getpipei(pre)
|
|
|
|
|
|
const newDevice = {
|
|
|
|
|
|
name: mac,
|
|
|
|
|
|
mac: mac,
|
|
|
|
|
|
pre: pre,
|
|
|
|
|
|
modelName: item.modelName || matched.modelName,
|
|
|
|
|
|
modelPicture: item.modelPicture || matched.picture,
|
|
|
|
|
|
userId: item.userId,
|
|
|
|
|
|
isBound: isBound,
|
|
|
|
|
|
hasCheckBind: true,
|
|
|
|
|
|
ssid: ssid
|
|
|
|
|
|
}
|
|
|
|
|
|
this.addToDisplayQueue(newDevice)
|
|
|
|
|
|
}
|
2025-05-19 16:32:27 +08:00
|
|
|
|
})
|
2025-04-21 17:59:16 +08:00
|
|
|
|
},
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2025-04-18 13:42:53 +08:00
|
|
|
|
// 获取附近蓝牙设备列表
|
|
|
|
|
|
funListenDeviceMsgEvent: function(options) {
|
|
|
|
|
|
switch (options.type) {
|
|
|
|
|
|
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS:
|
|
|
|
|
|
if (options.result) {
|
2025-05-19 16:32:27 +08:00
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
if (now - this.lastSearchTime < this.searchInterval) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.lastSearchTime = now
|
|
|
|
|
|
|
2026-06-18 11:31:43 +08:00
|
|
|
|
let hasNewDevice = false
|
|
|
|
|
|
const deviceData = Array.isArray(options.data) ? options.data : []
|
|
|
|
|
|
this.devicesList = deviceData
|
|
|
|
|
|
// 同时兼容 localName/name/deviceId,累计去重后再批量查询
|
|
|
|
|
|
deviceData.forEach(item => {
|
|
|
|
|
|
const parsed = this.parseBleIdentity(item || {})
|
|
|
|
|
|
if (!parsed || !parsed.mac) return
|
|
|
|
|
|
this.upsertScannedDevice(item, parsed)
|
|
|
|
|
|
const old = this.discoveredDeviceMap[parsed.mac]
|
|
|
|
|
|
if (!old) {
|
|
|
|
|
|
hasNewDevice = true
|
|
|
|
|
|
this.discoveredDeviceMap[parsed.mac] = {
|
|
|
|
|
|
pre: parsed.pre,
|
|
|
|
|
|
mac: parsed.mac
|
2026-03-26 17:48:21 +08:00
|
|
|
|
}
|
2026-06-18 11:31:43 +08:00
|
|
|
|
} else if (!old.pre && parsed.pre) {
|
|
|
|
|
|
this.discoveredDeviceMap[parsed.mac].pre = parsed.pre
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.RSSI != null) {
|
|
|
|
|
|
const oldRssi = this.rssiMap[parsed.mac]
|
|
|
|
|
|
this.rssiMap[parsed.mac] = oldRssi == null ? item.RSSI : Math.max(oldRssi, item.RSSI)
|
2026-01-17 17:37:00 +08:00
|
|
|
|
}
|
2024-10-23 18:04:30 +08:00
|
|
|
|
})
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.arr = Object.keys(this.discoveredDeviceMap).map(mac => this.discoveredDeviceMap[mac])
|
|
|
|
|
|
if (hasNewDevice) {
|
|
|
|
|
|
this.flags = true
|
|
|
|
|
|
}
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
2026-03-26 17:48:21 +08:00
|
|
|
|
// 使用防抖 + 请求中的标记,避免一次性触发很多请求
|
2025-05-19 16:32:27 +08:00
|
|
|
|
if (this.throttleTimer) {
|
|
|
|
|
|
clearTimeout(this.throttleTimer)
|
2025-04-18 13:42:53 +08:00
|
|
|
|
}
|
2026-03-26 17:48:21 +08:00
|
|
|
|
// 500ms 内多次回调只发一个请求
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.throttleTimer = setTimeout(() => {
|
2026-03-26 17:48:21 +08:00
|
|
|
|
if (!this.devicesList.length || !this.arr.length) return
|
|
|
|
|
|
// 如果上一次请求还在进行中,则本次略过,等待下一轮回调再发
|
|
|
|
|
|
if (this.isBatchRequesting) return
|
|
|
|
|
|
this.isBatchRequesting = true
|
|
|
|
|
|
const payload = this.arr.slice(0) // 拷贝一份当前 mac 列表
|
|
|
|
|
|
this.$u.post(`/app/device/batchInsert`, payload).then(res => {
|
|
|
|
|
|
if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
|
|
|
this.updateDeviceList(res.data)
|
|
|
|
|
|
}
|
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
|
this.isBatchRequesting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 500)
|
2025-04-18 13:42:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS_START:
|
|
|
|
|
|
if (!options.result) {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
// 扫描启动失败自动重试,减少偶发初始化失败
|
|
|
|
|
|
if (this.isSearching && this.discoverStartRetryCount < this.maxDiscoverStartRetry) {
|
|
|
|
|
|
this.discoverStartRetryCount += 1
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
if (!this.isSearching) return
|
|
|
|
|
|
const ready = await this.ensureBluetoothReady(false)
|
|
|
|
|
|
if (!ready) return
|
|
|
|
|
|
this.startDiscoverWithRetry()
|
|
|
|
|
|
}, 700)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const failData = options.data || {}
|
|
|
|
|
|
let parsed = this.classifyBluetoothError(failData)
|
|
|
|
|
|
if (this.isWechatBluetoothUnauthorized()) {
|
|
|
|
|
|
parsed = { kind: 'WECHAT_AUTH' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (parsed.kind === 'UNKNOWN') {
|
|
|
|
|
|
const msg = String(failData.errMsg || '').toLowerCase()
|
|
|
|
|
|
if (msg.includes('startbluetoothdevicesdiscovery') && (msg.includes('10001') || msg.includes('not available'))) {
|
|
|
|
|
|
parsed = { kind: 'PHONE_BT_OFF' }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.showBluetoothIssueModal(parsed, failData)
|
|
|
|
|
|
this.flags = this.jiaohuaqi.length > 0
|
2025-04-18 13:42:53 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.discoverStartRetryCount = 0
|
2025-04-18 13:42:53 +08:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-05-19 16:32:27 +08:00
|
|
|
|
|
|
|
|
|
|
btnss() {
|
2026-06-18 11:31:43 +08:00
|
|
|
|
// 每次点击后隐藏 6 秒,避免连续重复点击
|
|
|
|
|
|
this.showSearchButton = false
|
|
|
|
|
|
if (this.searchButtonCooldownTimer) {
|
|
|
|
|
|
clearTimeout(this.searchButtonCooldownTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.searchButtonCooldownTimer = setTimeout(() => {
|
|
|
|
|
|
this.showSearchButton = true
|
|
|
|
|
|
this.searchButtonCooldownTimer = null
|
|
|
|
|
|
}, 6000)
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.stopSearch()
|
|
|
|
|
|
this.jiaohuaqi = []
|
|
|
|
|
|
this.displayQueue = []
|
|
|
|
|
|
this.processingQueue = false
|
2026-06-18 11:31:43 +08:00
|
|
|
|
this.discoveredDeviceMap = {}
|
|
|
|
|
|
this.rssiMap = {}
|
2025-05-19 16:32:27 +08:00
|
|
|
|
this.startSearch()
|
|
|
|
|
|
},
|
|
|
|
|
|
// 处理搜索按钮点击
|
|
|
|
|
|
handleSearch() {
|
|
|
|
|
|
this.btnss()
|
|
|
|
|
|
},
|
2024-10-17 18:00:05 +08:00
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
}
|
2024-10-17 18:00:05 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less">
|
2025-09-22 15:24:01 +08:00
|
|
|
|
::v-deep .u-input__input{
|
2025-04-21 17:59:16 +08:00
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
padding-left: 10rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
2025-09-22 15:24:01 +08:00
|
|
|
|
::v-deep .u-title {
|
2024-10-17 18:00:05 +08:00
|
|
|
|
margin-bottom: 22rpx;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
2025-09-22 15:24:01 +08:00
|
|
|
|
::v-deep .uicon-nav-back {
|
2024-10-17 18:00:05 +08:00
|
|
|
|
margin-bottom: 22rpx;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.page {
|
|
|
|
|
|
padding-bottom: 300rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wei {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
text-align: center;
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
image {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
width: 380rpx;
|
|
|
|
|
|
height: 394rpx;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.sbname {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
color: #3D3D3D;
|
|
|
|
|
|
margin-top: 80rpx;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.sbwz {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #737B80;
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
width: 100%;
|
2026-06-18 11:31:43 +08:00
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding: 0 40rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
line-height: 1.55;
|
2024-10-18 17:59:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.btnss {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
width: 512rpx;
|
|
|
|
|
|
height: 92rpx;
|
|
|
|
|
|
background: #48893B;
|
|
|
|
|
|
border-radius: 46rpx 46rpx 46rpx 46rpx;
|
|
|
|
|
|
border-radius: 50rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
line-height: 92rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
color: #FFFFFF;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
bottom: 106rpx;
|
2025-05-19 16:32:27 +08:00
|
|
|
|
transition: all 0.3s ease;
|
2024-10-18 17:59:38 +08:00
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.list {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
margin: auto;
|
|
|
|
|
|
margin-top: 72rpx;
|
2025-05-19 16:32:27 +08:00
|
|
|
|
will-change: transform; // 优化动画性能
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.list_item {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
margin-top: 18rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 152rpx;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
border-radius: 20rpx;
|
2025-04-18 13:42:53 +08:00
|
|
|
|
box-shadow: 0rpx 10rpx 64rpx 0rpx rgba(0, 0, 0, 0.08);
|
2024-10-18 17:59:38 +08:00
|
|
|
|
padding: 18rpx 30rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
2025-05-19 16:32:27 +08:00
|
|
|
|
animation: slideIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
|
will-change: transform, opacity; // 优化动画性能
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
image {
|
|
|
|
|
|
width: 94rpx;
|
2024-10-18 17:59:38 +08:00
|
|
|
|
height: 94rpx;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.cen {
|
|
|
|
|
|
.name {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #50565A;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.devmac {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #BDBCBC;
|
|
|
|
|
|
margin-top: 6rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.add {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
width: 108rpx;
|
|
|
|
|
|
height: 60rpx;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
border: 3rpx solid #48893B;
|
|
|
|
|
|
filter: blur(0px);
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
line-height: 60rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #48893B;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
page {
|
2024-10-17 18:00:05 +08:00
|
|
|
|
width: 100%;
|
2024-10-18 17:59:38 +08:00
|
|
|
|
padding: 20rpx 64rpx;
|
2024-10-17 18:00:05 +08:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.topone {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
|
color: #3D3D3D;
|
|
|
|
|
|
display: flex;
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
image {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
width: 48rpx;
|
|
|
|
|
|
height: 48rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-18 13:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
.toptwo {
|
2024-10-18 17:59:38 +08:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #737B7F;
|
|
|
|
|
|
margin-top: 14rpx;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding-left: 48rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
2025-04-21 17:59:16 +08:00
|
|
|
|
|
|
|
|
|
|
.custom-name-dialog {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 40rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-title {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-btns {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-top: 40rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
width: 240rpx;
|
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
|
line-height: 80rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
border-radius: 40rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
|
|
|
|
|
|
&.cancel {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.confirm {
|
|
|
|
|
|
background: #48893B;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-10-17 18:00:05 +08:00
|
|
|
|
</style>
|