@@ -1,7 +1,7 @@
< script setup lang = "ts" >
import { computed , onBeforeUnmount , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { AlertTriangle , Image , RefreshCw , Ruler , Save , Send } from '@lucide/vue'
import { ElMessage , ElMessageBox , type UploadFile } from 'element-plus'
import { AlertTriangle , Image , Plus , RefreshCw , Ruler , Save , Send , Upload } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -9,7 +9,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { inboundProcessStatusLabels , inboundStatusLabel } from '../status'
import { downloadInboundMeasurementImage , getInboundMeasurement , submitInboundMeasurement , updateConfirmedPackageCount } from './api'
import { createManualCartonScan , downloadInboundMeasurementImage , getInboundMeasurement , submitInboundMeasurement , updateConfirmedPackageCount } from './api'
import type {
InboundMeasurementDetail ,
InboundMeasurementItem ,
@@ -42,10 +42,25 @@ interface MeasurementFormModel {
items : EditableMeasurementItem [ ]
}
interface ManualCartonFormModel {
boxCode : string
length : number | null
width : number | null
height : number | null
weight : number | null
scannedShipmentId : string
machineNo : string
measuredAt : string
}
const access = useAccessPreviewStore ( )
const loading = ref ( false )
const saving = ref ( false )
const submitting = ref ( false )
const manualEntryOpen = ref ( false )
const manualEntrySubmitting = ref ( false )
const manualEntryImage = ref < File | null > ( null )
const manualEntryErrors = ref < string [ ] > ( [ ] )
const detail = ref < InboundMeasurementDetail | null > ( null )
const loadError = ref < ApiErrorDetails | null > ( null )
const operationError = ref < ApiErrorDetails | null > ( null )
@@ -69,6 +84,17 @@ const form = reactive<MeasurementFormModel>({
items : [ ] ,
} )
const manualForm = reactive < ManualCartonFormModel > ( {
boxCode : '' ,
length : null ,
width : null ,
height : null ,
weight : null ,
scannedShipmentId : '' ,
machineNo : 'MANUAL-ADMIN' ,
measuredAt : '' ,
} )
const canView = computed ( ( ) => access . can ( 'platform:firstleg:inbound:measure:view' ) )
const canEdit = computed ( ( ) => access . can ( 'platform:firstleg:inbound:measure:edit' ) )
const canSubmit = computed ( ( ) => access . can ( 'platform:firstleg:inbound:measure:submit' ) )
@@ -77,6 +103,8 @@ const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
const countEditableProcess = computed ( ( ) => [ 'WAIT_MEASURE' , 'MEASURING' , 'MEASURE_PENDING' , 'MEASURE_REJECTED' ] . includes ( detail . value ? . processStatus || '' ) )
const submitEditableProcess = computed ( ( ) => [ 'WAIT_MEASURE' , 'MEASURING' , 'MEASURE_REJECTED' ] . includes ( detail . value ? . processStatus || '' ) )
const mayMaintain = computed ( ( ) => canEdit . value && countEditableProcess . value && ! loading . value && ! submitting . value )
const manualEntryEditableProcess = computed ( ( ) => [ 'WAIT_MEASURE' , 'MEASURING' , 'MEASURE_REJECTED' ] . includes ( detail . value ? . processStatus || '' ) )
const mayCreateManualEntry = computed ( ( ) => canEdit . value && manualEntryEditableProcess . value && ! loading . value && ! saving . value && ! submitting . value && ! isDirty . value )
const confirmedPackageCount = computed ( ( ) => form . items . reduce ( ( sum , item ) => sum + ( Number . isFinite ( item . packageCount ) ? item . packageCount : 0 ) , 0 ) )
const expectedPackageCount = computed ( ( ) => detail . value ? . originalPackageCount ? ? null )
const packageCountDifference = computed ( ( ) => expectedPackageCount . value == null ? null : confirmedPackageCount . value - expectedPackageCount . value )
@@ -108,6 +136,81 @@ function nextItemKey() {
return ` measurement-item- ${ itemSequence } `
}
function localDateTimeNow ( ) {
const now = new Date ( )
const pad = ( value : number ) => String ( value ) . padStart ( 2 , '0' )
return ` ${ now . getFullYear ( ) } - ${ pad ( now . getMonth ( ) + 1 ) } - ${ pad ( now . getDate ( ) ) } T ${ pad ( now . getHours ( ) ) } : ${ pad ( now . getMinutes ( ) ) } : ${ pad ( now . getSeconds ( ) ) } `
}
function resetManualEntry ( ) {
manualForm . boxCode = ''
manualForm . length = null
manualForm . width = null
manualForm . height = null
manualForm . weight = null
manualForm . scannedShipmentId = ''
manualForm . machineNo = 'MANUAL-ADMIN'
manualForm . measuredAt = localDateTimeNow ( )
manualEntryImage . value = null
manualEntryErrors . value = [ ]
}
function openManualEntry ( ) {
if ( ! mayCreateManualEntry . value ) {
if ( isDirty . value ) ElMessage . warning ( '请先保存当前确认箱数,再录入新的箱规' )
return
}
resetManualEntry ( )
manualEntryOpen . value = true
}
function selectManualImage ( uploadFile : UploadFile ) {
const file = uploadFile . raw
manualEntryImage . value = file || null
}
function validateManualEntry ( ) {
const errors : string [ ] = [ ]
if ( ! manualForm . boxCode . trim ( ) ) errors . push ( '请输入箱号' )
if ( manualForm . boxCode . trim ( ) . length > 128 ) errors . push ( '箱号不能超过 128 个字符' )
if ( ! ( manualForm . length != null && manualForm . length > 0 ) ) errors . push ( '测量长必须大于 0' )
if ( ! ( manualForm . width != null && manualForm . width > 0 ) ) errors . push ( '测量宽必须大于 0' )
if ( ! ( manualForm . height != null && manualForm . height > 0 ) ) errors . push ( '测量高必须大于 0' )
if ( ! ( manualForm . weight != null && manualForm . weight > 0 ) ) errors . push ( '单箱毛重必须大于 0' )
if ( ! manualForm . measuredAt ) errors . push ( '请选择测量时间' )
if ( ! manualEntryImage . value ) errors . push ( '请上传当前箱测量图片' )
if ( manualEntryImage . value && manualEntryImage . value . size > 5 * 1024 * 1024 ) errors . push ( '测量图片不能超过 5 MB' )
manualEntryErrors . value = errors
return errors . length === 0
}
async function submitManualEntry ( ) {
if ( ! mayCreateManualEntry . value || ! validateManualEntry ( ) || manualForm . weight == null ) return
manualEntrySubmitting . value = true
operationError . value = null
try {
await createManualCartonScan ( customerId . value , props . inboundId , {
boxCode : manualForm . boxCode . trim ( ) ,
length : manualForm . length ! ,
width : manualForm . width ! ,
height : manualForm . height ! ,
weight : manualForm . weight ,
scannedShipmentId : manualForm . scannedShipmentId . trim ( ) || null ,
machineNo : manualForm . machineNo . trim ( ) || 'MANUAL-ADMIN' ,
measuredAt : manualForm . measuredAt ,
} , manualEntryImage . value ! )
manualEntryOpen . value = false
ElMessage . success ( '箱规已录入,服务端计算结果已更新' )
await loadMeasurement ( )
emit ( 'changed' )
} catch ( error ) {
const details = setOperationError ( error , '手动箱规录入失败,请检查后重试' )
ElMessage . error ( details . message )
} finally {
manualEntrySubmitting . value = false
}
}
function editableItem ( item : InboundMeasurementItem ) : EditableMeasurementItem {
return {
itemId : String ( item . id ) ,
@@ -453,6 +556,7 @@ onBeforeUnmount(releaseImagePreview)
< section class = "measurement-section measurement-items-section" >
< div class = "measurement-section__heading" >
< div > < h3 > 扫描箱测量数据明细 < / h3 > < p > 每条记录对应一个实际扫描箱 ; 人工只确认该条记录代表的箱数 , 设备实测值不可在此修改 。 < / p > < / div >
< el-button v-if = "canEdit" type="primary" plain :disabled="!mayCreateManualEntry" @click="openManualEntry"><Plus :size="15" / > 手动录入箱规 < / el-button >
< / div >
< el-empty v-if = "!form.items.length" description="暂无扫描箱测量数据" / >
@@ -503,6 +607,34 @@ onBeforeUnmount(releaseImagePreview)
< el-empty v-else description = "暂无可预览图片" / >
< / div >
< / el-dialog >
< el-dialog v-model = "manualEntryOpen" title="手动录入箱规" width="min(680px, 92vw)" destroy -on -close >
< el-alert v-if = "manualEntryErrors.length" type="error" :closable="false" show-icon class="manual-entry-alert" title="请修正以下问题" >
< ul > < li v-for = "error in manualEntryErrors" :key="error" > {{ error }} < / li > < / ul >
< / el -alert >
< el-form label -position = " top " class = "manual-entry-form" @submit.prevent ="submitManualEntry" >
< div class = "measurement-form-grid" >
< el-form-item label = "箱号 *" > < el-input v-model = "manualForm.boxCode" placeholder="请输入已打印箱唛上的箱号" / > < / el-form-item >
< el-form-item label = "测量长(cm) *" > < el-input-number v-model = "manualForm.length" :min="0.001" :precision="3" controls-position="right" / > < / el-form-item >
< el-form-item label = "测量宽(cm) *" > < el-input-number v-model = "manualForm.width" :min="0.001" :precision="3" controls-position="right" / > < / el-form-item >
< el-form-item label = "测量高(cm) *" > < el-input-number v-model = "manualForm.height" :min="0.001" :precision="3" controls-position="right" / > < / el-form-item >
< el-form-item label = "单箱毛重(kg) *" > < el-input-number v-model = "manualForm.weight" :min="0.001" :precision="3" controls-position="right" / > < / el-form-item >
< el-form-item label = "测量时间 *" > < el-date-picker v-model = "manualForm.measuredAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm:ss" / > < / el-form-item >
< el-form-item label = "设备/来源编号" > < el-input v-model = "manualForm.machineNo" maxlength="64" / > < / el-form-item >
< el-form-item label = "Shipment ID" > < el-input v-model = "manualForm.scannedShipmentId" maxlength="64" / > < / el-form-item >
< / div >
< el-form-item label = "当前箱测量图片 *" >
< el-upload class = "manual-entry-upload" :auto-upload = "false" :limit = "1" accept = "image/*" :on-change = "selectManualImage" : on -remove = " ( ) = > { manualEntryImage = null } ">
<el-button><Upload :size=" 15 " />选择图片</el-button>
<template #tip><div class=" el - upload _ _tip ">支持图片格式,大小不超过 5 MB</div></template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled=" manualEntrySubmitting " @click=" manualEntryOpen = false ">取消</el-button>
<el-button type=" primary " :loading=" manualEntrySubmitting " @click=" submitManualEntry " > 保存箱规 < / el-button >
< / template >
< / el-dialog >
< / template >
< / section >
< / template >
@@ -545,6 +677,10 @@ onBeforeUnmount(releaseImagePreview)
. measurement - image - field : deep ( . el - button ) { width : 100 % ; margin : 0 ; }
. measurement - image - preview { display : grid ; min - height : 240 px ; place - items : center ; }
. measurement - image - preview img { display : block ; width : auto ; max - width : 100 % ; max - height : 68 vh ; object - fit : contain ; }
. manual - entry - alert { margin - bottom : 16 px ; }
. manual - entry - alert ul { margin : 6 px 0 0 ; padding - left : 18 px ; }
. manual - entry - form : deep ( . el - input - number ) , . manual - entry - form : deep ( . el - date - editor ) { width : 100 % ; }
. manual - entry - upload { width : 100 % ; }
. measurement - items - section { background : # fafbfc ; }
. measurement - item { overflow : hidden ; margin - top : 14 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 9 px ; background : # fff ; }
. measurement - item . has - error { border - color : var ( -- el - color - danger - light - 5 ) ; }