拍照上传身份证
原ocr代码(官方已废弃,无法使用)
<!--
* @Date: 2024-02-22 18:53:33
* @LastEditTime: 2025-07-29 13:49:25
* @Description: 上传身份证
-->
<template>
<view class="wrap">
<image class="bg-img" :src="TOP_BG_PNG" mode="widthFix" />
<view class="main-content">
<!-- 1-1.验证和识别通过,提示“验证通过”,并覆盖页面的身份证信息缓存(照片信息、格式化信息)
1-2.验证不通过,报错“验证失败,请保证照片的清晰重新上传” -->
<view class="upload-wrap box-shadow">
<view class="upload-label">
<view class="label-title">头像面</view>
<view class="label-subtitle">拍摄或上传</view>
</view>
<view class="ocr-area">
<ocr-navigator certificate-type="idCard" :opposite="false" @onSuccess="handleSuccessTop">
<view class="upload-area">
<view class="angle-wrap">
<view class="angle top-left" />
<view class="angle top-right" />
<view class="angle bottom-right" />
<view class="angle bottom-left" />
</view>
<!-- 微信OCR -->
<view v-if="!viewCertImgUrl" class="inner-area" @click="handleUploadClick">
<view class="upload-icon cross" />
<view class="upload-text">拍照或上传身份证头像面</view>
</view>
<!-- 身份证照片 -->
<image v-else class="cert-image" :src="viewCertImgUrl" />
</view>
</ocr-navigator>
</view>
</view>
<view v-if="!isInputShow && certNoLimit" class="forget-text" @click="showInput">忘记带身份证?</view>
<view v-else-if="isInputShow && certNoLimit" class="id-card-input-wrap box-shadow">
<view class="input-label">请输入身份证号</view>
<view class="input-wrap">
<input
type="text"
focus
placeholder="请输入身份证号"
placeholder-class="input-placeholder"
class="input input-id-card"
:disabled="ifOCR"
:value="idNumber"
@input="handleInput"
/>
</view>
</view>
</view>
<view class="button-wrap safe-padding-bottom">
<view class="button button-primary" :class="{ disabled: isDisabled }" @click="onConfirm">提交值机</view>
</view>
<!-- 登录弹框 -->
<popup-login ref="popupLoginRef" />
</view>
</template>
<script setup lang="ts">
import config from '@/../config/config';
import { checkIn, getServeOrderDetail } from '@/services/api-order';
import { subscribeMessage } from '@/utils/BusinessUtils';
import { getCheckInStorage } from '@/utils/Storage';
import { uploadFile } from '@/utils/UpLoadUtils';
import { computed, onMounted, onUnmounted, ref, toRaw } from 'vue';
import { useStore } from 'vuex';
import { goTabBarPage, goFlyGuidePage } from '@/utils/goPage';
const TOP_BG_PNG = `${config.assetPath}/images/fly/guide-bg.png`;
const store = useStore();
let certNoLimit = store?.state?.configStore?.globalConfig?.certNoLimit !== 'APP_CONTEXT_CERT_NO_LIMIT_OFF'; // APP_CONTEXT_CERT_NO_LIMIT_OFF 隐藏证件号,APP_CONTEXT_CERT_NO_LIMIT_ON 不隐藏证件号
let isInputShow = ref(false);
let idNumber = ref();
let oCRInfo = ref({});
let viewCertImgUrl = ref('');
let serviceOrderInfo = ref({});
let serverOrderId = ref('');
let ifOCR = ref(false);
const isDisabled = computed(() => {
return !oCRInfo.value?.certNo && !idNumber.value;
});
const handleUploadClick = () => {
// console.log('点击了上传按钮');
};
const handleSuccessTop = async (e) => {
const info = e.detail;
if (!info?.id?.text) {
uni.showToast({
title: '验证失败,请保证照片的清晰重新上传',
icon: 'none',
});
return;
}
let certImg;
uploadFile({
tempFilePath: info?.image_path,
moduleName: 'certImg',
})
.then((res) => {
certImg = res;
viewCertImgUrl.value = info?.image_path;
oCRInfo.value = {
certImg,
name: info?.name?.text,
certNo: info?.id?.text,
sex: info?.gender?.text === '男' ? 1 : info?.gender?.text === '女' ? 2 : 0,
birthday: info?.birth?.text,
nation: info?.nationality?.text,
address: info?.address?.text,
};
idNumber.value = info?.id?.text;
ifOCR.value = true;
store.dispatch('setCertInfo', { ...toRaw(oCRInfo.value) });
uni.showToast({
title: '验证通过',
icon: 'success',
});
})
.catch((error) => {
uni.showToast({
title: error || '验证失败,请保证照片的清晰重新上传',
icon: 'none',
});
});
};
/** 身份证号输入 */
const handleInput = (event) => {
if (isInputShow.value) {
const { value } = event.detail;
idNumber.value = value;
store.dispatch('setCertInfoCerNo', idNumber.value);
}
};
/** 展示文本输入框 */
const showInput = () => {
isInputShow.value = true;
};
let reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
/** 提交值机 */
const onConfirm = async () => {
const checkInInfo = await getCheckInStorage();
if (!reg.test(checkInInfo?.certInfo?.certNo)) {
uni.showToast({
title: '请输入正确的身份证号',
icon: 'none',
});
return;
}
const res = await checkIn({
serverOrderId: checkInInfo?.serverOrderId,
inviteTicket: checkInInfo?.inviteTicket,
checkCode: checkInInfo?.checkCode,
noticeSign: checkInInfo?.noticeSign,
...checkInInfo?.certInfo,
});
const config = store?.state?.configStore?.globalConfig;
const tempIds = [config.orderCompleteTemplateId];
await subscribeMessage(tempIds)
.catch((err) => {
uni.showToast({
title: '订阅消息失败',
icon: 'none',
});
})
.finally(() => {
//是否有值机完成图片/值机完成视频
if (serviceOrderInfo.value?.checkedInImages?.length || serviceOrderInfo.value?.checkedInVideos?.length) {
const type = 'CHECKIN';
goFlyGuidePage(type, serverOrderId.value);
} else {
goTabBarPage('fly');
}
});
};
const loadCheckInGuide = async () => {
const { data } = await getServeOrderDetail({ id: serverOrderId.value });
serviceOrderInfo.value = data || {};
};
const init = async () => {
const info = await getCheckInStorage();
serverOrderId.value = info?.serverOrderId;
loadCheckInGuide();
};
onMounted(() => {
init();
});
onUnmounted(() => {
//清除值机信息
store.dispatch('setCheckInInfo', null);
});
</script>
<style lang="scss">
@import './index.scss';
</style>
方案一:使用chooseMedia上传身份证安卓机只需要确认一次才能上传(推荐)
<!--
* @Date: 2024-02-22 18:53:33
* @LastEditTime: 2025-07-29 13:49:25
* @Description: 上传身份证
-->
<template>
<view class="wrap">
<image class="bg-img" :src="TOP_BG_PNG" mode="widthFix" />
<view class="main-content">
<!-- 1-1.验证和识别通过,提示“验证通过”,并覆盖页面的身份证信息缓存(照片信息、格式化信息)
1-2.验证不通过,报错“验证失败,请保证照片的清晰重新上传” -->
<view class="upload-wrap box-shadow">
<view class="upload-label">
<view class="label-title">头像面</view>
<view class="label-subtitle">拍摄或上传</view>
</view>
<view class="ocr-area">
<view class="upload-area">
<view class="angle-wrap">
<view class="angle top-left" />
<view class="angle top-right" />
<view class="angle bottom-right" />
<view class="angle bottom-left" />
</view>
<!-- 微信OCR -->
<view v-if="!viewCertImgUrl" class="inner-area" @click="chooseImage">
<view class="upload-icon cross" />
<view class="upload-text">拍照或上传身份证头像面</view>
</view>
<!-- 身份证照片 -->
<image v-else class="cert-image" :src="viewCertImgUrl" />
</view>
</view>
</view>
<view v-if="!isInputShow && certNoLimit" class="forget-text" @click="showInput">忘记带身份证?</view>
<view v-else-if="isInputShow && certNoLimit" class="id-card-input-wrap box-shadow">
<view class="input-label">请输入身份证号</view>
<view class="input-wrap">
<input
type="text"
focus
placeholder="请输入身份证号"
placeholder-class="input-placeholder"
class="input input-id-card"
:disabled="ifOCR"
:value="idNumber"
@input="handleInput"
/>
</view>
</view>
</view>
<view class="button-wrap safe-padding-bottom">
<view class="button button-primary" :class="{ disabled: isDisabled }" @click="onConfirm">提交值机</view>
</view>
<!-- 登录弹框 -->
<popup-login ref="popupLoginRef" />
</view>
</template>
<script setup lang="ts">
import config from '@/../config/config';
import { checkIn, getServeOrderDetail,idCardOCR } from '@/services/api-order';
import { subscribeMessage } from '@/utils/BusinessUtils';
import { getCheckInStorage } from '@/utils/Storage';
import { uploadFile } from '@/utils/UpLoadUtils';
import { computed, onMounted, onUnmounted, ref, toRaw } from 'vue';
import { useStore } from 'vuex';
import { goTabBarPage, goFlyGuidePage } from '@/utils/goPage';
const TOP_BG_PNG = `${config.assetPath}/images/fly/guide-bg.png`;
const store = useStore();
let certNoLimit = store?.state?.configStore?.globalConfig?.certNoLimit !== 'APP_CONTEXT_CERT_NO_LIMIT_OFF'; // APP_CONTEXT_CERT_NO_LIMIT_OFF 隐藏证件号,APP_CONTEXT_CERT_NO_LIMIT_ON 不隐藏证件号
let isInputShow = ref(false);
let idNumber = ref();
let oCRInfo = ref({});
let viewCertImgUrl = ref('');
let serviceOrderInfo = ref({});
let serverOrderId = ref('');
let ifOCR = ref(false);
const isDisabled = computed(() => {
return !oCRInfo.value?.certNo && !idNumber.value;
});
const chooseImage = async () => {
uni.chooseMedia({
count: 1,
sizeType: ['original', 'compressed'],
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
viewCertImgUrl.value = tempFilePath; // 预览本地图片
try {
// 1. 上传图片到自己的服务器(示例:调用项目的上传方法)
const uploadRes = await uploadFile({
tempFilePath,
moduleName: 'certImg',
});
const imageUrl = uploadRes; // 服务器返回的图片URL
// 2. 调用后端OCR接口(需自行实现后端)
const ocrRes = await idCardOCR({
imageUrl,
cardSide: 'FRONT', //FRONT:身份证有照片的一面(人像面),BACK:身份证有国徽的一面(国徽面),
});
// 3. 处理OCR结果(与原逻辑对齐)
const info = ocrRes.data;
oCRInfo.value = {
certImg: imageUrl,
name: info.name,
certNo: info.idNum,
sex: info?.sex === '男' ? 1 : info?.sex === '女' ? 2 : 0,
birthday: info.birth,
nation: info.nation,
address: info.address,
};
idNumber.value = info.idNum;
ifOCR.value = true;
store.dispatch('setCertInfo', { ...toRaw(oCRInfo.value) });
uni.showToast({
title: '验证通过',
icon: 'success',
});
} catch (error) {
if (error?.retcode !== 0) {
await nextTick(() => {
oCRInfo.value = ''; //清空信息,置灰按钮
idNumber.value = ''; //清空信息,置灰按钮
viewCertImgUrl.value = ''; // 清空预览图
});
}
}
},
fail: (err) => {
uni.showToast({
title: '选择图片失败,请重试',
icon: 'none',
});
},
});
};
/** 身份证号输入 */
const handleInput = (event) => {
if (isInputShow.value) {
const { value } = event.detail;
idNumber.value = value;
store.dispatch('setCertInfoCerNo', idNumber.value);
}
};
/** 展示文本输入框 */
const showInput = () => {
isInputShow.value = true;
};
let reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
/** 提交值机 */
const onConfirm = async () => {
const checkInInfo = await getCheckInStorage();
if (!reg.test(checkInInfo?.certInfo?.certNo)) {
uni.showToast({
title: '请输入正确的身份证号',
icon: 'none',
});
return;
}
const res = await checkIn({
serverOrderId: checkInInfo?.serverOrderId,
inviteTicket: checkInInfo?.inviteTicket,
checkCode: checkInInfo?.checkCode,
noticeSign: checkInInfo?.noticeSign,
...checkInInfo?.certInfo,
});
const config = store?.state?.configStore?.globalConfig;
const tempIds = [config.orderCompleteTemplateId];
await subscribeMessage(tempIds)
.catch((err) => {
uni.showToast({
title: '订阅消息失败',
icon: 'none',
});
})
.finally(() => {
//是否有值机完成图片/值机完成视频
if (serviceOrderInfo.value?.checkedInImages?.length || serviceOrderInfo.value?.checkedInVideos?.length) {
const type = 'CHECKIN';
goFlyGuidePage(type, serverOrderId.value);
} else {
goTabBarPage('fly');
}
});
};
const loadCheckInGuide = async () => {
const { data } = await getServeOrderDetail({ id: serverOrderId.value });
serviceOrderInfo.value = data || {};
};
const init = async () => {
const info = await getCheckInStorage();
serverOrderId.value = info?.serverOrderId;
loadCheckInGuide();
};
onMounted(() => {
init();
});
onUnmounted(() => {
//清除值机信息
store.dispatch('setCheckInInfo', null);
});
</script>
<style lang="scss">
@import './index.scss';
</style>
方案二:使用chooseImage上传身份证安卓机需要两次确认才能上传
<!--
* @Date: 2024-02-22 18:53:33
* @LastEditTime: 2025-07-29 13:49:25
* @Description: 上传身份证
-->
<template>
<view class="wrap">
<image class="bg-img" :src="TOP_BG_PNG" mode="widthFix" />
<view class="main-content">
<!-- 1-1.验证和识别通过,提示“验证通过”,并覆盖页面的身份证信息缓存(照片信息、格式化信息)
1-2.验证不通过,报错“验证失败,请保证照片的清晰重新上传” -->
<view class="upload-wrap box-shadow">
<view class="upload-label">
<view class="label-title">头像面</view>
<view class="label-subtitle">拍摄或上传</view>
</view>
<view class="ocr-area">
<view class="upload-area">
<view class="angle-wrap">
<view class="angle top-left" />
<view class="angle top-right" />
<view class="angle bottom-right" />
<view class="angle bottom-left" />
</view>
<!-- 微信OCR -->
<view v-if="!viewCertImgUrl" class="inner-area" @click="chooseImage">
<view class="upload-icon cross" />
<view class="upload-text">拍照或上传身份证头像面</view>
</view>
<!-- 身份证照片 -->
<image v-else class="cert-image" :src="viewCertImgUrl" />
</view>
</view>
</view>
<view v-if="!isInputShow && certNoLimit" class="forget-text" @click="showInput">忘记带身份证?</view>
<view v-else-if="isInputShow && certNoLimit" class="id-card-input-wrap box-shadow">
<view class="input-label">请输入身份证号</view>
<view class="input-wrap">
<input
type="text"
focus
placeholder="请输入身份证号"
placeholder-class="input-placeholder"
class="input input-id-card"
:disabled="ifOCR"
:value="idNumber"
@input="handleInput"
/>
</view>
</view>
</view>
<view class="button-wrap safe-padding-bottom">
<view class="button button-primary" :class="{ disabled: isDisabled }" @click="onConfirm">提交值机</view>
</view>
<!-- 登录弹框 -->
<popup-login ref="popupLoginRef" />
</view>
</template>
<script setup lang="ts">
import config from '@/../config/config';
import { checkIn, getServeOrderDetail,idCardOCR } from '@/services/api-order';
import { subscribeMessage } from '@/utils/BusinessUtils';
import { getCheckInStorage } from '@/utils/Storage';
import { uploadFile } from '@/utils/UpLoadUtils';
import { computed, onMounted, onUnmounted, ref, toRaw } from 'vue';
import { useStore } from 'vuex';
import { goTabBarPage, goFlyGuidePage } from '@/utils/goPage';
const TOP_BG_PNG = `${config.assetPath}/images/fly/guide-bg.png`;
const store = useStore();
let certNoLimit = store?.state?.configStore?.globalConfig?.certNoLimit !== 'APP_CONTEXT_CERT_NO_LIMIT_OFF'; // APP_CONTEXT_CERT_NO_LIMIT_OFF 隐藏证件号,APP_CONTEXT_CERT_NO_LIMIT_ON 不隐藏证件号
let isInputShow = ref(false);
let idNumber = ref();
let oCRInfo = ref({});
let viewCertImgUrl = ref('');
let serviceOrderInfo = ref({});
let serverOrderId = ref('');
let ifOCR = ref(false);
const isDisabled = computed(() => {
return !oCRInfo.value?.certNo && !idNumber.value;
});
const chooseImage = async () => {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0];
viewCertImgUrl.value = tempFilePath; // 预览本地图片
try {
// 1. 上传图片到自己的服务器(示例:调用项目的上传方法)
const uploadRes = await uploadFile({
tempFilePath,
moduleName: 'certImg',
});
const imageUrl = uploadRes; // 服务器返回的图片URL
// 2. 调用后端OCR接口(需自行实现后端)
const ocrRes = await idCardOCR({
imageUrl,
cardSide: 'FRONT', //FRONT:身份证有照片的一面(人像面),BACK:身份证有国徽的一面(国徽面),
});
// 3. 处理OCR结果(与原逻辑对齐)
const info = ocrRes.data;
oCRInfo.value = {
certImg: imageUrl,
name: info.name,
certNo: info.idNum,
sex: info?.sex === '男' ? 1 : info?.sex === '女' ? 2 : 0,
birthday: info.birth,
nation: info.nation,
address: info.address,
};
idNumber.value = info.idNum;
ifOCR.value = true;
store.dispatch('setCertInfo', { ...toRaw(oCRInfo.value) });
uni.showToast({
title: '验证通过',
icon: 'success',
});
} catch (error) {
if (error?.retcode !== 0) {
await nextTick(() => {
oCRInfo.value = ''; //清空信息,置灰按钮
idNumber.value = ''; //清空信息,置灰按钮
viewCertImgUrl.value = ''; // 清空预览图
});
}
}
},
fail: (err) => {
uni.showToast({
title: '选择图片失败,请重试',
icon: 'none',
});
},
});
};
/** 身份证号输入 */
const handleInput = (event) => {
if (isInputShow.value) {
const { value } = event.detail;
idNumber.value = value;
store.dispatch('setCertInfoCerNo', idNumber.value);
}
};
/** 展示文本输入框 */
const showInput = () => {
isInputShow.value = true;
};
let reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
/** 提交值机 */
const onConfirm = async () => {
const checkInInfo = await getCheckInStorage();
if (!reg.test(checkInInfo?.certInfo?.certNo)) {
uni.showToast({
title: '请输入正确的身份证号',
icon: 'none',
});
return;
}
const res = await checkIn({
serverOrderId: checkInInfo?.serverOrderId,
inviteTicket: checkInInfo?.inviteTicket,
checkCode: checkInInfo?.checkCode,
noticeSign: checkInInfo?.noticeSign,
...checkInInfo?.certInfo,
});
const config = store?.state?.configStore?.globalConfig;
const tempIds = [config.orderCompleteTemplateId];
await subscribeMessage(tempIds)
.catch((err) => {
uni.showToast({
title: '订阅消息失败',
icon: 'none',
});
})
.finally(() => {
//是否有值机完成图片/值机完成视频
if (serviceOrderInfo.value?.checkedInImages?.length || serviceOrderInfo.value?.checkedInVideos?.length) {
const type = 'CHECKIN';
goFlyGuidePage(type, serverOrderId.value);
} else {
goTabBarPage('fly');
}
});
};
const loadCheckInGuide = async () => {
const { data } = await getServeOrderDetail({ id: serverOrderId.value });
serviceOrderInfo.value = data || {};
};
const init = async () => {
const info = await getCheckInStorage();
serverOrderId.value = info?.serverOrderId;
loadCheckInGuide();
};
onMounted(() => {
init();
});
onUnmounted(() => {
//清除值机信息
store.dispatch('setCheckInInfo', null);
});
</script>
<style lang="scss">
@import './index.scss';
</style>