feat(user): 补充个人信息页接入身份证已存在复用弹框与登录引导

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
17792275749
2026-08-24 11:54:33 +08:00
co-authored by Claude Sonnet 4.6
parent 9911dea32f
commit 90f84cc7df
13 changed files with 353 additions and 72 deletions
+4 -3
View File
@@ -30,6 +30,7 @@ export interface UserInfo {
relationType: string | null relationType: string | null
sex_dictText: string sex_dictText: string
connectDeviceInfo: string connectDeviceInfo: string
flag?: number | null
} }
export interface LoginResult { export interface LoginResult {
@@ -43,8 +44,8 @@ export const checkOpenIdLogin = (code: string) =>
/** 根据身份证号查询用户信息 */ /** 根据身份证号查询用户信息 */
export const getUserInfoByIdCard = (params: { sn?: string; idCard: string; realname: string }) => export const getUserInfoByIdCard = (params: { sn?: string; idCard: string; realname: string }) =>
get<UserInfo | null>('weighingScale/v3/getUserInfoByIdcard', params, { loading: false }) get<UserInfo | null>('weighingScale/v6/getUserInfoByIdcard', params, { loading: true, loadingTitle: '正在查询用户信息' })
/** 更新用户信息 */ /** 更新当前登录微信用户信息 */
export const updateUser = (data: Record<string, any>) => export const updateUser = (data: Record<string, any>) =>
put<UserInfo>('weighingScale/v3/update/user', data) put<UserInfo>('weighingScale/v6/update/user', data)
+2 -2
View File
@@ -4,8 +4,8 @@
onLaunch() { onLaunch() {
// 已登录则跳过登录页 // 已登录则跳过登录页
const token = wx.getStorageSync('token') || null; const token = wx.getStorageSync('token') || null;
// const userInfo = wx.getStorageSync('userInfo') || null; const userInfo = wx.getStorageSync('userInfo') || null;
const userInfo = true; // const userInfo = true;
if (token && userInfo) { if (token && userInfo) {
wx.reLaunch({ wx.reLaunch({
url: '/pages/home/home' url: '/pages/home/home'
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,139 @@
.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;
}
}
@@ -0,0 +1,63 @@
/**
* 用户信息确认弹框组件
* 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')
}
}
})
@@ -0,0 +1,42 @@
<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>
<block wx:if="{{ userInfo }}">
<view class="modal-body">
<view class="modal-body-box">
<view class="info-row">
<view class="info-label">姓名:</view>
<view class="info-value">{{ userInfo.realname }}</view>
</view>
<view class="info-row">
<view class="info-label">身份证号:</view>
<view class="info-value">{{ userInfo.idCard }}</view>
</view>
<view class="info-row">
<view class="info-label">性别:</view>
<view class="info-value">{{ userInfo.sex_dictText }}</view>
</view>
<view class="info-row">
<view class="info-label">身高:</view>
<view class="info-value">{{ userInfo.height }}cm</view>
</view>
<view class="info-row">
<view class="info-label">体重:</view>
<view class="info-value">{{ userInfo.weight }}kg</view>
</view>
</view>
</view>
</block>
<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>
+5 -5
View File
@@ -25,12 +25,12 @@ Page({
const { token, user } = res.result const { token, user } = res.result
// 持久化登录凭证和用户信息 // 持久化登录凭证和用户信息
wx.setStorageSync('token', token) wx.setStorageSync('token', token)
// if (user) { if (user) {
// wx.setStorageSync('userInfo', user) wx.setStorageSync('userInfo', user)
wx.reLaunch({ url: '/pages/home/home' }) wx.reLaunch({ url: '/pages/home/home' })
// } else { } else {
// wx.reLaunch({ url: '/pages/supplementPersonal/supplementPersonal' }) wx.reLaunch({ url: '/pages/supplementPersonal/supplementPersonal' })
// } }
}).catch((err) => { }).catch((err) => {
wx.showToast({ title: err.msg || '登录失败,请重试', icon: 'none' }) wx.showToast({ title: err.msg || '登录失败,请重试', icon: 'none' })
}).finally(() => { }).finally(() => {
@@ -1,5 +1,7 @@
{ {
"usingComponents": {}, "usingComponents": {
"user-info-confirm-modal": "/components/userInfoConfirmModal/userInfoConfirmModal"
},
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#FFFFFF", "navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "完善个人信息" "navigationBarTitleText": "完善个人信息"
@@ -1,5 +1,5 @@
import { getUserInfoByIdCard, updateUser, type UserInfo } from '../../api/index' import { getUserInfoByIdCard, updateUser, type UserInfo } from '../../api/index'
import { parseIdCard } from '../../utils/common' import { parseIdCard, isIdCardValid } from '../../utils/idCard/index'
Page({ Page({
data: { data: {
@@ -7,34 +7,55 @@ Page({
idCard: '', idCard: '',
gender: '', gender: '',
age: '', age: '',
sex: 0,
birthday: '',
height: '', height: '',
weight: '', weight: '',
/** 查询接口返回的原始用户信息 */
serverResult: null as UserInfo | null, serverResult: null as UserInfo | null,
/** 身份证已存在(flag=101)时表单只读 */
readonly: false,
/** 是否展示「复用信息」确认弹框 */
showConfirmModal: false,
},
/** 姓名/身份证变动后,查询回填的信息失效,统一清空 */
_clearQuery() {
this.setData({
serverResult: null,
gender: '',
age: '',
sex: 0,
birthday: '',
height: '',
weight: '',
readonly: false,
})
}, },
onNameInput(e: WechatMiniprogram.Input) { onNameInput(e: WechatMiniprogram.Input) {
this.setData({ name: e.detail.value.trim() }) this.setData({ name: e.detail.value.trim() })
this._clearQuery()
}, },
/* 失焦时:清空查询结果 → 重新校验身份证,变更即失效 */ // 失焦重新校验:姓名改完、身份证仍合法时重新发起查询
onNameBlur() { onNameBlur() {
if (!this.data.name) return if (!this.data.name) return
this.setData({ serverResult: null })
this.validateIdCard() this.validateIdCard()
}, },
onIdCardInput(e: WechatMiniprogram.Input) { onIdCardInput(e: WechatMiniprogram.Input) {
const idCard = (e.detail.value || '').trim().toUpperCase() const idCard = (e.detail.value || '').trim().toUpperCase()
this.setData({ idCard, serverResult: null }) this.setData({ idCard })
this._clearQuery()
if (idCard.length === 18) this.validateIdCard() if (idCard.length === 18) this.validateIdCard()
}, },
/* 独立的身份证合法性校验,合法且姓名已填则请求接口 */ /* 独立的身份证合法性校验,合法且姓名已填则请求接口 */
validateIdCard() { validateIdCard() {
const { idCard, name } = this.data const { idCard, name } = this.data
const { gender, age } = parseIdCard(idCard) if (isIdCardValid(idCard) && name) this.fetchUser()
this.setData({ gender, age })
if (gender && name) this.fetchUser()
}, },
fetchUser() { fetchUser() {
@@ -42,13 +63,51 @@ Page({
idCard: this.data.idCard, idCard: this.data.idCard,
realname: this.data.name, realname: this.data.name,
}).then((res) => { }).then((res) => {
if (res.result) { const serverResult = res.result
const { gender, age } = parseIdCard(res.result.idCard) // 优先按服务器返回的身份证解析,未查到则回退用户输入
this.setData({ serverResult: res.result, gender, age }) const { gender, age, sex, birthday } = parseIdCard(serverResult?.idCard || this.data.idCard)
this.setData({ gender, age, sex, birthday })
if (!serverResult) {
wx.showToast({ title: res.msg || '未查询到用户信息', icon: 'none' })
return
}
// flag=101:身份证已存在,弹框确认是否复用
if (serverResult.flag === 101) {
this.setData({
serverResult,
name: serverResult.realname ?? '',
idCard: serverResult.idCard ?? '',
height: serverResult.height ? String(serverResult.height) : '',
weight: serverResult.weight ? String(serverResult.weight) : '',
readonly: true,
showConfirmModal: true,
})
return
}
// flag=null:查到但未建档,回填已有身高体重
if (serverResult.flag === null) {
this.setData({
serverResult,
height: serverResult.height ? String(serverResult.height) : '',
weight: serverResult.weight ? String(serverResult.weight) : '',
})
return
} }
}).catch(() => {}) }).catch(() => {})
}, },
// 复用已有信息:关闭弹框直接提交
onConfirmModalConfirm() {
this.setData({ showConfirmModal: false })
this.onSubmit()
},
// 不复用:清空整个表单重新填写
onConfirmModalCancel() {
this.setData({ showConfirmModal: false, name: '', idCard: '' })
this._clearQuery()
},
onHeightInput(e: WechatMiniprogram.Input) { onHeightInput(e: WechatMiniprogram.Input) {
this.setData({ height: e.detail.value }) this.setData({ height: e.detail.value })
}, },
@@ -58,7 +117,7 @@ Page({
}, },
onSubmit() { onSubmit() {
const { name, idCard, gender, height, weight, serverResult } = this.data const { name, idCard, gender, sex, birthday, height, weight, serverResult } = this.data
const heightNum = parseFloat(height) const heightNum = parseFloat(height)
const weightNum = parseFloat(weight) const weightNum = parseFloat(weight)
@@ -79,16 +138,14 @@ Page({
return return
} }
const parsed = parseIdCard(idCard)
updateUser({ updateUser({
...serverResult, ...serverResult,
idCard, idCard,
realname: name, realname: name,
height: heightNum, height: heightNum,
weight: weightNum || 0, weight: weightNum || 0,
sex: parsed.sex, sex,
birthday: parsed.birthday, birthday,
}).then((res) => { }).then((res) => {
wx.setStorageSync('userInfo', res.result) wx.setStorageSync('userInfo', res.result)
wx.switchTab({ url: '/pages/home/home' }) wx.switchTab({ url: '/pages/home/home' })
@@ -18,6 +18,7 @@
placeholder="请填写真实姓名" placeholder="请填写真实姓名"
placeholder-class="form-placeholder" placeholder-class="form-placeholder"
value="{{ name }}" value="{{ name }}"
disabled="{{ readonly }}"
bind:input="onNameInput" bind:input="onNameInput"
bind:blur="onNameBlur" /> bind:blur="onNameBlur" />
</view> </view>
@@ -35,6 +36,7 @@
placeholder="请填写身份证号" placeholder="请填写身份证号"
placeholder-class="form-placeholder" placeholder-class="form-placeholder"
value="{{ idCard }}" value="{{ idCard }}"
disabled="{{ readonly }}"
bind:input="onIdCardInput" /> bind:input="onIdCardInput" />
</view> </view>
</view> </view>
@@ -77,6 +79,7 @@
placeholder="请填写身高" placeholder="请填写身高"
placeholder-class="form-placeholder" placeholder-class="form-placeholder"
value="{{ height }}" value="{{ height }}"
disabled="{{ readonly }}"
bind:input="onHeightInput" /> bind:input="onHeightInput" />
<text class="form-suffix form-suffix-inside">cm</text> <text class="form-suffix form-suffix-inside">cm</text>
</view> </view>
@@ -95,6 +98,7 @@
placeholder="请填写体重" placeholder="请填写体重"
placeholder-class="form-placeholder" placeholder-class="form-placeholder"
value="{{ weight }}" value="{{ weight }}"
disabled="{{ readonly }}"
bind:input="onWeightInput" /> bind:input="onWeightInput" />
<text class="form-suffix form-suffix-inside">kg</text> <text class="form-suffix form-suffix-inside">kg</text>
</view> </view>
@@ -104,4 +108,6 @@
</view> </view>
<view class="submit-btn" bind:tap="onSubmit">完成并进入首页</view> <view class="submit-btn" bind:tap="onSubmit">完成并进入首页</view>
<user-info-confirm-modal show="{{ showConfirmModal }}" userInfo="{{ serverResult }}" bind:confirm="onConfirmModalConfirm" bind:cancel="onConfirmModalCancel" />
</view> </view>
@@ -1,47 +1,4 @@
/** 身份证解析结果 */ import type { IdCardParsed } from './types'
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 => { export const isIdCardValid = (idCard: string): boolean => {
@@ -69,7 +26,6 @@ export const parseIdCard = (idCard: string): IdCardParsed => {
// 计算年龄 // 计算年龄
const now = new Date() const now = new Date()
const birth = new Date(year, month - 1, day)
let age = now.getFullYear() - year let age = now.getFullYear() - year
if (now.getMonth() < month - 1 || (now.getMonth() === month - 1 && now.getDate() < day)) { if (now.getMonth() < month - 1 || (now.getMonth() === month - 1 && now.getDate() < day)) {
age-- age--
+11
View File
@@ -0,0 +1,11 @@
/** 身份证解析结果 */
export interface IdCardParsed {
/** 性别文本(男/女),解析失败返回空字符串 */
gender: string
/** 年龄文本,解析失败返回空字符串 */
age: string
/** 生日 YYYY-MM-DD */
birthday: string
/** 性别 1=男 2=女 */
sex: number
}
+1 -1
View File
@@ -27,7 +27,7 @@ const request = <T = any>(
const header: Record<string, string> = { 'content-type': contentType } const header: Record<string, string> = { 'content-type': contentType }
const token = wx.getStorageSync('token') as string const token = wx.getStorageSync('token') as string
if (token) header['abtoken'] = token if (token) header['X-Access-Token'] = token
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
wx.request({ wx.request({