|
- <template>
- <view class="page">
- <view class="page-content">
- <view class="page-title">请确认申请人信息</view>
-
- <!-- 第1步:证件类型选择 + 上传 -->
- <view class="section">
- <view class="section-header">
- <view class="step-num">1</view>
- <text class="label">为谁申请</text>
- <text class="tag warn">请用患者本人信息进行实名申请</text>
- </view>
- <view class="section-desc">请选择用来实名证件的类型</view>
-
- <view class="cert-tabs">
- <view class="cert-tab" :class="{ active: certType === 'idcard' }" @tap="certType = 'idcard'">
- <text>身份证证件</text>
- </view>
- <view class="cert-tab" :class="{ active: certType === 'child' }" @tap="certType = 'child'">
- <text>无证件儿童</text>
- </view>
- <view class="cert-tab" :class="{ active: certType === 'temp' }" @tap="certType = 'temp'">
- <text>临时身份证</text>
- </view>
- </view>
-
- <!-- 身份证 / 临时身份证 -->
- <view v-if="certType === 'idcard' || certType === 'temp'" class="upload-area">
- <view class="upload-item" @tap="chooseImage('front')">
- <image :src="form.frontImage || placeholderFront" mode="aspectFill" class="upload-img" />
- <text class="upload-label">请上传证件人像面</text>
- </view>
- <view class="upload-item" @tap="chooseImage('back')">
- <image :src="form.backImage || placeholderBack" mode="aspectFill" class="upload-img" />
- <text class="upload-label">请上传证件国徽面</text>
- </view>
- </view>
-
- <!-- 无证件儿童 -->
- <view v-if="certType === 'child'" class="child-upload">
- <view class="child-tip">请上传近期免冠照片 <text class="tip-highlight">用于无证件儿童身份核验</text></view>
- <view class="child-desc">为了避免增加审核时长,如已办理身份证,请走身份证件通道</view>
- <view class="child-photo-box" @tap="chooseImage('child')">
- <image v-if="form.childImage" :src="form.childImage" mode="aspectFill" class="child-photo-img" />
- <view v-else class="child-photo-placeholder">
- <u-icon name="camera" size="40" color="#ccc" />
- </view>
- </view>
- </view>
- </view>
-
- <!-- 第2步:信息核验 -->
- <view class="section">
- <view class="section-header">
- <view class="step-num">2</view>
- <text class="label">信息核验</text>
- <text class="tag info">如姓名有误,请进行修改再提交</text>
- </view>
- <view class="section-desc">请核对证件信息是否有误</view>
-
- <view class="form-row">
- <text class="form-label">证件姓名</text>
- <input class="form-input" v-model="form.name" placeholder="上传身份证后自动识别" />
- </view>
- <view class="form-row">
- <text class="form-label">证件号码</text>
- <input class="form-input" v-model="form.idNo" placeholder="上传身份证后自动识别" />
- </view>
- <view class="form-row">
- <text class="form-label">发证机关</text>
- <input class="form-input" v-model="form.authority" placeholder="上传身份证后自动识别" />
- </view>
- <view class="form-row border-none">
- <text class="form-label">有效期限</text>
- <input class="form-input" v-model="form.validity" placeholder="上传身份证后自动识别" />
- </view>
- </view>
-
- <!-- 第3步:手机号码绑定 -->
- <view class="section">
- <view class="section-header">
- <view class="step-num">3</view>
- <text class="label">手机号码绑定</text>
- </view>
- <view class="section-desc">请输入未进行过平台认证的号码进行绑定</view>
-
- <view class="phone-row">
- <input class="phone-input" type="number" v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
- </view>
- <view class="sms-row">
- <input class="sms-input" type="number" v-model="form.smsCode" placeholder="请输入验证码" maxlength="6" />
- <button class="sms-btn" :disabled="countdown > 0" @tap="sendSms">
- {{ countdown > 0 ? countdown + 's后重新获取' : '获取验证码' }}
- </button>
- </view>
- </view>
- </view>
-
- <!-- 底部提交按钮 -->
- <view class="submit-bar">
- <button class="submit-btn" @tap="handleSubmit">提交认证</button>
- </view>
- </view>
- </template>
-
- <script setup>
- import { ref, reactive, onUnmounted } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { get, post, upload } from '@/utils/request.js'
- import { getUserInfo, setUserInfo } from '@/utils/cache.js'
-
- const CDN_BASE = 'https://cdn.csybhelp.com'
- const placeholderFront = CDN_BASE + '/images/patient/user/idcard-front.webp?v=1'
- const placeholderBack = CDN_BASE + '/images/patient/user/idcard-back.webp?v=1'
-
- const certType = ref('idcard')
- const countdown = ref(0)
- const submitting = ref(false)
- let timer = null
-
- // 证件类型映射: certType -> idCardType (后端字段)
- const certTypeMap = { idcard: 1, child: 2, temp: 3 }
-
- const form = reactive({
- frontImage: '', // 上传后的CDN URL
- backImage: '',
- childImage: '',
- name: '',
- idNo: '',
- authority: '',
- validity: '',
- gender: '',
- birthday: '',
- phone: '',
- smsCode: ''
- })
-
- // 页面显示时加载已有认证信息
- onShow(() => {
- loadAuthInfo()
- })
-
- onUnmounted(() => {
- if (timer) { clearInterval(timer); timer = null }
- })
-
- const loadAuthInfo = async () => {
- try {
- const res = await get('/api/mp/authInfo')
- if (res.data.authStatus === 1) {
- const d = res.data
- // 反向映射 idCardType -> certType
- const typeMap = { 1: 'idcard', 2: 'child', 3: 'temp' }
- certType.value = typeMap[d.idCardType] || 'idcard'
- form.frontImage = d.idCardFront || ''
- form.backImage = d.idCardBack || ''
- form.childImage = d.photo || ''
- form.name = d.realName || ''
- form.idNo = d.idCard || ''
- form.authority = d.issuingAuthority || ''
- form.validity = d.validPeriod || ''
- form.gender = d.gender || ''
- form.birthday = d.birthday || ''
- form.phone = d.phone || ''
- }
- } catch (e) {
- // 未认证,忽略
- }
- }
-
- // 上传图片到COS,返回CDN URL
- const uploadImage = (tempFilePath) => {
- return new Promise((resolve, reject) => {
- uni.showLoading({ title: '上传中...' })
- upload('/api/mp/upload', { filePath: tempFilePath, name: 'file' })
- .then(res => {
- uni.hideLoading()
- resolve(res.data.url)
- })
- .catch(err => {
- uni.hideLoading()
- uni.showToast({ title: '上传失败', icon: 'none' })
- reject(err)
- })
- })
- }
-
- // OCR识别身份证正面
- const doOcrFront = async (imageUrl) => {
- uni.showLoading({ title: '识别中...' })
- try {
- const res = await post('/common/ocr/idcard', { imageUrl, cardSide: 'FRONT' })
- 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()
- uni.showToast({ title: '识别失败,请手动填写', icon: 'none' })
- }
- }
-
- // OCR识别身份证反面
- const doOcrBack = async (imageUrl) => {
- uni.showLoading({ title: '识别中...' })
- try {
- const res = await post('/common/ocr/idcard', { imageUrl, cardSide: 'BACK' })
- form.authority = res.data.authority || ''
- form.validity = res.data.validDate || ''
- uni.hideLoading()
- } catch (e) {
- uni.hideLoading()
- }
- }
-
- const chooseImage = (type) => {
- uni.chooseImage({
- count: 1,
- sizeType: ['compressed'],
- sourceType: ['album', 'camera'],
- success: async (res) => {
- const tempPath = res.tempFilePaths[0]
- try {
- const url = await uploadImage(tempPath)
- if (type === 'front') {
- form.frontImage = url
- await doOcrFront(url)
- } else if (type === 'back') {
- form.backImage = url
- await doOcrBack(url)
- } else if (type === 'child') {
- form.childImage = url
- }
- } catch (e) {
- // 上传失败已提示
- }
- }
- })
- }
-
- const sendSms = async () => {
- if (countdown.value > 0) return
- if (!form.phone || form.phone.length !== 11) {
- uni.showToast({ title: '请输入正确的手机号码', icon: 'none' })
- return
- }
- try {
- const res = await post('/api/mp/sendSmsCode', { mobile: form.phone, bizType: 'real_name_auth' })
- // 开发环境可能返回验证码
- if (res.data?.code) {
- uni.showToast({ title: `验证码: ${res.data.code}`, icon: 'none', duration: 3000 })
- } else {
- uni.showToast({ title: '验证码已发送', icon: 'none' })
- }
- countdown.value = 60
- timer = setInterval(() => {
- countdown.value--
- if (countdown.value <= 0) { clearInterval(timer); timer = null }
- }, 1000)
- } catch (e) {
- if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
- }
- }
-
- const handleSubmit = async () => {
- if (submitting.value) return
-
- // 前端校验
- const idCardType = certTypeMap[certType.value]
- if (idCardType === 2) {
- if (!form.childImage) return uni.showToast({ title: '请上传免冠照片', icon: 'none' })
- } else {
- if (!form.frontImage) return uni.showToast({ title: '请上传证件人像面', icon: 'none' })
- if (!form.backImage) return uni.showToast({ title: '请上传证件国徽面', icon: 'none' })
- }
- if (!form.name) return uni.showToast({ title: '请输入证件姓名', icon: 'none' })
- if (!form.idNo) return uni.showToast({ title: '请输入证件号码', icon: 'none' })
- 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' })
-
- submitting.value = true
- try {
- await post('/api/mp/authSubmit', {
- idCardType,
- idCardFront: form.frontImage,
- idCardBack: form.backImage,
- photo: form.childImage,
- realName: form.name,
- idCard: form.idNo,
- gender: form.gender,
- birthday: form.birthday,
- issuingAuthority: form.authority,
- validPeriod: form.validity,
- mobile: form.phone,
- code: form.smsCode
- })
-
- // 刷新用户信息缓存
- try {
- const userRes = await get('/api/mp/userinfo')
- setUserInfo(userRes.data)
- } catch (e) {}
-
- uni.showToast({ title: '认证成功', icon: 'success' })
- setTimeout(() => { uni.navigateBack() }, 1500)
- } catch (e) {
- if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
- } finally {
- submitting.value = false
- }
- }
- </script>
-
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: #f4f4f5;
- padding-bottom: 200rpx;
- }
-
- .page-content {
- padding: 32rpx;
- }
-
- .page-title {
- font-size: 36rpx;
- font-weight: 600;
- color: #303133;
- margin-bottom: 32rpx;
- }
-
- .section {
- background: #fff;
- border-radius: 8rpx;
- padding: 40rpx 32rpx;
- margin-bottom: 24rpx;
- border: 1rpx solid #ebeef5;
- }
-
- .section-header {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 12rpx;
- margin-bottom: 8rpx;
- }
-
- .step-num {
- width: 44rpx;
- height: 44rpx;
- border-radius: 50%;
- background: #0e63e3;
- color: #fff;
- font-size: 24rpx;
- font-weight: 600;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
-
- .label {
- font-size: 32rpx;
- font-weight: 600;
- color: #303133;
- }
-
- .tag {
- font-size: 22rpx;
- border-radius: 8rpx;
- padding: 4rpx 16rpx;
-
- &.warn {
- color: #e6a23c;
- background: #fdf6ec;
- border: 1rpx solid #faecd8;
- }
-
- &.info {
- color: #3a85f0;
- background: #e8f0fe;
- border: 1rpx solid #c6d9f7;
- }
- }
-
- .section-desc {
- font-size: 26rpx;
- color: #909399;
- margin: 12rpx 0 28rpx 56rpx;
- }
-
- .cert-tabs {
- display: flex;
- gap: 0;
- margin-bottom: 32rpx;
- }
-
- .cert-tab {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16rpx 0;
- border: 1rpx solid #dcdfe6;
- font-size: 26rpx;
- color: #606266;
- background: #fff;
- white-space: nowrap;
- margin-left: -1rpx;
-
- &:first-child {
- border-radius: 8rpx 0 0 8rpx;
- margin-left: 0;
- }
-
- &:last-child {
- border-radius: 0 8rpx 8rpx 0;
- }
-
- &.active {
- color: #0e63e3;
- border-color: #0e63e3;
- background: #e8f0fe;
- position: relative;
- z-index: 1;
- }
- }
-
- .upload-area {
- display: flex;
- gap: 24rpx;
- }
-
- .upload-item {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
-
- .upload-img {
- width: 100%;
- height: 200rpx;
- border-radius: 12rpx;
- }
-
- .upload-label {
- font-size: 24rpx;
- color: #606266;
- margin-top: 12rpx;
- }
-
-
- .child-upload {
- margin-top: 24rpx;
- }
-
- .child-tip {
- font-size: 26rpx;
- color: #303133;
- margin-bottom: 8rpx;
-
- .tip-highlight {
- color: #0e63e3;
- margin-left: 10rpx;
- }
- }
-
- .child-desc {
- font-size: 22rpx;
- color: #909399;
- margin-bottom: 20rpx;
- }
-
- .child-photo-box {
- width: 200rpx;
- height: 200rpx;
- border-radius: 12rpx;
- overflow: hidden;
- }
-
- .child-photo-img {
- width: 100%;
- height: 100%;
- }
-
- .child-photo-placeholder {
- width: 100%;
- height: 100%;
- background: #f5f5f5;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .form-row {
- display: flex;
- align-items: center;
- padding: 28rpx 0;
- border-bottom: 1rpx solid #f2f6fc;
-
- &.border-none {
- border-bottom: none;
- }
- }
-
- .form-label {
- width: 160rpx;
- font-size: 28rpx;
- color: #303133;
- flex-shrink: 0;
- }
-
- .form-input {
- flex: 1;
- font-size: 28rpx;
- color: #303133;
- }
-
- .phone-row {
- margin-top: 20rpx;
- }
-
- .phone-input {
- width: 100%;
- height: 80rpx;
- border: 1rpx solid #dcdfe6;
- border-radius: 8rpx;
- padding: 0 24rpx;
- font-size: 28rpx;
- box-sizing: border-box;
- }
-
- .sms-row {
- display: flex;
- align-items: center;
- gap: 20rpx;
- margin-top: 20rpx;
- }
-
- .sms-input {
- flex: 1;
- height: 80rpx;
- border: 1rpx solid #dcdfe6;
- border-radius: 8rpx;
- padding: 0 24rpx;
- font-size: 28rpx;
- box-sizing: border-box;
- }
-
- .sms-btn {
- height: 80rpx;
- line-height: 80rpx;
- padding: 0 28rpx;
- background: #0e63e3;
- color: #fff;
- border: none;
- border-radius: 8rpx;
- font-size: 26rpx;
- white-space: nowrap;
-
- &::after {
- border: none;
- }
-
- &:active {
- opacity: 0.85;
- }
-
- &[disabled] {
- background: #a0cfff;
- color: #fff;
- }
- }
-
- .submit-bar {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- padding: 24rpx 32rpx;
- padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
- background: #fff;
- border-top: 1rpx solid #ebeef5;
- z-index: 10;
- }
-
- .submit-btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- background: #0e63e3;
- color: #fff;
- border: none;
- border-radius: 44rpx;
- font-size: 32rpx;
- font-weight: 500;
- box-shadow: 0 8rpx 24rpx rgba(14, 99, 227, 0.3);
-
- &::after {
- border: none;
- }
-
- &:active {
- opacity: 0.85;
- }
- }
- </style>
|