Files
bodyWeight/miniprogram/pages/connectedDevice/connectedDevice.ts
T

424 lines
14 KiB
TypeScript
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.
import { leFuService } from '../../lefu/index'
import type { ScannedDevice } from '../../lefu/index'
import type { RawDevice } from '../../lefu/types'
import { post } from '../../utils/request/index'
import type { UserInfo } from '../../api/index'
/** 绑定设备 SN 到用户信息(运维模式走 ops/addDevice,普通模式走 v6/bindDevice */
const bindDevice = (data: { sn: string; idCard: string; connectDeviceInfo?: string }) =>
post(
getApp<IAppOption>().globalData.operationsEngineer
? 'weighingScale/ops/addDevice'
: 'weighingScale/v6/bindDevice',
data,
{ loading: false },
)
/** 渲染用的设备项(RawDevice 含复杂对象,不进 setData */
interface DeviceItem {
deviceId: string
name: string
connected: boolean
}
type DeviceStatus = 'idle' | 'searching' | 'found' | 'empty'
/** 扫描到的原始设备缓存 */
let scannedDevices: ScannedDevice[] = []
/** 扫描 / 断开回调的取消订阅句柄 */
let unsubDevicesList: (() => void) | null = null
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
/** 连接流程的清理句柄 */
let connectCleanup: {
timer: number | null
unsubState: (() => void) | null
unsubConnect: (() => void) | null
unsubInfo: (() => void) | null
} | null = null
/** 一次性防重入标记 */
let modalLocked = false
let autoConnectTried = false
/** 进入页面时是否已连接(返回时恢复被扫描打断的连接) */
let enteredConnected = false
Page({
data: {
status: 'idle' as DeviceStatus,
deviceList: [] as DeviceItem[],
},
onLoad() {
enteredConnected = leFuService.isConnected
},
onShow() {
// 从其他页面返回时同步连接状态
if (leFuService.isConnected) {
this._markConnected()
} else {
this._markAllDisconnected()
}
},
onHide() {
leFuService.stopScan()
// 搜索中切后台:停止扫描后重置状态,避免回来显示「搜索中」假状态
if (this.data.status === 'searching') {
this.setData({ status: 'idle' })
}
},
onUnload() {
leFuService.stopScan()
this._clearSubscriptions()
this._clearConnectCleanup()
// 进入前已连接、扫描打断连接后返回:恢复原连接
if (enteredConnected) {
leFuService.autoConnect()
}
},
/** 点击「开始 / 重新搜索」 */
async onStartSearch() {
if (modalLocked) return
scannedDevices = []
autoConnectTried = false
this.setData({ status: 'searching', deviceList: [] })
await this._requestPermissionAndScan()
},
/** 点击设备卡片:已连接跳配网,未连接走连接流程 */
onTapCard(e: WechatMiniprogram.TouchEvent) {
const connected = e.currentTarget.dataset.connected as boolean
if (connected) {
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
return
}
this.onConnect(e)
},
/** 手动连接指定设备 */
onConnect(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index as number
this._connectByIndex(index, false)
},
/** 断开当前连接 */
onDisconnect() {
this._markAllDisconnected()
leFuService.disconnect()
},
onOpenHelp() {
wx.openAppAuthorizeSetting()
},
/** 调用接口把设备 SN 绑定到已保存的用户信息,成功后回调 */
_bindDevice(sn: string, onSuccess?: () => void) {
const userInfo = wx.getStorageSync('userInfo') as UserInfo | null
const idCard = userInfo?.idCard
const connectDeviceInfo = leFuService.lastRawDevice ? JSON.stringify(leFuService.lastRawDevice) : ''
if (!idCard) {
onSuccess?.()
return
}
bindDevice({ sn, idCard, connectDeviceInfo })
.then((res) => {
console.log('[connectedDevice] bindDevice 成功:', res)
onSuccess?.()
})
.catch((err) => {
console.error('[connectedDevice] bindDevice 失败:', err)
this._resetAfterBindFail(err)
})
},
/** bindDevice 失败后的清理重置:断开连接、清缓存、重置页面状态 */
_resetAfterBindFail(err?: any) {
leFuService.disconnect()
wx.removeStorageSync('connectDeviceInfo')
scannedDevices = []
this.setData({ status: 'idle', deviceList: [] })
wx.showToast({ title: err?.msg || err?.message || '绑定失败,请重试', icon: 'none' })
},
/** 将所有卡片置为未连接 */
_markAllDisconnected() {
this.setData({
deviceList: this.data.deviceList.map(item => ({ ...item, connected: false })),
})
},
/** 获取当前已连接设备的 deviceId(未连接返回空字符串) */
_getConnectedId(): string {
if (!leFuService.isConnected) return ''
const cached = wx.getStorageSync('connectDeviceInfo') as Record<string, any> | null
return cached?.deviceId || ''
},
/** 连接成功后把对应设备卡片标记为已连接(覆盖手动连接与自动重连) */
_markConnected() {
const id = this._getConnectedId()
if (!id) return
this.setData({
deviceList: this.data.deviceList.map(item => ({ ...item, connected: item.deviceId === id })),
})
},
_clearSubscriptions() {
unsubDevicesList?.()
unsubDisconnected?.()
unsubDeviceConnect?.()
unsubDevicesList = null
unsubDisconnected = null
unsubDeviceConnect = null
},
_clearConnectCleanup() {
if (!connectCleanup) return
if (connectCleanup.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup.unsubState?.()
connectCleanup.unsubConnect?.()
connectCleanup.unsubInfo?.()
connectCleanup = null
},
/** 检查系统蓝牙开关 */
_checkBluetoothEnabled(): boolean {
try {
if (wx.getSystemSetting().bluetoothEnabled === false) {
this._showBluetoothOff()
return false
}
return true
} catch {
// 基础库不支持 wx.getSystemSetting 时跳过,交由 openBluetoothAdapter 报错兜底
return true
}
},
/** 检查系统定位开关 */
_checkLocationService(): boolean {
try {
if (wx.getSystemSetting().locationEnabled === false) {
this._showLocationOff()
return false
}
return true
} catch {
return true
}
},
/** 申请微信“附近设备/位置”授权 */
async _ensureLocationAuth(): Promise<boolean> {
try {
const { authSetting } = await wx.getSetting()
const setting = authSetting as Record<string, boolean | undefined>
if (!setting['scope.userLocation']) {
await wx.authorize({ scope: 'scope.userLocation' })
}
return true
} catch {
this._showLocationGuide()
return false
}
},
/** 编排:蓝牙开关 → 定位开关 → 微信授权 → 打开蓝牙并扫描 */
async _requestPermissionAndScan() {
if (!this._checkBluetoothEnabled()) return
if (!this._checkLocationService()) return
if (!(await this._ensureLocationAuth())) return
await this._openAdapterAndScan()
},
async _openAdapterAndScan() {
try {
await wx.openBluetoothAdapter()
} catch (err: any) {
console.error('[connectedDevice] openBluetoothAdapter 失败:', err)
this.setData({ status: 'idle' })
this._handleAdapterError()
return
}
this._startScan()
},
/** 区分蓝牙/定位服务开关,给出准确提示 */
_handleAdapterError() {
try {
const setting = wx.getSystemSetting()
if (!setting.bluetoothEnabled) {
this._showBluetoothOff()
return
}
if (!setting.locationEnabled) {
this._showLocationOff()
return
}
} catch {
// 基础库不支持 wx.getSystemSetting 时,退化为通用提示
}
wx.showToast({ title: '蓝牙初始化失败,请重试', icon: 'none' })
},
_startScan() {
this._clearSubscriptions()
unsubDevicesList = leFuService.onDevicesList((devices) => {
scannedDevices = devices
if (!devices.length) {
this.setData({ status: 'empty', deviceList: [] })
return
}
const connectedId = this._getConnectedId()
this.setData({
status: 'found',
deviceList: devices.map(d => ({
deviceId: d.raw.deviceId || '',
name: d.name,
connected: !!connectedId && d.raw.deviceId === connectedId,
})),
})
this._tryAutoConnect()
})
unsubDisconnected = leFuService.onDisconnected(() => {
this._markAllDisconnected()
})
unsubDeviceConnect = leFuService.onDeviceConnect(() => {
this._markConnected()
})
leFuService.startScan()
},
/** 命中缓存设备时自动连接一次 */
_tryAutoConnect() {
if (autoConnectTried) return
// 已连接时不自动重连,避免触发断开重连
if (leFuService.isConnected) return
const cached = wx.getStorageSync('connectDeviceInfo') as RawDevice | null
if (!cached?.deviceId) return
const index = scannedDevices.findIndex(d => d.raw.deviceId === cached.deviceId)
if (index < 0) return
autoConnectTried = true
this._connectByIndex(index, true)
},
/** 统一连接流程(手动 / 自动共用),手动连接成功后跳配网 */
_connectByIndex(index: number, isAuto: boolean) {
const scanned = scannedDevices[index]
if (!scanned) return
// 连接中:防止重复点击导致连接流程被重复触发
if (connectCleanup) return
wx.showLoading({ title: '连接中...', mask: true })
leFuService.stopScan()
leFuService.connect(scanned.raw)
this._clearConnectCleanup()
connectCleanup = { timer: null, unsubState: null, unsubConnect: null, unsubInfo: null }
const finish = () => {
if (connectCleanup?.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup?.unsubState?.()
connectCleanup?.unsubConnect?.()
connectCleanup?.unsubInfo?.()
connectCleanup = null
wx.hideLoading()
}
// 连接成功 + SN 到位两个信号都满足才调接口,各自事件独立触发、顺序不定
let connected = false
let sn: string | undefined
const tryBind = () => {
if (!connected || !sn) return
finish()
// 手动/自动连接:绑定设备成功后缓存设备并跳配网
this._bindDevice(sn, () => {
wx.setStorageSync('connectDeviceInfo', { ...scanned.raw, scaleDeviceName: scanned.raw.name })
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
})
}
connectCleanup.timer = setTimeout(() => {
finish()
if (connected) {
// 已连上但一直没拿到 SN:断开并重置,避免设备残留占用连接
leFuService.disconnect()
wx.removeStorageSync('connectDeviceInfo')
scannedDevices = []
this.setData({ status: 'idle', deviceList: [] })
wx.showToast({ title: '设备信息异常,请重试', icon: 'none' })
} else {
wx.showToast({ title: '连接超时', icon: 'none' })
}
}, 15000)
connectCleanup.unsubState = leFuService.onConnectState((state) => {
if (state === leFuService.BLUE_STATE.CONNECTFAILED) {
finish()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
}
// UNAVAILABLE 不在此处理:connect 过程中 clearProtocol 会触发一次 UNAVAILABLE 抖动,
// 后面紧跟 CONNECTSUCCESS,误判会打断连接流程并误报「蓝牙关闭」。
// 真正「连接后关闭蓝牙」由 onDisconnected 回调处理。
})
connectCleanup.unsubConnect = leFuService.onDeviceConnect(() => {
connected = true
sn = leFuService.deviceInfo?.serialNumber
tryBind()
})
connectCleanup.unsubInfo = leFuService.onDeviceInfo((info) => {
sn = info?.serialNumber
tryBind()
})
},
_showBluetoothOff() {
wx.showModal({
title: '蓝牙未开启',
content: '请先开启手机蓝牙后重试',
showCancel: false,
confirmText: '知道了',
success: () => this.setData({ status: 'idle' }),
})
},
_showLocationOff() {
wx.showModal({
title: '定位服务未开启',
content: '安卓手机搜索蓝牙设备需开启定位服务,请前往系统设置开启后重试。',
showCancel: false,
confirmText: '知道了',
success: () => this.setData({ status: 'idle' }),
})
},
_showLocationGuide() {
modalLocked = true
wx.showModal({
title: '需要授权',
content: '小程序需要位置权限才能搜索蓝牙设备,请前往设置开启。',
confirmText: '去开启',
cancelText: '取消',
success: (res) => {
modalLocked = false
this.setData({ status: 'idle' })
if (res.confirm) {
wx.openSetting({
success: () => this.onStartSearch(),
fail: () => this.setData({ status: 'idle' }),
})
}
},
fail: () => {
modalLocked = false
this.setData({ status: 'idle' })
},
})
},
})