Files
yunxinwei-uniapp/pages/order/delivery.vue
赵忠林 6abb49c911 feat(delivery): 优化身份证及凭证图片上传和提交校验
- 在提交前新增必填图片校验,确保身份证正反面及其他凭证全部上传
- 优化图片上传占位样式,新增上传提示文案
- uploadImage函数增加上传中loading提示及成功失败反馈toast
- 格式化后端错误提示,过滤错误码尾巴,提升用户体验
- 明确图片字段标签,标注为“身份证正面(人像面)”等,增强用户指引
2026-07-26 14:09:31 +08:00

588 lines
14 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="请输入家庭地址" />
</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']
}],
}
// 图片槽位说明(与 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: '',
// 按钮禁用
// 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 } = 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
}
receiptOrder({
orderId: orderId,
orderSourceData: JSON.stringify(orderSourceData),
captcha: captcha,
receiptPhone: receiptPhone,
emergentUser: emergentUser,
officeAddress: officeAddress,
homeAddress: homeAddress
}).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>