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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
17792275749
2026-08-10 14:21:00 +08:00
co-authored by Claude Sonnet 4.6
parent 439f560218
commit 83d909b95b
96 changed files with 370 additions and 9040 deletions
+2
View File
@@ -0,0 +1,2 @@
export { checkOpenIdLogin, getUserInfoByIdCard, updateUser } from './user'
export type { UserInfo, LoginResult } from './user'
+50
View File
@@ -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)
+4 -50
View File
@@ -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"
}
]
}
]
}
+7 -11
View File
@@ -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 {
+17 -13
View File
@@ -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:ifDOM 是否存在 */
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:ifDOM 是否存在 */
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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

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