17 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
25 changed files with 543 additions and 121 deletions
@@ -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>
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

+2 -2
View File
@@ -8,8 +8,8 @@ const DOMAINS: Record<EnvType, { domain1: string; domain2: string }> = {
domain2: 'http://10.10.10.10:8889/weight', domain2: 'http://10.10.10.10:8889/weight',
}, },
trial: { trial: {
domain1: 'http://10.10.10.10:8889', domain1: 'http://device.shuziweidao.com:80/gateway',
domain2: 'http://10.10.10.10:8889/weight', domain2: 'http://device.shuziweidao.com:80/weight',
}, },
release: { release: {
domain1: 'http://device.shuziweidao.com:80/gateway', domain1: 'http://device.shuziweidao.com:80/gateway',
+39 -2
View File
@@ -187,10 +187,17 @@ class LeFuService {
this._doReconnect() this._doReconnect()
return return
} }
// 蓝牙不可用:做完整清理 // 蓝牙不可用:蓝牙真关了才清空协议;扫描/连接期间的 UNAVAILABLE 抖动(蓝牙还开着)保留协议
if (res === plugin.BLUE_STATE.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._stopKeepAlive()
this._stopReconnectTimer() this._stopReconnectTimer()
// 蓝牙还开着:只是抖动,保留协议,避免 isConnected 误判为 false
if (!bluetoothOff) {
return
}
if (this._activeProtocol) { if (this._activeProtocol) {
this._activeProtocol = null this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb()) this._onDisconnectedCbs.forEach(cb => cb())
@@ -220,7 +227,9 @@ class LeFuService {
// 即将断开:清理协议实例,若非主动断开则触发重连 // 即将断开:清理协议实例,若非主动断开则触发重连
plugin.bus.subscribe('deviceWillDisconnect', () => { 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() this._stopKeepAlive()
if (this._activeProtocol) { if (this._activeProtocol) {
this._activeProtocol = null this._activeProtocol = null
@@ -414,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('请求失败'))
})
})
}
// ─── 用户同步 ───────────────────────────── // ─── 用户同步 ─────────────────────────────
/** /**
* 清空设备内的用户数据 * 清空设备内的用户数据
+9 -2
View File
@@ -233,10 +233,16 @@ Page({
if (idCard.length === 18) this.validateIdCard() if (idCard.length === 18) this.validateIdCard()
}, },
/** 身份证合法且姓名已填则查询 */ /** 身份证合法则解析:姓名已填走接口查询,未填先前端解析 */
validateIdCard() { validateIdCard() {
const { idCard, name } = this.data.form 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 复用 */ /** 查询身份证对应的用户信息,101 复用 */
@@ -414,6 +420,7 @@ Page({
const payload: Record<string, any> = { const payload: Record<string, any> = {
...baseSpread, ...baseSpread,
...(editId ? {} : { relationType: 2 }),
dataType: userType === 'family' ? 1 : 0, dataType: userType === 'family' ? 1 : 0,
parentUserId: userInfo?.userId ?? '', parentUserId: userInfo?.userId ?? '',
sn: targetSn, sn: targetSn,
@@ -76,7 +76,7 @@
<view class="card-btn-primary" data-index="{{ index }}" catch:tap="onConnect">连接</view> <view class="card-btn-primary" data-index="{{ index }}" catch:tap="onConnect">连接</view>
</block> </block>
</view> </view>
<!-- <view class="device-card-bottom">deviceId{{ item.deviceId }}</view> --> <view class="device-card-bottom">deviceId{{ item.deviceId }}</view>
</view> </view>
</block> </block>
</view> </view>
@@ -32,7 +32,7 @@
</block> </block>
</view> </view>
<view bind:tap="inputTypeChange"> <view bind:tap="inputTypeChange">
<image src="/images/connectedWifi/{{type ? 'open' : 'close'}}.png"/> <image src="/images/connectedWifi/{{type ? 'close' : 'open'}}.png"/>
</view> </view>
</view> </view>
</view> </view>
+8
View File
@@ -325,6 +325,14 @@
height: 26rpx; height: 26rpx;
line-height: 26rpx; line-height: 26rpx;
} }
&.diff-up {
background-color: #FEF0F0;
> view:nth-of-type(2) {
color: #F24439;
}
}
} }
&.arrow::after { &.arrow::after {
+8
View File
@@ -38,6 +38,10 @@ interface MeasureRecord {
weight: number weight: number
bmi: number bmi: number
fatRate: number fatRate: number
/** 相比上次体重差值绝对值(kg),首次测量为 null */
weightDiff: number | null
/** 变化方向:1增加 -1减少 0持平 null首次 */
weightTrend: number | null
} }
/** GET /weighingScale/v3/getMeasureHistoryPage 返回结构 */ /** GET /weighingScale/v3/getMeasureHistoryPage 返回结构 */
@@ -150,6 +154,8 @@ Page({
weight: r.weight ?? 0, weight: r.weight ?? 0,
bmi: r.bmi ?? 0, bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0, fatRate: r.fatRate ?? 0,
weightDiff: r.weightDiff == null ? null : Math.abs(r.weightDiff),
weightTrend: r.weightTrend ?? null,
})) }))
const total = page.total ?? 0 const total = page.total ?? 0
@@ -191,6 +197,8 @@ Page({
weight: r.weight ?? 0, weight: r.weight ?? 0,
bmi: r.bmi ?? 0, bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0, fatRate: r.fatRate ?? 0,
weightDiff: r.weightDiff == null ? null : Math.abs(r.weightDiff),
weightTrend: r.weightTrend ?? null,
})) }))
const allRecords = [...this.data.records, ...newRecords] const allRecords = [...this.data.records, ...newRecords]
this.setData({ this.setData({
+5 -3
View File
@@ -73,12 +73,14 @@
<view class="list-examTime">{{ item.examTime }}</view> <view class="list-examTime">{{ item.examTime }}</view>
<view class="list-weight arrow"> <view class="list-weight arrow">
<view>{{ item.weight }}<text>kg</text></view> <view>{{ item.weight }}<text>kg</text></view>
<block wx:if="{{ item.weightTrend === 1 || item.weightTrend === -1 }}">
<view class="{{ item.weightTrend === 1 ? 'diff-up' : '' }}">
<view> <view>
<view> <image src="{{ item.weightTrend === 1 ? '/images/data/add.png' : '/images/data/sub.png' }}" mode="aspectFit" />
<image src="/images/data/sub.png" mode="aspectFit" />
</view> </view>
<view>0.3</view> <view>{{ item.weightDiff }}</view>
</view> </view>
</block>
</view> </view>
<view class="list-bmi"> <view class="list-bmi">
<view> <view>
@@ -124,6 +124,15 @@ const BMI_TYPE_COLOR: Record<string, string> = {
: '#F24439', : '#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 -> 指标 的映射 */ /** 将 bodyDataList 转为 key -> 指标 的映射 */
const toMap = (list: LefuBodyDatum[] = []): Record<string, LefuBodyDatum> => { const toMap = (list: LefuBodyDatum[] = []): Record<string, LefuBodyDatum> => {
const map: Record<string, LefuBodyDatum> = {} const map: Record<string, LefuBodyDatum> = {}
@@ -259,21 +268,22 @@ Page({
changeText, changeText,
} }
// 基础指标(2 行 x 3 列,第 6 格留空保持布局) // 基础指标(2 行 x 3 列,第 6 格留空保持布局label 前端写死,缺失值显示 -
const basicKeys = ['ppWeightKg', 'ppBMI', 'ppFat', 'ppBodyScore', 'ppBodyHealth'] const basicCells: BasicCell[] = BASIC_INDICATORS.map(({ key, label }) => {
const basicCells: BasicCell[] = basicKeys.map((k) => { const d = map[key]
const d = map[k] const isScore = key === 'ppBodyScore'
if (!d) return { ...EMPTY_BASIC_CELL } const isHealth = key === 'ppBodyHealth'
const isScore = k === 'ppBodyScore' const isBmi = key === 'ppBMI'
const isHealth = k === 'ppBodyHealth' if (!d) {
const isBmi = k === 'ppBMI' return { label, labelExtra: '', labelExtraColor: '', value: '-', unit: '' }
}
return { return {
label: d.bodyParamName || k, label,
// BMI 名称后跟体型状态(消瘦/正常/超重/肥胖) // BMI 名称后跟体型状态(消瘦/正常/超重/肥胖)
labelExtra: isBmi ? data.bmiType || '' : '', labelExtra: isBmi ? data.bmiType || '' : '',
labelExtraColor: isBmi ? BMI_TYPE_COLOR[data.bmiType || ''] || DEFAULT_STATUS_COLOR : '', labelExtraColor: isBmi ? BMI_TYPE_COLOR[data.bmiType || ''] || DEFAULT_STATUS_COLOR : '',
// 健康评价展示等级文案(如"亚健康"),其余展示测量数值 // 健康评价展示等级文案(如"亚健康"),其余展示测量数值
value: isHealth ? d.standardTitle || d.currentValue || '' : d.currentValue || '', value: isHealth ? d.standardTitle || d.currentValue || '-' : d.currentValue || '-',
// 健康评分补"分" // 健康评分补"分"
unit: isScore ? d.unit || '分' : isHealth ? '' : d.unit || '', unit: isScore ? d.unit || '分' : isHealth ? '' : d.unit || '',
} }
@@ -350,7 +360,8 @@ Page({
['右腿', 'ppBodyFatKgRightLeg', 'ppBodyFatRateRightLeg'], ['右腿', 'ppBodyFatKgRightLeg', 'ppBodyFatRateRightLeg'],
['躯干', 'ppBodyFatKgTrunk', 'ppBodyFatRateTrunk'], ['躯干', 'ppBodyFatKgTrunk', 'ppBodyFatRateTrunk'],
] ]
const bodyFat = isEight const hasBodyFatData = fatPartLabels.some(([, massKey, rateKey]) => map[massKey] || map[rateKey])
const bodyFat = isEight && hasBodyFatData
? { ? {
title: '躯干/四肢脂肪', title: '躯干/四肢脂肪',
rows: chunkParts( rows: chunkParts(
@@ -368,7 +379,8 @@ Page({
['右腿', 'ppMuscleKgRightLeg', 'ppMuscleRateRightLeg'], ['右腿', 'ppMuscleKgRightLeg', 'ppMuscleRateRightLeg'],
['躯干', 'ppMuscleKgTrunk', 'ppMuscleRateTrunk'], ['躯干', 'ppMuscleKgTrunk', 'ppMuscleRateTrunk'],
] ]
const bodyMuscle = isEight const hasBodyMuscleData = musclePartLabels.some(([, massKey, rateKey]) => map[massKey] || map[rateKey])
const bodyMuscle = isEight && hasBodyMuscleData
? { ? {
title: '躯干/四肢肌肉', title: '躯干/四肢肌肉',
rows: chunkParts( rows: chunkParts(
+83 -20
View File
@@ -83,27 +83,47 @@
} }
} }
/* 描边按钮(蓝色/红色) */ .section-right {
.ops-btn {
width: 180rpx;
height: 48rpx; height: 48rpx;
display: flex;
flex-wrap: nowrap;
.section-right-red {
height: 48rpx;
padding: 0 15rpx;
border-radius: 8rpx; border-radius: 8rpx;
font-size: 24rpx; font-size: 24rpx;
line-height: 44rpx; line-height: 44rpx;
text-align: center;
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0;
}
.ops-btn--blue {
border: 2rpx solid #1385FA;
color: #1385FA;
}
.ops-btn--red {
border: 2rpx solid #F24439; border: 2rpx solid #F24439;
color: #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 { .device-actions {
width: 100%; width: 100%;
gap: 12px; gap: 10px;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: space-around; justify-content: space-around;
@@ -251,18 +271,61 @@
border: 2rpx solid #EAECF1; border: 2rpx solid #EAECF1;
> view:nth-of-type(1) { > view:nth-of-type(1) {
width: 26rpx; width: 24rpx;
height: 26rpx; height: 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 10rpx; margin-right: 6rpx;
} }
> view:nth-of-type(2) { > view:nth-of-type(2) {
color: #77849E; color: #77849E;
height: 26rpx; height: 24rpx;
font-size: 26rpx; font-size: 24rpx;
line-height: 26rpx; 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;
} }
} }
} }
+104 -2
View File
@@ -39,6 +39,25 @@ const uploadHistoryData = (data: Record<string, any>) =>
/** SN 转 MAC:每两位加一个冒号(如 CEE9052031AC → CE:E9:05:20:31:AC */ /** SN 转 MAC:每两位加一个冒号(如 CEE9052031AC → CE:E9:05:20:31:AC */
const formatMac = (sn: string) => sn.match(/.{2}/g)?.join(':') ?? '' 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 { interface DeviceItem {
id: string id: string
@@ -131,11 +150,16 @@ Page({
try { try {
info = JSON.parse(item.connectDeviceInfo || '{}') info = JSON.parse(item.connectDeviceInfo || '{}')
} catch {} } 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 { return {
id: item.id, id: item.id,
name: info.name ?? '', name: info.name ?? '',
status: leFuService.isConnected && leFuService.lastRawDevice?.deviceId === info.deviceId, status,
deviceId: info.deviceId ?? '', deviceId: infoDeviceId,
sn: item.scaleDeviceId, sn: item.scaleDeviceId,
userNum: item.userNum ?? 0, userNum: item.userNum ?? 0,
connectDeviceInfo: item.connectDeviceInfo ?? '', connectDeviceInfo: item.connectDeviceInfo ?? '',
@@ -322,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) { _uploadHistory(mainUserId: string, dataList: any[], done?: () => void) {
if (!dataList || !dataList.length) { if (!dataList || !dataList.length) {
+30 -5
View File
@@ -13,7 +13,6 @@
</view> </view>
<!-- 已配对设备行 --> <!-- 已配对设备行 -->
<block wx:if="{{ operationsEngineer }}">
<view class="section-bar"> <view class="section-bar">
<view class="section-left"> <view class="section-left">
<view class="bt-icon"> <view class="bt-icon">
@@ -21,9 +20,20 @@
</view> </view>
<view class="section-title">已配对设备</view> <view class="section-title">已配对设备</view>
</view> </view>
<view class="ops-btn ops-btn--red" bind:tap="onUnpairAll">取消全部配对</view> <view class="section-right">
<block wx:if="{{ operationsEngineer }}">
<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> </view>
</block> </block>
</view>
</view>
<!-- 设备列表 --> <!-- 设备列表 -->
<!-- <view class="device"> <!-- <view class="device">
@@ -82,11 +92,11 @@
<view>设备连接</view> <view>设备连接</view>
</view> </view>
</block> </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> <view>
<image src="/images/equipment/users.png"/> <image src="/images/equipment/users.png"/>
</view> </view>
<view>用户({{ item.userNum }})</view> <view>用户管理({{ item.userNum }})</view>
</view> </view>
<block wx:if="{{item.status}}"> <block wx:if="{{item.status}}">
<view class="device-action-btn" bind:tap="onTapDataRetransfer"> <view class="device-action-btn" bind:tap="onTapDataRetransfer">
@@ -97,6 +107,19 @@
</view> </view>
</block> </block>
</view> </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> </view>
</block> </block>
</block> </block>
@@ -105,12 +128,14 @@
</block> </block>
<!-- 添加设备按钮 --> <!-- 添加设备按钮 -->
<block wx:if="{{ operationsEngineer }}">
<view class="add-btn" bind:tap="onTapAdd"> <view class="add-btn" bind:tap="onTapAdd">
<view class="add-icon"> <view class="add-icon">
<image src="/images/addBtn.png"/> <image src="/images/addBtn.png"/>
</view> </view>
<view class="add-text">绑定设备</view> <view class="add-text">添加设备</view>
</view> </view>
</block>
<equipment-dialog show="{{ loadingShow }}" title="{{ loadingTitle }}" content="{{ loadingContent }}" <equipment-dialog show="{{ loadingShow }}" title="{{ loadingTitle }}" content="{{ loadingContent }}"
bind:close="onCloseLoading"/> bind:close="onCloseLoading"/>
@@ -109,7 +109,7 @@
</view> </view>
</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" /> <user-info-confirm-modal show="{{ showConfirmModal }}" userInfo="{{ serverResult }}" bind:confirm="onConfirmModalConfirm" bind:cancel="onConfirmModalCancel" />
</view> </view>
@@ -1,6 +1,7 @@
{ {
"usingComponents": { "usingComponents": {
"user-management-sync": "/components/userManagementSync/userManagementSync" "user-management-sync": "/components/userManagementSync/userManagementSync",
"user-management-add-user": "/components/userManagementAddUser/userManagementAddUser"
}, },
"navigationBarTitleText": "用户管理", "navigationBarTitleText": "用户管理",
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
@@ -120,14 +120,29 @@
line-height: 32rpx; line-height: 32rpx;
} }
.sync-btn { .section-addUser {
color: #FFFFFF; display: flex;
flex-wrap: nowrap;
align-items: center;
height: 48rpx; height: 48rpx;
font-size: 24rpx;
line-height: 48rpx;
padding: 0 20rpx; padding: 0 20rpx;
border-radius: 8rpx; border-radius: 32rpx;
background-color: #1385FA; 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;
}
} }
} }
@@ -342,36 +357,36 @@
height: 120rpx; height: 120rpx;
padding: 0 30rpx; padding: 0 30rpx;
display: flex; display: flex;
gap: 15rpx;
flex-wrap: nowrap;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
background-color: #FFFFFF; background-color: #FFFFFF;
box-shadow: 0 2rpx 8rpx 0 rgba(19, 133, 250, 0.06); box-shadow: 0 2rpx 8rpx 0 rgba(19, 133, 250, 0.06);
.btn-cancel { .sync-btn {
color: #1385FA;
flex: 1; flex: 1;
height: 88rpx; height: 88rpx;
font-size: 32rpx; display: flex;
text-align: center; flex-wrap: nowrap;
line-height: 84rpx; align-items: center;
border-radius: 44rpx; justify-content: center;
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;
border-radius: 44rpx; border-radius: 44rpx;
background-color: #1385FA; 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;
}
} }
} }
} }
@@ -96,6 +96,7 @@ Page({
syncShow: false, syncShow: false,
syncPercent: 0, syncPercent: 0,
syncStatus: '', syncStatus: '',
addUserShow: false,
isOps: false isOps: false
}, },
@@ -344,15 +345,30 @@ Page({
}) })
}, },
/** 长按标题:清除设备用户信息并全部重新下发(不上报) */ /** 长按标题:弹窗确认后重置设备用户 */
onLongPressSectionTitle() { onLongPressSectionTitle() {
wx.showModal({
title: '重置设备用户',
content: '清除设备用户,重新下发',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (!res.confirm) return
this._doReset()
},
})
},
/** 清空设备用户,重新下发全部正常用户,并按同数量上报用户 id */
_doReset() {
if (!this._ensureConnected()) return if (!this._ensureConnected()) return
const allUsers = this._buildAllUsers() const allUsers = this._buildAllUsers()
if (!allUsers.length) { const normalUsers = allUsers.filter(u => u.delFlag !== 1)
if (!normalUsers.length) {
wx.showToast({ title: '暂无用户可下发', icon: 'none' }) wx.showToast({ title: '暂无用户可下发', icon: 'none' })
return return
} }
this._clearAndSync(allUsers, null) this._clearAndSync(normalUsers, normalUsers)
}, },
/** 清除设备用户后重新下发;reportUsers 为 null 时不上报后台 */ /** 清除设备用户后重新下发;reportUsers 为 null 时不上报后台 */
@@ -375,12 +391,15 @@ Page({
this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在清空并重新下发...' }) this.setData({ syncShow: true, syncPercent: 0, syncStatus: '正在清空并重新下发...' })
leFuService.clearDeviceMembers() leFuService.clearDeviceMembers()
.then(() => leFuService.syncMembersToDevice(syncDataList, (current, total) => { .then(() => {
if (!syncDataList.length) return
return leFuService.syncMembersToDevice(syncDataList, (current, total) => {
this.setData({ this.setData({
syncPercent: Math.round((current / total) * 100), syncPercent: Math.round((current / total) * 100),
syncStatus: `正在同步 ${current}/${total}...`, syncStatus: `正在同步 ${current}/${total}...`,
}) })
})) })
})
.then(() => this._finishSync(reportUsers, '重新下发成功')) .then(() => this._finishSync(reportUsers, '重新下发成功'))
.catch((err: Error) => { .catch((err: Error) => {
this.setData({ syncShow: false }) this.setData({ syncShow: false })
@@ -388,14 +407,34 @@ Page({
}) })
}, },
/** 添加用户 */ /** 添加用户:运维模式或已有本人直接跳添加页,否则弹出选择弹框 */
onTapAddUser() { onTapAddUser() {
if (this.data.memberCount >= 10) { if (this.data.memberCount >= 10) {
wx.showToast({ title: '最多添加10位用户', icon: 'none' }) wx.showToast({ title: '最多添加10位用户', icon: 'none' })
return return
} }
const sn = targetDevice?.sn || '' if (this.data.isOps || this.data.self) {
wx.navigateTo({ url: `/pages/addUser/addUser?sn=${encodeURIComponent(sn)}` }) 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 编辑模式 */ /** 编辑成员:跳转 addUser 编辑模式 */
@@ -449,8 +488,10 @@ Page({
if (!sn) return if (!sn) return
addSelfUser({ addSelfUser({
relationType: 1,
dataType: 0, dataType: 0,
parentUserId: userInfo.userId, id: userInfo.id,
userId: userInfo.userId,
sn, sn,
scaleDeviceId: sn, scaleDeviceId: sn,
realname: userInfo.realname ?? '', realname: userInfo.realname ?? '',
@@ -24,7 +24,12 @@
<!-- 已录入用户 + 同步按钮 --> <!-- 已录入用户 + 同步按钮 -->
<view class="section-header"> <view class="section-header">
<view class="section-title" bind:longpress="onLongPressSectionTitle">已录入用户({{ memberCount }}/10</view> <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> </view>
<!-- 同步提示 --> <!-- 同步提示 -->
@@ -104,17 +109,17 @@
<!-- 底部常驻添加用户按钮 --> <!-- 底部常驻添加用户按钮 -->
<view class="footer"> <view class="footer">
<block wx:if="{{ self }}"> <view class="sync-btn" bind:tap="onTapSync">
<view class="btn-primary" bind:tap="onTapAddUser">添加用户(最多10位)</view> <view>
</block> <image src="/images/userManagement/reload.png" mode="aspectFit" />
<block wx:else> </view>
<block wx:if="{{ !isOps }}"> <view>同步用户至设备</view>
<view class="btn-primary" bind:tap="onTapAddSelf">一键添加本人</view> </view>
</block>
<view class="btn-cancel" bind:tap="onTapAddNew">添加新用户</view>
</block>
</view> </view>
<!-- 同步进度弹框 --> <!-- 同步进度弹框 -->
<user-management-sync show="{{ syncShow }}" percent="{{ syncPercent }}" status="{{ syncStatus }}" /> <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> </view>
+1 -1
View File
@@ -3,7 +3,7 @@ import { ENV, type EnvType } from '../env/index'
/** 各环境接口网关地址:develop / trial 走测试网关,release 走正式网关 */ /** 各环境接口网关地址:develop / trial 走测试网关,release 走正式网关 */
const BASE_URLS: Record<EnvType, string> = { const BASE_URLS: Record<EnvType, string> = {
develop: 'http://10.10.10.10:8889/', develop: 'http://10.10.10.10:8889/',
trial: 'http://10.10.10.10:8889/', trial: 'https://device.shuziweidao.com/gateway/',
release: 'https://device.shuziweidao.com/gateway/', release: 'https://device.shuziweidao.com/gateway/',
} }