@@ -66,6 +66,13 @@
< template v-else-if = "phase === 'receive'" >
< ReceivingSessionSummary :session = "current" / >
< view class = "scan-card" >
< template v-if = "currentDropshipInbound" >
< text class = "section-label" > 扫描国内封托来源托盘 < / text >
< view class = "matched-inbound-card" > < view class = "mode-banner overseas" > < text > 一件代发整托收货 < / text > < text > { { destinationLabel ( currentDropshipInbound ) } } < / text > < / view > < text class = "matched-inbound-code" > { { currentDropshipInbound . inboundCode } } < / text > < view class = "matched-inbound-progress" > < text > 总计 { { currentDropshipInbound . totalPackages } } 箱 < / text > < text > 已收 { { currentDropshipInbound . receivedPackages } } 箱 < / text > < text > 待收 { { currentDropshipInbound . pendingPackages } } 箱 < / text > < / view > < / view >
< ScanTargetInput name = "source-pallet" v-model = "palletInput" label="来源托盘码" placeholder="扫描 DP 开头的国内封托托盘码" :active="activeTarget === 'source-pallet'" @activate="activeTarget = 'source-pallet'" @submit="submitWholePallet" / >
< button class = "primary-button receipt-button" : disabled = "!canSubmitReceipt || busy" @tap ="submitWholePallet(palletInput)" > 确认整托收货 < / button >
< / template >
< template v-else >
< text class = "section-label" > 1. 扫描箱码定位入仓码 < / text >
< ScanTargetInput name = "carton" v-model = "cartonInput" label="箱码" placeholder="NYEXX2608260001#4-1" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" / >
< button class = "secondary-button" : disabled = "busy || !cartonInput.trim()" @tap ="resolveCarton(cartonInput)" > 解析箱码 < / button >
@@ -89,6 +96,7 @@
< / template >
< button class = "primary-button receipt-button" : disabled = "!canSubmitReceipt || busy" @tap ="submitReceipt" > {{ activeInbound.destinationType = = = ' FBA ' ? ' 确认按箱数收货 ' : ' 确认本箱收货 ' }} < / button >
< / template >
< / template >
< / view >
< view v-if = "lastReceipt" class="success-card" > 本次已收 {{ lastReceipt.acceptedPackages }} 箱 , {{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱 < / view >
< view v-if = "allInboundsReconciled" class="sticky-action one"><button class="primary-button complete-button" :disabled="busy" @tap="completeContainer" > 完成整柜入库 < / button > < / view >
@@ -122,7 +130,7 @@
import { idempotencyKey , receiving , receivingPhase , receivingProgress , releaseIdempotencyKey , resetReceivingDraft , setCurrentReceiving } from '@/stores/receiving.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
import { completeCurrentReceiving , downloadCurrentUnloadPhoto , getCurrentReceiving , getCurrentWarehouseProfile , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
import { completeCurrentReceiving , 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
export default {
@@ -134,9 +142,9 @@
candidates ( ) { return receiving . candidates } , selected ( ) { return receiving . selected } , current ( ) { return receiving . current } , photoSlots ( ) { return receiving . photoSlots } ,
resolvedCarton ( ) { return receiving . resolvedCarton } , resolvedPallet ( ) { return receiving . resolvedPallet } , lastReceipt ( ) { return receiving . lastReceipt } ,
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 this . resolvedCarton } ,
phase ( ) { return this . detailOpen && ! this . current ? 'detail' : receivingPhase ( this . current ) } , progress ( ) { return receivingProgress ( this . current ) } , currentDropshipInbound ( ) { return ( this . current ? . inbounds || [ ] ) . find ( ( item ) => item . wholePalletInbound && Number ( item . pendingPackages ) > 0 ) || null } , activeInbound ( ) { return this . resolvedCarton || this . currentDropshipInbound } ,
uploadedPhotoCount ( ) { return this . photoSlots . filter ( ( item ) => item ? . fileId ) . length } , photosReady ( ) { return this . uploadedPhotoCount === 3 && ! this . photoSlots . some ( ( item ) => item ? . status === 'uploading' ) } ,
canSubmitReceipt ( ) { if ( ! this . activeInbound || ! this . resolvedPallet ? . compatible ) return false ; if ( this . activeInbound . destinationType === 'FBA' ) { const count = Number ( this . packageCount ) ; return Number . isInteger ( count ) && count > 0 && count <= Number ( this . activeInbound . pendingPackages ) } return Boolean ( this . activeInbound . cartonMark ) } ,
canSubmitReceipt ( ) { if ( ! this . activeInbound ) return false ; if ( this . activeInbound . wholePalletInbound ) return Boolean ( this . palletInput . trim ( ) ) ; if ( ! this . resolvedPallet ? . compatible ) return false ; if ( this . activeInbound . destinationType === 'FBA' ) { const count = Number ( this . packageCount ) ; return Number . isInteger ( count ) && count > 0 && count <= Number ( this . activeInbound . pendingPackages ) } return Boolean ( this . activeInbound . cartonMark ) } ,
allInboundsReconciled ( ) { const values = this . current ? . inbounds || [ ] ; return values . length > 0 && values . every ( ( item ) => Number ( item . pendingPackages ) === 0 ) } ,
boundWarehouseLabel ( ) { const code = String ( this . user ? . warehouseCode || '' ) . trim ( ) ; const name = String ( this . user ? . warehouseName || '' ) . trim ( ) ; return code && name ? ` ${ code } · ${ name } ` : '海外仓未关联' } ,
pageTitle ( ) { return { candidates : '待卸货集装箱' , detail : '柜号详情' , sign : '拆柜拍照签收' , receive : '当前柜收货' , completed : '入库完成' } [ this . phase ] } ,
@@ -155,9 +163,9 @@
async refreshBoundWarehouse ( ) { this . user = applyWarehouseProfile ( await getCurrentWarehouseProfile ( ) ) ; return this . user } ,
photoStateLabel ( photo ) { if ( photo ? . status === 'uploading' ) return '正在上传…' ; if ( photo ? . status === 'error' ) return photo . errorMessage || ( photo . fileId ? '重拍失败,已保留原图' : '上传失败,请重试' ) ; return photo ? . fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handleHardwareScan ( value ) , onError : ( error ) => { this . error = error ; this . showToast ( '硬件扫码接收器异常,请检查扫描服务' , 'warning' ) } } ) } , stopScanner ( ) { if ( typeof this . _stopScanner === 'function' ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
handleHardwareScan ( value ) { console . info ( '[Overseas PDA scan] dispatch' , { value , phase : this . phase , target : this . activeTarget , matchedInbound : this . activeInbound ? . inboundCode } ) ; if ( ! this . inboundMode ) return this . showToast ( '该功能缺少海外 PDA 专用接口,扫码未提交' , 'warning' ) ; if ( this . phase === 'candidates' ) return this . showToast ( '请直接点击待卸货集装箱进入详情' , 'warning' ) ; if ( this . phase !== 'receive' ) return ; if ( this . activeTarget === 'pallet' && this . activeInbound ) { this . palletInput = value ; return this . resolvePallet ( value ) } this . cartonInput = value ; return this . resolveCarton ( value ) } ,
handleHardwareScan ( value ) { console . info ( '[Overseas PDA scan] dispatch' , { value , phase : this . phase , target : this . activeTarget , matchedInbound : this . activeInbound ? . inboundCode } ) ; if ( ! this . inboundMode ) return this . showToast ( '该功能缺少海外 PDA 专用接口,扫码未提交' , 'warning' ) ; if ( this . phase === 'candidates' ) return this . showToast ( '请直接点击待卸货集装箱进入详情' , 'warning' ) ; if ( this . phase !== 'receive' ) return ; if ( this . currentDropshipInbound ) { this . palletInput = value ; return this . submitWholePallet ( value ) } if ( this . activeTarget === 'pallet' && this . activeInbound ) { this . palletInput = value ; return this . resolvePallet ( value ) } this . cartonInput = value ; return this . resolveCarton ( value ) } ,
async run ( action , work ) { this . busy = true ; this . error = null ; try { const result = await work ( ) ; this . showToast ( ` ${ action } 成功 ` , 'success' ) ; return result } catch ( error ) { const mappedError = toReceivingError ( error , ` ${ action } 失败 ` ) ; this . error = mappedError ; playErrorSound ( ) ; this . showToast ( mappedError . message , 'error' ) ; throw mappedError } finally { this . busy = false } } ,
setScanTargetForCurrent ( value ) { receiving . activeScanTarget = value ? . status === 'WAIT_SIGN' ? 'container' : 'carton' } ,
setScanTargetForCurrent ( value ) { receiving . activeScanTarget = value ? . status === 'WAIT_SIGN' ? 'container' : ( value ? . inbounds || [ ] ) . some ( ( item ) => item . wholePalletInbound && Number ( item . pendingPackages ) > 0 ) ? 'source-pallet' : 'carton' } ,
async restorePhotoPreviews ( ) { await Promise . all ( receiving . photoSlots . filter ( ( photo ) => photo ? . downloadPath && ! photo . localPath ) . map ( async ( photo ) => { photo . status = 'restoring' ; try { photo . localPath = await downloadCurrentUnloadPhoto ( photo . downloadPath ) ; photo . status = 'success' } catch ( error ) { photo . status = 'error' ; photo . errorMessage = error . message || '照片加载失败' ; this . error = error } } ) ) } ,
async applyCurrentReceiving ( value ) { this . validateReceivingWarehouse ( value , '当前接柜会话' ) ; setCurrentReceiving ( value ) ; await this . restorePhotoPreviews ( ) ; this . setScanTargetForCurrent ( value ) } ,
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 } } ,
@@ -170,10 +178,11 @@
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 } ) } ,
async signContainer ( ) { if ( ! this . photosReady ) return ; const photos = this . photoSlots . map ( ( item , index ) => ( { role : this . photoSteps [ index ] . role , fileId : String ( item . fileId ) } ) ) ; const key = idempotencyKey ( 'sign' , { containerNo : this . current . containerNo , photos } ) ; try { const value = await this . run ( '签收当前柜' , async ( ) => this . validateReceivingWarehouse ( await signCurrentReceiving ( { photos , idempotencyKey : key . value } ) , '当前接柜会话' ) ) ; releaseIdempotencyKey ( key . signature ) ; await this . applyCurrentReceiving ( value ) ; this . clearReceiptContext ( ) } catch ( _ ) { } } ,
clearReceiptContext ( ) { receiving . resolvedCarton = null ; receiving . resolvedPallet = null ; this . cartonInput = '' ; this . palletInput = '' ; this . packageCount = 1 ; receiving . activeScanTarget = 'carton' } ,
clearReceiptContext ( ) { receiving . resolvedCarton = null ; receiving . resolvedPallet = null ; this . cartonInput = '' ; this . palletInput = '' ; this . packageCount = 1 ; receiving . activeScanTarget = this . currentDropshipInbound ? 'source-pallet' : 'carton' } ,
async resolveCarton ( raw ) { const cartonMark = String ( raw ? . detail ? . value ? ? raw ? ? this . cartonInput ) . trim ( ) ; if ( ! cartonMark ) return ; if ( this . busy ) { console . warn ( '[Overseas PDA carton] ignored while busy' , { cartonMark } ) ; return this . showToast ( '正在处理上一笔扫描,请稍后重试' , 'warning' ) } console . info ( '[Overseas PDA carton] resolving' , { cartonMark , containerNo : this . current ? . containerNo } ) ; try { const value = await this . run ( '解析箱码' , ( ) => resolveCurrentCarton ( cartonMark ) ) ; console . info ( '[Overseas PDA carton] resolved' , { cartonMark : value . cartonMark , inboundCode : value . inboundCode , destinationType : value . destinationType , pendingPackages : value . pendingPackages } ) ; receiving . resolvedCarton = value ; receiving . resolvedPallet = null ; this . cartonInput = value . cartonMark ; this . palletInput = '' ; this . packageCount = 1 ; playSuccessSound ( ) } catch ( error ) { console . error ( '[Overseas PDA carton] 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 ; 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 , palletCode : this . resolvedPallet . palletCode , packageCount : Number ( this . packageCount ) } : { cartonMark : this . activeInbound . 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 } ) ) ; const current = this . validateReceivingWarehouse ( await getCurrentReceiving ( ) , '当前接柜会话' ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result ; await this . applyCurrentReceiving ( current ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) } catch ( _ ) { } } ,
async submitWholePallet ( raw ) { const sourcePalletNo = String ( raw ? . detail ? . value ? ? raw ? ? this . palletInput ) . trim ( ) ; if ( ! sourcePalletNo || ! this . currentDropshipInbound || this . busy ) return ; this . palletInput = sourcePalletNo ; const payload = { sourcePalletNo } ; const key = idempotencyKey ( 'whole-pallet-receipt' , payload ) ; try { const result = await this . run ( '整托实收' , ( ) => receiveCurrentWholePallet ( { ... payload , idempotencyKey : key . value } ) ) ; const current = this . validateReceivingWarehouse ( await getCurrentReceiving ( ) , '当前接柜会话' ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result ; await this . applyCurrentReceiving ( current ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) } catch ( _ ) { } } ,
async completeContainer ( ) { if ( ! this . allInboundsReconciled ) return ; const key = idempotencyKey ( 'complete' , { containerNo : this . current . containerNo } ) ; try { const value = await this . run ( '完成入库' , async ( ) => this . validateReceivingWarehouse ( await completeCurrentReceiving ( { 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 ) }