<template>
|
<view class="page">
|
<!-- 自定义导航栏:状态栏留白由行内 paddingTop 撑开 -->
|
<view class="nav" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="nav-bar">
|
<!-- 返回 -->
|
<view class="nav-back" @tap="handleBack">
|
<view class="icon-back"></view>
|
</view>
|
<text class="nav-title">实名认证</text>
|
<!-- 更多 / 帮助(本期占位) -->
|
<view class="nav-actions">
|
<view class="nav-action" @tap="handlePlaceholder('更多')">
|
<view class="icon-more">
|
<view class="dot"></view>
|
<view class="dot"></view>
|
<view class="dot"></view>
|
</view>
|
</view>
|
<view class="nav-action" @tap="handlePlaceholder('帮助')">
|
<view class="icon-help"><text class="icon-help-text">?</text></view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="body">
|
<!-- 姓名 -->
|
<view class="field">
|
<text class="field-label">姓名</text>
|
<input
|
class="field-input"
|
:class="{ 'field-input--readonly': readonly }"
|
v-model="realName"
|
type="text"
|
placeholder="请输入姓名"
|
placeholder-class="field-placeholder"
|
maxlength="64"
|
:disabled="readonly"
|
/>
|
</view>
|
|
<!-- 身份证号 -->
|
<view class="field">
|
<text class="field-label">身份证号</text>
|
<input
|
class="field-input"
|
:class="{ 'field-input--readonly': readonly }"
|
v-model="idCard"
|
type="idcard"
|
placeholder="请输入身份证号"
|
placeholder-class="field-placeholder"
|
maxlength="18"
|
:disabled="readonly"
|
/>
|
<!-- 认证失败:紧贴身份证号下方展示驳回原因 -->
|
<view v-if="showRejectReason" class="reject-tip">
|
<text class="reject-tip-text">驳回原因:{{ rejectReason }}</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 底部提交 -->
|
<view class="footer">
|
<view class="btn-submit" :class="{ 'btn-submit--disabled': submitDisabled }" @tap="handleSubmit">
|
<text class="btn-submit-text">{{ submitText }}</text>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from 'vue'
|
import { onShow } from '@dcloudio/uni-app'
|
import { ensureLogin } from '@/utils/request.js'
|
import { getCertificationStatus, submitCertification } from '@/api/index.js'
|
|
/** 状态栏高度:自定义导航栏下用于顶部留白 */
|
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
|
/**
|
* 身份证号格式(与后端 AccountCertificationApplyDto 保持一致)
|
* 只校验结构:6 位地址码 + 8 位出生日期 + 3 位顺序码 + 1 位校验位(数字或 X),
|
* 不校验末位校验码,也不做三方实名核验。
|
*/
|
const ID_CARD_PATTERN = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
|
|
/** 姓名 */
|
const realName = ref('')
|
|
/** 身份证号 */
|
const idCard = ref('')
|
|
/** 提交中标记:防止重复点击造成重复提交 */
|
const submitting = ref(false)
|
|
/**
|
* 认证审核状态(取自最近一次申请,进入页面时回填)
|
* '0' 待审核 / '1' 已通过 / '2' 已驳回 / '' 从未提交(未认证)
|
*/
|
const auditStatus = ref('')
|
|
/** 驳回原因:仅审核状态为「已驳回」时有值 */
|
const rejectReason = ref('')
|
|
/** 表单只读:认证中 / 认证成功时姓名与身份证号不可编辑 */
|
const readonly = computed(() => auditStatus.value === '0' || auditStatus.value === '1')
|
|
/** 提交按钮置灰:只读状态或正在提交时不可点 */
|
const submitDisabled = computed(() => readonly.value || submitting.value)
|
|
/** 是否展示驳回原因:认证失败且后端返回了驳回原因 */
|
const showRejectReason = computed(() => auditStatus.value === '2' && !!rejectReason.value)
|
|
/**
|
* 按钮文案字典:按审核状态区分
|
* '' 未认证→提交 / '0' 认证中→审核中 / '1' 认证成功→已认证 / '2' 认证失败→重新提交
|
*/
|
const SUBMIT_TEXT_MAP = {
|
'': '提交',
|
'0': '审核中',
|
'1': '已认证',
|
'2': '重新提交'
|
}
|
|
/** 底部按钮文案:随认证状态变化,未匹配时兜底「提交」 */
|
const submitText = computed(() => SUBMIT_TEXT_MAP[auditStatus.value] || '提交')
|
|
/** 返回上一页:无上级页面(如直接进入)时回到个人中心 */
|
const handleBack = () => {
|
const pages = getCurrentPages()
|
if (pages.length > 1) {
|
uni.navigateBack()
|
return
|
}
|
uni.switchTab({ url: '/pages/profile/index' })
|
}
|
|
/** 占位交互:更多 / 帮助本期不接后端 */
|
const handlePlaceholder = (name) => {
|
uni.showToast({ title: `${name}功能开发中`, icon: 'none' })
|
}
|
|
/**
|
* 拉取当前认证状态并回填表单
|
* 认证中 / 认证成功回填姓名与身份证号(只读);认证失败回填资料并展示驳回原因;
|
* 从未提交(auditStatus 为空)保持空表单可编辑。查询失败静默降级为「未认证」,不阻断用户填报。
|
*/
|
const loadCertificationStatus = async () => {
|
try {
|
// request 返回完整响应体 { code, msg, data },业务数据在 data 字段
|
const res = await getCertificationStatus()
|
const data = res.data || {}
|
// 从未提交时后端返回空字段对象,此处保持空值即为「未认证」
|
auditStatus.value = data.auditStatus || ''
|
realName.value = data.realName || ''
|
idCard.value = data.idCard || ''
|
rejectReason.value = data.auditRemark || ''
|
} catch (error) {
|
console.error('[CERTIFICATION] 查询认证状态失败', error)
|
auditStatus.value = ''
|
rejectReason.value = ''
|
}
|
}
|
|
/**
|
* 提交实名认证:本地基础校验(姓名必填、身份证号格式)通过后调接口
|
* 成功提示等待审核并返回上一页;失败展示后端返回的业务提示
|
*/
|
const handleSubmit = async () => {
|
// STEP 0: 只读 / 提交中状态直接拦截,避免重复提交
|
if (submitDisabled.value) return
|
const name = realName.value.trim()
|
const card = idCard.value.trim()
|
// STEP 1: 姓名必填校验
|
if (!name) {
|
uni.showToast({ title: '请输入姓名', icon: 'none' })
|
return
|
}
|
// STEP 2: 身份证号格式校验
|
if (!ID_CARD_PATTERN.test(card)) {
|
uni.showToast({ title: '身份证号格式不正确', icon: 'none' })
|
return
|
}
|
// STEP 3: 锁定提交态并提交到后端,落后台「主体认证审核」待审队列
|
submitting.value = true
|
uni.showLoading({ title: '提交中', mask: true })
|
try {
|
await submitCertification(name, card)
|
uni.hideLoading()
|
uni.showToast({ title: '提交成功,等待审核', icon: 'none' })
|
setTimeout(() => uni.navigateBack(), 1500)
|
} catch (error) {
|
uni.hideLoading()
|
console.error('[CERTIFICATION] 提交实名认证失败', error)
|
uni.showToast({ title: (error && error.message) || '提交失败,请重试', icon: 'none' })
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
// 进入页面校验登录态并拉取认证状态回填(未登录跳登录页)
|
onShow(() => {
|
if (!ensureLogin()) return
|
loadCertificationStatus()
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.page {
|
min-height: 100vh;
|
display: flex;
|
flex-direction: column;
|
box-sizing: border-box;
|
background-color: #ffffff;
|
}
|
|
/* ==================== 自定义导航栏 ==================== */
|
|
.nav {
|
flex-shrink: 0;
|
background-color: #ffffff;
|
}
|
|
.nav-bar {
|
position: relative;
|
height: 88rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.nav-back {
|
position: absolute;
|
left: 16rpx;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 64rpx;
|
height: 64rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 纯 CSS 左向箭头:左 + 下边框旋转 45° */
|
.icon-back {
|
width: 22rpx;
|
height: 22rpx;
|
border-left: 4rpx solid #1f2937;
|
border-bottom: 4rpx solid #1f2937;
|
transform: rotate(45deg);
|
}
|
|
.nav-title {
|
font-size: 34rpx;
|
font-weight: 700;
|
line-height: 1;
|
letter-spacing: 1rpx;
|
color: #0f172a;
|
}
|
|
.nav-actions {
|
position: absolute;
|
right: 28rpx;
|
top: 50%;
|
transform: translateY(-50%);
|
display: flex;
|
align-items: center;
|
gap: 32rpx;
|
}
|
|
.nav-action {
|
width: 56rpx;
|
height: 56rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 更多:三点 */
|
.icon-more {
|
display: flex;
|
align-items: center;
|
gap: 6rpx;
|
}
|
|
.icon-more .dot {
|
width: 8rpx;
|
height: 8rpx;
|
border-radius: 50%;
|
background: #334155;
|
}
|
|
/* 帮助:圆圈问号 */
|
.icon-help {
|
width: 36rpx;
|
height: 36rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-sizing: border-box;
|
border: 3rpx solid #334155;
|
border-radius: 50%;
|
}
|
|
.icon-help-text {
|
font-size: 22rpx;
|
font-weight: 700;
|
line-height: 1;
|
color: #334155;
|
}
|
|
/* ==================== 表单 ==================== */
|
|
.body {
|
flex: 1;
|
padding: 24rpx 32rpx 0;
|
}
|
|
.field {
|
margin-bottom: 40rpx;
|
}
|
|
.field-label {
|
display: block;
|
font-size: 30rpx;
|
line-height: 1;
|
color: #1f2937;
|
}
|
|
.field-input {
|
height: 88rpx;
|
margin-top: 8rpx;
|
font-size: 30rpx;
|
color: #1f2937;
|
border-bottom: 2rpx solid #e2e8f0;
|
}
|
|
.field-placeholder {
|
font-size: 30rpx;
|
color: #cbd5e1;
|
}
|
|
/* 只读态:认证中 / 认证成功时置灰文字,视觉上表达不可编辑 */
|
.field-input--readonly {
|
color: #9ca3af;
|
}
|
|
/* ==================== 驳回原因 ==================== */
|
|
.reject-tip {
|
margin-top: 12rpx;
|
}
|
|
.reject-tip-text {
|
font-size: 26rpx;
|
line-height: 1.4;
|
color: #ef4444;
|
}
|
|
/* ==================== 底部提交 ==================== */
|
|
.footer {
|
/* 内容不足一屏时按钮自动贴底 */
|
margin-top: auto;
|
padding: 32rpx 48rpx calc(44rpx + env(safe-area-inset-bottom));
|
}
|
|
.btn-submit {
|
width: 100%;
|
height: 96rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 12rpx;
|
background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
|
box-shadow: 0 16rpx 40rpx rgba(99, 156, 31, 0.28);
|
}
|
|
.btn-submit--disabled {
|
opacity: 0.6;
|
}
|
|
.btn-submit-text {
|
font-size: 32rpx;
|
font-weight: 600;
|
line-height: 1;
|
color: #ffffff;
|
}
|
</style>
|