@@ -39,7 +39,8 @@
< text v-else class = "counted-quantity" > 已清点 { { line . actualQuantity } } 件 < / text >
< view v-if = "!isResolvedPendingCarton && lineDiffers(line)" class="field stacked difference-field"><text>差异说明</text><textarea v-model="line.discrepancyRemark" maxlength="500" placeholder="数量不一致时必填" / > < / view >
< / view >
< text v-if = "c urrentPallet" class="pallet-hint" > 归入 {{ currentPallet.palletNo }} < / text >
< text v-if = "assignableC urrentPallet" class="pallet-hint" > 归入 {{ assignableCurrentPallet.palletNo }} < / text >
< text v-else-if = "currentPallet?.status === 'SEALED'" class="pallet-hint blocked" > 当前托盘已封托 , 请先解除封托或扫描其他托盘 < / text >
< text v-else-if = "isResolvedPendingCarton" class="pallet-hint" > 本箱已清点 , 请先扫描目标托盘 < / text >
< button class = "primary-button" : disabled = "!canSubmitCarton || submitting" @tap ="confirmCarton" > {{ cartonActionLabel }} < / button >
< / view >
@@ -62,17 +63,26 @@
< text class = "pallet-meta" > { { currentPallet . cartonCount || 0 } } 箱 · { { currentPallet . skuCount || 0 } } SKU · { { currentPallet . weightKg || 0 } } kg · { { currentPallet . volumeCbm || 0 } } m³ < / text >
< view v-for = "item in (currentPalletDetails?.cartons || [])" :key="item.cartonId" class="carton-row"><text>{{ item.boxCode }}</text><button v-if="currentPallet.status === 'OPEN'" class="text-button" :disabled="submitting" @tap="moveCarton(item, null)" > 移回待绑托 < / button > < / view >
< button v-if = "currentPallet.palletId && currentPallet.status === 'OPEN'" class="primary-button" :disabled="!currentPallet.cartonCount || submitting" @tap="sealPallet(currentPallet)" > 封托 < / button >
< button v-if = "currentPallet.palletId && currentPallet.status === 'SEALED'" class="secondary-button unseal-button" :disabled="submitting" @tap="openUnseal(currentPallet)" > 解除封托 < / button >
< / view >
< / view >
< / template >
< / view >
< uni -popup ref = "unsealPopup" type = "center" :is-mask-click = "true" @change ="handleUnsealPopupChange" >
< view class = "unseal-dialog" role = "dialog" aria -modal = " true " aria -label = " 确认解除封托 " >
< PopupHeader title = "确认解除封托?" tone = "danger" @close ="closeUnseal" / >
< text class = "unseal-copy" > 托盘 { { unsealTarget ? . palletNo || '—' } } 将恢复为开放状态 , 可继续移箱 、 归托和重新封托 。 < / text >
< view class = "unseal-actions" > < button class = "secondary-button" @tap ="closeUnseal" > 取消 < / button > < button class = "danger-button" :disabled = "submitting" @tap ="confirmUnseal" > 确认解除 < / button > < / view >
< / view >
< / uni -popup >
< / view >
< / template >
< script >
import PortalHeader from '@/components/PortalHeader.vue'
import PopupHeader from '@/components/PopupHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { bindDropshipPendingCartons , claimDropshipPalletizationTask , countDropshipCarton , getDropshipPalletizationTask , moveDropshipCarton , resolveDropshipCarton , sealDropshipPallet } from '@/api/pda-dropship.js'
import { bindDropshipPendingCartons , claimDropshipPalletizationTask , countDropshipCarton , getDropshipPalletizationTask , moveDropshipCarton , resolveDropshipCarton , sealDropshipPallet , unsealDropshipPallet } from '@/api/pda-dropship.js'
import { session , requireSession } from '@/stores/session.js'
import { formatApiError } from '@/utils/user-facing.js'
import { stableOperationId } from '@/utils/idempotency.js'
@@ -82,8 +92,8 @@
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
export default {
components : { PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , taskId : '' , warehouseId : '' , task : null , palletCode : '' , selectedPalletId : '' , selectedPalletNo : '' , boxCode : '' , carton : null , skuCounts : [ ] , activeScan : 'boxCode' , loading : false , submitting : false , error : '' , notice : '' } } ,
components : { PopupHeader , PortalHeader , ScanTargetInput } ,
data ( ) { return { user : session . currentUser , taskId : '' , warehouseId : '' , task : null , palletCode : '' , selectedPalletId : '' , selectedPalletNo : '' , boxCode : '' , carton : null , skuCounts : [ ] , activeScan : 'boxCode' , loading : false , submitting : false , error : '' , notice : '' , unsealOpen : false , unsealTarget : null } } ,
computed : {
statusLabel ( ) { return this . task ? . taskStatus === 'COMPLETED' ? '已完成' : ( this . task ? . taskStatus === 'CLAIMED' ? '作业中' : '待领取' ) } ,
palletSetupReady ( ) { return this . task ? . palletSetupReady === true } ,
@@ -91,15 +101,17 @@
canResolveCarton ( ) { return Boolean ( this . palletSetupReady && this . task ? . taskStatus === 'CLAIMED' && String ( this . boxCode || '' ) . trim ( ) && ! this . loading && ! this . submitting ) } ,
canCount ( ) { return Boolean ( this . carton ? . canCount && this . skuCounts . length && this . skuCounts . every ( ( line ) => Number . isInteger ( Number ( line . actualQuantity ) ) && Number ( line . actualQuantity ) >= 0 && ( ! this . lineDiffers ( line ) || String ( line . discrepancyRemark || '' ) . trim ( ) ) ) ) } ,
isResolvedPendingCarton ( ) { return this . carton ? . countStatus === 'COUNTED_UNBOUND' } ,
canRebindResolvedCarton ( ) { return Boolean ( this . isResolvedPendingCarton && this . carton ? . cartonId && this . currentPallet ? . palletNo ) } ,
canSubmitCarton ( ) { return this . isResolvedPendingCarton ? this . canRebindResolvedCarton : this . canCount } ,
canRebindResolvedCarton ( ) { return Boolean ( this . isResolvedPendingCarton && this . carton ? . cartonId && this . assignableCurrentPallet ? . palletNo ) } ,
canSubmitCarton ( ) { return this . currentPallet ? . status === 'SEALED' ? false : ( this . isResolvedPendingCarton ? this . canRebindResolvedCarton : this . canCount ) } ,
cartonActionLabel ( ) { return this . isResolvedPendingCarton ? '确认归入当前托盘' : '确认清点' } ,
currentPalletDetails ( ) { return ( this . task ? . pallets || [ ] ) . find ( ( item ) => item . palletNo === this . selectedPalletNo ) || null } ,
currentPallet ( ) { return this . currentPalletDetails ? . status === 'OPEN' ? this . currentPalletDetails : ( this . selectedPalletNo ? { palletNo : this . selectedPalletNo , status : 'OPEN' } : null ) }
currentPallet ( ) { return this . currentPalletDetails || ( this . selectedPalletNo ? { palletNo : this . selectedPalletNo , status : 'OPEN' } : null ) } ,
assignableCurrentPallet ( ) { return this . currentPallet ? . status === 'OPEN' ? this . currentPallet : null }
} ,
onLoad ( options ) { this . taskId = String ( options . taskId || '' ) ; this . warehouseId = String ( options . warehouseId || '' ) } ,
onShow ( ) { if ( ! requireSession ( ) ) return ; this . user = session . currentUser ; const draft = restoreDropshipPalletizingDraft ( this . draftIdentity ( ) ) ; this . selectedPalletNo = draft ? . activePalletNo || '' ; this . startScanner ( ) ; this . loadTask ( ) } ,
onHide ( ) { this . stopScanner ( ) } , onUnload ( ) { this . stopScanner ( ) } , beforeUnmount ( ) { this . stopScanner ( ) } ,
onBackPress ( ) { if ( ! this . unsealOpen ) return false ; this . closeUnseal ( ) ; return true } ,
methods : {
startScanner ( ) { this . stopScanner ( ) ; this . _stopScanner = startScanBroadcast ( { onScan : ( value ) => this . handleScan ( value ) , onError : ( ) => { this . error = '扫码头连接失败,可使用输入框继续作业' } } ) } ,
stopScanner ( ) { if ( this . _stopScanner ) this . _stopScanner ( ) ; this . _stopScanner = null } ,
@@ -123,7 +135,7 @@
} ,
async countCarton ( ) {
const skuLines = this . skuCounts . map ( ( line ) => ( { inboundDetailId : line . inboundDetailId , skuCode : line . skuCode , actualQuantity : Number ( line . actualQuantity ) , discrepancyAction : this . lineDiffers ( line ) ? 'ACCEPT_VARIANCE' : 'MATCH' , discrepancyRemark : this . lineDiffers ( line ) ? String ( line . discrepancyRemark || '' ) . trim ( ) : null } ) )
const result = await this . runMutation ( ( ) => countDropshipCarton ( this . taskId , { warehouseId : this . warehouseId , boxCode : this . carton . boxCode , palletNo : this . currentPallet ? . palletNo || null , skuLines } ) , this . currentPallet ? '本箱已清点并归托' : '本箱已完成入库清点' )
const result = await this . runMutation ( ( ) => countDropshipCarton ( this . taskId , { warehouseId : this . warehouseId , boxCode : this . carton . boxCode , palletNo : this . assignableCurrentPallet ? . palletNo || null , skuLines } ) , this . assignableCurrentPallet ? '本箱已清点并归托' : '本箱已完成入库清点' )
if ( result ) { this . boxCode = '' ; this . carton = null ; this . skuCounts = [ ] }
} ,
async confirmCarton ( ) {
@@ -132,7 +144,7 @@
} ,
async rebindResolvedCarton ( ) {
if ( ! this . canRebindResolvedCarton ) return
const targetPalletNo = this . currentPallet . palletNo
const targetPalletNo = this . assignableCurrentPallet . palletNo
const payload = { warehouseId : this . warehouseId , targetPalletNo }
payload . operationId = stableOperationId ( 'dropship-move' , String ( this . user ? . id ) + '-' + this . taskId , { cartonId : this . carton . cartonId , taskVersion : this . task . version , ... payload } )
const result = await this . runMutation ( ( ) => moveDropshipCarton ( this . taskId , this . carton . cartonId , payload ) , ` 本箱已归入 ${ targetPalletNo } ` )
@@ -143,8 +155,8 @@
async selectPallet ( value ) {
if ( ! this . palletSetupReady ) { this . error = this . palletSetupMessage ; return }
const code = String ( value || this . palletCode || '' ) . trim ( ) . toUpperCase ( ) ; const pallet = ( this . task ? . pallets || [ ] ) . find ( ( item ) => item . palletNo === code )
if ( pallet && pallet . status !== 'OPEN' ) { this . error = '托盘已封托,不能继续归托' ; return }
this . palletCode = code ; this . selectedPalletId = pallet ? String ( pallet . palletId ) : '' ; this . selectedPalletNo = code ; this . activeScan = 'boxCode' ; saveDropshipPalletizingDraft ( this . draftIdentity ( ) , { activePalletNo : code } )
if ( pallet ? . status === 'SEALED' ) { this . notice = ` 托盘 ${ code } 已封托,可在托盘操作区解除封托 ` ; return }
if ( Number ( this . task ? . unboundCartons || 0 ) > 0 ) await this . runMutation ( ( ) => bindDropshipPendingCartons ( this . taskId , this . warehouseId , code , this . task . version ) , ` 待归托箱已绑定至 ${ code } ` )
else this . notice = pallet ? ` 当前托盘: ${ code } ` : ` 已选择仓库开放托盘 ${ code } ,清点箱码时将校验并归托 `
} ,
@@ -156,7 +168,17 @@
await this . runMutation ( ( ) => moveDropshipCarton ( this . taskId , carton . cartonId , payload ) , targetPalletNo ? '箱码已换托' : '箱码已移回待绑托区' )
} ,
continueOtherTask ( ) { uni . redirectTo ( { url : ` /pages/dropship-tasks/dropship-tasks?warehouseId= ${ encodeURIComponent ( this . warehouseId ) } ` } ) } ,
async sealPallet ( pallet ) { const result = await this . runMutation ( ( ) => sealDropshipPallet ( this . taskId , this . warehouseId , pallet ) , ` ${ pallet . palletNo } 已封托 ` ) ; if ( result && this . selectedPalletNo === pallet . palletNo ) { this . selectedPalletId = '' ; this . selectedPalletNo = '' ; this . palletCode = '' } } ,
async sealPallet ( pallet ) { await this . runMutation ( ( ) => sealDropshipPallet ( this . taskId , this . warehouseId , pallet ) , ` ${ pallet . palletNo } 已封托 ` ) } ,
openUnseal ( pallet ) { this . unsealTarget = pallet ; this . unsealOpen = true ; this . $refs . unsealPopup . open ( ) } ,
closeUnseal ( ) { this . unsealOpen = false ; this . $refs . unsealPopup ? . close ( ) ; this . unsealTarget = null } ,
handleUnsealPopupChange ( event ) { this . unsealOpen = Boolean ( event . show ) ; if ( ! event . show ) this . unsealTarget = null } ,
async confirmUnseal ( ) {
const pallet = this . unsealTarget ; if ( ! pallet || this . submitting ) return
this . $refs . unsealPopup . close ( ) ; this . unsealOpen = false
const result = await this . runMutation ( ( ) => unsealDropshipPallet ( this . taskId , this . warehouseId , pallet ) , ` ${ pallet . palletNo } 已解除封托 ` )
this . unsealTarget = null
if ( result ) { this . selectedPalletNo = pallet . palletNo ; saveDropshipPalletizingDraft ( this . draftIdentity ( ) , { activePalletNo : pallet . palletNo } ) }
} ,
async runMutation ( action , message ) {
if ( this . submitting ) return null
if ( ! isOnline ( ) ) { this . error = '当前网络不可用,请联网后刷新' ; return null }
@@ -190,6 +212,8 @@
. compact - metrics { display : grid ; grid - template - columns : repeat ( 5 , 1 fr ) ; margin - top : 8 px ; padding - top : 7 px ; border - top : 1 px solid # edf1f5 ; } . compact - metrics > text { display : flex ; align - items : center ; justify - content : center ; gap : 3 px ; color : # 667085 ; font - size : 11 px ; white - space : nowrap ; } . compact - metrics strong { color : # 12233 f ; font - size : 15 px ; } . compact - metrics . pallet - readiness { color : # 067647 ; } . compact - metrics . pallet - readiness . invalid , . compact - metrics . pallet - readiness . invalid strong { color : # b42318 ; } . pallet - setup - error { margin - top : 8 px ; margin - bottom : 0 ; text - align : left ; }
. section - title { display : block ; color : # 12233 f ; font - size : 18 px ; font - weight : 700 ; } . section - desc { display : block ; margin : 2 px 0 8 px ; color : # 667085 ; font - size : 13 px ; line - height : 18 px ; } . section - heading > view { min - width : 0 ; }
. primary - button , . secondary - button { box - sizing : border - box ; width : 100 % ; height : 46 px ; margin - top : 10 px ; border - radius : 9 px ; font - size : 14 px ; font - weight : 700 ; line - height : 44 px ; } . primary - button { border : 0 ; color : # fff ; background : # 1 a73e8 ; } . secondary - button { border : 1 px solid # 8 ab8ef ; color : # 155 eef ; background : # f6f9ff ; } . primary - button [ disabled ] , . secondary - button [ disabled ] { opacity : .45 ; }
. unseal - button { border - color : # fda29b ; color : # b42318 ; background : # fff5f4 ; } . pallet - hint . blocked { color : # b42318 ; }
. unseal - dialog { box - sizing : border - box ; width : min ( 350 px , calc ( 100 vw - 32 px ) ) ; overflow : hidden ; border - radius : 12 px ; background : # fff ; } . unseal - copy { display : block ; padding : 18 px 16 px ; color : # 475467 ; font - size : 14 px ; line - height : 21 px ; } . unseal - actions { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 10 px ; padding : 0 16 px 16 px ; } . unseal - actions . secondary - button { margin - top : 0 ; } . danger - button { height : 46 px ; margin : 0 ; border : 0 ; border - radius : 9 px ; color : # fff ; font - size : 14 px ; font - weight : 700 ; background : # d92d20 ; } . danger - button [ disabled ] { opacity : .45 ; }
. count - entry { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; margin - top : 6 px ; padding : 5 px 7 px ; border : 0 ; border - radius : 0 ; color : # 667085 ; font - size : 13 px ; background : # f6f9ff ; } . compact - action { width : 86 px ; height : 34 px ; margin : 0 ; border : 0 ; border - radius : 6 px ; color : # fff ; font - size : 13 px ; line - height : 34 px ; background : # 1 a73e8 ; } . compact - action [ disabled ] { opacity : .35 ; } . count - unavailable { padding : 12 px 8 px ; color : # b42318 ; font - size : 13 px ; line - height : 18 px ; text - align : center ; background : # fff1f0 ; } . pallet - scan - block { margin - top : 8 px ; padding - top : 8 px ; border - top : 1 px solid # e6edf5 ; } . field - label { display : block ; margin - bottom : 5 px ; color : # 344054 ; font - size : 15 px ; font - weight : 700 ; } . field - label text { margin - left : 3 px ; color : # 98 a2b3 ; font - size : 12 px ; font - weight : 400 ; }
. carton - card , . pallet - card { margin - top : 8 px ; padding : 8 px 0 0 ; border : 0 ; border - top : 1 px solid # e3eaf2 ; border - radius : 0 ; background : transparent ; } . sku - count - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 8 px ; padding : 8 px 0 ; border - bottom : 1 px solid # edf1f5 ; } . sku - copy { min - width : 0 ; } . sku - label { display : block ; color : # 667085 ; font - size : 11 px ; } . sku - code { display : block ; margin - top : 2 px ; color : # 12233 f ; font - size : 16 px ; font - weight : 700 ; overflow - wrap : anywhere ; } . pallet - meta { display : block ; margin - top : 5 px ; color : # 667085 ; font - size : 14 px ; line - height : 20 px ; }
. quantity - stepper { display : grid ; grid - template - columns : 38 px 56 px 38 px ; overflow : hidden ; border : 1 px solid # b8c6d8 ; border - radius : 8 px ; background : # fff ; } . quantity - stepper button , . quantity - stepper input { box - sizing : border - box ; height : 40 px ; margin : 0 ; border : 0 ; border - radius : 0 ; text - align : center ; background : # fff ; } . quantity - stepper button { color : # 155 eef ; font - size : 20 px ; line - height : 40 px ; } . quantity - stepper input { width : 56 px ; border - right : 1 px solid # dce4ed ; border - left : 1 px solid # dce4ed ; color : # 12233 f ; font - size : 15 px ; } . difference - field { grid - column : 1 / - 1 ; margin - top : 0 ; } . pallet - hint { display : block ; margin - top : 10 px ; color : # 155 eef ; font - size : 11 px ; text - align : center ; }