@@ -88,7 +88,7 @@
< / view >
< InboundList : items = "current?.inbounds || []" / >
< view v-if = "lastReceipt" class="success-card" > 本次已收 {{ lastReceipt.acceptedPackages }} 箱 , {{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱 < / view >
< view class = "sticky-action one" > < button class = "primary-button complete-button" : disabled = "busy || progress.pending !== 0 " @tap ="completeContainer" > {{ progress.pending ? ` 尚有 $ { progress.pending } 箱未收 ` : ' 数量已核平 , 完成入库 ' }} < / button > < / view >
< view class = "sticky-action one" > < button class = "primary-button complete-button" : disabled = "busy || !allInboundsReconciled " @tap ="completeContainer" > {{ allInboundsReconciled ? ' 每个入仓码已核平 , 完成整柜入库 ' : ` 尚有 $ { progress.pending } 箱未收 ` }} < / button > < / view >
< / template >
< template v-else-if = "phase === 'completed'" >
@@ -113,6 +113,7 @@
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { isNoCurrentReceiving , isReceivingConflict , RECEIVING _PHOTO _ROLES } from '@/api/receiving-adapter.js'
import { getOperationPrototype } from '@/data/overseas-workbench.js'
import { session , requireSession } from '@/stores/session.js'
import { idempotencyKey , receiving , receivingPhase , receivingProgress , releaseIdempotencyKey , resetReceivingDraft , selectedInbound , setCurrentReceiving } from '@/stores/receiving.js'
@@ -133,7 +134,7 @@
export default {
components : { PortalHeader , ScanTargetInput , SessionSummary , InboundList } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询' , loading : false , busy : false , detailOpen : false , containerInput : '' , cartonInput : '' , skuInput : '' , palletInput : '' , packageCount : 1 , prototypeInput : '' , error : null , toast : { visible : false , message : '' , type : '' } , photoSteps : [ { role : 'LICENSE_PLATE' , label : '车牌与柜号' } , { role : 'CONTAINER_SEAL ' , label : '集装箱封签' } , { role : 'DOOR_OPEN' , label : '开柜现场' } ] } } ,
data ( ) { return { user : session . currentUser , moduleKey : 'query' , operationName : '库存查询' , loading : false , busy : false , detailOpen : false , containerInput : '' , cartonInput : '' , skuInput : '' , palletInput : '' , packageCount : 1 , prototypeInput : '' , error : null , toast : { visible : false , message : '' , type : '' } , photoSteps : RECEIVING _PHOTO _ROLES . map ( ( role ) => ( { role , label : ( { LICENSE _PLATE : '车牌照片 ' , CONTAINER _SEAL : '集装箱封签' , DOOR _OPEN : '开柜现场' } ) [ role ] } ) ) } } ,
computed : {
prototype ( ) { return getOperationPrototype ( this . operationName ) } , inboundMode ( ) { return this . moduleKey === 'inbound' } ,
candidates ( ) { return receiving . candidates } , selected ( ) { return receiving . selected } , current ( ) { return receiving . current } , photoSlots ( ) { return receiving . photoSlots } ,
@@ -142,7 +143,8 @@
activeTarget : { get ( ) { return receiving . activeScanTarget } , set ( value ) { receiving . activeScanTarget = value } } ,
phase ( ) { return this . detailOpen && ! this . current ? 'detail' : receivingPhase ( this . current ) } , progress ( ) { return receivingProgress ( this . current ) } , activeInbound ( ) { return selectedInbound ( this . current ) } ,
uploadedPhotoCount ( ) { return this . photoSlots . filter ( ( item ) => item ? . fileId ) . length } , photosReady ( ) { return this . uploadedPhotoCount === 3 } ,
canSubmitReceipt ( ) { if ( ! this . activeInbound || ! this . resolvedPallet ? . compatible ) return false ; return this . activeInbound . destinationType === 'FBA' ? Boolean ( this . resolvedSku ? . skuCode ) && Number ( this . packageCount ) > 0 && Number ( this . packageCount ) <= Number ( this . activeInbound . pendingPackages ) : Boolean ( this . resolvedCarton ? . cartonMark ) } ,
canSubmitReceipt ( ) { if ( ! this . activeInbound || ! this . resolvedPallet ? . compatible ) return false ; if ( this . activeInbound . destinationType === 'FBA' ) { const count = Number ( this . packageCount ) ; return this . resolvedSku ? . inboundCode === this . activeInbound . inboundCode && Boolean ( this . resolvedSku ? . skuCode ) && Number . isInteger ( count ) && count > 0 && count <= Number ( this . activeInbound . pendingPackages ) } return this . resolvedCarton ? . inboundCode === this . activeInbound . inboundCode && Boolean ( this . resolvedCarton ? . cartonMark ) } ,
allInboundsReconciled ( ) { const values = this . current ? . inbounds || [ ] ; return values . length > 0 && values . every ( ( item ) => Number ( item . pendingPackages ) === 0 ) } ,
pageTitle ( ) { return { candidates : '待卸货集装箱' , detail : '柜号详情' , sign : '拆柜拍照签收' , receive : '当前柜收货' , completed : '入库完成' } [ this . phase ] } ,
pageSubtitle ( ) { return { candidates : '选择集装箱后拍摄 3 张照片完成签收' , detail : '核对全部入仓码、目的仓与箱数' , sign : '完成现场取证后进入收货' , receive : '扫码箱唛与托盘,按目的类型收货' , completed : '库存与柜级进度已由服务端核平' } [ this . phase ] }
} ,
@@ -156,11 +158,13 @@
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handleHardwareScan ( value ) , onError : ( error ) => { this . error = error } } ) } , stopScanner ( ) { if ( typeof this . _stopScanner === 'function' ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
handleHardwareScan ( value ) { if ( ! this . inboundMode ) { this . prototypeInput = value ; return this . prototypeScan ( ) } if ( this . phase === 'candidates' ) { this . containerInput = value ; return this . openContainer ( value ) } if ( this . phase !== 'receive' ) return ; if ( this . activeTarget === 'carton' && this . activeInbound ? . destinationType === 'OVERSEAS' ) { this . cartonInput = value ; return this . resolveCarton ( value ) } if ( this . activeTarget === 'sku' && this . activeInbound ? . destinationType === 'FBA' ) { this . skuInput = value ; return this . resolveSku ( value ) } this . palletInput = value ; return this . resolvePallet ( value ) } ,
async run ( action , work ) { this . busy = true ; this . error = null ; try { const result = await work ( ) ; this . showToast ( ` ${ action } 成功 ` , 'success' ) ; return result } catch ( error ) { this . error = error ; playErrorSound ( ) ; this . showToast ( error . message || ` ${ action } 失败 ` , 'error' ) ; throw error } finally { this . busy = false } } ,
async bootstrap ( ) { this . loading = true ; this . error = null ; this . detailOpen = false ; try { const current = await getCurrentReceiving ( ) ; setCurrentReceiving ( current ) ; receiving . activeScanTarget = current ? . status === 'WAIT_SIGN' ? 'container' : ( selectedInbound ( current ) ? . destinationType === 'OVERSEAS' ? 'carton' : 'sku' ) } catch ( error ) { if ( String ( error . code ) === '404' || String ( error . code ) === 'A0404' || String ( error . code ) === 'B0404' ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) } else this . error = error } finally { this . loading = false } } ,
setScanTargetForCurrent ( value ) { receiving . activeScanTarget = value ? . status === 'WAIT_SIGN' ? 'container' : ( selectedInbound ( value ) ? . destinationType === 'OVERSEAS' ? 'carton' : 'sku' ) } ,
async bootstrap ( ) { this . loading = true ; this . error = null ; this . detailOpen = false ; try { const current = await getCurrentReceiving ( ) ; setCurrentReceiving ( current ) ; this . setScanTargetForCurrent ( current ) } catch ( error ) { if ( isNoCurrentReceiving ( error ) ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) } else this . error = error } finally { this . loading = false } } ,
async recoverCurrentAfterConflict ( error ) { if ( ! isReceivingConflict ( error ) ) return false ; try { const current = await getCurrentReceiving ( ) ; setCurrentReceiving ( current ) ; this . setScanTargetForCurrent ( current ) ; this . detailOpen = false ; this . error = null ; this . showToast ( ` 账号已有未完成柜 ${ current . containerNo } ,已返回当前柜 ` , 'warning' ) ; return true } catch ( recoveryError ) { this . error = recoveryError ; return false } } ,
async loadCandidates ( feedback = true ) { try { const work = ( ) => listReceivingCandidates ( ) ; receiving . candidates = feedback ? await this . run ( '刷新待卸柜' , work ) : await work ( ) } catch ( _ ) { } } ,
async openContainer ( raw ) { const containerNo = String ( raw ? . detail ? . value ? ? raw ? ? this . containerInput ) . trim ( ) ; if ( ! containerNo || this . busy ) return ; try { receiving . selected = await this . run ( '查询柜号' , ( ) => getReceivingContainer ( containerNo ) ) ; this . containerInput = containerNo ; this . detailOpen = true ; playSuccessSound ( ) } catch ( _ ) { } } ,
async openContainer ( raw ) { const containerNo = String ( raw ? . detail ? . value ? ? raw ? ? this . containerInput ) . trim ( ) ; if ( ! containerNo || this . busy ) return ; try { receiving . selected = await this . run ( '查询柜号' , ( ) => getReceivingContainer ( containerNo ) ) ; this . containerInput = containerNo ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
cancelDetail ( ) { this . detailOpen = false ; receiving . selected = null } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo ) return ; const key = idempotencyKey ( 'prepare' , { containerNo : this . selected . containerNo } ) ; try { const value = await this . run ( '选择当前柜' , ( ) => prepareReceivingContainer ( this . selected . containerNo , { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; this . detailOpen = false } catch ( _ ) { } } ,
async prepareContainer ( ) { if ( ! this . selected ? . containerNo ) return ; const key = idempotencyKey ( 'prepare' , { containerNo : this . selected . containerNo } ) ; try { const value = await this . run ( '选择当前柜' , ( ) => prepareReceivingContainer ( this . selected . containerNo , { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; this . setScanTargetForCurrent ( value ) ; this . detailOpen = false } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
choosePhoto ( index ) { if ( this . busy ) return ; uni . chooseImage ( { count : 1 , sourceType : [ 'camera' , 'album' ] , success : async ( { tempFilePaths } ) => { const path = tempFilePaths ? . [ 0 ] ; if ( ! path ) return ; try { const file = await this . run ( '上传签收照片' , ( ) => uploadCurrentUnloadPhoto ( path ) ) ; receiving . photoSlots . splice ( index , 1 , { ... file , fileId : String ( file . fileId ) , role : this . photoSteps [ index ] . role , localPath : path , uploaded : true } ) } catch ( _ ) { } } } ) } ,
async signContainer ( ) { if ( ! this . photosReady ) return ; const photos = this . photoSlots . map ( ( item , index ) => ( { role : this . photoSteps [ index ] . role , fileId : Number ( item . fileId ) } ) ) ; const key = idempotencyKey ( 'sign' , { containerNo : this . current . containerNo , photos } ) ; try { const value = await this . run ( '签收当前柜' , ( ) => signCurrentReceiving ( { photos , idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; receiving . activeScanTarget = selectedInbound ( value ) ? . destinationType === 'OVERSEAS' ? 'carton' : 'sku' } catch ( _ ) { } } ,
chooseInbound ( item ) { receiving . selectedInboundCode = item . inboundCode ; receiving . resolvedCarton = null ; receiving . resolvedSku = null ; receiving . resolvedPallet = null ; this . cartonInput = '' ; this . skuInput = '' ; this . palletInput = '' ; this . packageCount = Math . max ( 1 , Number ( item . pendingPackages || 1 ) ) ; receiving . activeScanTarget = item . destinationType === 'OVERSEAS' ? 'carton' : 'sku' } ,
@@ -168,7 +172,7 @@
async resolveSku ( raw ) { const skuCode = String ( raw ? . detail ? . value ? ? raw ? ? this . skuInput ) . trim ( ) ; if ( ! skuCode || this . busy ) return ; try { const value = await this . run ( '解析 SKU' , ( ) => resolveCurrentSku ( skuCode ) ) ; receiving . resolvedSku = value ; receiving . selectedInboundCode = value . inboundCode ; receiving . resolvedPallet = null ; this . skuInput = value . skuCode ; this . palletInput = '' ; receiving . activeScanTarget = 'pallet' ; playSuccessSound ( ) } catch ( _ ) { } } ,
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 ; if ( ! value . compatible ) throw Object . assign ( new Error ( '托盘与当前入仓码目的仓不匹配' ) , { code : 'PALLET_INCOMPATIBLE' } ) ; 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 , skuCode : this . resolvedSku . skuCode , palletCode : this . resolvedPallet . palletCode , packageCount : Number ( this . packageCount ) } : { cartonMark : this . resolvedCarton . cartonMark , palletCode : this . resolvedPallet . palletCode } ; const key = idempotencyKey ( isFba ? 'bulk-receipt' : 'carton-receipt' , payload ) ; try { const result = await this . run ( '提交实收' , ( ) => isFba ? receiveCurrentBulk ( { ... payload , idempotencyKey : key . value } ) : receiveCurrentCarton ( { ... payload , idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result ; setCurrentReceiving ( await getCurrentReceiving ( ) ) ; receiving . resolvedCarton = null ; receiving . resolvedSku = null ; receiving . resolvedPallet = null ; this . cartonInput = '' ; this . skuInput = '' ; this . palletInput = '' ; const inbound = selectedInbound ( ) ; this . packageCount = Math . max ( 1 , Number ( inbound ? . pendingPackages || 1 ) ) ; receiving . activeScanTarget = inbound ? . destinationType === 'OVERSEAS' ? 'carton' : 'sku' ; playSuccessSound ( ) } catch ( _ ) { } } ,
async completeContainer ( ) { if ( this . progress . pending !== 0 ) return ; const key = idempotencyKey ( 'complete' , { containerNo : this . current . containerNo } ) ; try { const value = await this . run ( '完成入库' , ( ) => completeCurrentReceiving ( { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) } catch ( _ ) { } } ,
async completeContainer ( ) { if ( ! this . allInboundsReconciled ) return ; const key = idempotencyKey ( 'complete' , { containerNo : this . current . containerNo } ) ; try { const value = await this . run ( '完成入库' , ( ) => completeCurrentReceiving ( { idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) } catch ( _ ) { } } ,
async finishAndReturn ( ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) } ,
prototypeScan ( ) { if ( ! String ( this . prototypeInput || '' ) . trim ( ) ) return this . showToast ( '请先扫描或输入编码' , 'warning' ) ; this . showToast ( '该模块尚未接入真实业务接口' , 'warning' ) } ,
showToast ( message , type ) { clearTimeout ( this . _toastTimer ) ; this . toast = { visible : true , message , type } ; this . _toastTimer = setTimeout ( ( ) => { this . toast . visible = false } , 2600 ) }