身份证核验

This commit is contained in:
weichengwu 2025-11-26 23:27:25 +08:00
parent faba34294a
commit 379fcd1bfa
4 changed files with 706 additions and 181 deletions

View File

@ -75,23 +75,35 @@
<span class="inline-block w-25 text-right">结婚证字号</span>
<span class="pl-2">{{ formData.marriageNo }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">男方姓名</span>
<span class="pl-2">{{ formData.husbandName }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">女方姓名</span>
<span class="pl-2">{{ formData.wifeName }}</span>
</div>
</div>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">男方姓名</span>
<span class="pl-2">{{ formData.husbandName }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">男方身份证</span>
<span class="pl-2 break-all">{{ formData.husbandId || '-' }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">女方姓名</span>
<span class="pl-2">{{ formData.wifeName }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">女方身份证</span>
<span class="pl-2 break-all">{{ formData.wifeId || '-' }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">登记日期</span>
<span class="pl-2">{{ formData.registerDate || '-' }}</span>
</div>
</div>
</div>
<!-- 提交按钮 -->
<button @click="submitForm" :disabled="!formData.marriageNo"
class="w-full h-14 bg-gradient-to-r from-[#E8424D]! to-[#FF7A7A]! text-white! rounded-full text-lg font-bold transition-all hover:opacity-90 active:scale-98 disabled:from-[#8a8a8a]! disabled:to-[#8a8a8a]!">
<div class="text-lg">提交领取喜礼</div>
</button>
</div>
<!-- 提交按钮 -->
<button @click="goToIdUpload" :disabled="!formData.marriageNo"
class="w-full h-14 bg-gradient-to-r from-[#E8424D]! to-[#FF7A7A]! text-white! rounded-full text-lg font-bold transition-all hover:opacity-90 active:scale-98 disabled:from-[#8a8a8a]! disabled:to-[#8a8a8a]!">
<div class="text-lg">下一步</div>
</button>
</div>
<div v-if="formData.qrCode">
<div>
@ -202,18 +214,41 @@
</div>
</template>
<script setup lang="ts">
import apiService from '../../services/apiService'
import { ref, onMounted, nextTick } from 'vue'
import dayjs from 'dayjs'
import QrcodeVue from 'qrcode.vue'
import { showLoading, hideLoading } from '../../components/loading'
import { showDialog } from 'vant';
import 'vant/es/dialog/style';
const ocrUploadId = ref<HTMLInputElement>();
const qrCodeWrapper = ref<HTMLDivElement | null>(null);
const qrImageSrc = ref('');
<script setup lang="ts">
import apiService from '../../services/apiService'
import { ref, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import dayjs from 'dayjs'
import QrcodeVue from 'qrcode.vue'
import { showLoading, hideLoading } from '../../components/loading'
import { showDialog } from 'vant';
import 'vant/es/dialog/style';
const ocrUploadId = ref<HTMLInputElement>();
const qrCodeWrapper = ref<HTMLDivElement | null>(null);
const qrImageSrc = ref('');
const router = useRouter();
const STORAGE_KEY_USER = 'userAs';
const STORAGE_KEY_MARRIAGE = 'marriageOcr';
type MarriageStorage = {
marriageNo?: string
husbandName?: string
wifeName?: string
registerDate?: string
certificateHolder?: string
wifeId?: string
husbandId?: string
wifeGender?: string | null
husbandGender?: string | null
wifeBirthDate?: string
husbandBirthDate?: string
wifeNationality?: string
husbandNationality?: string
phone: string
smsCode: string
}
const syncQrImage = () => {
nextTick(() => {
@ -236,13 +271,22 @@ const formData = ref({
phone: '',
smsCode: '',
verifyCode: false, //
marriageNo: '', //
husbandName: '', //
wifeName: '', //
registerDate: '', //
qrCode: '', //
status: 0, // 0- 1-
})
marriageNo: '', //
husbandName: '', //
wifeName: '', //
registerDate: '', //
certificateHolder: '',
husbandId: '',
wifeId: '',
husbandGender: '',
wifeGender: '',
husbandBirthDate: '',
wifeBirthDate: '',
husbandNationality: '',
wifeNationality: '',
qrCode: '', //
status: 0, // 0- 1-
})
const activityInfo = ref<any>({}); //
onMounted(() => {
@ -265,12 +309,12 @@ onMounted(() => {
});
activityInfo.value.status = 2;
} else {
const user = JSON.parse(localStorage.getItem('userAs') || '{}');
if (user.phone && user.smsCode) {
formData.value.phone = user.phone;
formData.value.smsCode = user.smsCode;
verifyCode(false);
}
const user = JSON.parse(localStorage.getItem(STORAGE_KEY_USER) || '{}');
if (user.phone && user.smsCode) {
formData.value.phone = user.phone;
formData.value.smsCode = user.smsCode;
verifyCode(false);
}
}
} else {
activityInfo.value.status = 3;
@ -289,28 +333,38 @@ onMounted(() => {
const countdown = ref(0)
let countdownTimer: number | null = null
const clearPhone = () => {
formData.value.phone = '';
formData.value.smsCode = '';
formData.value.verifyCode = false;
formData.value.marriageNo = '';
formData.value.husbandName = '';
formData.value.wifeName = '';
formData.value.registerDate = '';
formData.value.qrCode = '';
formData.value.status = 0;
qrImageSrc.value = '';
countdown.value = 0;
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
localStorage.removeItem('userAs');
if (ocrUploadId.value) ocrUploadId.value.value = '';
}
//
const sendVerificationCode = () => {
const clearPhone = () => {
formData.value.phone = '';
formData.value.smsCode = '';
formData.value.verifyCode = false;
formData.value.marriageNo = '';
formData.value.husbandName = '';
formData.value.wifeName = '';
formData.value.registerDate = '';
formData.value.certificateHolder = '';
formData.value.husbandId = '';
formData.value.wifeId = '';
formData.value.husbandGender = '';
formData.value.wifeGender = '';
formData.value.husbandBirthDate = '';
formData.value.wifeBirthDate = '';
formData.value.husbandNationality = '';
formData.value.wifeNationality = '';
formData.value.qrCode = '';
formData.value.status = 0;
qrImageSrc.value = '';
countdown.value = 0;
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
localStorage.removeItem(STORAGE_KEY_USER);
localStorage.removeItem(STORAGE_KEY_MARRIAGE);
if (ocrUploadId.value) ocrUploadId.value.value = '';
}
//
const sendVerificationCode = () => {
const { phone } = formData.value
//
@ -321,16 +375,17 @@ const sendVerificationCode = () => {
return
}
showLoading();
//
apiService.sendSms({
mobile: phone,
type: 3,
}).then(() => {
formData.value.verifyCode = false;
formData.value.smsCode = '';
formData.value.qrCode = '';
//
//
apiService.sendSms({
mobile: phone,
type: 3,
}).then(() => {
localStorage.removeItem(STORAGE_KEY_MARRIAGE);
formData.value.verifyCode = false;
formData.value.smsCode = '';
formData.value.qrCode = '';
//
countdown.value = 60
if (countdownTimer) {
@ -369,27 +424,27 @@ const verifyCode = (flag: any) => {
showLoading();
//
apiService.verifySms({
mobile: formData.value.phone,
smsCode: smsCode,
type: 3,
}).then((res) => {
localStorage.setItem('userAs', JSON.stringify({
phone: formData.value.phone,
smsCode: formData.value.smsCode,
}));
formData.value.verifyCode = true;
apiService.verifySms({
mobile: formData.value.phone,
smsCode: smsCode,
type: 3,
}).then((res) => {
localStorage.setItem(STORAGE_KEY_USER, JSON.stringify({
phone: formData.value.phone,
smsCode: formData.value.smsCode,
}));
formData.value.verifyCode = true;
//
countdown.value = 0;
//
if (res?.data?.code) {
formData.value = {
...formData.value,
qrCode: res.data.code,
status: res.data.status,
if (res?.data?.code) {
formData.value = {
...formData.value,
qrCode: res.data.code,
status: res.data.status,
}
syncQrImage();
} else {
@ -399,43 +454,123 @@ const verifyCode = (flag: any) => {
husbandName: "",
wifeName: "",
registerDate: "",
qrCode: ""
}
}
}).catch((error) => {
localStorage.removeItem('userAs');
flag && showDialog({
message: error.msg || '验证码验证失败,请重试',
});
formData.value = {
...formData.value,
smsCode: "",
marriageNo: "",
husbandName: "",
wifeName: "",
registerDate: "",
qrCode: ""
}
qrImageSrc.value = '';
}).finally(() => {
hideLoading();
})
}
//
function resetMarriageInfo() {
formData.value = {
...formData.value,
marriageNo: "",
husbandName: "",
wifeName: "",
registerDate: "",
qrCode: ""
}
ocrUploadId.value && (ocrUploadId.value.value = '');
ocrUploadId.value?.click();
}
qrCode: ""
}
}
restoreMarriageInfo();
}).catch((error) => {
localStorage.removeItem(STORAGE_KEY_USER);
localStorage.removeItem(STORAGE_KEY_MARRIAGE);
flag && showDialog({
message: error.msg || '验证码验证失败,请重试',
});
formData.value = {
...formData.value,
smsCode: "",
marriageNo: "",
husbandName: "",
wifeName: "",
registerDate: "",
certificateHolder: "",
husbandId: "",
wifeId: "",
husbandGender: "",
wifeGender: "",
husbandBirthDate: "",
wifeBirthDate: "",
husbandNationality: "",
wifeNationality: "",
qrCode: ""
}
qrImageSrc.value = '';
}).finally(() => {
hideLoading();
})
}
const restoreMarriageInfo = () => {
const user = JSON.parse(localStorage.getItem(STORAGE_KEY_USER) || '{}');
const marriageStorage: MarriageStorage | null = (() => {
const raw = localStorage.getItem(STORAGE_KEY_MARRIAGE);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
})();
if (!marriageStorage || marriageStorage.smsCode !== user.smsCode) {
return;
}
formData.value = {
...formData.value,
marriageNo: marriageStorage.marriageNo || '',
husbandName: marriageStorage.husbandName || '',
wifeName: marriageStorage.wifeName || '',
registerDate: marriageStorage.registerDate || '',
certificateHolder: marriageStorage.certificateHolder || '',
husbandId: marriageStorage.husbandId || '',
wifeId: marriageStorage.wifeId || '',
husbandGender: marriageStorage.husbandGender || '',
wifeGender: marriageStorage.wifeGender || '',
husbandBirthDate: marriageStorage.husbandBirthDate || '',
wifeBirthDate: marriageStorage.wifeBirthDate || '',
husbandNationality: marriageStorage.husbandNationality || '',
wifeNationality: marriageStorage.wifeNationality || '',
}
}
const persistMarriageInfo = () => {
const user = JSON.parse(localStorage.getItem(STORAGE_KEY_USER) || '{}');
if (!user?.smsCode || !formData.value.marriageNo) {
return;
}
const payload: MarriageStorage = {
phone: user.phone,
smsCode: user.smsCode,
marriageNo: formData.value.marriageNo,
husbandName: formData.value.husbandName,
wifeName: formData.value.wifeName,
registerDate: formData.value.registerDate,
certificateHolder: formData.value.certificateHolder,
husbandId: formData.value.husbandId,
wifeId: formData.value.wifeId,
husbandGender: formData.value.husbandGender,
wifeGender: formData.value.wifeGender,
husbandBirthDate: formData.value.husbandBirthDate,
wifeBirthDate: formData.value.wifeBirthDate,
husbandNationality: formData.value.husbandNationality,
wifeNationality: formData.value.wifeNationality,
};
localStorage.setItem(STORAGE_KEY_MARRIAGE, JSON.stringify(payload));
}
//
function resetMarriageInfo() {
formData.value = {
...formData.value,
marriageNo: "",
husbandName: "",
wifeName: "",
registerDate: "",
certificateHolder: "",
husbandId: "",
wifeId: "",
husbandGender: "",
wifeGender: "",
husbandBirthDate: "",
wifeBirthDate: "",
husbandNationality: "",
wifeNationality: "",
qrCode: ""
}
ocrUploadId.value && (ocrUploadId.value.value = '');
localStorage.removeItem(STORAGE_KEY_MARRIAGE);
ocrUploadId.value?.click();
}
//
const handleScanClick = () => {
@ -456,21 +591,31 @@ const handleImageChange = () => {
showLoading();
apiService.uploadOcrImage(formFormData).then((response: any) => {
apiService.marriageParseOcrInfo({
mobile: formData.value.phone,
smsCode: formData.value.smsCode,
uploadId: response.data.uploadId,
}).then((res: any) => {
if (res?.data?.parsed?.marriageNo) {
formData.value.marriageNo = res?.data?.parsed?.marriageNo;
formData.value.husbandName = res?.data?.parsed?.husbandName;
formData.value.wifeName = res?.data?.parsed?.wifeName;
formData.value.registerDate = res?.data?.parsed?.registerDate;
} else {
showDialog({
message: '解析OCR信息失败请稍后重试',
});
ocrUploadId.value && (ocrUploadId.value.value = '');
apiService.marriageParseOcrInfo({
mobile: formData.value.phone,
smsCode: formData.value.smsCode,
uploadId: response.data.uploadId,
}).then((res: any) => {
if (res?.data?.parsed?.marriageNo) {
formData.value.marriageNo = res?.data?.parsed?.marriageNo || '';
formData.value.husbandName = res?.data?.parsed?.husbandName || '';
formData.value.wifeName = res?.data?.parsed?.wifeName || '';
formData.value.registerDate = res?.data?.parsed?.registerDate || '';
formData.value.certificateHolder = res?.data?.parsed?.certificateHolder || '';
formData.value.husbandId = res?.data?.parsed?.husbandId || '';
formData.value.wifeId = res?.data?.parsed?.wifeId || '';
formData.value.husbandGender = res?.data?.parsed?.husbandGender || '';
formData.value.wifeGender = res?.data?.parsed?.wifeGender || '';
formData.value.husbandBirthDate = res?.data?.parsed?.husbandBirthDate || '';
formData.value.wifeBirthDate = res?.data?.parsed?.wifeBirthDate || '';
formData.value.husbandNationality = res?.data?.parsed?.husbandNationality || '';
formData.value.wifeNationality = res?.data?.parsed?.wifeNationality || '';
persistMarriageInfo();
} else {
showDialog({
message: '解析OCR信息失败请稍后重试',
});
ocrUploadId.value && (ocrUploadId.value.value = '');
}
}).catch((error) => {
showDialog({
@ -485,38 +630,20 @@ const handleImageChange = () => {
message: error?.msg || '图片上传失败,请稍后重试',
});
ocrUploadId.value && (ocrUploadId.value.value = '');
hideLoading();
})
}
//
const submitForm = () => {
if (!formData.value.phone || !formData.value.smsCode) {
showDialog({
message: '请完善所有信息',
});
return
}
showLoading();
apiService.receiveCheck({
marriageNo: formData.value.marriageNo,
receiveName: formData.value.husbandName,
receiveMobile: formData.value.phone,
smsCode: formData.value.smsCode,
}).then((res: any) => {
formData.value.qrCode = res.data.code;
syncQrImage();
}).catch((error) => {
showDialog({
message: error?.msg || '提交失败,请稍后重试',
});
}).finally(() => {
hideLoading();
})
}
hideLoading();
})
}
const goToIdUpload = () => {
if (!formData.value.marriageNo) {
showDialog({
message: '请先完成结婚证识别',
});
return;
}
persistMarriageInfo();
router.push('/idcard');
}
//
import { onUnmounted } from 'vue'

343
src/pages/idcard/index.vue Normal file
View File

@ -0,0 +1,343 @@
<template>
<div class="min-h-screen bg-[#FFF7F9] p-4 pb-8">
<div
class="relative overflow-hidden rounded-2xl border border-[#FFD7DF] bg-gradient-to-br from-[#FFF7F9] via-white to-[#FFE4EA] shadow-[0_12px_32px_rgba(232,66,77,0.08)] p-6 sm:p-7 backdrop-blur-sm">
<div class="absolute -right-10 -top-16 h-40 w-40 rounded-full bg-[#FFE3EA] blur-3xl opacity-70"></div>
<div class="absolute -left-14 -bottom-16 h-40 w-40 rounded-full bg-[#FFD6E2] blur-3xl opacity-60"></div>
<div class="relative z-10 space-y-5">
<div class="text-center text-xl text-[#E8424D] font-semibold">身份证核验</div>
<!-- <div class="bg-white/90 rounded-xl p-4 shadow-[0_6px_18px_rgba(232,66,77,0.06)] border border-[#FFD7DF]">
<div class="flex items-center justify-between mb-3">
<div class="text-base font-semibold text-gray-800">结婚证信息</div>
<button class="text-xs text-[#E8424D]" @click="goBack">返回上一页</button>
</div>
<div class="text-sm text-[#7A5967] space-y-2">
<div>结婚证字号{{ marriageInfo?.marriageNo || '-' }}</div>
<div class="grid grid-cols-1 gap-2">
<div class="flex items-center">
<span class="inline-block w-24 text-gray-500">男方姓名</span>
<span class="text-gray-800">{{ marriageInfo?.husbandName || '-' }}</span>
</div>
<div class="flex items-center">
<span class="inline-block w-24 text-gray-500">男方证件号</span>
<span class="text-gray-800 break-all">{{ marriageInfo?.husbandId || '未识别' }}</span>
</div>
<div class="flex items-center">
<span class="inline-block w-24 text-gray-500">女方姓名</span>
<span class="text-gray-800">{{ marriageInfo?.wifeName || '-' }}</span>
</div>
<div class="flex items-center">
<span class="inline-block w-24 text-gray-500">女方证件号</span>
<span class="text-gray-800 break-all">{{ marriageInfo?.wifeId || '未识别' }}</span>
</div>
</div>
<div>登记日期{{ marriageInfo?.registerDate || '-' }}</div>
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
<div class="bg-white/95 rounded-2xl p-5 shadow-[0_6px_18px_rgba(232,66,77,0.06)] border border-[#FFD7DF] flex flex-col gap-4">
<div class="flex items-start gap-3">
<div class="space-y-1">
<div class="text-base font-semibold text-gray-800">上传男方身份证人像面</div>
<div class="text-xs text-[#7A5967] leading-relaxed space-y-1">
<div>需与结婚证信息一致</div>
<div class="flex flex-col gap-0.5 text-gray-800">
<span>姓名{{ marriageInfo?.husbandName || '-' }}</span>
<span class="break-all">证件号{{ marriageInfo?.husbandId || '身份证号未识别' }}</span>
</div>
</div>
</div>
</div>
<div class="text-xs text-[#7A5967] bg-[#FFF3F5] border border-[#FFE0E7] rounded-lg p-3 leading-relaxed">
身份证照片需清晰无遮挡保持文字方向正确以便快速校验
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<button
class="flex-1 h-14! px-5 py-3! rounded-xl text-base font-semibold border border-[#E8424D]! text-[#E8424D]! bg-white! hover:bg-[#FFE8EE]! active:bg-[#FFD6E2]! active:scale-98 transition disabled:border-gray-300! disabled:text-gray-400! disabled:bg-gray-100!"
@click="triggerUpload('husband')" :disabled="!userInfo">
上传
</button>
<div
:class="['flex items-center gap-2 text-sm font-medium rounded-lg border px-3 py-3 min-h-[48px] sm:w-48', statusPanelClass(husbandMatchedStatus)]">
<span :class="['inline-block w-2.5 h-2.5 rounded-full', statusDotClass(husbandMatchedStatus)]"></span>
<span>{{ husbandMatchedStatus === '待上传' ? '等待上传' : husbandMatchedStatus }}</span>
</div>
</div>
</div>
<div class="bg-white/95 rounded-2xl p-5 shadow-[0_6px_18px_rgba(232,66,77,0.06)] border border-[#FFD7DF] flex flex-col gap-4">
<div class="flex items-start gap-3">
<div class="space-y-1">
<div class="text-base font-semibold text-gray-800">上传女方身份证人像面</div>
<div class="text-xs text-[#7A5967] leading-relaxed space-y-1">
<div>需与结婚证信息一致</div>
<div class="flex flex-col gap-0.5 text-gray-800">
<span>姓名{{ marriageInfo?.wifeName || '-' }}</span>
<span class="break-all">证件号{{ marriageInfo?.wifeId || '身份证号未识别' }}</span>
</div>
</div>
</div>
</div>
<div class="text-xs text-[#7A5967] bg-[#FFF3F5] border border-[#FFE0E7] rounded-lg p-3 leading-relaxed">
拍摄或上传时确保证件四角完整避免反光与模糊
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<button
class="flex-1 h-14! px-5 py-3! rounded-xl text-base font-semibold border border-[#E8424D]! text-[#E8424D]! bg-white! hover:bg-[#FFE8EE]! active:bg-[#FFD6E2]! active:scale-98 transition disabled:border-gray-300! disabled:text-gray-400! disabled:bg-gray-100!"
@click="triggerUpload('wife')" :disabled="!userInfo">
上传
</button>
<div
:class="['flex items-center gap-2 text-sm font-medium rounded-lg border px-3 py-3 min-h-[48px] sm:w-48', statusPanelClass(wifeMatchedStatus)]">
<span :class="['inline-block w-2.5 h-2.5 rounded-full', statusDotClass(wifeMatchedStatus)]"></span>
<span>{{ wifeMatchedStatus === '待上传' ? '等待上传' : wifeMatchedStatus }}</span>
</div>
</div>
</div>
</div>
<div class="pt-2">
<button
class="w-full h-14 bg-gradient-to-r from-[#E8424D]! to-[#FF7A7A]! text-white! rounded-full text-lg font-bold transition-all hover:opacity-90 active:scale-98 disabled:from-[#8a8a8a]! disabled:to-[#8a8a8a]!"
@click="submitClaim" :disabled="!canSubmit || submitting">
<div class="text-lg">{{ submitting ? '提交中...' : '提交领取喜礼' }}</div>
</button>
</div>
</div>
</div>
<input type="file" ref="husbandUpload" class="hidden" accept="image/*" @change="handleIdChange('husband')" />
<input type="file" ref="wifeUpload" class="hidden" accept="image/*" @change="handleIdChange('wife')" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import apiService, { type IdCardOcrParseResponse } from '@/services/apiService'
import { showLoading, hideLoading } from '@/components/loading'
import { showDialog } from 'vant'
import 'vant/es/dialog/style'
type Role = 'husband' | 'wife'
type MarriageInfo = {
marriageNo?: string
husbandName?: string
wifeName?: string
registerDate?: string
certificateHolder?: string
wifeId?: string
husbandId?: string
wifeGender?: string | null
husbandGender?: string | null
wifeBirthDate?: string
husbandBirthDate?: string
wifeNationality?: string
husbandNationality?: string
phone?: string
smsCode?: string
}
const STORAGE_KEY_USER = 'userAs'
const STORAGE_KEY_MARRIAGE = 'marriageOcr'
const router = useRouter()
const userInfo = ref<{ phone: string; smsCode: string } | null>(null)
const marriageInfo = ref<MarriageInfo | null>(null)
const husbandUpload = ref<HTMLInputElement>()
const wifeUpload = ref<HTMLInputElement>()
const husbandIdParsed = ref<IdCardOcrParseResponse['parsed'] | null>(null)
const wifeIdParsed = ref<IdCardOcrParseResponse['parsed'] | null>(null)
const submitting = ref(false)
const loadFromStorage = () => {
const userRaw = localStorage.getItem(STORAGE_KEY_USER)
const marriageRaw = localStorage.getItem(STORAGE_KEY_MARRIAGE)
if (!userRaw || !marriageRaw) {
showDialog({
message: '请先完成手机验证与结婚证识别',
}).then(() => router.replace('/claim'))
return
}
try {
const user = JSON.parse(userRaw)
const marriage = JSON.parse(marriageRaw)
if (!user?.smsCode || marriage?.smsCode !== user.smsCode) {
localStorage.removeItem(STORAGE_KEY_MARRIAGE)
showDialog({
message: '验证码已失效,请重新验证后再试',
}).then(() => router.replace('/claim'))
return
}
userInfo.value = { phone: user.phone, smsCode: user.smsCode }
marriageInfo.value = marriage
} catch (error) {
console.error('读取本地缓存失败', error)
showDialog({
message: '身份信息读取失败,请重新验证',
}).then(() => router.replace('/claim'))
}
}
onMounted(() => {
loadFromStorage()
})
const normalize = (val?: string | null) => (val || '').replace(/\s+/g, '').toUpperCase()
const checkMatch = (parsed: IdCardOcrParseResponse['parsed'] | null, target: { name?: string; id?: string; gender?: string | null }) => {
if (!parsed || !target.name) return false
const nameMatched = normalize(parsed.name) === normalize(target.name)
const idMatched = target.id ? normalize(parsed.id_number) === normalize(target.id) : true
const genderMatched = target.gender ? parsed.gender === target.gender : true
return nameMatched && idMatched && genderMatched
}
const husbandMatched = computed(() =>
checkMatch(husbandIdParsed.value, {
name: marriageInfo.value?.husbandName,
id: marriageInfo.value?.husbandId,
gender: marriageInfo.value?.husbandGender,
})
)
const wifeMatched = computed(() =>
checkMatch(wifeIdParsed.value, {
name: marriageInfo.value?.wifeName,
id: marriageInfo.value?.wifeId,
gender: marriageInfo.value?.wifeGender,
})
)
const husbandMatchedStatus = computed(() => {
if (!husbandIdParsed.value) return '待上传'
return husbandMatched.value ? '已匹配' : '信息不一致'
})
const wifeMatchedStatus = computed(() => {
if (!wifeIdParsed.value) return '待上传'
return wifeMatched.value ? '已匹配' : '信息不一致'
})
const statusPanelClass = (status: string) => {
if (status === '已匹配') return 'text-green-700 bg-green-50 border-green-200'
if (status === '信息不一致') return 'text-[#E8424D] bg-[#FFF3F5] border-[#FFD7DF]'
return 'text-gray-600 bg-gray-50 border-gray-200'
}
const statusDotClass = (status: string) => {
if (status === '已匹配') return 'bg-green-500 shadow-[0_0_0_4px_rgba(34,197,94,0.15)]'
if (status === '信息不一致') return 'bg-[#E8424D] shadow-[0_0_0_4px_rgba(232,66,77,0.15)]'
return 'bg-gray-400 shadow-[0_0_0_4px_rgba(156,163,175,0.15)]'
}
const canSubmit = computed(() => husbandMatched.value && wifeMatched.value && !!marriageInfo.value && !!userInfo.value)
const triggerUpload = (role: Role) => {
if (role === 'husband') {
husbandUpload.value?.click()
} else {
wifeUpload.value?.click()
}
}
const handleIdChange = async (role: Role) => {
const input = role === 'husband' ? husbandUpload.value : wifeUpload.value
const file = input?.files?.[0]
if (!file) {
showDialog({
message: '请选择图片文件',
})
return
}
if (!userInfo.value) {
showDialog({
message: '请先返回上一页完成验证',
})
return
}
const formFormData = new FormData()
formFormData.append('file', file)
showLoading()
try {
const response: any = await apiService.uploadOcrImage(formFormData)
const res: any = await apiService.idCardParseOcrInfo({
mobile: userInfo.value.phone,
smsCode: userInfo.value.smsCode,
uploadId: response.data.uploadId,
})
const parsed = res?.data?.parsed
if (!parsed) {
throw new Error('识别失败,请重试')
}
if (role === 'husband') {
husbandIdParsed.value = parsed
} else {
wifeIdParsed.value = parsed
}
} catch (error: any) {
console.error('身份证识别失败', error)
const msg = error?.msg || error?.message || '身份证识别失败,请稍后重试'
showDialog({
message: msg,
}).then(() => {
if (msg.includes('验证码')) {
localStorage.removeItem(STORAGE_KEY_MARRIAGE)
router.replace('/claim')
}
})
} finally {
hideLoading()
if (input) {
input.value = ''
}
}
}
const submitClaim = async () => {
if (!canSubmit.value || !marriageInfo.value || !userInfo.value) return
submitting.value = true
showLoading()
try {
await apiService.receiveCheck({
marriageNo: marriageInfo.value.marriageNo,
receiveName: marriageInfo.value.husbandName || marriageInfo.value.certificateHolder || marriageInfo.value.wifeName,
receiveMobile: userInfo.value.phone,
smsCode: userInfo.value.smsCode,
})
showDialog({
message: '提交成功,请返回首页查看核销二维码',
}).then(() => {
router.replace('/claim')
})
} catch (error: any) {
const msg = error?.msg || '提交失败,请稍后重试'
showDialog({
message: msg,
}).then(() => {
if (msg.includes('验证码')) {
localStorage.removeItem(STORAGE_KEY_MARRIAGE)
router.replace('/claim')
}
})
} finally {
hideLoading()
submitting.value = false
}
}
const goBack = () => {
router.back()
}
</script>

View File

@ -26,6 +26,14 @@ const routes: Array<RouteRecordRaw> = [
meta: {
title: '宁福您彩 活动规则'
}
},
{
path: '/idcard',
name: 'IdCard',
component: () => import('../pages/idcard/index.vue'),
meta: {
title: '宁福您彩 身份核验'
}
}
]

View File

@ -13,14 +13,53 @@ export interface OcrUploadResponse {
uploadId: string
}
export interface IdCardOcrParseResponse {
raw: string
words: string[]
parsed: {
birthday?: string
id_number: string
address: string
image_status:
| 'normal'
| 'reversed_side'
| 'non_idcard'
| 'blurred'
| 'other_type_card'
| 'over_exposure'
| 'over_dark'
| 'unknown'
risk_type:
| 'normal'
| 'copy'
| 'scan'
| 'temporary'
| 'screen'
| 'screenshot'
| 'unknown'
gender: '男' | '女' | '未知' | null | undefined
name?: string
nationality?: string
}
}
export interface MarriageOcrParseResponse {
raw: string
words: string[]
parsed: {
marriageNo: string
husbandName: string
wifeName: string
registerDate: string
marriageNo?: string
registerDate?: string
certificateHolder?: string
wifeId?: string
wifeName?: string
wifeBirthDate?: string
wifeNationality?: string
wifeGender?: '男' | '女' | '未知' | null | undefined
husbandId?: string
husbandName?: string
husbandGender?: '男' | '女' | '未知' | null | undefined
husbandBirthDate?: string
husbandNationality?: string
}
}
@ -53,7 +92,8 @@ export interface MarriageCodeListVO {
export interface CommSmsDTO {
mobile: string
type: number // 0=登录1=兑换领取2=OCR识别
smsCode?: string
type: 0 | 1 | 2 | 3 // 0=登录1=兑换领取2=OCR识别3=本流程使用
}
// API服务接口定义
@ -76,6 +116,13 @@ export interface ApiService {
uploadId: string
}): Promise<any>
// OCR 识别身份证
idCardParseOcrInfo(data: {
mobile: string
smsCode: string
uploadId: string
}): Promise<any>
// 领取流程相关接口
// 领取前校验(生成二维码前的校验与预览)
receiveCheck(data: MarriageCodeDTO): Promise<any>
@ -103,8 +150,8 @@ export interface ApiService {
// 登录
login(data: {
mobile: string
password: string
smsCode: string
password?: string
smsCode?: string
}): Promise<any>
// 后台活动管理接口(可选,用于管理端)
@ -113,4 +160,4 @@ export interface ApiService {
}
export const apiService: ApiService
export default apiService
export default apiService