57 Commits
Author SHA1 Message Date
17792275749andClaude Code 51906af6dd fix(userManagement): 添加本人补传 id 字段
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 15:43:19 +08:00
17792275749andClaude Code deea304799 fix(userManagement): 添加本人参数 parentUserId 改为 userId
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 15:26:07 +08:00
17792275749andClaude Code 7e2033e235 feat(connectedDevice): 设备卡片底部显示 deviceId
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 15:17:23 +08:00
17792275749andClaude Code 9d56e63ce0 fix(userManagement): 补传添加成员 relationType 并修复删除后同步空列表报错
- 一键添加本人传 relationType=1,其他添加新增传 relationType=2
- _clearAndSync 待下发列表为空时跳过 syncMembersToDevice,避免空列表 reject

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 15:08:47 +08:00
17792275749andClaude Opus 4.7 9332990a77 feat(equipment): 设备页新增 OTA 升级与初始化功能
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-14 10:38:52 +08:00
17792275749andClaude Sonnet 4.6 109c9e1b70 style(equipment): 添加设备按钮改灰色胶囊,用户按钮蓝底高亮
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 15:38:14 +08:00
17792275749andClaude Sonnet 4.6 dc0a294406 feat(userManagement): 新增添加用户弹框组件
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 15:09:35 +08:00
17792275749andClaude Sonnet 4.6 9e8e26cb19 style(equipment): 设备操作按钮图标与文字尺寸微调
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 14:11:42 +08:00
17792275749andClaude Sonnet 4.6 6731ec2403 feat(supplementPersonal): 提交按钮文案改为"开始使用"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 14:09:35 +08:00
17792275749andClaude Sonnet 4.6 fa0246d1e5 feat(equipment): 已配对设备行按钮重构,添加设备入口仅运维可见
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 14:09:35 +08:00
17792275749andClaude Sonnet 4.6 b3e2a11423 chore(config): trial 环境域名切换到正式网关
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 14:09:32 +08:00
17792275749andClaude Opus 4.7 69d8fa75a5 feat(userManagement): 长按标题弹窗确认后重置设备用户并上报
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-04 17:36:48 +08:00
17792275749andClaude Opus 4.7 507915f2c4 fix(lefu): 搜索时 UNAVAILABLE 抖动不再清空协议,修复返回后设备误判断开
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 17:29:49 +08:00
17792275749andClaude Opus 4.7 97bf4674c0 fix(connectedWifi): 修正密码显隐图标切换逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 16:03:29 +08:00
17792275749andClaude Opus 4.7 bf501de628 feat(detailedReport): 基础指标label写死缺失显示-、躯干四肢无数据隐藏
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 11:45:39 +08:00
17792275749andClaude Opus 4.7 fb7c33c05f feat(addUser): 身份证合法时未填姓名则前端先解析性别年龄生日
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 11:39:33 +08:00
17792275749andClaude Opus 4.7 8284bf107b feat(data): 体重差值按 weightDiff/weightTrend 动态渲染并区分增减颜色
- 差值图标与数值改用后端 weightDiff/weightTrend
- 首次测量/持平不显示差值块
- 增加红色、减少绿色

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 08:50:51 +08:00
17792275749andClaude Opus 4.7 e0f201f15c feat(supplementPersonal): 身高体重范围校验并失焦清空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 19:44:56 +08:00
17792275749andClaude Opus 4.7 1dfda24645 feat(addUser): 生日不超今天、身高体重范围校验并失焦清空
- 出生年月 picker 限制到今天
- 身高 50-250cm、体重 20-300kg
- 失焦时越界值清空并提示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 19:44:53 +08:00
17792275749andClaude Opus 4.7 55cdeae1ba fix(addUser): 家庭用户身份证查询报错时提交前重新查询并重置状态
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 19:34:36 +08:00
17792275749andClaude Opus 4.7 4e51432b8a feat(userManagement): 删除成员确认文字区分他人添加
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 19:15:20 +08:00
17792275749andClaude Opus 4.7 8f5ccdd7e3 fix(supplementPersonal): 查询报错时提交前重新查询直到成功
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 19:15:14 +08:00
17792275749andClaude Opus 4.7 00394bf8c4 feat(userManagement): 运维模式隐藏一键添加本人并优化本人卡片与同步弹框
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 18:29:08 +08:00
17792275749andClaude Opus 4.7 e595ff9f64 fix(connectedDevice): 绑定失败读取接口返回的 msg
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 18:29:05 +08:00
17792275749andClaude Opus 4.7 8e3da13d52 feat(userManagement): 员工隐藏编辑按钮
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 16:51:03 +08:00
17792275749andClaude Opus 4.7 df48e3919d chore(connectedWifi): 附近WiFi刷新按钮常显
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 16:11:37 +08:00
17792275749andClaude Opus 4.7 a462feaa62 chore(connectedDevice): 恢复找不到设备帮助文案并优化权限引导
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 15:30:40 +08:00
17792275749andClaude Opus 4.7 0ffb3740fb feat(data): 历史记录列表卡片化改版
- 新增记录列表标题与总条数
- 每条记录展示体重及增减徽标、BMI与体脂率
- 新增加减号图标资源

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 11:13:45 +08:00
17792275749andClaude Opus 4.7 7a1e8942a9 feat(detailedReport): 基础指标完善评分单位、健康评价等级与BMI状态
- 健康评分补"分"单位
- 健康评价展示等级文案(如"亚健康")替代数值
- BMI名称后跟体型状态胶囊,颜色取底部刻度色

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 10:36:23 +08:00
17792275749andClaude Sonnet 4.6 680794702e fix(equipment): 连接设备增加超时与失败兜底,避免加载一直转圈
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 18:10:39 +08:00
17792275749andClaude Sonnet 4.6 4be9712000 chore(selectMemberDrawer): 空态增加提示文案
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 17:24:10 +08:00
17792275749andClaude Sonnet 4.6 f7dba359d3 chore(equipment): 调整绑定设备文案与已配对区块显示
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 17:24:07 +08:00
17792275749andClaude Sonnet 4.6 6069367b90 feat(userManagement): 已删除成员禁止编辑删除并优化展示
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 17:24:03 +08:00
17792275749andClaude Sonnet 4.6 1ef5c5d834 chore(userManagementSync): 完成对勾加粗
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 17:02:31 +08:00
17792275749andClaude Sonnet 4.6 95b6b7679b chore: 升级 ppScale 插件版本至 0.0.32
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 17:02:28 +08:00
17792275749andClaude Sonnet 4.6 8d69249147 feat: 接口错误处理增强,统一打印并展示后端错误信息
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 16:57:28 +08:00
17792275749andClaude Sonnet 4.6 85523109f6 feat(userManagement): 添加用户上限 10 位校验
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 16:57:19 +08:00
17792275749andClaude Sonnet 4.6 b90c79df9f feat(userManagementSync): 同步弹框增加转圈圈与完成对勾
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 16:56:46 +08:00
17792275749andClaude Opus 4.7 effd4f2068 fix(addUser): 姓名输入框补绑 blur,先输身份证后输姓名也能触发计算
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 16:11:04 +08:00
17792275749andClaude Opus 4.7 bbd6781a27 chore(userManagement): 调整设备卡片展示字段与文案
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 15:18:39 +08:00
17792275749andClaude Opus 4.7 fe3e0a7f0a chore(selectMemberDrawer): 标题栏增加图标并调整文案
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 15:16:47 +08:00
17792275749andClaude Opus 4.7 90c0ff65da chore(equipment): 调整设备卡片展示字段与文案
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 12:42:30 +08:00
17792275749andClaude Opus 4.7 d5b1bfb31a chore(connectedDevice): 隐藏帮助文案与 deviceId 调试信息
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 12:42:01 +08:00
17792275749andClaude Opus 4.7 322440b9c7 fix(connectedDevice): 返回时恢复被扫描打断的连接
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 12:41:58 +08:00
17792275749andClaude Opus 4.7 c0bc61621b feat(connectedWifi): 配网成功展示完成并隐藏刷新,未配网展示跳过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-01 11:57:28 +08:00
17792275749andClaude Opus 4.7 13d23376c2 fix(detailedReport): BMI 刻度落点按四段分界计算并计入色块间隙
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 15:49:08 +08:00
17792275749andClaude Opus 4.7 5a6281c9d8 fix(equipment): 取消配对时断开当前连接设备并清缓存
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 15:49:05 +08:00
17792275749andClaude Opus 4.7 5b9a66afc7 fix(addUser): 员工编号查询接口版本 v6 调整为 v5
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 15:49:01 +08:00
17792275749andClaude Opus 4.7 b94a6780f0 feat(env): 新增环境识别,网关地址按环境自动切换
- 新增 utils/env,用 wx.getAccountInfoSync().miniProgram.envVersion 识别 develop/trial/release
- request 与 lefu 的地址由硬编码改为按环境映射,develop/trial 走测试网关,release 走正式网关

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 11:59:44 +08:00
17792275749andClaude Opus 4.7 b4b2d2c7f3 fix(connectedDevice): 连接后等 SN 到位再绑定,拿不到 SN 时断开重置
- 连接成功后同时监听 deviceConnect 与 deviceInfo,两个信号都到位才调绑定接口,避免空 SN 绑定
- 15 秒超时仍拿不到 SN 时断开连接、清缓存、重置页面状态

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 11:38:52 +08:00
17792275749andClaude Opus 4.7 4d39449a0d fix(connectedDevice): 连接后绑定设备成功再跳转,失败时重置状态
- 手动/自动连接成功后 bindDevice 接口成功才缓存设备并跳配网
- bindDevice 失败时断开连接、清缓存、重置页面状态

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-27 18:16:41 +08:00
17792275749andClaude Opus 4.7 9a307783f9 feat(userManagement): 成员列表增加已删除状态展示
- delFlag=1 的成员显示「已删除」红色状态
- 已删除成员卡片半透明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-27 14:48:34 +08:00
17792275749andClaude Opus 4.7 dbb2ab8423 fix: 修复蓝牙连接误报与 WiFi 列表获取问题,完善一键添加本人
- connectedDevice 忽略 connect 过程中的 UNAVAILABLE 抖动,避免误报蓝牙关闭
- getWifiList 加 settled 保护,onStartSearch 加防重入
- userManagement 一键添加本人(仅加后台、不下发设备)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-27 14:02:21 +08:00
17792275749andClaude Opus 4.7 b4d3a034ba fix(detailedReport): 卡片无数据时隐藏整卡,wx:if 统一写 block 上
- 删除多余的 isEightElectrode 字段
- 各指标卡片 rows 为空时不渲染标题
- wx:if 统一移到 block 上,符合 wxml 规范

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-26 16:08:37 +08:00
17792275749andClaude Opus 4.7 92de746f9e fix(userManagement): 下发用户改用 userId,修正误用记录主键 id
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-26 14:32:31 +08:00
17792275749andClaude Opus 4.7 f771bb97d6 feat(userManagement): 同步支持删除标记,完成后统一刷新列表
- 列表含 delFlag=1 时清空设备后下发未删除用户并全量上报
- 长按标题清空重下发、不上报
- 同步完成统一重新拉取用户列表,前端不本地维护同步状态

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-26 09:53:22 +08:00
17792275749andClaude Opus 4.7 548c26cad5 feat: 数据补传改为整包上传,帮助中心补空状态
- equipment 数据补传一次上传整个列表(list 参数),注释掉 memberId
- helpCenter 问题列表补空数据展示
- userManagement 删除成员接口 v2 调整为 v6
- my 页面按钮字体加粗

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-26 09:07:35 +08:00
45 changed files with 1471 additions and 424 deletions
+1 -1
View File
@@ -65,7 +65,7 @@
"lazyCodeLoading": "requiredComponents",
"plugins": {
"ppScale-plugin": {
"version": "0.0.31",
"version": "0.0.32",
"provider": "wx0826af4e4908f524"
}
},
@@ -55,6 +55,11 @@
justify-content: space-between;
border-bottom: 2rpx solid #EAEDF1;
.drawer-icon {
width: 40rpx;
height: 40rpx;
}
.drawer-title {
color: #252535;
height: 40rpx;
@@ -135,8 +135,9 @@ Component({
}))
this.setData({ members })
})
.catch(() => {
wx.showToast({ title: '加载失败', icon: 'none' })
.catch((err: any) => {
console.error('[selectMemberDrawer] 加载成员失败:', err)
wx.showToast({ title: err?.msg || err?.message || '加载失败', icon: 'none' })
})
.finally(() => {
wx.hideLoading()
@@ -159,8 +160,9 @@ Component({
}))
this.setData({ members })
})
.catch(() => {
wx.showToast({ title: '加载失败', icon: 'none' })
.catch((err: any) => {
console.error('[selectMemberDrawer] 加载成员失败:', err)
wx.showToast({ title: err?.msg || err?.message || '加载失败', icon: 'none' })
})
.finally(() => {
wx.hideLoading()
@@ -10,7 +10,8 @@
<!-- 标题栏 -->
<view class="drawer-header">
<view class="drawer-title">选择已有用户</view>
<view class="drawer-icon"></view>
<view class="drawer-title">选择本人添加的用户</view>
<view class="drawer-close" bind:tap="onTapClose">
<view class="drawer-close-icon"></view>
</view>
@@ -53,7 +54,7 @@
<!-- 无用户:占位 -->
<block wx:else>
<empty-data />
<empty-data text="暂无用户" />
</block>
</view>
</view>
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,58 @@
.userManagementAddUser-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(37, 37, 53, 0.5);
z-index: 100;
}
.userManagementAddUser-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 660rpx;
box-sizing: border-box;
padding: 48rpx 40rpx 56rpx;
background-color: #FFFFFF;
border-radius: 24rpx;
z-index: 101;
display: flex;
flex-direction: column;
align-items: center;
.userManagementAddUser-title {
color: #252535;
height: 36rpx;
font-size: 36rpx;
font-weight: bolder;
line-height: 36rpx;
margin-bottom: 48rpx;
}
.userManagementAddUser-btn {
width: 100%;
height: 96rpx;
font-size: 36rpx;
font-weight: bold;
text-align: center;
border-radius: 48rpx;
box-sizing: border-box;
&--primary {
color: #FFFFFF;
line-height: 96rpx;
background-color: #1385FA;
margin-bottom: 40rpx;
}
&--outline {
color: #1385FA;
line-height: 92rpx;
border: 2rpx solid #1385FA;
background-color: #FFFFFF;
}
}
}
@@ -0,0 +1,26 @@
Component({
properties: {
/** 是否显示弹框 */
show: {
type: Boolean,
value: false
}
},
methods: {
/** 点击遮罩关闭 */
onClose() {
this.triggerEvent('close')
},
/** 一键添加本人 */
onAddSelf() {
this.triggerEvent('addself')
},
/** 添加其他用户 */
onAddOther() {
this.triggerEvent('addother')
}
}
})
@@ -0,0 +1,8 @@
<block wx:if="{{ show }}">
<view class="userManagementAddUser-mask" bind:tap="onClose"></view>
<view class="userManagementAddUser-panel">
<view class="userManagementAddUser-title">添加用户</view>
<view class="userManagementAddUser-btn userManagementAddUser-btn--primary" bind:tap="onAddSelf">一键添加本人</view>
<view class="userManagementAddUser-btn userManagementAddUser-btn--outline" bind:tap="onAddOther">添加其他用户</view>
</view>
</block>
@@ -23,11 +23,35 @@
align-items: center;
padding: 64rpx 48rpx;
.sync-spinner {
width: 96rpx;
height: 96rpx;
border: 8rpx solid rgba(19, 133, 250, 0.1);
border-top-color: #1385FA;
border-radius: 50%;
animation: sync-spin 1s linear infinite;
}
.sync-tick {
width: 96rpx;
height: 96rpx;
border: 8rpx solid #1385FA;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #1385FA;
font-size: 56rpx;
font-weight: bolder;
line-height: 56rpx;
}
.sync-title {
color: #252535;
height: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
margin-top: 40rpx;
}
.sync-status {
@@ -61,3 +85,13 @@
}
}
}
@keyframes sync-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -1,6 +1,12 @@
<block wx:if="{{ show }}">
<view class="sync-mask"></view>
<view class="sync-panel">
<block wx:if="{{ percent === 100 }}">
<view class="sync-tick">✓</view>
</block>
<block wx:else>
<view class="sync-spinner"></view>
</block>
<view class="sync-title">同步成员至设备</view>
<view class="sync-status">{{ status }}</view>
<view class="sync-percent">{{ percent }}%</view>
Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+18 -4
View File
@@ -1,11 +1,25 @@
import type { LeFuConfig } from './types'
import { ENV, type EnvType } from '../utils/env/index'
/** 各环境配网网关地址:develop / trial 走测试网关,release 走正式网关 */
const DOMAINS: Record<EnvType, { domain1: string; domain2: string }> = {
develop: {
domain1: 'http://10.10.10.10:8889',
domain2: 'http://10.10.10.10:8889/weight',
},
trial: {
domain1: 'http://device.shuziweidao.com:80/gateway',
domain2: 'http://device.shuziweidao.com:80/weight',
},
release: {
domain1: 'http://device.shuziweidao.com:80/gateway',
domain2: 'http://device.shuziweidao.com:80/weight',
},
}
/** 乐福 SDK 配置 */
export const LEFU_CONFIG: LeFuConfig = {
key: 'lefudc91611833e18d94',
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
// domain1: 'http://10.10.10.10:8889',
// domain2: 'http://10.10.10.10:8889/weight',
domain1: 'http://device.shuziweidao.com:80/gateway',
domain2: 'http://device.shuziweidao.com:80/weight',
...DOMAINS[ENV],
}
+50 -10
View File
@@ -187,10 +187,17 @@ class LeFuService {
this._doReconnect()
return
}
// 蓝牙不可用:做完整清理
// 蓝牙不可用:蓝牙真关了才清空协议;扫描/连接期间的 UNAVAILABLE 抖动(蓝牙还开着)保留协议
if (res === plugin.BLUE_STATE.UNAVAILABLE) {
let bluetoothOff = false
try { bluetoothOff = wx.getSystemSetting().bluetoothEnabled === false } catch (e) { bluetoothOff = false }
console.log(TAG, 'UNAVAILABLE 排查', { intentionalConnect: this._intentionalConnect, hasProtocol: !!this._activeProtocol, bluetoothOff })
this._stopKeepAlive()
this._stopReconnectTimer()
// 蓝牙还开着:只是抖动,保留协议,避免 isConnected 误判为 false
if (!bluetoothOff) {
return
}
if (this._activeProtocol) {
this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb())
@@ -220,7 +227,9 @@ class LeFuService {
// 即将断开:清理协议实例,若非主动断开则触发重连
plugin.bus.subscribe('deviceWillDisconnect', () => {
console.warn(TAG, 'bus[deviceWillDisconnect]intentionalConnect:', this._intentionalConnect)
let bluetoothEnabled = ''
try { bluetoothEnabled = String(wx.getSystemSetting().bluetoothEnabled) } catch (e) { bluetoothEnabled = 'unknown' }
console.warn(TAG, 'deviceWillDisconnect 排查', { intentionalConnect: this._intentionalConnect, hasProtocol: !!this._activeProtocol, bluetoothEnabled })
this._stopKeepAlive()
if (this._activeProtocol) {
this._activeProtocol = null
@@ -324,25 +333,28 @@ class LeFuService {
reject(new Error('设备未连接,无法获取 WiFi 列表'))
return
}
let settled = false
let timer: number | null = null
let delayTimer: number | null = null
// 15s 整体超时
timer = setTimeout(() => {
const settle = (err: Error | null, data: LeFuWifiItem[] = []) => {
if (settled) return
settled = true
if (timer !== null) { clearTimeout(timer); timer = null }
if (delayTimer !== null) { clearTimeout(delayTimer); delayTimer = null }
reject(new Error('获取 WiFi 列表超时'))
}, 15000)
err ? reject(err) : resolve(data)
}
// 15s 整体超时
timer = setTimeout(() => settle(new Error('获取 WiFi 列表超时')), 15000)
// 延时 2s 后下发查询指令(等待设备进入配网模式)
delayTimer = setTimeout(() => {
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
if (timer !== null) { clearTimeout(timer); timer = null }
if (delayTimer !== null) { clearTimeout(delayTimer); delayTimer = null }
console.log(TAG, 'getWifiList 结果:', res)
if (!res?.length) {
this._activeProtocol.dataExitWifiConfig(() => { })
resolve([])
settle(null, [])
return
}
resolve(res.map((item: any) => ({ ssid: item.ssid || item.name || '', signal: item.signal })))
settle(null, res.map((item: any) => ({ ssid: item.ssid || item.name || '', signal: item.signal })))
})
}, 2000)
})
@@ -411,6 +423,34 @@ class LeFuService {
})
}
// ─── OTA 升级 ─────────────────────────────
/**
* 下发 OTA 固件升级指令
* 只负责调用协议层 codeOtaUpdate,是否需要升级、版本比较等业务判断由调用方处理。
*/
otaUpdate(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
this._activeProtocol.codeOtaUpdate((status: number) => {
status === 0 ? resolve() : reject(new Error('升级指令下发失败'))
})
})
}
// ─── 设备初始化 ───────────────────────────
/**
* 初始化设备信息
* 下发 codeClearDeviceData('00') 指令清除设备信息,区别于 clearDeviceMembers 的 '01'(清空用户数据)。
*/
initDevice(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
this._activeProtocol.codeClearDeviceData('00', (res: number) => {
res === 0 ? resolve() : reject(new Error('请求失败'))
})
})
}
// ─── 用户同步 ─────────────────────────────
/**
* 清空设备内的用户数据
+90 -20
View File
@@ -16,7 +16,7 @@ interface MemberForm {
/** 员工编号查询接口(页面私有) */
const getEmployeeByWorkNo = (sn: string, workNo: string) =>
get<any>('weighingScale/v6/getUserInfoByWkno', { sn, workNo })
get<any>('weighingScale/v5/getUserInfoByWkno', { sn, workNo })
/** 添加/编辑用户接口(页面私有) */
const addUser = (data: Record<string, any>) =>
@@ -54,12 +54,17 @@ Page({
employeeQueryResult: null as Record<string, any> | null,
rawUser: null as Record<string, any> | null,
isEdit: false,
queryError: false,
today: '',
},
onLoad(options: Record<string, string>) {
targetSn = decodeURIComponent(options.sn || '')
console.log(targetSn);
editId = decodeURIComponent(options.id || '')
this.setData({ isEdit: !!editId })
const now = new Date()
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
this.setData({ isEdit: !!editId, today })
if (editId) {
this._loadEditUser(editId)
}
@@ -100,8 +105,9 @@ Page({
})
}
})
.catch(() => {
wx.showToast({ title: '加载用户信息失败', icon: 'none' })
.catch((err: any) => {
console.error('[addUser] 加载用户信息失败:', err)
wx.showToast({ title: err?.msg || err?.message || '加载用户信息失败', icon: 'none' })
})
},
@@ -119,6 +125,7 @@ Page({
serverResult: null,
form: { workNo: '', name: '', idCard: '', gender: '', age: '', height: '', weight: '', sex: 0 },
birthday: '',
queryError: false,
})
},
@@ -198,6 +205,7 @@ Page({
'form.weight': '',
birthday: '',
readonly: false,
queryError: false,
})
},
@@ -220,14 +228,21 @@ Page({
'form.weight': '',
birthday: '',
readonly: false,
queryError: false,
})
if (idCard.length === 18) this.validateIdCard()
},
/** 身份证合法且姓名已填则查询 */
/** 身份证合法则解析:姓名已填走接口查询,未填先前端解析 */
validateIdCard() {
const { idCard, name } = this.data.form
if (isIdCardValid(idCard) && name) this.fetchUser()
if (!isIdCardValid(idCard)) return
if (name) {
this.fetchUser()
return
}
const { gender, age, sex, birthday } = parseIdCard(idCard)
this.setData({ 'form.gender': gender, 'form.age': age, 'form.sex': sex, birthday })
},
/** 查询身份证对应的用户信息,101 复用 */
@@ -239,9 +254,8 @@ Page({
}).then((res) => {
const serverResult = res.result
const { gender, age, sex, birthday } = parseIdCard(serverResult?.idCard || this.data.form.idCard)
this.setData({ 'form.gender': gender, 'form.age': age, 'form.sex': sex, birthday })
this.setData({ 'form.gender': gender, 'form.age': age, 'form.sex': sex, birthday, queryError: false })
if (!serverResult) {
wx.showToast({ title: res.msg || '未查询到用户信息', icon: 'none' })
return
}
if (serverResult.flag === 101) {
@@ -264,7 +278,11 @@ Page({
})
return
}
}).catch(() => {})
}).catch(() => {
// 查询报错:仍按身份证解析性别/年龄,标记需重新查询
const { gender, age, sex, birthday } = parseIdCard(this.data.form.idCard)
this.setData({ 'form.gender': gender, 'form.age': age, 'form.sex': sex, birthday, queryError: true })
})
},
/** 家庭用户:性别切换 */
@@ -286,6 +304,28 @@ Page({
this.setData({ 'form.weight': e.detail.value })
},
/** 身高失焦校验:不在 50-250 则清空 */
onHeightBlur() {
const height = this.data.form.height.trim()
if (!height) return
const h = parseFloat(height)
if (isNaN(h) || h < 50 || h > 250) {
wx.showToast({ title: '身高需在50-250cm之间', icon: 'none' })
this.setData({ 'form.height': '' })
}
},
/** 体重失焦校验:不在 20-300 则清空 */
onWeightBlur() {
const weight = this.data.form.weight.trim()
if (!weight) return
const w = parseFloat(weight)
if (isNaN(w) || w < 20 || w > 300) {
wx.showToast({ title: '体重需在20-300kg之间', icon: 'none' })
this.setData({ 'form.weight': '' })
}
},
/** 复用已有信息:关弹框直接提交 */
onConfirmModalConfirm() {
this.setData({ showConfirmModal: false })
@@ -300,6 +340,7 @@ Page({
serverResult: null,
form: { workNo: '', name: '', idCard: '', gender: '', age: '', height: '', weight: '', sex: 0 },
birthday: '',
queryError: false,
})
},
@@ -308,7 +349,7 @@ Page({
},
/** 提交用户 */
onSubmit() {
async onSubmit() {
const { name, idCard, gender, height, weight, sex } = this.data.form
const { userType } = this.data
const heightNum = parseFloat(height)
@@ -319,10 +360,6 @@ Page({
wx.showToast({ title: '请先查询员工信息', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
return
}
} else {
if (!name.trim()) {
wx.showToast({ title: '请填写昵称', icon: 'none' })
@@ -336,8 +373,39 @@ Page({
wx.showToast({ title: '请填写出生年月', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
}
// 身高体重范围校验(员工/家庭用户共用)
if (!height || isNaN(heightNum) || heightNum < 50 || heightNum > 250) {
wx.showToast({ title: '身高需在50-250cm之间', icon: 'none' })
return
}
if (weight && (isNaN(weightNum) || weightNum < 20 || weightNum > 300)) {
wx.showToast({ title: '体重需在20-300kg之间', icon: 'none' })
return
}
// 家庭用户新增:身份证查询报错 → 重新查询,成功后再提交
let sexFinal = sex
let birthdayFinal = this.data.birthday
if (userType === 'family' && !editId && this.data.queryError) {
try {
const res = await getUserInfoByIdCard({ sn: targetSn, idCard, realname: name.trim() })
const result = res.result
const parsed = parseIdCard(result?.idCard || idCard)
this.setData({
serverResult: result,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: parsed.birthday,
queryError: false,
})
sexFinal = parsed.sex
birthdayFinal = parsed.birthday
} catch (err: any) {
console.error('[addUser] 重新查询失败:', err)
wx.showToast({ title: err?.msg || err?.message || '查询失败,请重试', icon: 'none' })
return
}
}
@@ -352,14 +420,15 @@ Page({
const payload: Record<string, any> = {
...baseSpread,
...(editId ? {} : { relationType: 2 }),
dataType: userType === 'family' ? 1 : 0,
parentUserId: userInfo?.userId ?? '',
sn: targetSn,
scaleDeviceId: targetSn,
realname: name.trim(),
idCard: idCard || null,
sex,
birthday: this.data.birthday,
sex: sexFinal,
birthday: birthdayFinal,
height: heightNum,
weight: isNaN(weightNum) ? 0 : weightNum,
mode: getApp<IAppOption>().globalData.operationsEngineer ? 'ops' : 'normal',
@@ -377,8 +446,9 @@ Page({
prevPage?._loadMembers?.()
setTimeout(() => wx.navigateBack(), 1500)
})
.catch(() => {
wx.showToast({ title: '添加失败,请重试', icon: 'none' })
.catch((err: any) => {
console.error('[addUser] 添加用户失败:', err)
wx.showToast({ title: err?.msg || err?.message || '添加失败,请重试', icon: 'none' })
})
},
})
+6 -6
View File
@@ -78,7 +78,7 @@
</view>
<view class="form-control">
<view class="form-input-wrap">
<input class="form-input form-input-with-suffix" type="digit" maxlength="5" placeholder="请填写身高" placeholder-class="form-placeholder" value="{{ form.height }}" bind:input="onHeightInput" />
<input class="form-input form-input-with-suffix" type="digit" maxlength="5" placeholder="请填写身高" placeholder-class="form-placeholder" value="{{ form.height }}" bind:input="onHeightInput" bind:blur="onHeightBlur" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
@@ -87,7 +87,7 @@
<view class="form-label"><text class="label-text">体重</text></view>
<view class="form-control">
<view class="form-input-wrap">
<input class="form-input form-input-with-suffix" type="digit" maxlength="6" placeholder="请填写体重" placeholder-class="form-placeholder" value="{{ form.weight }}" bind:input="onWeightInput" />
<input class="form-input form-input-with-suffix" type="digit" maxlength="6" placeholder="请填写体重" placeholder-class="form-placeholder" value="{{ form.weight }}" bind:input="onWeightInput" bind:blur="onWeightBlur" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
@@ -109,7 +109,7 @@
<text class="label-star">*</text>
</view>
<view class="form-control">
<input class="form-input" type="text" maxlength="20" placeholder="请填写昵称" placeholder-class="form-placeholder" value="{{ form.name }}" disabled="{{ readonly }}" bind:input="onNameInput" />
<input class="form-input" type="text" maxlength="20" placeholder="请填写昵称" placeholder-class="form-placeholder" value="{{ form.name }}" disabled="{{ readonly }}" bind:input="onNameInput" bind:blur="onNameBlur" />
</view>
</view>
@@ -142,7 +142,7 @@
<text class="label-star">*</text>
</view>
<view class="form-control">
<picker mode="date" value="{{ birthday }}" bindchange="onBirthdayChange">
<picker mode="date" value="{{ birthday }}" end="{{ today }}" bindchange="onBirthdayChange">
<view class="form-picker {{ birthday ? '' : 'form-picker-placeholder' }}">{{ birthday || '请选择出生年月' }}</view>
</picker>
</view>
@@ -157,7 +157,7 @@
</view>
<view class="form-control">
<view class="form-input-wrap">
<input class="form-input form-input-with-suffix" type="digit" maxlength="5" placeholder="请填写身高" placeholder-class="form-placeholder" value="{{ form.height }}" bind:input="onHeightInput" />
<input class="form-input form-input-with-suffix" type="digit" maxlength="5" placeholder="请填写身高" placeholder-class="form-placeholder" value="{{ form.height }}" bind:input="onHeightInput" bind:blur="onHeightBlur" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
@@ -166,7 +166,7 @@
<view class="form-label"><text class="label-text">体重</text></view>
<view class="form-control">
<view class="form-input-wrap">
<input class="form-input form-input-with-suffix" type="digit" maxlength="6" placeholder="请填写体重" placeholder-class="form-placeholder" value="{{ form.weight }}" bind:input="onWeightInput" />
<input class="form-input form-input-with-suffix" type="digit" maxlength="6" placeholder="请填写体重" placeholder-class="form-placeholder" value="{{ form.weight }}" bind:input="onWeightInput" bind:blur="onWeightBlur" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
@@ -29,16 +29,18 @@ let scannedDevices: ScannedDevice[] = []
let unsubDevicesList: (() => void) | null = null
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
let unsubDeviceInfo: (() => void) | null = null
/** 连接流程的清理句柄 */
let connectCleanup: {
timer: number | null
unsubState: (() => void) | null
unsubConnect: (() => void) | null
unsubInfo: (() => void) | null
} | null = null
/** 一次性防重入标记 */
let modalLocked = false
let autoConnectTried = false
/** 进入页面时是否已连接(返回时恢复被扫描打断的连接) */
let enteredConnected = false
Page({
data: {
@@ -46,9 +48,11 @@ Page({
deviceList: [] as DeviceItem[],
},
onLoad() {
enteredConnected = leFuService.isConnected
},
onShow() {
// 订阅设备信息,连接后拿到 SN 绑定到用户
this._subscribeDeviceInfo()
// 从其他页面返回时同步连接状态
if (leFuService.isConnected) {
this._markConnected()
@@ -66,11 +70,13 @@ Page({
},
onUnload() {
unsubDeviceInfo?.()
unsubDeviceInfo = null
leFuService.stopScan()
this._clearSubscriptions()
this._clearConnectCleanup()
// 进入前已连接、扫描打断连接后返回:恢复原连接
if (enteredConnected) {
leFuService.autoConnect()
}
},
/** 点击「开始 / 重新搜索」 */
@@ -108,31 +114,35 @@ Page({
wx.openAppAuthorizeSetting()
},
/** 订阅设备固件信息,拿到 SN 绑定到当前用户 */
_subscribeDeviceInfo() {
unsubDeviceInfo?.()
unsubDeviceInfo = leFuService.onDeviceInfo((info) => {
const sn = info?.serialNumber
if (!sn) return
this._bindDevice(sn)
})
},
/** 调用接口把设备 SN 绑定到已保存的用户信息 */
_bindDevice(sn: string) {
/** 调用接口把设备 SN 绑定到已保存的用户信息,成功后回调 */
_bindDevice(sn: string, onSuccess?: () => void) {
const userInfo = wx.getStorageSync('userInfo') as UserInfo | null
const idCard = userInfo?.idCard
if (!idCard) return
const connectDeviceInfo = leFuService.lastRawDevice ? JSON.stringify(leFuService.lastRawDevice) : ''
if (!idCard) {
onSuccess?.()
return
}
bindDevice({ sn, idCard, connectDeviceInfo })
.then((res) => {
console.log('[connectedDevice] bindDevice 成功:', res)
onSuccess?.()
})
.catch((err) => {
console.error('[connectedDevice] bindDevice 失败:', err)
this._resetAfterBindFail(err)
})
},
/** bindDevice 失败后的清理重置:断开连接、清缓存、重置页面状态 */
_resetAfterBindFail(err?: any) {
leFuService.disconnect()
wx.removeStorageSync('connectDeviceInfo')
scannedDevices = []
this.setData({ status: 'idle', deviceList: [] })
wx.showToast({ title: err?.msg || err?.message || '绑定失败,请重试', icon: 'none' })
},
/** 将所有卡片置为未连接 */
_markAllDisconnected() {
this.setData({
@@ -170,6 +180,7 @@ Page({
if (connectCleanup.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup.unsubState?.()
connectCleanup.unsubConnect?.()
connectCleanup.unsubInfo?.()
connectCleanup = null
},
@@ -306,38 +317,63 @@ Page({
leFuService.connect(scanned.raw)
this._clearConnectCleanup()
connectCleanup = { timer: null, unsubState: null, unsubConnect: null }
connectCleanup = { timer: null, unsubState: null, unsubConnect: null, unsubInfo: null }
const finish = () => {
if (connectCleanup?.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup?.unsubState?.()
connectCleanup?.unsubConnect?.()
connectCleanup?.unsubInfo?.()
connectCleanup = null
wx.hideLoading()
}
// 连接成功 + SN 到位两个信号都满足才调接口,各自事件独立触发、顺序不定
let connected = false
let sn: string | undefined
const tryBind = () => {
if (!connected || !sn) return
finish()
// 手动/自动连接:绑定设备成功后缓存设备并跳配网
this._bindDevice(sn, () => {
wx.setStorageSync('connectDeviceInfo', { ...scanned.raw, scaleDeviceName: scanned.raw.name })
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
})
}
connectCleanup.timer = setTimeout(() => {
finish()
if (connected) {
// 已连上但一直没拿到 SN:断开并重置,避免设备残留占用连接
leFuService.disconnect()
wx.removeStorageSync('connectDeviceInfo')
scannedDevices = []
this.setData({ status: 'idle', deviceList: [] })
wx.showToast({ title: '设备信息异常,请重试', icon: 'none' })
} else {
wx.showToast({ title: '连接超时', icon: 'none' })
}
}, 15000)
connectCleanup.unsubState = leFuService.onConnectState((state) => {
if (state === leFuService.BLUE_STATE.CONNECTFAILED) {
finish()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
} else if (state === leFuService.BLUE_STATE.UNAVAILABLE) {
// 连接过程中蓝牙被关闭:立即结束 loading,避免卡到超时
finish()
wx.showToast({ title: '蓝牙已断开,请重新开启', icon: 'none' })
}
// UNAVAILABLE 不在此处理:connect 过程中 clearProtocol 会触发一次 UNAVAILABLE 抖动,
// 后面紧跟 CONNECTSUCCESS,误判会打断连接流程并误报「蓝牙关闭」。
// 真正「连接后关闭蓝牙」由 onDisconnected 回调处理。
})
connectCleanup.unsubConnect = leFuService.onDeviceConnect(() => {
finish()
wx.setStorageSync('connectDeviceInfo', { ...scanned.raw, scaleDeviceName: scanned.raw.name })
if (!isAuto) {
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
}
connected = true
sn = leFuService.deviceInfo?.serialNumber
tryBind()
})
connectCleanup.unsubInfo = leFuService.onDeviceInfo((info) => {
sn = info?.serialNumber
tryBind()
})
},
@@ -26,7 +26,7 @@
<!-- 帮助文案 -->
<view class="help">
<view class="help-text">找不到设备?</view>
<view class="help-link" bind:tap="onOpenHelp">检查附近设备权限</view>
<view class="help-link" bind:tap="onOpenHelp">前往设置页面打开权限</view>
</view>
<!-- 搜索结果模块:found / empty 时整体显示 -->
@@ -355,10 +355,6 @@
border-radius: 44rpx;
background-color: #1385FA;
}
.wifiBtn-disabled {
background-color: #A8C9F0;
}
}
/* 空态 */
@@ -4,6 +4,8 @@ import { leFuService } from '../../lefu/index'
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
let retryTimer: number | null = null
/** 获取 WiFi 列表防重入标记 */
let searching = false
/** 渲染用的 WiFi 项(在 SSID 基础上追加连接状态) */
interface WifiItem {
@@ -74,10 +76,12 @@ Page({
/** 通过设备协议获取周围 WiFi 列表 */
async onStartSearch() {
if (searching) return
if (!this._ensureConnected()) {
this.setData({ status: 'idle', wifiList: [] })
return
}
searching = true
this.setData({ status: 'searching', wifiList: [] })
wx.showLoading({ title: '正在获取 WiFi 列表...', mask: true })
try {
@@ -95,6 +99,8 @@ Page({
wx.hideLoading()
this.setData({ status: 'empty', wifiList: [] })
wx.showToast({ title: '获取 WiFi 列表失败', icon: 'none' })
} finally {
searching = false
}
},
@@ -165,8 +171,11 @@ Page({
},
onConfirm() {
if (!this.data.hasConnected) return
if (!this._ensureConnected()) return
wx.navigateTo({ url: '/pages/equipment/equipment' })
wx.reLaunch({ url: '/pages/equipment/equipment' })
},
onSkip() {
wx.reLaunch({ url: '/pages/home/home' })
}
})
@@ -32,7 +32,7 @@
</block>
</view>
<view bind:tap="inputTypeChange">
<image src="/images/connectedWifi/{{type ? 'open' : 'close'}}.png"/>
<image src="/images/connectedWifi/{{type ? 'close' : 'open'}}.png"/>
</view>
</view>
</view>
@@ -84,6 +84,9 @@
<view class="wifiHeader">
<view>
<view>附近WiFi</view>
<!-- <block wx:if="{{!hasConnected}}"> -->
<view bind:tap="onRefresh">刷新</view>
<!-- </block> -->
</view>
<view>网络列表(请选择一个网络进行连接)</view>
</view>
@@ -113,7 +116,12 @@
</block>
</scroll-view>
<view class="wifiBtn">
<view class="{{hasConnected ? '' : 'wifiBtn-disabled'}}" bind:tap="onConfirm">确认</view>
<block wx:if="{{hasConnected}}">
<view bind:tap="onConfirm">完成</view>
</block>
<block wx:else>
<view bind:tap="onSkip">跳过</view>
</block>
</view>
</block>
@@ -131,6 +139,9 @@
</view>
<text>未查找到可用wifi,请重试</text>
</view>
<view class="wifiBtn">
<view bind:tap="onSkip">跳过</view>
</view>
</block>
</block>
</view>
+155 -19
View File
@@ -12,7 +12,7 @@
padding: 40rpx 30rpx 30rpx;
box-sizing: border-box;
border-radius: 24rpx;
margin-bottom: 30rpx;
margin-bottom: 15rpx;
background-color: #FFFFFF;
&::before {
@@ -225,13 +225,37 @@
}
}
.data-user-title {
width: 100%;
height: 80rpx;
display: flex;
flex-wrap: nowrap;
box-sizing: border-box;
justify-content: space-between;
> view:nth-of-type(1) {
color: #252535;
height: 80rpx;
font-size: 32rpx;
font-weight: bolder;
line-height: 80rpx;
}
> view:nth-of-type(2) {
color: #808080;
height: 80rpx;
font-size: 26rpx;
line-height: 80rpx;
}
}
.data-user-list {
flex: 1;
overflow: hidden;
.scrollViewContent {
width: 100%;
padding: 24rpx 30rpx;
padding: 0 30rpx;
box-sizing: border-box;
/* 历史记录列表 */
@@ -240,41 +264,153 @@
.list {
width: 100%;
margin-bottom: 30rpx;
padding: 30rpx 48rpx 30rpx 30rpx;
border-radius: 24rpx;
box-sizing: border-box;
background-color: #FFFFFF;
margin-bottom: 24rpx;
>view:first-of-type {
.list-examTime {
color: #808080;
width: 100%;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
margin-bottom: 20rpx;
margin-bottom: 24rpx;
}
>view:last-of-type {
.list-weight {
width: 100%;
height: 96rpx;
height: 48rpx;
display: flex;
justify-content: space-between;
padding-left: 30rpx;
padding-right: 50rpx;
box-sizing: border-box;
flex-wrap: nowrap;
border-radius: 16rpx;
background-color: #FFFFFF;
justify-content: space-between;
margin-bottom: 30rpx;
>view {
color: #808080;
font-size: 26rpx;
height: 96rpx;
line-height: 96rpx;
> view:nth-of-type(1) {
color: #252535;
height: 48rpx;
font-size: 48rpx;
font-weight: bolder;
line-height: 48rpx;
text {
color: #808080;
font-size: 24rpx;
margin-left: 6rpx;
font-weight: normal;
}
}
> view:nth-of-type(2) {
height: 48rpx;
padding: 0 16rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
border-radius: 8rpx;
background-color: #EAFAF6;
> view:nth-of-type(1) {
width: 14rpx;
height: 18rpx;
display: flex;
align-items: center;
margin-right: 8rpx;
}
> view:nth-of-type(2) {
color: #2AC79F;
font-size: 26rpx;
height: 26rpx;
line-height: 26rpx;
}
&.diff-up {
background-color: #FEF0F0;
> view:nth-of-type(2) {
color: #F24439;
}
}
}
&.arrow::after {
right: -24rpx;
}
}
.list-bmi {
width: 100%;
height: 26rpx;
display: flex;
flex-wrap: nowrap;
> view {
height: 26rpx;
display: flex;
flex-wrap: nowrap;
margin-right: 55rpx;
> view:nth-of-type(1) {
height: 26rpx;
width: 26rpx;
display: flex;
align-items: center;
margin-right: 10rpx;
}
> view:nth-of-type(2) {
color: #77849E;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
margin-right: 20rpx;
}
> view:nth-of-type(3) {
color: #252535;
font-weight: bold;
height: 26rpx;
font-size: 26rpx;
font-weight: bolder;
line-height: 26rpx;
}
}
}
// >view:first-of-type {
// color: #808080;
// width: 100%;
// height: 24rpx;
// font-size: 24rpx;
// line-height: 24rpx;
// margin-bottom: 20rpx;
// }
// >view:last-of-type {
// width: 100%;
// height: 96rpx;
// display: flex;
// justify-content: space-between;
// padding-left: 30rpx;
// padding-right: 50rpx;
// box-sizing: border-box;
// flex-wrap: nowrap;
// border-radius: 16rpx;
// background-color: #FFFFFF;
// >view {
// color: #808080;
// font-size: 26rpx;
// height: 96rpx;
// line-height: 96rpx;
// text {
// color: #252535;
// font-weight: bold;
// }
// }
// }
}
}
+11 -2
View File
@@ -38,6 +38,10 @@ interface MeasureRecord {
weight: number
bmi: number
fatRate: number
/** 相比上次体重差值绝对值(kg),首次测量为 null */
weightDiff: number | null
/** 变化方向:1增加 -1减少 0持平 null首次 */
weightTrend: number | null
}
/** GET /weighingScale/v3/getMeasureHistoryPage 返回结构 */
@@ -150,6 +154,8 @@ Page({
weight: r.weight ?? 0,
bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0,
weightDiff: r.weightDiff == null ? null : Math.abs(r.weightDiff),
weightTrend: r.weightTrend ?? null,
}))
const total = page.total ?? 0
@@ -191,6 +197,8 @@ Page({
weight: r.weight ?? 0,
bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0,
weightDiff: r.weightDiff == null ? null : Math.abs(r.weightDiff),
weightTrend: r.weightTrend ?? null,
}))
const allRecords = [...this.data.records, ...newRecords]
this.setData({
@@ -199,8 +207,9 @@ Page({
hasMore: allRecords.length < this.data.total,
})
})
.catch(() => {
wx.showToast({ title: '加载失败,请重试', icon: 'none' })
.catch((err: any) => {
console.error('[data] 加载历史数据失败:', err)
wx.showToast({ title: err?.msg || err?.message || '加载失败,请重试', icon: 'none' })
})
.finally(() => {
wx.hideLoading()
+35 -2
View File
@@ -56,6 +56,11 @@
</view>
</view>
<view class="data-user-title page-container">
<view>记录列表</view>
<view>共 {{total}} 条记录</view>
</view>
<view class="data-user-list">
<scroll-view class="scrollView" scroll-y refresher-enabled refresher-triggered="{{ refresherTriggered }}" bind:refresherrefresh="onRefresh" bind:scrolltolower="onScrollToLower">
<view class="scrollViewContent">
@@ -65,12 +70,40 @@
<view class="data-list">
<block wx:for="{{ records }}" wx:key="recordId">
<view class="list" bind:tap="onTapRecord" data-record-id="{{ item.recordId }}">
<view>{{ item.examTime }}</view>
<view class="list-examTime">{{ item.examTime }}</view>
<view class="list-weight arrow">
<view>{{ item.weight }}<text>kg</text></view>
<block wx:if="{{ item.weightTrend === 1 || item.weightTrend === -1 }}">
<view class="{{ item.weightTrend === 1 ? 'diff-up' : '' }}">
<view>
<image src="{{ item.weightTrend === 1 ? '/images/data/add.png' : '/images/data/sub.png' }}" mode="aspectFit" />
</view>
<view>{{ item.weightDiff }}</view>
</view>
</block>
</view>
<view class="list-bmi">
<view>
<view>
<image src="/images/data/user.png" mode="aspectFit" />
</view>
<view>BMI</view>
<view>{{ item.bmi }}</view>
</view>
<view>
<view>
<image src="/images/data/droplet.png" mode="aspectFit" />
</view>
<view>体脂率</view>
<view>{{ item.fatRate }}%</view>
</view>
</view>
<!-- <view>{{ item.examTime }}</view>
<view class="arrow">
<view>体重:<text>{{ item.weight }}kg</text></view>
<view>BMI<text>{{ item.bmi }}</text></view>
<view>体脂率:<text>{{ item.fatRate }}%</text></view>
</view>
</view> -->
</view>
</block>
</view>
@@ -208,11 +208,27 @@
}
.basicIndicatorsLabel {
display: flex;
align-items: center;
height: 30rpx;
margin-bottom: 20rpx;
.basicIndicatorsLabelText {
color: #808080;
height: 30rpx;
font-size: 26rpx;
line-height: 30rpx;
margin-bottom: 20rpx;
}
.basicIndicatorsLabelExtra {
color: #FFFFFF;
height: 28rpx;
font-size: 20rpx;
line-height: 28rpx;
padding: 0 12rpx;
border-radius: 8rpx;
margin-left: 8rpx;
}
}
.basicIndicatorsValueRow {
@@ -83,6 +83,10 @@ interface MetricRow {
/** 基础指标网格单元 */
interface BasicCell {
label: string
/** 名称后跟的状态文字(如 BMI 的"超重" */
labelExtra: string
/** 状态文字背景色 */
labelExtraColor: string
value: string
unit: string
}
@@ -108,10 +112,27 @@ interface BodyPartsCard {
rows: BodyPart[][]
}
const EMPTY_BASIC_CELL: BasicCell = { label: '', value: '', unit: '' }
const EMPTY_BASIC_CELL: BasicCell = { label: '', labelExtra: '', labelExtraColor: '', value: '', unit: '' }
const DEFAULT_STATUS_COLOR = '#B6B6B6'
/** BMI 体型状态对应的刻度色 */
const BMI_TYPE_COLOR: Record<string, string> = {
: '#1385FA',
: '#2AC79F',
: '#FFA151',
: '#F24439',
}
/** 基础指标:label 前端写死 */
const BASIC_INDICATORS: { key: string; label: string }[] = [
{ key: 'ppWeightKg', label: '体重' },
{ key: 'ppBMI', label: 'BMI' },
{ key: 'ppFat', label: '体脂率' },
{ key: 'ppBodyScore', label: '健康评分' },
{ key: 'ppBodyHealth', label: '健康评价' },
]
/** 将 bodyDataList 转为 key -> 指标 的映射 */
const toMap = (list: LefuBodyDatum[] = []): Record<string, LefuBodyDatum> => {
const map: Record<string, LefuBodyDatum> = {}
@@ -159,11 +180,28 @@ const splitWeight = (weight: number): { int: string; decimal: string } => {
return { int, decimal: `.${dec}` }
}
/** 根据 BMI 计算刻度圆圈位置(百分比) */
/**
* 刻度轨道结构(rpx):屏宽 750 - basicIndicators 左右 padding 30*2 - 左右 border 4*2 = 682。
* BLOCK_GAP 与 less 中 basicIndicatorsTrack 的 gap: 6rpx 保持同步。
*/
const TRACK_WIDTH = 682
const BLOCK_GAP = 6
const BLOCK_WIDTH = (TRACK_WIDTH - BLOCK_GAP * 3) / 4
/** 根据 BMI 计算刻度圆圈圆心位置(百分比),对齐消瘦/正常/超重/肥胖四段分界 18.5/24/28,并计入色块间隙 */
const calcBmiLeft = (bmi?: number): number => {
if (bmi == null || isNaN(bmi)) return 0
const pct = (bmi - 14) / (40 - 14)
return Math.min(Math.max(pct, 0), 1) * 100
let left: number
if (bmi < 18.5) {
left = (bmi / 18.5) * BLOCK_WIDTH
} else if (bmi < 24) {
left = BLOCK_WIDTH + BLOCK_GAP + ((bmi - 18.5) / (24 - 18.5)) * BLOCK_WIDTH
} else if (bmi < 28) {
left = BLOCK_WIDTH * 2 + BLOCK_GAP * 2 + ((bmi - 24) / (28 - 24)) * BLOCK_WIDTH
} else {
left = BLOCK_WIDTH * 3 + BLOCK_GAP * 3 + ((bmi - 28) / (40 - 28)) * BLOCK_WIDTH
}
return Math.min(Math.max(left / TRACK_WIDTH, 0), 1) * 100
}
Page({
@@ -189,7 +227,6 @@ Page({
weightControl: { title: '体重管控', rows: [] } as MetricCard,
// 以下卡片为 8 电极专属
isEightElectrode: false,
cellWater: { title: '细胞液分析', rows: [] } as MetricCard,
advancedComposition: { title: '进阶成分', rows: [] } as MetricCard,
bodyFat: { title: '躯干/四肢脂肪', rows: [] } as BodyPartsCard,
@@ -231,12 +268,25 @@ Page({
changeText,
}
// 基础指标(2 行 x 3 列,第 6 格留空保持布局)
const basicKeys = ['ppWeightKg', 'ppBMI', 'ppFat', 'ppBodyScore', 'ppBodyHealth']
const basicCells: BasicCell[] = basicKeys.map((k) => {
const d = map[k]
if (!d) return { ...EMPTY_BASIC_CELL }
return { label: d.bodyParamName || k, value: d.currentValue ?? '', unit: d.unit || '' }
// 基础指标(2 行 x 3 列,第 6 格留空保持布局label 前端写死,缺失值显示 -
const basicCells: BasicCell[] = BASIC_INDICATORS.map(({ key, label }) => {
const d = map[key]
const isScore = key === 'ppBodyScore'
const isHealth = key === 'ppBodyHealth'
const isBmi = key === 'ppBMI'
if (!d) {
return { label, labelExtra: '', labelExtraColor: '', value: '-', unit: '' }
}
return {
label,
// BMI 名称后跟体型状态(消瘦/正常/超重/肥胖)
labelExtra: isBmi ? data.bmiType || '' : '',
labelExtraColor: isBmi ? BMI_TYPE_COLOR[data.bmiType || ''] || DEFAULT_STATUS_COLOR : '',
// 健康评价展示等级文案(如"亚健康"),其余展示测量数值
value: isHealth ? d.standardTitle || d.currentValue || '-' : d.currentValue || '-',
// 健康评分补"分"
unit: isScore ? d.unit || '分' : isHealth ? '' : d.unit || '',
}
})
basicCells.push({ ...EMPTY_BASIC_CELL })
const basicRows = [basicCells.slice(0, 3), basicCells.slice(3, 6)]
@@ -310,7 +360,8 @@ Page({
['右腿', 'ppBodyFatKgRightLeg', 'ppBodyFatRateRightLeg'],
['躯干', 'ppBodyFatKgTrunk', 'ppBodyFatRateTrunk'],
]
const bodyFat = isEight
const hasBodyFatData = fatPartLabels.some(([, massKey, rateKey]) => map[massKey] || map[rateKey])
const bodyFat = isEight && hasBodyFatData
? {
title: '躯干/四肢脂肪',
rows: chunkParts(
@@ -328,7 +379,8 @@ Page({
['右腿', 'ppMuscleKgRightLeg', 'ppMuscleRateRightLeg'],
['躯干', 'ppMuscleKgTrunk', 'ppMuscleRateTrunk'],
]
const bodyMuscle = isEight
const hasBodyMuscleData = musclePartLabels.some(([, massKey, rateKey]) => map[massKey] || map[rateKey])
const bodyMuscle = isEight && hasBodyMuscleData
? {
title: '躯干/四肢肌肉',
rows: chunkParts(
@@ -347,7 +399,6 @@ Page({
fatAnalysis,
physique,
weightControl,
isEightElectrode: isEight,
cellWater,
advancedComposition,
bodyFat,
@@ -28,8 +28,12 @@
<view class="weight-unit">kg</view>
</view>
<view class="header-info">
<view class="header-bmi" wx:if="{{ header.bmiText }}">{{ header.bmiText }}</view>
<view class="header-change" wx:if="{{ header.changeText }}">{{ header.changeText }}</view>
<block wx:if="{{ header.bmiText }}">
<view class="header-bmi">{{ header.bmiText }}</view>
</block>
<block wx:if="{{ header.changeText }}">
<view class="header-change">{{ header.changeText }}</view>
</block>
</view>
</view>
</view>
@@ -44,10 +48,17 @@
<view class="basicIndicatorsRow" wx:for="{{ basicRows }}" wx:for-item="row" wx:key="index">
<view class="basicIndicatorsCell" wx:for="{{ row }}" wx:for-item="cell" wx:key="label">
<block wx:if="{{ cell.label }}">
<view class="basicIndicatorsLabel">{{ cell.label }}</view>
<view class="basicIndicatorsLabel">
<view class="basicIndicatorsLabelText">{{ cell.label }}</view>
<block wx:if="{{ cell.labelExtra }}">
<view class="basicIndicatorsLabelExtra" style="background-color: {{ cell.labelExtraColor }};">{{ cell.labelExtra }}</view>
</block>
</view>
<view class="basicIndicatorsValueRow">
<view class="basicIndicatorsValue">{{ cell.value }}</view>
<view class="basicIndicatorsUnit" wx:if="{{ cell.unit }}">{{ cell.unit }}</view>
<block wx:if="{{ cell.unit }}">
<view class="basicIndicatorsUnit">{{ cell.unit }}</view>
</block>
</view>
</block>
</view>
@@ -77,6 +88,7 @@
</view>
<!-- 身体成分 -->
<block wx:if="{{ bodyComposition.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -86,12 +98,16 @@
<view class="metricCardRow" wx:for="{{ bodyComposition.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
</block>
<!-- 脂肪分析 -->
<block wx:if="{{ fatAnalysis.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -101,12 +117,16 @@
<view class="metricCardRow" wx:for="{{ fatAnalysis.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
</block>
<!-- 体态管理 -->
<block wx:if="{{ physique.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -116,12 +136,16 @@
<view class="metricCardRow" wx:for="{{ physique.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
</block>
<!-- 体重管控 -->
<block wx:if="{{ weightControl.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -131,14 +155,16 @@
<view class="metricCardRow" wx:for="{{ weightControl.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
<block wx:if="{{ isEightElectrode }}">
</block>
<!-- 细胞液分析 -->
<block wx:if="{{ cellWater.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -148,12 +174,16 @@
<view class="metricCardRow" wx:for="{{ cellWater.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
</block>
<!-- 进阶成分 -->
<block wx:if="{{ advancedComposition.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -163,12 +193,16 @@
<view class="metricCardRow" wx:for="{{ advancedComposition.rows }}" wx:key="label">
<view class="metricCardLabel">{{ item.label }}</view>
<view class="metricCardValue">{{ item.value }}{{ item.unit }}</view>
<view class="metricCardStatus" wx:if="{{ item.status }}" style="background-color: {{ item.color }};">{{ item.status }}</view>
<block wx:if="{{ item.status }}">
<view class="metricCardStatus" style="background-color: {{ item.color }};">{{ item.status }}</view>
</block>
</view>
</view>
</view>
</block>
<!-- 躯干/四肢脂肪 -->
<block wx:if="{{ bodyFat.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -189,8 +223,10 @@
</view>
</view>
</view>
</block>
<!-- 躯干/四肢肌肉 -->
<block wx:if="{{ bodyMuscle.rows.length }}">
<view class="metricCard">
<view class="metricCardTitle">
<view class="metricCardBar"></view>
@@ -211,7 +247,6 @@
</view>
</view>
</view>
</block>
</view>
+83 -20
View File
@@ -83,27 +83,47 @@
}
}
/* 描边按钮(蓝色/红色) */
.ops-btn {
width: 180rpx;
.section-right {
height: 48rpx;
display: flex;
flex-wrap: nowrap;
.section-right-red {
height: 48rpx;
padding: 0 15rpx;
border-radius: 8rpx;
font-size: 24rpx;
line-height: 44rpx;
text-align: center;
box-sizing: border-box;
flex-shrink: 0;
}
.ops-btn--blue {
border: 2rpx solid #1385FA;
color: #1385FA;
}
.ops-btn--red {
border: 2rpx solid #F24439;
color: #F24439;
}
.section-right-blue {
height: 48rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 0 15rpx;
border-radius: 24rpx;
background-color: #E5E9EF;
> view:nth-child(1) {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
margin-right: 6rpx;
}
> view:nth-child(2) {
color: #77849E;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
}
/* 设备卡片 */
@@ -235,7 +255,7 @@
.device-actions {
width: 100%;
gap: 12px;
gap: 10px;
display: flex;
flex-wrap: nowrap;
justify-content: space-around;
@@ -251,18 +271,61 @@
border: 2rpx solid #EAECF1;
> view:nth-of-type(1) {
width: 26rpx;
height: 26rpx;
width: 24rpx;
height: 24rpx;
display: flex;
align-items: center;
margin-right: 10rpx;
margin-right: 6rpx;
}
> view:nth-of-type(2) {
color: #77849E;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
&.user {
background-color: #1385FA;
> view:nth-of-type(2) {
color: #FFFFFF;
}
}
}
}
.device-update {
width: 100%;
gap: 10px;
display: flex;
flex-wrap: nowrap;
margin-top: 20rpx;
justify-content: space-around;
> view {
flex: 1;
height: 56rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
border-radius: 28rpx;
border: 2rpx solid #EAECF1;
> view:nth-of-type(1) {
width: 24rpx;
height: 24rpx;
display: flex;
align-items: center;
margin-right: 6rpx;
}
> view:nth-of-type(2) {
color: #77849E;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
+159 -17
View File
@@ -39,6 +39,25 @@ const uploadHistoryData = (data: Record<string, any>) =>
/** SN 转 MAC:每两位加一个冒号(如 CEE9052031AC → CE:E9:05:20:31:AC */
const formatMac = (sn: string) => sn.match(/.{2}/g)?.join(':') ?? ''
/** 服务端固件版本(weighingScale/v2/firmware/version 接口 result 字段) */
interface FirmwareVersion {
mcuVersion: string
bleVersion: string
wifiVersion: string
resVersion: string
}
/** 获取服务端最新固件版本 */
const getFirmwareVersion = (modelNumber: string) =>
get<FirmwareVersion>('weighingScale/v2/firmware/version', { type: modelNumber }, { loading: false })
/** 判断服务端版本是否比设备版本新(两端都去掉点号后转数字比较) */
const isServerNewer = (deviceVer: string, serverVer: string) => {
const dn = parseInt(deviceVer, 10)
const sn = parseInt((serverVer || '').replace(/\./g, ''), 10)
return sn > dn
}
/** 展示用设备项 */
interface DeviceItem {
id: string
@@ -54,7 +73,9 @@ let unsubDevicesList: (() => void) | null = null
let unsubConnect: (() => void) | null = null
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
let unsubConnectState: (() => void) | null = null
let scanTimer: number | null = null
let connectTimer: number | null = null
Page({
data: {
@@ -90,7 +111,10 @@ Page({
unsubDevicesList = null
unsubConnect?.()
unsubConnect = null
unsubConnectState?.()
unsubConnectState = null
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
if (connectTimer !== null) { clearTimeout(connectTimer); connectTimer = null }
},
/** 订阅断开事件:设备长时间未操作或意外断开时,同步页面状态为未连接 */
@@ -126,11 +150,16 @@ Page({
try {
info = JSON.parse(item.connectDeviceInfo || '{}')
} catch {}
const isConnected = leFuService.isConnected
const lastDeviceId = leFuService.lastRawDevice?.deviceId ?? ''
const infoDeviceId = info.deviceId ?? ''
const status = isConnected && lastDeviceId === infoDeviceId
console.log('[equipment] loadDevices 设备状态', { name: info.name, isConnected, lastDeviceId, infoDeviceId, status })
return {
id: item.id,
name: info.name ?? '',
status: leFuService.isConnected && leFuService.lastRawDevice?.deviceId === info.deviceId,
deviceId: info.deviceId ?? '',
status,
deviceId: infoDeviceId,
sn: item.scaleDeviceId,
userNum: item.userNum ?? 0,
connectDeviceInfo: item.connectDeviceInfo ?? '',
@@ -155,6 +184,10 @@ Page({
if (!res.confirm) return
unbindDevice(item.id)
.then(() => {
// 取消配对的是当前连接的设备:断开连接并清缓存
if (leFuService.isConnected && leFuService.lastRawDevice?.deviceId === item.deviceId) {
leFuService.disconnect()
}
const cached = wx.getStorageSync('connectDeviceInfo') as Record<string, any> | null
if (cached?.deviceId === item.deviceId) {
wx.removeStorageSync('connectDeviceInfo')
@@ -195,7 +228,10 @@ Page({
unsubDevicesList = null
unsubConnect?.()
unsubConnect = null
unsubConnectState?.()
unsubConnectState = null
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
if (connectTimer !== null) { clearTimeout(connectTimer); connectTimer = null }
wx.showLoading({ title: '连接中...', mask: true })
wx.openBluetoothAdapter({
@@ -214,12 +250,40 @@ Page({
unsubDevicesList?.()
unsubDevicesList = null
leFuService.stopScan()
leFuService.connect(matched.raw)
unsubConnect = leFuService.onDeviceConnect(() => {
// 连接超时兜底:15 秒未连上则结束 loading
connectTimer = setTimeout(() => {
connectTimer = null
unsubConnect?.()
unsubConnect = null
unsubConnectState?.()
unsubConnectState = null
wx.hideLoading()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
}, 15000)
// 连接失败:立即结束 loading
unsubConnectState = leFuService.onConnectState((state) => {
if (state !== leFuService.BLUE_STATE.CONNECTFAILED) return
if (connectTimer !== null) { clearTimeout(connectTimer); connectTimer = null }
unsubConnectState?.()
unsubConnectState = null
unsubConnect?.()
unsubConnect = null
wx.hideLoading()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
})
leFuService.connect(matched.raw)
unsubConnect = leFuService.onDeviceConnect(() => {
if (connectTimer !== null) { clearTimeout(connectTimer); connectTimer = null }
unsubConnect?.()
unsubConnect = null
unsubConnectState?.()
unsubConnectState = null
wx.hideLoading()
wx.setStorageSync('connectDeviceInfo', matched.raw)
this._updateStatus(targetDeviceId, true)
})
})
leFuService.startScan()
@@ -282,6 +346,84 @@ Page({
})
},
/** OTA 升级:拉服务端最新版本,比较后下发升级指令 */
onTapOta() {
const deviceInfo = leFuService.deviceInfo
if (!leFuService.isConnected) {
wx.showToast({ title: '请先连接设备', icon: 'none' })
return
}
if (!deviceInfo) {
wx.showToast({ title: '固件信息未就绪', icon: 'none' })
return
}
const { firmwareRevision, modelNumber } = deviceInfo
const [mcuVer, bleVer, wifiVer, resVer] = (firmwareRevision || '').split('.')
console.log('[OTA] 设备版本', { mcuVer, bleVer, wifiVer, resVer })
this.setData({ loadingShow: true, loadingTitle: '正在检测新版本' })
getFirmwareVersion(modelNumber)
.then((res) => {
const { mcuVersion, bleVersion, wifiVersion, resVersion } = res.result
console.log('[OTA] 服务端版本', { mcuVersion, bleVersion, wifiVersion, resVersion })
const needUpdate = isServerNewer(mcuVer, mcuVersion)
|| isServerNewer(bleVer, bleVersion)
|| isServerNewer(wifiVer, wifiVersion)
|| isServerNewer(resVer, resVersion)
if (!needUpdate) {
this.setData({ loadingShow: false })
wx.showToast({ title: '已是最新版本', icon: 'none' })
return
}
this.setData({ loadingTitle: '正在升级' })
leFuService.otaUpdate()
.then(() => {
// 升级后设备重启,清空连接信息
leFuService.disconnect()
this.setData({ loadingShow: false })
wx.showToast({ title: '升级指令已下发', icon: 'success' })
})
.catch(() => {
this.setData({ loadingShow: false })
wx.showToast({ title: '升级失败', icon: 'none' })
})
})
.catch(() => {
this.setData({ loadingShow: false })
})
},
/** 初始化设备信息 */
onTapInit() {
if (!leFuService.isConnected) {
wx.showToast({ title: '请先连接设备', icon: 'none' })
return
}
wx.showModal({
title: '初始化设备',
content: '是否确认初始化设备信息?',
cancelText: '取消',
confirmText: '确定',
confirmColor: '#F24439',
success: (res) => {
if (!res.confirm) return
this.setData({ loadingShow: true, loadingTitle: '正在初始化设备' })
leFuService.initDevice()
.then(() => {
// 初始化后设备归零,清空连接信息
leFuService.disconnect()
this.setData({ loadingShow: false })
wx.showToast({ title: '初始化完成', icon: 'success' })
})
.catch((err: Error) => {
this.setData({ loadingShow: false })
wx.showToast({ title: err?.message || '初始化失败', icon: 'none' })
})
},
})
},
/** 处理历史数据并上传到后台 */
_uploadHistory(mainUserId: string, dataList: any[], done?: () => void) {
if (!dataList || !dataList.length) {
@@ -294,9 +436,7 @@ Page({
const mac = formatMac(sn)
const bat = deviceInfo?.devicePower != null ? String(deviceInfo.devicePower / 100) : ''
let remain = dataList.length
dataList.forEach((data) => {
const payload: Record<string, any> = {
const list = dataList.map((data) => ({
sn,
type,
mac,
@@ -306,7 +446,7 @@ Page({
weight: (data.weight ?? 0) / 100,
heartRate: data.heartRate ?? 0,
impedance: data.impedance ?? 0,
memberId: data.memberId ?? '',
// memberId: data.memberId ?? '',
isEnd: data.isEnd ?? false,
z20KhzLeftArmEnCode: data.z20KhzLeftArmEnCode ?? 0,
z20KhzRightArmEnCode: data.z20KhzRightArmEnCode ?? 0,
@@ -318,18 +458,16 @@ Page({
z100KhzLeftLegEnCode: data.z100KhzLeftLegEnCode ?? 0,
z100KhzRightLegEnCode: data.z100KhzRightLegEnCode ?? 0,
z100KhzTrunkEnCode: data.z100KhzTrunkEnCode ?? 0,
}
uploadHistoryData(payload)
}))
uploadHistoryData({ list })
.then(() => {
console.log('[数据补传] 上传成功:', payload)
console.log('[数据补传] 上传成功')
done?.()
})
.catch(() => {
console.error('[数据补传] 上传失败:', payload)
})
.finally(() => {
remain--
if (remain === 0) done?.()
})
console.error('[数据补传] 上传失败')
done?.()
})
},
@@ -363,6 +501,10 @@ Page({
if (!res.confirm) return
unbindDevice()
.then(() => {
// 取消全部配对:断开当前连接并清缓存
if (leFuService.isConnected) {
leFuService.disconnect()
}
wx.removeStorageSync('connectDeviceInfo')
wx.showToast({ title: '已取消全部配对', icon: 'success' })
this.loadDevices()
+33 -8
View File
@@ -20,9 +20,19 @@
</view>
<view class="section-title">已配对设备</view>
</view>
<view class="section-right">
<block wx:if="{{ operationsEngineer }}">
<view class="ops-btn ops-btn--red" bind:tap="onUnpairAll">取消全部配对</view>
<view class="section-right-red" bind:tap="onUnpairAll">取消全部配对</view>
</block>
<block wx:else>
<view class="section-right-blue" bind:tap="onTapAdd">
<view>
<image src="/images/equipment/add.png"/>
</view>
<view>添加设备</view>
</view>
</block>
</view>
</view>
<!-- 设备列表 -->
@@ -42,7 +52,7 @@
</view>
<view class="device-info">
<view class="device-info_">
<view class="device-name">{{ item.name }}</view>
<view class="device-name">蓝牙体脂秤</view>
<view class="device-unpair" bind:tap="onTapUnpair" data-item="{{ item }}">取消配对</view>
</view>
<view class="device-status" bind:tap="onTapStatus" data-item="{{ item }}">
@@ -56,11 +66,11 @@
<view class="device-meta">
<view class="meta-row">
<view class="meta-label">设备ID</view>
<view class="meta-value">{{ item.deviceId }}</view>
<view class="meta-label">型号</view>
<view class="meta-value">{{ item.name }}</view>
</view>
<view class="meta-row">
<view class="meta-label">设备SN</view>
<view class="meta-label">设备编码</view>
<view class="meta-value">{{ item.sn }}</view>
</view>
</view>
@@ -82,11 +92,11 @@
<view>设备连接</view>
</view>
</block>
<view class="device-action-btn" bind:tap="onTapUser" data-item="{{ item }}">
<view class="device-action-btn user" bind:tap="onTapUser" data-item="{{ item }}">
<view>
<image src="/images/equipment/users.png"/>
</view>
<view>用户({{ item.userNum }})</view>
<view>用户管理({{ item.userNum }})</view>
</view>
<block wx:if="{{item.status}}">
<view class="device-action-btn" bind:tap="onTapDataRetransfer">
@@ -97,20 +107,35 @@
</view>
</block>
</view>
<block wx:if="{{item.status}}">
<view class="device-update">
<view bind:tap="onTapOta">
<view></view>
<view>OTA升级</view>
</view>
<view bind:tap="onTapInit">
<view></view>
<view>初始化</view>
</view>
</view>
</block>
</view>
</block>
</block>
<block wx:else>
<empty-data containerStyle="margin: 100rpx 0 200rpx;" />
<empty-data text="暂未绑定可用设备,请绑定.." containerStyle="margin: 100rpx 0 200rpx;" />
</block>
<!-- 添加设备按钮 -->
<block wx:if="{{ operationsEngineer }}">
<view class="add-btn" bind:tap="onTapAdd">
<view class="add-icon">
<image src="/images/addBtn.png"/>
</view>
<view class="add-text">添加设备</view>
</view>
</block>
<equipment-dialog show="{{ loadingShow }}" title="{{ loadingTitle }}" content="{{ loadingContent }}"
bind:close="onCloseLoading"/>
@@ -13,6 +13,7 @@
</view>
<view class="list">
<block wx:if="{{ questions && questions.length }}">
<scroll-view class="scrollView" scroll-y>
<block wx:for="{{ questions }}" wx:key="id">
<view class="question" bind:tap="onTapQuestion" data-id="{{ item.id }}">
@@ -22,5 +23,9 @@
</view>
</block>
</scroll-view>
</block>
<block wx:else>
<empty-data containerStyle="margin: 100rpx 0 200rpx;" />
</block>
</view>
</view>
+1
View File
@@ -32,6 +32,7 @@ Page({
wx.reLaunch({ url: '/pages/supplementPersonal/supplementPersonal' })
}
}).catch((err) => {
console.error('[login] 登录失败:', err)
wx.showToast({ title: err.msg || '登录失败,请重试', icon: 'none' })
}).finally(() => {
this.setData({ loading: false })
+1
View File
@@ -113,6 +113,7 @@
width: 100%;
height: 110rpx;
font-size: 32rpx;
font-weight: bolder;
text-align: center;
line-height: 110rpx;
border-radius: 24rpx;
@@ -18,6 +18,8 @@ Page({
readonly: false,
/** 是否展示「复用信息」确认弹框 */
showConfirmModal: false,
/** 查询接口是否报错(报错时提交前需重新查询) */
queryError: false,
},
/** 姓名/身份证变动后,查询回填的信息失效,统一清空 */
@@ -31,6 +33,7 @@ Page({
height: '',
weight: '',
readonly: false,
queryError: false,
})
},
@@ -66,9 +69,9 @@ Page({
const serverResult = res.result
// 优先按服务器返回的身份证解析,未查到则回退用户输入
const { gender, age, sex, birthday } = parseIdCard(serverResult?.idCard || this.data.idCard)
this.setData({ gender, age, sex, birthday })
this.setData({ gender, age, sex, birthday, queryError: false })
if (!serverResult) {
wx.showToast({ title: res.msg || '未查询到用户信息', icon: 'none' })
// wx.showToast({ title: res.msg || '未查询到用户信息', icon: 'none' })
return
}
// flag=101:身份证已存在,弹框确认是否复用
@@ -93,7 +96,11 @@ Page({
})
return
}
}).catch(() => {})
}).catch(() => {
// 查询报错:仍按身份证解析性别/年龄,标记需重新查询
const { gender, age, sex, birthday } = parseIdCard(this.data.idCard)
this.setData({ gender, age, sex, birthday, queryError: true })
})
},
// 复用已有信息:关闭弹框直接提交
@@ -116,8 +123,30 @@ Page({
this.setData({ weight: e.detail.value })
},
onSubmit() {
const { name, idCard, gender, sex, birthday, height, weight, serverResult } = this.data
/** 身高失焦校验:不在 50-250 则清空 */
onHeightBlur() {
const height = this.data.height.trim()
if (!height) return
const h = parseFloat(height)
if (isNaN(h) || h < 50 || h > 250) {
wx.showToast({ title: '身高需在50-250cm之间', icon: 'none' })
this.setData({ height: '' })
}
},
/** 体重失焦校验:不在 20-300 则清空 */
onWeightBlur() {
const weight = this.data.weight.trim()
if (!weight) return
const w = parseFloat(weight)
if (isNaN(w) || w < 20 || w > 300) {
wx.showToast({ title: '体重需在20-300kg之间', icon: 'none' })
this.setData({ weight: '' })
}
},
async onSubmit() {
const { name, idCard, height, weight } = this.data
const heightNum = parseFloat(height)
const weightNum = parseFloat(weight)
@@ -125,21 +154,40 @@ Page({
wx.showToast({ title: '请填写姓名', icon: 'none' })
return
}
if (!gender) {
if (!this.data.gender || !this.data.age || !this.data.birthday) {
wx.showToast({ title: '请填写正确的身份证号', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
if (!height || isNaN(heightNum) || heightNum < 50 || heightNum > 250) {
wx.showToast({ title: '身高需在50-250cm之间', icon: 'none' })
return
}
if (!serverResult) {
wx.showToast({ title: '请先验证姓名与身份证号', icon: 'none' })
if (weight && (isNaN(weightNum) || weightNum < 20 || weightNum > 300)) {
wx.showToast({ title: '体重需在20-300kg之间', icon: 'none' })
return
}
let sex = this.data.sex
let birthday = this.data.birthday
// 查询接口报错 → 重新查询,成功后再提交
if (this.data.queryError) {
try {
const res = await getUserInfoByIdCard({ idCard, realname: name })
const result = res.result
const parsed = parseIdCard(result?.idCard || idCard)
this.setData({ serverResult: result, ...parsed, queryError: false })
sex = parsed.sex
birthday = parsed.birthday
} catch (err: any) {
console.error('[supplementPersonal] 重新查询失败:', err)
wx.showToast({ title: err?.msg || err?.message || '查询失败,请重试', icon: 'none' })
return
}
}
updateUser({
...serverResult,
...(this.data.serverResult || {}),
idCard,
realname: name,
height: heightNum,
@@ -149,8 +197,9 @@ Page({
}).then((res) => {
wx.setStorageSync('userInfo', res.result)
wx.switchTab({ url: '/pages/home/home' })
}).catch(() => {
wx.showToast({ title: '保存失败,请重试', icon: 'none' })
}).catch((err: any) => {
console.error('[supplementPersonal] 保存失败:', err)
wx.showToast({ title: err?.msg || err?.message || '保存失败,请重试', icon: 'none' })
})
},
})
@@ -80,7 +80,8 @@
placeholder-class="form-placeholder"
value="{{ height }}"
disabled="{{ readonly }}"
bind:input="onHeightInput" />
bind:input="onHeightInput"
bind:blur="onHeightBlur" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
@@ -99,7 +100,8 @@
placeholder-class="form-placeholder"
value="{{ weight }}"
disabled="{{ readonly }}"
bind:input="onWeightInput" />
bind:input="onWeightInput"
bind:blur="onWeightBlur" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
@@ -107,7 +109,7 @@
</view>
</view>
<view class="submit-btn" bind:tap="onSubmit">完成并进入首页</view>
<view class="submit-btn" bind:tap="onSubmit">开始使用</view>
<user-info-confirm-modal show="{{ showConfirmModal }}" userInfo="{{ serverResult }}" bind:confirm="onConfirmModalConfirm" bind:cancel="onConfirmModalCancel" />
</view>
@@ -1,6 +1,7 @@
{
"usingComponents": {
"user-management-sync": "/components/userManagementSync/userManagementSync"
"user-management-sync": "/components/userManagementSync/userManagementSync",
"user-management-add-user": "/components/userManagementAddUser/userManagementAddUser"
},
"navigationBarTitleText": "用户管理",
"navigationBarTextStyle": "black",
@@ -120,14 +120,29 @@
line-height: 32rpx;
}
.sync-btn {
color: #FFFFFF;
.section-addUser {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 48rpx;
font-size: 24rpx;
line-height: 48rpx;
padding: 0 20rpx;
border-radius: 8rpx;
background-color: #1385FA;
border-radius: 32rpx;
background-color: #D9E9FA;
> view:nth-of-type(1) {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
margin-right: 6rpx;
}
> view:nth-of-type(2) {
color: #1385FA;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
@@ -187,6 +202,10 @@
box-sizing: border-box;
margin-bottom: 24rpx;
&.member-card--deleted {
opacity: 0.5;
}
.member-top {
width: 100%;
display: flex;
@@ -291,6 +310,14 @@
background-color: #F2994A;
}
}
&.member-sync--deleted {
color: #F24439;
> view:nth-of-type(1) {
background-color: #F24439;
}
}
}
}
@@ -330,36 +357,36 @@
height: 120rpx;
padding: 0 30rpx;
display: flex;
gap: 15rpx;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
background-color: #FFFFFF;
box-shadow: 0 2rpx 8rpx 0 rgba(19, 133, 250, 0.06);
.btn-cancel {
color: #1385FA;
.sync-btn {
flex: 1;
height: 88rpx;
font-size: 32rpx;
text-align: center;
line-height: 84rpx;
border-radius: 44rpx;
box-sizing: border-box;
border: 2rpx solid #1385FA;
background-color: #FFFFFF;
}
.btn-primary {
color: #FFFFFF;
flex: 1;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 88rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
border-radius: 44rpx;
background-color: #1385FA;
> view:nth-of-type(1) {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
margin-right: 6rpx;
}
> view:nth-of-type(2) {
color: #FFFFFF;
height: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
font-weight: bold;
}
}
}
}
@@ -1,4 +1,4 @@
import { get, post, del } from '../../utils/request/index'
import { get, post, del, put } from '../../utils/request/index'
import { leFuService } from '../../lefu/index'
import type { SyncUserData } from '../../lefu/index'
import { calcAge } from '../../utils/idCard/index'
@@ -19,6 +19,7 @@ interface SysUserDevice {
dataType: number
relationType: number | null
syncStatus?: number
delFlag?: number
}
/** 同步用的用户项 */
@@ -30,6 +31,7 @@ interface SyncUser {
age: number
height: number
sync: string
delFlag?: number
}
/** 展示用成员项 */
@@ -44,6 +46,9 @@ interface MemberItem {
syncClass: string
/** 1=本人 2=自己添加 3=他人添加 */
relationType: number | null
/** 0=员工 1=家庭用户 */
dataType?: number
delFlag?: number
}
/** 展示用分组 */
@@ -67,7 +72,11 @@ const markSynced = (data: { sn: string; list: string[] }) =>
/** 删除设备成员 */
const deleteMember = (id: string) =>
del('weighingScale/v2/del/user', { id }, { loading: false })
del('weighingScale/v6/del/user', { id }, { loading: false })
/** 添加设备成员(本人) */
const addSelfUser = (data: Record<string, any>) =>
put('weighingScale/v6/edit/user', data)
/** equipment 页传入的目标设备 */
let targetDevice: { sn: string; deviceId: string; name: string; connected: boolean; connectDeviceInfo: string } | null = null
@@ -86,7 +95,9 @@ Page({
groups: [] as GroupItem[],
syncShow: false,
syncPercent: 0,
syncStatus: ''
syncStatus: '',
addUserShow: false,
isOps: false
},
onLoad(options: Record<string, string>) {
@@ -102,6 +113,7 @@ Page({
'device.deviceId': targetDevice.deviceId,
'device.sn': targetDevice.sn,
'device.status': targetDevice.connected,
isOps: getApp<IAppOption>().globalData.operationsEngineer ?? false,
})
this._subscribeDeviceState()
this._loadMembers()
@@ -144,9 +156,11 @@ Page({
gender: item.sex === 1 ? '男' : '女',
age: calcAge(item.birthday),
height: item.height,
sync: item.syncStatus === 1 ? '已同步' : '同步',
syncClass: item.syncStatus === 1 ? 'done' : 'pending',
sync: item.delFlag === 1 ? '已删除未同步' : (item.syncStatus === 1 ? '已同步' : '同步'),
syncClass: item.delFlag === 1 ? 'deleted' : (item.syncStatus === 1 ? 'done' : 'pending'),
relationType: item.relationType,
dataType: item.dataType,
delFlag: item.delFlag,
})
const self = list.find(m => m.relationType === 1)
this.setData({
@@ -159,8 +173,9 @@ Page({
hasData: list.length > 0,
})
})
.catch(() => {
wx.showToast({ title: '加载用户列表失败', icon: 'none' })
.catch((err: any) => {
console.error('[userManagement] 加载用户列表失败:', err)
wx.showToast({ title: err?.msg || err?.message || '加载用户列表失败', icon: 'none' })
})
},
@@ -234,44 +249,35 @@ Page({
...(self ? [{
id: self.id, userId: self.userId, name: self.name,
gender: self.gender === '男' ? 1 : 0,
age: self.age, height: self.height, sync: self.sync,
age: self.age, height: self.height, sync: self.sync, delFlag: self.delFlag,
}] : []),
...this.data.groups.flatMap(g => g.members.map(m => ({
id: String(m.id), userId: m.userId, name: m.name,
gender: m.gender === '男' ? 1 : 0, age: m.age, height: m.height, sync: m.sync,
gender: m.gender === '男' ? 1 : 0, age: m.age, height: m.height, sync: m.sync, delFlag: m.delFlag,
}))),
]
},
/** 同步完成后的统一处理:标记同步 + 更新列表状态 + 关闭弹框 */
_handleSyncSuccess(users: SyncUser[], successText: string) {
/** 同步完成后的统一处理:上报后刷新列表 + 关闭弹框 */
_finishSync(reportUsers: SyncUser[] | null, successText: string) {
this.setData({ syncPercent: 100, syncStatus: '同步完成' })
wx.showToast({ title: successText, icon: 'success' })
const syncedIds = users.map(u => u.userId).filter(Boolean)
if (targetDevice?.sn && syncedIds.length) {
// wx.showToast({ title: successText, icon: 'success' })
if (reportUsers && reportUsers.length && targetDevice?.sn) {
const syncedIds = reportUsers.map(u => u.userId).filter(Boolean)
if (syncedIds.length) {
markSynced({ sn: targetDevice.sn, list: syncedIds })
.then(() => {
const syncedSet = new Set(syncedIds)
this.setData({
self: this.data.self && syncedSet.has(this.data.self.userId)
? { ...this.data.self, sync: '已同步', syncClass: 'done' }
: this.data.self,
groups: this.data.groups.map(g => ({
...g,
members: g.members.map(m => syncedSet.has(m.userId) ? { ...m, sync: '已同步', syncClass: 'done' } : m),
})),
})
})
.then(() => this._loadMembers())
.catch(() => {})
}
}
if (syncCloseTimer !== null) { clearTimeout(syncCloseTimer); syncCloseTimer = null }
syncCloseTimer = setTimeout(() => {
syncCloseTimer = null
this.setData({ syncShow: false })
}, 1000)
}, 1500)
},
/** 同步用户至设备:只下发「待同步」的用户 */
/** 同步用户至设备:有删除标记则清空重下发,否则只下发「待同步」 */
onTapSync() {
if (!this._ensureConnected()) return
@@ -280,18 +286,25 @@ Page({
wx.showToast({ title: '暂无用户可同步', icon: 'none' })
return
}
// 只下发「待同步」的用户
// 有删除标记(delFlag=1):清空设备后下发未删除的用户,上报所有用户
const hasDeleted = allUsers.some(u => u.delFlag === 1)
if (hasDeleted) {
const normalUsers = allUsers.filter(u => u.delFlag !== 1)
this._clearAndSync(normalUsers, allUsers)
return
}
// 无删除标记:只下发「待同步」的用户
const pendingUsers = allUsers.filter(u => u.sync !== '已同步')
if (!pendingUsers.length) {
wx.showToast({ title: '没有需要同步的用户', icon: 'none' })
return
}
// 先读设备端主用户列表,判断设备是否已有主用户
leFuService.fetchDeviceUserIds()
.then((deviceUserIds) => this._syncUsers(pendingUsers, deviceUserIds))
.catch(() => {
wx.showToast({ title: '读取设备用户失败', icon: 'none' })
.catch((err: any) => {
console.error('[userManagement] 读取设备用户失败:', err)
wx.showToast({ title: err?.message || '读取设备用户失败', icon: 'none' })
})
},
@@ -301,12 +314,12 @@ Page({
const userIds = Array.isArray(deviceUserIds) ? deviceUserIds : [deviceUserIds].filter(Boolean)
const hasMainUser = userIds.length > 0
// 主用户 ID:已有主用户用设备端的,否则用第一个用户作为主用户
const mainUserId = hasMainUser ? String(userIds[0]) : pendingUsers[0].id
const mainUserId = hasMainUser ? String(userIds[0]) : pendingUsers[0].userId
const syncDataList: SyncUserData[] = pendingUsers.map((user, index) => ({
userID: mainUserId,
userName: user.name,
memberID: hasMainUser ? user.id : (user.id === pendingUsers[0].id ? '' : user.id),
memberID: hasMainUser ? user.userId : (user.userId === pendingUsers[0].userId ? '' : user.userId),
age: user.age,
gender: user.gender,
height: user.height,
@@ -325,31 +338,46 @@ Page({
syncStatus: `正在同步 ${current}/${total}...`,
})
})
.then(() => this._handleSyncSuccess(pendingUsers, '用户同步成功'))
.then(() => this._finishSync(pendingUsers, '用户同步成功'))
.catch((err: Error) => {
this.setData({ syncShow: false })
wx.showToast({ title: err.message || '同步失败,请重试', icon: 'none' })
})
},
/** 长按标题:清除设备用户信息并全部重新下发 */
/** 长按标题:弹窗确认后重置设备用户 */
onLongPressSectionTitle() {
wx.showModal({
title: '重置设备用户',
content: '清除设备用户,重新下发',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (!res.confirm) return
this._doReset()
},
})
},
/** 清空设备用户,重新下发全部正常用户,并按同数量上报用户 id */
_doReset() {
if (!this._ensureConnected()) return
const allUsers = this._buildAllUsers()
if (!allUsers.length) {
const normalUsers = allUsers.filter(u => u.delFlag !== 1)
if (!normalUsers.length) {
wx.showToast({ title: '暂无用户可下发', icon: 'none' })
return
}
this._resendAllUsers(allUsers)
this._clearAndSync(normalUsers, normalUsers)
},
/** 清除设备用户后全部重新下发 */
_resendAllUsers(allUsers: SyncUser[]) {
const mainUser = allUsers[0]
const syncDataList: SyncUserData[] = allUsers.map((user, index) => ({
userID: mainUser.id,
/** 清除设备用户后重新下发reportUsers 为 null 时不上报后台 */
_clearAndSync(syncUsers: SyncUser[], reportUsers: SyncUser[] | null) {
const mainUser = syncUsers[0]
const syncDataList: SyncUserData[] = syncUsers.map((user, index) => ({
userID: mainUser.userId,
userName: user.name,
memberID: user.id === mainUser.id ? '' : user.id,
memberID: user.userId === mainUser.userId ? '' : user.userId,
age: user.age,
gender: user.gender,
height: user.height,
@@ -363,28 +391,56 @@ Page({
this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在清空并重新下发...' })
leFuService.clearDeviceMembers()
.then(() => leFuService.syncMembersToDevice(syncDataList, (current, total) => {
.then(() => {
if (!syncDataList.length) return
return leFuService.syncMembersToDevice(syncDataList, (current, total) => {
this.setData({
syncPercent: Math.round((current / total) * 100),
syncStatus: `正在同步 ${current}/${total}...`,
})
}))
.then(() => this._handleSyncSuccess(allUsers, '重新下发成功'))
})
})
.then(() => this._finishSync(reportUsers, '重新下发成功'))
.catch((err: Error) => {
this.setData({ syncShow: false })
wx.showToast({ title: err.message || '重新下发失败', icon: 'none' })
})
},
/** 添加用户 */
/** 添加用户:运维模式或已有本人直接跳添加页,否则弹出选择弹框 */
onTapAddUser() {
const sn = targetDevice?.sn || ''
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}` })
if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return
}
if (this.data.isOps || this.data.self) {
this.onTapAddNew()
} else {
this.setData({ addUserShow: true })
}
},
/** 关闭添加用户弹框 */
onCloseAddUser() {
this.setData({ addUserShow: false })
},
/** 弹框:一键添加本人 */
onAddSelfTap() {
this.setData({ addUserShow: false })
this.onTapAddSelf()
},
/** 弹框:添加其他用户 */
onAddOtherTap() {
this.setData({ addUserShow: false })
this.onTapAddNew()
},
/** 编辑成员:跳转 addUser 编辑模式 */
onTapEdit(e: WechatMiniprogram.TouchEvent) {
const item = e.currentTarget.dataset.item as MemberItem
if (item.delFlag === 1) return
const sn = targetDevice?.sn || ''
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}&id=${encodeURIComponent(item.id)}` })
},
@@ -392,9 +448,13 @@ Page({
/** 删除成员 */
onTapDelete(e: WechatMiniprogram.TouchEvent) {
const item = e.currentTarget.dataset.item as MemberItem
if (item.delFlag === 1) return
const content = item.relationType === 3
? `${item.name}」成员为他人添加,请确认是否删除?`
: `是否确认删除「${item.name}」?`
wx.showModal({
title: '删除用户',
content: `是否确认删除「${item.name}」?`,
content,
cancelText: '取消',
confirmText: '删除',
confirmColor: '#F24439',
@@ -405,20 +465,60 @@ Page({
wx.showToast({ title: '删除成功', icon: 'success' })
this._loadMembers()
})
.catch(() => {
wx.showToast({ title: '删除失败,请重试', icon: 'none' })
.catch((err: any) => {
console.error('[userManagement] 删除成员失败:', err)
wx.showToast({ title: err?.msg || err?.message || '删除失败,请重试', icon: 'none' })
})
},
})
},
/** 一键添加本人 */
/** 一键添加本人:仅调接口把当前登录用户添加为设备成员,不下发设备 */
onTapAddSelf() {
// 一键添加本人(待接入)
if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return
}
const userInfo = wx.getStorageSync('userInfo') as Record<string, any> | null
if (!userInfo?.userId) {
wx.showToast({ title: '请先完善个人信息', icon: 'none' })
return
}
const sn = targetDevice?.sn
if (!sn) return
addSelfUser({
relationType: 1,
dataType: 0,
id: userInfo.id,
userId: userInfo.userId,
sn,
scaleDeviceId: sn,
realname: userInfo.realname ?? '',
idCard: userInfo.idCard || null,
sex: userInfo.sex ?? 0,
birthday: userInfo.birthday ?? '',
height: userInfo.height ?? 0,
weight: userInfo.weight ?? 0,
mode: getApp<IAppOption>().globalData.operationsEngineer ? 'ops' : 'normal',
})
.then(() => {
wx.showToast({ title: '添加成功', icon: 'success' })
this._loadMembers()
})
.catch((err: any) => {
console.error('[userManagement] 添加本人失败:', err)
wx.showToast({ title: err?.msg || err?.message || '添加失败,请重试', icon: 'none' })
})
},
/** 添加新用户 */
onTapAddNew() {
wx.navigateTo({ url: '/pages/addUser/addUser' })
if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return
}
const sn = targetDevice?.sn || ''
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}` })
}
})
@@ -8,7 +8,7 @@
</view>
<view class="device-info">
<view class="device-name-row">
<view class="device-name">{{ device.name }}</view>
<view class="device-name">蓝牙体脂秤</view>
<view class="device-status">
<view class="device-status_ {{ device.status ? 'connected' : '' }}">
<view class="status-dot"></view>
@@ -16,15 +16,20 @@
</view>
</view>
</view>
<view class="device-meta" style="margin-bottom: 20rpx">设备ID{{ device.deviceId }}</view>
<view class="device-meta">设备SN{{ device.sn }}</view>
<view class="device-meta" style="margin-bottom: 20rpx">型号{{ device.name }}</view>
<view class="device-meta">设备编号{{ device.sn }}</view>
</view>
</view>
<!-- 已录入用户 + 同步按钮 -->
<view class="section-header">
<view class="section-title" bind:longpress="onLongPressSectionTitle">已录入用户({{ memberCount }}/10</view>
<view class="sync-btn" bind:tap="onTapSync">同步用户至设备</view>
<view class="section-addUser" bind:tap="onTapAddUser">
<view>
<image src="/images/userManagement/add.png" mode="aspectFit" />
</view>
<view>添加用户</view>
</view>
</view>
<!-- 同步提示 -->
@@ -37,7 +42,7 @@
<view class="scrollViewContent">
<!-- 本人 -->
<block wx:if="{{ self }}">
<view class="member-card">
<view class="member-card {{ self.delFlag === 1 ? 'member-card--deleted' : '' }}">
<view class="member-top">
<view class="member-avatar">
<image src="{{ self.gender === '女' ? '/images/woman.png' : '/images/man.png' }}" mode="aspectFit" />
@@ -54,10 +59,12 @@
<view>{{ self.sync }}</view>
</view>
</view>
<!-- <block wx:if="{{ self.delFlag !== 1 }}">
<view class="member-divider"></view>
<view class="member-ops">
<view class="op-delete" bind:tap="onTapDelete" data-item="{{ self }}">删除</view>
</view>
</block> -->
</view>
</block>
@@ -65,7 +72,7 @@
<block wx:for="{{ groups }}" wx:key="title" wx:for-item="group">
<view class="group-title">{{ group.title }}</view>
<block wx:for="{{ group.members }}" wx:key="id" wx:for-item="member">
<view class="member-card">
<view class="member-card {{ member.delFlag === 1 ? 'member-card--deleted' : '' }}">
<view class="member-top">
<view class="member-avatar {{ member.gender === '女' ? 'member-avatar--female' : 'member-avatar--male' }}"><image src="{{ member.gender === '女' ? '/images/woman.png' : '/images/man.png' }}" mode="aspectFit" /></view>
<view class="member-info">
@@ -79,11 +86,15 @@
<view>{{ member.sync }}</view>
</view>
</view>
<block wx:if="{{ member.delFlag !== 1 }}">
<view class="member-divider"></view>
<view class="member-ops">
<block wx:if="{{ member.dataType !== 0 }}">
<view class="op-edit" bind:tap="onTapEdit" data-item="{{ member }}">编辑</view>
</block>
<view class="op-delete" bind:tap="onTapDelete" data-item="{{ member }}">删除</view>
</view>
</block>
</view>
</block>
</block>
@@ -98,15 +109,17 @@
<!-- 底部常驻添加用户按钮 -->
<view class="footer">
<block wx:if="{{ self }}">
<view class="btn-primary" bind:tap="onTapAddUser">添加用户(最多10位)</view>
</block>
<block wx:else>
<view class="btn-primary" bind:tap="onTapAddSelf">一键添加本人</view>
<view class="btn-cancel" bind:tap="onTapAddNew">添加新用户</view>
</block>
<view class="sync-btn" bind:tap="onTapSync">
<view>
<image src="/images/userManagement/reload.png" mode="aspectFit" />
</view>
<view>同步用户至设备</view>
</view>
</view>
<!-- 同步进度弹框 -->
<user-management-sync show="{{ syncShow }}" percent="{{ syncPercent }}" status="{{ syncStatus }}" />
<!-- 添加用户弹框 -->
<user-management-add-user show="{{ addUserShow }}" bind:addself="onAddSelfTap" bind:addother="onAddOtherTap" bind:close="onCloseAddUser" />
</view>
+12
View File
@@ -0,0 +1,12 @@
import type { EnvType } from './types'
export type { EnvType }
/** 获取当前运行环境,基础库不支持 getAccountInfoSync 时回退 develop */
export const ENV: EnvType = (() => {
try {
return wx.getAccountInfoSync().miniProgram.envVersion as EnvType
} catch {
return 'develop'
}
})()
+2
View File
@@ -0,0 +1,2 @@
/** 小程序运行环境:develop 开发版 / trial 体验版 / release 正式版 */
export type EnvType = 'develop' | 'trial' | 'release'
+9 -5
View File
@@ -1,8 +1,12 @@
// TODO: 替换为实际接口地址
const IS_DEV = true
import { ENV, type EnvType } from '../env/index'
export const BASE_URL = IS_DEV
? 'http://10.10.10.10:8889/'
: ''
/** 各环境接口网关地址:develop / trial 走测试网关,release 走正式网关 */
const BASE_URLS: Record<EnvType, string> = {
develop: 'http://10.10.10.10:8889/',
trial: 'https://device.shuziweidao.com/gateway/',
release: 'https://device.shuziweidao.com/gateway/',
}
export const BASE_URL = BASE_URLS[ENV]
export const TIMEOUT = 30000