This commit is contained in:
oneao committed 2026-03-24 17:29:55 +08:00
1 parent 401a6719f6
commit b7f50f3ccb
22 files changed
+1595 -343

No files matched your search

@@ -1,24 +1,35 @@
<script lang="ts" setup>
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, recognizeOcrBoxApi } from '@/request/api'
import { downloadFile, formatFileSize, getFileExtension, imageCompress, isImage, uploadFileToAliyun } from '@/utils/file'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage, uploadFileToAliyun } from '@/utils/file'
const boxImage = ref('')
const boxImage = ref(null)
const ocrFinish = ref(false)
const isError = ref(false)
const ocrScore = ref('0.00')
const showResult = ref(false)
const form = ref({
b_id: 0,
boxNumber: '',
boxImageUrl: '',
containerNo: '',
containerImageUrl: '',
ocrScore: 0.00,
ocrNo: '',
})
const subFileList = ref([])
const otherImages = ref([])
const tipContainerNo = ref('')
onLoad(async () => {
await getUniqueIdApi().then((res) => {
if (res.success) {
form.value.b_id = res.data
}
})
})
function handleChooseBox() {
uni.chooseImage({
@@ -27,7 +38,7 @@ function handleChooseBox() {
if (Array.isArray(res.tempFiles) && res.tempFiles.length > 0) {
const tempFile: any = res.tempFiles[0]
boxImage.value = tempFile.path
boxImage.value = tempFile
toast.loading('OCR识别中,请稍等 . . .', {
overlay: true,
@@ -37,20 +48,21 @@ function handleChooseBox() {
imageCompress(tempFile.path).then((compressRes) => {
uploadFileToAliyun(compressRes.path, tempFile.name, 'image').then((res) => {
if (res.url) {
form.value.boxImageUrl = res.url
form.value.containerImageUrl = res.url
ocrFinish.value = false
recognizeOcrBoxApi({
fileUrl: form.value.boxImageUrl,
fileUrl: form.value.containerImageUrl,
}).then((ocrRes) => {
if (ocrRes.success) {
toast.success('识别成功,请检查箱号是否正确')
const score = Number(ocrRes?.data?.score ?? 0)
ocrScore.value = `${(Math.floor(score * 10000) / 100).toFixed(2)}%`
form.value.ocrScore = Math.floor(score * 100) / 100
form.value.boxNumber = ocrRes.data.boxNumber
form.value.containerNo = ocrRes.data.boxNumber
form.value.ocrNo = ocrRes.data.boxNumber
isError.value = false
}
@@ -66,12 +78,21 @@ function handleChooseBox() {
},
})
}
const computedOcrScore = computed(() => {
return `${(Math.floor(form.value.ocrScore * 100)).toFixed(0)}%`
})
function resetForm() {
boxImage.value = null
form.value.containerImageUrl = ''
form.value.containerNo = ''
form.value.ocrNo = ''
form.value.ocrScore = 0.00
}
function handleDeleteBoxImage() {
if (!form.value.boxImageUrl) {
boxImage.value = ''
form.value.boxImageUrl = ''
form.value.boxNumber = ''
if (!form.value.containerImageUrl) {
resetForm()
return
}
@@ -80,12 +101,9 @@ function handleDeleteBoxImage() {
transparent: false,
})
deleteAliyunFileApi(form.value.boxImageUrl).then((res) => {
deleteAliyunFileApi(form.value.containerImageUrl).then((res) => {
if (res.success) {
boxImage.value = ''
form.value.boxImageUrl = ''
form.value.boxNumber = ''
resetForm()
toast.success('删除成功')
}
})
@@ -115,8 +133,27 @@ function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
}
async function afterRead(fileItem: UploadFileItem) {
// 设置上传中状态
fileItem.status = 'uploading'
fileItem.message = '正在上传'
fileItem.isUpload = false
// 上传到阿里云
const res = await uploadFileToAliyun(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 上传完成
fileItem.status = 'done'
fileItem.message = '上传成功'
fileItem.url = res.url
subFileList.value = [...subFileList.value]
}
@@ -135,13 +172,11 @@ function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promis
}
function onRemove(index: number, item: UploadFileItem) {
console.log(item)
if (!item.isUpload) {
return
}
deleteFileApi(item.b_id).then((res) => {
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
@@ -149,144 +184,281 @@ function onRemove(index: number, item: UploadFileItem) {
}
async function handleConfirm() {
if (form.value.containerImageUrl === '') {
toast.fail('集装箱号图片不能为空')
return
}
if (form.value.containerNo === '') {
toast.fail('集装箱号不能为空')
return
}
if (!validateContainerNo(form.value.containerNo)) {
toast.fail('集装箱号不符合规则,请检查')
return
}
toast.loading('正在保存中 . . .', {
overlay: true,
transparent: false,
})
// 图片已经上传成功,保存主表即可
const mainTable = {
b_id: form.value.b_id,
b_container_no: form.value.containerNo,
b_container_img: form.value.containerImageUrl,
b_ocr_no: form.value.ocrNo,
b_ocr_score: form.value.ocrScore,
}
const fileInserts = []
const containerNo = form.value.containerNo
subFileList.value.forEach(async (fileItem, index) => {
// 上传到阿里云
const res = await uploadFileToAliyun(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
const uniqueRes = await getUniqueIdApi()
const indexNumber = index + 2
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 调用后端接口保存文件信息
const saveRes = await aliyunpluploadApi({
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: index,
mx_filename: fileItem.file.name,
mx_xh: indexNumber,
mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: res.url,
mx_mapfilename: fileItem.url,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
})
if (saveRes.success) {
fileItem.isUpload = true
fileItem.b_id = saveRes.data
const uniqueRes = await getUniqueIdApi()
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: 0,
mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
mx_filesize: boxImage.value.size,
mx_fileext: getFileExtension(boxImage.value.name),
mx_mapfilename: form.value.containerImageUrl,
mx_moduleid: 'b_container',
mx_cate_id: '0',
})
const reqData = [
{
tableName: 'b_container',
keyField: 'b_id',
inserts: [mainTable],
updates: [],
deletes: [],
},
{
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
deletes: [],
},
]
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
tipContainerNo.value = form.value.containerNo
showResult.value = true
}
})
}
function buildFileName(containerNo, index, fileName) {
const formattedIndex = String(index).padStart(2, '0')
return `${containerNo}_${formattedIndex}.${fileName.split('.').pop()}`
}
// 集装箱号校验
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) {
boxImage.value = null
ocrFinish.value = false
isError.value = false
resetForm()
form.value.b_id = idRes.data
subFileList.value = []
showResult.value = false
tipContainerNo.value = ''
}
}
</script>
<template>
<view class="g3-container">
<view class="g3-content">
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
1、集装箱号识别(OCR自动识别)
</sar-tag>
<template v-if="!showResult">
<view class="g3-content">
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
1、集装箱号识别(OCR自动识别)
</sar-tag>
</view>
<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="image" size="56rpx" color="#999" />
<text class="placeholder-text">
上传箱号图片,自动识别箱号
</text>
</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.containerNo"
placeholder="请输入箱号"
/>
</view>
</view>
</view>
<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>
准确率:{{ ocrScore }}
</view>
</sar-alert>
<sar-alert
v-else
type="danger"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别失败,请检查手动填写集装箱号
</sar-alert>
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
2、其它照片录入
</sar-tag>
</view>
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="image" size="56rpx" color="#999" />
<text class="placeholder-text">
上传箱号图片,自动识别箱号
</text>
</view>
<view v-else class="box-image">
<wd-img
:src="boxImage"
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>集装箱号</view>
<view class="format">
格式: 4字母 + 7数字
</view>
</view>
<sar-input
v-model="form.boxNumber"
placeholder="请输入箱号"
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</view>
</view>
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
2、其它照片录入
</sar-tag>
</view>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
<view class="g3-footer">
<view class="footer-main">
<view class="submit-btn" @click="handleConfirm">
确认上传
</view>
</view>
</view>
</view>
</template>
<view class="g3-footer">
<view class="footer-main">
<view class="submit-btn" @click="handleConfirm">
确认上传
</view>
</view>
<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="width: 360rpx;font-size: 38rpx;" @click="continueAdd">
继续录入
</sar-button>
</sar-result>
</view>
</view>
</template>
<style lang="scss" scoped>
.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;
@@ -386,6 +558,18 @@ async function handleConfirm() {
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;
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { deleteFileApi } from '@/request/api'
import { useUserStore } from '@/store'
const userStore = useUserStore()
@@ -28,6 +29,16 @@ function toLinkUrl(url) {
url,
})
}
function deleteAliyunFiles() {
const subids = [
787154320330821,
787154320388165,
]
subids.forEach((subid) => {
deleteFileApi(subid)
})
}
</script>
<template>
@@ -75,6 +86,10 @@ function toLinkUrl(url) {
</view>
</sar-grid-item>
</sar-grid>
<sar-button @click="deleteAliyunFiles">
删除附件
</sar-button>
</view>
<sar-result v-else status="warning" title="尚未登陆" style="margin-top: 32rpx;">
<sar-button
@@ -42,9 +42,9 @@ export function recognizeOcrBoxApi(params: OcrBoxRequest) {
return httpPost(`/ocr/box`, params)
}
export function deleteFileApi(id) {
export function deleteFileApi(subid) {
return httpPost(`file/delete`, {
id,
subid,
tableName: 'bf_file',
bucketName: aliyunBucket,
})
@@ -56,3 +56,19 @@ export function deleteAliyunFileApi(fileUrl) {
mx_mapfilename: fileUrl,
})
}
interface SaveObjectParams {
tableName: string
keyField: string
inserts: any[]
updates: any[]
deletes: any[]
}
export function saveObjectApi(params: SaveObjectParams[]) {
return httpPost('/data/saveObject', params)
}
export function getUniqueIdApi() {
return httpPost<number>('/data/getUniqueId')
}