728 lines
17 KiB
Vue
728 lines
17 KiB
Vue
<script lang="ts" setup>
|
||
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
||
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
|
||
import { getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadContainerFileApi, uploadTempFileApi } from '@/request/api'
|
||
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
|
||
|
||
const boxImage = ref(null)
|
||
|
||
const ocrFinish = ref(false)
|
||
const isError = ref(false)
|
||
|
||
const showResult = ref(false)
|
||
|
||
const form = ref({
|
||
b_id: 0,
|
||
b_container_no: '',
|
||
b_container_img: '',
|
||
ocrScore: 0.00,
|
||
ocrNo: '',
|
||
})
|
||
|
||
const subFileListRef = ref()
|
||
const subFileListRef2 = ref()
|
||
|
||
const subFileList = ref([]) // 装货照片
|
||
const subFileList2 = ref([]) // 发货单、签收单、过磅单
|
||
|
||
const tipContainerNo = ref('')
|
||
|
||
onLoad(async () => {
|
||
toast.loading('正在初始化中...', {
|
||
overlay: true,
|
||
})
|
||
|
||
await getUniqueIdApi().then((res) => {
|
||
if (res.success) {
|
||
form.value.b_id = res.data
|
||
}
|
||
})
|
||
|
||
toast.hide()
|
||
})
|
||
|
||
function handleChooseBox() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
success: (res) => {
|
||
if (Array.isArray(res.tempFiles) && res.tempFiles.length > 0) {
|
||
const tempFile: any = res.tempFiles[0]
|
||
|
||
boxImage.value = tempFile
|
||
|
||
toast.loading('OCR识别中,请稍等 . . .', {
|
||
overlay: true,
|
||
transparent: false,
|
||
})
|
||
|
||
imageCompress(tempFile.path).then((compressRes) => {
|
||
form.value.b_container_img = compressRes.path
|
||
const fileName = tempFile.name || `image_${Date.now()}.jpg`
|
||
boxImage.value = {
|
||
...tempFile,
|
||
name: fileName,
|
||
path: compressRes.path,
|
||
size: compressRes.size,
|
||
}
|
||
|
||
uploadTempFileApi(compressRes.path, {
|
||
fileName,
|
||
}).then((res) => {
|
||
if (res.success) {
|
||
recognizeOcrBoxApi({
|
||
fileUrl: res.data.url,
|
||
}).then((ocrRes) => {
|
||
if (ocrRes.success) {
|
||
toast.success('识别成功,请检查箱号是否正确')
|
||
|
||
const score = Number(ocrRes?.data?.score ?? 0)
|
||
form.value.ocrScore = Math.floor(score * 100) / 100
|
||
|
||
form.value.b_container_no = ocrRes.data.boxNumber
|
||
form.value.ocrNo = ocrRes.data.boxNumber
|
||
|
||
isError.value = false
|
||
}
|
||
}).catch(() => {
|
||
isError.value = true
|
||
}).finally(() => {
|
||
ocrFinish.value = true
|
||
})
|
||
}
|
||
})
|
||
})
|
||
}
|
||
},
|
||
})
|
||
}
|
||
const computedOcrScore = computed(() => {
|
||
return `${(Math.floor(form.value.ocrScore * 100)).toFixed(0)}%`
|
||
})
|
||
|
||
function resetForm() {
|
||
boxImage.value = null
|
||
form.value.b_container_no = ''
|
||
form.value.b_container_img = ''
|
||
form.value.ocrNo = ''
|
||
form.value.ocrScore = 0.00
|
||
|
||
showResult.value = false
|
||
isError.value = false
|
||
ocrFinish.value = false
|
||
subFileList.value = []
|
||
subFileList2.value = []
|
||
}
|
||
|
||
function handleDeleteBoxImage() {
|
||
resetForm()
|
||
}
|
||
|
||
function handleBeforeRemove({ file, fileList, resolve }) {
|
||
dialog.confirm({
|
||
title: '提示',
|
||
message: '确定删除该文件吗?',
|
||
onConfirm: () => {
|
||
resolve(true)
|
||
},
|
||
onCancel: () => {
|
||
resolve(false)
|
||
},
|
||
})
|
||
}
|
||
|
||
async function handleConfirm() {
|
||
if (!boxImage.value) {
|
||
toast.fail('集装箱号图片不能为空')
|
||
return
|
||
}
|
||
|
||
if (!form.value.b_container_no) {
|
||
toast.fail('集装箱号不能为空')
|
||
return
|
||
}
|
||
|
||
if (!validateContainerNo(form.value.b_container_no)) {
|
||
toast.fail('集装箱号不符合规则,请检查')
|
||
return
|
||
}
|
||
|
||
toast.loading('正在保存中 . . .', {
|
||
overlay: true,
|
||
transparent: false,
|
||
})
|
||
|
||
try {
|
||
const mainTable = {
|
||
b_id: form.value.b_id,
|
||
b_container_no: form.value.b_container_no,
|
||
b_container_img: form.value.b_container_img,
|
||
b_ocr_no: form.value.ocrNo,
|
||
b_ocr_score: form.value.ocrScore || 0.00,
|
||
}
|
||
|
||
const fileInserts = []
|
||
|
||
// =========================
|
||
// 上传主图
|
||
// =========================
|
||
const mainRes = await uploadContainerFileApi(
|
||
boxImage.value.path,
|
||
{
|
||
jobId: form.value.b_id,
|
||
containerNo: form.value.b_container_no,
|
||
fileName: boxImage.value.name,
|
||
},
|
||
)
|
||
|
||
if (mainRes.success) {
|
||
mainTable.b_container_img = mainRes.data.path
|
||
|
||
fileInserts.push({
|
||
subid: mainRes.data.uid,
|
||
father: form.value.b_id,
|
||
mx_xh: 0,
|
||
mx_filename: mainRes.data.fileName,
|
||
mx_filesize: boxImage.value.size,
|
||
mx_fileext: getFileExtension(boxImage.value.name),
|
||
mx_mapfilename: mainRes.data.path,
|
||
mx_moduleid: 'b_container',
|
||
mx_cate_id: '0',
|
||
})
|
||
}
|
||
|
||
// =========================
|
||
// 附件排序
|
||
// =========================
|
||
const indexMap = {}
|
||
|
||
const getNextIndex = (cate) => {
|
||
if (!indexMap[cate]) {
|
||
indexMap[cate] = 1
|
||
}
|
||
|
||
return indexMap[cate]++
|
||
}
|
||
|
||
// =========================
|
||
// 获取附件
|
||
// =========================
|
||
const subFiles = subFileListRef.value?.getUploadFiles() || []
|
||
const subFiles2 = subFileListRef2.value?.getUploadFiles() || []
|
||
|
||
const allFiles = [
|
||
...subFiles.map(item => ({
|
||
...item,
|
||
cate: '1',
|
||
})),
|
||
...subFiles2.map(item => ({
|
||
...item,
|
||
cate: '2',
|
||
})),
|
||
]
|
||
|
||
// =========================
|
||
// 上传附件
|
||
// =========================
|
||
for (const fileItem of allFiles) {
|
||
try {
|
||
let uploadPath = fileItem.url
|
||
let uploadSize = fileItem.size
|
||
const fileName = fileItem.name
|
||
|
||
// 图片压缩
|
||
if (isImage(fileName)) {
|
||
const compressRes = await imageCompress(uploadPath)
|
||
uploadPath = compressRes.path
|
||
uploadSize = compressRes.size
|
||
}
|
||
|
||
const res = await uploadContainerFileApi(uploadPath, {
|
||
jobId: form.value.b_id,
|
||
containerNo: form.value.b_container_no,
|
||
fileName,
|
||
})
|
||
|
||
if (!res.success) {
|
||
continue
|
||
}
|
||
|
||
fileInserts.push({
|
||
subid: res.data.uid,
|
||
father: form.value.b_id,
|
||
mx_xh: getNextIndex(fileItem.cate),
|
||
mx_filename: res.data.fileName,
|
||
mx_filesize: uploadSize,
|
||
mx_fileext: getFileExtension(fileName),
|
||
mx_mapfilename: res.data.path,
|
||
mx_moduleid: 'b_container',
|
||
mx_cate_id: fileItem.cate,
|
||
})
|
||
}
|
||
catch (err) {
|
||
console.error('附件上传失败:', fileItem, err)
|
||
}
|
||
}
|
||
|
||
// =========================
|
||
// 保存数据
|
||
// =========================
|
||
const reqData = [
|
||
{
|
||
tableName: 'b_container',
|
||
keyField: 'b_id',
|
||
inserts: [mainTable],
|
||
updates: [],
|
||
deletes: [],
|
||
},
|
||
{
|
||
tableName: 'bf_file',
|
||
keyField: 'subid',
|
||
inserts: fileInserts,
|
||
updates: [],
|
||
deletes: [],
|
||
},
|
||
]
|
||
|
||
const saveRes = await saveObjectApi(reqData)
|
||
|
||
if (saveRes.success) {
|
||
toast.success('保存成功')
|
||
|
||
tipContainerNo.value = form.value.b_container_no
|
||
showResult.value = true
|
||
}
|
||
else {
|
||
toast.fail('保存失败')
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.error(err)
|
||
toast.fail('保存失败')
|
||
}
|
||
finally {
|
||
toast.hide()
|
||
}
|
||
}
|
||
|
||
// 集装箱号校验
|
||
function validateContainerNo(containerNo) {
|
||
// 正则表达式: 4个字母 + 7个数字
|
||
const pattern = /^[A-Z]{4}\d{7}$/i
|
||
|
||
// 使用正则进行匹配
|
||
if (pattern.test(containerNo)) {
|
||
return true
|
||
}
|
||
else {
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function continueAdd() {
|
||
const idRes = await getUniqueIdApi()
|
||
|
||
if (idRes.success) {
|
||
resetForm()
|
||
form.value.b_id = idRes.data
|
||
tipContainerNo.value = ''
|
||
}
|
||
}
|
||
|
||
const templateCollapse = ref<string[]>(['template1'])
|
||
</script>
|
||
|
||
<template>
|
||
<view class="g3-container">
|
||
<template v-if="!showResult">
|
||
<view class="g3-content">
|
||
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
|
||
<template #title>
|
||
<view style="display:flex;align-items: center;">
|
||
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||
<text>
|
||
箱门照(OCR识别)
|
||
</text>
|
||
</view>
|
||
</template>
|
||
<view class="step-content box">
|
||
<view v-if="ocrFinish" class="header">
|
||
<sar-alert
|
||
v-if="!isError"
|
||
type="success"
|
||
show-icon
|
||
closable
|
||
:root-style="{
|
||
width: '100%',
|
||
}"
|
||
>
|
||
识别成功,请检查箱号是否正确!
|
||
<view>
|
||
准确率:{{ computedOcrScore }}
|
||
</view>
|
||
</sar-alert>
|
||
<sar-alert
|
||
v-else
|
||
type="danger"
|
||
show-icon
|
||
closable
|
||
:root-style="{
|
||
width: '100%',
|
||
}"
|
||
>
|
||
识别失败,请检查手动填写集装箱号
|
||
</sar-alert>
|
||
</view>
|
||
|
||
<view class="box-main">
|
||
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
|
||
<sar-icon name="plus" size="156rpx" color="#999" />
|
||
</view>
|
||
|
||
<view v-else class="box-image">
|
||
<wd-img
|
||
:src="boxImage.path"
|
||
width="100%"
|
||
height="100%"
|
||
:enable-preview="true"
|
||
/>
|
||
|
||
<view class="delete-btn" @click="handleDeleteBoxImage()">
|
||
<sar-icon name="close" size="20rpx" color="#fff" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="box-input">
|
||
<view class="header">
|
||
<view class="label-container">
|
||
<view>集装箱号</view>
|
||
<view class="required-asterisk">
|
||
*
|
||
</view>
|
||
</view>
|
||
<view class="format">
|
||
格式: 4字母 + 7数字
|
||
</view>
|
||
</view>
|
||
<sar-input
|
||
v-model="form.b_container_no"
|
||
placeholder="请输入箱号"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</wd-cell-group>
|
||
|
||
<wd-collapse v-model="templateCollapse" style="margin-top: 32rpx;">
|
||
<wd-collapse-item name="template1">
|
||
<template #title>
|
||
<view class="template-header">
|
||
<view class="title">
|
||
<sar-icon
|
||
name="file"
|
||
size="38rpx"
|
||
style="margin-right: 16rpx;"
|
||
/>
|
||
<text>
|
||
上传模板(参考)
|
||
</text>
|
||
</view>
|
||
<sar-icon
|
||
:name="templateCollapse.includes('template1') ? 'up' : 'down'"
|
||
size="38rpx"
|
||
/>
|
||
</view>
|
||
</template>
|
||
<view class="template-container">
|
||
<view class="item">
|
||
<text class="title">
|
||
1、提空箱拍照模版
|
||
</text>
|
||
|
||
<wd-img
|
||
style="width:100%;height:400rpx;"
|
||
src="/static/images/D21304CD0CDAB09EE3349E8B7D078CC9.jpg"
|
||
:enable-preview="true"
|
||
@click.stop
|
||
/>
|
||
</view>
|
||
|
||
<view class="item" style="margin-left:16rpx">
|
||
<text class="title">
|
||
2、装箱拍照模版
|
||
</text>
|
||
|
||
<wd-img
|
||
style="width:100%;height:400rpx;"
|
||
src="/static/images/027C52B0A3986905F161127336244D72.jpg"
|
||
:enable-preview="true"
|
||
@click.stop
|
||
/>
|
||
</view>
|
||
</view>
|
||
</wd-collapse-item>
|
||
</wd-collapse>
|
||
|
||
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
|
||
<template #title>
|
||
<view style="display:flex;align-items: center;">
|
||
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||
<text>
|
||
装货照片
|
||
</text>
|
||
</view>
|
||
</template>
|
||
<view class="step-content">
|
||
<wd-upload
|
||
ref="subFileListRef"
|
||
v-model:file-list="subFileList"
|
||
:auto-upload="false"
|
||
:before-remove="handleBeforeRemove"
|
||
multiple
|
||
accept="all"
|
||
:max-size="50 * 1024 * 1024"
|
||
/>
|
||
</view>
|
||
</wd-cell-group>
|
||
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
|
||
<template #title>
|
||
<view style="display:flex;align-items: center;">
|
||
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||
<text>
|
||
发货单、签收单、过磅单
|
||
</text>
|
||
</view>
|
||
</template>
|
||
<view class="step-content">
|
||
<wd-upload
|
||
ref="subFileListRef2"
|
||
v-model:file-list="subFileList2"
|
||
:auto-upload="false"
|
||
:before-remove="handleBeforeRemove"
|
||
multiple
|
||
accept="all"
|
||
:max-size="50 * 1024 * 1024"
|
||
/>
|
||
</view>
|
||
</wd-cell-group>
|
||
</view>
|
||
<view class="g3-footer">
|
||
<view class="footer-main">
|
||
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
|
||
上传
|
||
</sar-button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<view v-else class="g3-result">
|
||
<sar-result status="success" title="操作成功" description="已成功录入箱号 为 xxx 的记录">
|
||
<template #description>
|
||
<view class="result-tip">
|
||
已成功录入箱号
|
||
<text class="container-no">
|
||
"{{ tipContainerNo }}"
|
||
</text>
|
||
</view>
|
||
</template>
|
||
<sar-button long style="font-size: 38rpx;" @click="continueAdd">
|
||
继续录入
|
||
</sar-button>
|
||
</sar-result>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
page {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.g3-result {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
.result-tip {
|
||
font-size: 36rpx;
|
||
}
|
||
|
||
.container-no {
|
||
font-weight: 600;
|
||
color: #4caf50;
|
||
}
|
||
}
|
||
|
||
.g3-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
|
||
.g3-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16rpx 0rpx;
|
||
}
|
||
|
||
.g3-footer {
|
||
width: 100%;
|
||
|
||
.footer-main {
|
||
padding: 16rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.step {
|
||
margin-bottom: 32rpx;
|
||
|
||
.step-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
}
|
||
|
||
.step-content {
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
.box-main {
|
||
height: 326rpx;
|
||
}
|
||
|
||
.box-placeholder {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
background: var(--sar-upload-select-bg);
|
||
border-radius: var(--sar-upload-select-border-radius);
|
||
|
||
.placeholder-text {
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
}
|
||
|
||
.box-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 12rpx;
|
||
right: 12rpx;
|
||
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
z-index: 10;
|
||
}
|
||
}
|
||
|
||
.box-input {
|
||
margin-top: 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 28rpx;
|
||
margin-bottom: 12rpx;
|
||
width: 100%;
|
||
.label-container {
|
||
position: relative;
|
||
}
|
||
|
||
.required-asterisk {
|
||
position: absolute;
|
||
top: -5rpx;
|
||
right: -12rpx;
|
||
color: red;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.format {
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.tip {
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.submit-btn {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
border-radius: 999px;
|
||
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, #3a6ee8, #5c8df6);
|
||
box-shadow: 0 6rpx 12rpx rgba(58, 110, 232, 0.18);
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.submit-btn:active {
|
||
transform: scale(0.98);
|
||
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
|
||
}
|
||
|
||
.template-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
font-weight: 500;
|
||
.title {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.template-container {
|
||
display: flex;
|
||
|
||
.item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 50%;
|
||
|
||
.title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
padding-left: 10rpx;
|
||
border-left: 6rpx solid var(--sar-primary);
|
||
}
|
||
}
|
||
}
|
||
</style>
|