@@ -8,6 +8,7 @@
< view v-if = "loading" class="state-card"><uni-load-more status="loading" / > < text > 正在恢复当前柜作业 … < / text > < / view >
< template v-else-if = "phase === 'candidates'" >
< view class = "warehouse-scope-card" > < text > 当前 PDA 绑定仓 < / text > < strong > { { boundWarehouseLabel } } < / strong > < text > 仅显示已分配到当前仓的集装箱 < / text > < / view >
< view class = "list-toolbar" > < text > 共 { { candidates . length } } 柜 < / text > < button : disabled = "busy || candidateLoading" @tap ="loadCandidates" > {{ candidateLoading ? ' 刷新中 … ' : ' 刷新 ' }} < / button > < / view >
< 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 >
@@ -17,15 +18,16 @@
< 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 >
< text v-if = "!item.operable" class="candidate-block-reason" > {{ item.blockReason | | ' 当前柜入库计划尚未齐全 ' }} < / text >
< text v-if = "item.destinationCodes?.length" class="candidate-destinations " > 仓码 : {{ item.destinationCodes.join ( ' / ' ) }} < / text >
< text class = "candidate-receiving-warehouse " > 海外接柜仓 : { { receivingWarehouseLabel ( item ) } } < / text >
< text v-if = "item.destinationCodes?.length" class="candidate-destinations" > 业务目的仓 : {{ item.destinationCodes.join ( ' / ' ) }} < / text >
< / view >
< / template >
< template v-else-if = "phase === 'detail'" >
< template v-if = "selected?.operable" >
< ReceivingSessionSummary :session = "selected" / >
< view class = "lock-notice" > 选择后账号将锁定当前柜 , 完成前不能切换其他柜号 。 < / view >
< view class = "sticky-action" > < button class = "ghost-button" :disabled = "busy" @tap ="cancelDetail" > 返回列表 < / button > < button class = "primary-button" : disabled = "busy || !selected.allPlansReady || !selected.operable" @tap ="prepareContainer" > 选择此柜 并开始拍照< / button > < / view >
< view class = "lock-notice" > 接柜仓由平台预先分配 ; 确认后只建立当前账号的接柜会话 , 完成前不能切柜 。 < / view >
< view class = "sticky-action" > < button class = "ghost-button" :disabled = "busy" @tap ="cancelDetail" > 返回列表 < / button > < button class = "primary-button" : disabled = "busy || !selected.allPlansReady || !selected.operable" @tap ="prepareContainer" > 确认接柜仓 并开始拍照< / button > < / view >
< / template >
< template v-else >
< view class = "readonly-candidate-card" >
@@ -33,6 +35,7 @@
< view class = "candidate-facts" > < view > < strong > { { selected ? . totalOrderCount } } < / strong > < text > 总订单 < / text > < / view > < view > < strong > { { selected ? . inboundCodeCount } } < / strong > < text > 入仓码 < / text > < / view > < view > < strong > { { selected ? . planCount } } < / strong > < text > 已生成计划 < / text > < / view > < view > < strong > { { selected ? . pendingPlanCount } } < / strong > < text > 未操作 < / text > < / view > < / view >
< text class = "candidate-progress-copy" > 已生成 { { selected ? . planCount } } 个入库计划 , { { selected ? . pendingPlanCount } } 个未操作 < template v-if = "selected?.missingInboundCodeCount" > , 其中 {{ selected.missingInboundCodeCount }} 个缺入仓码 < / template > < / text >
< text class = "candidate-block-reason" > { { selected ? . blockReason || '当前柜入库计划尚未齐全' } } < / text >
< text class = "candidate-receiving-warehouse" > 海外接柜仓 : { { receivingWarehouseLabel ( selected ) } } < / text >
< / view >
< view class = "sticky-action one" > < button class = "ghost-button" :disabled = "busy" @tap ="cancelDetail" > 返回列表 < / button > < / view >
< / template >
@@ -111,12 +114,12 @@
import PortalHeader from '@/components/PortalHeader.vue'
import ReceivingSessionSummary from '@/components/ReceivingSessionSummary.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { isNoCurrentReceiving , isReceivingConflict , RECEIVING _PHOTO _ROLES } from '@/api/receiving-adapter.js'
import { session , requireSession } from '@/stores/session.js'
import { assertReceivingWarehouse , isNoCurrentReceiving , isReceivingConflict , RECEIVING _PHOTO _ROLES } from '@/api/receiving-adapter.js'
import { applyWarehouseProfile , session , requireSession } from '@/stores/session.js'
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 , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
import { completeCurrentReceiving , downloadCurrentUnloadPhoto , getCurrentReceiving , getCurrentWarehouseProfile , getReceivingContainer , listReceivingCandidates , prepareReceivingContainer , receiveCurrentBulk , receiveCurrentCarton , resolveCurrentCarton , resolveCurrentPallet , signCurrentReceiving , uploadCurrentUnloadPhoto } from '@/api/inbound.js'
const MAX _UNLOAD _PHOTO _BYTES = 10 * 1024 * 1024
export default {
@@ -132,6 +135,7 @@
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 ) } ,
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 ] } ,
pageSubtitle ( ) { return { candidates : '选择集装箱后拍摄 3 张照片完成签收' , detail : '核对柜号与整柜计划箱数' , sign : '完成现场取证后进入收货' , receive : '扫描箱码定位入仓码后开始收货' , completed : '库存与柜级进度已由服务端核平' } [ this . phase ] }
} ,
@@ -143,28 +147,31 @@
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 ) : '待确认' } ,
destinationLabel ( item ) { return item ? . destinationType === 'FBA' ? ( item . fbaWarehouseCode || 'FBA' ) : ( item ? . destinationWarehouseName || item ? . destinationWarehouseCode || '海外仓' ) } ,
receivingWarehouseLabel ( item ) { const code = String ( item ? . overseasWarehouseCode || '' ) . trim ( ) ; const name = String ( item ? . overseasWarehouseName || '' ) . trim ( ) ; return code && name ? ` ${ code } · ${ name } ` : '—' } ,
validateReceivingWarehouse ( value , label ) { return assertReceivingWarehouse ( value , this . user , label ) } ,
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 ) } ,
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 } } ,
setScanTargetForCurrent ( value ) { receiving . activeScanTarget = value ? . status === 'WAIT_SIGN' ? 'container' : '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 ) { setCurrentReceiving ( value ) ; await this . restorePhotoPreviews ( ) ; this . setScanTargetForCurrent ( value ) } ,
async bootstrap ( ) { if ( this . loading ) return ; this . loading = true ; this . error = null ; this . detailOpen = false ; try { const current = await getCurrentReceiving ( ) ; await this . applyCurrentReceiving ( current ) ; this . candidateError = null ; this . bootstrapped = true } catch ( error ) { if ( isNoCurrentReceiving ( error ) ) { resetReceivingDraft ( ) ; await this . loadCandidates ( false ) ; this . bootstrapped = true } else this . candidateError = error } finally { this . loading = false } } ,
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 ) } 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 ) { if ( this . candidateLoading ) return ; this . candidateLoading = true ; this . candidateError = null ; this . error = null ; try { receiving . candidates = await listReceivingCandidates ( ) ; if ( feedback ) this . showToast ( '刷新待卸柜成功' , 'success' ) } catch ( error ) { 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 ; if ( ! candidate . operable ) { receiving . selected = candidate ; this . detailOpen = true ; return } try { const detail = await this . run ( '查询柜号' , ( ) => getReceivingContainer ( containerNo ) ) ; receiving . selected = { ... candidate , ... detail , allPlansReady : candidate . allPlansReady , operable : candidate . operable , blockReason : candidate . blockReason } ; this . detailOpen = true ; playSuccessSound ( ) } catch ( error ) { await this . recoverCurrentAfterConflict ( error ) } } ,
async loadCandidates ( feedback = true ) { if ( this . candidateLoading ) return ; this . candidateLoading = true ; this . candidateError = null ; this . error = null ; try { 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 ) ) { 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 { 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 , true ) } } ,
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 } } ,
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 } ) } ,
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 ( '签收当前柜' , ( ) => signCurrentReceiving ( { photos , idempotencyKey : key . value } ) ) ; releaseIdempotencyKey ( key . signature ) ; setCurrentReceiving ( value ) ; this . clearReceiptContext ( ) } catch ( _ ) { } } ,
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' } ,
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 } ) ) ; releaseIdempotencyKey ( key . signature ) ; receiving . lastReceipt = result ; setCurrentReceiving ( await getCurrentReceiving ( ) ) ; this . clearReceiptContext ( ) ; playSuccessSound ( ) } 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 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 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 ) }
}
@@ -202,4 +209,5 @@
. sticky - action button { display : flex ; align - items : center ; justify - content : center ; line - height : normal ; text - align : center }
. list - toolbar { height : 28 px ; margin : 0 2 px 7 px ; align - items : center } . list - toolbar button { box - sizing : border - box ; width : auto ; min - width : 48 px ; height : 28 px ; min - height : 28 px ; margin : 0 ; padding : 0 8 px ; border : 1 px solid # bdd1eb ; border - radius : 7 px ; color : # 1668 dc ; font - size : 11 px ; line - height : 26 px ; background : # f7faff } . container - card - compact { padding : 11 px 13 px ; cursor : pointer ; transition : background - color .12 s ease , border - color .12 s ease } . container - card - pressed { border - color : # 93 baf0 ; background : # f4f8ff } . container - card - actions { display : flex ; flex - shrink : 0 ; align - items : flex - end ; flex - direction : column ; gap : 5 px } . container - detail - link { box - sizing : border - box ; min - height : 24 px ; padding : 0 4 px ; color : # 1668 dc ; font - size : 11 px ; font - weight : 700 ; line - height : 24 px } . candidate - facts { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; margin - top : 9 px ; padding : 8 px 0 ; border - top : 1 px solid # edf1f5 ; border - bottom : 1 px solid # edf1f5 } . candidate - facts view { display : flex ; align - items : center ; flex - direction : column ; border - right : 1 px solid # edf1f5 } . candidate - facts view : last - child { border - right : 0 } . candidate - facts strong { color : # 17324 d ; font - size : 17 px ; line - height : 21 px } . candidate - facts text { margin - top : 2 px ; color : # 6 f8194 ; font - size : 10 px } . candidate - destinations { display : block ; margin - top : 7 px ; overflow : hidden ; color : # 6 f8194 ; font - size : 11 px ; line - height : 17 px ; text - overflow : ellipsis ; white - space : nowrap }
. candidate - arrival { display : block ; margin - top : 3 px ; color : # 718095 ; font - size : 10 px ; line - height : 15 px } . candidate - progress - copy { display : block ; margin - top : 7 px ; color : # 435 a72 ; font - size : 10 px ; line - height : 16 px } . candidate - block - reason { display : block ; margin - top : 6 px ; padding : 6 px 8 px ; border - radius : 7 px ; color : # 73551 b ; font - size : 10 px ; line - height : 16 px ; background : # f0e9dc } . container - card - disabled { border - color : # d9dee5 ; background : # f1f3f5 ; box - shadow : none } . container - card - disabled . container - no , . container - card - disabled . candidate - facts strong { color : # 778391 } . container - card - disabled . container - detail - link { color : # 788797 } . container - card - disabled . candidate - progress - copy , . container - card - disabled . candidate - arrival , . container - card - disabled . candidate - destinations { color : # 87919 d } . status - disabled { color : # 788491 ; background : # e1e5e9 } . readonly - candidate - card { padding : 13 px ; border : 1 px solid # d9dee5 ; border - radius : 13 px ; background : # f1f3f5 } . readonly - candidate - card . candidate - block - reason { margin - top : 9 px }
. warehouse - scope - card { display : flex ; flex - direction : column ; gap : 3 px ; margin - bottom : 10 px ; padding : 10 px 12 px ; border : 1 px solid # b8d2f3 ; border - radius : 10 px ; color : # 617389 ; font - size : 10 px ; line - height : 15 px ; background : # f2f7ff } . warehouse - scope - card strong { color : # 17324 d ; font - size : 13 px } . candidate - receiving - warehouse { display : block ; margin - top : 7 px ; color : # 315777 ; font - size : 11 px ; font - weight : 700 ; line - height : 17 px }
< / style >