Files
bodyWeight/miniprogram/pages/userManagement/userManagement.ts
T
2026-09-01 16:57:19 +08:00

465 lines
18 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 { get, post, del, put } 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
delFlag?: number
}
/** 同步用的用户项 */
interface SyncUser {
id: string
userId: string
name: string
gender: number
age: number
height: number
sync: string
delFlag?: number
}
/** 展示用成员项 */
interface MemberItem {
id: string
userId: string
name: string
gender: string
age: number
height: number
sync: string
syncClass: string
/** 1=本人 2=自己添加 3=他人添加 */
relationType: number | null
delFlag?: number
}
/** 展示用分组 */
interface GroupItem {
title: string
members: MemberItem[]
}
/** 获取设备成员列表(运维模式走 ops/getDeviceUsers,普通模式走 v3/selectUserBySn/user */
const getDeviceMembers = (sn: string, userId: string) =>
get<SysUserDevice[]>(
getApp<IAppOption>().globalData.operationsEngineer
? 'weighingScale/ops/getDeviceUsers'
: 'weighingScale/v3/selectUserBySn/user',
{ sn, userId },
)
/** 标记用户已同步到设备 */
const markSynced = (data: { sn: string; list: string[] }) =>
post('weighingScale/v6/markSynced', data, { loading: false })
/** 删除设备成员 */
const deleteMember = (id: string) =>
del('weighingScale/v6/del/user', { id }, { loading: false })
/** 添加设备成员(本人) */
const addSelfUser = (data: Record<string, any>) =>
put('weighingScale/v6/edit/user', data)
/** equipment 页传入的目标设备 */
let targetDevice: { sn: string; deviceId: string; name: string; connected: boolean; connectDeviceInfo: string } | null = null
Page({
data: {
device: {
name: '智能体重秤',
status: false,
deviceId: '',
sn: ''
},
memberCount: 0,
hasData: false,
self: null as null | MemberItem,
groups: [] as GroupItem[],
syncShow: false,
syncPercent: 0,
syncStatus: ''
},
onLoad(options: Record<string, string>) {
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() {
unsubDeviceConnect?.()
unsubDisconnected?.()
unsubDeviceConnect = null
unsubDisconnected = null
if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null }
},
/** 订阅连接状态,实时同步设备卡(判断是否连接的目标设备) */
_subscribeDeviceState() {
unsubDeviceConnect?.()
unsubDisconnected?.()
unsubDeviceConnect = leFuService.onDeviceConnect(() => {
this.setData({ 'device.status': leFuService.lastRawDevice?.deviceId === targetDevice?.deviceId })
})
unsubDisconnected = leFuService.onDisconnected(() => {
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.delFlag === 1 ? '已删除' : (item.syncStatus === 1 ? '已同步' : '未同步'),
syncClass: item.delFlag === 1 ? 'deleted' : (item.syncStatus === 1 ? 'done' : 'pending'),
relationType: item.relationType,
delFlag: item.delFlag,
})
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 {
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' })
},
})
},
/** 组装所有用户(本人第一个,作为主用户),性别:男=1 女=0 */
_buildAllUsers(): SyncUser[] {
const self = this.data.self
return [
...(self ? [{
id: self.id, userId: self.userId, name: self.name,
gender: self.gender === '男' ? 1 : 0,
age: self.age, height: self.height, sync: self.sync, delFlag: self.delFlag,
}] : []),
...this.data.groups.flatMap(g => g.members.map(m => ({
id: String(m.id), userId: m.userId, name: m.name,
gender: m.gender === '男' ? 1 : 0, age: m.age, height: m.height, sync: m.sync, delFlag: m.delFlag,
}))),
]
},
/** 同步完成后的统一处理:上报后刷新列表 + 关闭弹框 */
_finishSync(reportUsers: SyncUser[] | null, successText: string) {
this.setData({ syncPercent: 100, syncStatus: '同步完成' })
wx.showToast({ title: successText, icon: 'success' })
if (reportUsers && reportUsers.length && targetDevice?.sn) {
const syncedIds = reportUsers.map(u => u.userId).filter(Boolean)
if (syncedIds.length) {
markSynced({ sn: targetDevice.sn, list: syncedIds })
.then(() => this._loadMembers())
.catch(() => {})
}
}
if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null }
syncCloseTimer = setTimeout(() => {
syncCloseTimer = null
this.setData({ syncShow: false })
}, 1000)
},
/** 同步用户至设备:有删除标记则清空重下发,否则只下发「待同步」 */
onTapSync() {
if (!this._ensureConnected()) return
const allUsers = this._buildAllUsers()
if (!allUsers.length) {
wx.showToast({ title: '暂无用户可同步', icon: 'none' })
return
}
// 有删除标记(delFlag=1):清空设备后下发未删除的用户,上报所有用户
const hasDeleted = allUsers.some(u => u.delFlag === 1)
if (hasDeleted) {
const normalUsers = allUsers.filter(u => u.delFlag !== 1)
this._clearAndSync(normalUsers, allUsers)
return
}
// 无删除标记:只下发「待同步」的用户
const pendingUsers = allUsers.filter(u => u.sync !== '已同步')
if (!pendingUsers.length) {
wx.showToast({ title: '没有需要同步的用户', icon: 'none' })
return
}
// 先读设备端主用户列表,判断设备是否已有主用户
leFuService.fetchDeviceUserIds()
.then((deviceUserIds) => this._syncUsers(pendingUsers, deviceUserIds))
.catch(() => {
wx.showToast({ title: '读取设备用户失败', icon: 'none' })
})
},
/** 根据设备端主用户情况组装并下发(一主九子,子用户挂在主用户下) */
_syncUsers(pendingUsers: SyncUser[], deviceUserIds: any) {
// dataFetchUserID 只返回主用户,非空表示设备已有主用户
const userIds = Array.isArray(deviceUserIds) ? deviceUserIds : [deviceUserIds].filter(Boolean)
const hasMainUser = userIds.length > 0
// 主用户 ID:已有主用户用设备端的,否则用第一个用户作为主用户
const mainUserId = hasMainUser ? String(userIds[0]) : pendingUsers[0].userId
const syncDataList: SyncUserData[] = pendingUsers.map((user, index) => ({
userID: mainUserId,
userName: user.name,
memberID: hasMainUser ? user.userId : (user.userId === pendingUsers[0].userId ? '' : user.userId),
age: user.age,
gender: user.gender,
height: user.height,
isAthleteMode: 0,
deviceHeaderIndex: index,
currentWeight: '',
targetWeight: '',
idealWeight: '',
recentData: [],
}))
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._finishSync(pendingUsers, '用户同步成功'))
.catch((err: Error) => {
this.setData({ syncShow: false })
wx.showToast({ title: err.message || '同步失败,请重试', icon: 'none' })
})
},
/** 长按标题:清除设备用户信息并全部重新下发(不上报) */
onLongPressSectionTitle() {
if (!this._ensureConnected()) return
const allUsers = this._buildAllUsers()
if (!allUsers.length) {
wx.showToast({ title: '暂无用户可下发', icon: 'none' })
return
}
this._clearAndSync(allUsers, null)
},
/** 清除设备用户后重新下发;reportUsers 为 null 时不上报后台 */
_clearAndSync(syncUsers: SyncUser[], reportUsers: SyncUser[] | null) {
const mainUser = syncUsers[0]
const syncDataList: SyncUserData[] = syncUsers.map((user, index) => ({
userID: mainUser.userId,
userName: user.name,
memberID: user.userId === mainUser.userId ? '' : user.userId,
age: user.age,
gender: user.gender,
height: user.height,
isAthleteMode: 0,
deviceHeaderIndex: index,
currentWeight: '',
targetWeight: '',
idealWeight: '',
recentData: [],
}))
this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在清空并重新下发...' })
leFuService.clearDeviceMembers()
.then(() => leFuService.syncMembersToDevice(syncDataList, (current, total) => {
this.setData({
syncPercent: Math.round((current / total) * 100),
syncStatus: `正在同步 ${current}/${total}...`,
})
}))
.then(() => this._finishSync(reportUsers, '重新下发成功'))
.catch((err: Error) => {
this.setData({ syncShow: false })
wx.showToast({ title: err.message || '重新下发失败', icon: 'none' })
})
},
/** 添加用户 */
onTapAddUser() {
if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return
}
const sn = targetDevice?.sn || ''
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}` })
},
/** 编辑成员:跳转 addUser 编辑模式 */
onTapEdit(e: WechatMiniprogram.TouchEvent) {
const item = e.currentTarget.dataset.item as MemberItem
const sn = targetDevice?.sn || ''
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}&id=${encodeURIComponent(item.id)}` })
},
/** 删除成员 */
onTapDelete(e: WechatMiniprogram.TouchEvent) {
const item = e.currentTarget.dataset.item as MemberItem
wx.showModal({
title: '删除用户',
content: `是否确认删除「${item.name}」?`,
cancelText: '取消',
confirmText: '删除',
confirmColor: '#F24439',
success: (res) => {
if (!res.confirm) return
deleteMember(item.id)
.then(() => {
wx.showToast({ title: '删除成功', icon: 'success' })
this._loadMembers()
})
.catch(() => {
wx.showToast({ title: '删除失败,请重试', icon: 'none' })
})
},
})
},
/** 一键添加本人:仅调接口把当前登录用户添加为设备成员,不下发设备 */
onTapAddSelf() {
if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return
}
const userInfo = wx.getStorageSync('userInfo') as Record<string, any> | null
if (!userInfo?.userId) {
wx.showToast({ title: '请先完善个人信息', icon: 'none' })
return
}
const sn = targetDevice?.sn
if (!sn) return
addSelfUser({
dataType: 0,
parentUserId: userInfo.userId,
sn,
scaleDeviceId: sn,
realname: userInfo.realname ?? '',
idCard: userInfo.idCard || null,
sex: userInfo.sex ?? 0,
birthday: userInfo.birthday ?? '',
height: userInfo.height ?? 0,
weight: userInfo.weight ?? 0,
mode: getApp<IAppOption>().globalData.operationsEngineer ? 'ops' : 'normal',
})
.then(() => {
wx.showToast({ title: '添加成功', icon: 'success' })
this._loadMembers()
})
.catch(() => {
wx.showToast({ title: '添加失败,请重试', icon: 'none' })
})
},
/** 添加新用户 */
onTapAddNew() {
wx.navigateTo({ url: '/pages/addUser/addUser' })
}
})