consumer-app/pages/profileSub/realNameAuth.vue

781 lines
22 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="real-name-auth-page">
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="实名认证" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- 加载中 -->
<view class="loading-wrap" v-if="realNameLoading">
<text class="loading-text">加载实名信息中...</text>
</view>
<!-- 表单内容区 -->
<scroll-view class="form-content" scroll-y="true" v-else>
<!-- -->
<view class="form-section">
<view class="section-title">基本信息</view>
<!-- 实名状态提示不可编辑时显示 -->
<view class="status-tip" v-if="!formEditable && realNameStatus != null">
<text class="status-tip-text">当前状态:{{ realNameStatusText }},不可修改</text>
</view>
<!-- 真实姓名 -->
<view class="form-item">
<view class="input-wrapper">
<text class="input-label">真实姓名</text>
<input
class="input-field"
type="text"
v-model="formData.realName"
placeholder="请输入真实姓名"
maxlength="20"
:disabled="!formEditable"
/>
</view>
</view>
<!-- 证件类型 -->
<view class="form-item">
<view class="input-wrapper">
<text class="input-label">证件类型</text>
<picker
v-if="formEditable"
mode="selector"
:range="idTypeOptions"
range-key="label"
:value="idTypeIndex"
@change="handleIdTypeChange"
>
<view class="picker-view">
<text :class="['picker-text', !formData.idType ? 'placeholder' : '']">
{{ formData.idType ? idTypeOptions.find(item => item.value === formData.idType)?.label : '请选择证件类型' }}
</text>
</view>
</picker>
<text v-else class="picker-text readonly">{{ idTypeLabel }}</text>
</view>
</view>
<!-- 证件号码 -->
<view class="form-item">
<view class="input-wrapper">
<text class="input-label">证件号码</text>
<input
class="input-field"
type="text"
v-model="formData.idNumber"
placeholder="请输入证件号码"
maxlength="30"
:disabled="!formEditable"
/>
</view>
</view>
</view>
<!-- 身份证照片 -->
<view class="form-section">
<view class="section-title">身份证照片</view>
<!-- 身份证正面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">身份证正面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('idFrontImg')">
<image
v-if="formData.idFrontImg"
class="uploaded-image"
:src="formData.idFrontImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
<!-- 身份证背面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">身份证背面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('idBackImg')">
<image
v-if="formData.idBackImg"
class="uploaded-image"
:src="formData.idBackImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
</view>
<!-- 驾驶证照片 -->
<view class="form-section">
<view class="section-title">驾驶证照片</view>
<!-- 驾驶证正面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">驾驶证正面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('driverLicenseFrontImg')">
<image
v-if="formData.driverLicenseFrontImg"
class="uploaded-image"
:src="formData.driverLicenseFrontImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
<!-- 驾驶证背面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">驾驶证背面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('driverLicenseBackImg')">
<image
v-if="formData.driverLicenseBackImg"
class="uploaded-image"
:src="formData.driverLicenseBackImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
</view>
<!-- 行驶证照片 -->
<view class="form-section">
<view class="section-title">行驶证照片</view>
<!-- 行驶证正面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">行驶证正面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('vehicleLicenseFrontImg')">
<image
v-if="formData.vehicleLicenseFrontImg"
class="uploaded-image"
:src="formData.vehicleLicenseFrontImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
<!-- 行驶证背面 -->
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">行驶证背面</text>
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('vehicleLicenseBackImg')">
<image
v-if="formData.vehicleLicenseBackImg"
class="uploaded-image"
:src="formData.vehicleLicenseBackImg"
mode="aspectFill"
></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
</view>
</view>
</view>
</view>
<!-- 备注信息 -->
<view class="form-section">
<view class="section-title">备注信息</view>
<view class="form-item">
<view class="textarea-wrapper">
<textarea
class="textarea-field"
v-model="formData.imgVerifyRemark"
placeholder="请输入备注信息(选填)"
maxlength="200"
:auto-height="true"
:disabled="!formEditable"
></textarea>
</view>
</view>
</view>
<!-- 协议同意区域(仅可编辑时显示) -->
<view class="agreement-section" v-if="formEditable">
<view class="agreement-checkbox" @click="toggleAgreement">
<view class="checkbox-icon" :class="{ checked: agreedToTerms }">
<text v-if="agreedToTerms" class="checkmark">✓</text>
</view>
<text class="agreement-text">
我已阅读并同意
<text class="link-text" @click.stop="goToUserAgreement">《用户服务协议》</text>
<text class="link-text" @click.stop="goToPrivacyPolicy">《隐私政策》</text>
</text>
</view>
</view>
<!-- 提交按钮(仅可编辑时显示) -->
<view class="submit-section" v-if="formEditable">
<button
class="submit-btn"
:class="{ disabled: loading || !agreedToTerms }"
:disabled="loading || !agreedToTerms"
@click="handleSubmit"
>
{{ loading ? '提交中...' : '提交认证' }}
</button>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { createRealNameInfo, getRealNameInfo } from '@/api/profile.js'
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
components: {
NavHeader
},
data() {
return {
statusBarHeight: 0,
loading: false,
realNameLoading: true, // 实名信息加载中
realNameStatus: null, // 实名状态0 未认证/没有 1 待审核 2 已通过 3 已退回 4 已过期
agreedToTerms: false, // 是否同意协议
idTypeIndex: 0,
idTypeOptions: [
{ label: '身份证', value: 1 },
{ label: '其他', value: 2 }
],
formData: {
realName: '',
idType: null,
idNumber: '',
idFrontImg: '',
idBackImg: '',
driverLicenseFrontImg: '',
driverLicenseBackImg: '',
vehicleLicenseFrontImg: '',
vehicleLicenseBackImg: '',
imgVerifyRemark: ''
}
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44
},
// 仅 没有(无数据/0)、退回(3)、过期(4) 可编辑;待审核(1)、已通过(2) 不可编辑
formEditable() {
const s = this.realNameStatus
if (s == null) return true
return s === 0 || s === 3 || s === 4
},
realNameStatusText() {
const map = { 0: '未认证', 1: '待审核', 2: '已通过', 3: '已退回', 4: '已过期' }
return map[this.realNameStatus] ?? '未知'
},
idTypeLabel() {
if (this.formData.idType == null) return '请选择证件类型'
const opt = this.idTypeOptions.find(item => item.value === this.formData.idType)
return opt ? opt.label : '请选择证件类型'
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
this.loadRealNameInfo()
},
methods: {
async loadRealNameInfo() {
this.realNameLoading = true
try {
const userInfo = uni.getStorageSync('userInfo')
if (!userInfo || !userInfo.id) {
this.realNameStatus = 0
return
}
const res = await getRealNameInfo({ id: userInfo.id })
this.fillFormFromApi(res)
} catch (e) {
console.error('获取实名信息失败:', e)
this.realNameStatus = 0
} finally {
this.realNameLoading = false
}
},
fillFormFromApi(data) {
if (!data || typeof data !== 'object') {
this.realNameStatus = 0
return
}
const s = data.status != null ? Number(data.status) : (data.id ? 1 : 0)
this.realNameStatus = s
this.formData.realName = data.realName ?? ''
this.formData.idType = data.idType != null ? data.idType : null
this.formData.idNumber = data.idNumber ?? ''
this.formData.idFrontImg = data.idFrontImg ?? ''
this.formData.idBackImg = data.idBackImg ?? ''
this.formData.driverLicenseFrontImg = data.driverLicenseFrontImg ?? ''
this.formData.driverLicenseBackImg = data.driverLicenseBackImg ?? ''
this.formData.vehicleLicenseFrontImg = data.vehicleLicenseFrontImg ?? ''
this.formData.vehicleLicenseBackImg = data.vehicleLicenseBackImg ?? ''
this.formData.imgVerifyRemark = data.imgVerifyRemark ?? ''
const idx = this.idTypeOptions.findIndex(item => item.value === this.formData.idType)
this.idTypeIndex = idx >= 0 ? idx : 0
},
toggleAgreement() {
this.agreedToTerms = !this.agreedToTerms
},
goToUserAgreement() {
uni.navigateTo({ url: '/pages/profileSub/userAgreement' })
},
goToPrivacyPolicy() {
uni.navigateTo({ url: '/pages/profileSub/privacyPolicy' })
},
// 证件类型选择
handleIdTypeChange(e) {
this.idTypeIndex = e.detail.value
this.formData.idType = this.idTypeOptions[e.detail.value].value
},
// 选择图片
chooseImage(field) {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
// 先显示本地预览
this.formData[field] = tempFilePath
// 上传图片到服务器
this.uploadImage(tempFilePath, field)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
},
// 上传图片
uploadImage(filePath, field) {
uni.showLoading({
title: '上传中...',
mask: true
})
// 获取token
const token = uni.getStorageSync('token')
const BASE_URL = 'https://siji.chenjuncn.top'
uni.uploadFile({
url: `${BASE_URL}/app-api/infra/file/upload`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${token}`,
'tenant-id': '1'
},
success: (res) => {
uni.hideLoading()
try {
const data = JSON.parse(res.data)
if (data.code === 200 || data.code === 0) {
// 上传成功保存图片URL
const imageUrl = data.data?.url || data.data || data.url
if (imageUrl) {
this.formData[field] = imageUrl
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
})
} else {
throw new Error('上传成功但未返回图片地址')
}
} else {
throw new Error(data.message || data.msg || '上传失败')
}
} catch (error) {
console.error('解析上传结果失败:', error)
uni.showToast({
title: '上传失败,请重试',
icon: 'none'
})
// 上传失败,清除预览
this.formData[field] = ''
}
},
fail: (err) => {
uni.hideLoading()
console.error('上传图片失败:', err)
uni.showToast({
title: '上传失败,请检查网络',
icon: 'none'
})
// 上传失败,清除预览
this.formData[field] = ''
}
})
},
// 提交表单
async handleSubmit() {
// 基本验证
if (!this.formData.realName || !this.formData.realName.trim()) {
uni.showToast({
title: '请输入真实姓名',
icon: 'none'
})
return
}
if (!this.formData.idType) {
uni.showToast({
title: '请选择证件类型',
icon: 'none'
})
return
}
if (!this.formData.idNumber || !this.formData.idNumber.trim()) {
uni.showToast({
title: '请输入证件号码',
icon: 'none'
})
return
}
this.loading = true
try {
// 构建提交数据,只包含有值的字段
const submitData = {}
if (this.formData.realName) submitData.realName = this.formData.realName.trim()
if (this.formData.idType !== null) submitData.idType = this.formData.idType
if (this.formData.idNumber) submitData.idNumber = this.formData.idNumber.trim()
if (this.formData.idFrontImg) submitData.idFrontImg = this.formData.idFrontImg
if (this.formData.idBackImg) submitData.idBackImg = this.formData.idBackImg
if (this.formData.driverLicenseFrontImg) submitData.driverLicenseFrontImg = this.formData.driverLicenseFrontImg
if (this.formData.driverLicenseBackImg) submitData.driverLicenseBackImg = this.formData.driverLicenseBackImg
if (this.formData.vehicleLicenseFrontImg) submitData.vehicleLicenseFrontImg = this.formData.vehicleLicenseFrontImg
if (this.formData.vehicleLicenseBackImg) submitData.vehicleLicenseBackImg = this.formData.vehicleLicenseBackImg
if (this.formData.imgVerifyRemark) submitData.imgVerifyRemark = this.formData.imgVerifyRemark.trim()
await createRealNameInfo(submitData)
uni.showToast({
title: '提交成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('提交失败:', error)
// 错误信息已在request中统一处理
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.real-name-auth-page {
min-height: 100vh;
background: #e2e8f1;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.loading-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999999;
}
}
.form-content {
height: calc(100vh - 120rpx);
padding: 0 30rpx 40rpx;
box-sizing: border-box;
}
.form-section {
margin-bottom: 40rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
padding-left: 10rpx;
}
.status-tip {
margin-bottom: 20rpx;
padding: 16rpx 20rpx;
background: rgba(255, 152, 0, 0.1);
border-radius: 12rpx;
.status-tip-text {
font-size: 26rpx;
color: #e65100;
}
}
}
.form-item {
margin-bottom: 30rpx;
.input-wrapper {
position: relative;
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.input-label {
width: 160rpx;
font-size: 28rpx;
color: #333333;
font-weight: 500;
flex-shrink: 0;
}
.input-field {
flex: 1;
font-size: 28rpx;
color: #1a1819;
}
.picker-view {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
.picker-text {
font-size: 28rpx;
color: #1a1819;
&.placeholder {
color: #999999;
}
&.readonly {
color: #666666;
}
}
.picker-arrow {
font-size: 40rpx;
color: #cccccc;
line-height: 1;
}
}
}
.upload-wrapper {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.upload-label {
display: block;
font-size: 28rpx;
color: #333333;
font-weight: 500;
margin-bottom: 20rpx;
}
.upload-box {
width: 100%;
height: 300rpx;
border: 2rpx dashed #d0d0d0;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
position: relative;
overflow: hidden;
.uploaded-image {
width: 100%;
height: 100%;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
.upload-icon {
font-size: 60rpx;
color: #999999;
line-height: 1;
}
.upload-text {
font-size: 24rpx;
color: #999999;
}
}
&.disabled {
pointer-events: none;
opacity: 0.85;
}
}
}
.textarea-wrapper {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.textarea-field {
width: 100%;
min-height: 200rpx;
font-size: 28rpx;
color: #1a1819;
line-height: 1.6;
}
}
}
.agreement-section {
padding: 30rpx;
padding-bottom: 0;
.agreement-checkbox {
display: flex;
align-items: flex-start;
gap: 12rpx;
.checkbox-icon {
width: 32rpx;
height: 32rpx;
border: 1rpx solid #cccccc;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 4rpx;
&.checked {
background-color: #004294;
border-color: #004294;
.checkmark {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
}
}
}
.agreement-text {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
flex: 1;
.link-text {
color: #004294;
text-decoration: underline;
}
}
}
}
.submit-section {
margin-top: 20rpx;
padding: 0 30rpx;
padding-bottom: 40rpx;
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
&::after {
border: none;
}
&.disabled {
background: #cccccc;
color: #999999;
}
}
}
</style>