From e0573857e02ee779151cd8b8be1b2c8ee473090a Mon Sep 17 00:00:00 2001 From: 17792275749 <812100002@qq.com> Date: Fri, 15 May 2026 14:10:27 +0800 Subject: [PATCH] =?UTF-8?q?[AI=20Generated]:=20feat(*):=20=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=95=B0=E6=8D=AE=E9=A1=B5/=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E7=9C=9F=E5=AE=9E=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=88=90=E5=91=98=E7=AE=A1=E7=90=86=E3=80=81?= =?UTF-8?q?=E8=93=9D=E7=89=99=E8=BF=9E=E6=8E=A5=E5=8F=8A=E6=88=90=E5=91=98?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E6=8A=BD=E5=B1=89=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/claudeCodeTabState.xml | 2 +- miniprogram/app.ts | 17 +- .../selectMemberDrawer/selectMemberDrawer.ts | 91 +++++--- miniprogram/lefu/service.ts | 11 +- miniprogram/pages/addMember/addMember.ts | 217 +++++++++-------- miniprogram/pages/addMember/addMember.wxml | 27 ++- .../pages/connectedDevice/connectedDevice.ts | 2 +- miniprogram/pages/data/data.less | 36 ++- miniprogram/pages/data/data.ts | 221 +++++++++++++++--- miniprogram/pages/data/data.wxml | 61 +++-- miniprogram/pages/equipment/equipment.ts | 6 +- .../pages/equipmentMember/equipmentMember.ts | 13 +- .../measurementReport/measurementReport.less | 11 +- .../measurementReport/measurementReport.ts | 168 ++++++++----- .../measurementReport/measurementReport.wxml | 6 +- .../supplementPersonal/supplementPersonal.ts | 6 +- 16 files changed, 612 insertions(+), 283 deletions(-) diff --git a/.idea/claudeCodeTabState.xml b/.idea/claudeCodeTabState.xml index faec8fa..14424ed 100644 --- a/.idea/claudeCodeTabState.xml +++ b/.idea/claudeCodeTabState.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/miniprogram/app.ts b/miniprogram/app.ts index 514a129..7a5194a 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -5,15 +5,14 @@ App({ }, onLaunch() { - // 已登录则跳过登录页 - // const userInfo = wx.getStorageSync('userInfo') - // if (userInfo) { - // const connectDeviceInfo = wx.getStorageSync('connectDeviceInfo') - // const hasDevice = connectDeviceInfo && Object.keys(connectDeviceInfo).length > 0 - // wx.reLaunch({ - // url: hasDevice ? '/pages/home/home' : '/pages/connectedDevice/connectedDevice' - // }) - // } + const userInfo = wx.getStorageSync('userInfo') + if (userInfo) { + const connectDeviceInfo = wx.getStorageSync('connectDeviceInfo') + const hasDevice = connectDeviceInfo && Object.keys(connectDeviceInfo).length > 0 + wx.reLaunch({ + url: hasDevice ? '/pages/home/home' : '/pages/connectedDevice/connectedDevice' + }) + } }, }) diff --git a/miniprogram/components/selectMemberDrawer/selectMemberDrawer.ts b/miniprogram/components/selectMemberDrawer/selectMemberDrawer.ts index 1d3c6af..a02bc3f 100644 --- a/miniprogram/components/selectMemberDrawer/selectMemberDrawer.ts +++ b/miniprogram/components/selectMemberDrawer/selectMemberDrawer.ts @@ -1,25 +1,35 @@ -/** 成员数据项 */ -interface SelectMemberItem { - /** 唯一标识 */ +import { get } from '../../utils/request/index' +import { lefuService } from '../../lefu/index' + +/** GET /weighingScale/v3/select/addMembers/user 返回的单条结构 */ +interface AddMemberApiItem { id: string - /** 姓名 */ - name: string - /** 性别 */ - gender: '男' | '女' - /** 年龄 */ - age: number - /** 身高(cm) */ + realname: string + sex_dictText: '男' | '女' + birthday: string height: number - /** 头像 URL,无则显示彩色占位圆 */ + weight: number + idCard: string | null + avatar: string | null +} + +/** 抽屉内部成员项(含回填所需全量字段) */ +interface ExistingMember { + id: string + name: string + gender: '男' | '女' + age: number + height: number + weight: number + idCard: string avatar?: string - /** 是否已选中 */ selected?: boolean } /** * 选择已有成员抽屉组件 - * Properties: show(控制显示), members(成员列表) - * Events: close(关闭), select(选中成员,携带 SelectMemberItem) + * Properties: show(控制显示) + * Events: close(关闭), select(选中成员,携带 ExistingMember) */ Component({ properties: { @@ -27,11 +37,6 @@ Component({ show: { type: Boolean, value: false - }, - /** 成员列表 */ - members: { - type: Array, - value: [] as SelectMemberItem[] } }, @@ -39,25 +44,25 @@ Component({ /** 控制 wx:if,DOM 是否存在 */ innerShow: false, /** 控制 --visible 类,触发 CSS transition */ - animVisible: false + animVisible: false, + /** 成员列表(组件内部拉取) */ + members: [] as ExistingMember[] }, observers: { /** - * 监听外部 show 变化,错开两帧: - * 打开:先插入 DOM,下一帧加动画类 - * 关闭:先移除动画类,等 transition 结束再移除 DOM + * 监听外部 show 变化: + * 打开时先拉数据再展示抽屉;关闭时移除动画类后移除 DOM */ show(val: boolean) { if (val) { + this._fetchMembers() this.setData({ innerShow: true }) - // 延迟一帧,让 DOM 先以初始状态渲染,再触发 transition setTimeout(() => { this.setData({ animVisible: true }) }, 20) } else { this.setData({ animVisible: false }) - // 等 CSS transition 结束(0.3s)再移除 DOM setTimeout(() => { this.setData({ innerShow: false }) }, 300) @@ -66,6 +71,40 @@ Component({ }, methods: { + /** 从生日字符串(YYYY-MM-DD)计算周岁 */ + _calcAge(birthday: string): number { + if (!birthday) return 0 + const [year, month, day] = birthday.split('-').map(Number) + if (!year || !month || !day) return 0 + const now = new Date() + let age = now.getFullYear() - year + if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1 + return Math.max(0, age) + }, + + /** 调接口获取可选成员列表 */ + _fetchMembers() { + const userInfo = wx.getStorageSync('userInfo') as { userId?: string } | null + const sn = lefuService.deviceInfo?.serialNumber ?? '' + get('weighingScale/v3/select/addMembers/user', { + userId: userInfo?.userId ?? '', + sn, + }, { loading: false }) + .then((res: any) => { + const members: ExistingMember[] = (res.result ?? []).map((item: AddMemberApiItem) => ({ + id: item.id, + name: item.realname, + gender: item.sex_dictText, + age: this._calcAge(item.birthday), + height: item.height, + weight: item.weight, + idCard: item.idCard ?? '', + avatar: item.avatar ?? '', + })) + this.setData({ members }) + }) + }, + /** 点击遮罩关闭 */ onTapMask() { this.triggerEvent('close') @@ -81,7 +120,7 @@ Component({ * @param e 携带 data-member 的点击事件 */ onTapMember(e: WechatMiniprogram.TouchEvent) { - const member = e.currentTarget.dataset.member as SelectMemberItem + const member = e.currentTarget.dataset.member as ExistingMember this.triggerEvent('select', member) } } diff --git a/miniprogram/lefu/service.ts b/miniprogram/lefu/service.ts index 63b9764..09ce6df 100644 --- a/miniprogram/lefu/service.ts +++ b/miniprogram/lefu/service.ts @@ -124,7 +124,7 @@ class LeFuService { this._busSubscribed = true plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => { - console.log(TAG, 'bus[devicesList],设备数:', devList?.length) + console.log(TAG, 'bus[devicesList],设备数:', devList?.length, devList) if (!devList?.length) { this._onDevicesListCb?.([]) return @@ -190,8 +190,8 @@ class LeFuService { }) } - connect(device: ScannedDevice): void { - console.log(TAG, 'connect():', device.name) + connect(rawDevice: RawDevice): void { + console.log(TAG, 'connect():', rawDevice.name) this._intentionalConnect = true this._stopKeepAlive() this._stopReconnectTimer() @@ -200,8 +200,11 @@ class LeFuService { this._deviceInfo = null this._onDeviceInfoCb = null this._onDeviceConnectCb = null + // 直连缓存设备时不走 startScan,需手动初始化插件设备配置和总线订阅 + plugin.Blue.setDeviceSetting(DEVICE_SETTINGS) + this._subscribeBus() plugin.Blue.stopBluetoothDevicesDiscovery() - plugin.Blue.createBLEConnection(device.raw) + plugin.Blue.createBLEConnection(rawDevice) } disconnect(): void { diff --git a/miniprogram/pages/addMember/addMember.ts b/miniprogram/pages/addMember/addMember.ts index 7de9830..59adf52 100644 --- a/miniprogram/pages/addMember/addMember.ts +++ b/miniprogram/pages/addMember/addMember.ts @@ -1,3 +1,16 @@ +import { get, put } from '../../utils/request/index' +import { lefuService } from '../../lefu/index' + +/** GET /weighingScale/v3/selectUserBySn/user 返回的单条结构(编辑回填用) */ +interface SysUserDevice { + id: string + realname: string + idCard: string + height: number + sex: number + birthday: string +} + /** 表单数据模型 */ interface MemberForm { /** 姓名 */ @@ -10,80 +23,77 @@ interface MemberForm { age: string /** 身高(cm) */ height: string + /** 体重(kg) */ + weight: string } /** 身份证解析结果 */ interface IdCardParsed { - /** 性别,空字符串表示解析失败 */ gender: string - /** 年龄,空字符串表示解析失败 */ age: string + sex: number + birthday: string } -/** 已有成员数据项 */ -interface SelectMemberItem { - /** 唯一标识 */ - id: string - /** 姓名 */ +/** 从抽屉组件 select 事件接收的成员数据 */ +interface ExistingMember { name: string - /** 性别 */ gender: '男' | '女' - /** 年龄 */ age: number - /** 身高(cm) */ height: number - /** 头像 URL,无则显示彩色占位圆 */ - avatar?: string - /** 是否已选中 */ - selected?: boolean + weight: number + idCard: string } /** - * 添加成员页 - * 收集姓名、身份证号、身高,并根据身份证自动解析性别与年龄 - * 保存后返回设备成员列表页 + * 添加/编辑成员页 + * URL 参数含 userId 时为编辑模式,隐藏「从已有成员中选择」按钮并预填表单 */ Page({ data: { - // 表单数据 + /** 编辑模式标记 */ + isEdit: false, + /** 编辑时的用户 ID */ + userId: '', + form: { name: '', idCard: '', gender: '', age: '', - height: '' + height: '', + weight: '' } as MemberForm, - // 抽屉显示状态 showSelectDrawer: false, - - // TODO: 后续替换为接口数据 - existingMembers: [ - { id: '1', name: '陈小飞', gender: '男', age: 32, height: 172, avatar: '' }, - { id: '2', name: '李镇南', gender: '男', age: 35, height: 185, avatar: '' }, - { id: '3', name: '张颖', gender: '女', age: 35, height: 185, avatar: '' }, - { id: '4', name: '李镇南', gender: '男', age: 35, height: 185, avatar: '' }, - { id: '5', name: '赵雪', gender: '女', age: 31, height: 165, avatar: '' } - ] as SelectMemberItem[] }, - /** - * 姓名输入 - */ + onLoad(options: Record) { + const { userId } = options + if (!userId) return + this.setData({ isEdit: true, userId }) + get('weighingScale/v2/getUserInfo', { id: userId }, { loading: false }) + .then((res: any) => { + const u: SysUserDevice = res.result + if (!u) return + const parsed = this.parseIdCard(u.idCard ?? '') + this.setData({ + 'form.name': u.realname ?? '', + 'form.idCard': u.idCard ?? '', + 'form.gender': parsed.gender, + 'form.age': parsed.age, + 'form.height': String(u.height ?? ''), + }) + }) + }, + onNameInput(e: WechatMiniprogram.Input) { - this.setData({ - 'form.name': e.detail.value - }) + this.setData({ 'form.name': e.detail.value }) }, - /** - * 身份证号输入 - * 长度满 18 位时自动解析性别与年龄,否则清空 - */ onIdCardInput(e: WechatMiniprogram.Input) { const idCard = (e.detail.value || '').trim().toUpperCase() const parsed = this.parseIdCard(idCard) - this.setData({ 'form.idCard': idCard, 'form.gender': parsed.gender, @@ -91,102 +101,69 @@ Page({ }) }, - /** - * 身高输入 - */ onHeightInput(e: WechatMiniprogram.Input) { - this.setData({ - 'form.height': e.detail.value - }) + this.setData({ 'form.height': e.detail.value }) + }, + + onWeightInput(e: WechatMiniprogram.Input) { + this.setData({ 'form.weight': e.detail.value }) }, /** - * 解析身份证号,返回性别与年龄 - * @param idCard 18 位身份证号 + * 解析身份证,返回性别、年龄、sex 数值、出生日期 */ parseIdCard(idCard: string): IdCardParsed { - // 简单校验:18 位且前 17 位为数字 - const reg = /^\d{17}[\dX]$/ - if (!reg.test(idCard)) { - return { gender: '', age: '' } - } + const empty: IdCardParsed = { gender: '', age: '', sex: 0, birthday: '' } + if (!/^\d{17}[\dX]$/.test(idCard)) return empty - // 性别:第 17 位奇数为男,偶数为女 const genderCode = parseInt(idCard.charAt(16), 10) - const gender = genderCode % 2 === 1 ? '男' : '女' + const sex = genderCode % 2 === 1 ? 1 : 2 + const gender = sex === 1 ? '男' : '女' - // 出生日期:第 7-14 位 const year = parseInt(idCard.substr(6, 4), 10) const month = parseInt(idCard.substr(10, 2), 10) const day = parseInt(idCard.substr(12, 2), 10) + if (!year || month < 1 || month > 12 || day < 1 || day > 31) return empty - // 基本合法性校验 - if ( - !year || !month || !day || - month < 1 || month > 12 || - day < 1 || day > 31 - ) { - return { gender: '', age: '' } - } - - // 计算周岁(看是否过了今年生日) const now = new Date() let age = now.getFullYear() - year - const nowMonth = now.getMonth() + 1 - const nowDay = now.getDate() - if (nowMonth < month || (nowMonth === month && nowDay < day)) { - age -= 1 - } + if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1 + if (age < 0 || age > 150) return empty - if (age < 0 || age > 150) { - return { gender: '', age: '' } - } - - return { gender, age: String(age) } + const mm = String(month).padStart(2, '0') + const dd = String(day).padStart(2, '0') + return { gender, age: String(age), sex, birthday: `${year}-${mm}-${dd}` } }, - /** - * 从已有成员中选择 — 打开抽屉 - */ onTapSelectExisting() { this.setData({ showSelectDrawer: true }) }, - /** 关闭选择成员抽屉 */ onSelectMemberClose() { this.setData({ showSelectDrawer: false }) }, - /** - * 选中成员后填充表单 - * @param e 携带 detail(SelectMemberItem) 的自定义事件 - */ - onSelectMember(e: WechatMiniprogram.CustomEvent) { - const { name, gender, age, height } = e.detail + onSelectMember(e: WechatMiniprogram.CustomEvent) { + const { name, gender, age, height, weight, idCard } = e.detail this.setData({ showSelectDrawer: false, 'form.name': name, + 'form.idCard': idCard, 'form.gender': gender, 'form.age': String(age), - 'form.height': String(height) + 'form.height': String(height), + 'form.weight': String(weight), }) }, - /** - * 取消:直接返回上一页 - */ - onCancel() { +onCancel() { wx.navigateBack() }, - /** - * 保存成员信息 - * 必填:姓名、身份证号(18 位且解析成功)、身高 - * 未通过时 toast 提示,通过后返回上一页 - */ onSubmit() { - const { name, idCard, gender, age, height } = this.data.form + const { name, idCard, gender, age, height, weight } = this.data.form const heightNum = parseFloat(height) + const weightNum = parseFloat(weight) if (!name.trim()) { wx.showToast({ title: '请填写姓名', icon: 'none' }) @@ -197,15 +174,55 @@ Page({ return } if (!gender || !age) { - wx.showToast({ title: '身份证号有误,请检查', icon: 'none' }) + wx.showToast({ title: '身份证号有误,请检查', icon: 'none' }) return } if (!height || isNaN(heightNum) || heightNum <= 0) { wx.showToast({ title: '请填写身高', icon: 'none' }) return } + if (!weight || isNaN(weightNum) || weightNum <= 0) { + wx.showToast({ title: '请填写体重', icon: 'none' }) + return + } - // TODO: 此处后续接入成员保存接口 - wx.navigateBack() + const parsed = this.parseIdCard(idCard) + const userInfo = wx.getStorageSync('userInfo') + const connectDeviceInfoRaw = wx.getStorageSync('connectDeviceInfo') + + const payload: Record = { + parentUserId: userInfo?.userId ?? '', + scaleDeviceId: lefuService.deviceInfo?.serialNumber ?? '', + realname: name.trim(), + idCard, + sex: parsed.sex, + birthday: parsed.birthday, + height: heightNum, + weight: weightNum, + connectDeviceInfo: connectDeviceInfoRaw ? JSON.stringify(connectDeviceInfoRaw) : '', + } + if (this.data.isEdit) { + payload.id = this.data.userId + } + + const successMsg = this.data.isEdit ? '保存成功' : '添加成功' + const failMsg = this.data.isEdit ? '保存失败,请重试' : '添加失败,请重试' + + put('weighingScale/v3/edit/user', payload) + .then(() => { + wx.showToast({ title: successMsg, icon: 'success' }) + setTimeout(() => { + // 通知上一页(equipmentMember)刷新成员列表 + const pages = getCurrentPages() + const prevPage = pages[pages.length - 2] as any + if (prevPage && prevPage.data?.sn) { + prevPage._loadData(prevPage.data.sn) + } + wx.navigateBack() + }, 1500) + }) + .catch(() => { + wx.showToast({ title: failMsg, icon: 'none' }) + }) } }) diff --git a/miniprogram/pages/addMember/addMember.wxml b/miniprogram/pages/addMember/addMember.wxml index 85fb454..8ad52af 100644 --- a/miniprogram/pages/addMember/addMember.wxml +++ b/miniprogram/pages/addMember/addMember.wxml @@ -98,10 +98,32 @@ + + + + + 体重 + * + + + + + kg + + + - - 从已有成员中选择 + + + 从已有成员中选择 + @@ -113,7 +135,6 @@ diff --git a/miniprogram/pages/connectedDevice/connectedDevice.ts b/miniprogram/pages/connectedDevice/connectedDevice.ts index d4efd5a..e06a63b 100644 --- a/miniprogram/pages/connectedDevice/connectedDevice.ts +++ b/miniprogram/pages/connectedDevice/connectedDevice.ts @@ -170,7 +170,7 @@ Page({ } wx.showLoading({ title: '连接中...', mask: true }) - lefuService.connect(raw) + lefuService.connect(raw.raw) lefuService.onConnectState((state) => { if (state === lefuService.BLUE_STATE.CONNECTFAILED) { wx.hideLoading() diff --git a/miniprogram/pages/data/data.less b/miniprogram/pages/data/data.less index d4cee05..cd8a82a 100644 --- a/miniprogram/pages/data/data.less +++ b/miniprogram/pages/data/data.less @@ -34,13 +34,18 @@ align-items: center; margin-bottom: 30rpx; - /* 头像色块 */ + /* 头像色块(展示姓名首字) */ .avatar { width: 90rpx; height: 90rpx; margin-right: 24rpx; border-radius: 18rpx; - background: var(--avatar-bg); + color: #FFFFFF; + font-size: 36rpx; + font-weight: bold; + line-height: 90rpx; + text-align: center; + background-color: #1385FA; } /* 姓名 + meta */ @@ -269,4 +274,31 @@ } } } + + /* ===== 空态 ===== */ + .empty-state { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 160rpx; + + .empty-icon { + width: 193rpx; + height: 138rpx; + margin-bottom: 40rpx; + + image { + width: 100%; + height: 100%; + } + } + + .empty-text { + color: #808080; + height: 30rpx; + font-size: 30rpx; + line-height: 30rpx; + } + } } diff --git a/miniprogram/pages/data/data.ts b/miniprogram/pages/data/data.ts index 58f9a53..48bad4b 100644 --- a/miniprogram/pages/data/data.ts +++ b/miniprogram/pages/data/data.ts @@ -1,72 +1,219 @@ -import { getMockRecords, calcStats } from '../../mock/data.mock' -import type { TimeRange, RecordItem, StatsData } from '../../mock/data.mock' -import type { MemberItem } from '../../mock/member.mock' -import { mockMembers } from '../../mock/member.mock' +import { get } from '../../utils/request/index' /** 加载状态机 */ type LoadStatus = 'idle' | 'loading' | 'loaded' +/** 接口 type 参数:0=全部 1=近7天 2=近30天 3=近3月 */ +type RangeType = 0 | 1 | 2 | 3 + /** 时间范围 Tab 项 */ interface RangeTab { - key: TimeRange + key: RangeType label: string } +/** 缓存中的用户信息 */ +interface StorageUserInfo { + userId: string + realname: string + sex: number + height: number + birthday: string + avatar: string | null + relationType: number | string | null + [key: string]: any +} + +/** 页面展示用用户信息 */ +interface PageUserInfo extends StorageUserInfo { + sex_dictText: '男' | '女' | '' + age: number + isSelf: boolean +} + +/** GET /weighingScale/v3/getMeasureStatistics 返回结构 */ +interface MeasureStatistics { + maxWeight: number + minWeight: number + avgWeight: number + weightChange: number +} + +/** GET /weighingScale/v3/getMeasureHistoryPage records 单条 */ +interface MeasureRecord { + recordId: string + examTime: string + weight: number + bmi: number + fatRate: number +} + +/** 统计空态默认值 */ +const EMPTY_STATS: MeasureStatistics = { + maxWeight: 0, + minWeight: 0, + avgWeight: 0, + weightChange: 0, +} + +/** 从生日字符串(YYYY-MM-DD)计算周岁 */ +function calcAge(birthday: string): number { + if (!birthday) return 0 + const [year, month, day] = birthday.split('-').map(Number) + if (!year || !month || !day) return 0 + const now = new Date() + let age = now.getFullYear() - year + if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1 + return Math.max(0, age) +} + Page({ data: { status: 'idle' as LoadStatus, - activeRange: '7d' as TimeRange, + activeType: 1 as RangeType, rangeTabs: [ - { key: '7d', label: '近7天' }, - { key: '30d', label: '近30天' }, - { key: '3m', label: '近3月' }, + { key: 1, label: '近7天' }, + { key: 2, label: '近30天' }, + { key: 3, label: '近3月' }, ] as RangeTab[], - /** 当前查看的成员(默认本人) */ - currentMember: null as MemberItem | null, + userInfo: null as PageUserInfo | null, - stats: null as StatsData | null, - recordList: [] as RecordItem[], + stats: EMPTY_STATS as MeasureStatistics, + + records: [] as MeasureRecord[], + pageNo: 1, + total: 0, + hasMore: false, + hasData: false, + refresherTriggered: false, }, onLoad() { - // 默认选本人(isSelf=true),后备取第一个 - const members: MemberItem[] = wx.getStorageSync('memberList') || mockMembers - const self = members.find(m => m.isSelf) || members[0] || null - this.setData({ currentMember: self }) + const raw = wx.getStorageSync('userInfo') as StorageUserInfo | null + if (!raw?.userId) return + const userInfo: PageUserInfo = { + ...raw, + sex_dictText: raw.sex === 1 ? '男' : raw.sex === 2 ? '女' : '', + age: calcAge(raw.birthday), + isSelf: String(raw.relationType ?? '') === '1', + } + this.setData({ userInfo }) this.loadData() }, - onReady() {}, - onShow() {}, - onHide() {}, - onUnload() {}, - onPullDownRefresh() {}, - onReachBottom() {}, + /** 重置分页,并行拉统计 + 第一页列表 */ + loadData() { + const userInfo = this.data.userInfo + if (!userInfo?.userId) return + this.setData({ status: 'loading', pageNo: 1, records: [] }) - onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent { - return {} + const statsReq = get('weighingScale/v3/getMeasureStatistics', { + userId: userInfo.userId, + type: this.data.activeType, + }, { loading: false }) + + const pageReq = get('weighingScale/v3/getMeasureHistoryPage', { + userId: userInfo.userId, + type: this.data.activeType, + pageNo: 1, + pageSize: 10, + }, { loading: false }) + + wx.showLoading({ title: '加载中...', mask: true }) + Promise.all([statsReq, pageReq]) + .then(([statsRes, pageRes]: any[]) => { + const rawStats = statsRes.result + const stats: MeasureStatistics = rawStats ? { + maxWeight: rawStats.maxWeight ?? 0, + minWeight: rawStats.minWeight ?? 0, + avgWeight: rawStats.avgWeight ?? 0, + weightChange: rawStats.weightChange ?? 0, + } : EMPTY_STATS + + const page = pageRes.result ?? {} + const records: MeasureRecord[] = (page.records ?? []).map((r: any) => ({ + recordId: r.recordId ?? '', + examTime: r.examTime ?? '', + weight: r.weight ?? 0, + bmi: r.bmi ?? 0, + fatRate: r.fatRate ?? 0, + })) + const total = page.total ?? 0 + + this.setData({ + stats, + records, + total, + hasMore: records.length < total, + hasData: records.length > 0, + }) + }) + .finally(() => { + wx.hideLoading() + this.setData({ status: 'loaded', refresherTriggered: false }) + }) }, - /** 加载数据(切 Tab / 切成员 复用同一入口) */ - loadData() { - this.setData({ status: 'loading' }) - setTimeout(() => { - const records = getMockRecords(this.data.activeRange) - const stats = calcStats(records) - this.setData({ recordList: records, stats, status: 'loaded' }) - }, 300) + /** scroll-view 触底,加载下一页 */ + onScrollToLower() { + if (!this.data.hasMore) return + const userInfo = this.data.userInfo + if (!userInfo?.userId) return + const nextPage = this.data.pageNo + 1 + + wx.showLoading({ title: '加载中...', mask: true }) + get('weighingScale/v3/getMeasureHistoryPage', { + userId: userInfo.userId, + type: this.data.activeType, + pageNo: nextPage, + pageSize: 10, + }, { loading: false }) + .then((res: any) => { + const page = res.result ?? {} + const newRecords: MeasureRecord[] = (page.records ?? []).map((r: any) => ({ + recordId: r.recordId ?? '', + examTime: r.examTime ?? '', + weight: r.weight ?? 0, + bmi: r.bmi ?? 0, + fatRate: r.fatRate ?? 0, + })) + const allRecords = [...this.data.records, ...newRecords] + this.setData({ + records: allRecords, + pageNo: nextPage, + hasMore: allRecords.length < this.data.total, + }) + }) + .finally(() => { + wx.hideLoading() + }) + }, + + /** scroll-view 下拉刷新 */ + onRefresh() { + this.setData({ refresherTriggered: true }) + this.loadData() }, /** 切换时间范围 Tab */ onTapRange(e: WechatMiniprogram.TouchEvent) { - const key = e.currentTarget.dataset.key as TimeRange - if (key === this.data.activeRange) return - this.setData({ activeRange: key }) + const key = Number(e.currentTarget.dataset.key) as RangeType + if (key === this.data.activeType) return + this.setData({ activeType: key }) this.loadData() }, - /** 切换成员 */ + /** 点击记录行,携带 recordId + userId 跳转测量报告 */ + onTapRecord(e: WechatMiniprogram.TouchEvent) { + const recordId = e.currentTarget.dataset.recordId as string + const userId = this.data.userInfo?.userId ?? '' + wx.navigateTo({ + url: `/pages/measurementReport/measurementReport?recordId=${recordId}&userId=${userId}`, + }) + }, + + /** 切换成员占位 */ onTapSwitchMember() { wx.showToast({ title: '切换成员', icon: 'none' }) }, diff --git a/miniprogram/pages/data/data.wxml b/miniprogram/pages/data/data.wxml index 85b5863..3f418cf 100644 --- a/miniprogram/pages/data/data.wxml +++ b/miniprogram/pages/data/data.wxml @@ -1,4 +1,11 @@ - + @@ -6,20 +13,20 @@ - + {{ userInfo.realname[0] }} - {{ currentMember.name }} - + {{ userInfo.realname }} + 本人 - {{ currentMember.gender }} + {{ userInfo.sex_dictText }} · - {{ currentMember.age }}岁 + {{ userInfo.age }}岁 | - {{ currentMember.height }}cm + {{ userInfo.height }}cm 切换成员 @@ -31,7 +38,7 @@ {{ item.label }} @@ -54,26 +61,38 @@ 体重变化 - - {{ stats.weightDiff > 0 ? '+' : '' }}{{ stats.weightDiff }}kg + + {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange }}kg - - - - - {{ item.time }} - - 体重:{{ item.weight }}kg - BMI:{{ item.bmi }} - 体脂率:{{ item.bodyFat }}% + + + + + + {{ item.examTime }} + + 体重:{{ item.weight }}kg + BMI:{{ item.bmi }} + 体脂率:{{ item.fatRate }}% + + + + + + + + + + - - + 暂无称重数据 + + diff --git a/miniprogram/pages/equipment/equipment.ts b/miniprogram/pages/equipment/equipment.ts index b0ce340..0ce7d57 100644 --- a/miniprogram/pages/equipment/equipment.ts +++ b/miniprogram/pages/equipment/equipment.ts @@ -108,8 +108,7 @@ Page({ wx.showLoading({ title: '连接中...', mask: true }) wx.openBluetoothAdapter({ success: () => { - const target = { name: cachedRaw.scaleDeviceName ?? cachedRaw.name ?? '', raw: cachedRaw, model: {} } - lefuService.connect(target) + lefuService.connect(cachedRaw) lefuService.onDeviceConnect(() => { wx.hideLoading() }) @@ -150,8 +149,7 @@ Page({ wx.showLoading({ title: '连接中...', mask: true }) wx.openBluetoothAdapter({ success: () => { - const target = { name: rawDevice!.scaleDeviceName ?? rawDevice!.name ?? '', raw: rawDevice!, model: {} } - lefuService.connect(target) + lefuService.connect(rawDevice!) lefuService.onDeviceConnect(() => { wx.hideLoading() wx.setStorageSync('connectDeviceInfo', rawDevice) diff --git a/miniprogram/pages/equipmentMember/equipmentMember.ts b/miniprogram/pages/equipmentMember/equipmentMember.ts index 200c2e9..ca1cfc1 100644 --- a/miniprogram/pages/equipmentMember/equipmentMember.ts +++ b/miniprogram/pages/equipmentMember/equipmentMember.ts @@ -139,6 +139,9 @@ Page({ connected: lefuService.isConnected, 'deviceCard.name': deviceInfo?.name ?? '', }) + if (deviceInfo?.serialNumber) { + this._loadData(deviceInfo.serialNumber) + } // deviceInfo 实时同步:进页面时可能还未到达 lefuService.onDeviceInfo((info) => { this.setData({ @@ -157,12 +160,6 @@ Page({ }) }, - onShow() { - if (this.data.sn) { - this._loadData(this.data.sn) - } - }, - onUnload() { lefuService.onDeviceConnect(() => {}) lefuService.onDisconnected(() => {}) @@ -205,10 +202,10 @@ Page({ }) }, - /** 编辑成员(接口待接入) */ + /** 编辑成员:仅携带 userId 跳转,详情由目标页自行查询 */ onTapEdit(e: WechatMiniprogram.TouchEvent) { const id = e.currentTarget.dataset.id as string - wx.showToast({ title: `编辑 ${id}`, icon: 'none' }) + wx.navigateTo({ url: `/pages/addMember/addMember?userId=${id}` }) }, /** 删除成员 */ diff --git a/miniprogram/pages/measurementReport/measurementReport.less b/miniprogram/pages/measurementReport/measurementReport.less index 60ac4e8..7affb80 100644 --- a/miniprogram/pages/measurementReport/measurementReport.less +++ b/miniprogram/pages/measurementReport/measurementReport.less @@ -18,11 +18,14 @@ .info-avatar { width: 90rpx; height: 90rpx; - border-radius: 24rpx; margin-right: 24rpx; - box-sizing: border-box; - background-color: #C6E6FF; - border: 3rpx solid #50A6FF; + border-radius: 18rpx; + color: #FFFFFF; + font-size: 36rpx; + font-weight: bold; + line-height: 90rpx; + text-align: center; + background-color: #1385FA; } .info-meta { diff --git a/miniprogram/pages/measurementReport/measurementReport.ts b/miniprogram/pages/measurementReport/measurementReport.ts index 4d2b187..7f90b37 100644 --- a/miniprogram/pages/measurementReport/measurementReport.ts +++ b/miniprogram/pages/measurementReport/measurementReport.ts @@ -1,70 +1,122 @@ -/** 测量报告数据 */ +import { get } from '../../utils/request/index' + +/** GET /weighingScale/v3/getMeasureHistory result 结构 */ +interface MeasureHistoryVO { + recordId: string + height: number + weight: number + weightChange: number + bmi: number + bmiType: string + standardWeight: number + fatRate: number + fatValue: number + basalMetabolism: number + examTime: string + sn: string + userInfo: { + realname: string + sex: number + [key: string]: any + } | null +} + +/** 页面渲染用测量记录 */ 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 + /** 体重整数部分 */ + weight: number + /** 体重小数部分,含小数点,如 ".20" */ + weightDecimal: string + /** 测量时间 */ + date: string + /** 与上次体重差值(负=减轻,正=增重) */ + diff: number + /** 身高(cm) */ + height: number + /** BMI 值 */ + bmi: number + /** BMI 评级文本(来自接口 bmiType) */ + bmiLabel: string + /** 标准体重(kg) */ + stdWeight: number + /** 体脂率(%) */ + bodyFat: number + /** 体脂肪量(kg) */ + bodyFatMass: number + /** 基础代谢(kcal) */ + bmr: number +} + +/** 空态默认值,避免页面展示 null */ +const EMPTY_RECORD: WeightRecord = { + weight: 0, + weightDecimal: '', + date: '', + diff: 0, + height: 0, + bmi: 0, + bmiLabel: '', + stdWeight: 0, + bodyFat: 0, + bodyFatMass: 0, + bmr: 0, +} + +/** 将体重数字拆成整数 + 小数字符串 */ +function splitWeight(weight: number): { int: number; decimal: string } { + const str = weight.toFixed(2) + const [intPart, decPart] = str.split('.') + return { int: Number(intPart), decimal: `.${decPart}` } } /** * 测量报告页 - * 展示单次测量的详细数据报告 + * URL 参数:recordId(记录 ID)、userId(用户 ID) */ Page({ - data: { - // 报告数据 - record: { - 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 - }, + data: { + record: EMPTY_RECORD as WeightRecord, + /** 被查看用户的姓名(来自接口 userInfo.realname) */ + realname: '', + }, - onLoad(options) { - // TODO: 根据 options.id 获取报告详情 - }, + onLoad(options: Record) { + const { recordId, userId } = options + if (!recordId || !userId) return - onReady() {}, + get('weighingScale/v3/getMeasureHistory', { + userId, + recordId, + }, { loading: true }) + .then((res: any) => { + const d: MeasureHistoryVO = res.result + if (!d) return - onShow() {}, + const { int, decimal } = splitWeight(d.weight ?? 0) + const record: WeightRecord = { + weight: int, + weightDecimal: decimal, + date: d.examTime ?? '', + diff: d.weightChange ?? 0, + height: d.height ?? 0, + bmi: d.bmi ?? 0, + bmiLabel: d.bmiType ?? '', + stdWeight: d.standardWeight ?? 0, + bodyFat: d.fatRate ?? 0, + bodyFatMass: d.fatValue ?? 0, + bmr: d.basalMetabolism ?? 0, + } + this.setData({ + record, + realname: d.userInfo?.realname ?? '', + }) + }) + }, - onHide() {}, - - onUnload() {}, - - /** - * 分享报告 - */ - onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent { - return { - title: `${this.data.report.userName}的测量报告`, - path: '/pages/measurementReport/measurementReport' - } - } + onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent { + return { + title: `${this.data.realname}的测量报告`, + path: '/pages/measurementReport/measurementReport', + } + }, }) diff --git a/miniprogram/pages/measurementReport/measurementReport.wxml b/miniprogram/pages/measurementReport/measurementReport.wxml index e58b9cd..cf98741 100644 --- a/miniprogram/pages/measurementReport/measurementReport.wxml +++ b/miniprogram/pages/measurementReport/measurementReport.wxml @@ -2,10 +2,10 @@ - + {{ realname[0] }} - 陈万宁 - 2026-05-06 09:30 + {{ realname }} + {{ record.date }} diff --git a/miniprogram/pages/supplementPersonal/supplementPersonal.ts b/miniprogram/pages/supplementPersonal/supplementPersonal.ts index af01803..3c6f782 100644 --- a/miniprogram/pages/supplementPersonal/supplementPersonal.ts +++ b/miniprogram/pages/supplementPersonal/supplementPersonal.ts @@ -259,8 +259,10 @@ Page({ connectDeviceInfo } - put('weighingScale/v3/update/user', payload) - .then(() => { + put('weighingScale/v3/update/user', payload) + .then(res => { + const { connectDeviceInfo: _, ...userInfo } = res.result + wx.setStorageSync('userInfo', userInfo) wx.switchTab({ url: '/pages/home/home' }) }) }