diff --git a/miniprogram/app.json b/miniprogram/app.json index f1ce9ab..2933028 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -7,6 +7,7 @@ "pages/equipmentMember/equipmentMember", "pages/supplementPersonal/supplementPersonal", "pages/addMember/addMember", + "pages/editMember/editMember", "pages/equipment/equipment", "pages/measurementReport/measurementReport", "pages/data/data", diff --git a/miniprogram/pages/editMember/editMember.json b/miniprogram/pages/editMember/editMember.json new file mode 100644 index 0000000..71ddaee --- /dev/null +++ b/miniprogram/pages/editMember/editMember.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "编辑用户", + "usingComponents": { + "select-member-drawer": "/components/selectMemberDrawer/selectMemberDrawer", + "user-info-confirm-modal": "/components/userInfoConfirmModal/userInfoConfirmModal" + } + } + \ No newline at end of file diff --git a/miniprogram/pages/editMember/editMember.less b/miniprogram/pages/editMember/editMember.less new file mode 100644 index 0000000..e6f235a --- /dev/null +++ b/miniprogram/pages/editMember/editMember.less @@ -0,0 +1,224 @@ +.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; + } + + /* 表单卡片 */ + .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; + } + + /* 输入框 + 后缀容器(身高 cm) */ + .form-input-wrap { + position: relative; + width: 630rpx; + + .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: 630rpx; + height: 88rpx; + box-sizing: border-box; + padding: 0 24rpx; + background: #F6F6F6; + border-radius: 16rpx; + display: flex; + align-items: center; + + .readonly-text { + color: #252535; + font-size: 26rpx; + } + + .readonly-placeholder { + color: #B6B6B6; + font-size: 26rpx; + } + } + + .form-readonly-with-suffix { + justify-content: space-between; + + .form-suffix { + color: #77849E; + height: 88rpx; + font-size: 26rpx; + line-height: 88rpx; + } + } + } + + /* 从已有用户中选择(蓝色描边幽灵按钮) */ + .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; + } + } +} diff --git a/miniprogram/pages/editMember/editMember.ts b/miniprogram/pages/editMember/editMember.ts new file mode 100644 index 0000000..4808516 --- /dev/null +++ b/miniprogram/pages/editMember/editMember.ts @@ -0,0 +1,407 @@ +import { get, put } from '../../utils/request/index' +import { lefuService } from '../../lefu/index' + +/** GET /weighingScale/v3/selectUserBySn/user 返回的单条结构(编辑回填用) */ +interface SysUserDevice { + id: string + realname: string + idCard: string + height: number + weight: number + sex: number + birthday: string +} + +/** 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 + /** 身高(cm) */ + height: string + /** 体重(kg) */ + weight: string +} + +/** 身份证解析结果 */ +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 +} + +/** + * 添加/编辑用户页 + * URL 参数含 userId 时为编辑模式,隐藏「从已有用户中选择」按钮并预填表单 + */ +Page({ + data: { + /** 编辑模式标记 */ + isEdit: false, + /** 编辑时的用户 ID */ + userId: '', + + form: { + name: '', + idCard: '', + gender: '', + age: '', + height: '', + weight: '' + } as MemberForm, + + /** 从抽屉选中的完整用户数据,提交时作为 base spread */ + selectedMember: null as ExistingMember | null, + showSelectDrawer: false, + /** 正在请求身份证查询接口 */ + querying: false, + /** 是否已完成一次身份证查询(成功 200 即视为完成,无论 result 是否为 null) */ + hasQueried: false, + /** 接口返回的用户信息,姓名 + 身份证联动校正性别年龄 */ + serverResult: null as UserInfoFromServer | null, + /** 表单只读状态(code=101 时置为 true) */ + readonly: false, + /** 是否显示确认弹框 */ + showConfirmModal: false, + }, + + onLoad(options: Record) { + const { userId } = options + if (!userId) return + this.setData({ isEdit: true, userId }) + get('weighingScale/v3/select/userinfo/byId', { id: userId }, { loading: false }) + .then((res: any) => { + const u: SysUserDevice = res.result + if (!u) return + const parsed = this.parseIdCard(u.idCard ?? '') + this.setData({ + 'form.name': u.realname ?? '', + 'form.idCard': u.idCard ?? '', + 'form.gender': parsed.gender, + 'form.age': parsed.age, + 'form.height': String(u.height ?? ''), + 'form.weight': String(u.weight ?? ''), + }) + if ((u.realname ?? '').trim() && this.isIdCardValid(u.idCard ?? '')) { + this.fetchUserInfoByIdCard(u.realname.trim(), u.idCard) + } + }) + }, + + onNameInput(e: WechatMiniprogram.Input) { + const name = e.detail.value + this.setData({ + 'form.name': name, + 'form.gender': '', + 'form.age': '', + 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() + this.setData({ + 'form.idCard': idCard, + 'form.gender': '', + 'form.age': '', + serverResult: null, + hasQueried: false, + readonly: false + }) + if (this.data.form.name.trim() && this.isIdCardValid(idCard)) { + this.fetchUserInfoByIdCard(this.data.form.name.trim(), idCard) + } + }, + + onHeightInput(e: WechatMiniprogram.Input) { + this.setData({ 'form.height': e.detail.value }) + }, + + onWeightInput(e: WechatMiniprogram.Input) { + this.setData({ 'form.weight': e.detail.value }) + }, + + /** + * 校验身份证格式:18 位,前 17 位数字,末位数字或 X + */ + isIdCardValid(idCard: string): boolean { + return /^\d{17}[\dX]$/.test(idCard) + }, + + /** + * 解析身份证,返回性别、年龄、sex 数值、出生日期 + */ + 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}` } + }, + + /** + * 调用服务端接口校验身份证与姓名,成功后用接口返回的身份证重新计算性别年龄 + * result 为 null 视为「查无此人」,标记 hasQueried 允许新增提交 + * result.code 101 视为「信息已存在」,弹框提示并锁定表单 + * result.code 102 视为正常,走原有逻辑 + */ + fetchUserInfoByIdCard(name: string, idCard: string) { + if (this.data.querying) return + this.setData({ querying: true, serverResult: null, hasQueried: false }) + + get('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, + }) + 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.height': serverResult.height ? String(serverResult.height) : '', + 'form.weight': serverResult.weight ? String(serverResult.weight) : '' + }) + return + } + if (serverResult.flag === null) { + const parsed = this.parseIdCard(serverResult.idCard) + const data: Record = { + serverResult, + hasQueried: true, + 'form.gender': parsed.gender, + 'form.age': parsed.age + } + if (serverResult.height) { + data['form.height'] = String(serverResult.height) + } + if (serverResult.weight) { + data['form.weight'] = String(serverResult.weight) + } + this.setData(data) + } + }) + .catch(() => { + // 查询失败保留本地解析结果,hasQueried 维持 false 以便用户重试时再次触发 + }) + .finally(() => { + this.setData({ querying: false }) + }) + }, + + onTapSelectExisting() { + this.setData({ showSelectDrawer: true }) + }, + + onSelectMemberClose() { + this.setData({ showSelectDrawer: false }) + }, + + onSelectMember(e: WechatMiniprogram.CustomEvent) { + const member = e.detail + const parsed = this.parseIdCard(member.idCard) + this.setData({ + showSelectDrawer: false, + selectedMember: member, + '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: { + name: '', + idCard: '', + gender: '', + age: '', + height: '', + weight: '' + } + }) + }, + + onCancel() { + wx.navigateBack() + }, + + onSubmit() { + const { name, idCard, gender, age, height, weight } = this.data.form + 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 (!weight || isNaN(weightNum) || weightNum <= 0) { + wx.showToast({ title: '请填写体重', icon: 'none' }) + return + } + if (!this.data.hasQueried) { + wx.showToast({ title: this.data.querying ? '身份信息验证中,请稍候' : '请检查姓名与身份证号', icon: 'none' }) + return + } + + const parsed = this.parseIdCard(idCard) + const userInfo = wx.getStorageSync('userInfo') + const connectDeviceInfoRaw = wx.getStorageSync('connectDeviceInfo') + const serverResult = this.data.serverResult + + // selectedMember(抽屉选中) → serverResult(接口校验,可能为 null) → 表单/本地解析(用户输入) 依次覆盖 + const payload: Record = { + ...(this.data.selectedMember ?? {}), + ...(serverResult ?? {}), + parentUserId: userInfo?.userId ?? '', + sn: lefuService.deviceInfo?.serialNumber ?? '', + scaleDeviceId: lefuService.deviceInfo?.serialNumber ?? '', + realname: name.trim(), + idCard, + sex: parsed.sex, + birthday: parsed.birthday, + height: heightNum, + weight: weightNum, + connectDeviceInfo: connectDeviceInfoRaw ? JSON.stringify(connectDeviceInfoRaw) : '', + } + if (this.data.isEdit) { + payload.id = this.data.userId + } + + const successMsg = this.data.isEdit ? '保存成功' : '添加成功' + const failMsg = this.data.isEdit ? '保存失败,请重试' : '添加失败,请重试' + + put('weighingScale/v3/edit/user', payload) + .then(() => { + wx.showToast({ title: successMsg, icon: 'success' }) + setTimeout(() => { + // 通知上一页(equipmentMember)刷新用户列表 + 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: failMsg, icon: 'none' }) + }) + } +}) diff --git a/miniprogram/pages/editMember/editMember.wxml b/miniprogram/pages/editMember/editMember.wxml new file mode 100644 index 0000000..0ae86ce --- /dev/null +++ b/miniprogram/pages/editMember/editMember.wxml @@ -0,0 +1,157 @@ + + + + + ! + 最多支持 10 位用户 + + + + + + + + + + + + 姓名 + * + + + + + + + + + + 身份证号 + * + + + + + + + + + + 性别 + + + + + {{ form.gender }} + + + 自动生成,无需填写 + + + + + + + + + 年龄 + + + + + {{ form.age }} + + + 自动生成,无需填写 + + + + + + + + + + 身高 + * + + + + + cm + + + + + + + + 体重 + * + + + + + kg + + + + + + + + 从已有用户中选择 + + + + + + + + 取消 + 保存用户信息 + + + + + + + + + + diff --git a/miniprogram/pages/equipmentMember/equipmentMember.ts b/miniprogram/pages/equipmentMember/equipmentMember.ts index 380bc32..b4c5af5 100644 --- a/miniprogram/pages/equipmentMember/equipmentMember.ts +++ b/miniprogram/pages/equipmentMember/equipmentMember.ts @@ -229,7 +229,7 @@ Page({ /** 编辑用户:仅携带 userId 跳转,详情由目标页自行查询 */ onTapEdit(e: WechatMiniprogram.TouchEvent) { const id = e.currentTarget.dataset.id as string - wx.navigateTo({ url: `/pages/addMember/addMember?userId=${id}` }) + wx.navigateTo({ url: `/pages/editMember/editMember?userId=${id}` }) }, /** 删除用户:弹出自定义确认弹窗 */