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:
co-authored by
Claude Sonnet 4.6
parent
439f560218
commit
83d909b95b
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "添加用户",
|
||||
"usingComponents": {
|
||||
"select-member-drawer": "/components/selectMemberDrawer/selectMemberDrawer",
|
||||
"user-info-confirm-modal": "/components/userInfoConfirmModal/userInfoConfirmModal"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"select-member-drawer": "/components/selectMemberDrawer/selectMemberDrawer"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' })
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user