refactor: 项目重构,仅保留 login 和 supplementPersonal 页面

删除 11 个旧页面、组件、lefu SDK、旧资源文件。
新增 request 封装(GET/POST/PUT)、api 层、utils/common 身份证工具。
重写 login(登录流程提取到 app.ts) 和 supplementPersonal(简化表单逻辑)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
17792275749
2026-08-10 14:21:00 +08:00
co-authored by Claude Sonnet 4.6
parent 439f560218
commit 83d909b95b
96 changed files with 370 additions and 9040 deletions
@@ -1,7 +0,0 @@
{
"navigationBarTitleText": "添加用户",
"usingComponents": {
"select-member-drawer": "/components/selectMemberDrawer/selectMemberDrawer",
"user-info-confirm-modal": "/components/userInfoConfirmModal/userInfoConfirmModal"
}
}
-402
View File
@@ -1,402 +0,0 @@
.add-member {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
/* 顶部浅蓝提示条 */
.tips {
width: 100%;
height: 56rpx;
display: flex;
margin-bottom: 24rpx;
padding-left: 30rpx;
align-items: center;
box-sizing: border-box;
background-color: rgba(19, 133, 250, 0.2);
.tips-icon {
color: #1385FA;
width: 28rpx;
height: 28rpx;
font-size: 20rpx;
font-weight: bold;
margin-right: 10rpx;
line-height: 24rpx;
text-align: center;
border-radius: 50%;
box-sizing: border-box;
border: 1rpx solid #1385FA;
}
.tips-text {
color: #1385FA;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
.form {
width: 100%;
flex: 1;
overflow: hidden;
.scrollViewContent {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
/* 顶部 tabs - 圆角卡片样式 */
.tabsDom {
width: 100%;
box-sizing: border-box;
padding: 30rpx 30rpx 40rpx;
border-radius: 16rpx 16rpx 0 0;
background-color: #ffffff;
.tabs {
width: 100%;
height: 80rpx;
padding: 4rpx;
display: flex;
flex-wrap: nowrap;
box-sizing: border-box;
border-radius: 16rpx;
background-color: #F3F5F8;
.tab-item {
color: #808080;
flex: 1;
height: 72rpx;
font-size: 32rpx;
text-align: center;
line-height: 72rpx;
box-sizing: border-box;
border-radius: 16rpx;
&.active {
color: #1385FA;
font-weight: bold;
background-color: #FFFFFF;
}
}
}
}
.selectWorkNo {
width: 100%;
margin-bottom: 30rpx;
box-sizing: border-box;
padding: 0 30rpx 40rpx;
border-radius: 0 0 16rpx 16rpx;
background-color: #ffffff;
.selectWorkNoBtn {
color: #FFFFFF;
width: 580rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 88rpx;
border-radius: 44rpx;
text-align: center;
margin: 0 auto;
background-color: #1385FA;
}
}
.workNoDesc {
color: #B6B6B6;
width: 100%;
height: 24rpx;
font-size: 24rpx;
text-align: center;
line-height: 24rpx;
}
.workNoNull {
color: #F24439;
margin: 56rpx auto 0;
width: 640rpx;
height: 88rpx;
font-size: 28rpx;
line-height: 88rpx;
border-radius: 16rpx;
text-align: center;
background: rgba(242,68,57,0.12);
}
.workNoInfo {
width: 100%;
padding-bottom: 10rpx;
.workNoInfo_ {
width: 100%;
margin-bottom: 20rpx;
.workNoInfoTitle {
color: #808080;
width: 100%;
height: 64rpx;
font-size: 24rpx;
line-height: 64rpx;
}
.workNoInfoContent {
width: 100%;
border-radius: 16rpx;
box-sizing: border-box;
padding: 40rpx 30rpx 30rpx;
background-color: #FFFFFF;
> view {
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
.form-row {
width: 300rpx;
}
}
}
}
}
.familyInfo {
width: 100%;
padding-bottom: 10rpx;
.familyInfo_ {
width: 100%;
border-radius: 16rpx;
box-sizing: border-box;
padding: 0 30rpx 10rpx;
margin-bottom: 40rpx;
background-color: #FFFFFF;
}
.form-row {
width: 100%;
margin-bottom: 40rpx;
}
/* 身高体重同行 */
.form-row-inline {
width: 100%;
display: flex;
gap: 24rpx;
margin-bottom: 40rpx;
.form-row-half {
flex: 1;
}
}
}
.form-row {
width: 100%;
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
width: 100%;
height: 40rpx;
display: flex;
flex-wrap: nowrap;
margin-bottom: 18rpx;
.label-text {
color: #252535;
height: 40rpx;
font-size: 28rpx;
line-height: 40rpx;
font-weight: bold;
margin-right: 6rpx;
}
.label-star {
color: #FF4D4F;
height: 40rpx;
font-weight: 500;
font-size: 28rpx;
line-height: 40rpx;
}
}
.form-control {
width: 100%;
}
/* 可输入区域 */
.form-input {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
color: #252535;
font-size: 26rpx;
border-radius: 16rpx;
box-sizing: border-box;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.form-placeholder {
color: #B6B6B6;
font-size: 26rpx;
}
/* 日期选择器 */
.form-picker {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
color: #252535;
font-size: 26rpx;
line-height: 88rpx;
border-radius: 16rpx;
box-sizing: border-box;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.form-picker-placeholder {
color: #B6B6B6;
}
/* 输入框 + 后缀容器(身高 cm / 体重 kg) */
.form-input-wrap {
position: relative;
width: 100%;
.form-input-with-suffix {
padding-right: 80rpx;
}
.form-suffix-inside {
position: absolute;
right: 30rpx;
top: 0;
height: 88rpx;
color: #77849E;
font-size: 26rpx;
line-height: 88rpx;
}
}
/* 只读区域 */
.form-readonly {
width: 100%;
height: 88rpx;
box-sizing: border-box;
padding: 0 24rpx;
background: #F6F6F6;
border-radius: 16rpx;
display: flex;
align-items: center;
border: 2rpx solid #E6E6EA;
.readonly-text {
color: #252535;
font-size: 26rpx;
}
}
.form-readonly-with-suffix {
justify-content: space-between;
.form-suffix {
color: #77849E;
height: 88rpx;
font-size: 26rpx;
line-height: 88rpx;
}
}
/* 性别切换按钮(家庭用户) */
.gender-toggle {
display: flex;
gap: 24rpx;
.gender-option {
width: 300rpx;
height: 88rpx;
font-size: 28rpx;
text-align: center;
line-height: 88rpx;
border-radius: 16rpx;
box-sizing: border-box;
color: #77849E;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.gender-option-active {
color: #FFFFFF;
font-weight: bold;
background-color: #1385FA;
border-color: #1385FA;
}
}
}
/* 从已有用户中选择(蓝色描边幽灵按钮) */
.ghost-btn {
color: #1385FA;
width: 580rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 84rpx;
border-radius: 44rpx;
box-sizing: border-box;
border: 2rpx solid #1385FA;
margin: 0 auto;
}
}
/* 底部双按钮 */
.actions {
width: 100%;
height: 120rpx;
padding: 0 55rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
background-color: #FFFFFF;
box-shadow: 0 2rpx 8rpx 0 rgba(19, 133, 250, 0.06);
.btn-cancel {
color: #1385FA;
width: 300rpx;
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;
width: 300rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 88rpx;
border-radius: 44rpx;
background-color: #1385FA;
}
}
}
-518
View File
@@ -1,518 +0,0 @@
import { get, put } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
/** GET /weighingScale/v3/getUserInfoByIdcard 返回的 result 结构 */
interface UserInfoFromServer {
id: string | null
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
userType: string | null
idCard: string
height: number
weight: number
birthday: string
sex: number
avatar: string | null
phone: string
thirdId: string | null
realname: string
bmi: number
workNo: string
remark: string | null
relationType: string | null
sn: string
delFlag: number
sex_dictText: string
}
/** 表单数据模型 */
interface MemberForm {
/** 员工编号 */
workNo: string
/** 姓名 / 昵称 */
name: string
/** 身份证号 */
idCard: string
/** 性别 */
gender: string
/** 年龄 */
age: string
/** 身高(cm) */
height: string
/** 体重(kg) */
weight: string
/** 1=男 2=女 */
sex: number
}
/** 身份证解析结果 */
interface IdCardParsed {
gender: string
age: string
sex: number
birthday: string
}
/** 从抽屉组件 select 事件接收的用户数据(完整 item) */
interface ExistingMember {
id: string
name: string
gender: '男' | '女'
age: number
height: number
weight: number
idCard: string
avatar?: string
[key: string]: any
}
/**
* 添加用户页
* 员工 tab:员工编号查询 → 回填信息 → 补充身高体重
* 家庭 tab:昵称/身份证/性别/出生年月/身高体重
*/
Page({
data: {
/** tabs 当前选中:employee | family */
userType: 'employee' as 'employee' | 'family',
form: {
workNo: '',
name: '',
idCard: '',
gender: '',
age: '',
height: '',
weight: '',
sex: 0,
} as MemberForm,
/** 出生年月(家庭用户 date picker */
birthday: '',
/** 从抽屉选中的完整用户数据,提交时作为 base spread */
selectedMember: null as ExistingMember | null,
showSelectDrawer: false,
/** 正在请求身份证查询接口 */
querying: false,
/** 是否已完成一次身份证查询 */
hasQueried: false,
/** 接口返回的用户信息 */
serverResult: null as UserInfoFromServer | null,
/** 表单只读状态(code=101 时置为 true */
readonly: false,
/** 是否显示确认弹框 */
showConfirmModal: false,
/** 员工编号是否已查询 */
employeeQueried: false,
/** 员工编号查询是否未找到 */
employeeNotFound: false,
/** 部门展示文案 */
unitDisplay: '',
/** 员工编号查询返回的完整数据,提交时作为 base 合并 */
employeeQueryResult: null as Record<string, any> | null,
},
/** tabs 切换:清空查询结果、重置表单 */
onTabTap(e: WechatMiniprogram.TouchEvent) {
const tab = e.currentTarget.dataset.value as string
this.setData({
userType: tab as 'employee' | 'family',
employeeQueried: false,
employeeNotFound: false,
unitDisplay: '',
employeeQueryResult: null,
readonly: false,
hasQueried: false,
serverResult: null,
'form.workNo': '',
'form.name': '',
'form.idCard': '',
'form.gender': '',
'form.age': '',
'form.height': '',
'form.weight': '',
'form.sex': 0,
birthday: '',
selectedMember: null,
})
},
/** 员工编号变更即清空查询状态 */
onWorkNoInput(e: WechatMiniprogram.Input) {
this.setData({
'form.workNo': e.detail.value,
employeeQueried: false,
employeeNotFound: false,
unitDisplay: '',
employeeQueryResult: null,
'form.name': '',
'form.gender': '',
'form.age': '',
'form.height': '',
'form.weight': '',
})
},
/** 员工编号查询 */
onQueryEmployee() {
const workNo = this.data.form.workNo.trim()
if (!workNo) {
wx.showToast({ title: '请填写员工编号', icon: 'none' })
return
}
if (this.data.querying) return
this.setData({ querying: true, employeeQueried: false, employeeNotFound: false })
get<any>('weighingScale/v5/getUserInfoByWkno', {
sn: lefuService.deviceInfo?.serialNumber ?? '',
workNo
}, { loading: true })
.then(res => {
const result = res.result
if (!result) {
this.setData({
employeeQueried: true,
employeeNotFound: true,
unitDisplay: '',
employeeQueryResult: null,
'form.name': '',
'form.gender': '',
'form.age': '',
'form.height': '',
'form.weight': '',
})
return
}
const idCard = result.idCard || ''
const parsed = this.parseIdCard(idCard)
const sex = result.sex || parsed.sex
this.setData({
employeeQueried: true,
employeeNotFound: false,
employeeQueryResult: result,
unitDisplay: [result.secondDepart, result.thirdDepart].filter(Boolean).join(' / '),
'form.name': result.realname || '',
'form.idCard': idCard,
'form.gender': sex === 1 ? '男' : (sex === 2 ? '女' : parsed.gender),
'form.age': parsed.age,
'form.sex': sex,
'form.height': result.height ? String(result.height) : '',
'form.weight': result.weight ? String(result.weight) : '',
birthday: result.birthday || '',
})
})
.catch(() => {
this.setData({
employeeQueried: true,
employeeNotFound: true,
unitDisplay: '',
employeeQueryResult: null,
})
})
.finally(() => {
this.setData({ querying: false })
})
},
onNameInput(e: WechatMiniprogram.Input) {
const name = e.detail.value
this.setData({
'form.name': name,
serverResult: null,
hasQueried: false,
readonly: false
})
if (name.trim() && this.isIdCardValid(this.data.form.idCard)) {
this.fetchUserInfoByIdCard(name.trim(), this.data.form.idCard)
}
},
onIdCardInput(e: WechatMiniprogram.Input) {
const idCard = (e.detail.value || '').trim().toUpperCase()
if (this.isIdCardValid(idCard)) {
const parsed = this.parseIdCard(idCard)
this.setData({
'form.idCard': idCard,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: parsed.birthday,
})
} else {
this.setData({
'form.idCard': idCard,
'form.gender': '',
'form.age': '',
'form.sex': 0,
birthday: '',
})
}
},
/** 家庭用户性别切换 */
onGenderTap(e: WechatMiniprogram.TouchEvent) {
const value = e.currentTarget.dataset.value as string
const sex = value === '男' ? 1 : 2
this.setData({
'form.gender': value,
'form.sex': sex,
})
},
/** 出生年月变更 */
onBirthdayChange(e: WechatMiniprogram.PickerChange) {
this.setData({ birthday: e.detail.value as string })
},
onHeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.height': e.detail.value })
},
onWeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.weight': e.detail.value })
},
/** 从生日字符串计算周岁 */
calcAge(birthday: string): number {
if (!birthday) return 0
const parts = birthday.split('-').map(Number)
const [year, month, day] = parts
if (!year || !month || !day) return 0
const now = new Date()
let age = now.getFullYear() - year
const nowMonth = now.getMonth() + 1
const nowDay = now.getDate()
if (nowMonth < month || (nowMonth === month && nowDay < day)) age -= 1
return Math.max(0, age)
},
isIdCardValid(idCard: string): boolean {
return /^\d{17}[\dX]$/.test(idCard)
},
parseIdCard(idCard: string): IdCardParsed {
const empty: IdCardParsed = { gender: '', age: '', sex: 0, birthday: '' }
if (!this.isIdCardValid(idCard)) return empty
const genderCode = parseInt(idCard.charAt(16), 10)
const sex = genderCode % 2 === 1 ? 1 : 2
const gender = sex === 1 ? '男' : '女'
const year = parseInt(idCard.substr(6, 4), 10)
const month = parseInt(idCard.substr(10, 2), 10)
const day = parseInt(idCard.substr(12, 2), 10)
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return empty
const now = new Date()
let age = now.getFullYear() - year
if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1
if (age < 0 || age > 150) return empty
const mm = String(month).padStart(2, '0')
const dd = String(day).padStart(2, '0')
return { gender, age: String(age), sex, birthday: `${year}-${mm}-${dd}` }
},
fetchUserInfoByIdCard(name: string, idCard: string) {
if (this.data.querying) return
this.setData({ querying: true, serverResult: null, hasQueried: false })
get<any>('weighingScale/v3/getUserInfoByIdcard', {
sn: lefuService.deviceInfo?.serialNumber ?? '',
idCard: idCard || null,
realname: name
}, { loading: true })
.then(res => {
const serverResult = res.result
if (!serverResult) {
wx.showToast({ title: res.message || '未查询到用户信息', icon: 'none' })
const parsed = this.parseIdCard(idCard)
this.setData({
hasQueried: true,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: parsed.birthday,
})
return
}
if (serverResult.flag == 101) {
const parsed = this.parseIdCard(serverResult.idCard)
this.setData({
serverResult,
hasQueried: true,
readonly: true,
showConfirmModal: true,
'form.name': serverResult.realname ?? '',
'form.idCard': serverResult.idCard ?? '',
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
'form.height': serverResult.height ? String(serverResult.height) : '',
'form.weight': serverResult.weight ? String(serverResult.weight) : '',
birthday: serverResult.birthday || parsed.birthday
})
return
}
if (serverResult.flag === null) {
const parsed = this.parseIdCard(serverResult.idCard)
const data: Record<string, any> = {
serverResult,
hasQueried: true,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: serverResult.birthday || parsed.birthday
}
if (serverResult.height) {
data['form.height'] = String(serverResult.height)
}
if (serverResult.weight) {
data['form.weight'] = String(serverResult.weight)
}
this.setData(data)
}
})
.catch(() => {})
.finally(() => {
this.setData({ querying: false })
})
},
onTapSelectExisting() {
this.setData({ showSelectDrawer: true })
},
onSelectMemberClose() {
this.setData({ showSelectDrawer: false })
},
onSelectMember(e: WechatMiniprogram.CustomEvent<ExistingMember>) {
const member = e.detail
const parsed = this.parseIdCard(member.idCard)
this.setData({
showSelectDrawer: false,
selectedMember: member,
'form.workNo': member.workNo || '',
'form.name': member.name,
'form.idCard': member.idCard,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.height': String(member.height),
'form.weight': String(member.weight),
})
if (member.name.trim() && this.isIdCardValid(member.idCard)) {
this.fetchUserInfoByIdCard(member.name.trim(), member.idCard)
}
},
onConfirmModalConfirm() {
this.setData({ showConfirmModal: false })
this.onSubmit()
},
onConfirmModalCancel() {
this.setData({
showConfirmModal: false,
readonly: false,
form: {
workNo: '',
name: '',
idCard: '',
gender: '',
age: '',
height: '',
weight: '',
sex: 0,
}
})
},
onCancel() {
wx.navigateBack()
},
onSubmit() {
const { name, idCard, gender, height, weight, sex } = this.data.form
const { userType } = this.data
const heightNum = parseFloat(height)
const weightNum = parseFloat(weight)
if (userType === 'employee') {
if (!this.data.employeeQueried || this.data.employeeNotFound) {
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' })
return
}
if (!gender) {
wx.showToast({ title: '请选择性别', icon: 'none' })
return
}
if (!this.data.birthday.trim()) {
wx.showToast({ title: '请填写出生年月', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
return
}
}
const userInfo = wx.getStorageSync('userInfo')
const connectDeviceInfoRaw = wx.getStorageSync('connectDeviceInfo')
// 员工:以查询接口返回为 base;家庭:以选中的已有用户 + 身份证查询结果为 base
const baseSpread = userType === 'employee'
? (this.data.employeeQueryResult ?? {})
: { ...(this.data.selectedMember ?? {}), ...(this.data.serverResult ?? {}) }
const birthday = this.data.birthday
const finalSex = userType === 'family' ? sex : this.data.form.sex
const payload: Record<string, any> = {
...(baseSpread),
dataType: userType === 'family' ? 1 : 0,
parentUserId: userInfo?.userId ?? '',
sn: lefuService.deviceInfo?.serialNumber ?? '',
scaleDeviceId: lefuService.deviceInfo?.serialNumber ?? '',
realname: name.trim(),
idCard: idCard || null,
sex: finalSex,
birthday,
height: heightNum,
weight: isNaN(weightNum) ? 0 : weightNum,
connectDeviceInfo: connectDeviceInfoRaw ? JSON.stringify(connectDeviceInfoRaw) : '',
}
put('weighingScale/v5/edit/user', payload)
.then(() => {
wx.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => {
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2] as any
if (prevPage && prevPage.data?.sn) {
prevPage._loadData(prevPage.data.sn)
}
wx.navigateBack()
}, 1500)
})
.catch(() => {
wx.showToast({ title: '添加失败,请重试', icon: 'none' })
})
}
})
-207
View File
@@ -1,207 +0,0 @@
<view class="add-member">
<!-- 顶部浅蓝提示条 -->
<view class="tips">
<view class="tips-icon">!</view>
<text class="tips-text">最多支持 10 位用户</text>
</view>
<view class="form">
<scroll-view scroll-y class="scrollView">
<view class="scrollViewContent">
<!-- 顶部胶囊式 tabs -->
<view class="tabsDom">
<view class="tabs">
<view class="tab-item {{ userType === 'employee' ? 'active' : '' }}" bind:tap="onTabTap" data-value="employee">员工</view>
<view class="tab-item {{ userType === 'family' ? 'active' : '' }}" bind:tap="onTabTap" data-value="family">家庭用户</view>
</view>
</view>
<!-- ========== 员工 tab ========== -->
<block wx:if="{{ userType === 'employee' }}">
<!-- 员工编号输入 + 查询按钮 -->
<view class="selectWorkNo">
<view class="form-row">
<view class="form-label">
<text class="label-text">员工编号</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<input class="form-input" type="number" placeholder="请填写员工编号" placeholder-class="form-placeholder" value="{{ form.workNo }}" bind:input="onWorkNoInput" />
</view>
</view>
<view class="selectWorkNoBtn" bind:tap="onQueryEmployee">确认查询</view>
</view>
<!-- 未查询 -->
<view class="workNoDesc" wx:if="{{ !employeeQueried }}">输入员工编号后自动查询员工信息</view>
<!-- 查询未找到 -->
<view class="workNoNull" wx:if="{{ employeeQueried && employeeNotFound }}">未查询到员工信息,请确认员工编号是否正确</view>
<!-- 查询成功 -->
<view class="workNoInfo" wx:if="{{ employeeQueried && !employeeNotFound }}">
<view class="workNoInfo_">
<view class="workNoInfoTitle">用户信息</view>
<view class="workNoInfoContent">
<view>
<view class="form-row">
<view class="form-label"><text class="label-text">姓名</text></view>
<view class="form-control">
<view class="form-readonly"><text class="readonly-text">{{ form.name }}</text></view>
</view>
</view>
<view class="form-row">
<view class="form-label"><text class="label-text">单位</text></view>
<view class="form-control">
<view class="form-readonly"><text class="readonly-text">{{ unitDisplay }}</text></view>
</view>
</view>
</view>
<view>
<view class="form-row" style="margin-bottom: 0;">
<view class="form-label"><text class="label-text">性别</text></view>
<view class="form-control">
<view class="form-readonly"><text class="readonly-text">{{ form.gender }}</text></view>
</view>
</view>
<view class="form-row">
<view class="form-label"><text class="label-text">年龄</text></view>
<view class="form-control">
<view class="form-readonly"><text class="readonly-text">{{ form.age }}</text></view>
</view>
</view>
</view>
</view>
</view>
<view class="workNoInfo_">
<view class="workNoInfoTitle">补充信息</view>
<view class="workNoInfoContent">
<view>
<view class="form-row" style="margin-bottom: 0;">
<view class="form-label">
<text class="label-text">身高</text>
<text class="label-star">*</text>
</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" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<view class="form-row">
<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" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</block>
<!-- ========== 家庭用户 tab ========== -->
<block wx:else>
<view class="familyInfo">
<view class="familyInfo_">
<!-- 昵称 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">昵称</text>
<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" />
</view>
</view>
<!-- 身份证号码 -->
<view class="form-row">
<view class="form-label"><text class="label-text">身份证号</text></view>
<view class="form-control">
<input class="form-input" type="idcard" maxlength="18" placeholder="请填写身份证号" placeholder-class="form-placeholder" value="{{ form.idCard }}" disabled="{{ readonly }}" bind:input="onIdCardInput" />
</view>
</view>
<!-- 性别(左右切换) -->
<view class="form-row">
<view class="form-label">
<text class="label-text">性别</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<view class="gender-toggle">
<view class="gender-option {{ form.gender === '男' ? 'gender-option-active' : '' }}" data-value="男" bind:tap="onGenderTap">男</view>
<view class="gender-option {{ form.gender === '女' ? 'gender-option-active' : '' }}" data-value="女" bind:tap="onGenderTap">女</view>
</view>
</view>
</view>
<!-- 出生年月 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">出生年月</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<picker mode="date" value="{{ birthday }}" bindchange="onBirthdayChange">
<view class="form-picker {{ birthday ? '' : 'form-picker-placeholder' }}">{{ birthday || '请选择出生年月' }}</view>
</picker>
</view>
</view>
<!-- 身高 + 体重 同行 -->
<view class="form-row-inline">
<view class="form-row-half">
<view class="form-label">
<text class="label-text">身高</text>
<text class="label-star">*</text>
</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" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<view class="form-row-half">
<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" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</view>
</view>
<!-- 从已有用户中选择 -->
<view class="ghost-btn" bind:tap="onTapSelectExisting">从已有用户中选择</view>
</view>
</block>
</view>
</scroll-view>
</view>
<!-- 底部双按钮:家庭用户始终展示,员工仅查询到信息后展示 -->
<block wx:if="{{ userType === 'family' || (userType === 'employee' && employeeQueried && !employeeNotFound) }}">
<view class="actions">
<view class="btn-cancel" bind:tap="onCancel">取消</view>
<view class="btn-primary" bind:tap="onSubmit">保存用户信息</view>
</view>
</block>
<!-- 选择已有用户抽屉 -->
<select-member-drawer show="{{ showSelectDrawer }}" bind:close="onSelectMemberClose" bind:select="onSelectMember">
</select-member-drawer>
<!-- 确认弹框 -->
<user-info-confirm-modal show="{{ showConfirmModal }}" userInfo="{{ serverResult }}" bind:confirm="onConfirmModalConfirm" bind:cancel="onConfirmModalCancel">
</user-info-confirm-modal>
</view>
@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "搜索设备",
"usingComponents": {}
}
@@ -1,285 +0,0 @@
/* 连接设备页 —— 三态(idle / searching / found / empty)共用 */
.connected-device {
width: 100%;
height: 100%;
padding: 48rpx 30rpx 0;
box-sizing: border-box;
background-color: #ffffff;
/* 三层同心圆波纹动画 */
.ripple {
position: relative;
width: 300rpx;
height: 300rpx;
margin: 0 auto 48rpx;
display: flex;
align-items: center;
justify-content: center;
/* 波纹圆环:默认隐藏,仅在 .ripple--active 时播放动画 */
.ripple-ring {
position: absolute;
width: 300rpx;
height: 300rpx;
border-radius: 50%;
background-color: #1385FA;
opacity: 0;
}
&.ripple--active {
.ripple-ring {
animation: ripple-expand 2.4s ease-out infinite;
}
.ripple-ring--delay1 {
animation-delay: 0.8s;
}
.ripple-ring--delay2 {
animation-delay: 1.6s;
}
}
/* 中心实心圆 + 图标 */
.ripple-core {
position: relative;
z-index: 1;
width: 140rpx;
height: 140rpx;
padding: 30rpx;
box-sizing: border-box;
border-radius: 50%;
background-color: #1385FA;
display: flex;
align-items: center;
justify-content: center;
image {
width: 80rpx;
height: 80rpx;
}
}
}
@keyframes ripple-expand {
0% {
transform: scale(0.46);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
.title {
color: #252535;
width: 100%;
height: 36rpx;
font-size: 36rpx;
font-weight: bold;
line-height: 36rpx;
text-align: center;
margin-bottom: 22rpx;
}
.sub-title {
color: #808080;
width: 100%;
height: 30rpx;
font-size: 30rpx;
text-align: center;
line-height: 30rpx;
margin-bottom: 40rpx;
}
.search-btn {
color: #FFFFFF;
width: 580rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 88rpx;
border-radius: 44rpx;
margin: 0 auto 32rpx;
background-color: #1385FA;
}
// .help {
// width: 100%;
// height: 30rpx;
// text-align: center;
// line-height: 30rpx;
// margin-bottom: 60rpx;
// .help-text {
// color: #808080;
// font-size: 30rpx;
// line-height: 30rpx;
// margin-right: 8rpx;
// }
// .help-link {
// color: #1385FA;
// font-size: 30rpx;
// line-height: 30rpx;
// }
// }
.search-device {
width: 100%;
.search-device-header {
width: 100%;
height: 32rpx;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
margin-bottom: 38rpx;
.search-device-title {
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
}
.search-device-count {
height: 32rpx;
display: flex;
flex-wrap: nowrap;
.count-text {
color: #808080;
height: 32rpx;
font-size: 28rpx;
line-height: 32rpx;
}
.count-num {
color: #1385FA;
height: 32rpx;
font-size: 28rpx;
font-weight: bold;
line-height: 32rpx;
padding: 0 6rpx;
}
}
}
.empty {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 160rpx;
.empty-image {
width: 193rpx;
height: 138rpx;
margin-bottom: 40rpx;
background-color: #F5F5F5;
}
.empty-text {
color: #808080;
width: 100%;
height: 30rpx;
font-size: 30rpx;
line-height: 30rpx;
text-align: center;
}
}
.device-list {
width: 100%;
display: flex;
flex-direction: column;
.device-card {
width: 100%;
height: 150rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
border-radius: 16rpx;
box-sizing: border-box;
margin-bottom: 24rpx;
.card-icon {
width: 90rpx;
height: 90rpx;
overflow: hidden;
border-radius: 24rpx;
margin-right: 24rpx;
}
.card-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.card-name {
color: #252535;
font-size: 32rpx;
font-weight: bold;
height: 32rpx;
line-height: 32rpx;
}
.card-status {
margin-top: 18rpx;
display: flex;
align-items: center;
.card-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #2AC79F;
margin-right: 15rpx;
}
.card-status-text {
color: #2AC79F;
height: 28rpx;
font-size: 28rpx;
font-weight: 500;
line-height: 28rpx;
}
}
}
.card-btn-primary {
color: #FFFFFF;
font-size: 24rpx;
font-weight: 500;
width: 88rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
border-radius: 8rpx;
background-color: #2AC79F;
}
.card-btn-danger {
color: #F24439;
font-size: 24rpx;
font-weight: 500;
width: 120rpx;
height: 48rpx;
line-height: 44rpx;
text-align: center;
border: 2rpx solid #F24439;
border-radius: 8rpx;
box-sizing: border-box;
}
}
}
}
}
@@ -1,264 +0,0 @@
import { lefuService } from '../../lefu/index'
import type { ScannedDevice } from '../../lefu/index'
import type { RawDevice } from '../../lefu/types'
/** 展示用设备项(仅存入 setData,不含复杂对象) */
interface DeviceItem {
name: string
connected: boolean
}
/** 页面状态 */
type DeviceStatus = 'idle' | 'searching' | 'found' | 'empty'
/** 页面级原始设备缓存,不进入 setData 避免序列化异常 */
let _rawDevices: ScannedDevice[] = []
const TAG = '[connectedDevice]'
Page({
data: {
status: 'idle' as DeviceStatus,
deviceList: [] as DeviceItem[],
/** 防止权限弹框重入 */
_modalLock: false,
/** 自动连接缓存设备的一次性标记,防止扫描回调反复触发 */
_autoConnectTried: false,
},
onShow() {
console.log(TAG, 'onShow')
// 回到页面时同步实际连接状态,避免回调被其他页面覆盖导致状态不同步
if (!lefuService.isConnected) {
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
this.setData({ deviceList: list })
}
},
onHide() {
console.log(TAG, 'onHide, 停止扫描')
// _rawDevices = []
// this.setData({ status: 'idle', deviceList: [] })
lefuService.stopScan()
},
onUnload() {
console.log(TAG, 'onUnload, 停止扫描')
lefuService.stopScan()
},
/** 点击「开始搜索 / 重新搜索」按钮:先验权限,再扫描 */
async onStartSearch() {
console.log(TAG, 'onStartSearch, _modalLock:', this.data._modalLock)
if (this.data._modalLock) return
_rawDevices = []
this.setData({ status: 'searching', deviceList: [], _autoConnectTried: false })
await this._checkPermissionAndScan()
},
/** 检查蓝牙 + 位置权限,通过后开始扫描 */
async _checkPermissionAndScan() {
console.log(TAG, '_checkPermissionAndScan 开始')
try {
const { authSetting } = await wx.getSetting()
const setting = authSetting as Record<string, boolean | undefined>
const hasBle = setting['scope.bluetooth']
const hasLoc = setting['scope.userLocation']
console.log(TAG, '权限状态 → 蓝牙:', hasBle, '位置:', hasLoc)
if (hasBle && hasLoc) {
await this._openAdapterAndScan()
return
}
// 逐个申请缺失权限
const missing: Array<{ scope: string; label: string }> = []
if (!hasBle) missing.push({ scope: 'scope.bluetooth', label: '蓝牙' })
if (!hasLoc) missing.push({ scope: 'scope.userLocation', label: '位置' })
console.log(TAG, '缺失权限:', missing.map(m => m.label))
try {
for (const item of missing) {
console.log(TAG, '申请权限:', item.scope)
await wx.authorize({ scope: item.scope })
console.log(TAG, '权限申请成功:', item.scope)
}
await this._openAdapterAndScan()
} catch (err) {
console.warn(TAG, '权限申请被拒绝:', err)
this._showPermissionModal(missing.map(m => m.label).join('和'))
}
} catch (err) {
console.error(TAG, 'getSetting 失败:', err)
wx.showToast({ title: '获取权限设置失败', icon: 'none' })
this.setData({ status: 'idle' })
}
},
/** 引导用户去设置页开启权限 */
_showPermissionModal(permissionNames: string) {
console.log(TAG, '弹出权限引导框, 缺失:', permissionNames)
this.setData({ _modalLock: true })
wx.showModal({
title: '需要授权',
content: `小程序需要您的${permissionNames}权限才能搜索蓝牙设备,请前往手机设置开启。`,
confirmText: '去开启',
cancelText: '取消',
success: (res) => {
console.log(TAG, '权限弹框结果:', res.confirm ? '去开启' : '取消')
this.setData({ _modalLock: false, status: 'idle' })
if (res.confirm) {
wx.openSetting({
success: () => this.onStartSearch(),
})
}
},
fail: () => {
this.setData({ _modalLock: false, status: 'idle' })
},
})
},
/** 打开蓝牙适配器,成功后启动扫描 */
async _openAdapterAndScan() {
console.log(TAG, 'wx.openBluetoothAdapter() 开始')
try {
await wx.openBluetoothAdapter()
console.log(TAG, 'wx.openBluetoothAdapter() 成功')
this._startScan()
} catch (err: any) {
console.error(TAG, 'wx.openBluetoothAdapter() 失败, errCode:', err?.errCode, err?.errMsg)
if (err?.errCode === 10001) {
wx.showModal({
title: '蓝牙未开启',
content: '请先开启手机蓝牙后重试',
showCancel: false,
confirmText: '知道了',
success: () => this.setData({ status: 'idle' }),
})
} else {
wx.showToast({ title: '蓝牙初始化失败', icon: 'none' })
this.setData({ status: 'idle' })
}
}
},
/** 权限已就绪,注册设备列表回调并启动扫描 */
_startScan() {
console.log(TAG, '_startScan 启动')
lefuService.onDevicesList((devices) => {
console.log(TAG, 'onDevicesList 回调, 设备数:', devices.length, devices.map(d => d.name))
if (!devices.length) {
_rawDevices = []
this.setData({ status: 'empty', deviceList: [] })
return
}
_rawDevices = devices
// 保留已连接设备的状态,避免扫描刷新时把 connected 重置为 false
const connectedName = this.data.deviceList.find(d => d.connected)?.name
const list: DeviceItem[] = devices.map(d => ({
name: d.name,
connected: d.name === connectedName,
}))
this.setData({ status: 'found', deviceList: list })
// 缓存设备命中:自动连接一次
if (this.data._autoConnectTried) return
const cached = wx.getStorageSync('connectDeviceInfo') as RawDevice | null
const cachedDeviceId = cached?.deviceId
if (!cachedDeviceId) return
const matchedIndex = devices.findIndex(d => d.raw.deviceId === cachedDeviceId)
if (matchedIndex < 0) return
console.log(TAG, '命中缓存设备, deviceId:', cachedDeviceId, 'index:', matchedIndex)
this.setData({ _autoConnectTried: true })
this._autoConnectByIndex(matchedIndex)
})
lefuService.onDisconnected(() => {
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
this.setData({ deviceList: list })
})
lefuService.startScan()
console.log(TAG, 'lefuService.startScan() 已调用')
},
/** 缓存设备自动连接(与 onConnect 共享连接流程) */
_autoConnectByIndex(index: number) {
const raw = _rawDevices[index]
if (!raw) return
wx.showLoading({ title: '连接中...', mask: true })
lefuService.stopScan()
lefuService.connect(raw.raw)
lefuService.onConnectState((state) => {
if (state === lefuService.BLUE_STATE.CONNECTFAILED) {
wx.hideLoading()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
}
})
lefuService.onDeviceConnect(() => {
wx.setStorageSync('connectDeviceInfo', { ...raw.raw, scaleDeviceName: raw.raw.name })
const list = this.data.deviceList.map((item, i) => ({
...item,
connected: i === index,
}))
this.setData({ deviceList: list })
wx.hideLoading()
console.log(TAG, '缓存设备已自动连接(不跳转)')
})
},
/** 点击整张卡片:已连接 → 跳转配网页;未连接 → 走手动连接流程 */
onTapCard(e: WechatMiniprogram.TouchEvent) {
const connected = e.currentTarget.dataset.connected as boolean
console.log(TAG, 'onTapCard, connected:', connected)
if (connected) {
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
return
}
this.onConnect(e)
},
/** 连接指定设备 */
onConnect(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index as number
const raw = _rawDevices[index]
console.log(TAG, 'onConnect, index:', index, 'device:', raw)
if (!raw) {
console.warn(TAG, 'onConnect: 找不到对应 raw device, index:', index)
return
}
wx.showLoading({ title: '连接中...', mask: true })
lefuService.connect(raw.raw)
lefuService.onConnectState((state) => {
if (state === lefuService.BLUE_STATE.CONNECTFAILED) {
wx.hideLoading()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
}
})
lefuService.onDeviceConnect(() => {
wx.setStorageSync('connectDeviceInfo', { ...raw.raw, scaleDeviceName: raw.raw.name })
const list = this.data.deviceList.map((item, i) => ({
...item,
connected: i === index,
}))
this.setData({ deviceList: list })
wx.hideLoading()
console.log(TAG, '设备连接就绪,跳转配网页')
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
})
},
/** 断开当前连接 */
onDisconnect() {
console.log(TAG, 'onDisconnect')
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
this.setData({ deviceList: list })
lefuService.disconnect()
},
/** 查看帮助 */
onOpenHelp() {
wx.showToast({ title: '请确保设备已开机且在附近', icon: 'none' })
},
})
@@ -1,83 +0,0 @@
<view class="connected-device">
<!-- 三层同心圆波纹动画:仅在非 idle(已点击开始搜索)时播放 -->
<view class="ripple {{ status === 'idle' ? '' : 'ripple--active' }}">
<view class="ripple-ring"></view>
<view class="ripple-ring ripple-ring--delay1"></view>
<view class="ripple-ring ripple-ring--delay2"></view>
<view class="ripple-core">
<image src="/images/connectedDevice/icon.png" mode=""/>
</view>
</view>
<!-- 主标题 -->
<view class="title">
{{ status === 'idle' ? '搜索附近设备' : '搜索附近设备中...' }}
</view>
<!-- 副标题 -->
<view class="sub-title">请确保蓝牙已开启,设备处于待机状态</view>
<!-- 主按钮:idle 显示开始搜索,其他显示重新搜索 -->
<view class="search-btn" bind:tap="onStartSearch">
{{ status === 'idle' ? '开始搜索' : '重新搜索' }}
</view>
<!-- 帮助文案 -->
<!-- <view class="help">
<text class="help-text">无法配对设备?</text>
<text class="help-link" bind:tap="onOpenHelp">查看帮助</text>
</view> -->
<!-- 搜索结果模块:found / empty 时整体显示 -->
<block wx:if="{{ status === 'found' || status === 'empty' }}">
<view class="search-device">
<!-- 头部:标题 + 计数 -->
<view class="search-device-header">
<text class="search-device-title">搜索结果</text>
<view class="search-device-count">
<text class="count-text">发现</text>
<text class="count-num">{{ deviceList.length }}</text>
<text class="count-text">台设备</text>
</view>
</view>
<!-- 空状态 -->
<block wx:if="{{ status === 'empty' }}">
<view class="empty">
<view class="empty-image"></view>
<view class="empty-text">暂未搜索到可用设备,请重新搜索</view>
</view>
</block>
<!-- 找到设备:统一循环,内部按 connected 切换 UI -->
<block wx:if="{{ status === 'found' }}">
<view class="device-list">
<block wx:for="{{ deviceList }}" wx:key="name">
<view class="device-card" data-index="{{ index }}" data-connected="{{ item.connected }}" bind:tap="onTapCard">
<view class="card-icon">
<image src="/images/device/{{ item.name }}.png" mode="aspectFit" style="width:100%;height:100%;"></image>
</view>
<view class="card-info">
<view class="card-name">{{ item.name }}</view>
<block wx:if="{{ item.connected }}">
<view class="card-status">
<view class="card-dot"></view>
<text class="card-status-text">已连接</text>
</view>
</block>
</view>
<block wx:if="{{ item.connected }}">
<view class="card-btn-danger" catch:tap="onDisconnect">断开连接</view>
</block>
<block wx:else>
<view class="card-btn-primary" data-index="{{ index }}" catch:tap="onConnect">连接</view>
</block>
</view>
</block>
</view>
</block>
</view>
</block>
</view>
@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "WiFi连接",
"usingComponents": {}
}
@@ -1,389 +0,0 @@
.connectedWifi {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
padding: 30rpx 30rpx 0;
box-sizing: border-box;
background-color: #FFFFFF;
}
.setWifi {
width: 100%;
height: 100%;
padding: 30rpx 10rpx 0;
box-sizing: border-box;
}
.setWifi-title {
color: #252535;
width: 100%;
height: 48rpx;
font-size: 48rpx;
font-weight: bold;
line-height: 48rpx;
margin-bottom: 48rpx;
}
.setWifi-form {
width: 100%;
margin-bottom: 64rpx;
}
.setWifi-form>view {
width: 100%;
height: 96rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 16rpx;
background-color: #F6F6F6;
}
.setWifi-form>view:first-of-type {
margin-bottom: 32rpx;
}
.setWifi-form>view>view:nth-of-type(1) {
color: #252535;
width: 100rpx;
height: 96rpx;
line-height: 96rpx;
font-size: 32rpx;
font-weight: bold;
}
.setWifi-form>view:first-of-type>view:nth-of-type(1) {
padding: 24rpx 45rpx 24rpx 7rpx;
box-sizing: border-box;
display: flex;
align-items: center;
}
.setWifi-form>view>view:nth-of-type(2) {
color: #252535;
flex: 1;
width: 0;
height: 96rpx;
font-size: 32rpx;
line-height: 96rpx;
}
.setWifi-form>view>view:nth-of-type(2) input {
color: #252535;
width: 100%;
height: 96rpx;
font-size: 32rpx;
line-height: 96rpx;
}
.placeholderClass {
color: #B6B6B6;
height: 96rpx;
font-size: 32rpx;
line-height: 96rpx;
}
.setWifi-form>view>view:nth-of-type(3) {
width: 56rpx;
height: 56rpx;
}
.setWifi-btn {
width: 100%;
height: 88rpx;
padding: 0 15rpx;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
box-sizing: border-box;
}
.setWifi-btn>view {
color: #FFFFFF;
width: 300rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
border-radius: 44rpx;
line-height: 84rpx;
box-sizing: border-box;
background-color: #1385FA;
border: 2rpx solid #1385FA;
&:first-of-type {
color: #1385FA;
background-color: #FFFFFF;
}
}
/* 顶部已连接设备卡 */
.deviceCard {
width: 100%;
height: 150rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 30rpx;
box-sizing: border-box;
border: 2rpx solid #E6E6EA;
border-radius: 16rpx;
background-color: #FFFFFF;
margin-bottom: 46rpx;
/* 设备图标占位 */
>view:nth-of-type(1) {
width: 90rpx;
height: 90rpx;
overflow: hidden;
margin-right: 24rpx;
border-radius: 24rpx;
flex-shrink: 0;
}
/* 右侧信息 */
>view:nth-of-type(2) {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
/* 设备名 */
>view:nth-of-type(1) {
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
margin-bottom: 16rpx;
}
/* 状态行 */
>view:nth-of-type(2) {
display: flex;
flex-wrap: nowrap;
align-items: center;
}
}
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 14rpx;
flex-shrink: 0;
&--green { background-color: #2AC79F; }
&--red { background-color: #F24439; }
&--gray { background-color: #999999; }
}
.status-text {
height: 28rpx;
font-size: 28rpx;
font-weight: 500;
line-height: 28rpx;
&--green { color: #2AC79F; }
&--red { color: #F24439; }
&--gray { color: #999999; }
}
/* idle / searching 两态:居中文案 */
.wifiIdle,
.wifiSearching {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: #B6B6B6;
font-size: 32rpx;
line-height: 32rpx;
}
/* 附近WiFi 标题区:title 行 + 副标题 */
.wifiHeader {
width: 100%;
padding: 0 12rpx;
box-sizing: border-box;
/* 标题行:附近WiFi + 可选「刷新」 */
>view:nth-of-type(1) {
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
>view:nth-of-type(1) {
color: #252535;
height: 36rpx;
font-size: 36rpx;
font-weight: bold;
line-height: 36rpx;
}
>view:nth-of-type(2) {
color: #1385FA;
height: 36rpx;
font-size: 28rpx;
font-weight: 500;
line-height: 36rpx;
}
}
/* 副标题 */
>view:nth-of-type(2) {
color: #B6B6B6;
height: 30rpx;
font-size: 28rpx;
line-height: 30rpx;
margin-bottom: 32rpx;
}
}
/* WiFi 列表:scroll-view Y 轴滚动,撑满剩余高度 */
.wifiList {
width: 100%;
flex: 1;
min-height: 0;
padding: 0 12rpx;
box-sizing: border-box;
}
.arrow {
&::after {
right: 4rpx;
}
}
/* 列表项:普通态 */
.wifiItem {
width: 100%;
height: 80rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
margin-bottom: 32rpx;
/* 左侧 wifi 图标容器 */
>view:nth-of-type(1) {
width: 80rpx;
height: 80rpx;
margin-right: 24rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
image {
width: 100%;
height: 100%;
}
}
/* WiFi 名称 */
>text {
flex: 1;
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: 500;
line-height: 32rpx;
}
/* 右箭头 */
>view:nth-of-type(2) {
width: 20rpx;
height: 18rpx;
}
}
/* 列表项:已连接态 */
.wifiItem-connected {
>text {
color: #1385FA;
}
/* 右侧「已连接」+ 勾 */
>view:nth-of-type(2) {
width: auto;
height: auto;
display: flex;
flex-wrap: nowrap;
align-items: center;
background-color: transparent;
>text {
color: #2AC79F;
font-size: 24rpx;
font-weight: 500;
line-height: 24rpx;
margin-right: 5rpx;
}
/* 右箭头 */
>view {
width: 20rpx;
height: 18rpx;
}
}
}
/* 列表态底部「确认」按钮:贴底 */
.wifiBtn {
width: 100%;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
>view {
color: #FFFFFF;
width: 580rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 88rpx;
border-radius: 44rpx;
background-color: #1385FA;
}
.wifiBtn-disabled {
background-color: #A8C9F0;
}
}
/* 空态 */
.wifiEmpty {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
padding: 39rpx 42rpx 0;
box-sizing: border-box;
>view {
width: 566rpx;
height: 499rpx;
margin-bottom: 40rpx;
border-radius: 16rpx;
background-color: #F5F8FF;
}
>text {
color: #1385FA;
font-size: 36rpx;
font-weight: bold;
line-height: 36rpx;
margin-top: 8rpx;
}
}
@@ -1,134 +0,0 @@
import { lefuService } from '../../lefu/index'
import type { LeFuWifiItem } from '../../lefu/index'
interface WifiItem {
ssid: string
connected: boolean
}
type WifiStatus = 'idle' | 'searching' | 'list' | 'empty' | 'password' | 'configuring'
type BleStatus = 'connected' | 'failed' | 'connecting'
Page({
data: {
status: 'idle' as WifiStatus,
deviceName: '智能体重秤',
bleStatus: 'connecting' as BleStatus,
wifiList: [] as WifiItem[],
hasConnected: false,
selectedSsid: '',
selectWifiPWD: '',
type: true,
},
_toBleStatus(state: string): BleStatus {
const bs = lefuService.BLUE_STATE
if (state === bs.CONNECTSUCCESS || state === bs.WIFISUCCESS) return 'connected'
if (state === bs.CONNECTFAILED || state === bs.UNAVAILABLE) return 'failed'
return 'connecting'
},
onLoad() {
// 尽早注册 deviceInfo 回调,防止 onShow 前已触发而错过
lefuService.onDeviceInfo((info) => {
this.setData({ deviceName: info.modelNumber })
})
// 设备断开即退出配网页
lefuService.onDisconnected(() => {
wx.showToast({ title: '设备已断开连接', icon: 'none', duration: 1500 })
setTimeout(() => wx.navigateBack(), 1500)
})
this.setData({
bleStatus: this._toBleStatus(lefuService.connectState),
deviceName: lefuService.deviceInfo?.modelNumber ?? '智能体重秤',
})
this.onStartSearch()
},
onShow() {
lefuService.onConnectState((state) => {
this.setData({ bleStatus: this._toBleStatus(state) })
})
},
onUnload() {
wx.hideLoading()
lefuService.onDisconnected(() => {})
if (this.data.status === 'configuring') {
lefuService.disconnect()
}
},
onStartSearch() {
this.setData({ status: 'searching', wifiList: [] })
wx.showLoading({ title: '正在获取 WiFi 列表...', mask: true })
lefuService.getWifiList()
.then((items: LeFuWifiItem[]) => {
wx.hideLoading()
if (!items.length) {
this.setData({ status: 'empty' })
return
}
this.setData({
status: 'list',
wifiList: items.map(item => ({ ssid: item.ssid, connected: false })),
})
})
.catch(() => {
wx.hideLoading()
this.setData({ status: 'empty' })
})
},
onRefresh() {
this.onStartSearch()
},
onTapWifi(e: WechatMiniprogram.TouchEvent) {
const ssid = e.currentTarget.dataset.ssid as string
const target = this.data.wifiList.find(item => item.ssid === ssid)
if (!target || target.connected) return
this.setData({ status: 'password', selectedSsid: ssid, selectWifiPWD: '', type: true })
},
passwordInput(e: WechatMiniprogram.Input) {
this.setData({ selectWifiPWD: e.detail.value })
},
inputTypeChange() {
this.setData({ type: !this.data.type })
},
onCancelPwd() {
this.setData({ status: 'list', selectedSsid: '', selectWifiPWD: '' })
},
onConfirmPwd() {
const { selectedSsid, selectWifiPWD } = this.data
if (!selectedSsid) return
this.setData({ status: 'configuring' })
wx.showLoading({ title: '正在配网...', mask: true })
lefuService.configWifi(selectedSsid, selectWifiPWD)
.then(() => {
wx.hideLoading()
this.setData({
status: 'list',
wifiList: this.data.wifiList.map(item => ({ ...item, connected: item.ssid === selectedSsid })),
hasConnected: true,
selectedSsid: '',
selectWifiPWD: '',
})
})
.catch((err: Error) => {
wx.hideLoading()
wx.showToast({ title: err.message || '配网失败,请重试', icon: 'none' })
this.setData({ status: 'password' })
})
},
onConfirm() {
if (!this.data.hasConnected) return
wx.navigateTo({ url: '/pages/supplementPersonal/supplementPersonal' })
},
})
@@ -1,134 +0,0 @@
<view class="connectedWifi">
<block wx:if="{{status === 'password'}}">
<view class="setWifi">
<view class="setWifi-title">输入Wi-Fi密码</view>
<view class="setWifi-form">
<view>
<view>
<image src="/images/connectedWifi/wifiPwd.png"/>
</view>
<view>{{selectedSsid}}</view>
</view>
<view>
<view>密码</view>
<view>
<block wx:if="{{type}}">
<input
password="{{true}}"
value="{{selectWifiPWD}}"
bind:input="passwordInput"
placeholder="输入您的wi-fi密码"
placeholder-class="placeholderClass"/>
</block>
<block wx:else>
<input
password="{{false}}"
value="{{selectWifiPWD}}"
bind:input="passwordInput"
placeholder="输入您的wi-fi密码"
placeholder-class="placeholderClass"/>
</block>
</view>
<view bind:tap="inputTypeChange">
<image src="/images/connectedWifi/{{type ? 'open' : 'close'}}.png"/>
</view>
</view>
</view>
<view class="setWifi-btn">
<view bind:tap="onCancelPwd">取消</view>
<view bind:tap="onConfirmPwd">连接</view>
</view>
</view>
</block>
<block wx:else>
<!-- 顶部已连接设备卡 -->
<view class="deviceCard">
<view class="card-icon">
<image src="/images/device/{{deviceName}}.png" mode="aspectFit" style="width:100%;height:100%;"></image>
</view>
<view>
<view>{{deviceName}}</view>
<view>
<block wx:if="{{bleStatus === 'connected'}}">
<view class="status-dot status-dot--green"></view>
<text class="status-text status-text--green">已连接</text>
</block>
<block wx:elif="{{bleStatus === 'failed'}}">
<view class="status-dot status-dot--red"></view>
<text class="status-text status-text--red">连接断开</text>
</block>
<block wx:else>
<view class="status-dot status-dot--gray"></view>
<text class="status-text status-text--gray">连接中...</text>
</block>
</view>
</view>
</view>
<block wx:if="{{status === 'idle'}}">
<view class="wifiIdle">
查找周边WiFi
</view>
</block>
<block wx:if="{{status === 'searching'}}">
<view class="wifiSearching">
搜索中...
</view>
</block>
<block wx:if="{{status === 'list'}}">
<view class="wifiHeader">
<view>
<view>附近WiFi</view>
</view>
<view>网络列表(请选择一个网络进行连接)</view>
</view>
<scroll-view class="wifiList" scroll-y>
<block wx:for="{{wifiList}}" wx:key="ssid">
<block wx:if="{{item.connected}}">
<view class="wifiItem wifiItem-connected" data-ssid="{{item.ssid}}" bind:tap="onTapWifi">
<view>
<image src="/images/connectedWifi/wifi.png" mode="aspectFit"/>
</view>
<text>{{item.ssid}}</text>
<view>
<text>已连接</text>
<view class="arrow"></view>
</view>
</view>
</block>
<block wx:else>
<view class="wifiItem" data-ssid="{{item.ssid}}" bind:tap="onTapWifi">
<view>
<image src="/images/connectedWifi/wifi.png" mode="aspectFit"/>
</view>
<text>{{item.ssid}}</text>
<view class="arrow"></view>
</view>
</block>
</block>
</scroll-view>
<view class="wifiBtn">
<view class="{{hasConnected ? '' : 'wifiBtn-disabled'}}" bind:tap="onConfirm">确认</view>
</view>
</block>
<block wx:if="{{status === 'empty'}}">
<view class="wifiHeader">
<view>
<view>附近WiFi</view>
<view bind:tap="onRefresh">刷新</view>
</view>
<view>网络列表(请选择一个网络进行连接)</view>
</view>
<view class="wifiEmpty">
<view>
<image src="/images/connectedWifi/noData.png" mode="aspectFit"/>
</view>
<text>未查找到可用wifi,请重试</text>
</view>
</block>
</block>
</view>
-5
View File
@@ -1,5 +0,0 @@
{
"usingComponents": {
"select-member-drawer": "/components/selectMemberDrawer/selectMemberDrawer"
}
}
-304
View File
@@ -1,304 +0,0 @@
.scrollViewContent {
width: 100%;
padding: 24rpx 30rpx;
box-sizing: border-box;
.data-card {
width: 100%;
overflow: hidden;
zoom: 1;
position: relative;
padding: 40rpx 30rpx 30rpx;
box-sizing: border-box;
border-radius: 24rpx;
margin-bottom: 30rpx;
background-color: #FFFFFF;
&::before {
content: "";
position: absolute;
top: 2rpx;
left: 2rpx;
right: 2rpx;
height: 88rpx;
background: linear-gradient(180deg, #D3E9FF 0%, #FCFBFD 100%);
}
/* 用户信息行 */
.userInfo {
position: relative;
z-index: 2;
width: 100%;
display: flex;
align-items: center;
margin-bottom: 30rpx;
/* 头像色块(展示姓名首字) */
.avatar {
width: 90rpx;
height: 90rpx;
margin-right: 24rpx;
border-radius: 18rpx;
color: #FFFFFF;
font-size: 36rpx;
font-weight: bold;
line-height: 90rpx;
text-align: center;
background-color: #1385FA;
}
/* 姓名 + meta */
.info {
flex: 1;
.info-name-row {
height: 40rpx;
display: flex;
align-items: center;
margin-bottom: 10rpx;
.info-name {
color: #252535;
height: 40rpx;
font-size: 30rpx;
font-weight: bold;
line-height: 40rpx;
margin-right: 20rpx;
}
/* 本人 badge */
.badge-self {
color: #1385FA;
height: 40rpx;
padding: 0 18rpx;
font-size: 24rpx;
line-height: 40rpx;
border-radius: 8rpx;
background-color: rgba(19, 133, 250, 0.2);
}
}
.info-meta {
height: 26rpx;
display: flex;
align-items: center;
.meta-text {
color: #808080;
height: 26rpx;
font-size: 24rpx;
line-height: 26rpx;
}
.meta-divider {
color: #CCCCCC;
font-size: 24rpx;
line-height: 26rpx;
margin: 0 12rpx;
}
}
}
/* 切换用户按钮 */
.btn {
color: #1385FA;
width: 120rpx;
height: 48rpx;
font-size: 24rpx;
font-weight: 500;
line-height: 48rpx;
border-radius: 8rpx;
text-align: center;
border: 2rpx solid #1385FA;
}
}
.card-divider {
position: relative;
z-index: 2;
width: calc(100% + 60rpx);
left: -30rpx;
height: 1rpx;
margin-bottom: 30rpx;
border-bottom: 2rpx dashed #EAECF1;
&::before {
content: "";
position: absolute;
left: -15rpx;
top: 0;
bottom: 0;
margin: auto;
width: 30rpx;
height: 30rpx;
background-color: #F5F7FB;
border-radius: 50%;
}
&::after {
content: "";
position: absolute;
right: -15rpx;
top: 0;
bottom: 0;
margin: auto;
width: 30rpx;
height: 30rpx;
background-color: #F5F7FB;
border-radius: 50%;
}
}
/* 时间范围 Tab */
.nav {
width: 100%;
height: 72rpx;
padding: 4rpx;
box-sizing: border-box;
display: flex;
flex-wrap: nowrap;
background-color: #E8EBF2;
border-radius: 16rpx;
margin-bottom: 24rpx;
>view {
flex: 1;
height: 64rpx;
border-radius: 16rpx;
font-size: 28rpx;
color: #808080;
line-height: 64rpx;
text-align: center;
&.active {
color: #FFFFFF;
font-weight: bold;
background-color: #1385FA;
}
}
}
/* 统计摘要 4 格 */
.summary {
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
padding: 24rpx 0;
background-color: #F3F4F6;
border-radius: 16rpx;
.summary-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.summary-label {
color: #808080;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
margin-bottom: 20rpx;
}
.summary-value {
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
text {
color: #808080;
font-size: 24rpx;
font-weight: normal;
margin-left: 4rpx;
}
}
/* 体重下降(利好) */
.value-down {
color: #2AC79F;
}
/* 体重上升(警示) */
.value-up {
color: #FF4D4F;
}
}
}
}
/* 历史记录列表 */
.data-list {
width: 100%;
.list {
width: 100%;
margin-bottom: 30rpx;
>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;
}
}
}
}
}
/* ===== 空态 ===== */
.empty-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 160rpx;
.empty-icon {
width: 193rpx;
height: 138rpx;
margin-bottom: 40rpx;
image {
width: 100%;
height: 100%;
}
}
.empty-text {
color: #808080;
height: 30rpx;
font-size: 30rpx;
line-height: 30rpx;
}
}
}
-252
View File
@@ -1,252 +0,0 @@
import { get } from '../../utils/request/index'
/** 接口 type 参数:0=全部 1=近7天 2=近30天 3=近3月 */
type RangeType = 0 | 1 | 2 | 3
/** 时间范围 Tab 项 */
interface RangeTab {
key: RangeType
label: string
}
/** 缓存中的用户信息 */
interface StorageUserInfo {
userId: string
realname: string
sex: number
height: number
birthday: string
avatar: string | null
relationType: number | string | null
[key: string]: any
}
/** 页面展示用用户信息 */
interface PageUserInfo extends StorageUserInfo {
sex_dictText: '男' | '女' | ''
age: number
isSelf: boolean
}
/** GET /weighingScale/v3/getMeasureStatistics 返回结构 */
interface MeasureStatistics {
maxWeight: number
minWeight: number
avgWeight: number
weightChange: number
}
/** GET /weighingScale/v3/getMeasureHistoryPage records 单条 */
interface MeasureRecord {
recordId: string
examTime: string
weight: number
bmi: number
fatRate: number
}
/** 统计空态默认值 */
const EMPTY_STATS: MeasureStatistics = {
maxWeight: 0,
minWeight: 0,
avgWeight: 0,
weightChange: 0,
}
/** 从生日字符串(YYYY-MM-DD)计算周岁 */
const calcAge = (birthday: string): number => {
if (!birthday) return 0
const [year, month, day] = birthday.split('-').map(Number)
if (!year || !month || !day) return 0
const now = new Date()
let age = now.getFullYear() - year
if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1
return Math.max(0, age)
}
Page({
data: {
activeType: 1 as RangeType,
rangeTabs: [
{ key: 1, label: '近7天' },
{ key: 2, label: '近30天' },
{ key: 3, label: '近3月' },
] as RangeTab[],
userInfo: null as PageUserInfo | null,
stats: EMPTY_STATS as MeasureStatistics,
records: [] as MeasureRecord[],
pageNo: 1,
total: 0,
hasMore: false,
refresherTriggered: false,
showSelectDrawer: false,
},
onLoad() {
const raw = wx.getStorageSync('userInfo') as StorageUserInfo | null
if (!raw?.userId) return
const userInfo: PageUserInfo = {
...raw,
sex_dictText: raw.sex === 1 ? '男' : raw.sex === 2 ? '女' : '',
age: calcAge(raw.birthday),
isSelf: true,
}
this.setData({ userInfo })
this.loadData()
},
/** 重置分页,并行拉统计 + 第一页列表 */
loadData() {
const userInfo = this.data.userInfo
if (!userInfo?.userId) return
this.setData({ status: 'loading', pageNo: 1, records: [] })
const statsReq = get<MeasureStatistics>('weighingScale/v3/getMeasureStatistics', {
userId: userInfo.userId,
type: this.data.activeType,
}, { loading: false })
const pageReq = get('weighingScale/v3/getMeasureHistoryPage', {
userId: userInfo.userId,
type: this.data.activeType,
pageNo: 1,
pageSize: 10,
}, { loading: false })
wx.showLoading({ title: '加载中...', mask: true })
Promise.all([statsReq, pageReq])
.then(([statsRes, pageRes]: any[]) => {
const rawStats = statsRes.result
const stats: MeasureStatistics = rawStats ? {
maxWeight: rawStats.maxWeight ?? 0,
minWeight: rawStats.minWeight ?? 0,
avgWeight: rawStats.avgWeight ?? 0,
weightChange: rawStats.weightChange ?? 0,
} : EMPTY_STATS
const page = pageRes.result ?? {}
const records: MeasureRecord[] = (page.records ?? []).map((r: any) => ({
recordId: r.recordId ?? '',
examTime: r.examTime ?? '',
weight: r.weight ?? 0,
bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0,
}))
const total = page.total ?? 0
this.setData({
stats,
records,
total,
hasMore: records.length < total,
})
})
.finally(() => {
wx.hideLoading()
this.setData({ refresherTriggered: false })
})
},
/** scroll-view 触底,加载下一页 */
onScrollToLower() {
if (!this.data.hasMore) return
const userInfo = this.data.userInfo
if (!userInfo?.userId) return
const nextPage = this.data.pageNo + 1
wx.showLoading({ title: '加载中...', mask: true })
get('weighingScale/v3/getMeasureHistoryPage', {
userId: userInfo.userId,
type: this.data.activeType,
pageNo: nextPage,
pageSize: 10,
}, { loading: false })
.then((res: any) => {
const page = res.result ?? {}
const newRecords: MeasureRecord[] = (page.records ?? []).map((r: any) => ({
recordId: r.recordId ?? '',
examTime: r.examTime ?? '',
weight: r.weight ?? 0,
bmi: r.bmi ?? 0,
fatRate: r.fatRate ?? 0,
}))
const allRecords = [...this.data.records, ...newRecords]
this.setData({
records: allRecords,
pageNo: nextPage,
hasMore: allRecords.length < this.data.total,
})
})
.finally(() => {
wx.hideLoading()
})
},
/** scroll-view 下拉刷新 */
onRefresh() {
this.setData({ refresherTriggered: true })
this.loadData()
},
/** 切换时间范围 Tab */
onTapRange(e: WechatMiniprogram.TouchEvent) {
const key = Number(e.currentTarget.dataset.key) as RangeType
if (key === this.data.activeType) return
this.setData({ activeType: key })
this.loadData()
},
/** 点击记录行,携带 recordId + userId 跳转测量报告 */
onTapRecord(e: WechatMiniprogram.TouchEvent) {
const recordId = e.currentTarget.dataset.recordId as string
const userId = this.data.userInfo?.userId ?? ''
wx.navigateTo({
url: `/pages/measurementReport/measurementReport?recordId=${recordId}&userId=${userId}`,
})
},
/** 打开切换用户抽屉 */
onTapSwitchMember() {
this.setData({ showSelectDrawer: true })
},
/** 抽屉关闭 */
onCloseDrawer() {
this.setData({ showSelectDrawer: false })
},
/** 选中用户后更新 userInfo 并重新请求数据 */
onSelectMember(e: WechatMiniprogram.CustomEvent) {
const m = e.detail as {
userId?: string
id: string
name: string
gender: '男' | '女'
age: number
height: number
sex?: number
birthday?: string
relationType?: number | string | null
avatar?: string
}
const storedUserId = (wx.getStorageSync('userInfo') as StorageUserInfo | null)?.userId ?? ''
const userInfo: PageUserInfo = {
id: m.id,
userId: m.userId ?? '',
realname: m.name,
sex: m.sex ?? (m.gender === '男' ? 1 : 2),
height: m.height,
birthday: m.birthday ?? '',
avatar: m.avatar ?? null,
relationType: m.relationType ?? null,
sex_dictText: m.gender,
age: m.age,
isSelf: m.userId === storedUserId,
}
this.setData({ showSelectDrawer: false, userInfo })
this.loadData()
},
})
-106
View File
@@ -1,106 +0,0 @@
<scroll-view
class="scrollView"
scroll-y
refresher-enabled
refresher-triggered="{{ refresherTriggered }}"
bind:refresherrefresh="onRefresh"
bind:scrolltolower="onScrollToLower"
>
<view class="scrollViewContent">
<!-- 数据卡(用户信息 + 时间 Tab + 统计) -->
<view class="data-card">
<!-- 用户信息 -->
<view class="userInfo">
<view class="avatar">{{ userInfo.realname[0] }}</view>
<view class="info">
<view class="info-name-row">
<view class="info-name">{{ userInfo.realname }}</view>
<block wx:if="{{ userInfo.isSelf }}">
<view class="badge-self">本人</view>
</block>
</view>
<view class="info-meta">
<view class="meta-text">{{ userInfo.sex_dictText }}</view>
<view class="meta-divider">·</view>
<view class="meta-text">{{ userInfo.age }}岁</view>
<view class="meta-divider">|</view>
<view class="meta-text">{{ userInfo.height }}cm</view>
</view>
</view>
<view class="btn" bind:tap="onTapSwitchMember">切换用户</view>
</view>
<view class="card-divider"></view>
<!-- 时间范围 Tab -->
<view class="nav">
<block wx:for="{{ rangeTabs }}" wx:key="key">
<view
class="{{ activeType === item.key ? 'active' : '' }}"
data-key="{{ item.key }}"
bind:tap="onTapRange"
>{{ item.label }}</view>
</block>
</view>
<!-- 统计摘要(4格) -->
<view class="summary">
<view class="summary-item">
<view class="summary-label">最高体重</view>
<view class="summary-value">{{ stats.maxWeight }}<text>kg</text></view>
</view>
<view class="summary-item">
<view class="summary-label">最低体重</view>
<view class="summary-value">{{ stats.minWeight }}<text>kg</text></view>
</view>
<view class="summary-item">
<view class="summary-label">平均体重</view>
<view class="summary-value">{{ stats.avgWeight }}<text>kg</text></view>
</view>
<view class="summary-item">
<view class="summary-label">体重变化</view>
<view class="summary-value {{ stats.weightChange < 0 ? 'value-down' : 'value-up' }}">
{{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange }}<text>kg</text>
</view>
</view>
</view>
</view>
<!-- 有数据:记录列表 -->
<block wx:if="{{ records.length }}">
<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="arrow">
<view>体重:<text>{{ item.weight }}kg</text></view>
<view>BMI<text>{{ item.bmi }}</text></view>
<view>体脂率:<text>{{ item.fatRate }}%</text></view>
</view>
</view>
</block>
</view>
</block>
<!-- 无数据:占位 -->
<block wx:else>
<view class="empty-state">
<view class="empty-icon">
<image src="/images/data/noData.png" mode="aspectFit"/>
</view>
<view class="empty-text">暂无称重数据</view>
</view>
</block>
</view>
</scroll-view>
<select-member-drawer
show="{{ showSelectDrawer }}"
source="history"
exclude-user-id="{{ userInfo.userId }}"
bind:select="onSelectMember"
bind:close="onCloseDrawer"
/>
@@ -1,6 +0,0 @@
{
"navigationBarTitleText": "编辑用户",
"usingComponents": {
"user-info-confirm-modal": "/components/userInfoConfirmModal/userInfoConfirmModal"
}
}
@@ -1,249 +0,0 @@
.edit-member {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
/* 顶部浅蓝提示条 */
.tips {
width: 100%;
height: 56rpx;
display: flex;
margin-bottom: 24rpx;
padding-left: 30rpx;
align-items: center;
box-sizing: border-box;
background-color: rgba(19, 133, 250, 0.2);
.tips-icon {
color: #1385FA;
width: 28rpx;
height: 28rpx;
font-size: 20rpx;
font-weight: bold;
margin-right: 10rpx;
line-height: 24rpx;
text-align: center;
border-radius: 50%;
box-sizing: border-box;
border: 1rpx solid #1385FA;
}
.tips-text {
color: #1385FA;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
.form {
width: 100%;
flex: 1;
overflow: hidden;
.scrollViewContent {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
}
/* 表单卡片 */
.form-card {
width: 100%;
padding: 40rpx 30rpx;
border-radius: 16rpx;
box-sizing: border-box;
margin-bottom: 40rpx;
background-color: #FFFFFF;
.form-row {
width: 100%;
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
width: 100%;
height: 40rpx;
display: flex;
flex-wrap: nowrap;
margin-bottom: 18rpx;
.label-text {
color: #252535;
height: 40rpx;
font-size: 28rpx;
line-height: 40rpx;
font-weight: bold;
margin-right: 6rpx;
}
.label-star {
color: #FF4D4F;
height: 40rpx;
font-weight: 500;
font-size: 28rpx;
line-height: 40rpx;
}
}
.form-control {
width: 100%;
}
/* 可输入区域 */
.form-input {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
color: #252535;
font-size: 26rpx;
border-radius: 16rpx;
box-sizing: border-box;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.form-placeholder {
color: #B6B6B6;
font-size: 26rpx;
}
/* 日期选择器 */
.form-picker {
width: 100%;
height: 88rpx;
padding: 0 30rpx;
color: #252535;
font-size: 26rpx;
line-height: 88rpx;
border-radius: 16rpx;
box-sizing: border-box;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.form-picker-placeholder {
color: #B6B6B6;
}
/* 输入框 + 后缀容器(身高 cm / 体重 kg) */
.form-input-wrap {
position: relative;
width: 100%;
.form-input-with-suffix {
padding-right: 80rpx;
}
.form-suffix-inside {
position: absolute;
right: 30rpx;
top: 0;
height: 88rpx;
color: #77849E;
font-size: 26rpx;
line-height: 88rpx;
}
}
/* 只读区域(单位 / 性别 / 年龄 / 出生年月) */
.form-readonly {
width: 100%;
height: 88rpx;
box-sizing: border-box;
padding: 0 24rpx;
background: #F6F6F6;
border-radius: 16rpx;
display: flex;
align-items: center;
border: 2rpx solid #E6E6EA;
.readonly-text {
color: #252535;
font-size: 26rpx;
}
}
.form-readonly-with-suffix {
justify-content: space-between;
.form-suffix {
color: #77849E;
height: 88rpx;
font-size: 26rpx;
line-height: 88rpx;
}
}
/* 性别切换按钮(家庭用户) */
.gender-toggle {
display: flex;
gap: 24rpx;
.gender-option {
width: 300rpx;
height: 88rpx;
font-size: 28rpx;
text-align: center;
line-height: 88rpx;
border-radius: 16rpx;
box-sizing: border-box;
color: #77849E;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
}
.gender-option-active {
color: #FFFFFF;
font-weight: bold;
background-color: #1385FA;
border-color: #1385FA;
}
}
}
}
/* 底部双按钮 */
.actions {
width: 100%;
height: 120rpx;
padding: 0 55rpx;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
background-color: #FFFFFF;
box-shadow: 0 2rpx 8rpx 0 rgba(19, 133, 250, 0.06);
.btn-cancel {
color: #1385FA;
width: 300rpx;
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;
width: 300rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
line-height: 88rpx;
border-radius: 44rpx;
background-color: #1385FA;
}
}
}
-382
View File
@@ -1,382 +0,0 @@
import { get, put } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
/** GET /weighingScale/v3/getUserInfoByIdcard 返回的 result 结构 */
interface UserInfoFromServer {
id: string | null
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
userType: string | null
idCard: string
height: number
weight: number
birthday: string
sex: number
avatar: string | null
phone: string
thirdId: string | null
realname: string
bmi: number
workNo: string
remark: string | null
relationType: string | null
sn: string
delFlag: number
sex_dictText: string
}
/** 表单数据模型 */
interface MemberForm {
name: string
idCard: string
gender: string
age: string
height: string
weight: string
/** 1=男 2=女 */
sex: number
}
/** 身份证解析结果 */
interface IdCardParsed {
gender: string
age: string
sex: number
birthday: string
}
/**
* 编辑用户页
* 根据用户 dataType 区分非家庭/家庭两种编辑模式
*/
Page({
data: {
userId: '',
/** dataType === 1 为家庭用户 */
isFamily: false,
/** 单位(非家庭用户) */
unitDisplay: '',
/** 出生年月 YYYY-MM-DD(家庭用户) */
birthday: '',
/** 接口返回的原始用户数据,提交时以此为 base 覆盖修改字段 */
rawUser: null as Record<string, any> | null,
/** 正在请求身份证查询接口 */
querying: false,
/** 是否已完成一次身份证查询 */
hasQueried: false,
/** 接口返回的用户信息 */
serverResult: null as UserInfoFromServer | null,
/** 表单只读状态(code=101 时置为 true */
readonly: false,
/** 是否显示确认弹框 */
showConfirmModal: false,
form: {
name: '',
idCard: '',
gender: '',
age: '',
height: '',
weight: '',
sex: 0,
} as MemberForm,
},
onLoad(options: Record<string, string>) {
const { userId } = options
if (!userId) {
wx.showToast({ title: '缺少用户信息', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
this.setData({ userId })
get<Record<string, any>>('weighingScale/v3/select/userinfo/byId', { id: userId }, { loading: true })
.then((res: any) => {
const u = res.result
if (!u) return
const isFamily = u.dataType === 1
const sex = u.sex ?? 0
const gender = sex === 1 ? '男' : sex === 2 ? '女' : ''
const age = this.calcAge(u.birthday ?? '')
this.setData({
rawUser: u,
isFamily,
unitDisplay: [u.secondDepart, u.thirdDepart].filter(Boolean).join(' / '),
birthday: u.birthday ?? '',
'form.name': u.realname ?? '',
'form.idCard': u.idCard ?? '',
'form.gender': gender,
'form.age': String(age),
'form.height': String(u.height ?? ''),
'form.weight': String(u.weight ?? ''),
'form.sex': sex,
})
})
.catch(() => {
wx.showToast({ title: '加载用户信息失败', icon: 'none' })
})
},
/** 从生日字符串计算周岁 */
calcAge(birthday: string): number {
if (!birthday) return 0
const parts = birthday.split('-').map(Number)
const [year, month, day] = parts
if (!year || !month || !day) return 0
const now = new Date()
let age = now.getFullYear() - year
const nowMonth = now.getMonth() + 1
const nowDay = now.getDate()
if (nowMonth < month || (nowMonth === month && nowDay < day)) age -= 1
return Math.max(0, age)
},
onNameInput(e: WechatMiniprogram.Input) {
const name = e.detail.value
this.setData({
'form.name': name,
serverResult: null,
hasQueried: false,
readonly: false
})
if (this.data.isFamily && name.trim() && this.isIdCardValid(this.data.form.idCard)) {
this.fetchUserInfoByIdCard(name.trim(), this.data.form.idCard)
}
},
onIdCardInput(e: WechatMiniprogram.Input) {
const idCard = (e.detail.value || '').trim().toUpperCase()
if (!this.data.isFamily) {
this.setData({ 'form.idCard': idCard })
return
}
if (this.isIdCardValid(idCard)) {
const parsed = this.parseIdCard(idCard)
this.setData({
'form.idCard': idCard,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: parsed.birthday,
})
} else {
this.setData({
'form.idCard': idCard,
'form.gender': '',
'form.age': '',
'form.sex': 0,
birthday: '',
})
}
},
/** 家庭用户性别切换 */
onGenderTap(e: WechatMiniprogram.TouchEvent) {
const value = e.currentTarget.dataset.value as string
const sex = value === '男' ? 1 : 2
this.setData({
'form.gender': value,
'form.sex': sex,
})
},
onHeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.height': e.detail.value })
},
onWeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.weight': e.detail.value })
},
isIdCardValid(idCard: string): boolean {
return /^\d{17}[\dX]$/.test(idCard)
},
parseIdCard(idCard: string): IdCardParsed {
const empty: IdCardParsed = { gender: '', age: '', sex: 0, birthday: '' }
if (!this.isIdCardValid(idCard)) return empty
const genderCode = parseInt(idCard.charAt(16), 10)
const sex = genderCode % 2 === 1 ? 1 : 2
const gender = sex === 1 ? '男' : '女'
const year = parseInt(idCard.substr(6, 4), 10)
const month = parseInt(idCard.substr(10, 2), 10)
const day = parseInt(idCard.substr(12, 2), 10)
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return empty
const now = new Date()
let age = now.getFullYear() - year
if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1
if (age < 0 || age > 150) return empty
const mm = String(month).padStart(2, '0')
const dd = String(day).padStart(2, '0')
return { gender, age: String(age), sex, birthday: `${year}-${mm}-${dd}` }
},
fetchUserInfoByIdCard(name: string, idCard: string) {
if (this.data.querying) return
this.setData({ querying: true, serverResult: null, hasQueried: false })
get<any>('weighingScale/v3/getUserInfoByIdcard', {
sn: lefuService.deviceInfo?.serialNumber ?? '',
idCard,
realname: name
}, { loading: true })
.then(res => {
const serverResult = res.result
if (!serverResult) {
wx.showToast({ title: res.message || '未查询到用户信息', icon: 'none' })
const parsed = this.parseIdCard(idCard)
this.setData({
hasQueried: true,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: parsed.birthday,
})
return
}
if (serverResult.flag == 101) {
const parsed = this.parseIdCard(serverResult.idCard)
this.setData({
serverResult,
hasQueried: true,
readonly: true,
showConfirmModal: true,
'form.name': serverResult.realname ?? '',
'form.idCard': serverResult.idCard ?? '',
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
'form.height': serverResult.height ? String(serverResult.height) : '',
'form.weight': serverResult.weight ? String(serverResult.weight) : '',
birthday: serverResult.birthday || parsed.birthday
})
return
}
if (serverResult.flag === null) {
const parsed = this.parseIdCard(serverResult.idCard)
const data: Record<string, any> = {
serverResult,
hasQueried: true,
'form.gender': parsed.gender,
'form.age': parsed.age,
'form.sex': parsed.sex,
birthday: serverResult.birthday || parsed.birthday
}
if (serverResult.height) {
data['form.height'] = String(serverResult.height)
}
if (serverResult.weight) {
data['form.weight'] = String(serverResult.weight)
}
this.setData(data)
}
})
.catch(() => {})
.finally(() => {
this.setData({ querying: false })
})
},
onConfirmModalConfirm() {
this.setData({ showConfirmModal: false })
this.onSubmit()
},
onConfirmModalCancel() {
this.setData({
showConfirmModal: false,
readonly: false,
'form.name': '',
'form.idCard': '',
'form.gender': '',
'form.age': '',
'form.height': '',
'form.weight': '',
serverResult: null,
hasQueried: false,
})
},
/** 出生年月变更,同步更新年龄 */
onBirthdayChange(e: WechatMiniprogram.PickerChange) {
const birthday = e.detail.value as string
const age = this.calcAge(birthday)
this.setData({
birthday,
'form.age': String(age),
})
},
onCancel() {
wx.navigateBack()
},
onSubmit() {
const { name, idCard, gender, height, weight, sex } = this.data.form
const { isFamily, rawUser } = this.data
const heightNum = parseFloat(height)
const weightNum = parseFloat(weight)
// 昵称(仅家庭用户必填)
if (isFamily && !name.trim()) {
wx.showToast({ title: '请填写昵称', icon: 'none' })
return
}
// 家庭用户性别必选
if (isFamily && !gender) {
wx.showToast({ title: '请选择性别', icon: 'none' })
return
}
// 家庭用户出生年月必填
if (isFamily && !this.data.birthday.trim()) {
wx.showToast({ title: '请填写出生年月', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
return
}
const connectDeviceInfoRaw = wx.getStorageSync('connectDeviceInfo')
// 以接口原始返回 + 身份证查询结果为 base,覆盖编辑后的字段
const serverResult = this.data.serverResult
const payload: Record<string, any> = {
...(rawUser ?? {}),
...(serverResult ?? {}),
id: this.data.userId,
realname: name.trim(),
idCard,
sex,
birthday: this.data.birthday,
height: heightNum,
weight: isNaN(weightNum) ? 0 : weightNum,
connectDeviceInfo: connectDeviceInfoRaw ? JSON.stringify(connectDeviceInfoRaw) : (rawUser?.connectDeviceInfo ?? ''),
}
put('weighingScale/v5/edit/user', payload)
.then(() => {
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2] as any
if (prevPage && prevPage.data?.sn) {
prevPage._loadData(prevPage.data.sn)
}
wx.navigateBack()
}, 1500)
})
.catch(() => {
wx.showToast({ title: '保存失败,请重试', icon: 'none' })
})
}
})
@@ -1,190 +0,0 @@
<view class="edit-member">
<!-- 顶部浅蓝提示条 -->
<view class="tips">
<view class="tips-icon">!</view>
<text class="tips-text">最多支持 10 位用户</text>
</view>
<view class="form">
<scroll-view scroll-y class="scrollView">
<view class="scrollViewContent">
<view class="form-card">
<!-- ========== 非家庭用户(员工/本人) ========== -->
<block wx:if="{{ !isFamily }}">
<!-- 姓名 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">姓名</text>
</view>
<view class="form-control">
<view class="form-readonly">
<text class="readonly-text">{{ form.name }}</text>
</view>
</view>
</view>
<!-- 单位 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">单位</text>
</view>
<view class="form-control">
<view class="form-readonly">
<text class="readonly-text">{{ unitDisplay }}</text>
</view>
</view>
</view>
<!-- 性别 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">性别</text>
</view>
<view class="form-control">
<view class="form-readonly">
<text class="readonly-text">{{ form.gender }}</text>
</view>
</view>
</view>
<!-- 年龄 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">年龄</text>
</view>
<view class="form-control">
<view class="form-readonly form-readonly-with-suffix">
<text class="readonly-text">{{ form.age }}</text>
<text class="form-suffix">岁</text>
</view>
</view>
</view>
<!-- 身高 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">身高</text>
<text class="label-star">*</text>
</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" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<!-- 体重 -->
<view class="form-row">
<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" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</block>
<!-- ========== 家庭用户 ========== -->
<block wx:else>
<!-- 昵称 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">昵称</text>
<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" />
</view>
</view>
<!-- 身份证号码 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">身份证号码</text>
</view>
<view class="form-control">
<input class="form-input" type="idcard" maxlength="18" placeholder="请填写身份证号" placeholder-class="form-placeholder" value="{{ form.idCard }}" disabled="{{ readonly }}" bind:input="onIdCardInput" />
</view>
</view>
<!-- 性别(左右切换) -->
<view class="form-row">
<view class="form-label">
<text class="label-text">性别</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<view class="gender-toggle">
<view class="gender-option {{ form.gender === '男' ? 'gender-option-active' : '' }}" data-value="男" bind:tap="onGenderTap">男</view>
<view class="gender-option {{ form.gender === '女' ? 'gender-option-active' : '' }}" data-value="女" bind:tap="onGenderTap">女</view>
</view>
</view>
</view>
<!-- 出生年月 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">出生年月</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<picker mode="date" value="{{ birthday }}" bindchange="onBirthdayChange">
<view class="form-picker {{ birthday ? '' : 'form-picker-placeholder' }}">
{{ birthday || '请选择出生年月' }}
</view>
</picker>
</view>
</view>
<!-- 身高 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">身高</text>
<text class="label-star">*</text>
</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" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<!-- 体重 -->
<view class="form-row">
<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" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</block>
</view>
</view>
</scroll-view>
</view>
<!-- 底部双按钮:取消 / 保存 -->
<view class="actions">
<view class="btn-cancel" bind:tap="onCancel">取消</view>
<view class="btn-primary" bind:tap="onSubmit">保存用户信息</view>
</view>
<!-- 确认弹框 -->
<user-info-confirm-modal show="{{ showConfirmModal }}" userInfo="{{ serverResult }}" bind:confirm="onConfirmModalConfirm" bind:cancel="onConfirmModalCancel">
</user-info-confirm-modal>
</view>
@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "设备管理",
"usingComponents": {}
}
-196
View File
@@ -1,196 +0,0 @@
/* 设备管理页 —— 已配对设备列表 + 添加设备主按钮 */
.equipment {
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
/* 副标题:已配对设备 */
.section-title {
color: #808080;
height: 80rpx;
font-size: 26rpx;
padding: 0 30rpx;
line-height: 80rpx;
flex-shrink: 0;
}
/* 卡片列表(scroll-view */
.device-list {
flex: 1;
overflow: hidden;
width: 100%;
.scroll-view-content {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
.device-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-sizing: border-box;
margin-bottom: 24rpx;
// margin: 0 30rpx 24rpx;
&:last-child {
margin-bottom: 30rpx;
}
/* 上半:图标 + 设备名 + 状态 */
.card-top {
width: 100%;
display: flex;
align-items: center;
.card-icon {
width: 90rpx;
height: 90rpx;
border-radius: 24rpx;
margin-right: 24rpx;
}
.card-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.card-name {
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
margin-bottom: 16rpx;
}
.card-status {
display: flex;
align-items: center;
.card-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 14rpx;
}
.card-dot-on {
background-color: #2AC79F;
}
.card-dot-off {
background-color: #F24439;
}
.card-status-text {
height: 28rpx;
font-size: 28rpx;
font-weight: 500;
line-height: 28rpx;
}
.card-status-on {
color: #2AC79F;
}
.card-status-off {
color: #F24439;
}
}
}
}
/* 分隔线 */
.card-divider {
width: 100%;
height: 2rpx;
background-color: #EAECF1;
border-radius: 1rpx;
margin: 30rpx 0 30rpx;
}
/* 操作按钮区:单/双形态共用 */
.card-actions {
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
/* 单按钮:已连接态 */
&.card-actions-single {
.btn-ghost-full {
width: 100%;
height: 82rpx;
line-height: 78rpx;
text-align: center;
color: #77849E;
font-size: 28rpx;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
border-radius: 39rpx;
box-sizing: border-box;
}
}
/* 双按钮:未连接态 */
&.card-actions-double {
justify-content: space-between;
.btn-ghost-half {
width: 280rpx;
height: 82rpx;
line-height: 78rpx;
text-align: center;
color: #77849E;
font-size: 28rpx;
background-color: #FFFFFF;
border: 2rpx solid #E6E6EA;
border-radius: 39rpx;
box-sizing: border-box;
}
}
}
}
}
}
/* 底部「添加设备」主按钮 */
.add-btn {
width: 580rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #1385FA;
border-radius: 44rpx;
margin: 20rpx auto;
flex-shrink: 0;
.add-btn-icon {
color: #FFFFFF;
width: 32rpx;
font-size: 28rpx;
font-weight: bold;
height: 32rpx;
line-height: 30rpx;
border-radius: 50%;
text-align: center;
margin-right: 15rpx;
border: 2rpx solid #ffffff;
}
.add-btn-text {
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
}
}
}
-199
View File
@@ -1,199 +0,0 @@
import { get } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
import type { RawDevice } from '../../lefu/types'
/** API 返回的设备记录 */
interface DeviceRecord {
id: string
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
userType: number
idCard: string
height: number
weight: number
birthday: string
sex: number
avatar: string | null
phone: string | null
thirdId: string
realname: string
bmi: number
createBy: string
createTime: string
updateBy: string
updateTime: string
delFlag: number
sn: string | null
workNo: string
remark: string | null
relationType: string | null
sex_dictText: string
/** 设备连接信息 JSON 字符串,与登录接口返回格式一致 */
connectDeviceInfo: string
}
/** 展示用设备项 */
interface DeviceItem {
id: string
scaleDeviceId: string
scaleDeviceName: string
connectDeviceInfo: string
realname: string
/** 蓝牙连接状态,接口不返回,由业务层更新 */
connected: boolean
userId: string
sex: number
height: number
birthday: string
avatar: string | null
relationType: string | null
sex_dictText: string
}
Page({
data: {
loading: false,
deviceList: [] as DeviceItem[],
},
onShow() {
this.loadData()
},
onUnload() {
lefuService.stopScan()
lefuService.onDeviceConnect(() => {})
lefuService.onDeviceInfo(() => {})
lefuService.onDisconnected(() => {})
},
/** 拉取已绑定设备列表,加载完成后检查缓存自动连接 */
loadData() {
this.setData({ loading: true })
const userInfo = wx.getStorageSync('userInfo')
get<DeviceRecord[]>('weighingScale/v3/list/device', { userId: userInfo?.userId })
.then(res => {
const list: DeviceItem[] = res.result.map(item => {
let scaleDeviceName = ''
try {
const info = JSON.parse(item.connectDeviceInfo || '{}')
scaleDeviceName = info.name ?? ''
} catch {}
return {
id: item.id,
scaleDeviceId: item.scaleDeviceId,
scaleDeviceName,
connectDeviceInfo: item.connectDeviceInfo ?? '',
realname: item.realname,
connected: false,
userId: item.userId,
sex: item.sex,
height: item.height,
birthday: item.birthday,
avatar: item.avatar,
relationType: item.relationType,
sex_dictText: item.sex_dictText,
}
})
this.setData({ deviceList: list }, () => {
this._checkAndAutoConnect()
})
})
.finally(() => {
this.setData({ loading: false })
})
},
/** 每次进入页面都走真实扫描连接,确保状态最新 */
_checkAndAutoConnect() {
lefuService.onDeviceInfo((info) => {
this._markConnected(info.serialNumber as string)
})
lefuService.onDisconnected(() => {
this._markAllDisconnected()
})
lefuService.autoConnect()
},
/** 将 serialNumber 对应的列表项标记为已连接,其余置为未连接 */
_markConnected(serialNumber: string) {
const list = this.data.deviceList.map(item => ({
...item,
connected: item.scaleDeviceId === serialNumber,
}))
this.setData({ deviceList: list })
},
/** 将所有设备标记为未连接 */
_markAllDisconnected() {
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
this.setData({ deviceList: list })
},
/** 点击「连接」:扫描周边设备,找到 deviceId 匹配的设备后连接 */
onTapConnect(e: WechatMiniprogram.TouchEvent) {
const userInfo = e.currentTarget.dataset.userInfo as DeviceItem
const connectDeviceInfoStr = e.currentTarget.dataset.connectDeviceInfo as string
let cachedDevice: RawDevice | null = null
try {
cachedDevice = JSON.parse(connectDeviceInfoStr || '{}')
} catch {}
const targetDeviceId = cachedDevice?.deviceId as string | undefined
if (!targetDeviceId) return
if (lefuService.isConnected) lefuService.disconnect()
this.setData({ deviceList: this.data.deviceList.map(item => ({ ...item, connected: false })) })
let scanTimer: number | null = null
wx.showLoading({ title: '扫描中...', mask: true })
wx.openBluetoothAdapter({
success: () => {
scanTimer = setTimeout(() => {
lefuService.stopScan()
wx.hideLoading()
wx.showToast({ title: '未找到设备', icon: 'none' })
}, 15000)
lefuService.onDevicesList((devices) => {
const matched = devices.find(d => d.raw.deviceId === targetDeviceId)
if (!matched) return
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
lefuService.stopScan()
wx.showLoading({ title: '连接中...', mask: true })
lefuService.connect(matched.raw)
lefuService.onDeviceConnect(() => {
wx.hideLoading()
wx.setStorageSync('connectDeviceInfo', matched.raw)
// 连接成功后缓存当前设备关联的用户信息
if (userInfo) {
wx.setStorageSync('userInfo', userInfo)
}
})
lefuService.onDeviceInfo((info) => {
this._markConnected(info.serialNumber as string)
})
})
lefuService.startScan()
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '蓝牙未开启', icon: 'none' })
},
})
},
/* 点击「用户管理」→ 跳转用户管理页,sn 由目标页从缓存读取 */
onTapMember() {
wx.navigateTo({ url: '/pages/equipmentMember/equipmentMember' })
},
/* 点击「添加设备」→ 跳转设备搜索页 */
onTapAdd() {
wx.navigateTo({ url: '/pages/connectedDevice/connectedDevice' })
},
})
@@ -1,61 +0,0 @@
<view class="equipment">
<!-- 副标题:已配对设备 -->
<view class="section-title">已配对设备</view>
<!-- 设备卡片列表 -->
<view class="device-list">
<scroll-view class="scrollView" scroll-y>
<view class="scroll-view-content">
<block wx:for="{{ deviceList }}" wx:key="id">
<view class="device-card">
<!-- 上半:图标 + 设备信息 + 状态(色点 + 文字) -->
<view class="card-top">
<image class="card-icon" src="/images/device/{{ item.scaleDeviceName }}.png" mode="aspectFit"></image>
<view class="card-info">
<view class="card-name">{{ item.scaleDeviceName }}</view>
<view class="card-status">
<!-- 已连接:绿点 + "已连接" -->
<block wx:if="{{ item.connected }}">
<view class="card-dot card-dot-on"></view>
<text class="card-status-text card-status-on">已连接</text>
</block>
<!-- 未连接:红点 + "未连接" -->
<block wx:else>
<view class="card-dot card-dot-off"></view>
<text class="card-status-text card-status-off">未连接</text>
</block>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="card-divider"></view>
<!-- 下半:操作按钮(单/双形态) -->
<block wx:if="{{ item.connected }}">
<!-- 已连接:单按钮居中 -->
<view class="card-actions card-actions-single">
<view class="btn-ghost-full" bind:tap="onTapMember">用户管理</view>
</view>
</block>
<block wx:else>
<!-- 未连接:双按钮 连接 / 用户管理 -->
<view class="card-actions card-actions-single">
<view class="btn-ghost-full" data-connect-device-info="{{ item.connectDeviceInfo }}" data-user-info="{{ item }}" bind:tap="onTapConnect">连接</view>
</view>
</block>
</view>
</block>
</view>
</scroll-view>
</view>
<!-- 底部「添加设备」主按钮 -->
<view class="add-btn" bind:tap="onTapAdd">
<view class="add-btn-icon">+</view>
<text class="add-btn-text">添加设备</text>
</view>
</view>
@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "设备用户",
"usingComponents": {}
}
@@ -1,405 +0,0 @@
.equipmentMember {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
/* 顶部浅红提示条(未连接态) */
.tips {
width: 100%;
height: 56rpx;
display: flex;
padding-left: 30rpx;
align-items: center;
flex-shrink: 0;
margin-bottom: 24rpx;
box-sizing: border-box;
background-color: rgba(255, 77, 79, 0.12);
.tips-icon {
color: #FF4D4F;
width: 28rpx;
height: 28rpx;
font-size: 20rpx;
font-weight: bold;
margin-right: 10rpx;
line-height: 24rpx;
text-align: center;
border-radius: 50%;
box-sizing: border-box;
border: 1rpx solid #FF4D4F;
}
.tips-text {
color: #FF4D4F;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
.seaction {
width: 100%;
flex: 1;
overflow: hidden;
display: flex;
padding-top: 24rpx;
flex-direction: column;
margin-bottom: 24rpx;
/* 设备卡(大卡:左插画 + 右信息) */
.device-card {
width: 100%;
display: flex;
align-items: center;
padding: 30rpx;
border-radius: 20rpx;
box-sizing: border-box;
margin-bottom: 24rpx;
background-color: #FFFFFF;
.device-illustration {
width: 90rpx;
height: 90rpx;
border-radius: 24rpx;
margin-right: 24rpx;
background-color: rgba(19, 133, 250, 0.12);
}
.device-info {
flex: 1;
height: auto;
.device-name {
color: #252535;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
margin-bottom: 10rpx;
}
.device-status {
height: 28rpx;
display: flex;
align-items: center;
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 10rpx;
}
.status-dot-on {
background-color: #2AC79F;
}
.status-dot-off {
background-color: #B6B6B6;
}
.status-text {
color: #77849E;
height: 28rpx;
font-size: 28rpx;
line-height: 28rpx;
}
}
}
}
.scrollViewContent {
box-sizing: border-box;
/* 用户卡(容量小标题 + 列表整体白卡) */
.member-card {
width: 100%;
flex: 1;
box-sizing: border-box;
overflow: hidden;
margin-bottom: 20rpx;
/* 容量小标题 */
.member-card-header {
width: 100%;
height: 88rpx;
display: flex;
padding: 0 30rpx;
box-sizing: border-box;
border-radius: 20rpx 20rpx 0 0;
background-color: #FFFFFF;
border-bottom: 1rpx solid #EAECF1;
justify-content: space-between;
.capacity-label {
color: #252535;
height: 88rpx;
font-size: 30rpx;
font-weight: bold;
line-height: 88rpx;
}
.capacity-value {
color: #1385FA;
height: 88rpx;
font-size: 28rpx;
font-weight: bold;
line-height: 88rpx;
}
}
.member-card-title {
height: 60rpx;
font-size: 24rpx;
line-height: 60rpx;
padding-left: 30rpx;
box-sizing: border-box;
}
.member-item {
width: 100%;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
box-sizing: border-box;
margin-bottom: 24rpx;
border-radius: 24rpx;
background-color: #FFFFFF;
&:last-of-type {
margin-bottom: 0;
}
.member-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 20rpx;
margin-right: 24rpx;
background-color: #1385FA;
color: #ffffff;
font-size: 36rpx;
font-weight: bold;
line-height: 90rpx;
text-align: center;
}
.member-info {
flex: 1;
display: flex;
flex-flow: column;
justify-content: center;
overflow: hidden;
.member-name-row {
height: 40rpx;
display: flex;
align-items: center;
margin-bottom: 16rpx;
.member-name {
color: #252535;
height: 40rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 40rpx;
margin-right: 20rpx;
}
}
.member-meta {
height: 26rpx;
display: flex;
align-items: center;
.meta-text {
color: #77849E;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
}
.meta-divider {
color: #B6B6B6;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
margin: 0 12rpx;
}
}
}
/* 右侧:badge 或纯文字编辑/删除 */
.member-actions {
height: 48rpx;
display: flex;
align-items: center;
flex-shrink: 0;
.action-text {
color: #808080;
height: 48rpx;
font-size: 24rpx;
line-height: 46rpx;
margin-left: 20rpx;
border-radius: 8rpx;
box-sizing: border-box;
padding: 0 20rpx;
border: 1rpx solid #E6E6EA;
}
.action-text-danger {
color: #FF4D4F;
border: 1rpx solid #FF4D4F;
}
}
}
.my {
border-radius: 0 0 24rpx 24rpx;
}
}
}
/* 通用 badge(本人 / 他人添加) */
.badge {
height: 40rpx;
font-size: 24rpx;
line-height: 40rpx;
border-radius: 8rpx;
}
.badge-self {
color: #1385FA;
padding: 0 18rpx;
background-color: rgba(19, 133, 250, 0.12);
}
.badge-my {
color: #1385FA;
padding: 0 18rpx;
background-color: rgba(19, 133, 250, 0.08);
}
.badge-other {
color: #808080;
}
}
.btn {
width: 100%;
height: 120rpx;
display: flex;
align-items: center;
background-color: #FFFFFF;
/* 底部添加用户按钮 */
.add-member {
width: 580rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
border-radius: 44rpx;
background-color: #1385FA;
.add-member-icon {
color: #FFFFFF;
width: 32rpx;
font-size: 28rpx;
font-weight: bold;
height: 32rpx;
line-height: 30rpx;
border-radius: 50%;
text-align: center;
margin-right: 15rpx;
border: 2rpx solid #ffffff;
}
.add-member-text {
color: #FFFFFF;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 88rpx;
text {
font-weight: 500;
}
}
}
.add-member-disabled {
opacity: 0.4;
}
}
/* 删除确认弹窗 — 仿原生 wx.showModal 风格 */
.delete-modal-mask {
position: fixed;
top: -10vh;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.delete-modal {
width: 650rpx;
padding-top: 50rpx;
border-radius: 24rpx;
background-color: #FFFFFF;
}
.delete-modal-title {
width: 100%;
height: 40rpx;
line-height: 40rpx;
color: #000000;
text-align: center;
font-size: 36rpx;
font-weight: bolder;
}
.delete-modal-content {
padding: 40rpx 0 60rpx;
font-size: 28rpx;
color: #353535;
line-height: 44rpx;
text-align: center;
}
.delete-modal-warning {
color: #F24439;
}
.delete-modal-footer {
width: 100%;
height: 110rpx;
display: flex;
border-top: 2rpx solid #EAECF1;
}
.delete-modal-btn {
flex: 1;
height: 110rpx;
line-height: 110rpx;
text-align: center;
font-size: 36rpx;
}
.delete-modal-btn-cancel {
color: #77849E;
border-right: 2rpx solid #EAECF1;
}
.delete-modal-btn-confirm {
color: #1385FA;
}
}
@@ -1,337 +0,0 @@
import { get, del } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
import type { DeviceMember } from '../../lefu/index'
/** GET /weighingScale/v3/selectUserBySn/user 返回的 result 单条结构 */
interface SysUserDevice {
id: string
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
/** 用户类型:1=主用户 0=子用户 */
userType: number
idCard: string
height: number
weight: number
birthday: string
/** 性别:1=男 2=女 */
sex: number
avatar: string | null
phone: string | null
thirdId: string | null
realname: string
bmi: number
createBy: string | null
createTime: string | null
updateBy: string | null
updateTime: string | null
delFlag: number
sn: string | null
workNo: string | null
remark: string | null
connectDeviceInfo: string | null
/** 关系标记:1=本人 2=自己添加 3=他人添加 */
relationType: number | null
}
/** 页面展示用用户模型 */
interface MemberDisplay {
id: string
/** 用户 ID */
userId: string
realname: string
/** '男' | '女' */
sex_dictText: string
/** 周岁,由 birthday 计算而来 */
ageDisplay: string
height: number
/** 1=本人 2=自己添加 3=他人添加 */
relationType: number
userType: number
avatar: string | null
/** 原始 sex 数值,供 lefu 层转换使用 */
sex: number
/** 周岁数值,供 lefu 层使用 */
age: number
/** 0=员工 1=家庭成员 */
dataType: number
}
/** 设备卡展示模型 */
interface DeviceCard {
/** 设备 SN,来自接口返回的 scaleDeviceId */
name: string
capacityUsed: number
capacityTotal: number
}
/**
* 从生日字符串(YYYY-MM-DD)计算周岁
* @param birthday 生日字符串
*/
const calcAge = (birthday: string): number => {
if (!birthday) return 0
const parts = birthday.split('-').map(Number)
const [year, month, day] = parts
if (!year || !month || !day) return 0
const now = new Date()
let age = now.getFullYear() - year
const nowMonth = now.getMonth() + 1
const nowDay = now.getDate()
if (nowMonth < month || (nowMonth === month && nowDay < day)) age -= 1
return Math.max(0, age)
}
const toDeviceMember = (m: MemberDisplay): DeviceMember => ({
id: m.userId,
name: m.realname,
gender: m.sex === 1 ? 1 : 0,
age: m.age,
height: m.height,
isSelf: m.relationType === 1,
})
const syncToDevice = (members: MemberDisplay[]): void => {
const deviceMembers = members.map(toDeviceMember)
const total = deviceMembers.length
wx.showLoading({ title: `加载中...`, mask: true })
console.log(`正在下发第1/${total}个用户...`)
lefuService.syncMembersToDevice(deviceMembers, (current, t) => {
wx.showLoading({ title: `加载中...`, mask: true })
console.log(`正在下发第${current}/${t}个用户...`)
})
.then(() => {
wx.hideLoading()
wx.showToast({ title: '用户同步成功', icon: 'success' })
})
.catch((err: Error) => {
wx.hideLoading()
wx.showToast({ title: err.message || '同步失败,请重试', icon: 'none' })
})
}
/**
* 设备用户管理页
* sn 与设备名直接读 lefuService.deviceInfo,连接状态实时同步
*/
Page({
/** 非响应式标记:首次加载不同步设备,数据变更后的刷新才同步 */
_firstLoad: true as boolean,
data: {
connected: false,
/** 当前设备 SN,来自 lefuService.deviceInfo.serialNumber */
sn: '',
loading: false,
deviceCard: {
name: '',
capacityUsed: 0,
capacityTotal: 10,
} as DeviceCard,
/** 本人(单个) */
selfMember: null as MemberDisplay | null,
/** 员工列表 */
employeeList: [] as MemberDisplay[],
/** 家庭成员列表 */
familyList: [] as MemberDisplay[],
/** 自定义删除确认弹窗 */
showDeleteModal: false,
deleteTarget: null as MemberDisplay | null,
},
// 当前子页 addMember 未注册任何 SDK 回调,不会覆盖本页回调槽位,故无需 onShow 兜底。
// 若未来子页新增 onDeviceConnect / onDisconnected 注册,需启用以下 onShow 同步连接状态。
// onShow() {
// this.setData({ connected: lefuService.isConnected })
// },
onLoad() {
const deviceInfo = lefuService.deviceInfo
this.setData({
sn: deviceInfo?.serialNumber ?? '',
connected: lefuService.isConnected,
'deviceCard.name': deviceInfo?.modelNumber ?? '智能体重秤',
})
if (deviceInfo?.serialNumber) {
this._loadData(deviceInfo.serialNumber)
}
// deviceInfo 实时同步:进页面时可能还未到达
lefuService.onDeviceInfo((info) => {
this.setData({
sn: info.serialNumber ?? '',
'deviceCard.name': info.modelNumber ?? '智能体重秤',
})
if (!this.data.selfMember && !this.data.employeeList.length && !this.data.familyList.length && info.serialNumber) {
this._loadData(info.serialNumber)
}
})
lefuService.onDeviceConnect(() => {
this.setData({ connected: true })
})
lefuService.onDisconnected(() => {
this.setData({ connected: false })
})
},
onUnload() {
lefuService.onDeviceConnect(() => {})
lefuService.onDisconnected(() => {})
lefuService.onDeviceInfo(() => {})
},
/**
* 调 v3 接口拉取用户列表,成功后更新设备名与用户数据
* @param sn 设备 SN
*/
_loadData(sn: string) {
const userInfo = wx.getStorageSync('userInfo') as { userId?: string } | null
const userId = userInfo?.userId ?? ''
this.setData({ loading: true })
get<SysUserDevice[]>('weighingScale/v3/selectUserBySn/user', { sn, userId })
.then(res => {
const list = res.result.map((item): MemberDisplay => {
const age = calcAge(item.birthday)
return {
id: item.id,
userId: item.userId,
realname: item.realname,
sex_dictText: item.sex === 1 ? '男' : '女',
ageDisplay: String(age),
height: item.height,
relationType: item.relationType ?? 2,
userType: item.userType,
avatar: item.avatar,
sex: item.sex,
age,
dataType: item.dataType,
}
})
const selfMember = list.find(m => m.relationType === 1) || null
const employeeList = list.filter(m => m.relationType !== 1 && m.dataType === 0)
const familyList = list.filter(m => m.relationType !== 1 && m.dataType === 1)
this.setData({
selfMember,
employeeList,
familyList,
'deviceCard.capacityUsed': list.length,
})
syncToDevice(list)
})
.finally(() => {
this.setData({ loading: false })
})
},
/** 编辑用户:仅携带 userId 跳转,详情由目标页自行查询 */
onTapEdit(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
wx.navigateTo({ url: `/pages/editMember/editMember?userId=${id}` })
},
/** 删除用户:弹出自定义确认弹窗 */
onTapDelete(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
const target = this.data.employeeList.find(m => m.id === id)
|| this.data.familyList.find(m => m.id === id)
if (!target) return
if (!this.data.connected) {
wx.showToast({ title: '请先连接设备', icon: 'none' })
return
}
// if (target.userType === 1) {
// const hasSubUser = this.data.employeeList.some(m => m.userType !== 1)
// || this.data.familyList.some(m => m.userType !== 1)
// if (hasSubUser) {
// wx.showToast({ title: '请先删除所有普通用户', icon: 'none' })
// return
// }
// }
this.setData({
showDeleteModal: true,
deleteTarget: target,
})
},
/** 关闭删除确认弹窗 */
onHideDeleteModal() {
this.setData({
showDeleteModal: false,
deleteTarget: null,
})
},
/** 确认删除 */
onConfirmDelete() {
const target = this.data.deleteTarget
if (!target) return
const id = target.id
const newEmployeeList = this.data.employeeList.filter(m => m.id !== id)
const newFamilyList = this.data.familyList.filter(m => m.id !== id)
const merged: MemberDisplay[] = [
...(this.data.selfMember ? [this.data.selfMember] : []),
...newEmployeeList,
...newFamilyList,
]
this.setData({
employeeList: newEmployeeList,
familyList: newFamilyList,
'deviceCard.capacityUsed': merged.length,
showDeleteModal: false,
deleteTarget: null,
})
wx.showLoading({ title: '加载中...', mask: true })
console.log('正在同步设备数据...')
let devicePromise: Promise<void>
if (merged.length) {
devicePromise = lefuService.syncMembersToDevice(
merged.map(toDeviceMember),
(current, total) => {
wx.showLoading({ title: `加载中...`, mask: true })
console.log(`正在同步设备数据 ${current}/${total}...`)
},
)
} else {
devicePromise = lefuService.clearDeviceMembers()
}
devicePromise
.then(() => {
return del('weighingScale/v2/del/user', { id }, { loading: false })
})
.then(() => {
wx.hideLoading()
wx.showToast({ title: '删除成功', icon: 'success' })
})
.catch((err: Error) => {
wx.hideLoading()
this._loadData(this.data.sn)
wx.showToast({ title: err.message || '删除失败,请重试', icon: 'none' })
})
},
/** 空方法,阻止弹窗事件冒泡 */
noop() {},
/** 添加用户:未连接态禁用 */
onTapAddMember() {
if (!this.data.connected) return
const total = (this.data.selfMember ? 1 : 0) + this.data.employeeList.length + this.data.familyList.length
if (total >= 10) {
wx.showToast({ title: '超过最大用户限制', icon: 'none' })
return
}
wx.navigateTo({ url: '/pages/addMember/addMember' })
},
})
@@ -1,148 +0,0 @@
<view class="equipmentMember">
<!-- 未连接提示条 -->
<block wx:if="{{ !connected }}">
<view class="tips">
<view class="tips-icon">!</view>
<text class="tips-text">设备未连接,请前往连接</text>
</view>
</block>
<view class="seaction">
<!-- 设备卡 -->
<view class="page-container">
<view class="device-card">
<image class="device-illustration" src="/images/device/{{ deviceCard.name }}.png" mode="aspectFit"></image>
<view class="device-info">
<view class="device-name">{{ deviceCard.name }}</view>
<view class="device-status">
<view class="status-dot {{ connected ? 'status-dot-on' : 'status-dot-off' }}"></view>
<view class="status-text">{{ connected ? '设备已连接' : '未连接' }}</view>
</view>
</view>
</view>
</view>
<scroll-view class="scrollView" scroll-y>
<view class="scrollViewContent page-container">
<!-- 用户卡(容量 + 列表) -->
<view class="member-card">
<!-- 设备容量(仅已连接态) -->
<block wx:if="{{ connected }}">
<view class="member-card-header">
<view class="capacity-label">设备容量</view>
<view class="capacity-value">
{{ deviceCard.capacityUsed }}/{{ deviceCard.capacityTotal }}
</view>
</view>
</block>
<!-- 本人 -->
<view class="member-item my" wx:if="{{ selfMember }}">
<view class="member-avatar">{{ selfMember.realname[0] }}</view>
<view class="member-info">
<view class="member-name-row">
<view class="member-name">{{ selfMember.realname }}</view>
<view class="badge badge-self">本人</view>
</view>
<view class="member-meta">
<text class="meta-text">{{ selfMember.sex_dictText }}</text>
<text class="meta-divider">·</text>
<text class="meta-text">{{ selfMember.ageDisplay }}岁</text>
<text class="meta-divider">|</text>
<text class="meta-text">{{ selfMember.height }}cm</text>
</view>
</view>
<view class="member-actions">
<view class="action-text" data-id="{{ selfMember.id }}" bind:tap="onTapEdit">编辑</view>
</view>
</view>
</view>
<!-- 员工 -->
<view class="member-card" wx:if="{{ employeeList.length }}">
<view class="member-card-title" style="color: #10B981;">员工</view>
<block wx:for="{{ employeeList }}" wx:key="id">
<view class="member-item">
<view class="member-avatar">{{ item.realname[0] }}</view>
<view class="member-info">
<view class="member-name-row">
<view class="member-name">{{ item.realname }}</view>
<block wx:if="{{ item.relationType == 3 }}">
<view class="badge badge-other">他人添加</view>
</block>
</view>
<view class="member-meta">
<text class="meta-text">{{ item.sex_dictText }}</text>
<text class="meta-divider">·</text>
<text class="meta-text">{{ item.ageDisplay }}岁</text>
<text class="meta-divider">|</text>
<text class="meta-text">{{ item.height }}cm</text>
</view>
</view>
<view class="member-actions">
<view class="action-text" data-id="{{ item.id }}" bind:tap="onTapEdit">编辑</view>
<view class="action-text action-text-danger" data-id="{{ item.id }}" bind:tap="onTapDelete">删除</view>
</view>
</view>
</block>
</view>
<!-- 家庭成员 -->
<view class="member-card" wx:if="{{ familyList.length }}">
<view class="member-card-title" style="color: #F59E0B;">家庭成员</view>
<block wx:for="{{ familyList }}" wx:key="id">
<view class="member-item">
<view class="member-avatar">{{ item.realname[0] }}</view>
<view class="member-info">
<view class="member-name-row">
<view class="member-name">{{ item.realname }}</view>
<block wx:if="{{ item.relationType == 3 }}">
<view class="badge badge-other">他人添加</view>
</block>
</view>
<view class="member-meta">
<text class="meta-text">{{ item.sex_dictText }}</text>
<text class="meta-divider">·</text>
<text class="meta-text">{{ item.ageDisplay }}岁</text>
<text class="meta-divider">|</text>
<text class="meta-text">{{ item.height }}cm</text>
</view>
</view>
<view class="member-actions">
<view class="action-text" data-id="{{ item.id }}" bind:tap="onTapEdit">编辑</view>
<view class="action-text action-text-danger" data-id="{{ item.id }}" bind:tap="onTapDelete">删除</view>
</view>
</view>
</block>
</view>
</view>
</scroll-view>
</view>
<!-- 底部添加用户按钮 -->
<view class="btn">
<view class="add-member {{ connected ? '' : 'add-member-disabled' }}" bind:tap="onTapAddMember">
<view class="add-member-icon">+</view>
<view class="add-member-text">添加用户<text>(最多10位)</text></view>
</view>
</view>
<!-- 删除确认弹窗 -->
<view class="delete-modal-mask" wx:if="{{ showDeleteModal }}" bind:tap="onHideDeleteModal">
<view class="delete-modal" catch:tap="noop">
<view class="delete-modal-title">确认删除</view>
<view class="delete-modal-content">
<block wx:if="{{ deleteTarget.relationType == 3 }}">
"<text class="delete-modal-warning">{{ deleteTarget.realname }}</text>"用户为他人添加,请确认是否删除
</block>
<block wx:else>
确定删除"{{ deleteTarget.realname }}"吗?删除后将不再显示
</block>
</view>
<view class="delete-modal-footer">
<view class="delete-modal-btn delete-modal-btn-cancel" bind:tap="onHideDeleteModal">取消</view>
<view class="delete-modal-btn delete-modal-btn-confirm" bind:tap="onConfirmDelete">删除</view>
</view>
</view>
</view>
</view>
-4
View File
@@ -1,4 +0,0 @@
{
"usingComponents": {},
"navigationStyle": "custom"
}
-496
View File
@@ -1,496 +0,0 @@
.header {
width: 100%;
position: relative;
height: 450rpx;
margin-bottom: 25rpx;
.bg {
width: 100%;
height: 450rpx;
}
.device {
position: absolute;
z-index: 2;
top: 285rpx;
left: 60rpx;
right: 60rpx;
bottom: 0;
display: flex;
align-items: center;
.device-icon {
width: 90rpx;
height: 90rpx;
overflow: hidden;
border-radius: 24rpx;
margin-right: 24rpx;
}
.device-info {
flex: 1;
.device-name {
color: #FFFFFF;
font-size: 32rpx;
font-weight: 700;
line-height: 36rpx;
margin-bottom: 14rpx;
}
.device-status {
display: flex;
align-items: center;
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 10rpx;
}
.dot-on {
background-color: #FFFFFF;
}
.dot-off {
background-color: rgba(255, 255, 255, 0.5);
}
.status-text {
color: #FFFFFF;
font-size: 28rpx;
line-height: 28rpx;
}
}
}
.device-switch {
color: #FFFFFF;
height: 48rpx;
padding: 0 20rpx;
font-size: 24rpx;
font-weight: 500;
line-height: 44rpx;
border-radius: 8rpx;
box-sizing: border-box;
border: 2rpx solid #FFFFFF;
}
}
}
.scrollViewContent {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
/* ===== 最近测量卡 ===== */
.weight-card {
width: 100%;
position: relative;
margin-bottom: 24rpx;
padding: 2rpx 2rpx 50rpx;
border-radius: 24rpx;
box-sizing: border-box;
background-color: #FFFFFF;
/* 卡头(带渐变背景) */
.card-header {
width: 100%;
height: 88rpx;
padding-left: 30rpx;
padding-right: 10rpx;
box-sizing: border-box;
display: flex;
align-items: center;
margin-bottom: 18rpx;
justify-content: space-between;
background: linear-gradient(180deg, rgba(211, 233, 255, 0.6) 0%, rgba(252, 251, 253, 0.6) 100%);
.card-header-left {
display: flex;
align-items: center;
.card-title-bar {
width: 6rpx;
height: 24rpx;
border-radius: 3rpx;
background-color: #1385FA;
margin-right: 14rpx;
}
.card-title {
color: #252535;
font-size: 30rpx;
font-weight: 700;
line-height: 30rpx;
}
}
.card-header-right {
display: flex;
align-items: center;
.link-text {
color: #77849E;
font-size: 24rpx;
line-height: 24rpx;
}
.arrow {
padding-right: 40rpx;
box-sizing: border-box;
}
}
}
.card-content {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
/* ===== 最近测量:体重主数据行 ===== */
.weight-row {
display: flex;
align-items: flex-end;
margin-bottom: 30rpx;
.weight-main {
flex: 1;
display: flex;
flex-direction: column;
.weight-num-wrap {
height: 96rpx;
display: flex;
margin-bottom: 20rpx;
align-items: baseline;
.weight-int {
color: #252535;
height: 96rpx;
font-size: 96rpx;
font-weight: bold;
line-height: 96rpx;
}
.weight-dec {
color: #252535;
font-size: 56rpx;
font-weight: bold;
line-height: 56rpx;
margin-right: 6rpx;
}
.weight-unit {
color: #808080;
font-size: 28rpx;
font-weight: bold;
line-height: 28rpx;
}
}
.weight-date {
color: #B6B6B6;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
/* 右侧变化量 */
.weight-diff {
width: 250rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
.diff-num-wrap {
display: flex;
align-items: flex-end;
margin-bottom: 14rpx;
.diff-num {
height: 48rpx;
font-size: 48rpx;
font-weight: bold;
line-height: 48rpx;
margin-right: 6rpx;
}
.diff-unit {
position: relative;
color: #808080;
height: 28rpx;
font-weight: bold;
font-size: 28rpx;
line-height: 28rpx;
text {
position: absolute;
left: 0;
bottom: 42rpx;
}
}
/* 向下箭头(减轻) */
.arrow-down {
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 16rpx solid #2AC79F;
}
/* 向上箭头(增重) */
.arrow-up {
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-bottom: 16rpx solid #FF4D4F;
}
.diff-down {
color: #2AC79F;
}
.diff-up {
color: #FF4D4F;
}
.diff-neutral {
color: #000000;
}
}
.diff-label {
color: #B6B6B6;
font-size: 24rpx;
line-height: 28rpx;
margin-top: 8rpx;
}
}
}
/* 分割线 */
.card-divider {
width: 100%;
height: 2rpx;
background-color: #EAECF1;
margin-bottom: 35rpx;
}
/* 身高 / BMI / 标准体重 指标行 */
.metrics-row {
display: flex;
padding: 0 10rpx;
justify-content: space-between;
.metric-col {
display: flex;
flex-direction: column;
.metric-label {
color: #808080;
height: 30rpx;
display: flex;
font-size: 26rpx;
line-height: 30rpx;
margin-bottom: 20rpx;
.metric-label-text {
color: #808080;
height: 30rpx;
font-size: 26rpx;
line-height: 30rpx;
margin-right: 6rpx;
}
.metric-label-value {
color: #FFFFFF;
height: 30rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 30rpx;
background-color: #2AC79F;
border-radius: 8rpx;
}
}
/* BMI 行(标签 + badge 并排) */
.metric-label-row {
height: 30rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
.metric-label {
margin-bottom: 0;
}
/* BMI 标准/偏高 badge */
.bmi-badge {
height: 30rpx;
padding: 0 10rpx;
margin-left: 10rpx;
font-size: 18rpx;
line-height: 30rpx;
border-radius: 8rpx;
color: #FFFFFF;
background-color: #2AC79F;
}
}
.metric-value-row {
display: flex;
align-items: baseline;
.metric-value {
color: #252535;
height: 44rpx;
font-size: 44rpx;
font-weight: 700;
line-height: 44rpx;
margin-right: 6rpx;
}
.metric-unit {
color: #252535;
height: 26rpx;
font-size: 26rpx;
font-weight: bold;
line-height: 26rpx;
}
}
}
}
}
.card-lianjie {
position: absolute;
right: 24rpx;
width: 32rpx;
height: 84rpx;
bottom: -55rpx;
}
}
/* ===== 详细数据卡 ===== */
.detail-card {
width: 100%;
margin-bottom: 24rpx;
padding: 2rpx 2rpx 30rpx;
border-radius: 24rpx;
box-sizing: border-box;
background-color: #FFFFFF;
/* 卡头(带渐变背景) */
.card-header {
width: 100%;
height: 88rpx;
padding-left: 30rpx;
padding-right: 50rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.card-header-left {
display: flex;
align-items: center;
.card-title-bar {
width: 6rpx;
height: 24rpx;
border-radius: 3rpx;
background-color: #1385FA;
margin-right: 14rpx;
}
.card-title {
color: #252535;
font-size: 30rpx;
font-weight: 700;
line-height: 30rpx;
}
}
.card-header-right {
display: flex;
align-items: center;
.link-text {
color: #77849E;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
/* ===== 详细数据:三格 ===== */
.detail-grid {
display: flex;
padding: 0 30rpx;
justify-content: space-between;
.detail-item {
width: 195rpx;
padding: 24rpx;
box-sizing: border-box;
border-radius: 16rpx;
background-color: #F3F4F6;
display: flex;
flex-direction: column;
.detail-label {
color: #808080;
font-size: 24rpx;
line-height: 24rpx;
margin-bottom: 30rpx;
}
.detail-value-row {
display: flex;
align-items: baseline;
.detail-value {
color: #252535;
height: 44rpx;
font-size: 44rpx;
font-weight: bold;
line-height: 44rpx;
margin-right: 2rpx;
}
.detail-unit {
color: #252535;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
}
}
}
}
}
/* ===== 空态 ===== */
.empty-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 280rpx;
.empty-icon {
width: 193rpx;
height: 138rpx;
margin-bottom: 40rpx;
border-radius: 20rpx;
}
.empty-text {
color: #808080;
height: 30rpx;
font-size: 30rpx;
line-height: 30rpx;
}
}
}
-174
View File
@@ -1,174 +0,0 @@
import { get } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
/** 设备信息 */
interface DeviceInfo {
name: string
connected: boolean
}
/** GET /weighingScale/v3/getMeasureHistory result 结构 */
interface MeasureHistoryVO {
recordId: string
height: number
weight: number
weightChange: number
bmi: number
bmiType: string
standardWeight: number
fatRate: number
fatValue: number
basalMetabolism: number
examTime: string
sn: string
userInfo: {
realname: string
sex: number
[key: string]: any
} | null
}
/** 页面渲染用测量记录 */
interface WeightRecord {
weight: number
weightDecimal: string
date: string
diff: number
height: number
bmi: number
bmiLabel: string
stdWeight: number
bodyFat: number
bodyFatMass: number
bmr: number
}
/** 缓存中的用户信息 */
interface StorageUserInfo {
userId: string
[key: string]: any
}
/** 将体重数字拆成整数 + 小数字符串 */
const splitWeight = (weight: number): { int: number; decimal: string } => {
const str = weight.toFixed(2)
const [intPart, decPart] = str.split('.')
return { int: Number(intPart), decimal: `.${decPart}` }
}
Page({
_otaChecked: false as boolean,
data: {
hasData: false,
device: {
name: '智能体重秤',
connected: false,
} as DeviceInfo,
record: null as WeightRecord | null,
},
onLoad() {
lefuService.onDeviceInfo((info) => {
this.setData({ 'device.name': info.modelNumber ?? '智能体重秤' })
})
lefuService.onDeviceConnect(() => {
this.setData({ 'device.connected': true })
this._checkOta()
})
lefuService.onDisconnected(() => {
this.setData({ 'device.connected': false })
})
},
onShow() {
// 每次进入 tab 同步最新设备状态
const connected = lefuService.isConnected
this.setData({
'device.connected': connected,
'device.name': lefuService.deviceInfo?.modelNumber ?? '智能体重秤',
})
// 进来时设备已连接(如热启动/切 tab 回来),onDeviceConnect 不会再触发,需在此补一次检测
if (connected && !this._otaChecked) {
this._checkOta()
}
// 断连时自动扫描重连,重连成功后 onDeviceConnect 回调会触发 OTA 检测
if (!connected) {
lefuService.autoConnect()
}
this.loadData()
},
onUnload() {
lefuService.onDeviceConnect(() => {})
lefuService.onDisconnected(() => {})
lefuService.onDeviceInfo(() => {})
},
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
return {
title: '智能蓝牙秤',
path: '/pages/home/home',
imageUrl: '/images/share/share.jpg',
}
},
/** 拉取最新一次测量记录 */
loadData() {
const raw = wx.getStorageSync('userInfo') as StorageUserInfo | null
if (!raw?.userId) return
get<MeasureHistoryVO>('weighingScale/v3/getMeasureHistory', {
userId: raw.userId,
}, { loading: false })
.then((res: any) => {
const d: MeasureHistoryVO = res.result
if (!d) {
this.setData({ hasData: false, record: null })
return
}
const { int, decimal } = splitWeight(d.weight ?? 0)
const record: WeightRecord = {
weight: int,
weightDecimal: decimal,
date: d.examTime ?? '',
diff: d.weightChange ?? 0,
height: d.height ?? 0,
bmi: d.bmi ?? 0,
bmiLabel: d.bmiType ?? '',
stdWeight: d.standardWeight ?? 0,
bodyFat: d.fatRate ?? 0,
bodyFatMass: d.fatValue ?? 0,
bmr: d.basalMetabolism ?? 0,
}
this.setData({ hasData: true, record })
})
},
/** OTA 固件检测,连接成功后自动调用,有新版本直接升级,整个页面生命周期只触发一次 */
_checkOta() {
if (this._otaChecked) return
this._otaChecked = true
lefuService.checkAndUpgrade()
.then(() => {
wx.showLoading({ title: '加载中...', mask: true })
setTimeout(() => {
wx.hideLoading()
// 升级完成后设备重启,旧连接与订阅失效,重新扫描连接
this.setData({ 'device.connected': false })
lefuService.autoConnect()
}, 15000)
})
.catch(() => {})
},
/** 跳转历史数据页 */
onTapHistory() {
wx.switchTab({ url: '/pages/data/data' })
},
/** 跳转设备列表页 */
onTapSwitchDevice() {
wx.switchTab({ url: '/pages/equipment/equipment' })
},
})
-148
View File
@@ -1,148 +0,0 @@
<scroll-view class="scrollView" scroll-y>
<view class="header">
<view class="bg">
<image src="/images/home/header.jpg" mode=""/>
</view>
<view class="device">
<image class="device-icon" src="/images/device/{{ device.name }}.png" mode="aspectFit"></image>
<view class="device-info">
<view class="device-name">{{ device.name }}</view>
<view class="device-status">
<view class="status-dot {{ device.connected ? 'dot-on' : 'dot-off' }}"></view>
<view class="status-text">{{ device.connected ? '设备已连接' : '未连接' }}</view>
</view>
</view>
<view class="device-switch" bind:tap="onTapSwitchDevice">切换设备</view>
</view>
</view>
<view class="scrollViewContent">
<!-- 有数据态 -->
<block wx:if="{{ hasData }}">
<!-- 最近测量卡 -->
<view class="weight-card">
<!-- 卡头 -->
<view class="card-header">
<view class="card-header-left">
<view class="card-title-bar"></view>
<view class="card-title">最近测量</view>
</view>
<view class="card-header-right" bind:tap="onTapHistory">
<view class="link-text">查看历史</view>
<view class="arrow"></view>
</view>
</view>
<view class="card-content">
<!-- 体重主数据行 -->
<view class="weight-row">
<view class="weight-main">
<view class="weight-num-wrap">
<text class="weight-int">{{ record.weight }}</text>
<text class="weight-dec">{{ record.weightDecimal }}</text>
<text class="weight-unit">kg</text>
</view>
<view class="weight-date">{{ record.date }}</view>
</view>
<view class="weight-diff">
<view class="diff-num-wrap">
<view class="diff-num {{ record.diff < 0 ? 'diff-down' : (record.diff > 0 ? 'diff-up' : 'diff-neutral') }}">{{ record.diff < 0 ? -record.diff : record.diff }}</view>
<view class="diff-unit">
kg
<text wx:if="{{ record.diff !== 0 }}" class="diff-arrow {{ record.diff < 0 ? 'arrow-down' : 'arrow-up' }}"></text>
</view>
</view>
<view class="diff-label">{{ record.diff < 0 ? '体重较上次减轻' : (record.diff > 0 ? '体重较上次增加' : '与上次持平') }}</view>
</view>
</view>
<!-- 分割线 -->
<view class="card-divider"></view>
<!-- 身高 / BMI / 标准体重 指标行 -->
<view class="metrics-row">
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">身高</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.height }}</text>
<text class="metric-unit">cm</text>
</view>
</view>
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">BMI</view>
<view class="metric-label-value">{{ record.bmiLabel }}</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.bmi }}</text>
</view>
</view>
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">标准体重</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.stdWeight }}</text>
<text class="metric-unit">kg</text>
</view>
</view>
</view>
</view>
<view class="card-lianjie">
<image src="/images/home/lianjie.png" mode=""/>
</view>
</view>
<!-- 详细数据卡 -->
<view class="detail-card">
<!-- 卡头 -->
<view class="card-header">
<view class="card-header-left">
<view class="card-title-bar"></view>
<view class="card-title">详细数据</view>
</view>
</view>
<!-- 三格数据 -->
<view class="detail-grid">
<view class="detail-item">
<view class="detail-label">体脂率</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bodyFat }}</text>
<text class="detail-unit">%</text>
</view>
</view>
<view class="detail-item">
<view class="detail-label">体脂肪</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bodyFatMass }}</text>
<text class="detail-unit">kg</text>
</view>
</view>
<view class="detail-item">
<view class="detail-label">基础代谢</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bmr }}</text>
<text class="detail-unit">kcal</text>
</view>
</view>
</view>
</view>
</block>
<!-- 空态(无称重数据) -->
<block wx:else>
<view class="empty-state">
<view class="empty-icon">
<image src="/images/home/noData.png" mode="aspectFit"/>
</view>
<view class="empty-text">暂无称重数据</view>
</view>
</block>
</view>
</scroll-view>
+22 -64
View File
@@ -1,43 +1,6 @@
import { post } from '../../utils/request/index'
import { checkOpenIdLogin } from '../../api/index'
/** 通过 OpenID 登录的用户信息 */
interface UserInfo {
id: string
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
userType: string | null
idCard: string
height: number
weight: number
birthday: string
sex: number
avatar: string | null
phone: string | null
thirdId: string
realname: string
bmi: number
createBy: string
createTime: string
updateBy: string | null
updateTime: string | null
delFlag: number
sn: string | null
workNo: string | null
remark: string | null
relationType: string | null
sex_dictText: string
connectDeviceInfo: string
}
/** 登录接口 result 结构 */
interface CheckOpenIdLoginResult {
user: UserInfo | null
token: string
}
const app = getApp<IAppOption>()
Page({
data: {
@@ -47,7 +10,7 @@ Page({
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
return {
title: '智能蓝牙秤',
path: '/pages/home/home',
path: '/pages/login/login',
imageUrl: '/images/share/share.jpg',
}
},
@@ -57,30 +20,25 @@ Page({
if (this.data.loading) return
this.setData({ loading: true })
wx.login({
success: ({ code }) => {
post<CheckOpenIdLoginResult>('weighingScale/v2/checkOpenIdLogin', { code })
.then(({ result }) => {
wx.setStorageSync('token', result.token)
wx.setStorageSync('userInfo', result.user)
/* 上次连接设备不为空对象则直接进首页 */
const connectDeviceInfo = result.user?.connectDeviceInfo
if (connectDeviceInfo && connectDeviceInfo !== '{}') {
wx.setStorageSync('connectDeviceInfo', JSON.parse(connectDeviceInfo))
wx.reLaunch({ url: '/pages/home/home' })
} else {
wx.reLaunch({ url: '/pages/connectedDevice/connectedDevice' })
}
})
.catch(() => {
this.setData({ loading: false })
})
},
fail: () => {
wx.showToast({ title: '获取登录凭证失败', icon: 'none' })
// 获取微信临时 code → 调用 OpenID 登录接口
app.getWxLoginCode()
.then((code) => checkOpenIdLogin(code))
.then((res) => {
const { token, user } = res.data
// 持久化登录凭证和用户信息
wx.setStorageSync('token', token)
if (user) {
wx.setStorageSync('userInfo', user)
wx.reLaunch({ url: '/pages/home/home' })
} else {
wx.reLaunch({ url: '/pages/supplementPersonal/supplementPersonal' })
}
})
.catch((err) => {
wx.showToast({ title: err.msg || '登录失败,请重试', icon: 'none' })
})
.finally(() => {
this.setData({ loading: false })
},
})
})
},
})
@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "测量报告",
"usingComponents": {}
}
@@ -1,421 +0,0 @@
.scrollViewContent {
width: 100%;
padding: 24rpx 30rpx;
box-sizing: border-box;
/* ===== 测量信息卡片 ===== */
.info-card {
width: 100%;
display: flex;
align-items: center;
padding: 36rpx 30rpx;
margin-bottom: 24rpx;
border-radius: 24rpx;
box-sizing: border-box;
//background-color: #FFFFFF;
background: linear-gradient(180deg, #D3E9FF 0%, #FCFBFD 100%);
.info-avatar {
width: 90rpx;
height: 90rpx;
margin-right: 24rpx;
border-radius: 18rpx;
color: #FFFFFF;
font-size: 36rpx;
font-weight: bold;
line-height: 90rpx;
text-align: center;
background-color: #1385FA;
}
.info-meta {
flex: 1;
.info-name {
color: #333333;
height: 32rpx;
font-size: 32rpx;
font-weight: bold;
line-height: 32rpx;
margin-bottom: 20rpx;
}
.info-time {
color: #77849E;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
}
}
}
/* ===== 最近测量卡 ===== */
.weight-card {
width: 100%;
position: relative;
margin-bottom: 24rpx;
padding: 2rpx 2rpx 50rpx;
border-radius: 24rpx;
box-sizing: border-box;
background-color: #FFFFFF;
/* 卡头(带渐变背景) */
.card-header {
width: 100%;
height: 88rpx;
padding-left: 30rpx;
padding-right: 10rpx;
box-sizing: border-box;
display: flex;
align-items: center;
margin-bottom: 18rpx;
justify-content: space-between;
.card-header-left {
display: flex;
align-items: center;
.card-title-bar {
width: 6rpx;
height: 24rpx;
border-radius: 3rpx;
background-color: #1385FA;
margin-right: 14rpx;
}
.card-title {
color: #252535;
font-size: 30rpx;
font-weight: 700;
line-height: 30rpx;
}
}
}
.card-content {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
/* ===== 最近测量:体重主数据行 ===== */
.weight-row {
display: flex;
align-items: flex-end;
margin-bottom: 30rpx;
.weight-main {
flex: 1;
display: flex;
flex-direction: column;
.weight-num-wrap {
height: 96rpx;
display: flex;
margin-bottom: 20rpx;
align-items: baseline;
.weight-int {
color: #252535;
height: 96rpx;
font-size: 96rpx;
font-weight: bold;
line-height: 96rpx;
}
.weight-dec {
color: #252535;
font-size: 56rpx;
font-weight: bold;
line-height: 56rpx;
margin-right: 6rpx;
}
.weight-unit {
color: #808080;
font-size: 28rpx;
font-weight: bold;
line-height: 28rpx;
}
}
.weight-date {
color: #B6B6B6;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
/* 右侧变化量 */
.weight-diff {
width: 250rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
.diff-num-wrap {
display: flex;
align-items: flex-end;
margin-bottom: 14rpx;
.diff-num {
height: 48rpx;
font-size: 48rpx;
font-weight: bold;
line-height: 48rpx;
margin-right: 6rpx;
}
.diff-unit {
position: relative;
color: #808080;
height: 28rpx;
font-weight: bold;
font-size: 28rpx;
line-height: 28rpx;
text {
position: absolute;
left: 0;
bottom: 42rpx;
}
}
/* 向下箭头(减轻) */
.arrow-down {
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 16rpx solid #2AC79F;
}
/* 向上箭头(增重) */
.arrow-up {
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-bottom: 16rpx solid #FF4D4F;
}
.diff-down {
color: #2AC79F;
}
.diff-up {
color: #FF4D4F;
}
.diff-neutral {
color: #000000;
}
}
.diff-label {
color: #B6B6B6;
font-size: 24rpx;
line-height: 28rpx;
margin-top: 8rpx;
}
}
}
/* 分割线 */
.card-divider {
width: 100%;
height: 2rpx;
background-color: #EAECF1;
margin-bottom: 35rpx;
}
/* 身高 / BMI / 标准体重 指标行 */
.metrics-row {
display: flex;
padding: 0 10rpx;
justify-content: space-between;
.metric-col {
display: flex;
flex-direction: column;
.metric-label {
color: #808080;
height: 30rpx;
display: flex;
font-size: 26rpx;
line-height: 30rpx;
margin-bottom: 20rpx;
.metric-label-text {
color: #808080;
height: 30rpx;
font-size: 26rpx;
line-height: 30rpx;
margin-right: 6rpx;
}
.metric-label-value {
color: #FFFFFF;
height: 30rpx;
padding: 0 6rpx;
font-size: 18rpx;
line-height: 30rpx;
background-color: #2AC79F;
border-radius: 8rpx;
}
}
/* BMI 行(标签 + badge 并排) */
.metric-label-row {
height: 30rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
.metric-label {
margin-bottom: 0;
}
/* BMI 标准/偏高 badge */
.bmi-badge {
height: 30rpx;
padding: 0 10rpx;
margin-left: 10rpx;
font-size: 18rpx;
line-height: 30rpx;
border-radius: 8rpx;
color: #FFFFFF;
background-color: #2AC79F;
}
}
.metric-value-row {
display: flex;
align-items: baseline;
.metric-value {
color: #252535;
height: 44rpx;
font-size: 44rpx;
font-weight: 700;
line-height: 44rpx;
margin-right: 6rpx;
}
.metric-unit {
color: #252535;
height: 26rpx;
font-size: 26rpx;
font-weight: bold;
line-height: 26rpx;
}
}
}
}
}
.card-lianjie {
position: absolute;
right: 24rpx;
width: 32rpx;
height: 84rpx;
bottom: -55rpx;
}
}
/* ===== 详细数据卡 ===== */
.detail-card {
width: 100%;
margin-bottom: 24rpx;
padding: 2rpx 2rpx 30rpx;
border-radius: 24rpx;
box-sizing: border-box;
background-color: #FFFFFF;
/* 卡头(带渐变背景) */
.card-header {
width: 100%;
height: 88rpx;
padding-left: 30rpx;
padding-right: 50rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.card-header-left {
display: flex;
align-items: center;
.card-title-bar {
width: 6rpx;
height: 24rpx;
border-radius: 3rpx;
background-color: #1385FA;
margin-right: 14rpx;
}
.card-title {
color: #252535;
font-size: 30rpx;
font-weight: 700;
line-height: 30rpx;
}
}
.card-header-right {
display: flex;
align-items: center;
.link-text {
color: #77849E;
height: 24rpx;
font-size: 24rpx;
line-height: 24rpx;
}
}
}
/* ===== 详细数据:三格 ===== */
.detail-grid {
display: flex;
padding: 0 30rpx;
justify-content: space-between;
.detail-item {
width: 195rpx;
padding: 24rpx;
box-sizing: border-box;
border-radius: 16rpx;
background-color: #F3F4F6;
display: flex;
flex-direction: column;
.detail-label {
color: #808080;
font-size: 24rpx;
line-height: 24rpx;
margin-bottom: 30rpx;
}
.detail-value-row {
display: flex;
align-items: baseline;
.detail-value {
color: #252535;
height: 44rpx;
font-size: 44rpx;
font-weight: bold;
line-height: 44rpx;
margin-right: 2rpx;
}
.detail-unit {
color: #252535;
height: 26rpx;
font-size: 26rpx;
line-height: 26rpx;
}
}
}
}
}
}
@@ -1,122 +0,0 @@
import { get } from '../../utils/request/index'
/** GET /weighingScale/v3/getMeasureHistory result 结构 */
interface MeasureHistoryVO {
recordId: string
height: number
weight: number
weightChange: number
bmi: number
bmiType: string
standardWeight: number
fatRate: number
fatValue: number
basalMetabolism: number
examTime: string
sn: string
userInfo: {
realname: string
sex: number
[key: string]: any
} | null
}
/** 页面渲染用测量记录 */
interface WeightRecord {
/** 体重整数部分 */
weight: number
/** 体重小数部分,含小数点,如 ".20" */
weightDecimal: string
/** 测量时间 */
date: string
/** 与上次体重差值(负=减轻,正=增重) */
diff: number
/** 身高(cm) */
height: number
/** BMI 值 */
bmi: number
/** BMI 评级文本(来自接口 bmiType */
bmiLabel: string
/** 标准体重(kg) */
stdWeight: number
/** 体脂率(%) */
bodyFat: number
/** 体脂肪量(kg) */
bodyFatMass: number
/** 基础代谢(kcal) */
bmr: number
}
/** 空态默认值,避免页面展示 null */
const EMPTY_RECORD: WeightRecord = {
weight: 0,
weightDecimal: '',
date: '',
diff: 0,
height: 0,
bmi: 0,
bmiLabel: '',
stdWeight: 0,
bodyFat: 0,
bodyFatMass: 0,
bmr: 0,
}
/** 将体重数字拆成整数 + 小数字符串 */
const splitWeight = (weight: number): { int: number; decimal: string } => {
const str = weight.toFixed(2)
const [intPart, decPart] = str.split('.')
return { int: Number(intPart), decimal: `.${decPart}` }
}
/**
* 测量报告页
* URL 参数:recordId(记录 ID)、userId(用户 ID
*/
Page({
data: {
record: EMPTY_RECORD as WeightRecord,
/** 被查看用户的姓名(来自接口 userInfo.realname */
realname: '',
},
onLoad(options: Record<string, string>) {
const { recordId, userId } = options
if (!recordId || !userId) return
get<MeasureHistoryVO>('weighingScale/v3/getMeasureHistory', {
userId,
recordId,
}, { loading: true })
.then((res: any) => {
const d: MeasureHistoryVO = res.result
if (!d) return
const { int, decimal } = splitWeight(d.weight ?? 0)
const record: WeightRecord = {
weight: int,
weightDecimal: decimal,
date: d.examTime ?? '',
diff: d.weightChange ?? 0,
height: d.height ?? 0,
bmi: d.bmi ?? 0,
bmiLabel: d.bmiType ?? '',
stdWeight: d.standardWeight ?? 0,
bodyFat: d.fatRate ?? 0,
bodyFatMass: d.fatValue ?? 0,
bmr: d.basalMetabolism ?? 0,
}
this.setData({
record,
realname: d.userInfo?.realname ?? '',
})
})
},
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
return {
title: `${this.data.realname}的测量报告`,
path: '/pages/measurementReport/measurementReport',
}
},
})
@@ -1,121 +0,0 @@
<scroll-view class="scrollView" scroll-y>
<view class="scrollViewContent">
<!-- 测量信息卡片 -->
<view class="info-card">
<view class="info-avatar">{{ realname[0] }}</view>
<view class="info-meta">
<view class="info-name">{{ realname }}</view>
<view class="info-time">{{ record.date }}</view>
</view>
</view>
<!-- 最近测量卡 -->
<view class="weight-card">
<!-- 卡头 -->
<view class="card-header">
<view class="card-header-left">
<view class="card-title-bar"></view>
<view class="card-title">体重</view>
</view>
</view>
<view class="card-content">
<!-- 体重主数据行 -->
<view class="weight-row">
<view class="weight-main">
<view class="weight-num-wrap">
<text class="weight-int">{{ record.weight }}</text>
<text class="weight-dec">{{ record.weightDecimal }}</text>
<text class="weight-unit">kg</text>
</view>
<view class="weight-date">{{ record.date }}</view>
</view>
<view class="weight-diff">
<view class="diff-num-wrap">
<view class="diff-num {{ record.diff < 0 ? 'diff-down' : (record.diff > 0 ? 'diff-up' : 'diff-neutral') }}">{{ record.diff < 0 ? -record.diff : record.diff }}</view>
<view class="diff-unit">
kg
<text wx:if="{{ record.diff !== 0 }}" class="diff-arrow {{ record.diff < 0 ? 'arrow-down' : 'arrow-up' }}"></text>
</view>
</view>
<view class="diff-label">{{ record.diff < 0 ? '体重较上次减轻' : (record.diff > 0 ? '体重较上次增加' : '与上次持平') }}</view>
</view>
</view>
<!-- 分割线 -->
<view class="card-divider"></view>
<!-- 身高 / BMI / 标准体重 指标行 -->
<view class="metrics-row">
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">身高</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.height }}</text>
<text class="metric-unit">cm</text>
</view>
</view>
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">BMI</view>
<view class="metric-label-value">{{ record.bmiLabel }}</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.bmi }}</text>
</view>
</view>
<view class="metric-col">
<view class="metric-label">
<view class="metric-label-text">标准体重</view>
</view>
<view class="metric-value-row">
<text class="metric-value">{{ record.stdWeight }}</text>
<text class="metric-unit">kg</text>
</view>
</view>
</view>
</view>
<view class="card-lianjie">
<image src="/images/home/lianjie.png" mode=""/>
</view>
</view>
<!-- 详细数据卡 -->
<view class="detail-card">
<!-- 卡头 -->
<view class="card-header">
<view class="card-header-left">
<view class="card-title-bar"></view>
<view class="card-title">详细数据</view>
</view>
</view>
<!-- 三格数据 -->
<view class="detail-grid">
<view class="detail-item">
<view class="detail-label">体脂率</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bodyFat }}</text>
<text class="detail-unit">%</text>
</view>
</view>
<view class="detail-item">
<view class="detail-label">体脂肪</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bodyFatMass }}</text>
<text class="detail-unit">kg</text>
</view>
</view>
<view class="detail-item">
<view class="detail-label">基础代谢</view>
<view class="detail-value-row">
<text class="detail-value">{{ record.bmr }}</text>
<text class="detail-unit">kcal</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
-4
View File
@@ -1,4 +0,0 @@
{
"usingComponents": {},
"navigationStyle": "custom"
}
-107
View File
@@ -1,107 +0,0 @@
.header {
width: 100%;
height: 320rpx;
margin-bottom: 26rpx;
.bg {
width: 100%;
height: 100%;
}
.user {
position: absolute;
top: 185rpx;
left: 40rpx;
right: 40rpx;
width: 100%;
display: flex;
box-sizing: border-box;
.user-avatar {
width: 120rpx;
height: 120rpx;
margin-right: 30rpx;
border-radius: 24rpx;
color: #FFFFFF;
font-size: 48rpx;
font-weight: bold;
line-height: 110rpx;
text-align: center;
box-sizing: border-box;
border: 5rpx solid #ffffff;
background-color: #1385FA;
}
.user-info {
flex: 1;
display: flex;
flex-flow: column;
justify-content: center;
overflow: hidden;
.user-name {
color: #252536;
height: 40rpx;
font-weight: bold;
font-size: 40rpx;
line-height: 40rpx;
margin-bottom: 22rpx;
}
.user-meta {
height: 28rpx;
display: flex;
align-items: center;
.meta-text {
color: #77849E;
height: 28rpx;
font-size: 28rpx;
line-height: 28rpx;
}
.meta-divider {
color: #B6B6B6;
height: 28rpx;
font-size: 28rpx;
line-height: 28rpx;
margin: 0 12rpx;
}
}
}
}
}
.list {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
.list-item {
width: 100%;
height: 110rpx;
display: flex;
padding-left: 30rpx;
padding-right: 20rpx;
box-sizing: border-box;
align-items: center;
flex-wrap: nowrap;
border-radius: 16rpx;
background-color: #ffffff;
>view:nth-of-type(1) {
width: 38rpx;
height: 38rpx;
margin-right: 22rpx;
}
>view:nth-of-type(2) {
flex: 1;
height: 30rpx;
color: #394356;
font-weight: bold;
font-size: 30rpx;
line-height: 30rpx;
}
}
}
-54
View File
@@ -1,54 +0,0 @@
/** 缓存中的用户信息 */
interface StorageUserInfo {
userId: string
realname: string
sex: number
height: number
birthday: string
[key: string]: any
}
/** 页面展示用用户信息 */
interface PageUserInfo {
realname: string
sex_dictText: '男' | '女' | ''
age: number
height: number
}
/** 从生日字符串(YYYY-MM-DD)计算周岁 */
const calcAge = (birthday: string): number => {
if (!birthday) return 0
const [year, month, day] = birthday.split('-').map(Number)
if (!year || !month || !day) return 0
const now = new Date()
let age = now.getFullYear() - year
if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1
return Math.max(0, age)
}
Page({
data: {
userInfo: null as PageUserInfo | null,
},
onShow() {
const raw = wx.getStorageSync('userInfo') as StorageUserInfo | null
if (!raw?.userId) return
const userInfo: PageUserInfo = {
realname: raw.realname ?? '',
sex_dictText: raw.sex === 1 ? '男' : raw.sex === 2 ? '女' : '',
age: calcAge(raw.birthday),
height: raw.height ?? 0,
}
this.setData({ userInfo })
},
onShareAppMessage(): WechatMiniprogram.Page.ICustomShareContent {
return {
title: '智能蓝牙秤',
path: '/pages/home/home',
imageUrl: '/images/share/share.jpg',
}
},
})
-29
View File
@@ -1,29 +0,0 @@
<view class="header">
<view class="bg">
<image src="/images/my/header.png" mode=""/>
</view>
<view class="user" wx:if="{{userInfo}}">
<view class="user-avatar">{{ userInfo.realname[0] }}</view>
<view class="user-info">
<view class="user-name">{{ userInfo.realname }}</view>
<view class="user-meta">
<text class="meta-text">{{ userInfo.sex_dictText }}</text>
<text class="meta-divider">·</text>
<text class="meta-text">{{ userInfo.age }}岁</text>
<text class="meta-divider">|</text>
<text class="meta-text">{{ userInfo.height }}cm</text>
</view>
</view>
</view>
</view>
<view class="list">
<view class="list-item">
<view>
<image src="/images/my/about.png" mode=""/>
</view>
<view>关于我们</view>
<view class="arrow"></view>
</view>
</view>
@@ -1,4 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "完善个人信息"
"usingComponents": {},
"navigationBarTitleText": "完善个人信息"
}
@@ -1,328 +1,88 @@
import { get, put } from '../../utils/request/index'
import { lefuService } from '../../lefu/index'
import type { DeviceMember } from '../../lefu/index'
import { getUserInfoByIdCard, updateUser, type UserInfo } from '../../api/index'
import { parseIdCard } from '../../utils/common'
/** 表单数据模型 */
interface PersonalForm {
/** 姓名 */
name: string
/** 身份证号 */
idCard: string
/** 性别(由身份证解析,只读) */
gender: string
/** 年龄(由身份证解析,只读) */
age: string
/** 身高(cm) */
height: string
/** 体重(kg) */
weight: string
}
/**
* 身份证解析结果
* sex / birthday 用于接口提交,gender / age 用于页面展示
*/
interface IdCardParsed {
gender: string // '男' | '女' | ''
age: string // 周岁字符串,解析失败为 ''
sex: number // 1=男 2=女,0=解析失败
birthday: string // YYYY-MM-DD,解析失败为 ''
}
/** GET /weighingScale/v3/getUserInfoByIdcard 返回的 result 结构 */
interface UserInfoFromServer {
id: string | null
userId: string
parentUserId: string | null
scaleDeviceId: string
deviceId: string | null
deviceType: string
dataType: number
userType: string | null
idCard: string
height: number
weight: number
birthday: string
sex: number
avatar: string | null
phone: string
thirdId: string | null
realname: string
bmi: number
workNo: string
remark: string | null
relationType: string | null
sn: string
delFlag: number
sex_dictText: string
}
/** PUT /weighingScale/v3/update/user 请求体 */
type UpdateUserPayload = UserInfoFromServer & {
/** storage 中取出的 connectDeviceInfo 对象,JSON.stringify 后传入 */
connectDeviceInfo: string
}
/**
* 完善个人信息页
* 收集姓名、身份证号、身高、体重,身份证满 18 位且姓名非空时自动调接口校验并回填性别年龄
*/
Page({
data: {
form: {
name: '',
idCard: '',
gender: '',
age: '',
height: '',
weight: ''
} as PersonalForm,
/** 正在请求身份证查询接口 */
querying: false,
/** 接口返回的用户信息,提交时透传给更新接口 */
serverResult: null as UserInfoFromServer | null,
/** 表单是否只读(从添加设备流程过来时为 true) */
readonly: false
name: '',
idCard: '',
gender: '',
age: '',
height: '',
weight: '',
serverResult: null as UserInfo | null,
},
onLoad() {
// 已有连接设备 → 从缓存 userInfo 回填表单并禁用编辑
const connectDeviceInfo = wx.getStorageSync('connectDeviceInfo')
if (!connectDeviceInfo || JSON.stringify(connectDeviceInfo) === '{}') return
const userInfo = wx.getStorageSync('userInfo') as Record<string, any> | null
if (!userInfo?.userId) return
const parsed = this.parseIdCard(userInfo.idCard ?? '')
this.setData({
readonly: true,
'form.name': userInfo.realname ?? '',
'form.idCard': userInfo.idCard ?? '',
'form.gender': parsed.gender || userInfo.sex_dictText || '',
'form.age': parsed.age || '',
'form.height': userInfo.height != null ? String(userInfo.height) : '',
'form.weight': userInfo.weight != null ? String(userInfo.weight) : '',
})
},
/**
* 姓名输入
* 如果身份证已合法则联动触发服务端查询
*/
onNameInput(e: WechatMiniprogram.Input) {
const name = e.detail.value
this.setData({ 'form.name': name })
if (!name.trim()) {
this.setData({ serverResult: null })
return
}
if (this.isIdCardValid(this.data.form.idCard)) {
this.fetchUserInfoByIdCard(name.trim(), this.data.form.idCard)
}
this.setData({ name: e.detail.value.trim() })
},
/* 失焦时:清空查询结果 → 重新校验身份证,变更即失效 */
onNameBlur() {
if (!this.data.name) return
this.setData({ serverResult: null })
this.validateIdCard()
},
/**
* 身份证号输入
* 本地解析性别年龄;满足合法格式且姓名非空时触发服务端查询
*/
onIdCardInput(e: WechatMiniprogram.Input) {
const idCard = (e.detail.value || '').trim().toUpperCase()
const parsed = this.parseIdCard(idCard)
this.setData({
'form.idCard': idCard,
'form.gender': parsed.gender,
'form.age': parsed.age
})
if (!parsed.gender) {
// 身份证不合法,清除上次服务端结果
this.setData({ serverResult: null })
return
}
if (this.data.form.name.trim()) {
this.fetchUserInfoByIdCard(this.data.form.name.trim(), idCard)
}
this.setData({ idCard, serverResult: null })
if (idCard.length === 18) this.validateIdCard()
},
/* 独立的身份证合法性校验,合法且姓名已填则请求接口 */
validateIdCard() {
const { idCard, name } = this.data
const { gender, age } = parseIdCard(idCard)
this.setData({ gender, age })
if (gender && name) this.fetchUser()
},
fetchUser() {
getUserInfoByIdCard({
sn: '',
idCard: this.data.idCard,
realname: this.data.name,
}).then((res) => {
if (res.data) {
const { gender, age } = parseIdCard(res.data.idCard)
this.setData({ serverResult: res.data, gender, age })
}
}).catch(() => {})
},
/** 身高输入 */
onHeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.height': e.detail.value })
this.setData({ height: e.detail.value })
},
/** 体重输入 */
onWeightInput(e: WechatMiniprogram.Input) {
this.setData({ 'form.weight': e.detail.value })
this.setData({ weight: e.detail.value })
},
/**
* 校验身份证格式:18 位,前 17 位数字,末位数字或 X
* @param idCard 待校验的身份证号
*/
isIdCardValid(idCard: string): boolean {
return /^\d{17}[\dX]$/.test(idCard)
},
/**
* 解析身份证,返回性别、年龄、sex 数值、出生日期
* @param idCard 18 位身份证号
*/
parseIdCard(idCard: string): IdCardParsed {
const empty: IdCardParsed = { gender: '', age: '', sex: 0, birthday: '' }
if (!this.isIdCardValid(idCard)) return empty
// 性别:第 17 位奇数为男,偶数为女
const genderCode = parseInt(idCard.charAt(16), 10)
const sex = genderCode % 2 === 1 ? 1 : 2
const gender = sex === 1 ? '男' : '女'
// 出生日期:第 7-14 位
const year = parseInt(idCard.substr(6, 4), 10)
const month = parseInt(idCard.substr(10, 2), 10)
const day = parseInt(idCard.substr(12, 2), 10)
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return empty
// 计算周岁
const now = new Date()
let age = now.getFullYear() - year
const nowMonth = now.getMonth() + 1
const nowDay = now.getDate()
if (nowMonth < month || (nowMonth === month && nowDay < day)) age -= 1
if (age < 0 || age > 150) return empty
const mm = String(month).padStart(2, '0')
const dd = String(day).padStart(2, '0')
const birthday = `${year}-${mm}-${dd}`
return { gender, age: String(age), sex, birthday }
},
/**
* 调用服务端接口校验身份证与姓名,成功后用接口返回的身份证重新计算性别年龄
* @param name 用户填写的姓名
* @param idCard 用户填写的身份证号
*/
fetchUserInfoByIdCard(name: string, idCard: string) {
if (this.data.querying) return
this.setData({ querying: true, serverResult: null })
get<UserInfoFromServer>('weighingScale/v3/getUserInfoByIdcard', {
sn: lefuService.deviceInfo?.serialNumber ?? '',
idCard,
realname: name
}, { loading: false })
.then(res => {
const serverResult = res.result
// 用接口返回的身份证重新计算性别和年龄(以服务端为准)
const parsed = this.parseIdCard(serverResult.idCard)
this.setData({
serverResult,
'form.gender': parsed.gender,
'form.age': parsed.age
})
})
.catch(() => {
// 查询失败静默处理,保留本地解析结果
})
.finally(() => {
this.setData({ querying: false })
})
},
/**
* 提交表单
* 校验全部必填项 → 构建 payload(透传 serverResult + 用户手动输入)→ PUT 更新接口
*/
onSubmit() {
const { name, idCard, gender, age, height, weight } = this.data.form
const { name, idCard, gender, height, weight, serverResult } = this.data
const heightNum = parseFloat(height)
const weightNum = parseFloat(weight)
if (!name.trim()) {
wx.showToast({ title: '请填写姓名', icon: 'none' })
return
}
if (idCard.length !== 18) {
wx.showToast({ title: '请填写正确的身份证号', icon: 'none' })
return
}
if (!gender || !age) {
wx.showToast({ title: '身份证号有误,请检查', icon: 'none' })
return
}
if (!height || isNaN(heightNum) || heightNum <= 0) {
wx.showToast({ title: '请填写身高', icon: 'none' })
return
}
if (!name) return wx.showToast({ title: '请填写姓名', icon: 'none' })
if (!gender) return wx.showToast({ title: '请填写正确的身份证号', icon: 'none' })
if (!height || isNaN(heightNum) || heightNum <= 0) return wx.showToast({ title: '请填写身高', icon: 'none' })
if (!serverResult) return wx.showToast({ title: '请先验证姓名与身份证号', icon: 'none' })
const parsed = this.parseIdCard(idCard)
const parsed = parseIdCard(idCard)
// connectDeviceInfo 存储时已 JSON.parse 为对象,提交时还原为 JSON 字符串
const connectDeviceInfoRaw = wx.getStorageSync('connectDeviceInfo')
const connectDeviceInfo: string = connectDeviceInfoRaw ? JSON.stringify(connectDeviceInfoRaw) : ''
let payload: UpdateUserPayload
if (this.data.readonly) {
// readonly 模式:从缓存 userInfo 构建 payload
const userInfo = wx.getStorageSync('userInfo') as Record<string, any>
const { connectDeviceInfo: _c, ...rest } = userInfo
payload = {
...rest,
idCard,
realname: name.trim(),
height: heightNum,
weight: weightNum,
sex: parsed.sex,
birthday: parsed.birthday,
sn: lefuService.deviceInfo?.serialNumber ?? '',
scaleDeviceId: lefuService.deviceInfo?.serialNumber ?? '',
connectDeviceInfo,
} as UpdateUserPayload
} else {
// 正常模式:需要 serverResult
if (!this.data.serverResult) {
wx.showToast({ title: '身份信息未验证,请检查姓名与身份证号', icon: 'none' })
return
}
const serverResult = this.data.serverResult as UserInfoFromServer
payload = {
...serverResult,
idCard,
realname: name.trim(),
height: heightNum,
weight: weightNum,
sex: parsed.sex,
birthday: parsed.birthday,
sn: lefuService.deviceInfo?.serialNumber ?? '',
scaleDeviceId: lefuService.deviceInfo?.serialNumber ?? '',
connectDeviceInfo,
}
}
put<UpdateUserPayload>('weighingScale/v3/update/user', payload)
.then(res => {
const { connectDeviceInfo: _, ...userInfo } = res.result
wx.setStorageSync('userInfo', userInfo)
const mainUser: DeviceMember = {
id: payload.userId,
name: payload.realname,
gender: payload.sex === 1 ? 1 : 0,
age: parseInt(parsed.age, 10) || 0,
height: payload.height,
isSelf: true,
}
wx.showLoading({ title: '加载中...', mask: true })
lefuService.syncMembersToDevice([mainUser])
.then(() => {
wx.hideLoading()
wx.switchTab({ url: '/pages/home/home' })
})
.catch((err: Error) => {
wx.hideLoading()
wx.showToast({ title: err.message || '主用户下发失败', icon: 'none' })
})
})
}
updateUser({
...serverResult,
idCard,
realname: name,
height: heightNum,
weight: weightNum || 0,
sex: parsed.sex,
birthday: parsed.birthday,
}).then((res) => {
wx.setStorageSync('userInfo', res.data)
wx.showToast({ title: '保存成功', icon: 'success' })
}).catch(() => {
wx.showToast({ title: '保存失败,请重试', icon: 'none' })
})
},
})
@@ -1,129 +1,106 @@
<view class="supplement-personal">
<view class="tips">
<view class="tips-icon">!</view>
<text class="tips-text">请完整填写信息,用于精准计算体脂数据</text>
</view>
<!-- 顶部蓝色提示条 -->
<view class="tips">
<view class="tips-icon">!</view>
<text class="tips-text">请完整填写信息,用于精准计算体脂数据</text>
</view>
<view class="form">
<view class="form-card">
<view class="form-row">
<view class="form-label">
<text class="label-text">姓名</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<input class="form-input"
type="text"
maxlength="20"
placeholder="请填写真实姓名"
placeholder-class="form-placeholder"
value="{{ name }}"
bind:input="onNameInput"
bind:blur="onNameBlur" />
</view>
</view>
<view class="form">
<!-- 白色圆角表单卡片 -->
<view class="form-card">
<view class="form-row">
<view class="form-label">
<text class="label-text">身份证号</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<input class="form-input"
type="idcard"
maxlength="18"
placeholder="请填写身份证号"
placeholder-class="form-placeholder"
value="{{ idCard }}"
bind:input="onIdCardInput" />
</view>
</view>
<!-- 姓名 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">姓名</text>
<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" />
</view>
</view>
<view class="form-row">
<view class="form-label">
<text class="label-text">性别</text>
</view>
<view class="form-control">
<view class="form-readonly">
<text wx:if="{{ gender }}" class="readonly-text">{{ gender }}</text>
<text wx:else class="readonly-placeholder">自动生成,无需填写</text>
</view>
</view>
</view>
<!-- 身份证号 -->
<view class="form-row">
<view class="form-label">
<text class="label-text">身份证号</text>
<text class="label-star">*</text>
</view>
<view class="form-control">
<input class="form-input"
type="idcard"
maxlength="18"
placeholder="请填写身份证号"
placeholder-class="form-placeholder"
value="{{ form.idCard }}"
disabled="{{ readonly }}"
bind:input="onIdCardInput" />
</view>
</view>
<view class="form-row">
<view class="form-label">
<text class="label-text">年龄</text>
</view>
<view class="form-control">
<view class="form-readonly form-readonly-with-suffix">
<text wx:if="{{ age }}" class="readonly-text">{{ age }}</text>
<text wx:else class="readonly-placeholder">自动生成,无需填写</text>
<text class="form-suffix">岁</text>
</view>
</view>
</view>
<!-- 性别(由身份证自动生成) -->
<view class="form-row">
<view class="form-label">
<text class="label-text">性别</text>
</view>
<view class="form-control">
<view class="form-readonly">
<block wx:if="{{ form.gender }}">
<text class="readonly-text">{{ form.gender }}</text>
</block>
<block wx:else>
<text class="readonly-placeholder">自动生成,无需填写</text>
</block>
</view>
</view>
</view>
<view class="form-row">
<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="5"
placeholder="请填写身高"
placeholder-class="form-placeholder"
value="{{ height }}"
bind:input="onHeightInput" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<!-- 年龄(由身份证自动生成) -->
<view class="form-row">
<view class="form-label">
<text class="label-text">年龄</text>
</view>
<view class="form-control">
<view class="form-readonly form-readonly-with-suffix">
<block wx:if="{{ form.age }}">
<text class="readonly-text">{{ form.age }}</text>
</block>
<block wx:else>
<text class="readonly-placeholder">自动生成,无需填写</text>
</block>
<text class="form-suffix">岁</text>
</view>
</view>
</view>
<view class="form-row">
<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="{{ weight }}"
bind:input="onWeightInput" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</view>
</view>
<!-- 身高 -->
<view class="form-row">
<view class="form-label">
<text class="label-star">*</text>
<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="5"
placeholder="请填写身高"
placeholder-class="form-placeholder"
value="{{ form.height }}"
disabled="{{ readonly }}"
bind:input="onHeightInput" />
<text class="form-suffix form-suffix-inside">cm</text>
</view>
</view>
</view>
<!-- 体重 -->
<view class="form-row">
<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 }}"
disabled="{{ readonly }}"
bind:input="onWeightInput" />
<text class="form-suffix form-suffix-inside">kg</text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部提交按钮 -->
<view class="submit-btn" bind:tap="onSubmit">完成并进入首页</view>
<view class="submit-btn" bind:tap="onSubmit">完成</view>
</view>