diff --git a/miniprogram/pages/equipment/equipment.less b/miniprogram/pages/equipment/equipment.less index c4e4a1f..7dabe06 100644 --- a/miniprogram/pages/equipment/equipment.less +++ b/miniprogram/pages/equipment/equipment.less @@ -226,6 +226,9 @@ height: 26rpx; font-size: 26rpx; line-height: 26rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } } diff --git a/miniprogram/pages/equipment/equipment.ts b/miniprogram/pages/equipment/equipment.ts index 9476aa0..2840239 100644 --- a/miniprogram/pages/equipment/equipment.ts +++ b/miniprogram/pages/equipment/equipment.ts @@ -33,6 +33,7 @@ interface DeviceItem { let unsubDevicesList: (() => void) | null = null let unsubConnect: (() => void) | null = null +let unsubDisconnected: (() => void) | null = null let scanTimer: number | null = null Page({ @@ -45,6 +46,34 @@ Page({ onShow() { this.loadDevices() + this._subscribeDisconnected() + }, + + onHide() { + leFuService.stopScan() + unsubDisconnected?.() + unsubDisconnected = null + }, + + onUnload() { + leFuService.stopScan() + unsubDisconnected?.() + unsubDisconnected = null + unsubDevicesList?.() + unsubDevicesList = null + unsubConnect?.() + unsubConnect = null + if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null } + }, + + /** 订阅断开事件:设备长时间未操作或意外断开时,同步页面状态为未连接 */ + _subscribeDisconnected() { + unsubDisconnected?.() + unsubDisconnected = leFuService.onDisconnected(() => { + this.setData({ + devices: this.data.devices.map(d => ({ ...d, status: false })), + }) + }) }, /** 拉取体重设备列表 */ @@ -182,8 +211,11 @@ Page({ }, /** 用户管理 */ - onTapUser() { - wx.navigateTo({ url: '/pages/userManagement/userManagement' }) + onTapUser(e: WechatMiniprogram.TouchEvent) { + const item = e.currentTarget.dataset.item as DeviceItem + wx.navigateTo({ + url: `/pages/userManagement/userManagement?sn=${encodeURIComponent(item.sn)}&deviceId=${encodeURIComponent(item.deviceId)}&name=${encodeURIComponent(item.name)}&status=${item.status}&connectDeviceInfo=${encodeURIComponent(item.connectDeviceInfo)}`, + }) }, onTapAdd() { diff --git a/miniprogram/pages/equipment/equipment.wxml b/miniprogram/pages/equipment/equipment.wxml index 56d93d4..edf6c27 100644 --- a/miniprogram/pages/equipment/equipment.wxml +++ b/miniprogram/pages/equipment/equipment.wxml @@ -78,7 +78,7 @@ 设备连接 - + diff --git a/miniprogram/pages/userManagement/userManagement.less b/miniprogram/pages/userManagement/userManagement.less index cdbb72a..8dc5b7b 100644 --- a/miniprogram/pages/userManagement/userManagement.less +++ b/miniprogram/pages/userManagement/userManagement.less @@ -13,7 +13,7 @@ .device-card { width: 100%; - padding: 30rpx; + padding: 30rpx 24rpx; display: flex; border-radius: 24rpx; background-color: #FFFFFF; @@ -24,9 +24,8 @@ width: 88rpx; height: 88rpx; overflow: hidden; - margin-right: 24rpx; + margin-right: 20rpx; border-radius: 24rpx; - background-color: #1385FA; } .device-info { @@ -49,35 +48,57 @@ } .device-status { - height: 40rpx; display: flex; - padding: 0 16rpx; - border-radius: 20rpx; - align-items: center; - background-color: #EAFAF6; + flex-wrap: nowrap; - .status-dot { - width: 12rpx; - height: 12rpx; - border-radius: 50%; - background-color: #2AC79F; - margin-right: 8rpx; + .device-status_ { + height: 40rpx; + padding: 0 16rpx; + display: flex; + flex-wrap: nowrap; + align-items: center; + border-radius: 20rpx; + overflow: hidden; + background-color: #F2F3F6; + + .status-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + background-color: #77849E; + margin-right: 8rpx; + } + + .status-text { + color: #77849E; + height: 22rpx; + font-size: 22rpx; + line-height: 22rpx; + } } - .status-text { - color: #2AC79F; - height: 22rpx; - font-size: 22rpx; - line-height: 22rpx; + .connected { + background-color: #EAFAF6 !important; + + .status-dot { + background-color: #2AC79F !important; + } + + .status-text { + color: #2AC79F !important; + } } } } .device-meta { color: #808080; - height: 26rpx; - font-size: 26rpx; - line-height: 26rpx; + height: 24rpx; + font-size: 24rpx; + line-height: 24rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } } @@ -176,6 +197,11 @@ overflow: hidden; border-radius: 24rpx; margin-right: 24rpx; + color: #252535; + font-size: 40rpx; + font-weight: bold; + line-height: 88rpx; + text-align: center; &.member-avatar--male { background-color: #C6E6FF; diff --git a/miniprogram/pages/userManagement/userManagement.ts b/miniprogram/pages/userManagement/userManagement.ts index f4d0544..dde7a89 100644 --- a/miniprogram/pages/userManagement/userManagement.ts +++ b/miniprogram/pages/userManagement/userManagement.ts @@ -1,54 +1,88 @@ +import { get, post } from '../../utils/request/index' import { leFuService } from '../../lefu/index' import type { SyncUserData } from '../../lefu/index' +import { calcAge } from '../../utils/idCard/index' /** 连接状态订阅句柄 */ let unsubDeviceConnect: (() => void) | null = null let unsubDisconnected: (() => void) | null = null /** 同步完成关闭弹框定时器 */ let syncCloseTimer: number | null = null +/** selectUserBySn/user 返回的单条结构 */ +interface SysUserDevice { + id: string + userId: string + realname: string + height: number + birthday: string + sex: number + dataType: number + relationType: number | null + syncStatus?: number +} + +/** 展示用成员项 */ +interface MemberItem { + id: string + userId: string + name: string + gender: string + age: number + height: number + sync: string + syncClass: string +} + +/** 展示用分组 */ +interface GroupItem { + title: string + members: MemberItem[] +} + +/** 获取设备成员列表 */ +const getDeviceMembers = (sn: string, userId: string) => + get('weighingScale/v3/selectUserBySn/user', { sn, userId }) + +/** 标记用户已同步到设备 */ +const markSynced = (data: { sn: string; list: string[] }) => + post('weighingScale/v6/markSynced', data, { loading: false }) + +/** equipment 页传入的目标设备 */ +let targetDevice: { sn: string; deviceId: string; name: string; connected: boolean; connectDeviceInfo: string } | null = null Page({ data: { device: { name: '智能体重秤', - connected: false, - model: '', - serial: '' + status: false, + deviceId: '', + sn: '' }, - memberCount: 5, - hasData: true, - self: { - name: '张晓敏', - gender: '男', - age: 36, - height: 172, - sync: '已同步', - syncClass: 'done' - }, - groups: [ - { - title: '员工', - members: [ - { id: 2, name: '李建国', gender: '男', age: 36, height: 182, sync: '已同步', syncClass: 'done' }, - { id: 5, name: '赵明亮', gender: '男', age: 32, height: 172, sync: '已同步', syncClass: 'done' } - ] - }, - { - title: '家庭用户', - members: [ - { id: 3, name: '吉夏', gender: '女', age: 28, height: 165, sync: '已同步', syncClass: 'done' }, - { id: 4, name: '张雪', gender: '女', age: 32, height: 158, sync: '未同步', syncClass: 'pending' } - ] - } - ], + memberCount: 0, + hasData: false, + self: null as null | MemberItem, + groups: [] as GroupItem[], syncShow: false, syncPercent: 0, syncStatus: '' }, - onLoad() { - this._updateDeviceInfo() + onLoad(options: Record) { + targetDevice = { + sn: decodeURIComponent(options.sn || ''), + deviceId: decodeURIComponent(options.deviceId || ''), + name: decodeURIComponent(options.name || ''), + connected: options.status === 'true', + connectDeviceInfo: decodeURIComponent(options.connectDeviceInfo || ''), + } + this.setData({ + 'device.name': targetDevice.name || '智能体重秤', + 'device.deviceId': targetDevice.deviceId, + 'device.sn': targetDevice.sn, + 'device.status': targetDevice.connected, + }) this._subscribeDeviceState() + this._loadMembers() }, onUnload() { @@ -59,59 +93,150 @@ Page({ 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() + this.setData({ 'device.status': leFuService.lastRawDevice?.deviceId === targetDevice?.deviceId }) }) unsubDisconnected = leFuService.onDisconnected(() => { - this.setData({ 'device.connected': false }) + this.setData({ 'device.status': false }) }) }, - /** 校验设备连接状态:未连接则同步状态 + 提示 */ + /** 拉取设备成员列表,映射到本人 + 员工/家庭用户分组 */ + _loadMembers() { + if (!targetDevice?.sn) return + const userInfo = wx.getStorageSync('userInfo') as { userId?: string } | null + const userId = userInfo?.userId ?? '' + if (!userId) return + + getDeviceMembers(targetDevice.sn, userId) + .then(res => { + const list = res.result + const toMember = (item: SysUserDevice): MemberItem => ({ + id: item.id, + userId: item.userId, + name: item.realname, + gender: item.sex === 1 ? '男' : '女', + age: calcAge(item.birthday), + height: item.height, + sync: item.syncStatus === 1 ? '已同步' : '待同步', + syncClass: item.syncStatus === 1 ? 'done' : 'pending', + }) + const self = list.find(m => m.relationType === 1) + this.setData({ + self: self ? toMember(self) : null, + groups: [ + { title: '员工', members: list.filter(m => m.relationType !== 1 && m.dataType === 0).map(toMember) }, + { title: '家庭用户', members: list.filter(m => m.relationType !== 1 && m.dataType === 1).map(toMember) }, + ].filter(g => g.members.length), + memberCount: list.length, + hasData: list.length > 0, + }) + }) + .catch(() => { + wx.showToast({ title: '加载用户列表失败', icon: 'none' }) + }) + }, + + /** 校验是否连接的是目标设备:不是则弹框提示连接/取消 */ _ensureConnected(): boolean { - if (leFuService.isConnected) return true - this.setData({ 'device.connected': false }) - wx.showToast({ title: '请先连接设备', icon: 'none' }) + const connected = leFuService.isConnected && leFuService.lastRawDevice?.deviceId === targetDevice?.deviceId + if (connected) return true + this.setData({ 'device.status': false }) + wx.showModal({ + title: '未连接设备', + content: '请先连接该设备后再同步用户', + cancelText: '取消同步', + confirmText: '连接设备', + success: (res) => { + if (res.confirm) { + this._connectTargetDevice() + } + }, + }) return false }, - /** 同步用户至设备 */ + /** 本页面扫描并连接目标设备 */ + _connectTargetDevice() { + const targetDeviceId = targetDevice?.deviceId + if (!targetDeviceId) return + if (leFuService.isConnected) leFuService.disconnect() + + wx.showLoading({ title: '连接中...', mask: true }) + wx.openBluetoothAdapter({ + success: () => { + let unsubList: (() => void) | null = null + let unsubConnect: (() => void) | null = null + const timer = setTimeout(() => { + unsubList?.() + unsubList = null + wx.hideLoading() + wx.showToast({ title: '未找到设备', icon: 'none' }) + }, 15000) + + unsubList = leFuService.onDevicesList((devices) => { + const matched = devices.find(d => d.raw.deviceId === targetDeviceId) + if (!matched) return + clearTimeout(timer) + unsubList?.() + unsubList = null + leFuService.stopScan() + leFuService.connect(matched.raw) + unsubConnect = leFuService.onDeviceConnect(() => { + unsubConnect?.() + unsubConnect = null + wx.hideLoading() + wx.setStorageSync('connectDeviceInfo', matched.raw) + this.setData({ 'device.status': true }) + this.onTapSync() + }) + }) + leFuService.startScan() + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '蓝牙未开启', icon: 'none' }) + }, + }) + }, + + /** 同步用户至设备:只下发「待同步」的用户 */ onTapSync() { if (!this._ensureConnected()) return - // 组装所有用户(self 第一个,作为主用户),性别:男=1 女=0 + // 组装所有用户(本人第一个,作为主用户),性别:男=1 女=0 + const self = this.data.self 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, - }, + ...(self ? [{ + id: self.id, userId: self.userId, name: self.name, + gender: self.gender === '男' ? 1 : 0, + age: self.age, height: self.height, sync: self.sync, + }] : []), ...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, + id: String(m.id), userId: m.userId, name: m.name, + gender: m.gender === '男' ? 1 : 0, age: m.age, height: m.height, sync: m.sync, }))), ] + if (!allUsers.length) { + wx.showToast({ title: '暂无用户可同步', icon: 'none' }) + return + } const mainUser = allUsers[0] - // 组装下发参数(SyncUserData),第一个为主用户(memberID 为空) - const syncDataList: SyncUserData[] = allUsers.map((user, index) => ({ + // 只下发「待同步」的用户 + const pendingUsers = allUsers.filter(u => u.sync !== '已同步') + if (!pendingUsers.length) { + wx.showToast({ title: '没有需要同步的用户', icon: 'none' }) + return + } + // 组装下发参数(SyncUserData),主用户 memberID 为空 + const syncDataList: SyncUserData[] = pendingUsers.map((user, index) => ({ userID: mainUser.id, userName: user.name, - memberID: index === 0 ? '' : user.id, + memberID: user.id === mainUser.id ? '' : user.id, age: user.age, gender: user.gender, height: user.height, @@ -123,23 +248,35 @@ Page({ 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}...`, - }) - }) + // 直接下发待同步用户(不清空) + this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在同步信息,请勿离开...' }) + 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' }) + const syncedIds = pendingUsers.map(u => u.userId).filter(Boolean) + if (targetDevice?.sn && syncedIds.length) { + markSynced({ sn: targetDevice.sn, list: syncedIds }) + .then(() => { + // 标记成功后,把对应成员更新为「已同步」 + const syncedSet = new Set(syncedIds) + this.setData({ + self: this.data.self && syncedSet.has(this.data.self.userId) + ? { ...this.data.self, sync: '已同步', syncClass: 'done' } + : this.data.self, + groups: this.data.groups.map(g => ({ + ...g, + members: g.members.map(m => syncedSet.has(m.userId) ? { ...m, sync: '已同步', syncClass: 'done' } : m), + })), + }) + }) + .catch(() => {}) + } if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null } syncCloseTimer = setTimeout(() => { syncCloseTimer = null diff --git a/miniprogram/pages/userManagement/userManagement.wxml b/miniprogram/pages/userManagement/userManagement.wxml index f685855..5d75224 100644 --- a/miniprogram/pages/userManagement/userManagement.wxml +++ b/miniprogram/pages/userManagement/userManagement.wxml @@ -3,17 +3,21 @@ - + + + {{ device.name }} - - {{ device.connected ? '已连接' : '未连接' }} + + + {{ device.status ? '已连接' : '未连接' }} + - 型号:{{ device.model }} - 设备编号:{{ device.serial }} + 设备ID:{{ device.deviceId }} + 设备SN:{{ device.sn }} @@ -32,9 +36,9 @@ - + - + {{ self.name[0] }} {{ self.name }} @@ -59,7 +63,7 @@ - + {{ member.name[0] }} {{ member.name }} (他人添加)