@@ -13,7 +13,7 @@
< view v-if = "candidateError" class="error-card candidate-error-card"><text class="error-message">{{ candidateError.message || '待卸货集装箱加载失败' }}</text><text v-if="candidateError.code">code={{ candidateError.code }}</text><text v-if="candidateError.requestId">requestId={{ candidateError.requestId }}</text><text v-if="candidateError.traceId">traceId={{ candidateError.traceId }}</text><button class="secondary-button" :disabled="candidateLoading" @tap="loadCandidates" > 重新加载 < / button > < / view >
< view v-if = "candidateLoading && !candidates.length" class="state-card"><uni-load-more status="loading" / > < text > 正在刷新待卸货集装箱 … < / text > < / view >
< view v-else-if = "!candidateError && !candidates.length" class="empty-card"><uni-icons type="info" color="#8090a3" size="34" /><text>暂无待卸货集装箱</text><button class="secondary-button" :disabled="candidateLoading" @tap="loadCandidates" > 重新加载 < / button > < / view >
< view v-for = "item in candidates" :key="item.containerNo " class="container-card container-card-compact" :class="{ 'container-card-disabled': !item.operable }" role="button" :aria-label="`${item.containerNo}, ${item.operable ? '查看可操作柜详情' : '查看未齐全柜摘要'}`" hover-class="container-card-pressed" @tap="openContainer(item)" >
< view v-for = "item in candidates" :key="candidateKey(item) " class="container-card container-card-compact" :class="{ 'container-card-disabled': !item.operable }" role="button" :aria-label="`${item.containerNo}, ${item.operable ? '查看可操作柜详情' : '查看未齐全柜摘要'}`" hover-class="container-card-pressed" @tap="openContainer(item)" >
< view class = "card-head" > < view > < text class = "container-no" > { { item . containerNo } } < / text > < text class = "candidate-arrival" > 计划到仓 : { { formatPlannedArrival ( item . plannedArrivalAt ) } } < / text > < / view > < view class = "container-card-actions" > < text class = "status-tag" : class = "item.operable ? statusTone(item.status) : 'status-disabled'" > { { item . operable ? statusLabel ( item . status ) : '待齐全' } } < / text > < text class = "container-detail-link" > { { item . operable ? ( item . currentAccount ? '继续' : '详情' ) : '只读' } } › < / text > < / view > < / view >
< view class = "candidate-facts" > < view > < strong > { { item . totalOrderCount } } < / strong > < text > 总订单 < / text > < / view > < view > < strong > { { item . inboundCodeCount } } < / strong > < text > 入仓码 < / text > < / view > < view > < strong > { { item . planCount } } < / strong > < text > 已生成计划 < / text > < / view > < view > < strong > { { item . pendingPlanCount } } < / strong > < text > 未操作 < / text > < / view > < / view >
< text class = "candidate-progress-copy" > 已生成 { { item . planCount } } 个入库计划 , { { item . pendingPlanCount } } 个未操作 < template v-if = "item.missingInboundCodeCount" > , 其中 {{ item.missingInboundCodeCount }} 个缺入仓码 < / template > < / text >
@@ -144,6 +144,8 @@
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
import { completeCurrentReceiving , downloadCurrentDomesticLoadingPhoto , downloadCurrentUnloadPhoto , getCurrentReceiving , getCurrentWarehouseProfile , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , receiveCurrentWholePallet , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
const MAX _UNLOAD _PHOTO _BYTES = 10 * 1024 * 1024
const MAX _PACKAGES _PER _PALLET = 66
const PALLET _CAPACITY _MESSAGE = '一个托盘最多只能收66箱,请拆分到其他托盘'
export default {
components : { PortalHeader , ReceivingSessionSummary , ScanTargetInput } ,
@@ -168,6 +170,7 @@
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; this . startScanner ( ) ; if ( this . inboundMode && ( ! this . bootstrapped || this . phase === 'candidates' ) ) this . bootstrap ( ) } ,
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { clearTimeout ( this . _toastTimer ) ; this . stopScanner ( ) } ,
methods : {
candidateKey ( item ) { return ` ${ item ? . sourceContainerBookingId || '' } : ${ item ? . containerNo || '' } ` } ,
statusLabel ( status ) { return ( { WAIT _SIGN : '待签收' , SIGNED : '已签收' , RECEIVING : '收货中' , COMPLETED : '已完成' , COMPLETED _WITH _DIFFERENCE : '差异待核平' } ) [ status ] || status } ,
statusTone ( status ) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' } ,
formatPlannedArrival ( value ) { const text = String ( value || '' ) . trim ( ) ; return text ? text . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) : '待确认' } ,
@@ -190,9 +193,9 @@
async bootstrap ( ) { if ( this . loading ) return ; this . loading = true ; this . error = null ; this . detailOpen = false ; try { await this . refreshBoundWarehouse ( ) ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . candidateError = null } catch ( error ) { if ( ! isNoCurrentReceiving ( error ) ) throw error ; resetReceivingDraft ( ) ; await this . loadCandidates ( false , false ) } this . bootstrapped = true } catch ( error ) { this . candidateError = error } finally { this . loading = false } } ,
async recoverCurrentAfterConflict ( error , preserveEvidence = false ) { if ( ! isReceivingConflict ( error ) ) return false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . detailOpen = false ; this . error = preserveEvidence ? error : null ; this . showToast ( ` 账号已有未完成柜 ${ current . containerNo } ,已返回当前柜 ` , 'warning' ) ; return true } catch ( recoveryError ) { if ( isNoCurrentReceiving ( recoveryError ) ) { this . detailOpen = false ; receiving . selected = null ; await this . loadCandidates ( false ) ; this . candidateError = preserveEvidence ? error : null ; this . error = preserveEvidence ? error : null ; this . showToast ( '柜状态已变化,列表已刷新' , 'warning' ) ; return true } this . error = error ; return false } } ,
async loadCandidates ( feedback = true , refreshWarehouse = true ) { if ( this . candidateLoading ) return ; this . candidateLoading = true ; this . candidateError = null ; this . error = null ; try { if ( refreshWarehouse ) await this . refreshBoundWarehouse ( ) ; const values = await listReceivingCandidates ( ) ; receiving . candidates = values . map ( ( item ) => this . validateReceivingWarehouse ( item , ` 柜号 ${ item . containerNo || '—' } ` ) ) ; if ( feedback ) this . showToast ( '刷新待卸柜成功' , 'success' ) } catch ( error ) { receiving . candidates = [ ] ; this . candidateError = error ; if ( feedback ) { playErrorSound ( ) ; this . showToast ( error . message || '刷新待卸柜失败' , 'error' ) } } finally { this . candidateLoading = false } } ,
async openContainer ( candidate ) { const containerNo = String ( candidate ? . containerNo || '' ) . trim ( ) ; if ( ! containerNo || this . busy ) return ; this . error = null ; try { this . validateReceivingWarehouse ( candidate , ` 柜号 ${ containerNo } ` ) ; if ( ! candidate . operable ) { receiving . selected = candidate ; this . detailOpen = true ; return } const detail = await this . run ( '查询柜号' , async ( ) => this . validateReceivingWarehouse ( await getReceivingContainer ( containerNo ) , ` 柜号 ${ containerNo } ` ) ) ; receiving . selected = { ... candidate , ... detail , allPlansReady : candidate . allPlansReady , operable : candidate . operable , blockReason : candidate . blockReason } ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { if ( ! await this . recoverCurrentAfterConflict ( error , true ) ) { this . error = error ; this . showToast ( error . message || '查询柜号失败' , 'error' ) } } } ,
async openContainer ( candidate ) { const containerNo = String ( candidate ? . containerNo || '' ) . trim ( ) ; const sourceContainerBookingId = String ( candidate ? . sourceContainerBookingId || '' ) . trim ( ) ; if ( ! containerNo || ! sourceContainerBookingId || this . busy ) return ; this . error = null ; try { this . validateReceivingWarehouse ( candidate , ` 柜号 ${ containerNo } ` ) ; if ( ! candidate . operable ) { receiving . selected = candidate ; this . detailOpen = true ; return } const detail = await this . run ( '查询柜号' , async ( ) => this . validateReceivingWarehouse ( await getReceivingContainer ( containerNo , sourceContainerBookingId ) , ` 柜号 ${ containerNo } ` ) ) ; receiving . selected = { ... candidate , ... detail , sourceContainerBookingId , allPlansReady : candidate . allPlansReady , operable : candidate . operable , blockReason : candidate . blockReason } ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { if ( ! await this . recoverCurrentAfterConflict ( error , true ) ) { this . error = error ; this . showToast ( error . message || '查询柜号失败' , 'error' ) } } } ,
cancelDetail ( ) { this . detailOpen = false ; receiving . selected = null } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo || ! this . selected ? . allPlansReady || ! this . selected ? . operable || this . busy ) return ; const key = idempotencyKey ( 'prepare' , { containerNo : this . selected . containerNo } ) ; try { this . validateReceivingWarehouse ( this . selected , ` 柜号 ${ this . selected . containerNo } ` ) ; const value = await this . run ( '选择当前柜' , async ( ) => this . validateReceivingWarehouse ( await prepareReceivingContainer ( this . selected . containerNo , { idempotencyKey : key . value } ) , '新建接柜会话' ) ) ; releaseIdempotencyKey ( key . signature ) ; await this . applyCurrentReceiving ( value ) ; this . detailOpen = false } catch ( error ) { if ( ! await this . recoverCurrentAfterConflict ( error , true ) ) this . error = error } } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo || ! this . selected ? . sourceContainerBookingId || ! this . selected ? . allPlansReady || ! this . selected ? . operable || this . busy ) return ; const key = idempotencyKey ( 'prepare' , { sourceContainerBookingId : this . selected . sourceContainerBookingId , containerNo : this . selected . containerNo } ) ; try { this . validateReceivingWarehouse ( this . selected , ` 柜号 ${ this . selected . containerNo } ` ) ; const value = await this . run ( '选择当前柜' , async ( ) => this . validateReceivingWarehouse ( await prepareReceivingContainer ( this . selected . containerNo , this . selected . sourceContainerBookingId , { idempotencyKey : key . value } ) , '新建接柜会话' ) ) ; releaseIdempotencyKey ( key . signature ) ; await this . applyCurrentReceiving ( value ) ; this . detailOpen = false } catch ( error ) { if ( ! await this . recoverCurrentAfterConflict ( error , true ) ) this . error = error } } ,
choosePhoto ( index ) { if ( this . busy ) return ; uni . chooseImage ( { count : 1 , sourceType : [ 'camera' ] , success : ( { tempFilePaths , tempFiles } ) => { const path = tempFilePaths ? . [ 0 ] ; const size = Number ( tempFiles ? . [ 0 ] ? . size || 0 ) ; if ( size > MAX _UNLOAD _PHOTO _BYTES ) { const message = '照片不能超过10MB' ; this . error = Object . assign ( new Error ( message ) , { code : 'A0400' } ) ; this . showToast ( message , 'error' ) ; playErrorSound ( ) ; return } if ( path ) this . replacePhoto ( index , path ) } } ) } ,
async replacePhoto ( index , path ) { const previous = receiving . photoSlots [ index ] ; receiving . photoSlots . splice ( index , 1 , { ... ( previous || { } ) , role : this . photoSteps [ index ] . role , status : 'uploading' , errorMessage : '' } ) ; this . busy = true ; this . error = null ; try { const file = await uploadCurrentUnloadPhoto ( path ) ; receiving . photoSlots . splice ( index , 1 , { ... file , fileId : String ( file . fileId ) , role : this . photoSteps [ index ] . role , localPath : path , uploaded : true , status : 'success' } ) ; this . showToast ( ` ${ this . photoSteps [ index ] . label } 上传成功 ` , 'success' ) } catch ( error ) { receiving . photoSlots . splice ( index , 1 , previous ? . fileId ? { ... previous , status : 'error' , errorMessage : error . message || '重拍失败,已保留原图' } : { role : this . photoSteps [ index ] . role , localPath : path , uploaded : false , status : 'error' , errorMessage : error . message || '上传失败,请重试' } ) ; this . error = error ; this . showToast ( error . message || '照片上传失败' , 'error' ) ; playErrorSound ( ) } finally { this . busy = false } } ,
previewPhoto ( index ) { const path = this . photoSlots [ index ] ? . localPath ; if ( ! path ) return ; uni . previewImage ( { urls : [ path ] , current : path } ) } ,
@@ -200,7 +203,7 @@
clearReceiptContext ( ) { receiving . resolvedCarton = null ; receiving . resolvedPallet = null ; this . cartonInput = '' ; this . palletInput = '' ; this . packageCount = '' ; receiving . activeScanTarget = 'carton' ; setTimeout ( ( ) => this . drainScanQueue ( ) , 0 ) } ,
async resolveCarton ( raw ) { const cartonMark = String ( raw ? . detail ? . value ? ? raw ? ? this . cartonInput ) . trim ( ) ; if ( ! cartonMark ) return ; if ( this . busy ) return this . handleHardwareScan ( cartonMark ) ; try { const outcome = await this . run ( '处理货物码' , async ( ) => { const value = await resolveCurrentCarton ( cartonMark ) ; if ( value . operationMode !== 'DROPSHIP_PALLET' ) return { value } ; const payload = { sourcePalletNo : value . sourcePalletNo || cartonMark } ; const key = idempotencyKey ( 'whole-pallet-receipt' , { containerNo : this . current ? . containerNo , ... payload } ) ; const result = await receiveCurrentWholePallet ( { ... payload , idempotencyKey : key . value } ) ; const current = this . validateReceivingWarehouse ( await getCurrentReceiving ( ) , '当前接柜会话' ) ; releaseIdempotencyKey ( key . signature ) ; return { value , result , current } } ) ; if ( outcome . result ) { receiving . lastReceipt = outcome . result ; await this . applyCurrentReceiving ( outcome . current ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) ; return } receiving . resolvedCarton = outcome . value ; receiving . resolvedPallet = null ; this . cartonInput = outcome . value . cartonMark ; this . palletInput = '' ; this . packageCount = '' ; receiving . activeScanTarget = 'pallet' ; playSuccessSound ( ) } catch ( error ) { console . error ( '[Overseas PDA cargo] failed' , { cartonMark , message : error . message , code : error . code , requestId : error . requestId , traceId : error . traceId } ) } } ,
async resolvePallet ( raw ) { const palletCode = String ( raw ? . detail ? . value ? ? raw ? ? this . palletInput ) . trim ( ) ; if ( ! palletCode || ! this . activeInbound || this . busy ) return ; try { const value = await this . run ( '校验托盘' , ( ) => resolveCurrentPallet ( palletCode , this . activeInbound . inboundCode ) ) ; receiving . resolvedPallet = value ; this . palletInput = value . palletCode ; this . packageCount = Number ( value . currentReceivedPackages || 0 ) || '' ; if ( ! value . compatible ) throw Object . assign ( new Error ( '托盘不匹配' ) , { code : 'PALLET_INCOMPATIBLE' } ) ; receiving . activeScanTarget = this . activeInbound . destinationType === 'FBA' ? 'quantity' : 'carton' ; playSuccessSound ( ) } catch ( error ) { if ( ! this . error ) this . error = error } } ,
async submitReceipt ( ) { if ( ! this . canSubmitReceipt ) return ; const isFba = this . activeInbound . destinationType === 'FBA' ; const payload = isFba ? { inboundCode : this . activeInbound . inboundCode , palletCode : this . resolvedPallet . palletCode , packageCount : Number ( this . packageCount ) , expectedPalletVersion : this . resolvedPallet . version } : { cartonMark : this . activeInbound . cartonMark , palletCode : this . resolvedPallet . palletCode } ; const key = idempotencyKey ( isFba ? 'bulk-receipt' : 'carton-receipt' , { containerNo : this . current ? . containerNo , groupKey : this . activeInbound . groupKey , ... payload } ) ; try { const result = await this . run ( '提交实收' , async ( ) => { const receipt = isFba ? await receiveCurrentBulk ( { ... payload , idempotencyKey : key . value } ) : await receiveCurrentCarton ( { ... payload , idempotencyKey : key . value } ) ; const current = this . validateReceivingWarehouse ( await getCurrentReceiving ( ) , '当前接柜会话' ) ; return { receipt , current } } ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result . receipt ; await this . applyCurrentReceiving ( result . current ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) } catch ( _ ) { } } ,
async submitReceipt ( ) { const isFba = this . activeInbound ? . destinationType === 'FBA' ; if ( isFba && Number ( this . packageCount ) > MAX _PACKAGES _PER _PALLET ) { this . error = Object . assign ( new Error ( PALLET _CAPACITY _MESSAGE ) , { code : 'A0409' } ) ; playErrorSound ( ) ; this . showToast ( PALLET _CAPACITY _MESSAGE , 'error' ) ; return } if ( ! this . canSubmitReceipt ) return ; const payload = isFba ? { inboundCode : this . activeInbound . inboundCode , palletCode : this . resolvedPallet . palletCode , packageCount : Number ( this . packageCount ) , expectedPalletVersion : this . resolvedPallet . version } : { cartonMark : this . activeInbound . cartonMark , palletCode : this . resolvedPallet . palletCode } ; const key = idempotencyKey ( isFba ? 'bulk-receipt' : 'carton-receipt' , { containerNo : this . current ? . containerNo , groupKey : this . activeInbound . groupKey , ... payload } ) ; try { const result = await this . run ( '提交实收' , async ( ) => { const receipt = isFba ? await receiveCurrentBulk ( { ... payload , idempotencyKey : key . value } ) : await receiveCurrentCarton ( { ... payload , idempotencyKey : key . value } ) ; const current = this . validateReceivingWarehouse ( await getCurrentReceiving ( ) , '当前接柜会话' ) ; return { receipt , current } } ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result . receipt ; await this . applyCurrentReceiving ( result . current ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) } catch ( _ ) { } } ,
async completeContainer ( ) { if ( ! this . canFinishOnsite || this . activeTarget === 'quantity' || this . scanQueue . length ) return ; const payload = { allowDifference : ! this . allInboundsReconciled } ; const key = idempotencyKey ( 'complete' , { containerNo : this . current . containerNo , ... payload } ) ; try { const value = await this . run ( this . allInboundsReconciled ? '完成入库' : '结束现场收货' , async ( ) => this . validateReceivingWarehouse ( await completeCurrentReceiving ( { ... payload , idempotencyKey : key . value } ) , '当前接柜会话' ) ) ; releaseIdempotencyKey ( key . signature ) ; await this . applyCurrentReceiving ( value ) } catch ( _ ) { } } ,
async finishAndReturn ( ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) } ,
showToast ( message , type ) { clearTimeout ( this . _toastTimer ) ; this . toast = { visible : true , message , type } ; this . _toastTimer = setTimeout ( ( ) => { this . toast . visible = false } , 2600 ) }