Sprinkler-app/page_user/lanya.vue
2026-06-18 11:31:43 +08:00

1056 lines
32 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>
<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 = [
'不少机型会反复提示「蓝牙初始化失败」,常见原因包括:系统蓝牙栈卡住、微信权限未开全、安卓定位限制等,不一定是硬件损坏。',
'',
'请按顺序尝试(每完成一两步即可点「重新搜索」试一次):',
'① 手机系统设置里关闭蓝牙,等 23 秒再打开',
'② 微信:标题栏右侧「⋯」→「设置」→「蓝牙」→ 允许本小程序',
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>