1056 lines
32 KiB
Vue
1056 lines
32 KiB
Vue
<template>
|
||
<view>
|
||
<u-navbar :is-back="true" title=' ' title-color="#000" :border-bottom="false" :background="bgc"
|
||
id="navbar">
|
||
</u-navbar>
|
||
<view class="page">
|
||
<!-- 有搜索到设备 -->
|
||
<view class="you" v-if="flags">
|
||
<view class="topone">
|
||
<image src="https://api.ccttiot.com/smartmeter/img/static/ubrPcpGQEXTadkBa1gKh" mode=""></image>
|
||
扫描到以下设备,点击添加!
|
||
</view>
|
||
<view class="toptwo">
|
||
如未找到想添加的设备,点击重新搜索
|
||
</view>
|
||
<view class="list">
|
||
<view class="list_item" v-for="(item,index) in sortedJiaohuaqi" :key="index" :class="{ show: item.show }">
|
||
<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">
|
||
MAC:{{item.mac == undefined ? item.name.slice(-12) : item.mac}}
|
||
</view>
|
||
<view class="devmac" style="display: flex;align-items: center;">
|
||
信号:
|
||
<!-- 最强信号:-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>
|
||
</view>
|
||
</view>
|
||
<!-- 绑定状态:优先显示“已添加”,未查询完显示“查询中”,其余显示“添加” -->
|
||
<view class="add" style="color: #ccc;border: 1px solid #ccc;" v-if="item.isBound === true">
|
||
已添加
|
||
</view>
|
||
<view class="add" style="color: #ccc;border: 1px solid #ccc;" v-else-if="item.hasCheckBind !== true">
|
||
查询中…
|
||
</view>
|
||
<view class="add" @click="btnadd(item)" v-else>
|
||
添加
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 未搜索到设备 -->
|
||
<view class="wei" v-else>
|
||
<image src="https://api.ccttiot.com/smartmeter/img/static/uQ4g6A27FGtF34ebOtea" mode=""></image>
|
||
<view class="sbname">
|
||
{{ lastBleIssue && lastBleIssue.title ? lastBleIssue.title : '搜索附近的设备失败' }}
|
||
</view>
|
||
<view class="sbwz">
|
||
<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>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 点击搜索 -->
|
||
<view class="btnss" v-if="showSearchButton" @click="handleSearch">
|
||
重新搜索
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 自定义名称弹框 -->
|
||
<u-popup v-model="showNameDialog" mode="center" border-radius="14" width="600rpx">
|
||
<view class="custom-name-dialog">
|
||
<view class="dialog-title">设备名称</view>
|
||
<u-input v-model="customDeviceName" placeholder="请输入设备名称" />
|
||
<view class="dialog-btns">
|
||
<view class="btn cancel" @click="showNameDialog = false">取消</view>
|
||
<view class="btn confirm" @click="confirmAddDevice">确定</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
var xBlufi = require("@/components/blufi/xBlufi.js");
|
||
export default {
|
||
data() {
|
||
return {
|
||
bgc: {
|
||
backgroundColor: "#fff",
|
||
},
|
||
active: 1,
|
||
flag: true,
|
||
devicesList: [],
|
||
newlist:[],
|
||
deviceId: '',
|
||
name: '',
|
||
mac: '',
|
||
flags: true,
|
||
userid: '',
|
||
arr: [],
|
||
jiaohuaqi: [],
|
||
getpre: [],
|
||
showNameDialog: false,
|
||
customDeviceName: '',
|
||
currentDevice: null,
|
||
searchTimer: null,
|
||
checkTimer: null,
|
||
isSearching: false,
|
||
searchTimeout: null,
|
||
throttleTimer: null,
|
||
lastSearchTime: 0,
|
||
searchInterval: 800, // 搜索节流间隔(更细致)
|
||
displayQueue: [], // 显示队列
|
||
processingQueue: false, // 是否正在处理队列
|
||
isBatchRequesting: false, // 是否有批量请求进行中
|
||
rssiMap: {}, // 记录每个 mac 的信号强度
|
||
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,
|
||
}
|
||
},
|
||
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);
|
||
});
|
||
},
|
||
// 与弹窗一致:指微信小程序标题栏右侧「⋯」路径,避免说「左上角」与真实界面不符
|
||
wechatBleAuthGuideBrief() {
|
||
return '看本页最顶部:标题栏最右侧点「⋯」→ 弹出层里点「设置」→ 再点「蓝牙」并打开,然后点下面「重新搜索」。'
|
||
}
|
||
},
|
||
// 分享到好友(会话)
|
||
onShareAppMessage: function() {
|
||
return {
|
||
title: '绿小能',
|
||
path: '/pages/index/index'
|
||
}
|
||
},
|
||
|
||
// 分享到朋友圈
|
||
onShareTimeline: function() {
|
||
return {
|
||
title: '绿小能',
|
||
query: '',
|
||
path: '/pages/index/index'
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.getmodel()
|
||
this.getinfo()
|
||
// 预热蓝牙适配器,减少首次进入时初始化失败
|
||
this.ensureBluetoothReady(false)
|
||
},
|
||
onShow() {
|
||
this.startSearch()
|
||
},
|
||
onHide() {
|
||
this.stopSearch()
|
||
},
|
||
onUnload() {
|
||
if (this.searchButtonCooldownTimer) {
|
||
clearTimeout(this.searchButtonCooldownTimer)
|
||
this.searchButtonCooldownTimer = null
|
||
}
|
||
this.stopSearch()
|
||
},
|
||
methods: {
|
||
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 })
|
||
},
|
||
// 获取用户信息
|
||
getinfo() {
|
||
this.$u.get(`/system/user/profile`).then((res) => {
|
||
if (res.code == 200) {
|
||
this.userid = res.data.userId
|
||
uni.setStorageSync('user',res.data)
|
||
uni.setStorageSync('userId',res.data.userId)
|
||
} else if (res.code == 401) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '您还未登录,是否前去登录?',
|
||
success: (r) => {
|
||
if (r.confirm) {
|
||
uni.navigateTo({
|
||
url: '/pages/login/login'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 点击添加按钮
|
||
btnadd(e) {
|
||
this.currentDevice = e;
|
||
this.customDeviceName = e.modelName || '未知设备';
|
||
this.showNameDialog = true;
|
||
},
|
||
// 确认添加设备
|
||
confirmAddDevice() {
|
||
if (!this.customDeviceName.trim()) {
|
||
uni.showToast({
|
||
title: '请输入设备名称',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
let mac = this.currentDevice.name.slice(-12);
|
||
let data = {
|
||
mac: mac,
|
||
// userId: this.userid,
|
||
pre: this.currentDevice.pre,
|
||
deviceName: this.customDeviceName
|
||
}
|
||
console.log(data,'参数');
|
||
this.$u.post(`/app/device/bindDeviceByBlueTooth`, data).then((res) => {
|
||
if (res.code == 200) {
|
||
uni.showToast({
|
||
title: '绑定成功',
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
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
|
||
// })
|
||
// }
|
||
// })
|
||
} else {
|
||
console.log(res,'报错');
|
||
uni.showToast({
|
||
title: res.msg,
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
}
|
||
})
|
||
},
|
||
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) || {
|
||
modelName: '未知型号',
|
||
picture: ''
|
||
};
|
||
},
|
||
|
||
// 开始搜索
|
||
async startSearch() {
|
||
if (this.isSearching) return
|
||
const ready = await this.ensureBluetoothReady(true)
|
||
if (!ready) {
|
||
this.flags = false
|
||
return
|
||
}
|
||
this.lastBleIssue = null
|
||
this.isSearching = true
|
||
this.jiaohuaqi = []
|
||
this.displayQueue = []
|
||
this.processingQueue = false
|
||
this.discoveredDeviceMap = {}
|
||
this.rssiMap = {}
|
||
this.arr = []
|
||
this.lastSearchTime = 0
|
||
this.discoverStartRetryCount = 0
|
||
this.flag = false
|
||
|
||
// 开始蓝牙搜索
|
||
xBlufi.listenDeviceMsgEvent(true, this.funListenDeviceMsgEvent)
|
||
this.startDiscoverWithRetry()
|
||
// 周期重启扫描,降低系统层扫描卡住导致搜不到的概率
|
||
this.scanRestartTimer = setInterval(() => {
|
||
if (!this.isSearching) return
|
||
xBlufi.notifyStartDiscoverBle({ isStart: false })
|
||
setTimeout(() => {
|
||
if (!this.isSearching) return
|
||
this.startDiscoverWithRetry()
|
||
}, 250)
|
||
}, 8000)
|
||
|
||
// 持续慢速扫描,不自动停止;仅在离开页面或手动重新搜索时停止
|
||
},
|
||
|
||
// 停止搜索
|
||
stopSearch() {
|
||
this.isSearching = false
|
||
if (this.checkTimer) {
|
||
clearInterval(this.checkTimer)
|
||
this.checkTimer = null
|
||
}
|
||
if (this.searchTimeout) {
|
||
clearTimeout(this.searchTimeout)
|
||
this.searchTimeout = null
|
||
}
|
||
if (this.scanRestartTimer) {
|
||
clearInterval(this.scanRestartTimer)
|
||
this.scanRestartTimer = null
|
||
}
|
||
if (this.throttleTimer) {
|
||
clearTimeout(this.throttleTimer)
|
||
this.throttleTimer = null
|
||
}
|
||
xBlufi.notifyStartDiscoverBle({ 'isStart': false })
|
||
xBlufi.listenDeviceMsgEvent(false, this.funListenDeviceMsgEvent)
|
||
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)
|
||
}
|
||
|
||
// 延迟处理下一个设备
|
||
setTimeout(() => {
|
||
this.processingQueue = false
|
||
this.processDisplayQueue()
|
||
}, 700) // 每个设备显示间隔700ms,慢速渐进展示
|
||
},
|
||
|
||
// 添加设备到显示队列
|
||
addToDisplayQueue(device) {
|
||
this.displayQueue.push(device)
|
||
if (!this.processingQueue) {
|
||
this.processDisplayQueue()
|
||
}
|
||
},
|
||
// 扫描到设备后先直接展示,再由接口补充绑定状态
|
||
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
|
||
})
|
||
},
|
||
|
||
// 更新设备列表(只展示接口返回的数据,累加去重)
|
||
updateDeviceList(existList) {
|
||
if (!Array.isArray(existList) || existList.length === 0) return
|
||
|
||
existList.forEach(item => {
|
||
// 后端返回的设备信息中 mac 为唯一标识
|
||
const mac = item.mac
|
||
if (!mac) return
|
||
|
||
// 计算信号强度:优先取本地扫描到的 rssiMap
|
||
const ssid = this.rssiMap[mac] != null ? this.rssiMap[mac] : undefined
|
||
|
||
// 设备是否已绑定
|
||
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)
|
||
}
|
||
})
|
||
},
|
||
|
||
// 获取附近蓝牙设备列表
|
||
funListenDeviceMsgEvent: function(options) {
|
||
switch (options.type) {
|
||
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS:
|
||
if (options.result) {
|
||
const now = Date.now()
|
||
if (now - this.lastSearchTime < this.searchInterval) {
|
||
return
|
||
}
|
||
this.lastSearchTime = now
|
||
|
||
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
|
||
}
|
||
} 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)
|
||
}
|
||
})
|
||
this.arr = Object.keys(this.discoveredDeviceMap).map(mac => this.discoveredDeviceMap[mac])
|
||
if (hasNewDevice) {
|
||
this.flags = true
|
||
}
|
||
|
||
// 使用防抖 + 请求中的标记,避免一次性触发很多请求
|
||
if (this.throttleTimer) {
|
||
clearTimeout(this.throttleTimer)
|
||
}
|
||
// 500ms 内多次回调只发一个请求
|
||
this.throttleTimer = setTimeout(() => {
|
||
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)
|
||
}
|
||
break
|
||
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS_START:
|
||
if (!options.result) {
|
||
// 扫描启动失败自动重试,减少偶发初始化失败
|
||
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
|
||
return
|
||
}
|
||
this.discoverStartRetryCount = 0
|
||
break
|
||
}
|
||
},
|
||
|
||
btnss() {
|
||
// 每次点击后隐藏 6 秒,避免连续重复点击
|
||
this.showSearchButton = false
|
||
if (this.searchButtonCooldownTimer) {
|
||
clearTimeout(this.searchButtonCooldownTimer)
|
||
}
|
||
this.searchButtonCooldownTimer = setTimeout(() => {
|
||
this.showSearchButton = true
|
||
this.searchButtonCooldownTimer = null
|
||
}, 6000)
|
||
this.stopSearch()
|
||
this.jiaohuaqi = []
|
||
this.displayQueue = []
|
||
this.processingQueue = false
|
||
this.discoveredDeviceMap = {}
|
||
this.rssiMap = {}
|
||
this.startSearch()
|
||
},
|
||
// 处理搜索按钮点击
|
||
handleSearch() {
|
||
this.btnss()
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less">
|
||
::v-deep .u-input__input{
|
||
border: 1px solid #ccc;
|
||
border-radius: 10rpx;
|
||
padding-left: 10rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
::v-deep .u-title {
|
||
margin-bottom: 22rpx;
|
||
}
|
||
|
||
::v-deep .uicon-nav-back {
|
||
margin-bottom: 22rpx;
|
||
}
|
||
|
||
.page {
|
||
padding-bottom: 300rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.wei {
|
||
text-align: center;
|
||
|
||
image {
|
||
width: 380rpx;
|
||
height: 394rpx;
|
||
}
|
||
|
||
.sbname {
|
||
font-size: 40rpx;
|
||
color: #3D3D3D;
|
||
margin-top: 80rpx;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.sbwz {
|
||
font-size: 28rpx;
|
||
color: #737B80;
|
||
margin-top: 24rpx;
|
||
width: 100%;
|
||
text-align: left;
|
||
padding: 0 40rpx;
|
||
box-sizing: border-box;
|
||
line-height: 1.55;
|
||
}
|
||
}
|
||
|
||
.btnss {
|
||
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;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.list {
|
||
width: 100%;
|
||
border-radius: 20rpx;
|
||
margin: auto;
|
||
margin-top: 72rpx;
|
||
will-change: transform; // 优化动画性能
|
||
|
||
.list_item {
|
||
margin-top: 18rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 152rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0rpx 10rpx 64rpx 0rpx rgba(0, 0, 0, 0.08);
|
||
padding: 18rpx 30rpx;
|
||
box-sizing: border-box;
|
||
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);
|
||
}
|
||
}
|
||
|
||
image {
|
||
width: 94rpx;
|
||
height: 94rpx;
|
||
}
|
||
|
||
.cen {
|
||
.name {
|
||
font-size: 32rpx;
|
||
color: #50565A;
|
||
}
|
||
|
||
.devmac {
|
||
font-size: 24rpx;
|
||
color: #BDBCBC;
|
||
margin-top: 6rpx;
|
||
}
|
||
}
|
||
|
||
.add {
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
page {
|
||
width: 100%;
|
||
padding: 20rpx 64rpx;
|
||
box-sizing: border-box;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.topone {
|
||
font-size: 36rpx;
|
||
color: #3D3D3D;
|
||
display: flex;
|
||
|
||
image {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
}
|
||
}
|
||
|
||
.toptwo {
|
||
font-size: 28rpx;
|
||
color: #737B7F;
|
||
margin-top: 14rpx;
|
||
width: 100%;
|
||
padding-left: 48rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |