From 9a81092c995c881b11e12b40b300a9ca205664ad Mon Sep 17 00:00:00 2001 From: 17792275749 <812100002@qq.com> Date: Fri, 21 Aug 2026 11:08:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(userManagement):=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8C=E6=AD=A5=E5=88=B0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=EF=BC=8C=E6=81=A2=E5=A4=8D=E6=B8=85=E7=A9=BA=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - service 恢复 clearDeviceMembers/syncMembersToDevice,操作中断开立即失败 - syncMembersToDevice 只负责逐个下发,参数由业务层组装 - _doReconnect 增加蓝牙不可用判断 - userManagement 接入真实同步(连接状态实时同步 + 进度弹框) - equipment 页增加用户管理入口 Co-Authored-By: Claude Opus 4.7 --- miniprogram/app.json | 2 +- miniprogram/lefu/index.ts | 1 + miniprogram/lefu/service.ts | 85 ++++++++++++ miniprogram/lefu/types.ts | 24 ++++ miniprogram/pages/equipment/equipment.wxml | 7 + .../pages/userManagement/userManagement.ts | 124 ++++++++++++++++-- 6 files changed, 233 insertions(+), 10 deletions(-) diff --git a/miniprogram/app.json b/miniprogram/app.json index e1b8129..99c2c69 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -1,8 +1,8 @@ { "pages": [ - "pages/connectedDevice/connectedDevice", "pages/login/login", "pages/supplementPersonal/supplementPersonal", + "pages/connectedDevice/connectedDevice", "pages/connectedWifi/connectedWifi", "pages/home/home", "pages/detailedReport/detailedReport", diff --git a/miniprogram/lefu/index.ts b/miniprogram/lefu/index.ts index 86df503..3097e2d 100644 --- a/miniprogram/lefu/index.ts +++ b/miniprogram/lefu/index.ts @@ -7,4 +7,5 @@ export type { LeFuConfig, DeviceSetting, DeviceInfo, + SyncUserData, } from './types' diff --git a/miniprogram/lefu/service.ts b/miniprogram/lefu/service.ts index 10428be..36e442b 100644 --- a/miniprogram/lefu/service.ts +++ b/miniprogram/lefu/service.ts @@ -9,6 +9,7 @@ import type { LockData, LeFuConfig, DeviceInfo, + SyncUserData, } from './types' /** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */ @@ -358,6 +359,84 @@ class LeFuService { }) } + // ─── 用户同步 ───────────────────────────── + /** + * 清空设备内的用户数据 + * 下发清空指令,让设备清除已录入的所有成员。 + */ + clearDeviceMembers(): Promise { + 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 { + 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,防止设备主动断开 */ private _startKeepAlive(): void { @@ -400,6 +479,12 @@ class LeFuService { console.log(TAG, `_doReconnect 第 ${this._reconnectCount} 次`) this._stopReconnectTimer() this._reconnectTimer = setTimeout(() => { + // 延时期间蓝牙可能已不可用,重连前再判断一次 + if (this._connectState === plugin.BLUE_STATE.UNAVAILABLE) { + console.warn(TAG, '蓝牙不可用,放弃重连') + this._onDisconnectedCbs.forEach(cb => cb()) + return + } plugin.Blue.clearProtocol() plugin.Blue.createBLEConnection(this._lastRawDevice!) }, 2000) diff --git a/miniprogram/lefu/types.ts b/miniprogram/lefu/types.ts index 7f0caf5..be03c21 100644 --- a/miniprogram/lefu/types.ts +++ b/miniprogram/lefu/types.ts @@ -88,3 +88,27 @@ export interface DeviceInfo { serialNumber: string [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[] +} diff --git a/miniprogram/pages/equipment/equipment.wxml b/miniprogram/pages/equipment/equipment.wxml index 836b8d5..325f80b 100644 --- a/miniprogram/pages/equipment/equipment.wxml +++ b/miniprogram/pages/equipment/equipment.wxml @@ -21,6 +21,13 @@ 取消全部配对 + + + + + 用户 + + diff --git a/miniprogram/pages/userManagement/userManagement.ts b/miniprogram/pages/userManagement/userManagement.ts index 0c06efa..f4d0544 100644 --- a/miniprogram/pages/userManagement/userManagement.ts +++ b/miniprogram/pages/userManagement/userManagement.ts @@ -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({ data: { device: { - name: '家庭蓝牙体脂秤 S1', - connected: true, - model: 'BS-A100', - serial: 'BL2024B00321' + name: '智能体重秤', + connected: false, + model: '', + serial: '' }, memberCount: 5, hasData: true, @@ -38,12 +46,110 @@ Page({ 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() { - this.setData({ syncShow: true, syncPercent: 32, syncStatus: '正在同步信息,请勿离开...' }) - setTimeout(() => { - this.setData({ syncPercent: 100, syncStatus: '同步完成' }) - }, 2000) + if (!this._ensureConnected()) return + + // 组装所有用户(self 第一个,作为主用户),性别:男=1 女=0 + 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' }) + }) }, /** 添加用户 */