diff --git a/pages/index/index.vue b/pages/index/index.vue index 1e98d27..a9f5515 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -106,10 +106,16 @@ const handleJoin = () => { }) return } - // 审核通过 -> 不可点击 - if (patientStatus.value === 1) return - // 待审核 -> 不可点击 - if (patientStatus.value === 0) return + // 审核通过 -> 跳转到个人中心 + if (patientStatus.value === 1) { + uni.switchTab({ url: '/pages/profile/profile' }) + return + } + // 待审核 -> 跳转到个人中心 + if (patientStatus.value === 0) { + uni.switchTab({ url: '/pages/profile/profile' }) + return + } // 已拒绝 -> 跳转资料页重新提交 if (patientStatus.value === 2) { uni.navigateTo({ url: '/pages/myinfo/myinfo' }) diff --git a/pages/myinfo/myinfo.vue b/pages/myinfo/myinfo.vue index 9228124..d617a52 100644 --- a/pages/myinfo/myinfo.vue +++ b/pages/myinfo/myinfo.vue @@ -104,6 +104,17 @@ 去签署 + + + 个人信息处理同意书(监护人) + {{ signedPrivacyJhr ? '已签署' : '未签署' }} + + + 查看 + 重签 + + 去签署 + 声明与承诺 @@ -157,6 +168,7 @@ const form = reactive({ documents: [], sign_income: '', sign_privacy: '', + sign_privacy_jhr: '', sign_promise: '', income_amount: '' }) @@ -212,8 +224,23 @@ const maskedPhone = computed(() => { // 签署状态:form 中有新签的 URL 或 info 中有已保存的 URL const signedIncome = computed(() => form.sign_income || info.value.sign_income) const signedPrivacy = computed(() => form.sign_privacy || info.value.sign_privacy) +const signedPrivacyJhr = computed(() => form.sign_privacy_jhr || info.value.sign_privacy_jhr) const signedPromise = computed(() => form.sign_promise || info.value.sign_promise) +// 判断是否未成年(从身份证号解析年龄) +const isMinor = computed(() => { + const idCard = info.value.id_card || '' + if (idCard.length !== 18) return false + const birthYear = parseInt(idCard.substring(6, 10)) + const birthMonth = parseInt(idCard.substring(10, 12)) + const birthDay = parseInt(idCard.substring(12, 14)) + const now = new Date() + let age = now.getFullYear() - birthYear + const monthDiff = (now.getMonth() + 1) - birthMonth + if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDay)) age-- + return age < 18 +}) + const regionText = computed(() => { const parts = [] if (form.province_code) { @@ -247,6 +274,8 @@ const onSignResult = (data) => { if (data.amount) form.income_amount = data.amount } else if (data.type === 'privacy') { form.sign_privacy = data.url + } else if (data.type === 'privacy_jhr') { + form.sign_privacy_jhr = data.url } else if (data.type === 'promise') { form.sign_promise = data.url } @@ -271,6 +300,7 @@ const previewSign = (type) => { const urlMap = { income: form.sign_income || info.value.sign_income, privacy: form.sign_privacy || info.value.sign_privacy, + privacy_jhr: form.sign_privacy_jhr || info.value.sign_privacy_jhr, promise: form.sign_promise || info.value.sign_promise } const url = urlMap[type] @@ -335,6 +365,7 @@ const loadInfo = async () => { form.documents = res.data.documents || [] form.sign_income = res.data.sign_income || '' form.sign_privacy = res.data.sign_privacy || '' + form.sign_privacy_jhr = res.data.sign_privacy_jhr || '' form.sign_promise = res.data.sign_promise || '' form.income_amount = res.data.income_amount || '' // 设置地区选择器默认索引 @@ -385,6 +416,19 @@ const handleSubmit = async () => { if (!form.address.trim()) { return uni.showToast({ title: '请填写详细地址', icon: 'none' }) } + // 签名校验:全部必须签 + if (!signedIncome.value) { + return uni.showToast({ title: '请签署个人可支配收入声明', icon: 'none' }) + } + if (!signedPrivacy.value) { + return uni.showToast({ title: '请签署个人信息处理同意书', icon: 'none' }) + } + if (isMinor.value && !signedPrivacyJhr.value) { + return uni.showToast({ title: '请签署个人信息处理同意书(监护人)', icon: 'none' }) + } + if (!signedPromise.value) { + return uni.showToast({ title: '请签署声明与承诺', icon: 'none' }) + } // 已通过状态需要二次确认 if (info.value.status === 1) { showConfirmPopup.value = true @@ -410,6 +454,7 @@ const doSubmit = async () => { documents: form.documents, sign_income: form.sign_income, sign_privacy: form.sign_privacy, + sign_privacy_jhr: form.sign_privacy_jhr, sign_promise: form.sign_promise, income_amount: form.income_amount || null, // #ifdef MP-WEIXIN diff --git a/pages/profile/profile.vue b/pages/profile/profile.vue index 9b231be..205be9f 100644 --- a/pages/profile/profile.vue +++ b/pages/profile/profile.vue @@ -53,7 +53,7 @@ 实名认证 已认证 去认证 - + @@ -180,6 +180,10 @@ const goMessage = () => { const goVerify = () => { if (!checkLogin()) return + if (isAuthed.value) { + uni.showToast({ title: '已认证', icon: 'none' }) + return + } uni.navigateTo({ url: '/pages/verify/verify' }) } diff --git a/pages/sign/sign.vue b/pages/sign/sign.vue index 0bd4fcf..a2af3c1 100644 --- a/pages/sign/sign.vue +++ b/pages/sign/sign.vue @@ -11,7 +11,24 @@ 请填写您的个人年可支配收入(元) ¥ - + + + + + + + 监护人信息 + + 监护人姓名 + + + + 监护人身份证 + + + + 与患者关系 + @@ -46,6 +63,9 @@ const signType = ref('') const docTitle = ref('') const docContent = ref('') const incomeAmount = ref('') +const guardianName = ref('') +const guardianIdCard = ref('') +const guardianRelation = ref('') const signImageUrl = ref('') const submitting = ref(false) @@ -83,13 +103,22 @@ const confirmSign = async () => { if (signType.value === 'income' && (!incomeAmount.value || Number(incomeAmount.value) <= 0)) { return uni.showToast({ title: '请填写有效的收入金额', icon: 'none' }) } + if (signType.value === 'privacy_jhr') { + if (!guardianName.value.trim()) return uni.showToast({ title: '请输入监护人姓名', icon: 'none' }) + if (!guardianIdCard.value.trim()) return uni.showToast({ title: '请输入监护人身份证号', icon: 'none' }) + if (!/^\d{17}[\dXx]$/.test(guardianIdCard.value)) return uni.showToast({ title: '监护人身份证格式不正确', icon: 'none' }) + if (!guardianRelation.value.trim()) return uni.showToast({ title: '请输入与患者关系', icon: 'none' }) + } submitting.value = true try { const params = { type: signType.value, signImage: signImageUrl.value, - amount: signType.value === 'income' ? incomeAmount.value : undefined + amount: signType.value === 'income' ? incomeAmount.value : undefined, + guardianName: signType.value === 'privacy_jhr' ? guardianName.value.trim() : undefined, + guardianIdCard: signType.value === 'privacy_jhr' ? guardianIdCard.value.trim() : undefined, + guardianRelation: signType.value === 'privacy_jhr' ? guardianRelation.value.trim() : undefined } const res = await post('/api/mp/sign', params) @@ -162,6 +191,21 @@ const confirmSign = async () => { background: #f8f8f8; } +.form-row { + margin-bottom: 20rpx; + + &:last-child { + margin-bottom: 0; + } +} + +.row-label { + font-size: 26rpx; + color: #555; + margin-bottom: 12rpx; + display: block; +} + .sign-label { font-size: 28rpx; color: #333; diff --git a/pages/verify/verify.vue b/pages/verify/verify.vue index fcc840c..f1cb58b 100644 --- a/pages/verify/verify.vue +++ b/pages/verify/verify.vue @@ -60,17 +60,17 @@ 证件姓名 - + 证件号码 - + - + 发证机关 - + 有效期限 @@ -100,6 +100,18 @@ + + + + + 提示 + 检测到患者信息已存在({{ bindPatientInfo.name }} {{ bindPatientInfo.phone }}),是否确认绑定该患者信息? + + + + + + @@ -116,8 +128,20 @@ const placeholderBack = CDN_BASE + '/images/patient/user/idcard-back.webp?v=1' const certType = ref('idcard') const countdown = ref(0) const submitting = ref(false) +const showBindPopup = ref(false) +const bindPatientInfo = ref({ name: '', phone: '' }) let timer = null +// 身份证号格式校验(18位,含校验位) +const validateIdCard = (id) => { + if (!/^\d{17}[\dXx]$/.test(id)) return false + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + let sum = 0 + for (let i = 0; i < 17; i++) sum += parseInt(id[i]) * weights[i] + return checkCodes[sum % 11] === id[17].toUpperCase() +} + // 证件类型映射: certType -> idCardType (后端字段) const certTypeMap = { idcard: 1, child: 2, temp: 3 } @@ -193,12 +217,6 @@ const doOcrFront = async (imageUrl) => { const d = res.data form.name = d.name || '' form.idNo = d.idNum || '' - // 从身份证号解析性别和生日 - if (d.idNum && d.idNum.length === 18) { - const genderNum = parseInt(d.idNum.charAt(16)) - form.gender = genderNum % 2 === 1 ? '男' : '女' - form.birthday = `${d.idNum.substring(6, 10)}-${d.idNum.substring(10, 12)}-${d.idNum.substring(12, 14)}` - } uni.hideLoading() } catch (e) { uni.hideLoading() @@ -281,13 +299,28 @@ const handleSubmit = async () => { } if (!form.name) return uni.showToast({ title: '请输入证件姓名', icon: 'none' }) if (!form.idNo) return uni.showToast({ title: '请输入证件号码', icon: 'none' }) + // 身份证格式校验 + if (!validateIdCard(form.idNo)) return uni.showToast({ title: '身份证号格式不正确', icon: 'none' }) + + // 从身份证号解析性别和生日(提交时统一解析,确保一定有值) + if (form.idNo.length === 18) { + const genderNum = parseInt(form.idNo.charAt(16)) + form.gender = genderNum % 2 === 1 ? '男' : '女' + form.birthday = `${form.idNo.substring(6, 10)}-${form.idNo.substring(10, 12)}-${form.idNo.substring(12, 14)}` + } + if (!form.phone || form.phone.length !== 11) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' }) if (!form.smsCode || form.smsCode.length !== 6) return uni.showToast({ title: '请输入6位验证码', icon: 'none' }) + await doSubmit(false) +} + +const doSubmit = async (confirmBind) => { + showBindPopup.value = false submitting.value = true try { await post('/api/mp/authSubmit', { - idCardType, + idCardType: certTypeMap[certType.value], idCardFront: form.frontImage, idCardBack: form.backImage, photo: form.childImage, @@ -298,7 +331,8 @@ const handleSubmit = async () => { issuingAuthority: form.authority, validPeriod: form.validity, mobile: form.phone, - code: form.smsCode + code: form.smsCode, + confirmBind: confirmBind || undefined }) // 刷新用户信息缓存 @@ -310,7 +344,16 @@ const handleSubmit = async () => { uni.showToast({ title: '认证成功', icon: 'success' }) setTimeout(() => { uni.navigateBack() }, 1500) } catch (e) { - if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) + if (e && e.code === 1010) { + // 患者已存在,弹出确认绑定弹窗 + bindPatientInfo.value = { + name: e.data?.patientName || '', + phone: e.data?.patientPhone || '' + } + showBindPopup.value = true + } else if (e && e.msg) { + uni.showToast({ title: e.msg, icon: 'none' }) + } } finally { submitting.value = false } @@ -611,4 +654,30 @@ const handleSubmit = async () => { opacity: 0.85; } } + +.confirm-popup { + padding: 48rpx 40rpx 40rpx; + width: 560rpx; +} + +.confirm-title { + font-size: 34rpx; + font-weight: 600; + color: #333; + text-align: center; + margin-bottom: 28rpx; +} + +.confirm-content { + font-size: 28rpx; + color: #666; + line-height: 1.7; + text-align: center; + margin-bottom: 40rpx; +} + +.confirm-btns { + display: flex; + gap: 24rpx; +} diff --git a/utils/request.js b/utils/request.js index bc8c3df..491a534 100644 --- a/utils/request.js +++ b/utils/request.js @@ -49,6 +49,11 @@ export function initRequest(http) { return Promise.reject({ code, msg: msg || '请先登录' }) } + // 1010: 需要前端确认的场景,不自动弹toast,由业务层处理 + if (code === 1010) { + return Promise.reject({ code, data, msg: msg || '' }) + } + uni.showToast({ title: msg || '请求失败', icon: 'none' }) return Promise.reject({ code, msg: msg || '请求失败' }) },