feat(userManagement): 接入用户同步到设备,恢复清空设备用户能力
- service 恢复 clearDeviceMembers/syncMembersToDevice,操作中断开立即失败 - syncMembersToDevice 只负责逐个下发,参数由业务层组装 - _doReconnect 增加蓝牙不可用判断 - userManagement 接入真实同步(连接状态实时同步 + 进度弹框) - equipment 页增加用户管理入口 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
db865748f2
commit
9a81092c99
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
"pages/connectedDevice/connectedDevice",
|
|
||||||
"pages/login/login",
|
"pages/login/login",
|
||||||
"pages/supplementPersonal/supplementPersonal",
|
"pages/supplementPersonal/supplementPersonal",
|
||||||
|
"pages/connectedDevice/connectedDevice",
|
||||||
"pages/connectedWifi/connectedWifi",
|
"pages/connectedWifi/connectedWifi",
|
||||||
"pages/home/home",
|
"pages/home/home",
|
||||||
"pages/detailedReport/detailedReport",
|
"pages/detailedReport/detailedReport",
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export type {
|
|||||||
LeFuConfig,
|
LeFuConfig,
|
||||||
DeviceSetting,
|
DeviceSetting,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
|
SyncUserData,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
LockData,
|
LockData,
|
||||||
LeFuConfig,
|
LeFuConfig,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
|
SyncUserData,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
/** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */
|
/** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */
|
||||||
@@ -358,6 +359,84 @@ class LeFuService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 用户同步 ─────────────────────────────
|
||||||
|
/**
|
||||||
|
* 清空设备内的用户数据
|
||||||
|
* 下发清空指令,让设备清除已录入的所有成员。
|
||||||
|
*/
|
||||||
|
clearDeviceMembers(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||||
|
|
||||||
|
let settled = false
|
||||||
|
const settle = (err: Error | null) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
this._onDisconnectedCbs.delete(onDisconnect)
|
||||||
|
err ? reject(err) : resolve()
|
||||||
|
}
|
||||||
|
// 操作中设备断开:立即失败,不等超时
|
||||||
|
const onDisconnect = () => settle(new Error('设备已断开'))
|
||||||
|
this._onDisconnectedCbs.add(onDisconnect)
|
||||||
|
const timer = setTimeout(() => settle(new Error('请求超时')), 15000)
|
||||||
|
|
||||||
|
this._activeProtocol.codeClearDeviceData('01', (res: number) => {
|
||||||
|
settle(res === 0 ? null : new Error('请求失败'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步成员到设备(只负责逐个下发)
|
||||||
|
* 参数列表由调用方组装好,本方法不做字段转换,onProgress 回调已完成的用户数。
|
||||||
|
*/
|
||||||
|
syncMembersToDevice(list: SyncUserData[], onProgress?: (current: number, total: number) => void): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||||
|
if (!list.length) { reject(new Error('无用户可同步')); return }
|
||||||
|
|
||||||
|
let settled = false
|
||||||
|
let index = 0
|
||||||
|
let syncTimer: number | null = null
|
||||||
|
|
||||||
|
const settle = (err: Error | null) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||||
|
this._onDisconnectedCbs.delete(onDisconnect)
|
||||||
|
err ? reject(err) : resolve()
|
||||||
|
}
|
||||||
|
// 操作中设备断开:立即失败,不等超时
|
||||||
|
const onDisconnect = () => settle(new Error('设备已断开'))
|
||||||
|
this._onDisconnectedCbs.add(onDisconnect)
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||||
|
syncTimer = setTimeout(() => settle(new Error('请求超时')), 15000)
|
||||||
|
}
|
||||||
|
const syncNext = () => {
|
||||||
|
if (settled) return
|
||||||
|
if (index >= list.length) {
|
||||||
|
settle(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resetTimer()
|
||||||
|
this._activeProtocol.dataSyncUserInfo(list[index], (res: number) => {
|
||||||
|
if (settled) return
|
||||||
|
if (res !== 0) {
|
||||||
|
settle(new Error('请求失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
onProgress?.(index, list.length)
|
||||||
|
syncNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
syncNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 私有:保活与重连 ───────────────────────
|
// ─── 私有:保活与重连 ───────────────────────
|
||||||
/** 启动保活心跳:文档建议每 10s 发送一次 keepAlive,防止设备主动断开 */
|
/** 启动保活心跳:文档建议每 10s 发送一次 keepAlive,防止设备主动断开 */
|
||||||
private _startKeepAlive(): void {
|
private _startKeepAlive(): void {
|
||||||
@@ -400,6 +479,12 @@ class LeFuService {
|
|||||||
console.log(TAG, `_doReconnect 第 ${this._reconnectCount} 次`)
|
console.log(TAG, `_doReconnect 第 ${this._reconnectCount} 次`)
|
||||||
this._stopReconnectTimer()
|
this._stopReconnectTimer()
|
||||||
this._reconnectTimer = setTimeout(() => {
|
this._reconnectTimer = setTimeout(() => {
|
||||||
|
// 延时期间蓝牙可能已不可用,重连前再判断一次
|
||||||
|
if (this._connectState === plugin.BLUE_STATE.UNAVAILABLE) {
|
||||||
|
console.warn(TAG, '蓝牙不可用,放弃重连')
|
||||||
|
this._onDisconnectedCbs.forEach(cb => cb())
|
||||||
|
return
|
||||||
|
}
|
||||||
plugin.Blue.clearProtocol()
|
plugin.Blue.clearProtocol()
|
||||||
plugin.Blue.createBLEConnection(this._lastRawDevice!)
|
plugin.Blue.createBLEConnection(this._lastRawDevice!)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|||||||
@@ -88,3 +88,27 @@ export interface DeviceInfo {
|
|||||||
serialNumber: string
|
serialNumber: string
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 下发到设备的单个用户数据(dataSyncUserInfo 参数,业务层组装后传入) */
|
||||||
|
export interface SyncUserData {
|
||||||
|
/** 主用户 ID */
|
||||||
|
userID: string
|
||||||
|
/** 用户名 */
|
||||||
|
userName: string
|
||||||
|
/** 成员 ID,主用户为空 */
|
||||||
|
memberID: string
|
||||||
|
/** 年龄 */
|
||||||
|
age: number
|
||||||
|
/** 性别:1=男,0=女 */
|
||||||
|
gender: number
|
||||||
|
/** 身高(cm) */
|
||||||
|
height: number
|
||||||
|
/** 是否运动员模式 */
|
||||||
|
isAthleteMode: number
|
||||||
|
/** 设备内索引 */
|
||||||
|
deviceHeaderIndex: number
|
||||||
|
currentWeight: string
|
||||||
|
targetWeight: string
|
||||||
|
idealWeight: string
|
||||||
|
recentData: any[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@
|
|||||||
<view class="ops-btn ops-btn--red" bind:tap="onUnpairAll">取消全部配对</view>
|
<view class="ops-btn ops-btn--red" bind:tap="onUnpairAll">取消全部配对</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="device-action-btn" bind:tap="onTapUser">
|
||||||
|
<view>
|
||||||
|
<image src="/images/equipment/users.png"/>
|
||||||
|
</view>
|
||||||
|
<view>用户</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 设备列表 -->
|
<!-- 设备列表 -->
|
||||||
<block wx:if="{{ devices && devices.length }}">
|
<block wx:if="{{ devices && devices.length }}">
|
||||||
<block wx:for="{{ devices }}" wx:key="id">
|
<block wx:for="{{ devices }}" wx:key="id">
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
// pages/userManagement/userManagement.ts
|
import { leFuService } from '../../lefu/index'
|
||||||
|
import type { SyncUserData } from '../../lefu/index'
|
||||||
|
|
||||||
|
/** 连接状态订阅句柄 */
|
||||||
|
let unsubDeviceConnect: (() => void) | null = null
|
||||||
|
let unsubDisconnected: (() => void) | null = null
|
||||||
|
/** 同步完成关闭弹框定时器 */
|
||||||
|
let syncCloseTimer: number | null = null
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
device: {
|
device: {
|
||||||
name: '家庭蓝牙体脂秤 S1',
|
name: '智能体重秤',
|
||||||
connected: true,
|
connected: false,
|
||||||
model: 'BS-A100',
|
model: '',
|
||||||
serial: 'BL2024B00321'
|
serial: ''
|
||||||
},
|
},
|
||||||
memberCount: 5,
|
memberCount: 5,
|
||||||
hasData: true,
|
hasData: true,
|
||||||
@@ -38,12 +46,110 @@ Page({
|
|||||||
syncStatus: ''
|
syncStatus: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this._updateDeviceInfo()
|
||||||
|
this._subscribeDeviceState()
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload() {
|
||||||
|
unsubDeviceConnect?.()
|
||||||
|
unsubDisconnected?.()
|
||||||
|
unsubDeviceConnect = null
|
||||||
|
unsubDisconnected = null
|
||||||
|
if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null }
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从 service 读取设备信息 + 连接状态 */
|
||||||
|
_updateDeviceInfo() {
|
||||||
|
const info = leFuService.deviceInfo
|
||||||
|
this.setData({
|
||||||
|
'device.name': info?.modelNumber || '智能体重秤',
|
||||||
|
'device.model': info?.modelNumber || '',
|
||||||
|
'device.serial': info?.serialNumber || '',
|
||||||
|
'device.connected': leFuService.isConnected,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 订阅连接状态,实时同步设备卡 */
|
||||||
|
_subscribeDeviceState() {
|
||||||
|
unsubDeviceConnect?.()
|
||||||
|
unsubDisconnected?.()
|
||||||
|
unsubDeviceConnect = leFuService.onDeviceConnect(() => {
|
||||||
|
this._updateDeviceInfo()
|
||||||
|
})
|
||||||
|
unsubDisconnected = leFuService.onDisconnected(() => {
|
||||||
|
this.setData({ 'device.connected': false })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 校验设备连接状态:未连接则同步状态 + 提示 */
|
||||||
|
_ensureConnected(): boolean {
|
||||||
|
if (leFuService.isConnected) return true
|
||||||
|
this.setData({ 'device.connected': false })
|
||||||
|
wx.showToast({ title: '请先连接设备', icon: 'none' })
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
/** 同步用户至设备 */
|
/** 同步用户至设备 */
|
||||||
onTapSync() {
|
onTapSync() {
|
||||||
this.setData({ syncShow: true, syncPercent: 32, syncStatus: '正在同步信息,请勿离开...' })
|
if (!this._ensureConnected()) return
|
||||||
setTimeout(() => {
|
|
||||||
this.setData({ syncPercent: 100, syncStatus: '同步完成' })
|
// 组装所有用户(self 第一个,作为主用户),性别:男=1 女=0
|
||||||
}, 2000)
|
const allUsers = [
|
||||||
|
{
|
||||||
|
id: 'self', name: this.data.self.name,
|
||||||
|
gender: this.data.self.gender === '男' ? 1 : 0,
|
||||||
|
age: this.data.self.age, height: this.data.self.height,
|
||||||
|
},
|
||||||
|
...this.data.groups.flatMap(g => g.members.map(m => ({
|
||||||
|
id: String(m.id), name: m.name,
|
||||||
|
gender: m.gender === '男' ? 1 : 0, age: m.age, height: m.height,
|
||||||
|
}))),
|
||||||
|
]
|
||||||
|
const mainUser = allUsers[0]
|
||||||
|
// 组装下发参数(SyncUserData),第一个为主用户(memberID 为空)
|
||||||
|
const syncDataList: SyncUserData[] = allUsers.map((user, index) => ({
|
||||||
|
userID: mainUser.id,
|
||||||
|
userName: user.name,
|
||||||
|
memberID: index === 0 ? '' : user.id,
|
||||||
|
age: user.age,
|
||||||
|
gender: user.gender,
|
||||||
|
height: user.height,
|
||||||
|
isAthleteMode: 0,
|
||||||
|
deviceHeaderIndex: index,
|
||||||
|
currentWeight: '',
|
||||||
|
targetWeight: '',
|
||||||
|
idealWeight: '',
|
||||||
|
recentData: [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 阶段1:清空设备用户,进度 0% → 100%
|
||||||
|
this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在清空设备用户...' })
|
||||||
|
leFuService.clearDeviceMembers()
|
||||||
|
.then(() => {
|
||||||
|
this.setData({ syncPercent: 100, syncStatus: '清空完成' })
|
||||||
|
// 阶段2:下发用户,进度从 0% 重新开始
|
||||||
|
this.setData({ syncPercent: 0, syncStatus: '正在同步信息,请勿离开...' })
|
||||||
|
return leFuService.syncMembersToDevice(syncDataList, (current, total) => {
|
||||||
|
this.setData({
|
||||||
|
syncPercent: Math.round((current / total) * 100),
|
||||||
|
syncStatus: `正在同步 ${current}/${total}...`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.setData({ syncPercent: 100, syncStatus: '同步完成' })
|
||||||
|
wx.showToast({ title: '用户同步成功', icon: 'success' })
|
||||||
|
if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null }
|
||||||
|
syncCloseTimer = setTimeout(() => {
|
||||||
|
syncCloseTimer = null
|
||||||
|
this.setData({ syncShow: false })
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setData({ syncShow: false })
|
||||||
|
wx.showToast({ title: err.message || '同步失败,请重试', icon: 'none' })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 添加用户 */
|
/** 添加用户 */
|
||||||
|
|||||||
Reference in New Issue
Block a user