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>
@@ -0,0 +1,2 @@
|
||||
export { checkOpenIdLogin, getUserInfoByIdCard, updateUser } from './user'
|
||||
export type { UserInfo, LoginResult } from './user'
|
||||
@@ -0,0 +1,50 @@
|
||||
import { get, post, put } from '../utils/request/index'
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
user: UserInfo | null
|
||||
token: string
|
||||
}
|
||||
|
||||
/** OpenID 登录 */
|
||||
export const checkOpenIdLogin = (code: string) =>
|
||||
post<LoginResult>('weighingScale/v2/checkOpenIdLogin', { code })
|
||||
|
||||
/** 根据身份证号查询用户信息 */
|
||||
export const getUserInfoByIdCard = (params: { sn: string; idCard: string; realname: string }) =>
|
||||
get<UserInfo | null>('weighingScale/v3/getUserInfoByIdcard', params, { loading: false })
|
||||
|
||||
/** 更新用户信息 */
|
||||
export const updateUser = (data: Record<string, any>) =>
|
||||
put<UserInfo>('weighingScale/v3/update/user', data)
|
||||
@@ -1,23 +1,9 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/login/login",
|
||||
"pages/home/home",
|
||||
"pages/connectedDevice/connectedDevice",
|
||||
"pages/connectedWifi/connectedWifi",
|
||||
"pages/equipmentMember/equipmentMember",
|
||||
"pages/supplementPersonal/supplementPersonal",
|
||||
"pages/addMember/addMember",
|
||||
"pages/editMember/editMember",
|
||||
"pages/equipment/equipment",
|
||||
"pages/measurementReport/measurementReport",
|
||||
"pages/data/data",
|
||||
"pages/my/my"
|
||||
"pages/login/login"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "智能体重秤"
|
||||
},
|
||||
"window": {},
|
||||
"style": "v2",
|
||||
"rendererOptions": {
|
||||
"skyline": {
|
||||
@@ -32,7 +18,7 @@
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"plugins": {
|
||||
"ppScale-plugin": {
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.31",
|
||||
"provider": "wx0826af4e4908f524"
|
||||
}
|
||||
},
|
||||
@@ -43,37 +29,5 @@
|
||||
},
|
||||
"requiredPrivateInfos": [
|
||||
"getLocation"
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#000000",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"text": "首页",
|
||||
"iconPath": "images/tabBar/home_default.png",
|
||||
"selectedIconPath": "images/tabBar/home.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/equipment/equipment",
|
||||
"text": "设备",
|
||||
"iconPath": "images/tabBar/equipment_default.png",
|
||||
"selectedIconPath": "images/tabBar/equipment.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/data/data",
|
||||
"text": "数据",
|
||||
"iconPath": "images/tabBar/data_default.png",
|
||||
"selectedIconPath": "images/tabBar/data.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/my",
|
||||
"text": "我的",
|
||||
"iconPath": "images/tabBar/my_default.png",
|
||||
"selectedIconPath": "images/tabBar/my.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
/**app.wxss**/
|
||||
@import "./styles/index.less";
|
||||
|
||||
page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg);
|
||||
/* 左右边距不加全局,避免全屏元素被挤 */
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
/* 需要页面标准边距的业务容器,在 WXML 根节点使用 */
|
||||
.page-container {
|
||||
padding-left: var(--page-padding-x);
|
||||
padding-right: var(--page-padding-x);
|
||||
padding-left: 30rpx;
|
||||
padding-right: 30rpx;
|
||||
}
|
||||
|
||||
image {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
App<IAppOption>({
|
||||
globalData: {
|
||||
/** 当前已连接设备的蓝牙状态,页面可读取 */
|
||||
connectState: '' as string,
|
||||
},
|
||||
globalData: {},
|
||||
|
||||
onLaunch() {
|
||||
// 已登录则跳过登录页
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
const connectDeviceInfo = wx.getStorageSync('connectDeviceInfo')
|
||||
const hasDevice = connectDeviceInfo && Object.keys(connectDeviceInfo).length > 0
|
||||
wx.reLaunch({
|
||||
url: hasDevice ? '/pages/home/home' : '/pages/connectedDevice/connectedDevice'
|
||||
onLaunch() {},
|
||||
|
||||
getWxLoginCode(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res.code)
|
||||
} else {
|
||||
reject(new Error('获取登录凭证失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '获取登录凭证失败'))
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
/* 遮罩层 */
|
||||
.drawer-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(37, 37, 53, 0.6);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抽屉面板 */
|
||||
.drawer-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/* 最大高度约 75% 屏幕 */
|
||||
height: 85vh;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&--visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽把手 */
|
||||
.drawer-handle {
|
||||
width: 72rpx;
|
||||
height: 6rpx;
|
||||
background-color: #77849E;
|
||||
border-radius: 3rpx;
|
||||
margin: 32rpx auto 40rpx;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.drawer-header {
|
||||
width: 100%;
|
||||
height: 32rpx;
|
||||
padding: 0 30rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.drawer-title {
|
||||
color: #252535;
|
||||
height: 32rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
|
||||
.drawer-close {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.drawer-close-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
position: relative;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 3rpx;
|
||||
background-color: #252535;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部分隔线 */
|
||||
.drawer-divider {
|
||||
width: 690rpx;
|
||||
height: 2rpx;
|
||||
background-color: #EAEDF1;
|
||||
margin: 39rpx 30rpx 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 用户滚动列表 */
|
||||
.drawer-list {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 用户行 */
|
||||
.member-item {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 24rpx;
|
||||
|
||||
/* 有图片头像 */
|
||||
|
||||
.member-avatar {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--selected {
|
||||
border: 3rpx solid #50A6FF;
|
||||
}
|
||||
}
|
||||
|
||||
/* 纯色占位头像 */
|
||||
|
||||
.member-avatar-placeholder {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* 男:蓝色 */
|
||||
|
||||
&--male {
|
||||
background-color: #C6E6FF;
|
||||
}
|
||||
|
||||
/* 女:粉色 */
|
||||
|
||||
&--female {
|
||||
background-color: #FFD5E3;
|
||||
}
|
||||
|
||||
/* 选中男 */
|
||||
|
||||
&--selected-male {
|
||||
border: 3rpx solid #50A6FF;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin: -3rpx 0 0 -3rpx;
|
||||
}
|
||||
|
||||
/* 选中女 */
|
||||
|
||||
&--selected-female {
|
||||
border: 3rpx solid #FF8FB5;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin: -3rpx 0 0 -3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 姓名 + 副标题 */
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
height: 78rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 23rpx;
|
||||
|
||||
.member-name {
|
||||
color: #333333;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
line-height: 32rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-sub {
|
||||
color: #77849E;
|
||||
font-size: 26rpx;
|
||||
line-height: 34rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.member-type {
|
||||
height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.member-type-tag {
|
||||
height: 40rpx;
|
||||
padding: 0 14rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 40rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.member-type-tag--self {
|
||||
color: #1385FA;
|
||||
background-color: rgba(19, 133, 250, 0.12);
|
||||
}
|
||||
|
||||
.member-type-tag--employee {
|
||||
color: #10B981;
|
||||
background-color: rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
.member-type-tag--family {
|
||||
color: #F59E0B;
|
||||
background-color: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 行间水平分隔线 */
|
||||
.member-separator {
|
||||
width: 690rpx;
|
||||
height: 2rpx;
|
||||
background-color: #EAEDF1;
|
||||
margin: 24rpx 30rpx 0;
|
||||
}
|
||||
|
||||
/* 空态占位 */
|
||||
.drawer-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 80rpx;
|
||||
|
||||
.drawer-empty-icon {
|
||||
width: 193rpx;
|
||||
height: 138rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.drawer-empty-text {
|
||||
color: #808080;
|
||||
font-size: 28rpx;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { get } from '../../utils/request/index'
|
||||
import { lefuService } from '../../lefu/index'
|
||||
|
||||
/** GET /weighingScale/v3/select/addMembers/user 返回的单条结构 */
|
||||
interface AddMemberApiItem {
|
||||
id: string
|
||||
realname: string
|
||||
sex_dictText: '男' | '女'
|
||||
birthday: string
|
||||
height: number
|
||||
weight: number
|
||||
idCard: string | null
|
||||
avatar: string | null
|
||||
dataType?: number
|
||||
relationType?: number | string | null
|
||||
}
|
||||
|
||||
/** GET /weighingScale/v3/select/history/user 返回的单条结构 */
|
||||
interface HistoryApiItem {
|
||||
id: string
|
||||
userId: string
|
||||
realname: string
|
||||
sex: number
|
||||
height: number
|
||||
birthday: string
|
||||
avatar: string | null
|
||||
relationType: number | string | null
|
||||
dataType?: number
|
||||
}
|
||||
|
||||
/** 抽屉内部用户项(含回填所需全量字段) */
|
||||
interface ExistingMember {
|
||||
id: string
|
||||
name: string
|
||||
gender: '男' | '女'
|
||||
age: number
|
||||
height: number
|
||||
weight: number
|
||||
idCard: string
|
||||
avatar?: string
|
||||
selected?: boolean
|
||||
/** history 模式额外携带,供页面构建 userInfo */
|
||||
userId?: string
|
||||
sex?: number
|
||||
birthday?: string
|
||||
relationType?: number | string | null
|
||||
/** 0=员工 1=家庭成员 */
|
||||
dataType?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择已有用户抽屉组件
|
||||
* Properties: show(控制显示)
|
||||
* Events: close(关闭), select(选中用户,携带 ExistingMember)
|
||||
*/
|
||||
Component({
|
||||
properties: {
|
||||
/** 是否显示抽屉 */
|
||||
show: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
/** 数据来源模式:addMember=新增用户选择 / history=历史数据切换用户 */
|
||||
source: {
|
||||
type: String,
|
||||
value: 'addMember'
|
||||
},
|
||||
/** 需要排除的用户 ID(history 模式下排除当前查看的用户) */
|
||||
excludeUserId: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
/** 控制 wx:if,DOM 是否存在 */
|
||||
innerShow: false,
|
||||
/** 控制 --visible 类,触发 CSS transition */
|
||||
animVisible: false,
|
||||
/** 用户列表(组件内部拉取) */
|
||||
members: [] as ExistingMember[]
|
||||
},
|
||||
|
||||
observers: {
|
||||
/**
|
||||
* 监听外部 show 变化:
|
||||
* 打开时先拉数据再展示抽屉;关闭时移除动画类后移除 DOM
|
||||
*/
|
||||
show(val: boolean) {
|
||||
if (val) {
|
||||
this._fetchMembers()
|
||||
this.setData({ innerShow: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ animVisible: true })
|
||||
}, 20)
|
||||
} else {
|
||||
this.setData({ animVisible: false })
|
||||
setTimeout(() => {
|
||||
this.setData({ innerShow: false })
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 从生日字符串(YYYY-MM-DD)计算周岁 */
|
||||
_calcAge(birthday: string): number {
|
||||
if (!birthday) return 0
|
||||
const [year, month, day] = birthday.split('-').map(Number)
|
||||
if (!year || !month || !day) return 0
|
||||
const now = new Date()
|
||||
let age = now.getFullYear() - year
|
||||
if (now.getMonth() + 1 < month || (now.getMonth() + 1 === month && now.getDate() < day)) age -= 1
|
||||
return Math.max(0, age)
|
||||
},
|
||||
|
||||
/** 调接口获取可选用户列表,根据 source 调不同接口 */
|
||||
_fetchMembers() {
|
||||
const userInfo = wx.getStorageSync('userInfo') as { userId?: string } | null
|
||||
const userId = userInfo?.userId ?? ''
|
||||
|
||||
wx.showLoading({ title: '加载中...', mask: true })
|
||||
|
||||
if (this.properties.source === 'history') {
|
||||
const params: Record<string, string> = { userId }
|
||||
if (this.properties.excludeUserId) {
|
||||
params.userIdExclude = this.properties.excludeUserId
|
||||
}
|
||||
get<HistoryApiItem[]>('weighingScale/v3/select/history/user', params, { loading: false })
|
||||
.then((res: any) => {
|
||||
const members: ExistingMember[] = (res.result ?? []).map((item: HistoryApiItem) => ({
|
||||
id: item.id,
|
||||
name: item.realname,
|
||||
gender: item.sex === 1 ? '男' : '女',
|
||||
age: this._calcAge(item.birthday),
|
||||
height: item.height,
|
||||
weight: 0,
|
||||
idCard: '',
|
||||
avatar: item.avatar ?? '',
|
||||
userId: item.userId,
|
||||
sex: item.sex,
|
||||
birthday: item.birthday,
|
||||
relationType: item.relationType,
|
||||
dataType: item.dataType,
|
||||
}))
|
||||
this.setData({ members })
|
||||
})
|
||||
.finally(() => {
|
||||
wx.hideLoading()
|
||||
})
|
||||
} else {
|
||||
const sn = lefuService.deviceInfo?.serialNumber ?? ''
|
||||
get<AddMemberApiItem[]>('weighingScale/v5/select/addMembers/user', { userId, sn }, { loading: false })
|
||||
.then((res: any) => {
|
||||
const members: ExistingMember[] = (res.result ?? []).map((item: AddMemberApiItem) => ({
|
||||
id: item.id,
|
||||
name: item.realname,
|
||||
gender: item.sex_dictText,
|
||||
age: this._calcAge(item.birthday),
|
||||
height: item.height,
|
||||
weight: item.weight,
|
||||
idCard: item.idCard ?? '',
|
||||
avatar: item.avatar ?? '',
|
||||
relationType: item.relationType,
|
||||
dataType: item.dataType,
|
||||
}))
|
||||
this.setData({ members })
|
||||
})
|
||||
.finally(() => {
|
||||
wx.hideLoading()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 点击遮罩关闭 */
|
||||
onTapMask() {
|
||||
this.triggerEvent('close')
|
||||
},
|
||||
|
||||
/** 点击关闭按钮 */
|
||||
onTapClose() {
|
||||
this.triggerEvent('close')
|
||||
},
|
||||
|
||||
/**
|
||||
* 点击用户行,触发 select 事件
|
||||
* @param e 携带 data-member 的点击事件
|
||||
*/
|
||||
onTapMember(e: WechatMiniprogram.TouchEvent) {
|
||||
const member = e.currentTarget.dataset.member as ExistingMember
|
||||
this.triggerEvent('select', member)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
<!-- 选择已有用户抽屉 -->
|
||||
<block wx:if="{{ innerShow }}">
|
||||
<!-- 遮罩 -->
|
||||
<view class="drawer-mask {{ animVisible ? 'drawer-mask--visible' : '' }}" bind:tap="onTapMask"></view>
|
||||
|
||||
<!-- 抽屉面板 -->
|
||||
<view class="drawer-panel {{ animVisible ? 'drawer-panel--visible' : '' }}">
|
||||
|
||||
<!-- 顶部拖拽把手 -->
|
||||
<view class="drawer-handle"></view>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<view class="drawer-header">
|
||||
<text class="drawer-title">选择已有用户</text>
|
||||
<view class="drawer-close" bind:tap="onTapClose">
|
||||
<view class="drawer-close-icon"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<view class="drawer-divider"></view>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<block wx:if="{{ members.length }}">
|
||||
<scroll-view class="drawer-list" scroll-y>
|
||||
<block wx:for="{{ members }}" wx:key="id">
|
||||
<view class="member-item" bind:tap="onTapMember" data-member="{{ item }}">
|
||||
<!-- 头像 -->
|
||||
<block wx:if="{{ item.avatar }}">
|
||||
<image class="member-avatar {{ item.selected ? 'member-avatar--selected' : '' }}"
|
||||
src="{{ item.avatar }}" mode="aspectFill"></image>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="member-avatar-placeholder {{ item.gender === '女' ? 'member-avatar-placeholder--female' : 'member-avatar-placeholder--male' }} {{ item.selected ? 'member-avatar-placeholder--selected-male' : '' }} {{ item.selected && item.gender === '女' ? 'member-avatar-placeholder--selected-female' : '' }}"></view>
|
||||
</block>
|
||||
|
||||
<!-- 姓名 + 性别·年龄 -->
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ item.name }}</text>
|
||||
<text class="member-sub">{{ item.gender }}·{{ item.age }}岁 丨 {{ item.height }}cm</text>
|
||||
</view>
|
||||
|
||||
<view class="member-type">
|
||||
<block wx:if="{{ item.relationType == 1 }}">
|
||||
<view class="member-type-tag member-type-tag--self">本人</view>
|
||||
</block>
|
||||
<block wx:elif="{{ item.dataType === 0 }}">
|
||||
<view class="member-type-tag member-type-tag--employee">员工</view>
|
||||
</block>
|
||||
<block wx:elif="{{ item.dataType === 1 }}">
|
||||
<view class="member-type-tag member-type-tag--family">家庭用户</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 水平分隔线 -->
|
||||
<view class="member-separator"></view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</block>
|
||||
|
||||
<!-- 无用户:占位 -->
|
||||
<block wx:else>
|
||||
<view class="drawer-empty">
|
||||
<image class="drawer-empty-icon" src="/images/selectMemberDrawer/noData.png" mode="aspectFit"/>
|
||||
<text class="drawer-empty-text">暂无可选用户</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
.confirm-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.anim-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 660rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.anim-visible {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding-top: 50rpx;
|
||||
width: 100%;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.modal-title {
|
||||
width: 100%;
|
||||
height: 36rpx;
|
||||
font-weight: bolder;
|
||||
font-size: 36rpx;
|
||||
color: #252535;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.modal-sub-title {
|
||||
width: 100%;
|
||||
height: 32rpx;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #1385FA;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.modal-body-box {
|
||||
width: 100%;
|
||||
padding: 0 30rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16rpx;
|
||||
background-color: #F6F6F6;
|
||||
|
||||
.info-row {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
padding: 10rpx 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #EAECF1;
|
||||
|
||||
.info-label {
|
||||
color: #77849E;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #252535;
|
||||
height: 80rpx;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
width: 100%;
|
||||
height: 110rpx;
|
||||
display: flex;
|
||||
border-top: 1rpx solid #EAECF1;
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 110rpx;
|
||||
line-height: 110rpx;
|
||||
font-size: 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
color: #77849E;
|
||||
border-right: 1rpx solid #EAECF1;
|
||||
}
|
||||
|
||||
.modal-btn-confirm {
|
||||
color: #1385FA;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* 用户信息确认弹框组件
|
||||
* Properties: show(控制显示), userInfo(用户信息)
|
||||
* Events: confirm(确认), cancel(取消)
|
||||
*/
|
||||
Component({
|
||||
properties: {
|
||||
/** 是否显示弹框 */
|
||||
show: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
/** 用户信息数据 */
|
||||
userInfo: {
|
||||
type: Object,
|
||||
value: {}
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
/** 控制 wx:if,DOM 是否存在 */
|
||||
innerShow: false,
|
||||
/** 控制 --visible 类,触发 CSS transition */
|
||||
animVisible: false
|
||||
},
|
||||
|
||||
observers: {
|
||||
/**
|
||||
* 监听外部 show 变化:
|
||||
* 打开时先展示弹框再执行动画;关闭时移除动画类后移除 DOM
|
||||
*/
|
||||
show(val: boolean) {
|
||||
if (val) {
|
||||
this.setData({ innerShow: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ animVisible: true })
|
||||
}, 20)
|
||||
} else {
|
||||
this.setData({ animVisible: false })
|
||||
setTimeout(() => {
|
||||
this.setData({ innerShow: false })
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 点击遮罩关闭 */
|
||||
onTapMask() {
|
||||
this.triggerEvent('cancel')
|
||||
},
|
||||
|
||||
/** 点击取消按钮 */
|
||||
onTapCancel() {
|
||||
this.triggerEvent('cancel')
|
||||
},
|
||||
|
||||
/** 点击确认按钮 */
|
||||
onTapConfirm() {
|
||||
this.triggerEvent('confirm')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
<view class="confirm-modal" wx:if="{{ innerShow }}">
|
||||
<!-- 遮罩层 -->
|
||||
<view class="modal-mask {{ animVisible ? 'anim-visible' : '' }}" bind:tap="onTapMask"></view>
|
||||
|
||||
<!-- 弹框内容 -->
|
||||
<view class="modal-content {{ animVisible ? 'anim-visible' : '' }}">
|
||||
<!-- 标题 -->
|
||||
<view class="modal-header">
|
||||
<view class="modal-title">当前身份证号已存在</view>
|
||||
<view class="modal-sub-title">是否直接复用信息?</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息展示 -->
|
||||
<view class="modal-body" wx:if="{{ userInfo }}">
|
||||
<view class="modal-body-box">
|
||||
<view class="info-row">
|
||||
<text class="info-label">姓名:</text>
|
||||
<text class="info-value">{{ userInfo.realname }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">身份证号:</text>
|
||||
<text class="info-value">{{ userInfo.idCard }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">性别:</text>
|
||||
<text class="info-value">{{ userInfo.sex_dictText }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">身高:</text>
|
||||
<text class="info-value">{{ userInfo.height }}cm</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">体重:</text>
|
||||
<text class="info-value">{{ userInfo.weight }}kg</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn modal-btn-cancel" bind:tap="onTapCancel">取消</view>
|
||||
<view class="modal-btn modal-btn-confirm" bind:tap="onTapConfirm">确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 745 B |
|
Before Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 987 B |
|
Before Width: | Height: | Size: 983 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,12 +0,0 @@
|
||||
export { lefuService } from './service'
|
||||
export type {
|
||||
ScannedDevice,
|
||||
LeFuWifiItem,
|
||||
ProgressData,
|
||||
LockData,
|
||||
LeFuConfig,
|
||||
DeviceSetting,
|
||||
DeviceMember,
|
||||
DeviceInfo,
|
||||
FirmwareVersion,
|
||||
} from './types'
|
||||
@@ -1,488 +0,0 @@
|
||||
import { get } from '../utils/request/index'
|
||||
import type {
|
||||
RawDevice,
|
||||
LeFuPlugin,
|
||||
ScannedDevice,
|
||||
LeFuWifiItem,
|
||||
ProgressData,
|
||||
LockData,
|
||||
LeFuConfig,
|
||||
DeviceSetting,
|
||||
DeviceMember,
|
||||
DeviceInfo,
|
||||
FirmwareVersion,
|
||||
} from './types'
|
||||
|
||||
/** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */
|
||||
const plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
|
||||
|
||||
/** 乐福 SDK 配置 */
|
||||
const LEFU_CONFIG: LeFuConfig = {
|
||||
key: 'lefudc91611833e18d94',
|
||||
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
|
||||
// domain1: 'http://10.10.10.10:8889',
|
||||
// domain2: 'http://10.10.10.10:8889/weight',
|
||||
domain1: 'http://device.shuziweidao.com:80/gateway',
|
||||
domain2: 'http://device.shuziweidao.com:80/weight',
|
||||
}
|
||||
|
||||
/** 支持的设备型号配置列表(5款) */
|
||||
const DEVICE_SETTINGS: DeviceSetting[] = [
|
||||
{
|
||||
advLength: 999, calorieStatus: 0, createBy: null,
|
||||
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
|
||||
deviceFuncType: 223, deviceName: 'CF568_G', devicePowerType: 3,
|
||||
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
|
||||
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||
},
|
||||
{
|
||||
advLength: 999, calorieStatus: 0, createBy: null,
|
||||
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
|
||||
deviceFuncType: 223, deviceName: 'YX-B4-568-BW', devicePowerType: 3,
|
||||
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
|
||||
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||
},
|
||||
{
|
||||
advLength: 999, calorieStatus: 0, createBy: null,
|
||||
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||
deviceFuncType: 65759, deviceName: 'CF636_G', devicePowerType: 3,
|
||||
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
|
||||
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
|
||||
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||
},
|
||||
{
|
||||
advLength: 999, calorieStatus: 0, createBy: null,
|
||||
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||
deviceFuncType: 65759, deviceName: 'YX-B8-636-BW', devicePowerType: 3,
|
||||
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
|
||||
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
|
||||
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||
},
|
||||
{
|
||||
advLength: 999, calorieStatus: 0, createBy: null,
|
||||
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
|
||||
deviceFuncType: 223, deviceName: 'CF577', devicePowerType: 3,
|
||||
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
|
||||
id: 51, imgUrl: null, macAddressStart: 6, remark: null,
|
||||
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
|
||||
},
|
||||
]
|
||||
|
||||
const TAG = '[LeFuService]'
|
||||
|
||||
class LeFuService {
|
||||
private _activeProtocol: any = null
|
||||
private _connectState = ''
|
||||
private _deviceInfo: DeviceInfo | null = null
|
||||
private _busSubscribed = false
|
||||
private _intentionalConnect = false
|
||||
|
||||
private _reconnectCount = 0
|
||||
private readonly _maxReconnect = 3
|
||||
private _reconnectTimer: number | null = null
|
||||
private _keepAliveTimer: number | null = null
|
||||
private _lastRawDevice: RawDevice | null = null
|
||||
|
||||
private _onDevicesListCb: ((devices: ScannedDevice[]) => void) | null = null
|
||||
private _onConnectStateCb: ((state: string) => void) | null = null
|
||||
private _onDeviceConnectCb: (() => void) | null = null
|
||||
private _onProgressCb: ((data: ProgressData) => void) | null = null
|
||||
private _onLockedCb: ((data: LockData) => void) | null = null
|
||||
private _onDisconnectedCb: (() => void) | null = null
|
||||
private _onDeviceInfoCb: ((info: DeviceInfo) => void) | null = null
|
||||
|
||||
get config(): LeFuConfig { return LEFU_CONFIG }
|
||||
get isConnected(): boolean { return !!this._activeProtocol }
|
||||
get connectState(): string { return this._connectState }
|
||||
get BLUE_STATE(): Record<string, string> { return plugin.BLUE_STATE ?? {} }
|
||||
get deviceInfo(): DeviceInfo | null { return this._deviceInfo }
|
||||
|
||||
// ─── 回调注册 ─────────────────────────────────
|
||||
onDevicesList(cb: (devices: ScannedDevice[]) => void): void { this._onDevicesListCb = cb }
|
||||
onConnectState(cb: (state: string) => void): void { this._onConnectStateCb = cb }
|
||||
onDeviceConnect(cb: () => void): void { this._onDeviceConnectCb = cb }
|
||||
onMeasuring(cb: (data: ProgressData) => void): void { this._onProgressCb = cb }
|
||||
onLocked(cb: (data: LockData) => void): void { this._onLockedCb = cb }
|
||||
onDisconnected(cb: () => void): void { this._onDisconnectedCb = cb }
|
||||
onDeviceInfo(cb: (info: DeviceInfo) => void): void { this._onDeviceInfoCb = cb }
|
||||
|
||||
// ─── 扫描与连接 ───────────────────────────────
|
||||
startScan(): void {
|
||||
console.log(TAG, 'startScan()')
|
||||
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
|
||||
this._subscribeBus()
|
||||
plugin.Blue.start(DEVICE_SETTINGS.map(s => s.deviceName), false)
|
||||
}
|
||||
|
||||
private _subscribeBus(): void {
|
||||
if (this._busSubscribed) {
|
||||
console.log(TAG, 'bus 已订阅,跳过')
|
||||
return
|
||||
}
|
||||
this._busSubscribed = true
|
||||
|
||||
plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => {
|
||||
console.log(TAG, 'bus[devicesList],设备数:', devList?.length, devList)
|
||||
if (!devList?.length) {
|
||||
this._onDevicesListCb?.([])
|
||||
return
|
||||
}
|
||||
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
|
||||
name: item.name || item.deviceName || '未知设备',
|
||||
raw: item,
|
||||
model: {},
|
||||
}))
|
||||
this._onDevicesListCb?.(devices)
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('devicesModel', (res: any) => {
|
||||
console.log(TAG, 'bus[devicesModel] 完整数据:', JSON.stringify(res))
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('deviceInfo', (res: DeviceInfo) => {
|
||||
console.log(TAG, 'bus[deviceInfo]:', res)
|
||||
this._deviceInfo = res
|
||||
this._onDeviceInfoCb?.(res)
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('deviceConnect', () => {
|
||||
console.log(TAG, 'bus[deviceConnect]')
|
||||
this._intentionalConnect = false
|
||||
this._reconnectCount = 0
|
||||
plugin.ScaleAction.startDataProgress(true)
|
||||
this._activeProtocol = plugin.ScaleAction.getActiveProtocol()
|
||||
this._activeProtocol.codeUpdateMTU((mtuRes: any) => {
|
||||
console.log(TAG, 'codeUpdateMTU:', mtuRes)
|
||||
this._activeProtocol.codeSyncTime((syncRes: any) => {
|
||||
console.log(TAG, 'codeSyncTime:', syncRes)
|
||||
this._onDeviceConnectCb?.()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('connectState', (res: string) => {
|
||||
console.log(TAG, 'bus[connectState]:', res)
|
||||
this._connectState = res
|
||||
this._onConnectStateCb?.(res)
|
||||
if (res === plugin.BLUE_STATE.CONNECTSUCCESS) {
|
||||
this._startKeepAlive()
|
||||
return
|
||||
}
|
||||
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
|
||||
this._stopKeepAlive()
|
||||
if (this._intentionalConnect) {
|
||||
this._intentionalConnect = false
|
||||
} else {
|
||||
this._doReconnect()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 蓝牙不可用:做完整清理
|
||||
if (res === plugin.BLUE_STATE.UNAVAILABLE) {
|
||||
this._stopKeepAlive()
|
||||
this._stopReconnectTimer()
|
||||
if (this._activeProtocol) {
|
||||
this._activeProtocol = null
|
||||
this._onDisconnectedCb?.()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 蓝牙就绪:如有缓存设备且非主动断开,尝试连接
|
||||
if (res === plugin.BLUE_STATE.READY) {
|
||||
if (this._intentionalConnect) {
|
||||
this._intentionalConnect = false
|
||||
} else if (this._lastRawDevice && !this._activeProtocol) {
|
||||
this._stopReconnectTimer()
|
||||
wx.openBluetoothAdapter({
|
||||
success: () => this.connect(this._lastRawDevice!),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('progressData', (res: ProgressData) => {
|
||||
this._onProgressCb?.(res)
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('lockData', (res: LockData) => {
|
||||
this._onLockedCb?.(res)
|
||||
})
|
||||
|
||||
plugin.bus.subscribe('deviceWillDisconnect', () => {
|
||||
console.warn(TAG, 'bus[deviceWillDisconnect],intentionalConnect:', this._intentionalConnect)
|
||||
this._stopKeepAlive()
|
||||
if (this._activeProtocol) {
|
||||
this._activeProtocol = null
|
||||
this._onDisconnectedCb?.()
|
||||
}
|
||||
if (!this._intentionalConnect) {
|
||||
this._doReconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connect(rawDevice: RawDevice): void {
|
||||
console.log(TAG, 'connect():', rawDevice.name)
|
||||
this._lastRawDevice = rawDevice
|
||||
this._intentionalConnect = true
|
||||
this._stopKeepAlive()
|
||||
this._stopReconnectTimer()
|
||||
// 先断开旧连接(此时状态仍在),再清空内部状态
|
||||
if (this.isConnected) {
|
||||
plugin.Blue.disconnect()
|
||||
}
|
||||
this._activeProtocol = null
|
||||
this._connectState = ''
|
||||
this._deviceInfo = null
|
||||
// 直连缓存设备时不走 startScan,需手动初始化插件设备配置和总线订阅
|
||||
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
|
||||
this._subscribeBus()
|
||||
plugin.Blue.stopBluetoothDevicesDiscovery()
|
||||
plugin.Blue.clearProtocol()
|
||||
plugin.Blue.createBLEConnection(rawDevice)
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
console.log(TAG, 'disconnect()')
|
||||
this._intentionalConnect = true
|
||||
this._stopKeepAlive()
|
||||
plugin.Blue.disconnect()
|
||||
}
|
||||
|
||||
/** 读缓存设备信息,自动扫描匹配后连接,无缓存时直接返回 */
|
||||
autoConnect(): void {
|
||||
|
||||
const cached = wx.getStorageSync('connectDeviceInfo') as RawDevice | null
|
||||
const targetDeviceId = cached?.deviceId as string | undefined
|
||||
if (!targetDeviceId) return
|
||||
|
||||
let scanTimer: number | null = null
|
||||
|
||||
wx.openBluetoothAdapter({
|
||||
success: () => {
|
||||
scanTimer = setTimeout(() => {
|
||||
this.stopScan()
|
||||
}, 15000)
|
||||
|
||||
this.onDevicesList((devices) => {
|
||||
const matched = devices.find(d => d.raw.deviceId === targetDeviceId)
|
||||
if (!matched) return
|
||||
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
|
||||
this.stopScan()
|
||||
wx.setStorageSync('connectDeviceInfo', matched.raw)
|
||||
this.connect(matched.raw)
|
||||
})
|
||||
this.startScan()
|
||||
},
|
||||
fail: () => {
|
||||
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
stopScan(): void {
|
||||
console.log(TAG, 'stopScan()')
|
||||
this._stopReconnectTimer()
|
||||
wx.stopBluetoothDevicesDiscovery()
|
||||
this._onDevicesListCb = null
|
||||
}
|
||||
|
||||
// ─── WiFi 配网 ────────────────────────────────
|
||||
getWifiList(): Promise<LeFuWifiItem[]> {
|
||||
console.log(TAG, 'getWifiList()')
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) {
|
||||
reject(new Error('设备未连接,无法获取 WiFi 列表'))
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => reject(new Error('获取 WiFi 列表超时')), 15000)
|
||||
setTimeout(() => {
|
||||
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
|
||||
clearTimeout(timer)
|
||||
console.log(TAG, 'getWifiList 结果:', res)
|
||||
if (!res?.length) {
|
||||
this._activeProtocol.dataExitWifiConfig(() => { })
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
resolve(res.map((item: any) => ({ ssid: item.ssid || item.name || '', signal: item.signal })))
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
configWifi(ssid: string, password: string): Promise<void> {
|
||||
const version = (this._deviceInfo?.modelNumber ?? '').split('-').length === 5 ? 'domain2' : 'domain1'
|
||||
const startTs = Date.now()
|
||||
console.log(TAG, 'configWifi():', ssid, version, 'ts=', startTs, 'activeProtocol=', !!this._activeProtocol)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) {
|
||||
console.warn(TAG, 'configWifi 早期 reject:_activeProtocol 为空')
|
||||
reject(new Error('设备未连接,无法配网'))
|
||||
return
|
||||
}
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(TAG, 'configWifi 超时触发 elapsed=', Date.now() - startTs, 'settled=', settled)
|
||||
if (settled) return
|
||||
settled = true
|
||||
reject(new Error('配网超时,请重试'))
|
||||
}, 20000)
|
||||
console.log(TAG, 'configWifi 已注册 20s 定时器 timerId=', timer)
|
||||
const callback = (res: number, ...extra: any[]) => {
|
||||
console.log(TAG, 'configWifi 回调 res=', res, 'extra=', extra, 'elapsed=', Date.now() - startTs, 'settled=', settled)
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
res === 23 ? resolve() : reject(new Error(`配网失败,错误码:${res}`))
|
||||
}
|
||||
if (version === 'domain1') {
|
||||
console.log(TAG, 'domain1 dataConfigNetWork')
|
||||
this._activeProtocol.dataConfigNetWork({ domain: LEFU_CONFIG.domain1, ssid, password }, callback)
|
||||
} else {
|
||||
console.log(TAG, 'domain2 dataConfigUserNetWork')
|
||||
this._activeProtocol.dataConfigUserNetWork(
|
||||
{ domain: LEFU_CONFIG.domain2, ssid, password, userName: 'apiUser', userPassword: '3acebb95eb49577e9c2a2082589b9bd6' },
|
||||
callback,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkWifiConfig(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||
this._activeProtocol.codeFetchWifiConfig((res: number | undefined | null) => {
|
||||
resolve(res === 0x01)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ─── OTA 升级 ─────────────────────────────────
|
||||
checkAndUpgrade(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||
if (!this._deviceInfo) { reject(new Error('固件信息未就绪')); return }
|
||||
|
||||
const { firmwareRevision, modelNumber } = this._deviceInfo
|
||||
const [mcuVer, bleVer, wifiVer, resVer] = (firmwareRevision || '').split('.')
|
||||
const isServerNewer = (dv: string, sv: string) => {
|
||||
const dn = parseInt(dv, 10)
|
||||
const sn = (sv || '').split('.').map(v => parseInt(v, 10)).reduce((a, v) => a * 1000 + v, 0)
|
||||
return sn > dn
|
||||
}
|
||||
|
||||
get<FirmwareVersion>('weighingScale/v2/firmware/version', { type: modelNumber }, { loading: false })
|
||||
.then(res => {
|
||||
const { mcuVersion, bleVersion, wifiVersion, resVersion } = res.result
|
||||
const needUpdate = isServerNewer(mcuVer, mcuVersion) || isServerNewer(bleVer, bleVersion)
|
||||
|| isServerNewer(wifiVer, wifiVersion) || isServerNewer(resVer, resVersion)
|
||||
if (!needUpdate) { resolve(false); return }
|
||||
this._activeProtocol.codeOtaUpdate((status: number) => {
|
||||
console.log(`codeOtaUpdatecodeOtaUpdatecodeOtaUpdate ${status}`)
|
||||
status != 0 ? reject(new Error('请求失败')) : resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── 私有:保活与重连 ─────────────────────────
|
||||
private _startKeepAlive(): void {
|
||||
this._stopKeepAlive()
|
||||
this._keepAliveTimer = setInterval(() => { this._activeProtocol?.sendKeepAliveCode?.() }, 15000)
|
||||
}
|
||||
|
||||
private _stopKeepAlive(): void {
|
||||
if (this._keepAliveTimer !== null) {
|
||||
clearInterval(this._keepAliveTimer)
|
||||
this._keepAliveTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
private _stopReconnectTimer(): void {
|
||||
if (this._reconnectTimer !== null) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
this._reconnectTimer = null
|
||||
}
|
||||
this._reconnectCount = 0
|
||||
}
|
||||
|
||||
private _doReconnect(): void {
|
||||
if (this._reconnectCount >= this._maxReconnect) {
|
||||
console.warn(TAG, '重连次数已达上限')
|
||||
this._onDisconnectedCb?.()
|
||||
return
|
||||
}
|
||||
if (!this._lastRawDevice) {
|
||||
console.warn(TAG, '无缓存设备,放弃重连')
|
||||
return
|
||||
}
|
||||
this._reconnectCount++
|
||||
console.log(TAG, `_doReconnect 第 ${this._reconnectCount} 次`)
|
||||
this._stopReconnectTimer()
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
plugin.Blue.clearProtocol()
|
||||
plugin.Blue.createBLEConnection(this._lastRawDevice!)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// ─── 请求用户(不下发) ─────────────────────
|
||||
clearDeviceMembers(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||
const timer = setTimeout(() => reject(new Error('请求超时')), 15000)
|
||||
this._activeProtocol.codeClearDeviceData('01', (res: number) => {
|
||||
clearTimeout(timer)
|
||||
res === 0 ? resolve() : reject(new Error('请求失败'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ─── 用户同步 ─────────────────────────────────
|
||||
syncMembersToDevice(members: DeviceMember[], onProgress?: (current: number, total: number) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
|
||||
const mainUser = members.find(m => m.isSelf)
|
||||
if (!mainUser) { reject(new Error('缺少主用户')); return }
|
||||
|
||||
const clearTimer = setTimeout(() => reject(new Error('请求超时')), 15000)
|
||||
this._activeProtocol.codeClearDeviceData('01', (clearRes: number) => {
|
||||
clearTimeout(clearTimer)
|
||||
if (clearRes != 0) { reject(new Error('请求失败')); return }
|
||||
|
||||
let index = 0
|
||||
let syncTimer: number | null = null
|
||||
const resetTimer = () => {
|
||||
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||
syncTimer = setTimeout(() => reject(new Error(`请求超时`)), 15000)
|
||||
}
|
||||
const syncNext = () => {
|
||||
if (index >= members.length) {
|
||||
if (syncTimer !== null) clearTimeout(syncTimer)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const member = members[index]
|
||||
onProgress?.(index + 1, members.length)
|
||||
resetTimer()
|
||||
this._activeProtocol.dataSyncUserInfo(
|
||||
{
|
||||
userID: mainUser.id, userName: member.name,
|
||||
memberID: member.isSelf ? '' : member.id,
|
||||
age: member.age, gender: member.gender, height: member.height,
|
||||
isAthleteMode: 0, deviceHeaderIndex: index,
|
||||
currentWeight: '', targetWeight: '', idealWeight: '', recentData: [],
|
||||
},
|
||||
(res: number) => { if (res === 0) index++; syncNext() },
|
||||
)
|
||||
}
|
||||
syncNext()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const lefuService = new LeFuService()
|
||||
@@ -1,112 +0,0 @@
|
||||
/** ppScale-plugin 原始设备对象 */
|
||||
export type RawDevice = Record<string, any>
|
||||
|
||||
/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */
|
||||
export type LeFuPlugin = Record<string, any>
|
||||
|
||||
|
||||
/** 设备配置(来自 master device.setting 列表) */
|
||||
export interface DeviceSetting {
|
||||
advLength: number
|
||||
calorieStatus: number
|
||||
deviceAccuracyType: number
|
||||
deviceCalcuteType: number
|
||||
deviceConnectType: number
|
||||
deviceFuncType: number
|
||||
deviceName: string
|
||||
devicePowerType: number
|
||||
deviceProtocolType: number
|
||||
deviceType: number
|
||||
deviceUnitType: string
|
||||
id: number
|
||||
macAddressStart: number
|
||||
sign: string
|
||||
status: number
|
||||
uhStatus: number
|
||||
createBy: null
|
||||
imgUrl: null
|
||||
remark: null
|
||||
updateBy: null
|
||||
}
|
||||
|
||||
/** 扫描到的设备(展示层使用) */
|
||||
export interface ScannedDevice {
|
||||
/** 设备名称 */
|
||||
name: string
|
||||
/** plugin 返回的原始设备对象,连接时传回给 SDK */
|
||||
raw: RawDevice
|
||||
/** 设备模型(含 deviceConnectType 等信息) */
|
||||
model: Record<string, any>
|
||||
}
|
||||
|
||||
/** WiFi 项 */
|
||||
export interface LeFuWifiItem {
|
||||
/** WiFi SSID */
|
||||
ssid: string
|
||||
/** 信号强度(部分设备上报) */
|
||||
signal?: number
|
||||
}
|
||||
|
||||
/** 测量进度数据(progressData 事件,测量过程中多次回调) */
|
||||
export interface ProgressData {
|
||||
/** 体重,单位:克 */
|
||||
weight: number
|
||||
/** 单位枚举值 */
|
||||
unit: number
|
||||
/** 是否正在心率测量 */
|
||||
isHeartRating: boolean
|
||||
}
|
||||
|
||||
/** 测量锁定数据(lockData 事件,测量完成时回调一次) */
|
||||
export interface LockData {
|
||||
/** 体重,单位:克 */
|
||||
weight: number
|
||||
/** 阻抗值(用于体成分分析) */
|
||||
resistance: number
|
||||
/** 心率(部分设备上报) */
|
||||
heartRate?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 乐福服务配置 */
|
||||
export interface LeFuConfig {
|
||||
key: string
|
||||
secret: string
|
||||
/** 老协议域名(无鉴权) */
|
||||
domain1: string
|
||||
/** 新协议域名(有鉴权) */
|
||||
domain2: string
|
||||
}
|
||||
|
||||
/** 设备固件信息(deviceInfo 事件返回) */
|
||||
export interface DeviceInfo {
|
||||
/** 固件版本号,格式:mcu.ble.wifi.res,如 "006.005.004.305" */
|
||||
firmwareRevision: string
|
||||
/** 设备型号,用于请求服务端版本接口 */
|
||||
modelNumber: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 服务端固件版本(firmware/version 接口 result 字段) */
|
||||
export interface FirmwareVersion {
|
||||
mcuVersion: string
|
||||
bleVersion: string
|
||||
wifiVersion: string
|
||||
resVersion: string
|
||||
}
|
||||
|
||||
/** 下发给设备的用户信息(lefu 层使用,业务层转换后传入) */
|
||||
export interface DeviceMember {
|
||||
/** 用户唯一 ID */
|
||||
id: string
|
||||
/** 姓名 */
|
||||
name: string
|
||||
/** 性别:1=男,0=女 */
|
||||
gender: 0 | 1
|
||||
/** 年龄 */
|
||||
age: number
|
||||
/** 身高(cm) */
|
||||
height: number
|
||||
/** 是否本人(主用户) */
|
||||
isSelf: boolean
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/* ================================================================
|
||||
* 全局主题变量聚合入口
|
||||
* ----------------------------------------------------------------
|
||||
* 由 app.less 引入,Less 编译期合并到 app.wxss,无运行时开销。
|
||||
* ================================================================ */
|
||||
@import "./theme.less";
|
||||
@import "./spacing.less";
|
||||
@@ -1,9 +0,0 @@
|
||||
/* ================================================================
|
||||
* 全局主题变量(Design Token)—— 间距
|
||||
* ----------------------------------------------------------------
|
||||
* 规则:业务代码只允许引用本文件中的语义变量,严禁直接写硬编码值。
|
||||
* ================================================================ */
|
||||
page {
|
||||
/* 页面边距 —— 仅用于 page 级容器横向 padding */
|
||||
--page-padding-x: 30rpx; /* 页面左右统一边距 */
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/* ================================================================
|
||||
* 全局主题变量(Design Token)—— 颜色
|
||||
* ----------------------------------------------------------------
|
||||
* 规则:样式类型禁止混用。即使颜色值相同,也必须按语义独立定义,
|
||||
* 业务代码只允许引用本文件中的语义变量,严禁直接写色值。
|
||||
* 分组:品牌色 / 文字色 / 背景色 / 按钮色 / 线条色 / 状态色 / 阴影色
|
||||
* ================================================================ */
|
||||
page {
|
||||
/* 品牌色 —— 主色调,用于链接、强调、聚焦等品牌表达 */
|
||||
--color-primary: #000000; /* 主色 */
|
||||
--color-primary-disabled: #cccccc; /* 主色禁用态 */
|
||||
|
||||
/* 文字色 —— 仅用于 color 属性,禁止用于按钮/背景 */
|
||||
--color-text: #333333; /* 一级正文 */
|
||||
--color-text-secondary: #999999; /* 二级辅助文字 */
|
||||
--color-text-placeholder: #cccccc; /* 占位文字 */
|
||||
--color-text-inverse: #ffffff; /* 反白文字(深底上的白字) */
|
||||
--color-text-disabled: #cccccc; /* 禁用文字 */
|
||||
|
||||
/* 背景色 —— 仅用于容器 background,禁止用于按钮 */
|
||||
--color-bg: #f5f5f5; /* 页面底色 */
|
||||
--color-bg-card: #ffffff; /* 卡片/容器底色 */
|
||||
--color-bg-elevated: #ffffff; /* 浮层/弹框底色 */
|
||||
--color-bg-mask: rgba(0, 0, 0, 0.5); /* 弹框蒙层(action-sheet/confirm) */
|
||||
|
||||
/* 按钮色 —— 仅用于按钮组件,与文字色/背景色完全解耦 */
|
||||
--color-btn-primary: #000000; /* 主按钮底色 */
|
||||
--color-btn-primary-text: #ffffff; /* 主按钮文字 */
|
||||
--color-btn-default: #ffffff; /* 默认按钮底色 */
|
||||
--color-btn-default-text: #333333; /* 默认按钮文字 */
|
||||
--color-btn-default-border: #e5e5e5; /* 默认按钮边框 */
|
||||
--color-btn-disabled: #cccccc; /* 禁用按钮底色 */
|
||||
--color-btn-disabled-text: #ffffff; /* 禁用按钮文字 */
|
||||
--color-btn-danger: #fa5151; /* 危险按钮底色(删除等) */
|
||||
--color-btn-danger-text: #ffffff; /* 危险按钮文字 */
|
||||
--color-btn-link: #000000; /* 链接按钮文字色 */
|
||||
|
||||
/* 线条色 —— 仅用于 border,禁止用于文字/背景 */
|
||||
--color-border: #e5e5e5; /* 常规边框 */
|
||||
--color-border-light: #f0f0f0; /* 浅边框 */
|
||||
--color-divider: #eeeeee; /* 分割线 */
|
||||
|
||||
/* 状态色 —— 成功/警告/危险/信息提示 */
|
||||
--color-success: #07c160; /* 成功(微信绿) */
|
||||
--color-warning: #ff9500; /* 警告(橙) */
|
||||
--color-danger: #fa5151; /* 危险/错误(红) */
|
||||
--color-info: #1677ff; /* 信息(蓝) */
|
||||
|
||||
/* 阴影色 —— 仅用于 box-shadow */
|
||||
--color-shadow: rgba(0, 0, 0, 0.08); /* 常规阴影 */
|
||||
--color-shadow-card: rgba(0, 0, 0, 0.04); /* 卡片浅阴影 */
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/** 身份证解析结果 */
|
||||
export interface IdCardParsed {
|
||||
/** 性别文本(男/女),解析失败返回空字符串 */
|
||||
gender: string
|
||||
/** 年龄文本,解析失败返回空字符串 */
|
||||
age: string
|
||||
/** 生日 YYYY-MM-DD */
|
||||
birthday: string
|
||||
/** 性别 1=男 2=女 */
|
||||
sex: number
|
||||
}
|
||||
|
||||
/** storage 中 userInfo 的类型 */
|
||||
export interface StorageUserInfo {
|
||||
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
|
||||
}
|
||||
|
||||
/** 校验身份证号格式是否合法 */
|
||||
export const isIdCardValid = (idCard: string): boolean => {
|
||||
if (idCard.length !== 18) return false
|
||||
// 前 17 位必须为数字
|
||||
if (!/^\d{17}$/.test(idCard.slice(0, 17))) return false
|
||||
// 第 18 位为数字或 X
|
||||
if (!/^[\dXx]$/.test(idCard[17])) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/** 解析身份证号,提取性别、年龄、生日 */
|
||||
export const parseIdCard = (idCard: string): IdCardParsed => {
|
||||
const empty: IdCardParsed = { gender: '', age: '', birthday: '', sex: 0 }
|
||||
if (!isIdCardValid(idCard)) return empty
|
||||
|
||||
// 第 17 位奇数为男,偶数为女
|
||||
const sexCode = parseInt(idCard[16], 10)
|
||||
const sex = sexCode % 2 === 1 ? 1 : 2
|
||||
|
||||
// 第 7-14 位为生日 YYYYMMDD
|
||||
const year = parseInt(idCard.slice(6, 10), 10)
|
||||
const month = parseInt(idCard.slice(10, 12), 10)
|
||||
const day = parseInt(idCard.slice(12, 14), 10)
|
||||
|
||||
// 计算年龄
|
||||
const now = new Date()
|
||||
const birth = new Date(year, month - 1, day)
|
||||
let age = now.getFullYear() - year
|
||||
if (now.getMonth() < month - 1 || (now.getMonth() === month - 1 && now.getDate() < day)) {
|
||||
age--
|
||||
}
|
||||
|
||||
return {
|
||||
gender: sex === 1 ? '男' : '女',
|
||||
age: String(age),
|
||||
birthday: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
||||
sex,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// TODO: 替换为实际接口地址
|
||||
const IS_DEV = false
|
||||
|
||||
export const BASE_URL = IS_DEV
|
||||
? ''
|
||||
: ''
|
||||
|
||||
export const TIMEOUT = 30000
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { ApiResponse, HttpMethod, RequestOptions } from './types'
|
||||
import { BASE_URL, TIMEOUT } from './config'
|
||||
|
||||
export type { ApiResponse, RequestOptions }
|
||||
|
||||
// const BASE_URL = 'http://10.10.10.10:8889/' // 测试环境
|
||||
const BASE_URL = 'https://device.shuziweidao.com/gateway/' // 正式环境
|
||||
|
||||
let _loadingCount = 0
|
||||
|
||||
const _showLoading = (title: string): void => {
|
||||
@@ -29,7 +27,7 @@ const request = <T = any>(
|
||||
|
||||
const header: Record<string, string> = { 'content-type': contentType }
|
||||
const token = wx.getStorageSync('token') as string
|
||||
if (token) header['X-Access-Token'] = token
|
||||
if (token) header['abtoken'] = token
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
@@ -37,17 +35,14 @@ const request = <T = any>(
|
||||
data,
|
||||
method,
|
||||
header,
|
||||
timeout: TIMEOUT,
|
||||
success: (res) => {
|
||||
if (loading) _hideLoading()
|
||||
const body = res.data as ApiResponse<T>
|
||||
if (body.code === 200) {
|
||||
if (body.code === '00000') {
|
||||
resolve(body)
|
||||
} else if (body.code === 401) {
|
||||
wx.clearStorageSync()
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
reject(body)
|
||||
} else {
|
||||
wx.showToast({ title: body.message || '请求失败', icon: 'none' })
|
||||
wx.showToast({ title: body.msg || '请求失败', icon: 'none' })
|
||||
reject(body)
|
||||
}
|
||||
},
|
||||
@@ -77,12 +72,3 @@ export const put = <T = any>(
|
||||
data?: Record<string, any>,
|
||||
options?: RequestOptions,
|
||||
): Promise<ApiResponse<T>> => request<T>(url, data, 'PUT', options)
|
||||
|
||||
export const del = <T = any>(
|
||||
url: string,
|
||||
data?: Record<string, any>,
|
||||
options?: RequestOptions,
|
||||
): Promise<ApiResponse<T>> => request<T>(url, data, 'DELETE', {
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
...options,
|
||||
})
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
result: T
|
||||
message?: string
|
||||
code: string
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT'
|
||||
|
||||
export interface RequestOptions {
|
||||
/** 是否显示 loading,默认 true */
|
||||
loading?: boolean
|
||||
/** loading 文案,默认"加载中..." */
|
||||
loadingTitle?: string
|
||||
/** Content-Type,默认 application/json */
|
||||
contentType?: 'application/json' | 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export const formatTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return (
|
||||
[year, month, day].map(formatNumber).join('/') +
|
||||
' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
const formatNumber = (n: number) => {
|
||||
const s = n.toString()
|
||||
return s[1] ? s : '0' + s
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
/// <reference path="./types/index.d.ts" />
|
||||
|
||||
interface IAppOption {
|
||||
globalData: {
|
||||
userInfo?: WechatMiniprogram.UserInfo,
|
||||
/** 当前已连接设备的蓝牙状态 */
|
||||
connectState: string,
|
||||
}
|
||||
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback,
|
||||
}
|
||||
globalData: {}
|
||||
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback
|
||||
/** 获取微信登录临时 code */
|
||||
getWxLoginCode(): Promise<string>
|
||||
}
|
||||
|
||||