[AI Generated]: feat(*): 新增 lefu 蓝牙服务层、mock 业务数据层及 request 请求封装,接入各页面
This commit is contained in:
+11
-16
@@ -1,18 +1,13 @@
|
|||||||
// app.ts
|
import { lefuService } from './lefu/index'
|
||||||
App<IAppOption>({
|
|
||||||
globalData: {},
|
|
||||||
onLaunch() {
|
|
||||||
// 展示本地存储能力
|
|
||||||
const logs = wx.getStorageSync('logs') || []
|
|
||||||
logs.unshift(Date.now())
|
|
||||||
wx.setStorageSync('logs', logs)
|
|
||||||
|
|
||||||
// 登录
|
App<IAppOption>({
|
||||||
wx.login({
|
globalData: {
|
||||||
success: res => {
|
/** 当前已连接设备的蓝牙状态,页面可读取 */
|
||||||
console.log(res.code)
|
connectState: '' as string,
|
||||||
// 发送 res.code 到后台换取 openId, sessionKey, unionId
|
},
|
||||||
},
|
|
||||||
})
|
onLaunch() {
|
||||||
},
|
// 初始化乐福蓝牙 SDK
|
||||||
|
lefuService.init()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export { lefuService } from './service'
|
||||||
|
export type {
|
||||||
|
ScannedDevice,
|
||||||
|
LeFuWifiItem,
|
||||||
|
ProgressData,
|
||||||
|
LockData,
|
||||||
|
WifiVersion,
|
||||||
|
LeFuConfig,
|
||||||
|
DeviceSetting,
|
||||||
|
DeviceMember,
|
||||||
|
} from './types'
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
import type {
|
||||||
|
RawDevice,
|
||||||
|
LeFuPlugin,
|
||||||
|
ScannedDevice,
|
||||||
|
LeFuWifiItem,
|
||||||
|
ProgressData,
|
||||||
|
LockData,
|
||||||
|
WifiVersion,
|
||||||
|
LeFuConfig,
|
||||||
|
DeviceSetting,
|
||||||
|
DeviceMember,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
/** 乐福 SDK 配置 */
|
||||||
|
const LEFU_CONFIG: LeFuConfig = {
|
||||||
|
key: 'lefudc91611833e18d94',
|
||||||
|
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
|
||||||
|
domain1: 'http://device.shuziweidao.com:80/gateway',
|
||||||
|
domain2: 'http://device.shuziweidao.com:80/weight',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支持的设备型号配置列表(5款) */
|
||||||
|
const DEVICE_SETTINGS: DeviceSetting[] = [
|
||||||
|
{
|
||||||
|
advLength: 999, calorieStatus: 0, createBy: null,
|
||||||
|
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
|
||||||
|
deviceFuncType: 223, deviceName: 'CF568_G', devicePowerType: 3,
|
||||||
|
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||||
|
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
|
||||||
|
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
advLength: 999, calorieStatus: 0, createBy: null,
|
||||||
|
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
|
||||||
|
deviceFuncType: 223, deviceName: 'YX-B4-568-BW', devicePowerType: 3,
|
||||||
|
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||||
|
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
|
||||||
|
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
advLength: 999, calorieStatus: 0, createBy: null,
|
||||||
|
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||||
|
deviceFuncType: 65759, deviceName: 'CF636_G', devicePowerType: 3,
|
||||||
|
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
|
||||||
|
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
|
||||||
|
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
advLength: 999, calorieStatus: 0, createBy: null,
|
||||||
|
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||||
|
deviceFuncType: 65759, deviceName: 'YX-B8-636-BW', devicePowerType: 3,
|
||||||
|
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
|
||||||
|
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
|
||||||
|
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
advLength: 999, calorieStatus: 0, createBy: null,
|
||||||
|
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||||
|
deviceFuncType: 223, deviceName: 'CF577', devicePowerType: 3,
|
||||||
|
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||||
|
id: 51, imgUrl: null, macAddressStart: 6, remark: null,
|
||||||
|
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
class LeFuService {
|
||||||
|
private _plugin: LeFuPlugin | null = null
|
||||||
|
/** 设备协议对象,devicesModel 事件返回,用于 WiFi 配网等高级操作 */
|
||||||
|
private _activeProtocol: any = null
|
||||||
|
|
||||||
|
/** 重连次数 */
|
||||||
|
private _reconnectCount = 0
|
||||||
|
private readonly _maxReconnect = 3
|
||||||
|
private _reconnectTimer: number | null = null
|
||||||
|
|
||||||
|
/** 保活心跳定时器 */
|
||||||
|
private _keepAliveTimer: number | null = null
|
||||||
|
|
||||||
|
/** 回调注册(页面通过 on* 方法注册,stopScan 时清空) */
|
||||||
|
private _onDevicesListCb: ((devices: ScannedDevice[]) => void) | null = null
|
||||||
|
private _onConnectStateCb: ((state: string) => void) | null = null
|
||||||
|
private _onProgressCb: ((data: ProgressData) => void) | null = null
|
||||||
|
private _onLockedCb: ((data: LockData) => void) | null = null
|
||||||
|
private _onDisconnectedCb: (() => void) | null = null
|
||||||
|
|
||||||
|
/** 在 app.ts onLaunch 中调用,加载 ppScale-plugin */
|
||||||
|
init(): void {
|
||||||
|
this._plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取插件(未初始化时抛出) */
|
||||||
|
private get _p(): LeFuPlugin {
|
||||||
|
if (!this._plugin) throw new Error('[LeFu] 服务未初始化,请先调用 init()')
|
||||||
|
return this._plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 乐福配置(供体成分分析等外部使用) */
|
||||||
|
get config(): LeFuConfig {
|
||||||
|
return LEFU_CONFIG
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 回调注册 ─────────────────────────────────
|
||||||
|
|
||||||
|
/** 注册设备列表回调(扫描到设备时触发) */
|
||||||
|
onDevicesList(cb: (devices: ScannedDevice[]) => void): void {
|
||||||
|
this._onDevicesListCb = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册蓝牙连接状态回调 */
|
||||||
|
onConnectState(cb: (state: string) => void): void {
|
||||||
|
this._onConnectStateCb = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册实时测量回调(测量过程中多次触发) */
|
||||||
|
onMeasuring(cb: (data: ProgressData) => void): void {
|
||||||
|
this._onProgressCb = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册测量完成回调(数据锁定,触发一次) */
|
||||||
|
onLocked(cb: (data: LockData) => void): void {
|
||||||
|
this._onLockedCb = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册设备断开回调 */
|
||||||
|
onDisconnected(cb: () => void): void {
|
||||||
|
this._onDisconnectedCb = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 扫描与连接 ───────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始蓝牙扫描
|
||||||
|
* 先注册所有 bus 事件再调用 plugin.Blue.start()
|
||||||
|
* 页面 onShow 时调用
|
||||||
|
*/
|
||||||
|
startScan(): void {
|
||||||
|
const plugin = this._p
|
||||||
|
|
||||||
|
// 扫描到设备列表
|
||||||
|
plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => {
|
||||||
|
// 广播秤(BleAdv)自动调用 setbroadcastDev,无需用户手动点击连接
|
||||||
|
const advDev = devList.find((item: RawDevice) => {
|
||||||
|
const model = plugin.Blue.getDeviceModel(item)
|
||||||
|
return model?.deviceConnectType === plugin.PPBluetoothDefine.PPDeviceConnectType.PPDeviceConnectTypeBleAdv
|
||||||
|
})
|
||||||
|
if (advDev) {
|
||||||
|
plugin.Blue.setbroadcastDev(advDev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为展示层数据
|
||||||
|
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
|
||||||
|
name: item.name || item.deviceName || '未知设备',
|
||||||
|
raw: item,
|
||||||
|
model: plugin.Blue.getDeviceModel(item) || {},
|
||||||
|
}))
|
||||||
|
this._onDevicesListCb?.(devices)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 协议对象(持有 activeProtocol,WiFi 配网等高级操作依赖它)
|
||||||
|
plugin.bus.subscribe('devicesModel', (res: any) => {
|
||||||
|
this._activeProtocol = res
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接成功
|
||||||
|
plugin.bus.subscribe('deviceConnect', () => {
|
||||||
|
this._reconnectCount = 0
|
||||||
|
plugin.ScaleAction.startDataProgress()
|
||||||
|
this._startKeepAlive()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接状态变化
|
||||||
|
plugin.bus.subscribe('connectState', (res: string) => {
|
||||||
|
this._onConnectStateCb?.(res)
|
||||||
|
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
|
||||||
|
this._stopKeepAlive()
|
||||||
|
this._doReconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 实时测量数据
|
||||||
|
plugin.bus.subscribe('progressData', (res: ProgressData) => {
|
||||||
|
this._onProgressCb?.(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测量锁定(完成)
|
||||||
|
plugin.bus.subscribe('lockData', (res: LockData) => {
|
||||||
|
this._onLockedCb?.(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设备自动断开
|
||||||
|
plugin.bus.subscribe('deviceWillDisconnect', () => {
|
||||||
|
this._stopKeepAlive()
|
||||||
|
this._onDisconnectedCb?.()
|
||||||
|
this._doReconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 开始扫描,传入设备配置过滤列表
|
||||||
|
plugin.Blue.start(DEVICE_SETTINGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动连接指定设备
|
||||||
|
* BleAdv 类型已在 devicesList 中自动处理,无需调用此方法
|
||||||
|
* BleConnect 类型需要用户点击后调用
|
||||||
|
*/
|
||||||
|
connect(device: ScannedDevice): void {
|
||||||
|
const connectType = this._p.PPBluetoothDefine.PPDeviceConnectType
|
||||||
|
if (device.model?.deviceConnectType === connectType.PPDeviceConnectTypeBleConnect) {
|
||||||
|
this._p.Blue.createBLEConnection(device.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主动断开当前连接 */
|
||||||
|
disconnect(): void {
|
||||||
|
this._stopKeepAlive()
|
||||||
|
this._p.Blue.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止扫描,清理所有 bus 订阅与回调
|
||||||
|
* 页面 onUnload / onHide 时调用
|
||||||
|
*/
|
||||||
|
stopScan(): void {
|
||||||
|
this._stopKeepAlive()
|
||||||
|
this._stopReconnectTimer()
|
||||||
|
this._p.Blue.stop()
|
||||||
|
this._activeProtocol = null
|
||||||
|
this._onDevicesListCb = null
|
||||||
|
this._onConnectStateCb = null
|
||||||
|
this._onProgressCb = null
|
||||||
|
this._onLockedCb = null
|
||||||
|
this._onDisconnectedCb = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WiFi 配网 ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备上报的 WiFi 列表(需设备已连接)
|
||||||
|
* 内部延迟 2 秒后调用,15 秒超时
|
||||||
|
*/
|
||||||
|
getWifiList(): Promise<LeFuWifiItem[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) {
|
||||||
|
reject(new Error('[LeFu] 设备未连接,无法获取 WiFi 列表'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error('[LeFu] 获取 WiFi 列表超时'))
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
// 延迟 2 秒,等待设备就绪后再请求
|
||||||
|
setTimeout(() => {
|
||||||
|
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
if (!res || res.length === 0) {
|
||||||
|
this._activeProtocol.dataExitWifiConfig(() => {})
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list: LeFuWifiItem[] = res.map((item: any) => ({
|
||||||
|
ssid: item.ssid || item.name || '',
|
||||||
|
signal: item.signal,
|
||||||
|
}))
|
||||||
|
resolve(list)
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 WiFi 配置写入设备
|
||||||
|
* @param ssid WiFi 名称
|
||||||
|
* @param password WiFi 密码
|
||||||
|
* @param version 协议版本:domain1(无鉴权)| domain2(有鉴权)
|
||||||
|
* 返回码 23 = 配网成功
|
||||||
|
*/
|
||||||
|
configWifi(ssid: string, password: string, version: WifiVersion): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) {
|
||||||
|
reject(new Error('[LeFu] 设备未连接,无法配网'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = (res: number) => {
|
||||||
|
if (res === 23) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error(`[LeFu] 配网失败,错误码:${res}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version === 'domain1') {
|
||||||
|
this._activeProtocol.dataConfigNetWork(
|
||||||
|
{ domain: LEFU_CONFIG.domain1, ssid, password },
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this._activeProtocol.dataConfigUserNetWork(
|
||||||
|
{
|
||||||
|
domain: LEFU_CONFIG.domain2,
|
||||||
|
ssid,
|
||||||
|
password,
|
||||||
|
userName: 'apiUser',
|
||||||
|
userPassword: '3acebb95eb49577e9c2a2082589b9bd6',
|
||||||
|
},
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备是否已完成配网
|
||||||
|
* 返回 true = 已配网(0x01),false = 未配网
|
||||||
|
*/
|
||||||
|
checkWifiConfig(): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) {
|
||||||
|
reject(new Error('[LeFu] 设备未连接'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._activeProtocol.codeFetchWifiConfig((res: number | undefined | null) => {
|
||||||
|
resolve(res === 0x01)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 私有:保活与重连 ─────────────────────────
|
||||||
|
|
||||||
|
private _startKeepAlive(): void {
|
||||||
|
this._stopKeepAlive()
|
||||||
|
this._keepAliveTimer = setInterval(() => {
|
||||||
|
this._activeProtocol?.sendKeepAliveCode?.()
|
||||||
|
}, 15000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopKeepAlive(): void {
|
||||||
|
if (this._keepAliveTimer !== null) {
|
||||||
|
clearInterval(this._keepAliveTimer)
|
||||||
|
this._keepAliveTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopReconnectTimer(): void {
|
||||||
|
if (this._reconnectTimer !== null) {
|
||||||
|
clearTimeout(this._reconnectTimer)
|
||||||
|
this._reconnectTimer = null
|
||||||
|
}
|
||||||
|
this._reconnectCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自动重连,最多 _maxReconnect 次,超限后等待用户手动重试 */
|
||||||
|
private _doReconnect(): void {
|
||||||
|
if (this._reconnectCount >= this._maxReconnect) return
|
||||||
|
this._reconnectCount++
|
||||||
|
this._stopReconnectTimer()
|
||||||
|
this._reconnectTimer = setTimeout(() => {
|
||||||
|
this._plugin?.Blue?.disconnect((res: any) => {
|
||||||
|
if (res?.errCode === 0) {
|
||||||
|
this._plugin?.Blue?.startBluetoothDevicesDiscovery()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 成员同步 ─────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量将成员列表同步到设备
|
||||||
|
* 流程:清除设备成员数据 → 逐个下发(顺序执行)
|
||||||
|
* 每步 15 秒超时保护,失败时自动重试当前成员
|
||||||
|
* @param members 成员列表,必须包含且仅包含一个 isSelf=true 的主用户
|
||||||
|
*/
|
||||||
|
syncMembersToDevice(members: DeviceMember[]): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._activeProtocol) {
|
||||||
|
reject(new Error('[LeFu] 设备未连接,无法同步成员'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainUser = members.find(m => m.isSelf)
|
||||||
|
if (!mainUser) {
|
||||||
|
reject(new Error('[LeFu] 成员列表中缺少主用户(isSelf=true)'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一步:清除设备上的所有成员数据("01" 仅清成员,保留历史)
|
||||||
|
const clearTimer = setTimeout(() => {
|
||||||
|
reject(new Error('[LeFu] 清除设备成员超时'))
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
this._activeProtocol.codeClearDeviceData('01', (clearRes: number) => {
|
||||||
|
clearTimeout(clearTimer)
|
||||||
|
if (clearRes !== 0x00) {
|
||||||
|
reject(new Error('[LeFu] 清除设备成员失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:逐个下发成员(顺序执行,失败自动重试当前)
|
||||||
|
let index = 0
|
||||||
|
let syncTimer: number | null = null
|
||||||
|
|
||||||
|
const resetTimer = () => {
|
||||||
|
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||||
|
syncTimer = setTimeout(() => {
|
||||||
|
reject(new Error(`[LeFu] 第 ${index + 1} 个成员下发超时`))
|
||||||
|
}, 15000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncNext = () => {
|
||||||
|
if (index >= members.length) {
|
||||||
|
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = members[index]
|
||||||
|
resetTimer()
|
||||||
|
|
||||||
|
this._activeProtocol.dataSyncUserInfo(
|
||||||
|
{
|
||||||
|
userID: mainUser.id,
|
||||||
|
userName: member.name,
|
||||||
|
// 主用户 memberID 为空,子用户用自己的 id
|
||||||
|
memberID: member.isSelf ? '' : member.id,
|
||||||
|
age: member.age,
|
||||||
|
gender: member.gender,
|
||||||
|
height: member.height,
|
||||||
|
isAthleteMode: 0,
|
||||||
|
deviceHeaderIndex: index,
|
||||||
|
currentWeight: '',
|
||||||
|
targetWeight: '',
|
||||||
|
idealWeight: '',
|
||||||
|
recentData: [],
|
||||||
|
},
|
||||||
|
(res: number) => {
|
||||||
|
if (res === 0) {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
// 成功进下一个,失败重试当前(index 不变)
|
||||||
|
syncNext()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncNext()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局单例,整个小程序共享同一个蓝牙服务实例 */
|
||||||
|
export const lefuService = new LeFuService()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/** ppScale-plugin 原始设备对象 */
|
||||||
|
export type RawDevice = Record<string, any>
|
||||||
|
|
||||||
|
/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */
|
||||||
|
export type LeFuPlugin = Record<string, any>
|
||||||
|
|
||||||
|
/** WiFi 配网协议版本 */
|
||||||
|
export type WifiVersion = 'domain1' | 'domain2'
|
||||||
|
|
||||||
|
/** 设备配置(来自 master device.setting 列表) */
|
||||||
|
export interface DeviceSetting {
|
||||||
|
advLength: number
|
||||||
|
calorieStatus: number
|
||||||
|
deviceAccuracyType: number
|
||||||
|
deviceCalcuteType: number
|
||||||
|
deviceConnectType: number
|
||||||
|
deviceFuncType: number
|
||||||
|
deviceName: string
|
||||||
|
devicePowerType: number
|
||||||
|
deviceProtocolType: number
|
||||||
|
deviceType: number
|
||||||
|
deviceUnitType: string
|
||||||
|
id: number
|
||||||
|
macAddressStart: number
|
||||||
|
sign: string
|
||||||
|
status: number
|
||||||
|
uhStatus: number
|
||||||
|
createBy: null
|
||||||
|
imgUrl: null
|
||||||
|
remark: null
|
||||||
|
updateBy: null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫描到的设备(展示层使用) */
|
||||||
|
export interface ScannedDevice {
|
||||||
|
/** 设备名称 */
|
||||||
|
name: string
|
||||||
|
/** plugin 返回的原始设备对象,连接时传回给 SDK */
|
||||||
|
raw: RawDevice
|
||||||
|
/** 设备模型(含 deviceConnectType 等信息) */
|
||||||
|
model: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WiFi 项 */
|
||||||
|
export interface LeFuWifiItem {
|
||||||
|
/** WiFi SSID */
|
||||||
|
ssid: string
|
||||||
|
/** 信号强度(部分设备上报) */
|
||||||
|
signal?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测量进度数据(progressData 事件,测量过程中多次回调) */
|
||||||
|
export interface ProgressData {
|
||||||
|
/** 体重,单位:克 */
|
||||||
|
weight: number
|
||||||
|
/** 单位枚举值 */
|
||||||
|
unit: number
|
||||||
|
/** 是否正在心率测量 */
|
||||||
|
isHeartRating: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测量锁定数据(lockData 事件,测量完成时回调一次) */
|
||||||
|
export interface LockData {
|
||||||
|
/** 体重,单位:克 */
|
||||||
|
weight: number
|
||||||
|
/** 阻抗值(用于体成分分析) */
|
||||||
|
resistance: number
|
||||||
|
/** 心率(部分设备上报) */
|
||||||
|
heartRate?: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 乐福服务配置 */
|
||||||
|
export interface LeFuConfig {
|
||||||
|
key: string
|
||||||
|
secret: string
|
||||||
|
/** 老协议域名(无鉴权) */
|
||||||
|
domain1: string
|
||||||
|
/** 新协议域名(有鉴权) */
|
||||||
|
domain2: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下发给设备的成员信息(lefu 层使用,业务层转换后传入) */
|
||||||
|
export interface DeviceMember {
|
||||||
|
/** 成员唯一 ID */
|
||||||
|
id: string
|
||||||
|
/** 姓名 */
|
||||||
|
name: string
|
||||||
|
/** 性别:1=男,0=女 */
|
||||||
|
gender: 0 | 1
|
||||||
|
/** 年龄 */
|
||||||
|
age: number
|
||||||
|
/** 身高(cm) */
|
||||||
|
height: number
|
||||||
|
/** 是否本人(主用户) */
|
||||||
|
isSelf: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/** 时间范围 */
|
||||||
|
export type TimeRange = '7d' | '30d' | '3m'
|
||||||
|
|
||||||
|
/** 单条测量记录 */
|
||||||
|
export interface RecordItem {
|
||||||
|
id: string
|
||||||
|
/** 格式 YYYY.MM.DD HH:mm */
|
||||||
|
time: string
|
||||||
|
weight: number
|
||||||
|
bmi: number
|
||||||
|
/** 体脂率(%) */
|
||||||
|
bodyFat: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计指标 */
|
||||||
|
export interface StatsData {
|
||||||
|
maxWeight: number
|
||||||
|
minWeight: number
|
||||||
|
avgWeight: number
|
||||||
|
/** 体重变化(kg,正=增重,负=减轻) */
|
||||||
|
weightDiff: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全量 mock 记录(最近 3 个月数据,按时间倒序) */
|
||||||
|
const ALL_RECORDS: RecordItem[] = [
|
||||||
|
{ id: 'r-001', time: '2026.05.12 08:12', weight: 68.2, bmi: 22.0, bodyFat: 18.5 },
|
||||||
|
{ id: 'r-002', time: '2026.05.11 21:30', weight: 68.5, bmi: 22.1, bodyFat: 18.7 },
|
||||||
|
{ id: 'r-003', time: '2026.05.10 08:05', weight: 68.8, bmi: 22.3, bodyFat: 18.9 },
|
||||||
|
{ id: 'r-004', time: '2026.05.09 07:58', weight: 69.0, bmi: 22.4, bodyFat: 19.0 },
|
||||||
|
{ id: 'r-005', time: '2026.05.08 22:10', weight: 69.3, bmi: 22.5, bodyFat: 19.2 },
|
||||||
|
{ id: 'r-006', time: '2026.05.07 08:20', weight: 69.5, bmi: 22.6, bodyFat: 19.3 },
|
||||||
|
{ id: 'r-007', time: '2026.05.06 08:00', weight: 70.0, bmi: 22.7, bodyFat: 19.5 },
|
||||||
|
{ id: 'r-008', time: '2026.04.30 08:10', weight: 70.3, bmi: 22.8, bodyFat: 19.7 },
|
||||||
|
{ id: 'r-009', time: '2026.04.20 07:45', weight: 71.0, bmi: 23.0, bodyFat: 20.0 },
|
||||||
|
{ id: 'r-010', time: '2026.04.10 08:30', weight: 71.8, bmi: 23.2, bodyFat: 20.3 },
|
||||||
|
{ id: 'r-011', time: '2026.03.28 08:15', weight: 72.5, bmi: 23.5, bodyFat: 20.7 },
|
||||||
|
{ id: 'r-012', time: '2026.03.15 09:00', weight: 73.2, bmi: 23.7, bodyFat: 21.0 },
|
||||||
|
{ id: 'r-013', time: '2026.03.01 08:05', weight: 73.8, bmi: 23.9, bodyFat: 21.3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 各时间范围截取数量 */
|
||||||
|
const RANGE_COUNTS: Record<TimeRange, number> = {
|
||||||
|
'7d': 7,
|
||||||
|
'30d': 10,
|
||||||
|
'3m': 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按时间范围获取 mock 记录列表
|
||||||
|
*/
|
||||||
|
export function getMockRecords(range: TimeRange): RecordItem[] {
|
||||||
|
return ALL_RECORDS.slice(0, RANGE_COUNTS[range])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据记录列表计算统计指标
|
||||||
|
*/
|
||||||
|
export function calcStats(records: RecordItem[]): StatsData {
|
||||||
|
if (!records.length) {
|
||||||
|
return { maxWeight: 0, minWeight: 0, avgWeight: 0, weightDiff: 0 }
|
||||||
|
}
|
||||||
|
const weights = records.map(r => r.weight)
|
||||||
|
const maxWeight = Math.max(...weights)
|
||||||
|
const minWeight = Math.min(...weights)
|
||||||
|
const avgWeight = parseFloat((weights.reduce((a, b) => a + b, 0) / weights.length).toFixed(1))
|
||||||
|
// 最新 - 最旧
|
||||||
|
const weightDiff = parseFloat((weights[0] - weights[weights.length - 1]).toFixed(1))
|
||||||
|
return { maxWeight, minWeight, avgWeight, weightDiff }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/** 最近一次测量记录 */
|
||||||
|
export interface WeightRecord {
|
||||||
|
/** 体重整数部分 */
|
||||||
|
weight: number
|
||||||
|
/** 体重小数部分,含小数点,如 ".20" */
|
||||||
|
weightDecimal: string
|
||||||
|
/** 测量时间显示,如 "今日 07:30" */
|
||||||
|
date: string
|
||||||
|
/** 与上次相比差值(负=减轻,正=增重) */
|
||||||
|
diff: number
|
||||||
|
/** 身高(cm) */
|
||||||
|
height: number
|
||||||
|
/** BMI 值 */
|
||||||
|
bmi: number
|
||||||
|
/** BMI 评级标签 */
|
||||||
|
bmiLabel: string
|
||||||
|
/** 标准体重(kg) */
|
||||||
|
stdWeight: number
|
||||||
|
/** 体脂率(%) */
|
||||||
|
bodyFat: number
|
||||||
|
/** 体脂肪(kg) */
|
||||||
|
bodyFatMass: number
|
||||||
|
/** 基础代谢(kcal) */
|
||||||
|
bmr: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mock 最近一次测量记录 */
|
||||||
|
export const mockLatestRecord: WeightRecord = {
|
||||||
|
weight: 68,
|
||||||
|
weightDecimal: '.20',
|
||||||
|
date: '今日 07:30',
|
||||||
|
diff: -0.3,
|
||||||
|
height: 176,
|
||||||
|
bmi: 22.4,
|
||||||
|
bmiLabel: '标准',
|
||||||
|
stdWeight: 67.2,
|
||||||
|
bodyFat: 22.3,
|
||||||
|
bodyFatMass: 15.2,
|
||||||
|
bmr: 1652,
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/** 成员项(与 equipmentMember / data 页面共享结构) */
|
||||||
|
export interface MemberItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
gender: '男' | '女'
|
||||||
|
age: number
|
||||||
|
/** 身高(cm) */
|
||||||
|
height: number
|
||||||
|
/** 头像色块 */
|
||||||
|
avatar: string
|
||||||
|
isSelf: boolean
|
||||||
|
/** 他人添加的成员(只读,不显示编辑/删除) */
|
||||||
|
isFromOther: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 头像色块池 */
|
||||||
|
const AVATAR_COLORS = ['#1385FA', '#2AC79F', '#FF8FAB', '#FFB347', '#A78BFA', '#F87171']
|
||||||
|
|
||||||
|
/** Mock 成员列表 */
|
||||||
|
export const mockMembers: MemberItem[] = [
|
||||||
|
{
|
||||||
|
id: 'm-001',
|
||||||
|
name: '陈小飞',
|
||||||
|
gender: '男',
|
||||||
|
age: 28,
|
||||||
|
height: 175,
|
||||||
|
avatar: AVATAR_COLORS[0],
|
||||||
|
isSelf: true,
|
||||||
|
isFromOther: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm-002',
|
||||||
|
name: '李镇南',
|
||||||
|
gender: '男',
|
||||||
|
age: 32,
|
||||||
|
height: 178,
|
||||||
|
avatar: AVATAR_COLORS[1],
|
||||||
|
isSelf: false,
|
||||||
|
isFromOther: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm-003',
|
||||||
|
name: '张颖',
|
||||||
|
gender: '女',
|
||||||
|
age: 26,
|
||||||
|
height: 165,
|
||||||
|
avatar: AVATAR_COLORS[2],
|
||||||
|
isSelf: false,
|
||||||
|
isFromOther: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm-004',
|
||||||
|
name: '王建国',
|
||||||
|
gender: '男',
|
||||||
|
age: 60,
|
||||||
|
height: 170,
|
||||||
|
avatar: AVATAR_COLORS[3],
|
||||||
|
isSelf: false,
|
||||||
|
isFromOther: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成下一个可用的头像色块
|
||||||
|
* @param existingCount 已有成员数量
|
||||||
|
*/
|
||||||
|
export function nextAvatarColor(existingCount: number): string {
|
||||||
|
return AVATAR_COLORS[existingCount % AVATAR_COLORS.length]
|
||||||
|
}
|
||||||
@@ -1,102 +1,102 @@
|
|||||||
/** 设备项模型 */
|
import { lefuService } from '../../lefu/index'
|
||||||
|
import type { ScannedDevice } from '../../lefu/index'
|
||||||
|
|
||||||
|
/** 展示用设备项 */
|
||||||
interface DeviceItem {
|
interface DeviceItem {
|
||||||
/** 设备唯一 id */
|
|
||||||
id: string
|
|
||||||
/** 设备名称 */
|
/** 设备名称 */
|
||||||
name: string
|
name: string
|
||||||
/** 设备图标 */
|
|
||||||
icon: string
|
|
||||||
/** 是否已连接 */
|
/** 是否已连接 */
|
||||||
connected: boolean
|
connected: boolean
|
||||||
|
/** 原始设备对象(连接时传回给 SDK) */
|
||||||
|
_raw: ScannedDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面状态 */
|
/** 页面状态 */
|
||||||
type DeviceStatus = 'idle' | 'searching' | 'found' | 'empty'
|
type DeviceStatus = 'idle' | 'searching' | 'found' | 'empty'
|
||||||
|
|
||||||
// 是否 mock 能搜索到设备,false 则走空状态
|
|
||||||
const MOCK_HAS_DEVICES = true
|
|
||||||
|
|
||||||
// 模拟搜索耗时(毫秒)
|
|
||||||
const MOCK_SEARCH_DURATION = 2000
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
/* 搜索定时器引用,onUnload 时清理 */
|
|
||||||
_searchTimer: null as number | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
/**
|
/**
|
||||||
* 页面状态
|
* 页面状态
|
||||||
* - idle: 未搜索(进入页面默认态)
|
* - idle: 未搜索(进入页面默认态)
|
||||||
* - searching:搜索中
|
* - searching: 扫描中
|
||||||
* - found: 搜索完成且有设备
|
* - found: 扫描完成且有设备
|
||||||
* - empty: 搜索完成但无设备
|
* - empty: 扫描完成但无设备
|
||||||
*/
|
*/
|
||||||
status: 'idle' as DeviceStatus,
|
status: 'idle' as DeviceStatus,
|
||||||
|
|
||||||
/* 设备列表(包含已连接和未连接,每项含 connected: boolean) */
|
/** 扫描到的设备列表 */
|
||||||
deviceList: [] as DeviceItem[],
|
deviceList: [] as DeviceItem[],
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onShow() {
|
||||||
|
// 注册回调后开始扫描
|
||||||
|
lefuService.onDevicesList((devices) => {
|
||||||
|
if (!devices.length) {
|
||||||
|
this.setData({ status: 'empty', deviceList: [] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list: DeviceItem[] = devices.map(d => ({
|
||||||
|
name: d.name,
|
||||||
|
connected: false,
|
||||||
|
_raw: d,
|
||||||
|
}))
|
||||||
|
this.setData({ status: 'found', deviceList: list })
|
||||||
|
})
|
||||||
|
|
||||||
|
lefuService.onConnectState((state) => {
|
||||||
|
const plugin = lefuService['_plugin']
|
||||||
|
if (!plugin) return
|
||||||
|
// 连接成功后跳转配网页
|
||||||
|
if (state === plugin.BLUE_STATE.CONNECTSUCCESS || state === plugin.BLUE_STATE.WIFISUCCESS) {
|
||||||
|
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.onStartSearch()
|
||||||
|
},
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
// 页面隐藏时停止扫描,释放 bus 订阅
|
||||||
|
lefuService.stopScan()
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
/* 清除搜索定时器,防止页面卸载后还触发 setData */
|
lefuService.stopScan()
|
||||||
if (this._searchTimer) {
|
|
||||||
clearTimeout(this._searchTimer)
|
|
||||||
this._searchTimer = null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 开始搜索 / 重新搜索 */
|
/** 开始 / 重新搜索 */
|
||||||
onStartSearch() {
|
onStartSearch() {
|
||||||
this.setData({
|
this.setData({ status: 'searching', deviceList: [] })
|
||||||
status: 'searching',
|
lefuService.startScan()
|
||||||
deviceList: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
/* 模拟搜索,后续接入蓝牙真实 API 时删除 */
|
|
||||||
this._searchTimer = setTimeout(() => {
|
|
||||||
if (MOCK_HAS_DEVICES) {
|
|
||||||
this.setData({
|
|
||||||
status: 'found',
|
|
||||||
deviceList: [
|
|
||||||
{ id: 'mock-connected', name: '智能体重秤 Pro', icon: '', connected: true },
|
|
||||||
{ id: 'mock-1', name: '体重秤 Basic', icon: '', connected: false },
|
|
||||||
{ id: 'mock-2', name: '体脂秤 Plus', icon: '', connected: false },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setData({ status: 'empty' })
|
|
||||||
}
|
|
||||||
}, MOCK_SEARCH_DURATION)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 连接某台设备:同时只能一台已连接,自动断开其他设备 */
|
/** 连接指定设备 */
|
||||||
onConnect(e: WechatMiniprogram.TouchEvent) {
|
onConnect(e: WechatMiniprogram.TouchEvent) {
|
||||||
const { id } = e.currentTarget.dataset
|
const index = e.currentTarget.dataset.index as number
|
||||||
const deviceList = this.data.deviceList.map(item => ({
|
const target = this.data.deviceList[index]
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
// 更新 UI 连接状态
|
||||||
|
const list = this.data.deviceList.map((item, i) => ({
|
||||||
...item,
|
...item,
|
||||||
connected: item.id === id,
|
connected: i === index,
|
||||||
}))
|
}))
|
||||||
this.setData({ deviceList })
|
this.setData({ deviceList: list })
|
||||||
/* TODO: 接入真实蓝牙连接逻辑 */
|
|
||||||
|
// 调用 SDK 连接(BleAdv 类型已在 startScan 内自动处理,BleConnect 类型需此处触发)
|
||||||
|
lefuService.connect(target._raw)
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 断开当前已连接设备 */
|
/** 断开当前连接 */
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
const deviceList = this.data.deviceList.map(item => ({
|
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
|
||||||
...item,
|
this.setData({ deviceList: list })
|
||||||
connected: false,
|
lefuService.disconnect()
|
||||||
}))
|
|
||||||
this.setData({ deviceList })
|
|
||||||
/* TODO: 接入真实蓝牙断开逻辑 */
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 查看帮助 */
|
/** 查看帮助 */
|
||||||
onOpenHelp() {
|
onOpenHelp() {
|
||||||
console.log('查看帮助')
|
wx.showToast({ title: '请确保设备已开机且在附近', icon: 'none' })
|
||||||
/* TODO: 跳帮助页 */
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,179 +1,167 @@
|
|||||||
/** WiFi 项模型 */
|
import { lefuService } from '../../lefu/index'
|
||||||
|
import type { LeFuWifiItem, WifiVersion } from '../../lefu/index'
|
||||||
|
|
||||||
|
/** WiFi 展示项 */
|
||||||
interface WifiItem {
|
interface WifiItem {
|
||||||
/** WiFi 唯一 id */
|
ssid: string
|
||||||
id: string
|
|
||||||
/** WiFi 名称(SSID) */
|
|
||||||
name: string
|
|
||||||
/** 是否已通过本页配网成功 */
|
/** 是否已通过本页配网成功 */
|
||||||
connected: boolean
|
connected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前已连接设备(顶部卡片) */
|
/** 当前已连接设备(顶部卡片) */
|
||||||
interface CurrentDevice {
|
interface CurrentDevice {
|
||||||
/** 设备唯一 id */
|
|
||||||
id: string
|
id: string
|
||||||
/** 设备名称 */
|
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 密码态选中的 WiFi */
|
/** 密码态选中的 WiFi */
|
||||||
interface SelectedWifi {
|
interface SelectedWifi {
|
||||||
/** WiFi id,空字符串表示未选中 */
|
ssid: string
|
||||||
id: string
|
|
||||||
/** WiFi 名称 */
|
|
||||||
name: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面状态 */
|
/** 页面状态 */
|
||||||
type WifiStatus = 'idle' | 'searching' | 'list' | 'empty' | 'password'
|
type WifiStatus = 'idle' | 'searching' | 'list' | 'empty' | 'password' | 'configuring'
|
||||||
|
|
||||||
// 是否 mock 能搜索到 WiFi,false 则走空状态
|
|
||||||
const MOCK_HAS_WIFI = true
|
|
||||||
|
|
||||||
// 模拟搜索耗时(毫秒)
|
|
||||||
const MOCK_SEARCH_DURATION = 600
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
/* 搜索定时器引用,onUnload 时清理 */
|
/** 配网协议版本,两种都支持,默认 domain2(有鉴权) */
|
||||||
_searchTimer: null as number | null,
|
_wifiVersion: 'domain2' as WifiVersion,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
/**
|
/**
|
||||||
* 页面状态
|
* 页面状态
|
||||||
* - idle: 未搜索(进入页面默认态,onLoad 立即流转)
|
* - idle: 未搜索
|
||||||
* - searching: 搜索中
|
* - searching: 正在从设备获取 WiFi 列表
|
||||||
* - list: 搜索完成且有 WiFi
|
* - list: 列表已就绪
|
||||||
* - empty: 搜索完成但无 WiFi
|
* - empty: 设备未扫描到任何 WiFi
|
||||||
* - password: 从列表选中某项后进入密码输入态
|
* - password: 选中某项,正在输入密码
|
||||||
|
* - configuring: 正在将 WiFi 写入设备
|
||||||
*/
|
*/
|
||||||
status: 'idle' as WifiStatus,
|
status: 'idle' as WifiStatus,
|
||||||
|
|
||||||
/* 当前已连接的设备(从上一页带过来的固定卡片信息) */
|
/** 顶部固定卡片(从上一页状态中读取,当前先用占位) */
|
||||||
currentDevice: {
|
currentDevice: {
|
||||||
id: 'mock-device',
|
id: '',
|
||||||
name: '智能体重秤 Pro',
|
name: '智能体重秤',
|
||||||
} as CurrentDevice,
|
} as CurrentDevice,
|
||||||
|
|
||||||
/* WiFi 列表(connected: true 表示已通过本页配网成功的网络) */
|
/** WiFi 列表 */
|
||||||
wifiList: [] as WifiItem[],
|
wifiList: [] as WifiItem[],
|
||||||
|
|
||||||
/* 是否已有任一 WiFi 配网成功,决定底部「确认」按钮是否可点击 */
|
/** 是否已有任一 WiFi 配网成功 */
|
||||||
hasConnected: false,
|
hasConnected: false,
|
||||||
|
|
||||||
/* 当前在密码态选中的 WiFi */
|
/** 当前密码态选中的 WiFi */
|
||||||
selectedWifi: {
|
selectedWifi: { ssid: '' } as SelectedWifi,
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
} as SelectedWifi,
|
|
||||||
|
|
||||||
/* 密码输入框值 */
|
/** 密码输入框值 */
|
||||||
selectWifiPWD: '',
|
selectWifiPWD: '',
|
||||||
|
|
||||||
/* 密码框明文/密文切换:true 为密文(默认),false 为明文 */
|
/** 密码框明文/密文切换:true 为密文(默认) */
|
||||||
type: true,
|
type: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
/* 设计稿表达"进入页面立即搜索",但状态机仍由 idle 起步,
|
// idle 起步,通过统一入口触发搜索
|
||||||
* 此处手动调用按钮事件,避免"自动逻辑"与"按钮逻辑"分叉 */
|
|
||||||
this.onStartSearch()
|
this.onStartSearch()
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
/* 清除搜索定时器,防止页面卸载后还触发 setData */
|
// 页面卸载时不调用 stopScan,蓝牙连接由 home 页维持
|
||||||
if (this._searchTimer) {
|
|
||||||
clearTimeout(this._searchTimer)
|
|
||||||
this._searchTimer = null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 开始搜索:idle/empty/list → searching → list/empty */
|
/** 从设备获取 WiFi 列表 */
|
||||||
onStartSearch() {
|
onStartSearch() {
|
||||||
this.setData({ status: 'searching' })
|
this.setData({ status: 'searching', wifiList: [] })
|
||||||
|
|
||||||
this._searchTimer = setTimeout(() => {
|
lefuService.getWifiList()
|
||||||
if (MOCK_HAS_WIFI) {
|
.then((items: LeFuWifiItem[]) => {
|
||||||
this.setData({
|
if (!items.length) {
|
||||||
status: 'list',
|
this.setData({ status: 'empty' })
|
||||||
wifiList: [
|
return
|
||||||
{ id: 'wifi-1', name: 'Rk-Health-5G', connected: false },
|
}
|
||||||
{ id: 'wifi-2', name: 'Rk-Guest', connected: false },
|
const list: WifiItem[] = items.map(item => ({
|
||||||
{ id: 'wifi-3', name: 'ChinaNet-7Hk2', connected: false },
|
ssid: item.ssid,
|
||||||
{ id: 'wifi-4', name: 'TP-LINK_8899', connected: false },
|
connected: false,
|
||||||
],
|
}))
|
||||||
})
|
this.setData({ status: 'list', wifiList: list })
|
||||||
} else {
|
})
|
||||||
this.setData({ status: 'empty', wifiList: [] })
|
.catch(() => {
|
||||||
}
|
this.setData({ status: 'empty' })
|
||||||
}, MOCK_SEARCH_DURATION)
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 空态「刷新」:重新进入搜索 */
|
/** 空态刷新 */
|
||||||
onRefresh() {
|
onRefresh() {
|
||||||
this.onStartSearch()
|
this.onStartSearch()
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 点击某个 WiFi 项 → 进密码态 */
|
/** 点击 WiFi 项 → 进密码态 */
|
||||||
onTapWifi(e: WechatMiniprogram.TouchEvent) {
|
onTapWifi(e: WechatMiniprogram.TouchEvent) {
|
||||||
const id = e.currentTarget.dataset.id as string
|
const ssid = e.currentTarget.dataset.ssid as string
|
||||||
const target = this.data.wifiList.find(item => item.id === id)
|
const target = this.data.wifiList.find(item => item.ssid === ssid)
|
||||||
if (!target) return
|
if (!target || target.connected) return
|
||||||
|
|
||||||
/* 已连接项再次点击不进入密码态 */
|
|
||||||
if (target.connected) return
|
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
status: 'password',
|
status: 'password',
|
||||||
selectedWifi: { id: target.id, name: target.name },
|
selectedWifi: { ssid },
|
||||||
selectWifiPWD: '',
|
selectWifiPWD: '',
|
||||||
type: true,
|
type: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 密码输入 */
|
/** 密码输入 */
|
||||||
passwordInput(e: WechatMiniprogram.Input) {
|
passwordInput(e: WechatMiniprogram.Input) {
|
||||||
this.setData({ selectWifiPWD: e.detail.value })
|
this.setData({ selectWifiPWD: e.detail.value })
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 密码明文/密文切换 */
|
/** 明文/密文切换 */
|
||||||
inputTypeChange() {
|
inputTypeChange() {
|
||||||
this.setData({ type: !this.data.type })
|
this.setData({ type: !this.data.type })
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 密码态「取消」:回列表态,清空当前选中 */
|
/** 密码态「取消」→ 回列表 */
|
||||||
onCancelPwd() {
|
onCancelPwd() {
|
||||||
this.setData({
|
this.setData({
|
||||||
status: 'list',
|
status: 'list',
|
||||||
selectedWifi: { id: '', name: '' },
|
selectedWifi: { ssid: '' },
|
||||||
selectWifiPWD: '',
|
selectWifiPWD: '',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 密码态「连接」:把对应项标 connected,回列表态 */
|
/** 密码态「连接」→ 将 WiFi 写入设备 */
|
||||||
onConfirmPwd() {
|
onConfirmPwd() {
|
||||||
const { selectedWifi, wifiList } = this.data
|
const { selectedWifi, selectWifiPWD } = this.data
|
||||||
if (!selectedWifi.id) return
|
if (!selectedWifi.ssid) return
|
||||||
|
|
||||||
const next = wifiList.map(item => ({
|
this.setData({ status: 'configuring' })
|
||||||
...item,
|
wx.showLoading({ title: '正在配网...', mask: true })
|
||||||
connected: item.id === selectedWifi.id,
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.setData({
|
lefuService.configWifi(selectedWifi.ssid, selectWifiPWD, this._wifiVersion)
|
||||||
status: 'list',
|
.then(() => {
|
||||||
wifiList: next,
|
wx.hideLoading()
|
||||||
hasConnected: true,
|
// 配网成功:标记该 WiFi,回列表态
|
||||||
selectedWifi: { id: '', name: '' },
|
const next = this.data.wifiList.map(item => ({
|
||||||
selectWifiPWD: '',
|
...item,
|
||||||
})
|
connected: item.ssid === selectedWifi.ssid,
|
||||||
|
}))
|
||||||
|
this.setData({
|
||||||
|
status: 'list',
|
||||||
|
wifiList: next,
|
||||||
|
hasConnected: true,
|
||||||
|
selectedWifi: { ssid: '' },
|
||||||
|
selectWifiPWD: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
wx.hideLoading()
|
||||||
|
wx.showToast({ title: err.message || '配网失败,请重试', icon: 'none' })
|
||||||
|
this.setData({ status: 'password' })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 列表底部「确认」:必须有连接成功的 WiFi 才可点击 */
|
/** 底部「确认」:已有 WiFi 配网成功才可点击 */
|
||||||
onConfirm() {
|
onConfirm() {
|
||||||
if (!this.data.hasConnected) return
|
if (!this.data.hasConnected) return
|
||||||
|
wx.navigateTo({ url: '/pages/supplementPersonal/supplementPersonal' })
|
||||||
wx.navigateTo({
|
|
||||||
url: '/pages/supplementPersonal/supplementPersonal',
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
height: 90rpx;
|
height: 90rpx;
|
||||||
margin-right: 24rpx;
|
margin-right: 24rpx;
|
||||||
border-radius: 18rpx;
|
border-radius: 18rpx;
|
||||||
|
background: var(--avatar-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 姓名 + meta */
|
/* 姓名 + meta */
|
||||||
|
|||||||
+55
-141
@@ -1,159 +1,73 @@
|
|||||||
/** 时间范围 */
|
import { getMockRecords, calcStats } from '../../mock/data.mock'
|
||||||
type TimeRange = '7d' | '30d' | '3m'
|
import type { TimeRange, RecordItem, StatsData } from '../../mock/data.mock'
|
||||||
|
import type { MemberItem } from '../../mock/member.mock'
|
||||||
|
import { mockMembers } from '../../mock/member.mock'
|
||||||
|
|
||||||
/** 加载状态机 */
|
/** 加载状态机 */
|
||||||
type LoadStatus = 'idle' | 'loading' | 'loaded'
|
type LoadStatus = 'idle' | 'loading' | 'loaded'
|
||||||
|
|
||||||
/** 成员信息 */
|
|
||||||
interface MemberInfo {
|
|
||||||
/** 唯一 ID */
|
|
||||||
id: string
|
|
||||||
/** 姓名 */
|
|
||||||
name: string
|
|
||||||
/** 性别 */
|
|
||||||
gender: '男' | '女'
|
|
||||||
/** 年龄 */
|
|
||||||
age: number
|
|
||||||
/** 身高(cm) */
|
|
||||||
height: number
|
|
||||||
/** 头像色块(无图片时使用) */
|
|
||||||
avatar: string
|
|
||||||
/** 是否本人 */
|
|
||||||
isSelf: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 统计指标 */
|
|
||||||
interface StatsData {
|
|
||||||
/** 最高体重(kg) */
|
|
||||||
maxWeight: number
|
|
||||||
/** 最低体重(kg) */
|
|
||||||
minWeight: number
|
|
||||||
/** 平均体重(kg) */
|
|
||||||
avgWeight: number
|
|
||||||
/** 体重变化(kg,正负) */
|
|
||||||
weightDiff: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 时间范围 Tab 项 */
|
/** 时间范围 Tab 项 */
|
||||||
interface RangeTab {
|
interface RangeTab {
|
||||||
key: TimeRange
|
key: TimeRange
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单条测量记录 */
|
|
||||||
interface RecordItem {
|
|
||||||
/** 唯一 ID */
|
|
||||||
id: string
|
|
||||||
/** 测量时间,格式 YYYY.MM.DD HH:mm */
|
|
||||||
time: string
|
|
||||||
/** 体重(kg) */
|
|
||||||
weight: number
|
|
||||||
/** BMI */
|
|
||||||
bmi: number
|
|
||||||
/** 体脂率(%) */
|
|
||||||
bodyFat: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 历史数据页
|
|
||||||
* 状态机:idle → loading → loaded
|
|
||||||
* 切换时间范围 / 切换成员 均复用同一加载流程
|
|
||||||
*/
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
// 加载状态
|
status: 'idle' as LoadStatus,
|
||||||
status: 'idle' as LoadStatus,
|
activeRange: '7d' as TimeRange,
|
||||||
|
rangeTabs: [
|
||||||
|
{ key: '7d', label: '近7天' },
|
||||||
|
{ key: '30d', label: '近30天' },
|
||||||
|
{ key: '3m', label: '近3月' },
|
||||||
|
] as RangeTab[],
|
||||||
|
|
||||||
// 当前选中的时间范围
|
/** 当前查看的成员(默认本人) */
|
||||||
activeRange: '7d' as TimeRange,
|
currentMember: null as MemberItem | null,
|
||||||
|
|
||||||
// 时间范围 Tab 选项
|
stats: null as StatsData | null,
|
||||||
rangeTabs: [
|
recordList: [] as RecordItem[],
|
||||||
{ key: '7d', label: '近7天' },
|
},
|
||||||
{ key: '30d', label: '近30天' },
|
|
||||||
{ key: '3m', label: '近3月' }
|
|
||||||
] as RangeTab[],
|
|
||||||
|
|
||||||
// 当前成员
|
onLoad() {
|
||||||
currentMember: {
|
// 默认选本人(isSelf=true),后备取第一个
|
||||||
id: 'm-001',
|
const members: MemberItem[] = wx.getStorageSync('memberList') || mockMembers
|
||||||
name: '陈小飞',
|
const self = members.find(m => m.isSelf) || members[0] || null
|
||||||
gender: '男',
|
this.setData({ currentMember: self })
|
||||||
age: 28,
|
this.loadData()
|
||||||
height: 175,
|
},
|
||||||
avatar: '#1385FA',
|
|
||||||
isSelf: true
|
|
||||||
} as MemberInfo,
|
|
||||||
|
|
||||||
// 统计数据
|
onReady() {},
|
||||||
stats: {
|
onShow() {},
|
||||||
maxWeight: 73.5,
|
onHide() {},
|
||||||
minWeight: 71.2,
|
onUnload() {},
|
||||||
avgWeight: 72.3,
|
onPullDownRefresh() {},
|
||||||
weightDiff: -2.3
|
onReachBottom() {},
|
||||||
} as StatsData,
|
|
||||||
|
|
||||||
// 历史记录列表
|
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
|
||||||
recordList: [] as RecordItem[]
|
return {}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
/** 加载数据(切 Tab / 切成员 复用同一入口) */
|
||||||
// idle 起步,通过统一入口触发加载
|
loadData() {
|
||||||
this.loadData()
|
this.setData({ status: 'loading' })
|
||||||
},
|
setTimeout(() => {
|
||||||
|
const records = getMockRecords(this.data.activeRange)
|
||||||
|
const stats = calcStats(records)
|
||||||
|
this.setData({ recordList: records, stats, status: 'loaded' })
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
|
||||||
onReady() {},
|
/** 切换时间范围 Tab */
|
||||||
|
onTapRange(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const key = e.currentTarget.dataset.key as TimeRange
|
||||||
|
if (key === this.data.activeRange) return
|
||||||
|
this.setData({ activeRange: key })
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
|
||||||
onShow() {},
|
/** 切换成员 */
|
||||||
|
onTapSwitchMember() {
|
||||||
onHide() {},
|
wx.showToast({ title: '切换成员', icon: 'none' })
|
||||||
|
},
|
||||||
onUnload() {},
|
|
||||||
|
|
||||||
onPullDownRefresh() {},
|
|
||||||
|
|
||||||
onReachBottom() {},
|
|
||||||
|
|
||||||
onShareAppMessage(opts): WechatMiniprogram.Page.ICustomShareContent {
|
|
||||||
console.log(opts.target)
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载数据(切 Tab / 切成员 复用)
|
|
||||||
*/
|
|
||||||
loadData() {
|
|
||||||
this.setData({ status: 'loading' })
|
|
||||||
|
|
||||||
// TODO: 后续接入真实接口,这里先用 mock
|
|
||||||
const mockList: RecordItem[] = [
|
|
||||||
{ id: 'r-001', time: '2026.05.12 08:12', weight: 71.2, bmi: 23.2, bodyFat: 18.5 },
|
|
||||||
{ id: 'r-002', time: '2026.05.11 21:30', weight: 71.8, bmi: 23.4, bodyFat: 18.7 },
|
|
||||||
{ id: 'r-003', time: '2026.05.11 08:05', weight: 71.5, bmi: 23.3, bodyFat: 18.6 },
|
|
||||||
{ id: 'r-004', time: '2026.05.10 07:58', weight: 72.0, bmi: 23.5, bodyFat: 18.9 },
|
|
||||||
{ id: 'r-005', time: '2026.05.10 22:10', weight: 72.6, bmi: 23.7, bodyFat: 19.1 },
|
|
||||||
{ id: 'r-006', time: '2026.05.09 08:20', weight: 72.8, bmi: 23.8, bodyFat: 19.2 },
|
|
||||||
{ id: 'r-007', time: '2026.05.08 08:00', weight: 73.5, bmi: 24.0, bodyFat: 19.5 }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.setData({ recordList: mockList, status: 'loaded' })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换时间范围 Tab
|
|
||||||
*/
|
|
||||||
onTapRange(e: WechatMiniprogram.TouchEvent) {
|
|
||||||
const key = e.currentTarget.dataset.key as TimeRange
|
|
||||||
if (key === this.data.activeRange) return
|
|
||||||
this.setData({ activeRange: key })
|
|
||||||
this.loadData()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换成员(后续接入成员选择浮层)
|
|
||||||
*/
|
|
||||||
onTapSwitchMember() {
|
|
||||||
// TODO: 接入成员选择浮层
|
|
||||||
wx.showToast({ title: '切换成员', icon: 'none' })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<!-- 成员信息 -->
|
<!-- 成员信息 -->
|
||||||
<view class="userInfo">
|
<view class="userInfo">
|
||||||
<view class="avatar" style="background: {{ currentMember.avatar }};"></view>
|
<view class="avatar" style="--avatar-bg: {{ currentMember.avatar }};"></view>
|
||||||
<view class="info">
|
<view class="info">
|
||||||
<view class="info-name-row">
|
<view class="info-name-row">
|
||||||
<view class="info-name">{{ currentMember.name }}</view>
|
<view class="info-name">{{ currentMember.name }}</view>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
margin-right: 24rpx;
|
margin-right: 24rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 3rpx solid #50A6FF;
|
border: 3rpx solid #50A6FF;
|
||||||
|
background: var(--avatar-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-info {
|
.member-info {
|
||||||
|
|||||||
@@ -1,135 +1,132 @@
|
|||||||
/** 成员项 */
|
import { lefuService } from '../../lefu/index'
|
||||||
interface MemberItem {
|
import type { DeviceMember } from '../../lefu/index'
|
||||||
/** 唯一 ID */
|
import { mockMembers } from '../../mock/member.mock'
|
||||||
id: string
|
import type { MemberItem } from '../../mock/member.mock'
|
||||||
/** 姓名 */
|
|
||||||
name: string
|
|
||||||
/** 性别 */
|
|
||||||
gender: '男' | '女'
|
|
||||||
/** 年龄 */
|
|
||||||
age: number
|
|
||||||
/** 身高(cm) */
|
|
||||||
height: number
|
|
||||||
/** 头像色块(无图片时使用) */
|
|
||||||
avatar: string
|
|
||||||
/** 是否本人 */
|
|
||||||
isSelf: boolean
|
|
||||||
/** 是否他人添加(只读,不显示编辑/删除) */
|
|
||||||
isFromOther: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 设备卡片 */
|
/** 设备卡片 */
|
||||||
interface DeviceCard {
|
interface DeviceCard {
|
||||||
/** 设备 ID */
|
id: string
|
||||||
id: string
|
name: string
|
||||||
/** 设备名称 */
|
capacityUsed: number
|
||||||
name: string
|
capacityTotal: number
|
||||||
/** 已使用容量(成员数) */
|
|
||||||
capacityUsed: number
|
|
||||||
/** 总容量 */
|
|
||||||
capacityTotal: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备成员页
|
* MemberItem → DeviceMember 转换
|
||||||
* 两态:已连接(connected=true) / 未连接(connected=false)
|
* lefu 层只认识 DeviceMember,业务层负责转换
|
||||||
* connected 为页面内部全局状态,后续接入真实设备状态时替换
|
|
||||||
*/
|
*/
|
||||||
|
function toDeviceMember(m: MemberItem): DeviceMember {
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
gender: m.gender === '男' ? 1 : 0,
|
||||||
|
age: m.age,
|
||||||
|
height: m.height,
|
||||||
|
isSelf: m.isSelf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将成员列表全量同步到设备
|
||||||
|
* 成功/失败均给出 Toast 提示
|
||||||
|
*/
|
||||||
|
function syncToDevice(members: MemberItem[]): void {
|
||||||
|
const deviceMembers = members.map(toDeviceMember)
|
||||||
|
wx.showLoading({ title: '正在同步成员...', mask: true })
|
||||||
|
lefuService.syncMembersToDevice(deviceMembers)
|
||||||
|
.then(() => {
|
||||||
|
wx.hideLoading()
|
||||||
|
wx.showToast({ title: '成员同步成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
wx.hideLoading()
|
||||||
|
wx.showToast({ title: err.message || '同步失败,请重试', icon: 'none' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
// 设备连接状态(全局,内部控制)
|
/** 设备连接状态(后续接入 lefuService.onConnectState 替换) */
|
||||||
connected: true,
|
connected: true,
|
||||||
|
|
||||||
// 设备卡片
|
deviceCard: {
|
||||||
deviceCard: {
|
id: 'device-001',
|
||||||
id: 'device-001',
|
name: 'MX-W30A1',
|
||||||
name: 'MX-W30A1',
|
capacityUsed: 0,
|
||||||
capacityUsed: 4,
|
capacityTotal: 10,
|
||||||
capacityTotal: 10
|
} as DeviceCard,
|
||||||
} as DeviceCard,
|
|
||||||
|
|
||||||
// 成员列表(色块占位头像)
|
memberList: [] as MemberItem[],
|
||||||
memberList: [
|
},
|
||||||
{
|
|
||||||
id: 'm-001',
|
|
||||||
name: '陈小飞',
|
|
||||||
gender: '男',
|
|
||||||
age: 28,
|
|
||||||
height: 175,
|
|
||||||
avatar: '#1385FA',
|
|
||||||
isSelf: true,
|
|
||||||
isFromOther: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'm-002',
|
|
||||||
name: '李镇南',
|
|
||||||
gender: '男',
|
|
||||||
age: 32,
|
|
||||||
height: 178,
|
|
||||||
avatar: '#2AC79F',
|
|
||||||
isSelf: false,
|
|
||||||
isFromOther: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'm-003',
|
|
||||||
name: '张颖',
|
|
||||||
gender: '女',
|
|
||||||
age: 26,
|
|
||||||
height: 165,
|
|
||||||
avatar: '#FF8FAB',
|
|
||||||
isSelf: false,
|
|
||||||
isFromOther: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'm-004',
|
|
||||||
name: '李镇南',
|
|
||||||
gender: '男',
|
|
||||||
age: 60,
|
|
||||||
height: 170,
|
|
||||||
avatar: '#FFB347',
|
|
||||||
isSelf: false,
|
|
||||||
isFromOther: true
|
|
||||||
}
|
|
||||||
] as MemberItem[]
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
onLoad() {
|
||||||
* 编辑成员
|
this._loadMembers()
|
||||||
*/
|
},
|
||||||
onTapEdit(e: WechatMiniprogram.TouchEvent) {
|
|
||||||
const id = e.currentTarget.dataset.id as string
|
|
||||||
// TODO: 后续接入编辑成员浮层/页面
|
|
||||||
wx.showToast({ title: `编辑 ${id}`, icon: 'none' })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/** 从 storage / mock 加载成员列表 */
|
||||||
* 删除成员
|
_loadMembers() {
|
||||||
*/
|
const list: MemberItem[] = wx.getStorageSync('memberList') || mockMembers
|
||||||
onTapDelete(e: WechatMiniprogram.TouchEvent) {
|
this.setData({
|
||||||
const id = e.currentTarget.dataset.id as string
|
memberList: list,
|
||||||
wx.showModal({
|
'deviceCard.capacityUsed': list.length,
|
||||||
title: '提示',
|
})
|
||||||
content: '确定删除该成员?',
|
},
|
||||||
success: (res) => {
|
|
||||||
if (!res.confirm) return
|
|
||||||
// TODO: 后续接入删除成员接口
|
|
||||||
const list = this.data.memberList.filter(m => m.id !== id)
|
|
||||||
this.setData({
|
|
||||||
memberList: list,
|
|
||||||
'deviceCard.capacityUsed': list.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/** 编辑成员 */
|
||||||
* 添加成员:跳转到 addMember 页
|
onTapEdit(e: WechatMiniprogram.TouchEvent) {
|
||||||
* 未连接态禁用
|
const id = e.currentTarget.dataset.id as string
|
||||||
*/
|
// TODO: 跳转编辑成员页面,编辑完成后同样需要调用 syncToDevice
|
||||||
onTapAddMember() {
|
wx.showToast({ title: `编辑 ${id}`, icon: 'none' })
|
||||||
if (!this.data.connected) return
|
},
|
||||||
wx.navigateTo({
|
|
||||||
url: '/pages/addMember/addMember'
|
/** 删除成员:更新本地 → 同步设备 */
|
||||||
})
|
onTapDelete(e: WechatMiniprogram.TouchEvent) {
|
||||||
}
|
const id = e.currentTarget.dataset.id as string
|
||||||
|
wx.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定删除该成员?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
|
||||||
|
const list = this.data.memberList.filter(m => m.id !== id)
|
||||||
|
wx.setStorageSync('memberList', list)
|
||||||
|
this.setData({
|
||||||
|
memberList: list,
|
||||||
|
'deviceCard.capacityUsed': list.length,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除后立即同步到设备
|
||||||
|
if (this.data.connected) {
|
||||||
|
syncToDevice(list)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 添加成员:跳转 addMember 页(未连接态禁用) */
|
||||||
|
onTapAddMember() {
|
||||||
|
if (!this.data.connected) return
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '/pages/addMember/addMember',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addMember 页保存成功后返回时刷新列表并同步设备
|
||||||
|
* 在 onShow 中检测 storage 变化
|
||||||
|
*/
|
||||||
|
onShow() {
|
||||||
|
const stored: MemberItem[] = wx.getStorageSync('memberList') || []
|
||||||
|
const current = this.data.memberList
|
||||||
|
// 数量有变化说明刚刚新增了成员
|
||||||
|
if (stored.length !== current.length) {
|
||||||
|
this.setData({
|
||||||
|
memberList: stored,
|
||||||
|
'deviceCard.capacityUsed': stored.length,
|
||||||
|
})
|
||||||
|
if (this.data.connected) {
|
||||||
|
syncToDevice(stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<block wx:for="{{ memberList }}" wx:key="id">
|
<block wx:for="{{ memberList }}" wx:key="id">
|
||||||
<view class="member-item">
|
<view class="member-item">
|
||||||
<!-- 头像色块 -->
|
<!-- 头像色块 -->
|
||||||
<view class="member-avatar" style="background: {{ item.avatar }};"></view>
|
<view class="member-avatar" style="--avatar-bg: {{ item.avatar }};"></view>
|
||||||
|
|
||||||
<!-- 信息列 -->
|
<!-- 信息列 -->
|
||||||
<view class="member-info">
|
<view class="member-info">
|
||||||
|
|||||||
+46
-103
@@ -1,118 +1,61 @@
|
|||||||
|
import { mockLatestRecord } from '../../mock/home.mock'
|
||||||
|
import type { WeightRecord } from '../../mock/home.mock'
|
||||||
|
|
||||||
/** 加载状态机 */
|
/** 加载状态机 */
|
||||||
type LoadStatus = 'idle' | 'loading' | 'loaded'
|
type LoadStatus = 'idle' | 'loading' | 'loaded'
|
||||||
|
|
||||||
/** 设备信息 */
|
/** 设备信息 */
|
||||||
interface DeviceInfo {
|
interface DeviceInfo {
|
||||||
/** 设备名称 */
|
name: string
|
||||||
name: string
|
connected: boolean
|
||||||
/** 蓝牙是否已连接 */
|
|
||||||
connected: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 最近一次测量记录 */
|
|
||||||
interface WeightRecord {
|
|
||||||
/** 体重整数部分 */
|
|
||||||
weight: number
|
|
||||||
/** 体重小数部分,含小数点,如 ".20" */
|
|
||||||
weightDecimal: string
|
|
||||||
/** 测量时间显示,如 "今日 07:30" */
|
|
||||||
date: string
|
|
||||||
/** 与上次相比差值(负=减轻,正=增重) */
|
|
||||||
diff: number
|
|
||||||
/** 身高(cm) */
|
|
||||||
height: number
|
|
||||||
/** BMI 值 */
|
|
||||||
bmi: number
|
|
||||||
/** BMI 评级标签,如 "标准" */
|
|
||||||
bmiLabel: string
|
|
||||||
/** 标准体重(kg) */
|
|
||||||
stdWeight: number
|
|
||||||
/** 体脂率(%) */
|
|
||||||
bodyFat: number
|
|
||||||
/** 体脂肪(kg) */
|
|
||||||
bodyFatMass: number
|
|
||||||
/** 基础代谢(kcal) */
|
|
||||||
bmr: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 首页
|
|
||||||
* 状态机:idle → loading → loaded
|
|
||||||
* loaded 后按 hasData 分为有数据态 / 空态
|
|
||||||
*/
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
// 加载状态
|
status: 'idle' as LoadStatus,
|
||||||
status: 'idle' as LoadStatus,
|
hasData: false,
|
||||||
|
device: {
|
||||||
|
name: '智能体重秤',
|
||||||
|
connected: false,
|
||||||
|
} as DeviceInfo,
|
||||||
|
record: null as WeightRecord | null,
|
||||||
|
},
|
||||||
|
|
||||||
// 是否有称重数据
|
onLoad() {
|
||||||
hasData: true,
|
this.loadData()
|
||||||
|
},
|
||||||
|
|
||||||
// 设备信息
|
onReady() {},
|
||||||
device: {
|
onShow() {},
|
||||||
name: '智能体重秤 Pro',
|
onHide() {},
|
||||||
connected: true
|
onUnload() {},
|
||||||
} as DeviceInfo,
|
onPullDownRefresh() {},
|
||||||
|
onReachBottom() {},
|
||||||
|
|
||||||
// 最近一次测量记录(hasData=true 时有效)
|
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
|
||||||
record: {
|
return {}
|
||||||
weight: 68,
|
},
|
||||||
weightDecimal: '.20',
|
|
||||||
date: '今日 07:30',
|
|
||||||
diff: -0.3,
|
|
||||||
height: 176,
|
|
||||||
bmi: 22.4,
|
|
||||||
bmiLabel: '标准',
|
|
||||||
stdWeight: 67.2,
|
|
||||||
bodyFat: 22.3,
|
|
||||||
bodyFatMass: 52.1,
|
|
||||||
bmr: 1652
|
|
||||||
} as WeightRecord
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoad() {
|
/** 加载首页数据(当前使用 mock,后续替换为真实接口) */
|
||||||
// idle 起步,通过统一入口触发加载
|
loadData() {
|
||||||
this.loadData()
|
this.setData({ status: 'loading' })
|
||||||
},
|
// 模拟异步加载
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setData({
|
||||||
|
status: 'loaded',
|
||||||
|
hasData: true,
|
||||||
|
record: mockLatestRecord,
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
|
||||||
onReady() {},
|
/** 跳转历史数据页 */
|
||||||
|
onTapHistory() {
|
||||||
|
wx.switchTab({ url: '/pages/data/data' })
|
||||||
|
},
|
||||||
|
|
||||||
onShow() {},
|
/** 切换设备 */
|
||||||
|
onTapSwitchDevice() {
|
||||||
onHide() {},
|
wx.showToast({ title: '切换设备', icon: 'none' })
|
||||||
|
},
|
||||||
onUnload() {},
|
|
||||||
|
|
||||||
onPullDownRefresh() {},
|
|
||||||
|
|
||||||
onReachBottom() {},
|
|
||||||
|
|
||||||
onShareAppMessage(opts): WechatMiniprogram.Page.ICustomShareContent {
|
|
||||||
console.log(opts.target)
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载首页数据(后续接入真实接口)
|
|
||||||
*/
|
|
||||||
loadData() {
|
|
||||||
this.setData({ status: 'loading' })
|
|
||||||
// TODO: 后续接入真实接口,当前使用 mock
|
|
||||||
this.setData({ status: 'loaded' })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 跳转数据页查看历史
|
|
||||||
*/
|
|
||||||
onTapHistory() {
|
|
||||||
wx.switchTab({ url: '/pages/data/data' })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换设备(后续接入设备选择浮层)
|
|
||||||
*/
|
|
||||||
onTapSwitchDevice() {
|
|
||||||
// TODO: 接入设备选择浮层
|
|
||||||
wx.showToast({ title: '切换设备', icon: 'none' })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type { ApiResponse, HttpMethod, RequestOptions } from './types'
|
||||||
|
|
||||||
|
export type { ApiResponse, RequestOptions }
|
||||||
|
|
||||||
|
const BASE_URL = 'https://device.shuziweidao.com/gateway/'
|
||||||
|
|
||||||
|
let _loadingCount = 0
|
||||||
|
|
||||||
|
const _showLoading = (title: string): void => {
|
||||||
|
if (_loadingCount === 0) wx.showLoading({ title, mask: true })
|
||||||
|
_loadingCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
const _hideLoading = (): void => {
|
||||||
|
if (_loadingCount > 0) _loadingCount--
|
||||||
|
if (_loadingCount === 0) wx.hideLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = <T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
method: HttpMethod = 'GET',
|
||||||
|
options: RequestOptions = {},
|
||||||
|
): Promise<ApiResponse<T>> => {
|
||||||
|
const { loading = true, loadingTitle = '加载中...', contentType = 'application/json' } = options
|
||||||
|
|
||||||
|
if (loading) _showLoading(loadingTitle)
|
||||||
|
|
||||||
|
const header: Record<string, string> = { 'content-type': contentType }
|
||||||
|
const token = wx.getStorageSync('token') as string
|
||||||
|
if (token) header['X-Access-Token'] = token
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
url: BASE_URL + url,
|
||||||
|
data,
|
||||||
|
method,
|
||||||
|
header,
|
||||||
|
success: (res) => {
|
||||||
|
if (loading) _hideLoading()
|
||||||
|
const body = res.data as ApiResponse<T>
|
||||||
|
if (body.code === 200) {
|
||||||
|
resolve(body)
|
||||||
|
} else if (body.code === 401) {
|
||||||
|
wx.clearStorageSync()
|
||||||
|
wx.reLaunch({ url: '/pages/login/login' })
|
||||||
|
reject(body)
|
||||||
|
} else {
|
||||||
|
wx.showToast({ title: body.message || '请求失败', icon: 'none' })
|
||||||
|
reject(body)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (loading) _hideLoading()
|
||||||
|
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||||
|
reject(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get = <T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options?: RequestOptions,
|
||||||
|
): Promise<ApiResponse<T>> => request<T>(url, data, 'GET', options)
|
||||||
|
|
||||||
|
export const post = <T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options?: RequestOptions,
|
||||||
|
): Promise<ApiResponse<T>> => request<T>(url, data, 'POST', options)
|
||||||
|
|
||||||
|
export const put = <T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options?: RequestOptions,
|
||||||
|
): Promise<ApiResponse<T>> => request<T>(url, data, 'PUT', options)
|
||||||
|
|
||||||
|
export const del = <T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
options?: RequestOptions,
|
||||||
|
): Promise<ApiResponse<T>> => request<T>(url, data, 'DELETE', {
|
||||||
|
contentType: 'application/x-www-form-urlencoded',
|
||||||
|
...options,
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
result: T
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
/** 是否显示 loading,默认 true */
|
||||||
|
loading?: boolean
|
||||||
|
/** loading 文案,默认"加载中..." */
|
||||||
|
loadingTitle?: string
|
||||||
|
/** Content-Type,默认 application/json */
|
||||||
|
contentType?: 'application/json' | 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user