|
|
@@ -0,0 +1,869 @@
|
|
|
+<template>
|
|
|
+ <view class="identity-auth">
|
|
|
+ <scroll-view scroll-y class="identity-auth__scroll">
|
|
|
+ <view class="identity-auth__hero">
|
|
|
+ <view class="identity-auth__hero-main">
|
|
|
+ <text class="identity-auth__hero-title">身份认证</text>
|
|
|
+ <text class="identity-auth__hero-desc">
|
|
|
+ 请上传身份证人像面与国徽面,系统会自动识别证件信息
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="identity-auth__hero-status">
|
|
|
+ {{ ocrFinished ? '已识别' : '待识别' }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="section-card">
|
|
|
+ <view class="section-header">
|
|
|
+ <view>
|
|
|
+ <text class="section-title">身份证照片</text>
|
|
|
+ <text class="section-subtitle">支持相册选择或拍照上传</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <text v-if="ocrLoading" class="section-status">OCR识别中</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="upload-grid">
|
|
|
+ <view
|
|
|
+ v-for="item in idCardUploadCards"
|
|
|
+ :key="item.side"
|
|
|
+ class="upload-card"
|
|
|
+ :class="{ 'has-image': item.url }"
|
|
|
+ @click="handleChooseImage(item.side)"
|
|
|
+ >
|
|
|
+ <image v-if="item.url" class="upload-card__image" :src="item.url" mode="aspectFill" />
|
|
|
+
|
|
|
+ <view v-else class="upload-card__empty">
|
|
|
+ <view class="upload-card__icon">+</view>
|
|
|
+ <text class="upload-card__title">点击上传</text>
|
|
|
+ <text class="upload-card__desc">{{ item.title }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ v-if="item.url && !item.loading"
|
|
|
+ class="upload-card__delete"
|
|
|
+ @click.stop="handleDeleteImage(item.side)"
|
|
|
+ >
|
|
|
+ ×
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="item.loading" class="upload-card__mask">
|
|
|
+ <text class="upload-card__mask-text">上传中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="upload-tip">
|
|
|
+ <text class="upload-tip__dot" />
|
|
|
+ <text class="upload-tip__text">
|
|
|
+ 请确保照片完整、清晰、无反光。两张照片上传成功后会自动进行 OCR 识别。
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="showOcrInfo" class="section-card">
|
|
|
+ <view class="section-header">
|
|
|
+ <view>
|
|
|
+ <text class="section-title">识别信息</text>
|
|
|
+ <text class="section-subtitle">请核对识别结果是否与身份证一致</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <button class="text-btn" :disabled="isPageBusy" @click="handleOcrRecognize">
|
|
|
+ 重新识别
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="ocr-list">
|
|
|
+ <view v-for="item in ocrFieldList" :key="item.key" class="ocr-row">
|
|
|
+ <text class="ocr-row__label">{{ item.label }}</text>
|
|
|
+ <text class="ocr-row__value">{{ formData[item.key] || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <view class="bottom-bar">
|
|
|
+ <button class="bottom-btn bottom-btn--secondary" :disabled="isPageBusy" @click="handleBack">
|
|
|
+ 返回
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button class="bottom-btn bottom-btn--primary" :disabled="!canSubmit" @click="handleSubmit">
|
|
|
+ {{ submitButtonText }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <BaseAuthDialog
|
|
|
+ v-model="phoneAuthFailVisible"
|
|
|
+ title="手机三要素认证未通过"
|
|
|
+ confirm-text="我知道啦"
|
|
|
+ @confirm="handleAuthFailConfirm"
|
|
|
+ >
|
|
|
+ <view class="phone-auth-desc"> 抱歉,您的登录手机号三要素认证未通过! </view>
|
|
|
+
|
|
|
+ <view class="phone-auth-card-text">
|
|
|
+ 请确保您的手机号持卡人身份证号与您提供的身份证号一致:
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="phone-auth-card-tip">
|
|
|
+ 您可以前往相应手机号所属运营商营业厅办理
|
|
|
+ <text class="phone-auth-highlight">手机号过户</text>
|
|
|
+ 业务。
|
|
|
+ </view>
|
|
|
+ </BaseAuthDialog>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+import { onLoad } from '@dcloudio/uni-app'
|
|
|
+
|
|
|
+import {
|
|
|
+ gigCertOcrApi,
|
|
|
+ gigCertPersonVerifyFaceApi,
|
|
|
+ gigCertPersonVerifyMobile3Api,
|
|
|
+} from '@/services/modules/auth'
|
|
|
+import type { GigCertOcrResponse } from '@/services/modules/auth/type'
|
|
|
+
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
+
|
|
|
+import BaseAuthDialog from '@/components/BaseAuthDialog/index.vue'
|
|
|
+
|
|
|
+const API_SUCCESS_CODE = 0
|
|
|
+const FACE_AUTH_URL = '/pages-auth/face/index'
|
|
|
+const ID_CARD_UPLOAD_URL = `${String(import.meta.env.VITE_BASE_API ?? '').replace(/\/$/, '')}/gig/cert/id-card-image-upload`
|
|
|
+
|
|
|
+type IdCardSide = 'front' | 'back'
|
|
|
+
|
|
|
+interface OcrFormData {
|
|
|
+ realName: string
|
|
|
+ sex: string
|
|
|
+ nation: string
|
|
|
+ born: string
|
|
|
+ address: string
|
|
|
+ idCard: string
|
|
|
+ begin: string
|
|
|
+ end: string
|
|
|
+ department: string
|
|
|
+}
|
|
|
+
|
|
|
+type OcrFieldKey = keyof OcrFormData
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+const currentUserInfo = computed(() => userStore.currentUserInfo)
|
|
|
+
|
|
|
+const phoneAuthFailVisible = ref(false)
|
|
|
+const uploading = ref(false)
|
|
|
+const ocrLoading = ref(false)
|
|
|
+const ocrFinished = ref(false)
|
|
|
+const submitLoading = ref(false)
|
|
|
+
|
|
|
+const uploadStatus = reactive<Record<IdCardSide, boolean>>({
|
|
|
+ front: false,
|
|
|
+ back: false,
|
|
|
+})
|
|
|
+
|
|
|
+const idCardImages = reactive<Record<IdCardSide, string>>({
|
|
|
+ front: '',
|
|
|
+ back: '',
|
|
|
+})
|
|
|
+
|
|
|
+const formData = reactive<OcrFormData>({
|
|
|
+ realName: '',
|
|
|
+ sex: '',
|
|
|
+ nation: '',
|
|
|
+ born: '',
|
|
|
+ address: '',
|
|
|
+ idCard: '',
|
|
|
+ begin: '',
|
|
|
+ end: '',
|
|
|
+ department: '',
|
|
|
+})
|
|
|
+
|
|
|
+const ocrFieldList: Array<{ key: OcrFieldKey; label: string }> = [
|
|
|
+ { key: 'realName', label: '姓名' },
|
|
|
+ { key: 'sex', label: '性别' },
|
|
|
+ { key: 'nation', label: '民族' },
|
|
|
+ { key: 'born', label: '生日' },
|
|
|
+ { key: 'address', label: '籍贯' },
|
|
|
+ { key: 'idCard', label: '证件号' },
|
|
|
+ { key: 'begin', label: '生效日期' },
|
|
|
+ { key: 'end', label: '截止日期' },
|
|
|
+ { key: 'department', label: '发放机关' },
|
|
|
+]
|
|
|
+
|
|
|
+const mobile = computed(() => currentUserInfo.value?.phone ?? '')
|
|
|
+
|
|
|
+const hasBothImages = computed(() => Boolean(idCardImages.front && idCardImages.back))
|
|
|
+const showOcrInfo = computed(() => ocrFinished.value)
|
|
|
+const isPageBusy = computed(() => uploading.value || ocrLoading.value || submitLoading.value)
|
|
|
+
|
|
|
+const canSubmit = computed(() => {
|
|
|
+ return Boolean(
|
|
|
+ formData.realName &&
|
|
|
+ formData.idCard &&
|
|
|
+ !uploading.value &&
|
|
|
+ !ocrLoading.value &&
|
|
|
+ !submitLoading.value
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const submitButtonText = computed(() => {
|
|
|
+ if (ocrLoading.value) return '识别中...'
|
|
|
+ if (uploading.value) return '上传中...'
|
|
|
+ if (submitLoading.value) return '认证中...'
|
|
|
+ return '去认证'
|
|
|
+})
|
|
|
+
|
|
|
+const idCardUploadCards = computed(() => [
|
|
|
+ {
|
|
|
+ side: 'front' as const,
|
|
|
+ title: '身份证人像面',
|
|
|
+ url: idCardImages.front,
|
|
|
+ loading: uploadStatus.front,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ side: 'back' as const,
|
|
|
+ title: '身份证国徽面',
|
|
|
+ url: idCardImages.back,
|
|
|
+ loading: uploadStatus.back,
|
|
|
+ },
|
|
|
+])
|
|
|
+
|
|
|
+const clearPageData = () => {
|
|
|
+ idCardImages.front = ''
|
|
|
+ idCardImages.back = ''
|
|
|
+
|
|
|
+ uploadStatus.front = false
|
|
|
+ uploadStatus.back = false
|
|
|
+
|
|
|
+ uploading.value = false
|
|
|
+ ocrLoading.value = false
|
|
|
+ ocrFinished.value = false
|
|
|
+ submitLoading.value = false
|
|
|
+ phoneAuthFailVisible.value = false
|
|
|
+
|
|
|
+ clearOcrFormData()
|
|
|
+}
|
|
|
+
|
|
|
+const clearOcrFormData = () => {
|
|
|
+ Object.keys(formData).forEach((key) => {
|
|
|
+ formData[key as OcrFieldKey] = ''
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const resetOcrResult = () => {
|
|
|
+ ocrFinished.value = false
|
|
|
+ clearOcrFormData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleChooseImage = (side: IdCardSide) => {
|
|
|
+ if (isPageBusy.value) return
|
|
|
+
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1,
|
|
|
+ sizeType: ['compressed'],
|
|
|
+ sourceType: ['album', 'camera'],
|
|
|
+ success: async (res) => {
|
|
|
+ const tempFilePath = res.tempFilePaths?.[0]
|
|
|
+
|
|
|
+ if (!tempFilePath) return
|
|
|
+
|
|
|
+ resetOcrResult()
|
|
|
+ idCardImages[side] = tempFilePath
|
|
|
+
|
|
|
+ let shouldRecognize = false
|
|
|
+
|
|
|
+ try {
|
|
|
+ uploading.value = true
|
|
|
+ uploadStatus[side] = true
|
|
|
+
|
|
|
+ idCardImages[side] = await uploadIdCardImage(tempFilePath)
|
|
|
+ shouldRecognize = hasBothImages.value
|
|
|
+ } catch (error) {
|
|
|
+ idCardImages[side] = ''
|
|
|
+ showToast(getErrorMessage(error, '上传失败,请重试'))
|
|
|
+ } finally {
|
|
|
+ uploading.value = false
|
|
|
+ uploadStatus[side] = false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (shouldRecognize) {
|
|
|
+ await handleOcrRecognize()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleDeleteImage = (side: IdCardSide) => {
|
|
|
+ if (isPageBusy.value) return
|
|
|
+
|
|
|
+ idCardImages[side] = ''
|
|
|
+ uploadStatus[side] = false
|
|
|
+
|
|
|
+ resetOcrResult()
|
|
|
+}
|
|
|
+
|
|
|
+const uploadIdCardImage = (filePath: string): Promise<string> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.uploadFile({
|
|
|
+ url: ID_CARD_UPLOAD_URL,
|
|
|
+ filePath,
|
|
|
+ name: 'file',
|
|
|
+ header: {
|
|
|
+ Authorization: getAuthorization(),
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ try {
|
|
|
+ const result = JSON.parse(String(res.data)) as {
|
|
|
+ code: number | string
|
|
|
+ msg?: string
|
|
|
+ message?: string
|
|
|
+ data?: string
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Number(result.code) !== API_SUCCESS_CODE) {
|
|
|
+ reject(new Error(result.msg || result.message || '身份证图片上传失败'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!result.data) {
|
|
|
+ reject(new Error('身份证图片上传接口未返回图片地址'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve(result.data)
|
|
|
+ } catch {
|
|
|
+ reject(new Error('身份证图片上传响应解析失败'))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ reject(new Error('身份证图片上传网络异常'))
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleOcrRecognize = async () => {
|
|
|
+ if (!hasBothImages.value || ocrLoading.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ ocrLoading.value = true
|
|
|
+ ocrFinished.value = false
|
|
|
+
|
|
|
+ const res = await gigCertOcrApi({
|
|
|
+ frontUrl: idCardImages.front,
|
|
|
+ backUrl: idCardImages.back,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (Number(res.code) !== API_SUCCESS_CODE) {
|
|
|
+ throw new Error(res.msg || 'OCR识别失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ clearOcrFormData()
|
|
|
+ setOcrFormData(res.data)
|
|
|
+ ocrFinished.value = true
|
|
|
+ } catch (error) {
|
|
|
+ ocrFinished.value = false
|
|
|
+ clearOcrFormData()
|
|
|
+ showToast(getErrorMessage(error, 'OCR识别失败'))
|
|
|
+ } finally {
|
|
|
+ ocrLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const setOcrFormData = (data: Partial<GigCertOcrResponse> = {}) => {
|
|
|
+ formData.realName = formatOcrValue(data.realName)
|
|
|
+ formData.sex = formatOcrValue(data.sex)
|
|
|
+ formData.nation = formatOcrValue(data.nation)
|
|
|
+ formData.born = formatDate(data.born)
|
|
|
+ formData.address = formatOcrValue(data.address)
|
|
|
+ formData.idCard = formatOcrValue(data.idCard)
|
|
|
+ formData.begin = formatDate(data.begin)
|
|
|
+ formData.end = formatDate(data.end)
|
|
|
+ formData.department = formatOcrValue(data.department)
|
|
|
+}
|
|
|
+
|
|
|
+const formatOcrValue = (value: unknown) => {
|
|
|
+ return value === undefined || value === null ? '' : String(value)
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (value: unknown) => {
|
|
|
+ if (!value) return ''
|
|
|
+ return String(value).split(' ')[0]
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (!canSubmit.value) return
|
|
|
+
|
|
|
+ if (!mobile.value) {
|
|
|
+ showToast('未获取到登录手机号')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ submitLoading.value = true
|
|
|
+
|
|
|
+ const mobile3Res = await gigCertPersonVerifyMobile3Api({
|
|
|
+ mobile: mobile.value,
|
|
|
+ realName: formData.realName,
|
|
|
+ cardId: formData.idCard,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (Number(mobile3Res.code) !== API_SUCCESS_CODE) {
|
|
|
+ phoneAuthFailVisible.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const faceRes = await gigCertPersonVerifyFaceApi({
|
|
|
+ cardId: formData.idCard,
|
|
|
+ realName: formData.realName,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (Number(faceRes.code) !== API_SUCCESS_CODE) {
|
|
|
+ throw new Error(faceRes.msg || '人脸认证初始化失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ const faceData = faceRes.data
|
|
|
+
|
|
|
+ if (!faceData?.serialNo || !faceData.faceUrl) {
|
|
|
+ throw new Error('人脸认证接口返回数据不完整')
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.navigateTo({
|
|
|
+ url: buildFaceAuthUrl({
|
|
|
+ serialNo: faceData.serialNo,
|
|
|
+ faceUrl: faceData.faceUrl,
|
|
|
+ realName: formData.realName,
|
|
|
+ cardId: formData.idCard,
|
|
|
+ }),
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ showToast(getErrorMessage(error, '认证失败,请重试'))
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const buildFaceAuthUrl = (params: {
|
|
|
+ serialNo: string
|
|
|
+ faceUrl: string
|
|
|
+ realName: string
|
|
|
+ cardId: string
|
|
|
+}) => {
|
|
|
+ const query = Object.entries(params)
|
|
|
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
|
+ .join('&')
|
|
|
+
|
|
|
+ return `${FACE_AUTH_URL}?${query}`
|
|
|
+}
|
|
|
+
|
|
|
+const handleAuthFailConfirm = () => {
|
|
|
+ phoneAuthFailVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleBack = () => {
|
|
|
+ if (isPageBusy.value) return
|
|
|
+ uni.navigateBack()
|
|
|
+}
|
|
|
+
|
|
|
+const getAuthorization = () => {
|
|
|
+ const token = userStore.accessToken || ''
|
|
|
+
|
|
|
+ if (!token) return ''
|
|
|
+
|
|
|
+ return token.startsWith('Bearer ') ? token : `Bearer ${token}`
|
|
|
+}
|
|
|
+
|
|
|
+const getErrorMessage = (error: unknown, fallback: string) => {
|
|
|
+ if (error instanceof Error && error.message) {
|
|
|
+ return error.message
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof error === 'object' && error !== null) {
|
|
|
+ const target = error as {
|
|
|
+ msg?: string
|
|
|
+ message?: string
|
|
|
+ }
|
|
|
+
|
|
|
+ return target.msg || target.message || fallback
|
|
|
+ }
|
|
|
+
|
|
|
+ return fallback
|
|
|
+}
|
|
|
+
|
|
|
+const showToast = (title: string) => {
|
|
|
+ uni.showToast({
|
|
|
+ title,
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+onLoad(() => {
|
|
|
+ clearPageData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.identity-auth {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #eef6ff 0%, #f7f8fa 420rpx), #f7f8fa;
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__scroll {
|
|
|
+ height: 100vh;
|
|
|
+ padding: 28rpx 28rpx 180rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__hero {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ padding: 34rpx 32rpx;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ background: linear-gradient(135deg, #1677ff 0%, #69b1ff 100%);
|
|
|
+ box-shadow: 0 18rpx 40rpx rgba(22, 119, 255, 0.18);
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__hero-main {
|
|
|
+ flex: 1;
|
|
|
+ padding-right: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__hero-title {
|
|
|
+ display: block;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 56rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__hero-desc {
|
|
|
+ display: block;
|
|
|
+ margin-top: 12rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.88);
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 38rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.identity-auth__hero-status {
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 48rpx;
|
|
|
+ padding: 0 18rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.18);
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.section-card {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ padding: 30rpx;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 10rpx 32rpx rgba(15, 23, 42, 0.06);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: block;
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 44rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.section-subtitle {
|
|
|
+ display: block;
|
|
|
+ margin-top: 6rpx;
|
|
|
+ color: #8a94a6;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 34rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.section-status {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 16rpx;
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: #eef6ff;
|
|
|
+ color: #1677ff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.text-btn {
|
|
|
+ height: 52rpx;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: #eef6ff;
|
|
|
+ color: #1677ff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 52rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.text-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.text-btn[disabled] {
|
|
|
+ opacity: 0.45;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-grid {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card {
|
|
|
+ position: relative;
|
|
|
+ flex: 1;
|
|
|
+ height: 238rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 2rpx dashed #cfd8e3;
|
|
|
+ border-radius: 22rpx;
|
|
|
+ background: #f8fafc;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card + .upload-card {
|
|
|
+ margin-left: 22rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card.has-image {
|
|
|
+ border-style: solid;
|
|
|
+ border-color: #e5e7eb;
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__empty {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__icon {
|
|
|
+ width: 54rpx;
|
|
|
+ height: 54rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #e8f3ff;
|
|
|
+ color: #1677ff;
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: 300;
|
|
|
+ line-height: 50rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__title {
|
|
|
+ color: #374151;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__desc {
|
|
|
+ margin-top: 4rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 34rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__delete {
|
|
|
+ position: absolute;
|
|
|
+ top: 14rpx;
|
|
|
+ right: 14rpx;
|
|
|
+ z-index: 3;
|
|
|
+ width: 46rpx;
|
|
|
+ height: 46rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(15, 23, 42, 0.62);
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 38rpx;
|
|
|
+ line-height: 42rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__mask {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 4;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(15, 23, 42, 0.56);
|
|
|
+}
|
|
|
+
|
|
|
+.upload-card__mask-text {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-top: 24rpx;
|
|
|
+ padding: 20rpx 22rpx;
|
|
|
+ border-radius: 18rpx;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip__dot {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 10rpx;
|
|
|
+ height: 10rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #1677ff;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip__text {
|
|
|
+ flex: 1;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 36rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.ocr-list {
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1rpx solid #eef0f3;
|
|
|
+ border-radius: 22rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.ocr-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ min-height: 88rpx;
|
|
|
+ padding: 24rpx 26rpx;
|
|
|
+ border-bottom: 1rpx solid #eef0f3;
|
|
|
+ background: #ffffff;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.ocr-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.ocr-row__label {
|
|
|
+ width: 150rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.ocr-row__value {
|
|
|
+ flex: 1;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+ text-align: right;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-bar {
|
|
|
+ position: fixed;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 20;
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+ padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
|
+ border-top: 1rpx solid rgba(229, 231, 235, 0.8);
|
|
|
+ background: rgba(255, 255, 255, 0.96);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 88rpx;
|
|
|
+ margin: 0;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 88rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--secondary {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #4b5563;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--primary {
|
|
|
+ background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
|
|
+ color: #ffffff;
|
|
|
+ box-shadow: 0 12rpx 28rpx rgba(22, 119, 255, 0.22);
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn[disabled] {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--secondary[disabled] {
|
|
|
+ background: #f3f4f6 !important;
|
|
|
+ color: #a1a1aa !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--primary[disabled] {
|
|
|
+ background: #d1d5db !important;
|
|
|
+ color: #ffffff !important;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-auth-desc {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 29rpx;
|
|
|
+ line-height: 44rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-auth-card-text {
|
|
|
+ margin-top: 28rpx;
|
|
|
+ padding: 28rpx 28rpx 0;
|
|
|
+ border-radius: 22rpx 22rpx 0 0;
|
|
|
+ background: #f8fafc;
|
|
|
+ color: #4b5563;
|
|
|
+ font-size: 29rpx;
|
|
|
+ line-height: 46rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-auth-card-tip {
|
|
|
+ padding: 20rpx 28rpx 30rpx;
|
|
|
+ border-radius: 0 0 22rpx 22rpx;
|
|
|
+ background: #f8fafc;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 29rpx;
|
|
|
+ line-height: 46rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-auth-highlight {
|
|
|
+ color: #1677ff;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+</style>
|