@@ -0,0 +1,46 @@
< template >
< view v-if = "user" class="page-shell portal-page" >
< PortalHeader title = "收货" :user = "user" fallback -url = " / pages / home / home " / >
< view class = "portal-content" >
< view v-if = "active" class="summary"><text class="title">当前柜 {{ active.containerNo }}</text><text>{{ active.inboundCodeCount }} 个入仓码 · {{ active.totalPallets }} 托 · {{ active.totalPackages }} 箱</text><text>已收 {{ active.receivedPackages }} · 待收 {{ active.remainingPackages }}</text><text class="locked" > 当前柜完成前不能处理其他柜号 < / text > < / view >
< view v-if = "!active && !busy" class="empty"><text>当前没有已签收柜号</text><button class="primary" @tap="goSign" > 去入库签收 < / button > < / view >
< template v-if = "active" >
< view class = "scan-card" > < text class = "title" > { { scanTitle } } < / text > < input v-model = "scanValue" class="scan-input" :placeholder="scanPhase === 'PALLET' ? '扫描托盘码' : '扫描箱唛'" confirm-type="done" @confirm="submitScan" /><button class="primary" :disabled="busy" @tap="submitScan" > 确认扫描 < / button > < / view >
< view v-if = "cargo" class="fact-card"><text class="title" > 入仓码 {{ cargo.inboundCode }} < / text > < text > { { cargo . skuCode } } · { { cargo . productName || '-' } } < / text > < text > 目的仓 : { { cargo . destinationWarehouseName } } ( { { destinationLabel } } ) < / text > < text > { { cargo . totalPallets } } 托 · 总计 { { cargo . totalPackages } } 箱 < / text > < text > 已收 { { cargo . receivedPackages } } · 待收 { { cargo . remainingPackages } } < / text > < / view >
< view v-if = "pallet" class="fact-card"><text class="title" > 托盘 {{ pallet.palletCode }} < / text > < text > { { pallet . locationCode || '待分配库位' } } · 已装 { { pallet . currentPackages } } 箱 < / text > < / view >
< view v-if = "cargo && pallet && cargo.destinationType === 'FBA'" class="action-card"><text class="title">FBA 按箱数收货</text><input v-model.number="packageCount" class="scan-input" type="number" inputmode="numeric" :placeholder="`最多 ${cargo.remainingPackages} 箱`" /><button class="primary" :disabled="busy" @tap="receiveBulk" > 确认放入 {{ pallet.palletCode }} < / button > < / view >
< view v-if = "cargo?.destinationType === 'OVERSEAS' && pallet" class="continuous" > < text > 海外仓逐箱扫码中 < / text > < text > 当前托盘 { { pallet . palletCode } } , 每扫一箱立即绑定 < / text > < / view >
< button v-if = "active.canComplete" class="complete" :disabled="busy" @tap="completeContainer" > 完成当前柜收货 < / button >
< / template >
< view v-if = "lastResult" class="success">{{ lastResult }}</view><view v-if="error" class="error"><text>{{ error.message }}</text><text v-if="error.code">code={{ error.code }}</text><text v-if="error.requestId">requestId={{ error.requestId }}</text><text v-if="error.traceId" > traceId = { { error . traceId } } < / text > < / view >
< / view >
< / view >
< / template >
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import { session , requireSession } from '@/stores/session.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
import { playErrorSound , playSuccessSound } from '@/utils/audio-feedback.js'
import { completeReceivingContainer , getActiveReceivingContainer , receiveContainerCargo , resolveReceivingCarton , resolveReceivingPallet } from '@/api/receiving.js'
const key = ( action , identity ) => ` ${ action } - ${ identity } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } `
export default {
components : { PortalHeader } ,
data ( ) { return { user : session . currentUser , active : null , cargo : null , pallet : null , scanPhase : 'CARTON' , scanValue : '' , packageCount : 1 , busy : false , error : null , lastResult : '' , pendingKeys : { } } } ,
computed : { scanTitle ( ) { if ( this . scanPhase === 'PALLET' ) return '扫描托盘码' ; return this . pallet && this . cargo ? . destinationType === 'OVERSEAS' ? ` 继续逐箱扫描至 ${ this . pallet . palletCode } ` : '扫描任意一箱' } , destinationLabel ( ) { return this . cargo ? . destinationType === 'FBA' ? 'FBA仓' : '海外仓' } } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; this . loadActive ( ) ; this . startScanner ( ) } , onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } ,
methods : {
async loadActive ( ) { this . busy = true ; this . error = null ; try { this . active = await getActiveReceivingContainer ( ) } catch ( error ) { if ( error . code === 'A0404' ) this . active = null ; else this . error = error } finally { this . busy = false } } ,
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => { this . scanValue = value ; this . submitScan ( ) } } ) } , stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
async submitScan ( ) { const value = this . scanValue . trim ( ) ; if ( ! value || ! this . active ) return ; this . busy = true ; this . error = null ; try { if ( this . scanPhase === 'PALLET' ) { this . pallet = await resolveReceivingPallet ( this . active . containerNo , { palletCode : value , inboundCode : this . cargo . inboundCode } ) ; this . scanPhase = 'CARTON' ; if ( this . cargo . destinationType === 'OVERSEAS' ) await this . receiveCarton ( ) } else { this . cargo = await resolveReceivingCarton ( this . active . containerNo , { cartonMark : value } ) ; if ( this . cargo . destinationType === 'OVERSEAS' && this . pallet ) await this . receiveCarton ( ) ; else this . scanPhase = 'PALLET' ; this . packageCount = Math . max ( 1 , Number ( this . cargo . remainingPackages || 1 ) ) } this . scanValue = '' ; playSuccessSound ( ) } catch ( error ) { this . error = error ; playErrorSound ( ) } finally { this . busy = false } } ,
async receiveCarton ( ) { const signature = ` carton: ${ this . cargo . cartonMark } : ${ this . pallet . palletCode } ` ; const idempotencyKey = this . pendingKeys [ signature ] || ( this . pendingKeys [ signature ] = key ( 'carton' , signature ) ) ; const result = await receiveContainerCargo ( this . active . containerNo , { receiptMode : 'CARTON_SCAN' , cartonMark : this . cargo . cartonMark , palletCode : this . pallet . palletCode , packageCount : 1 , idempotencyKey } ) ; delete this . pendingKeys [ signature ] ; this . lastResult = ` 箱唛 ${ this . cargo . cartonMark } 已放入 ${ this . pallet . palletCode } ` ; this . active = result . container ; this . cargo = null } ,
async receiveBulk ( ) { const count = Number ( this . packageCount ) ; if ( ! Number . isInteger ( count ) || count <= 0 || count > Number ( this . cargo . remainingPackages ) ) return this . error = new Error ( '请输入有效收货箱数' ) ; this . busy = true ; this . error = null ; const signature = ` bulk: ${ this . cargo . inboundCode } : ${ this . pallet . palletCode } : ${ count } ` ; const idempotencyKey = this . pendingKeys [ signature ] || ( this . pendingKeys [ signature ] = key ( 'bulk' , signature ) ) ; try { const result = await receiveContainerCargo ( this . active . containerNo , { receiptMode : 'BULK_COUNT' , sampleCartonMark : this . cargo . cartonMark , inboundCode : this . cargo . inboundCode , palletCode : this . pallet . palletCode , packageCount : count , idempotencyKey } ) ; delete this . pendingKeys [ signature ] ; this . lastResult = ` ${ this . cargo . inboundCode } 已按数量收货 ${ count } 箱 ` ; this . active = result . container ; this . cargo = null ; this . pallet = null ; this . scanPhase = 'CARTON' ; playSuccessSound ( ) } catch ( error ) { this . error = error ; playErrorSound ( ) } finally { this . busy = false } } ,
async completeContainer ( ) { this . busy = true ; this . error = null ; const signature = ` complete: ${ this . active . containerNo } ` ; const idempotencyKey = this . pendingKeys [ signature ] || ( this . pendingKeys [ signature ] = key ( 'complete' , signature ) ) ; try { await completeReceivingContainer ( this . active . containerNo , { idempotencyKey } ) ; delete this . pendingKeys [ signature ] ; this . lastResult = ` 柜号 ${ this . active . containerNo } 已完成收货 ` ; this . active = null ; this . cargo = null ; this . pallet = null ; playSuccessSound ( ) } catch ( error ) { this . error = error ; playErrorSound ( ) } finally { this . busy = false } } ,
goSign ( ) { uni . redirectTo ( { url : '/pages/inbound-sign/inbound-sign' } ) }
}
}
< / script >
< style scoped >
. summary , . scan - card , . fact - card , . action - card , . continuous , . empty , . success , . error { margin - bottom : 12 px ; padding : 14 px ; border - radius : 12 px ; background : # fff ; box - shadow : 0 4 px 15 px rgba ( 32 , 67 , 111 , .07 ) } . summary text , . fact - card text , . continuous text , . empty text , . error text { display : block ; margin - bottom : 7 px } . title { display : block ; margin - bottom : 9 px ; font - weight : 700 } . locked { color : # b54708 } . scan - input { box - sizing : border - box ; width : 100 % ; height : 46 px ; margin : 8 px 0 ; padding : 0 12 px ; border : 1 px solid # cfd8e3 ; border - radius : 8 px } . primary , . complete { width : 100 % ; color : # fff ; background : # 135 ee8 } . complete { margin - top : 14 px ; background : # 16834 b } . continuous { color : # 135 ee8 ; background : # edf4ff } . success { color : # 176 b3a ; background : # eefbf3 } . error { color : # b42318 ; background : # fff1f0 }
< / style >