|
@@ -30,6 +30,22 @@
|
|
|
<van-cell title="当前仓库" :value="warehouse" />
|
|
<van-cell title="当前仓库" :value="warehouse" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 快递单号输入框 -->
|
|
|
|
|
+ <div class="express-input-box">
|
|
|
|
|
+ <div class="express-input-text">
|
|
|
|
|
+ <div>快递单号</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <van-field
|
|
|
|
|
+ class="express-input"
|
|
|
|
|
+ ref="expressNoRef"
|
|
|
|
|
+ :style="expressNo!==''?'border: 2px solid #07c160':''"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ v-model="expressNo"
|
|
|
|
|
+ placeholder="识别完成后自动填充"
|
|
|
|
|
+ readonly
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<!-- 拍照上传区域 -->
|
|
<!-- 拍照上传区域 -->
|
|
|
<div class="upload-section">
|
|
<div class="upload-section">
|
|
|
<div class="upload-tips">
|
|
<div class="upload-tips">
|
|
@@ -75,9 +91,10 @@
|
|
|
import { ref, onMounted } from 'vue'
|
|
import { ref, onMounted } from 'vue'
|
|
|
import { showNotify, showFailToast, showLoadingToast, closeToast } from 'vant'
|
|
import { showNotify, showFailToast, showLoadingToast, closeToast } from 'vant'
|
|
|
import { uploadOCRImage } from '@/api/inbound/index'
|
|
import { uploadOCRImage } from '@/api/inbound/index'
|
|
|
-import { getHeader, goBack } from '@/utils/android'
|
|
|
|
|
|
|
+import { getHeader, goBack, scanSuccess, scanError } from '@/utils/android'
|
|
|
import { compressImage } from '@/utils/imageCompression'
|
|
import { compressImage } from '@/utils/imageCompression'
|
|
|
import { useStore } from '@/store/modules/user'
|
|
import { useStore } from '@/store/modules/user'
|
|
|
|
|
+import Quagga from '@ericblade/quagga2'
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
getHeader()
|
|
getHeader()
|
|
@@ -90,9 +107,12 @@ const warehouse = store.warehouse
|
|
|
|
|
|
|
|
const uploadImages = ref([])
|
|
const uploadImages = ref([])
|
|
|
const uploading = ref(false)
|
|
const uploading = ref(false)
|
|
|
|
|
+const expressNo = ref('') // 快递单号
|
|
|
|
|
+const expressNoRef = ref(null)
|
|
|
|
|
+const recognizing = ref(false) // 条码识别中
|
|
|
|
|
|
|
|
// 自动上传图片
|
|
// 自动上传图片
|
|
|
-const autoUploadImage = async (file) => {
|
|
|
|
|
|
|
+const autoUploadImage = async (file, barcode = null) => {
|
|
|
if (!warehouse) {
|
|
if (!warehouse) {
|
|
|
showFailToast('未获取到仓库信息')
|
|
showFailToast('未获取到仓库信息')
|
|
|
return
|
|
return
|
|
@@ -108,13 +128,13 @@ const autoUploadImage = async (file) => {
|
|
|
const toast = showLoadingToast('自动上传识别中...')
|
|
const toast = showLoadingToast('自动上传识别中...')
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const response = await uploadOCRImage(file, warehouse)
|
|
|
|
|
|
|
+ const response = await uploadOCRImage(file, warehouse, barcode)
|
|
|
|
|
|
|
|
closeToast()
|
|
closeToast()
|
|
|
|
|
|
|
|
if (response.code === 200) {
|
|
if (response.code === 200) {
|
|
|
showNotify({ type: 'success', message: '面单上传成功' })
|
|
showNotify({ type: 'success', message: '面单上传成功' })
|
|
|
- // 上传成功后重置表单
|
|
|
|
|
|
|
+ // 上传成功后重置表单(但保留快递单号)
|
|
|
uploadImages.value = []
|
|
uploadImages.value = []
|
|
|
} else {
|
|
} else {
|
|
|
showNotify({ type: 'danger', message: response.message || '上传失败' })
|
|
showNotify({ type: 'danger', message: response.message || '上传失败' })
|
|
@@ -149,6 +169,112 @@ const beforeReadImage = (file) => {
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 使用 Quagga2 识别图片中的条码(Code128)
|
|
|
|
|
+// 注意:decodeSingle 只能识别一个条码,如果图片中有多个条码,会识别到第一个找到的
|
|
|
|
|
+const recognizeBarcode = async (file) => {
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ recognizing.value = true
|
|
|
|
|
+ const imageFile = file
|
|
|
|
|
+
|
|
|
|
|
+ // 创建图片对象
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ const url = URL.createObjectURL(imageFile)
|
|
|
|
|
+
|
|
|
|
|
+ img.onload = () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 创建canvas
|
|
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
|
|
+ canvas.width = img.width
|
|
|
|
|
+ canvas.height = img.height
|
|
|
|
|
+ ctx.drawImage(img, 0, 0)
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 Quagga2 识别条码(优先识别 Code128 格式)
|
|
|
|
|
+ Quagga.decodeSingle(
|
|
|
|
|
+ {
|
|
|
|
|
+ decoder: {
|
|
|
|
|
+ readers: ['code_128_reader'] // 优先识别 Code128 格式(快递单号通常是 Code128)
|
|
|
|
|
+ },
|
|
|
|
|
+ locate: true,
|
|
|
|
|
+ src: canvas.toDataURL(),
|
|
|
|
|
+ numOfWorkers: 0 // 不使用 Web Workers,避免兼容性问题
|
|
|
|
|
+ },
|
|
|
|
|
+ (result) => {
|
|
|
|
|
+ if (result && result.codeResult && result.codeResult.code) {
|
|
|
|
|
+ const barcodeText = result.codeResult.code
|
|
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
|
|
+ recognizing.value = false
|
|
|
|
|
+ console.log('识别到条码(Code128):', barcodeText)
|
|
|
|
|
+ scanSuccess()
|
|
|
|
|
+ resolve(barcodeText)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果 Code128 失败,尝试所有格式
|
|
|
|
|
+ Quagga.decodeSingle(
|
|
|
|
|
+ {
|
|
|
|
|
+ decoder: {
|
|
|
|
|
+ readers: [
|
|
|
|
|
+ 'code_128_reader',
|
|
|
|
|
+ 'ean_reader',
|
|
|
|
|
+ 'ean_8_reader',
|
|
|
|
|
+ 'code_39_reader',
|
|
|
|
|
+ 'code_39_vin_reader',
|
|
|
|
|
+ 'codabar_reader',
|
|
|
|
|
+ 'upc_reader',
|
|
|
|
|
+ 'upc_e_reader',
|
|
|
|
|
+ 'i2of5_reader'
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ locate: true,
|
|
|
|
|
+ src: canvas.toDataURL(),
|
|
|
|
|
+ numOfWorkers: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ (result) => {
|
|
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
|
|
+ recognizing.value = false
|
|
|
|
|
+
|
|
|
|
|
+ if (result && result.codeResult && result.codeResult.code) {
|
|
|
|
|
+ const barcodeText = result.codeResult.code
|
|
|
|
|
+ console.log('识别到条码(其他格式):', barcodeText, '格式:', result.codeResult.format)
|
|
|
|
|
+ scanSuccess()
|
|
|
|
|
+ resolve(barcodeText)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('条码识别失败')
|
|
|
|
|
+ scanError()
|
|
|
|
|
+ resolve(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
|
|
+ recognizing.value = false
|
|
|
|
|
+ console.error('条码识别过程出错:', error)
|
|
|
|
|
+ scanError()
|
|
|
|
|
+ resolve(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ img.onerror = () => {
|
|
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
|
|
+ recognizing.value = false
|
|
|
|
|
+ console.error('图片加载失败')
|
|
|
|
|
+ scanError()
|
|
|
|
|
+ resolve(null)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ img.src = url
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ recognizing.value = false
|
|
|
|
|
+ console.error('条码识别初始化失败:', error)
|
|
|
|
|
+ scanError()
|
|
|
|
|
+ resolve(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 图片读取完成后处理
|
|
// 图片读取完成后处理
|
|
|
const afterReadImage = async (file) => {
|
|
const afterReadImage = async (file) => {
|
|
|
try {
|
|
try {
|
|
@@ -162,6 +288,26 @@ const afterReadImage = async (file) => {
|
|
|
if (originalFile) {
|
|
if (originalFile) {
|
|
|
let finalFile = originalFile
|
|
let finalFile = originalFile
|
|
|
|
|
|
|
|
|
|
+ // 先尝试识别条码
|
|
|
|
|
+ showNotify({
|
|
|
|
|
+ type: 'primary',
|
|
|
|
|
+ message: '正在识别条码...'
|
|
|
|
|
+ })
|
|
|
|
|
+ const barcodeResult = await recognizeBarcode(originalFile)
|
|
|
|
|
+ if (barcodeResult) {
|
|
|
|
|
+ expressNo.value = barcodeResult
|
|
|
|
|
+ showNotify({
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: `识别到快递单号: ${barcodeResult}`
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ expressNo.value = ''
|
|
|
|
|
+ showNotify({
|
|
|
|
|
+ type: 'warning',
|
|
|
|
|
+ message: '未识别到条码,请确保图片清晰'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 检查文件大小,如果大于1MB则进行压缩
|
|
// 检查文件大小,如果大于1MB则进行压缩
|
|
|
if (originalFile.size > 1 * 1024 * 1024) {
|
|
if (originalFile.size > 1 * 1024 * 1024) {
|
|
|
showNotify({
|
|
showNotify({
|
|
@@ -196,8 +342,8 @@ const afterReadImage = async (file) => {
|
|
|
console.log(`图片大小符合要求,无需压缩: ${(originalFile.size / 1024).toFixed(2)}KB`)
|
|
console.log(`图片大小符合要求,无需压缩: ${(originalFile.size / 1024).toFixed(2)}KB`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 压缩完成后自动上传
|
|
|
|
|
- await autoUploadImage(finalFile)
|
|
|
|
|
|
|
+ // 压缩完成后自动上传(带上识别到的条码)
|
|
|
|
|
+ await autoUploadImage(finalFile, expressNo.value || null)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -236,7 +382,7 @@ const submitOCR = async () => {
|
|
|
formData.append('file', image.file)
|
|
formData.append('file', image.file)
|
|
|
formData.append('warehouse', warehouse)
|
|
formData.append('warehouse', warehouse)
|
|
|
|
|
|
|
|
- const response = await uploadOCRImage(image.file, warehouse)
|
|
|
|
|
|
|
+ const response = await uploadOCRImage(image.file, warehouse, expressNo.value || undefined)
|
|
|
|
|
|
|
|
closeToast()
|
|
closeToast()
|
|
|
|
|
|
|
@@ -290,6 +436,24 @@ onMounted(() => {
|
|
|
border-radius: 8px
|
|
border-radius: 8px
|
|
|
overflow: hidden
|
|
overflow: hidden
|
|
|
|
|
|
|
|
|
|
+.express-input-box
|
|
|
|
|
+ margin-bottom: 20px
|
|
|
|
|
+ padding: 10px 5px
|
|
|
|
|
+ .express-input-text
|
|
|
|
|
+ display: flex
|
|
|
|
|
+ justify-content: space-between
|
|
|
|
|
+ align-items: center
|
|
|
|
|
+ font-size: 18px
|
|
|
|
|
+ font-weight: bold
|
|
|
|
|
+ margin: 10px 0
|
|
|
|
|
+ padding: 5px 0
|
|
|
|
|
+ .express-input
|
|
|
|
|
+ background: #eff0f2
|
|
|
|
|
+ padding: 10px 20px
|
|
|
|
|
+ font-size: 20px
|
|
|
|
|
+ border: 2px solid #0077ff
|
|
|
|
|
+ font-weight: 500
|
|
|
|
|
+
|
|
|
.upload-section
|
|
.upload-section
|
|
|
background: #fff
|
|
background: #fff
|
|
|
padding: 20px
|
|
padding: 20px
|