Files
yunxinwei-uniapp/pages/order/delivery.vue
T
gxwebsoft 5c41a5d764 feat(order): 新增身份证号手动填写及格式校验
- 在订单确认页新增身份证号输入框,支持识别失败时手动填写
- 身份证号字段改为可选项,仅有值时进行格式校验,格式校验规则为18位数字或含X/x结尾的号码
- 提交订单时仅当身份证号非空才将该字段传给后端,避免覆盖后端OCR识别结果
- 移除前端OCR相关密钥配置及调用,改为后端统一识别处理
- 优化上传逻辑及界面交互,确保身份证号信息正确传递及提示
2026-08-23 11:15:43 +08:00

607 lines
15 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="container" :style="appThemeStyle">
<view class="reject" v-if="form.apply_status == 10">
<u-alert-tips type="error" title="申请已提交:" description="请耐心等待工作人员的审核"></u-alert-tips>
</view>
<view class="reject" v-else-if="form.apply_status == 30">
<u-alert-tips type="error" title="您的申请已被驳回:" :description="form.reject_reason"></u-alert-tips>
</view>
<u-form :model="form" ref="uForm" label-width="180rpx">
<!-- 标题 -->
<view class="page-title">身份证信息必传</view>
<view class="form-wrapper">
<u-form-item label="身份证正面" prop="img1" required>
<image :src="orderSourceData[0]" v-if="orderSourceData[0]" mode="aspectFill" class="upload-img" @click="chooseImage(1)"></image>
<view v-else class="upload-placeholder" @click="chooseImage(1)">
<text class="placeholder-icon">+</text>
<text class="placeholder-text">点击上传身份证正面人像面</text>
</view>
</u-form-item>
<u-form-item label="身份证反面" prop="img2" required>
<image :src="orderSourceData[1]" v-if="orderSourceData[1]" mode="aspectFill" class="upload-img" @click="chooseImage(2)"></image>
<view v-else class="upload-placeholder" @click="chooseImage(2)">
<text class="placeholder-icon">+</text>
<text class="placeholder-text">点击上传身份证反面国徽面</text>
</view>
</u-form-item>
</view>
<!-- 标题 -->
<view class="page-title">其他凭证必传</view>
<!-- 表单组件 -->
<view class="form-wrapper">
<u-form-item label="人车合照" prop="img3" required>
<image :src="orderSourceData[2]" v-if="orderSourceData[2]" mode="aspectFill" class="upload-img" @click="chooseImage(3)"></image>
<view v-else class="upload-placeholder" @click="chooseImage(3)">
<text class="placeholder-icon">+</text>
<text class="placeholder-text">点击上传人车合照</text>
</view>
</u-form-item>
<u-form-item label="车子照片" prop="img4" required>
<image :src="orderSourceData[3]" v-if="orderSourceData[3]" mode="aspectFill" class="upload-img" @click="chooseImage(4)"></image>
<view v-else class="upload-placeholder" @click="chooseImage(4)">
<text class="placeholder-icon">+</text>
<text class="placeholder-text">点击上传车子照片</text>
</view>
</u-form-item>
<u-form-item label="安装照片" prop="img5" required>
<image :src="orderSourceData[4]" v-if="orderSourceData[4]" mode="aspectFill" class="upload-img" @click="chooseImage(5)"></image>
<view v-else class="upload-placeholder" @click="chooseImage(5)">
<text class="placeholder-icon">+</text>
<text class="placeholder-text">点击上传安装完成照片</text>
</view>
</u-form-item>
</view>
<view class="form-wrapper">
<u-input type="number" v-model="receiptPhone" placeholder="请输入收货人手机号" />
<view class="captcha-item">
<u-input type="text" placeholder="请输入验证码" v-model="captcha" placeholder-class="captcha_input" />
<u-button class="yanzhengma" @click="getCaptcha">{{time}} {{text}}</u-button>
</view>
<u-input type="number" v-model="emergentUser" placeholder="请输入紧急联系人电话" />
<u-input type="text" v-model="officeAddress" placeholder="请输入单位地址" />
<u-input type="text" v-model="homeAddress" placeholder="请输入家庭地址" />
<u-input type="idcard" v-model="idCode" placeholder="身份证号(一般自动识别,识别失败可手动填写)" />
</view>
</u-form>
<view class="btn">
<u-button type="primary" shape="circle" @click="handleSubmit()">
{{ submitText }}
</u-button>
</view>
<!-- <view class="btn">
<u-button shape="circle" @click="resetting">
重置
</u-button>
</view> -->
<view class="btn"></view>
</view>
</template>
<script>
import {
uploadFile
} from '@/api/upload'
import {
http
} from '@/websoft/api/index.js'
import {
pageOrder,
removeOrder,
receiptOrder,
getOrder
} from '@/websoft/api/order.js'
import { sendSmsCaptcha } from '@/websoft/api/captcha.js'
import { fileUrl } from '@/config.js'
import { isMobile } from '@/utils/verify'
// 表单验证规则
const rules = {
receiptPhone: [{
required: true,
message: '请输入手机号',
trigger: ['blur', 'change']
}, {
// 自定义验证函数
validator: (rule, value, callback) => {
// 返回true表示校验通过,返回false表示不通过
return isMobile(value)
},
message: '手机号码不正确',
// 触发器可以同时用blur和change
trigger: ['blur'],
}],
captcha: [{
required: true,
message: '请输入验证码',
trigger: ['blur', 'change']
}],
emergentUser: [{
required: true,
message: '请输入紧急联系人',
trigger: ['blur', 'change']
}],
officeAddress: [{
required: true,
message: '请输入单位地址',
trigger: ['blur', 'change']
}],
homeAddress: [{
required: true,
message: '请输入家庭地址',
trigger: ['blur', 'change']
}],
idCode: [{
validator: (rule, value, callback) => {
// 允许为空(身份证号由后端 OCR 自动识别);有值时才校验格式
if (!value) return true
return /^\d{17}[\dXx]$/.test(value || '')
},
message: '身份证号格式不正确',
trigger: ['blur', 'change']
}],
}
// 图片槽位说明(与 orderSourceData 下标一一对应)
const imageSlotLabels = ['身份证正面', '身份证反面', '人车合照', '车子照片', '安装照片']
// 过滤后端返回的错误码尾巴(如 ".FAILUREFP03333"),让提示更友好
function formatErrorMsg(msg) {
if (!msg) return '操作失败,请稍后重试'
return String(msg).replace(/\.[A-Z0-9]{4,}\s*$/, '').trim() || '操作失败,请稍后重试'
}
export default {
data() {
return {
form: {},
order: {},
orderSourceData: [],
// 验证码(主要代码,直接复制使用即可)
time: '',
text: '获取验证码',
receiptPhone: '',
captcha: '',
emergentUser: '',
officeAddress: '',
homeAddress: '',
// 身份证号(上传正面后由 OCR 自动识别回填,也可手动修改)
idCode: '',
// 按钮禁用
// disabled: false,
submitText: '证件已上传并确认收货',
}
},
// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
onReady() {
this.$refs.uForm.setRules(this.rules)
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
const app = this
app.orderId = options.orderId
},
onShow() {
this.getData()
},
methods: {
// 加载订单
getData() {
const app = this
const { orderId } = this
getOrder(orderId).then(res => {
app.order = res.data
if(res.data.orderSourceData.length > 0){
app.orderSourceData = JSON.parse(res.data.orderSourceData)
}
})
},
//获取验证码(主要代码,直接复制使用即可)
getCaptcha() {
if(this.disabled){
uni.showToast({
title: '请耐心等待',
icon: 'none'
})
return
}
var that = this;
if (!that.receiptPhone) {
uni.showToast({
title: '请输入手机号',
icon: 'none'
})
return
}
sendSmsCaptcha({
phone: that.receiptPhone,
type: 'RECEIPT'
}).then(function (result) {
if(result){
if(result.code == 0) {
that.$success("发送短信验证码成功")
that.disabled = true
that.setInterValFunc()
} else {
that.$toast(result.message)
}
}
}).catch(err => {
console.log('err: ',err);
that.$toast(err.message)
})
},
setInterValFunc() {
this.time = 60;
this.text = '秒';
this.setTime = setInterval(() => {
if (this.time - 1 == 0) {
this.time = '';
this.text = '重新获取';
this.code = '';
this.disabled = false;
clearInterval(this.setTime);
} else {
this.disabled = true;
this.time--;
}
}, 1000);
},
// 重置
/* resetting(){
const app = this
const { orderId } = app
receipt({
orderId: orderId,
orderSourceData: '',
}).then(result => {
app.$success("重置成功")
app.orderSourceData = []
receiptPhone = ''
captcha = ''
emergentUser = ''
officeAddress = ''
homeAddress = ''
time = ''
text = '获取验证码'
app.getData()
})
}, */
// 表单提交
handleSubmit() {
const that = this
const app = this
const { orderId, orderSourceData, captcha, receiptPhone, emergentUser, officeAddress, homeAddress, idCode } = app
// 1) 校验 5 张图是否都已上传
const missingIndex = []
for (let i = 0; i < imageSlotLabels.length; i++) {
if (!orderSourceData[i]) {
missingIndex.push(i)
}
}
if (missingIndex.length > 0) {
// 优先提示最关键的那张(身份证正面)
const firstMissing = missingIndex.includes(0) ? 0 : missingIndex[0]
that.$toast(`请先上传${imageSlotLabels[firstMissing]}`, 2500)
return
}
// 2) 身份证号优先由后端 OCR 自动识别并写库;前端仅在用户手动填写时才随请求带上(避免空串覆盖后端结果)
const params = {
orderId: orderId,
orderSourceData: JSON.stringify(orderSourceData),
captcha: captcha,
receiptPhone: receiptPhone,
emergentUser: emergentUser,
officeAddress: officeAddress,
homeAddress: homeAddress
}
if (idCode) {
params.idCode = idCode
}
receiptOrder(params).then(result => {
if(result.code == 0) {
that.$success("确认收货成功!")
// uni.navigateBack()
setTimeout(()=>{
that.$navTo('pages/order/index',{}, 'redirectTo')
}, 1000)
} else {
that.$toast(formatErrorMsg(result.message), 2500)
}
}).catch(err => {
that.$toast(formatErrorMsg((err && err.message) || '提交失败,请稍后重试'), 2500)
})
},
// 上传图片
chooseImage(id) {
const app = this
const { orderId } = this
const type = 'photo' + id
// 选择图片
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success(chooseImageRes) {
const tempFilePaths = chooseImageRes.tempFilePaths;
const slotLabel = imageSlotLabels[id - 1] || '图片'
uni.showLoading({ title: '上传中...', mask: true })
uploadFile({
filePath: tempFilePaths[0],
fileType: 'image',
name: 'file'
}).then(res => {
console.log('res', res)
// app.orderSourceData.push(res.data.url)
if(app.orderSourceData[id-1]){
app.orderSourceData.splice(id-1, 1, res.data.url)
} else {
app.orderSourceData.push(res.data.url)
}
console.log('app.orderSourceData', app.orderSourceData)
app.$success(`${slotLabel}上传成功`)
}).catch(err => {
app.$toast(formatErrorMsg((err && (err.errMsg || err.message)) || `${slotLabel}上传失败,请重试`), 2500)
}).finally(() => {
uni.hideLoading()
})
}
});
},
}
}
</script>
<style>
page {
background: #f7f8fa;
}
</style>
<style lang="scss" scoped>
.page-title {
width: 94%;
margin: 0 auto;
padding-top: 40rpx;
font-size: 28rpx;
color: rgba(69, 90, 100, 0.6);
}
.form-wrapper {
margin: 20rpx auto 20rpx auto;
padding: 0 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
/* 上传图片(已上传时) */
.upload-img {
width: 300rpx;
height: 200rpx;
border-radius: 8rpx;
}
/* 上传占位符(未上传时) */
.upload-placeholder {
width: 300rpx;
height: 200rpx;
border: 2rpx dashed #c8c8c8;
border-radius: 8rpx;
background: #fafafa;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
.placeholder-icon {
font-size: 56rpx;
line-height: 1;
color: #c0c4cc;
}
.placeholder-text {
font-size: 22rpx;
margin-top: 10rpx;
color: #909399;
}
}
/* 底部操作栏 */
.footer {
margin-top: 80rpx;
.btn-wrapper {
height: 100%;
// display: flex;
// align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 86rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-wechat {
background: #0ba90b;
margin-bottom: 26rpx;
}
.btn-item-main {
background: linear-gradient(to right, $main-bg, $main-bg2);
// 禁用按钮
/* &.disabled {
opacity: 0.6;
} */
}
}
// 上传凭证
.row-voucher {
padding: 24rpx 20rpx;
.image-list {
padding: 0 20rpx;
margin-top: 20rpx;
margin-bottom: -20rpx;
&:after {
clear: both;
content: " ";
display: table;
}
.image {
display: block;
width: 100%;
height: 100%;
}
.image-picker,
.image-preview {
width: 184rpx;
height: 184rpx;
margin-right: 30rpx;
margin-bottom: 30rpx;
float: left;
&:nth-child(3n+0) {
margin-right: 0;
}
}
.image-picker {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1rpx dashed #ccc;
color: #ccc;
.choose-icon {
font-size: 48rpx;
margin-bottom: 6rpx;
}
.choose-text {
font-size: 24rpx;
}
}
.image-preview {
position: relative;
.image-delete {
position: absolute;
top: -15rpx;
right: -15rpx;
height: 42rpx;
width: 42rpx;
background: rgba(0, 0, 0, 0.64);
border-radius: 50%;
color: #fff;
font-weight: bolder;
font-size: 22rpx;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.form-license {
padding: 20rpx;
}
.pops-content {
padding: 20rpx;
}
.sfz,
.sfz1,
.sfz2,
.yyzz {
width: 140rpx;
padding-top: 20rpx;
padding-right: 10rpx;
}
.btn {
width: 700rpx;
margin: 20rpx auto;
height: 100rpx;
}
.reject {
width: 700rpx;
margin: 20rpx auto;
}
.captcha_input {
font-size: 28rpx;
font-weight: 400;
color: #CACACA;
}
.yanzhengma {
position: absolute;
right: 0;
top: -8rpx;
bottom: 0;
width: 140rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
background: #00BD87;
border-radius: 30rpx;
font-size: 22rpx;
font-family: PingFang SC;
font-weight: 500;
color: #FFFFFF;
}
.captcha-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rpx;
border-bottom: 3rpx solid #DDDDDD;
margin-bottom: 25rpx;
position: relative;
}
</style>