xiangyixiangqin/miniapp/pages/realname/index.vue
2026-01-07 18:23:39 +08:00

1214 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="realname-page">
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">实名认证</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 已认证状态 (Requirements 12.4) -->
<view v-if="isVerified" class="verified-section" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<view class="verified-icon">✓</view>
<view class="verified-title">实名认证已完成</view>
<view class="verified-info">
<view class="info-item">
<text class="label">姓名:</text>
<text class="value">{{ maskedName }}</text>
</view>
<view class="info-item">
<text class="label">身份证号:</text>
<text class="value">{{ maskedIdNumber }}</text>
</view>
</view>
<view class="verified-badge">
<text class="badge-icon">🛡️</text>
<text class="badge-text">已实名认证</text>
</view>
</view>
<!-- 未认证状态 - 分步骤显示 -->
<view v-else class="unverified-section">
<!-- 固定的步骤指示器 -->
<view class="step-header-fixed" :style="{ top: (statusBarHeight + 44) + 'px' }">
<view class="step-indicator">
<view class="step-item" :class="{ active: currentStep === 1, completed: currentStep > 1 }">
<view class="step-num">{{ currentStep > 1 ? '✓' : '1' }}</view>
<text class="step-text">支付费用</text>
</view>
<view class="step-line" :class="{ completed: currentStep > 1 }"></view>
<view class="step-item" :class="{ active: currentStep === 2, completed: currentStep > 2 }">
<view class="step-num">{{ currentStep > 2 ? '✓' : '2' }}</view>
<text class="step-text">上传证件</text>
</view>
<view class="step-line" :class="{ completed: currentStep > 2 }"></view>
<view class="step-item" :class="{ active: currentStep === 3 }">
<view class="step-num">3</view>
<text class="step-text">认证完成</text>
</view>
</view>
</view>
<!-- 可滚动内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{
top: (statusBarHeight + 44 + 100) + 'px',
height: 'calc(100vh - ' + (statusBarHeight + 44 + 100 + 120) + 'px)'
}"
>
<!-- 步骤1: 支付页面 (Requirements 12.1) -->
<view v-if="currentStep === 1" class="step-payment">
<view class="payment-content">
<view class="payment-icon">🛡️</view>
<view class="payment-title">实名认证</view>
<view class="payment-desc">完成实名认证,提升信任度,获得更多关注</view>
<view class="payment-benefits">
<view class="benefit-item">
<text class="benefit-icon">✓</text>
<text class="benefit-text">展示"已实名"徽章</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">✓</text>
<text class="benefit-text">提升资料可信度</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">✓</text>
<text class="benefit-text">获得更多用户关注</text>
</view>
</view>
<view class="payment-price">
<text class="price-label">认证费用</text>
<view class="price-value">
<text class="symbol">¥</text>
<text class="amount">{{ verificationFee }}</text>
</view>
</view>
</view>
</view>
<!-- 步骤2: 身份证上传页面 (Requirements 12.2, 12.3) -->
<view v-if="currentStep === 2" class="step-upload">
<view class="upload-content">
<view class="upload-title">请上传身份证照片</view>
<view class="upload-desc">请确保照片清晰、完整,信息可辨认</view>
<!-- 身份证正面 -->
<view class="upload-card">
<view class="card-label">身份证人像面</view>
<view
class="card-upload"
:class="{ 'has-image': idCardFront }"
@click="chooseIdCardFront"
>
<image
v-if="idCardFront"
:src="idCardFront"
mode="aspectFill"
class="card-image"
/>
<view v-else class="card-placeholder">
<text class="placeholder-icon">📷</text>
<text class="placeholder-text">点击上传</text>
</view>
</view>
</view>
<!-- 身份证反面 -->
<view class="upload-card">
<view class="card-label">身份证国徽面</view>
<view
class="card-upload"
:class="{ 'has-image': idCardBack }"
@click="chooseIdCardBack"
>
<image
v-if="idCardBack"
:src="idCardBack"
mode="aspectFill"
class="card-image"
/>
<view v-else class="card-placeholder">
<text class="placeholder-icon">📷</text>
<text class="placeholder-text">点击上传</text>
</view>
</view>
</view>
<view class="upload-tips">
<view class="tips-title">温馨提示:</view>
<view class="tips-item">• 请上传本人有效身份证照片</view>
<view class="tips-item">• 照片需清晰可辨,无遮挡</view>
<view class="tips-item">• 您的信息将被严格保密</view>
</view>
</view>
</view>
<!-- 步骤3: 认证结果页面 (Requirements 12.4) -->
<view v-if="currentStep === 3" class="step-result">
<view class="result-content">
<view class="result-icon success">✓</view>
<view class="result-title">实名认证成功</view>
<view class="result-desc">您的身份信息已通过验证</view>
<view class="result-info">
<view class="info-item">
<text class="label">姓名:</text>
<text class="value">{{ maskedName }}</text>
</view>
<view class="info-item">
<text class="label">身份证号:</text>
<text class="value">{{ maskedIdNumber }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="bottom-action">
<button
v-if="currentStep === 1"
class="btn-pay"
:disabled="paying"
@click="handlePayment"
>
<text v-if="paying">支付中...</text>
<text v-else>立即支付 ¥{{ verificationFee }}</text>
</button>
<button
v-if="currentStep === 2"
class="btn-submit"
:disabled="!canSubmit || submitting"
@click="handleSubmitVerification"
>
<text v-if="submitting">提交中...</text>
<text v-else>提交认证</text>
</button>
<button
v-if="currentStep === 3"
class="btn-done"
@click="handleDone"
>
完成
</button>
</view>
</view>
</view>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { createOrder } from '@/api/order.js'
import { maskName, maskIdNumber } from '@/utils/format.js'
import { get } from '@/api/request.js'
import { getToken } from '@/utils/storage.js'
import Loading from '@/components/Loading/index.vue'
import config from '@/config/index.js'
// 从统一配置获取 API 基础地址
const BASE_URL = config.API_BASE_URL
export default {
name: 'RealnamePage',
components: {
Loading
},
setup() {
const userStore = useUserStore()
// 状态栏高度
const statusBarHeight = ref(20)
// 页面状态
const pageLoading = ref(true)
const paying = ref(false)
const submitting = ref(false)
const currentStep = ref(1) // 1: 支付, 2: 上传, 3: 结果
// 认证费用 (Requirements 12.1)
const verificationFee = ref(88)
// 身份证照片
const idCardFront = ref('')
const idCardBack = ref('')
// 认证结果
const verificationResult = ref(null)
// 获取系统信息
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
}
// 返回上一页
const handleBack = () => {
uni.navigateBack()
}
// 计算属性
const isVerified = computed(() => userStore.isRealName)
const canSubmit = computed(() => {
return idCardFront.value && idCardBack.value
})
const maskedName = computed(() => {
if (verificationResult.value?.name) {
return maskName(verificationResult.value.name)
}
return '***'
})
const maskedIdNumber = computed(() => {
if (verificationResult.value?.idNumber) {
return maskIdNumber(verificationResult.value.idNumber)
}
return '***************'
})
/**
* 获取实名认证状态
*/
const getRealNameStatus = async () => {
try {
const res = await get('/realname/status')
if (res && res.success && res.data) {
if (res.data.isVerified) {
userStore.setRealNameStatus(true)
verificationResult.value = {
name: res.data.name,
idNumber: res.data.idNumber
}
} else if (res.data.isPaid) {
// 已支付但未完成上传
currentStep.value = 2
}
}
} catch (error) {
console.error('获取实名状态失败:', error)
}
}
/**
* 初始化页面
*/
const initPage = async () => {
pageLoading.value = true
try {
getSystemInfo()
userStore.restoreFromStorage()
await getRealNameStatus()
} catch (error) {
console.error('初始化页面失败:', error)
} finally {
pageLoading.value = false
}
}
/**
* 处理支付 (Requirements 12.1, 12.2)
*/
const handlePayment = async () => {
if (paying.value) return
paying.value = true
try {
// 创建实名认证订单
const orderRes = await createOrder({
orderType: 2 // 实名认证订单
})
if (!orderRes || !orderRes.success) {
uni.showToast({
title: orderRes?.message || '创建订单失败',
icon: 'none'
})
return
}
const paymentParams = orderRes.data
// 调用微信支付
await requestPayment(paymentParams)
// 支付成功,进入上传步骤 (Requirements 12.2)
currentStep.value = 2
uni.showToast({
title: '支付成功',
icon: 'success'
})
} catch (error) {
console.error('支付失败:', error)
if (error.errMsg && error.errMsg.includes('cancel')) {
uni.showToast({ title: '支付已取消', icon: 'none' })
} else {
uni.showToast({
title: error.message || '支付失败,请重试',
icon: 'none'
})
}
} finally {
paying.value = false
}
}
/**
* 调用微信支付
*/
const requestPayment = (params) => {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType || 'MD5',
paySign: params.paySign,
success: resolve,
fail: reject
})
})
}
/**
* 选择身份证正面照片
*/
const chooseIdCardFront = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
idCardFront.value = res.tempFilePaths[0]
}
})
}
/**
* 选择身份证反面照片
*/
const chooseIdCardBack = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
idCardBack.value = res.tempFilePaths[0]
}
})
}
/**
* 提交实名认证 (Requirements 12.3)
*/
const handleSubmitVerification = async () => {
if (!canSubmit.value || submitting.value) return
submitting.value = true
try {
// 上传身份证照片并提交认证
const result = await uploadIdCards()
if (result && result.success) {
// 认证成功,更新状态
userStore.setRealNameStatus(true)
verificationResult.value = {
name: result.data.name,
idNumber: result.data.idNumber
}
// 进入结果页面 (Requirements 12.4)
currentStep.value = 3
uni.showToast({
title: '认证成功',
icon: 'success'
})
} else {
uni.showToast({
title: result?.message || '认证失败,请重试',
icon: 'none'
})
}
} catch (error) {
console.error('提交认证失败:', error)
uni.showToast({
title: error.message || '认证失败,请重试',
icon: 'none'
})
} finally {
submitting.value = false
}
}
/**
* 上传身份证照片
*/
const uploadIdCards = () => {
return new Promise((resolve, reject) => {
const token = getToken()
// 先上传正面
uni.uploadFile({
url: `${BASE_URL}/realname/verify`,
filePath: idCardFront.value,
name: 'frontImage',
formData: {
backImagePath: idCardBack.value
},
header: {
'Authorization': `Bearer ${token}`
},
success: async (uploadRes) => {
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data)
// 如果需要单独上传反面,再上传
if (data.needBackImage) {
const backResult = await uploadBackImage(token)
resolve(backResult)
} else {
resolve(data)
}
} catch (e) {
reject(new Error('解析响应失败'))
}
} else if (uploadRes.statusCode === 401) {
reject(new Error('未授权,请重新登录'))
} else {
try {
const errorData = JSON.parse(uploadRes.data)
reject(new Error(errorData.message || '认证失败'))
} catch (e) {
reject(new Error('认证失败'))
}
}
},
fail: (err) => {
reject(new Error('网络连接失败'))
}
})
})
}
/**
* 上传身份证反面
*/
const uploadBackImage = (token) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/realname/verify/back`,
filePath: idCardBack.value,
name: 'backImage',
header: {
'Authorization': `Bearer ${token}`
},
success: (uploadRes) => {
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data)
resolve(data)
} catch (e) {
reject(new Error('解析响应失败'))
}
} else {
reject(new Error('上传失败'))
}
},
fail: () => {
reject(new Error('网络连接失败'))
}
})
})
}
/**
* 完成认证,返回上一页
*/
const handleDone = () => {
uni.navigateBack()
}
onMounted(() => {
initPage()
})
return {
statusBarHeight,
pageLoading,
paying,
submitting,
currentStep,
verificationFee,
idCardFront,
idCardBack,
isVerified,
canSubmit,
maskedName,
maskedIdNumber,
handleBack,
handlePayment,
chooseIdCardFront,
chooseIdCardBack,
handleSubmitVerification,
handleDone
}
},
onShow() {
const userStore = useUserStore()
userStore.restoreFromStorage()
}
}
</script>
<style lang="scss" scoped>
.realname-page {
height: 100vh;
background-color: #f8f8f8;
}
// 顶部背景图
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 0;
.bg-img {
width: 100%;
height: 100%;
}
}
// 自定义导航栏
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
.navbar-content {
position: relative;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 56rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// 已认证状态
.verified-section {
padding: 60rpx 30rpx;
padding-top: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
.verified-icon {
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
color: #fff;
margin-bottom: 30rpx;
}
.verified-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 40rpx;
}
.verified-info {
width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.info-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #666;
width: 160rpx;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
.verified-badge {
display: flex;
align-items: center;
padding: 16rpx 32rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
border-radius: 40rpx;
.badge-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.badge-text {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
}
// 未认证状态
.unverified-section {
height: 100vh;
position: relative;
z-index: 1;
overflow: hidden;
}
// 可滚动内容区域
.content-scroll {
position: fixed;
left: 0;
right: 0;
background: transparent;
}
// 步骤内容区域
.step-payment,
.step-upload,
.step-result {
padding: 0;
}
// 固定的步骤指示器
.step-header-fixed {
position: fixed;
left: 0;
right: 0;
z-index: 99;
padding: 20rpx 30rpx;
background: transparent;
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.step-item {
display: flex;
flex-direction: column;
align-items: center;
.step-num {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #e0e0e0;
color: #999;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.step-text {
font-size: 22rpx;
color: #999;
}
&.active {
.step-num {
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
color: #fff;
}
.step-text {
color: #ff6b6b;
font-weight: 500;
}
}
&.completed {
.step-num {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: #fff;
}
.step-text {
color: #52c41a;
}
}
}
.step-line {
width: 80rpx;
height: 4rpx;
background: #e0e0e0;
margin: 0 16rpx;
margin-bottom: 30rpx;
&.completed {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
}
}
}
// 步骤指示器(保留旧样式兼容)
.step-header {
padding: 30rpx;
padding-top: 20rpx;
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.step-item {
display: flex;
flex-direction: column;
align-items: center;
.step-num {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #e0e0e0;
color: #999;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.step-text {
font-size: 22rpx;
color: #999;
}
&.active {
.step-num {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: #fff;
}
.step-text {
color: #52c41a;
}
}
}
.step-line {
width: 80rpx;
height: 4rpx;
background: #e0e0e0;
margin: 0 16rpx;
margin-bottom: 30rpx;
&.completed {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
}
}
}
// 步骤1: 支付页面
.step-payment {
.payment-content {
padding: 40rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
.payment-icon {
font-size: 100rpx;
margin-bottom: 24rpx;
}
.payment-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.payment-desc {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
}
.payment-benefits {
width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 40rpx;
.benefit-item {
display: flex;
align-items: center;
padding: 16rpx 0;
.benefit-icon {
width: 40rpx;
height: 40rpx;
background: #52c41a;
border-radius: 50%;
color: #fff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.benefit-text {
font-size: 28rpx;
color: #333;
}
}
}
.payment-price {
display: flex;
flex-direction: column;
align-items: center;
.price-label {
font-size: 26rpx;
color: #999;
margin-bottom: 12rpx;
}
.price-value {
display: flex;
align-items: baseline;
.symbol {
font-size: 32rpx;
color: #ff6b6b;
font-weight: 500;
}
.amount {
font-size: 72rpx;
color: #ff6b6b;
font-weight: 700;
}
}
}
}
}
// 步骤2: 上传页面
.step-upload {
.upload-content {
padding: 30rpx;
.upload-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 12rpx;
}
.upload-desc {
font-size: 26rpx;
color: #999;
text-align: center;
margin-bottom: 40rpx;
}
.upload-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
.card-label {
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.card-upload {
width: 100%;
height: 320rpx;
background: #f8f8f8;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
&.has-image {
border-style: solid;
border-color: #52c41a;
}
.card-image {
width: 100%;
height: 100%;
}
.card-placeholder {
display: flex;
flex-direction: column;
align-items: center;
.placeholder-icon {
font-size: 60rpx;
margin-bottom: 16rpx;
}
.placeholder-text {
font-size: 26rpx;
color: #999;
}
}
}
}
.upload-tips {
background: #fffbe6;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 20rpx;
.tips-title {
font-size: 26rpx;
font-weight: 500;
color: #d48806;
margin-bottom: 12rpx;
}
.tips-item {
font-size: 24rpx;
color: #d48806;
line-height: 1.8;
}
}
}
}
// 步骤3: 结果页面
.step-result {
.result-content {
padding: 60rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
.result-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
color: #fff;
margin-bottom: 30rpx;
&.success {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
}
.result-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.result-desc {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
}
.result-info {
width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
.info-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #666;
width: 160rpx;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
}
}
// 底部操作按钮
.bottom-action {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
.btn-pay,
.btn-submit,
.btn-done {
width: 100%;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 48rpx;
border: none;
&::after {
border: none;
}
text {
font-size: 32rpx;
font-weight: 600;
}
&[disabled] {
opacity: 0.6;
}
}
.btn-pay {
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
text {
color: #fff;
}
}
.btn-submit {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
text {
color: #fff;
}
}
.btn-done {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
text {
color: #fff;
}
}
}
</style>