Files
workspace/code/container-ocr/ocr-uniapp/src/pages-sub/upload/index.vue
T
2026-05-19 17:33:26 +08:00

728 lines
17 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.
<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>