@@ -9,6 +9,15 @@
< button v-for = "binding in currentBindings" :key="binding.mark" class="binding-row" :disabled="busy" @tap="selectRebind(binding)"><text class="binding-mark">{{ binding.mark }}</text><text class="binding-arrow">→</text><text class="binding-pallet">{{ binding.pallet }}</text><text class="binding-action" > 重新绑定 < / text > < / button >
< / view >
< view v-for = "pallet in boundPalletTotals" :key="pallet.palletCode" class="hint" > < text > { { pallet . palletCode } } · { { pallet . totalPackages } } 箱 · 体积 { { pallet . volumeCbm == null ? '待补测' : pallet . volumeCbm + ' m³' } } · 重量 { { pallet . grossWeightKg == null ? '待补测' : pallet . grossWeightKg + ' kg' } } < / text > < / view >
< view v-for = "pallet in measurements" :key="pallet.palletCode" class="measurement-card" >
< text class = "heading" > { { pallet . palletCode } } · { { pallet . packages } } 箱 < / text >
< text class = "hint" > { { pallet . inherited ? '原托盘货物未变,沿用原测量;包装变化请重新测量。' : '拆托、合托后请确认本托最终尺寸和毛重,原托盘剩余货物也需补测。' } } < / text >
< text v-if = "pallet.volumeCbm != null" class="hint" > 已确认 : {{ pallet.volumeCbm }} m ³ · {{ pallet.grossWeightKg }} kg < / text >
< view class = "measurement-inputs" >
< label v-for = "field in measurementFields" :key="field.key"><text>{{ field.label }}</text><input v-model="pallet[field.key]" type="digit" :disabled="busy" :placeholder="field.label" / > < / label >
< / view >
< button :disabled = "busy" @tap ="saveMeasurement(pallet)" > 确认本托实测 < / button >
< / view >
< view v-if = "rebindSelection"><text class="hint">正在改绑:{{ rebindSelection.mark }},当前托盘:{{ rebindSelection.pallet }}</text><button :disabled="busy" @tap="rebindSelection = null; targetCode = ''; target = null" > 取消改绑 < / button > < / view >
< template v-if = "!allBindingsComplete && !rebindSelection" >
< view v-for = "row in operableSourceRows" :key="row.positionId" class="source-row" :class="{ selected: selectedSource?.positionId === row.positionId }" @tap="selectSource(row)" >
@@ -34,6 +43,7 @@
< / template >
< template v-if = "!allBindingsComplete || rebindSelection" >
< ScanTargetInput v-model = "targetCode" name="relabel-target" label="目标托盘" placeholder="扫描同目的仓目标托盘或新托盘号" :active="active === 'target'" @activate="activate('target')" @submit="confirmTargetAndBind" / >
< button v-if = "canRetainOriginal" :disabled="busy" @tap="quantity = String(source.totalPackages); targetCode = source.palletCode; confirmTargetAndBind()" > 整托全部贴标 , 保留原托盘 < / button >
< button : disabled = "busy || !targetCode.trim()" @tap ="confirmTargetAndBind" > {{ rebindSelection ? ' 确认改绑目标托盘 ' : ' 确认绑定目标托盘 ' }} < / button >
< text v-if = "labelBlockReason" class="hint" > {{ labelBlockReason }} < / text >
< text v-if = "target" class="hint" > 已识别目标 : {{ target.palletCode }} , 尚未绑定成功 < / text >
@@ -44,16 +54,17 @@
< / template >
< script >
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { resolvePallet , createRelabelPallet , bindRelabelPallet , findRelabelSources } from '@/api/pallet.js'
import { resolvePallet , createRelabelPallet , bindRelabelPallet , findRelabelSources , listRelabelPalletMeasurements , measureRelabelPallet } from '@/api/pallet.js'
import { warehouseIdempotencyKey , releaseWarehouseKey } from '@/stores/warehouse-work.js'
export default {
components : { ScanTargetInput } ,
props : { task : { type : Object , required : true } , allowDisassemble : { type : Boolean , default : true } } ,
emits : [ 'activate' , 'busy' , 'saved' ] ,
data ( ) { return { palletTotals : { } , rebindSelection : null , active : 'source' , sourceCode : '' , targetCode : '' , source : null , target : null , marks : [ ] , busy : false , error : '' , message : '' , newPallet : null , sourceRows : [ ] , selectedSource : null , quantity : '' , selectedDestination : '' } } ,
data ( ) { return { measurements : [ ] , measurementFields : [ { key : 'lengthCm' , label : '长 cm' } , { key : 'widthCm' , label : '宽 cm' } , { key : 'heightCm' , label : '高 cm' } , { key : 'grossWeightKg' , label : '毛重 kg' } ] , palletTotals : { } , rebindSelection : null , active : 'source' , sourceCode : '' , targetCode : '' , source : null , target : null , marks : [ ] , busy : false , error : '' , message : '' , newPallet : null , sourceRows : [ ] , selectedSource : null , quantity : '' , selectedDestination : '' } } ,
mounted ( ) { this . loadSources ( ) } ,
computed : {
boundPalletTotals ( ) { const codes = new Set ( this . currentBindings . map ( binding => binding . pallet ) ) ; return Object . values ( this . palletTotals ) . filter ( pallet => codes . has ( pallet . palletCode ) ) } ,
canRetainOriginal ( ) { return ! this . isReturnBulk && ! this . unboundSourceMode && this . source && this . selectedSource && this . plannedPackages === Number ( this . source . totalPackages ) } ,
boundPalletTotals ( ) { const codes = new Set ( this . currentBindings . map ( binding => binding . pallet ) ) ; return Object . values ( this . palletTotals ) . filter ( pallet => codes . has ( pallet . palletCode ) && ! this . measurements . some ( measured => measured . palletCode === pallet . palletCode ) ) } ,
allBindingsComplete ( ) {
const required = ( this . task . items || [ ] ) . filter ( item => Number ( item . expectedPackages || 0 ) > 0 && ( item . returnCartonId || [ 'CARTON_LABEL' , 'CARTON_AND_SKU' , 'RETURN_BULK_SKU_LABEL' ] . includes ( item . operationType ) ) )
const bound = new Set ( this . currentBindings . map ( binding => binding . mark ) )
@@ -89,9 +100,21 @@ export default {
} ,
wholeSource ( ) { return this . isReturnBulk && this . selectedSource && ( ! this . allowDisassemble || this . plannedPackages === Number ( this . selectedSource . stockPackages || 0 ) ) } ,
unboundSourceMode ( ) { return ! this . isReturnBulk && this . returnCartonItems . length > 0 && ( this . selectedSource ? ( ! this . selectedSource . palletCode || this . selectedSource . palletCode === 'UNBOUND' ) : ! this . operableSourceRows . some ( row => row . palletCode && row . palletCode !== 'UNBOUND' ) ) } ,
readyToBind ( ) { return this . target && this . selectedSource && ( this . unboundSourceMode || ( this . source && this . samePallet ( this . source , this . selectedSource ) && ! this . samePallet ( this . source , this . target ) ) ) && Number . isSafeInteger ( Number ( this . quantity ) ) && Number ( this . quantity ) > 0 && Number ( this . quantity ) <= this . selectedSource . movablePackages && this . bindingSelectionValid ( ) }
readyToBind ( ) { return this . target && this . selectedSource && ( this . unboundSourceMode || ( this . source && this . samePallet ( this . source , this . selectedSource ) && ( ! this . samePallet ( this . source , this . target ) || this . canRetainOriginal ) ) ) && Number . isSafeInteger ( Number ( this . quantity ) ) && Number ( this . quantity ) > 0 && Number ( this . quantity ) <= this . selectedSource . movablePackages && this . bindingSelectionValid ( ) }
} ,
methods : {
async saveMeasurement ( pallet ) {
const dimensions = Object . fromEntries ( this . measurementFields . map ( field => [ field . key , Number ( pallet [ field . key ] ) ] ) )
if ( ! Object . values ( dimensions ) . every ( value => Number . isFinite ( value ) && value > 0 ) ) { this . error = '请填写有效的长、宽、高和毛重' ; return }
const payload = { relabelNo : this . task . relabelNo , palletCode : pallet . palletCode , stockVersion : pallet . stockVersion , ... dimensions }
const key = warehouseIdempotencyKey ( 'relabel' , 'measure-pallet' , payload )
await this . run ( async ( ) => {
this . measurements = await measureRelabelPallet ( { ... payload , idempotencyKey : key . value } )
releaseWarehouseKey ( 'relabel' , key . signature )
this . message = ` 托盘 ${ pallet . palletCode } 实测已保存 `
this . $emit ( 'saved' )
} )
} ,
savePalletTotals ( result ) { for ( const pallet of result ? . pallets || [ ] ) this . palletTotals = { ... this . palletTotals , [ pallet . palletCode ] : pallet } } ,
samePallet ( left , right ) {
if ( ! left || ! right ) return false
@@ -140,7 +163,7 @@ export default {
} ,
sourceItems ( ) { return ( this . task . items || [ ] ) . filter ( item => this . selectedSource ? . cartonMarks . includes ( item . cartonMark ) ) } ,
completedItems ( ) { const bound = new Set ( this . currentBindings . map ( binding => binding . mark ) ) ; return this . sourceItems ( ) . filter ( item => this . isReturnBulk ? Number ( item . expectedPackages ) > 0 && Number ( item . relabelledPackages ) >= Number ( item . expectedPackages ) : Number ( item . relabelledPackages ) >= 1 && ! bound . has ( item . cartonMark ) ) } ,
async loadSources ( ) { await this . run ( async ( ) => { try { this . sourceRows = await findRelabelSources ( this . task . relabelNo ) } catch ( error ) { if ( ! this . isReturnBulk && this . returnCartonItems . length ) this . sourceRows = [ ] ; else throw error } if ( ! this . isReturnBulk && this . returnCartonItems . length && ! this . sourceRows . some ( row => Number ( row . movablePackages ) > 0 ) ) this . sourceRows = [ { positionId : this . returnCartonItems [ 0 ] . returnCartonId , positionVersion : 0 , palletCode : 'UNBOUND' , stockPackages : this . returnCartonItems . length , movablePackages : this . returnCartonItems . length , cartonMarks : this . returnCartonItems . map ( item => item . cartonMark ) . filter ( Boolean ) } ] ; for ( const code of new Set ( this . currentBindings . map ( binding => binding . pallet ) ) ) { const pallet = await resolvePallet ( code ) ; this . savePalletTotals ( { pallets : [ pallet ] } ) } this . selectedSource = null ; this . source = null ; this . marks = [ ] ; this . quantity = '' ; if ( this . unboundSourceMode && this . operableSourceRows . length === 1 ) { this . selectedSource = this . operableSourceRows [ 0 ] ; this . selectedDestination = this . returnCartonItems [ 0 ] ? . destinationFbaWarehouseCode || '' ; this . marks = this . completedItems ( ) . map ( item => item . cartonMark ) ; this . quantity = String ( this . marks . length ) ; this . activate ( 'target' ) } } ) } ,
async loadSources ( ) { await this . run ( async ( ) => { try { this . sourceRows = await findRelabelSources ( this . task . relabelNo ) } catch ( error ) { if ( ! this . isReturnBulk && this . returnCartonItems . length ) this . sourceRows = [ ] ; else throw error } if ( ! this . isReturnBulk && this . returnCartonItems . length && ! this . sourceRows . some ( row => Number ( row . movablePackages ) > 0 ) ) this . sourceRows = [ { positionId : this . returnCartonItems [ 0 ] . returnCartonId , positionVersion : 0 , palletCode : 'UNBOUND' , stockPackages : this . returnCartonItems . length , movablePackages : this . returnCartonItems . length , cartonMarks : this . returnCartonItems . map ( item => item . cartonMark ) . filter ( Boolean ) } ] ; for ( const code of new Set ( this . currentBindings . map ( binding => binding . pallet ) ) ) { const pallet = await resolvePallet ( code ) ; this . savePalletTotals ( { pallets : [ pallet ] } ) } this . measurements = await listRelabelPalletMeasurements ( this . task . relabelNo ) ; this . selectedSource = null ; this . source = null ; this . marks = [ ] ; this . quantity = '' ; if ( this . unboundSourceMode && this . operableSourceRows . length === 1 ) { this . selectedSource = this . operableSourceRows [ 0 ] ; this . selectedDestination = this . returnCartonItems [ 0 ] ? . destinationFbaWarehouseCode || '' ; this . marks = this . completedItems ( ) . map ( item => item . cartonMark ) ; this . quantity = String ( this . marks . length ) ; this . activate ( 'target' ) } } ) } ,
async selectSource ( row ) {
if ( this . busy ) return
this . rebindSelection = null ; this . selectedSource = row ; this . source = null ; this . sourceCode = '' ; this . quantity = '' ; this . marks = [ ]
@@ -223,7 +246,7 @@ export default {
await this . resolve ( 'target' )
if ( this . error ) { this . showBindingMessage ( '绑定失败' , this . error ) ; return }
if ( ! this . readyToBind ) { this . error = '请核对来源箱、目标托盘和换标数量后重新确认。' ; this . showBindingMessage ( '尚未绑定' , this . error ) ; return }
if ( ! await this . confirmBindingChoice ( ` 是否绑定目标托盘 ${ this . target . palletCode } ( ${ this . quantity } ${ this . isReturnBulk ? '件' : '箱' } )? ` ) ) { this . target = null ; return }
if ( ! await this . confirmBindingChoice ( ` 是否绑定目标托盘 ${ this . target . palletCode } ( ${ this . quantity } ${ this . isReturnBulk ? '件' : '箱' } )? ${ this . samePallet ( this . source , this . target ) ? '整托保留原托盘且货物和包装未变可沿用原测量;包装变化请重新补测。' : '' } ` ) ) { this . target = null ; return }
await this . bind ( )
} ,
destination ( mark ) { return this . task . items . find ( item => item . cartonMark === mark ) ? . destinationFbaWarehouseCode || '目的仓待维护' } ,
@@ -278,6 +301,9 @@ export default {
< / script >
< style scoped >
. pallet - work { margin - top : 18 px ; padding : 14 px ; border : 1 px solid # d7e1ef ; border - radius : 10 px }
. measurement - card { margin : 12 px 0 ; padding : 12 px ; border : 1 px solid # d7e1ef ; border - radius : 8 px }
. measurement - inputs { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 10 px ; margin : 10 px 0 }
. measurement - inputs input { padding : 8 px ; border : 1 px solid # d7e1ef ; border - radius : 6 px }
. pallet - heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px }
. heading { display : block ; font - size : 18 px ; font - weight : 700 }
. refresh - button { flex : none ; margin : 0 ; padding : 6 px 14 px ; font - size : 13 px ; line - height : 1.5 }