@@ -5,20 +5,17 @@
< button class = "refresh-button" :disabled = "busy" @tap ="loadSources" > 刷新 < / button >
< / view >
< view v-if = "currentBindings.length" >
< text class = "hint" > 已绑定箱唛 : 选择后扫描新目标托盘可改绑 ( 已排车或出库不可直接改绑 ) 。 < / text >
< 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 . cartonAggregated ? '按当前托盘内实测箱自动汇总,移出扣减、移入增加。' : pallet . inherited ? '原托盘货物未变,沿用来源测量。' : '请核对托盘测量数据。' } } < / text >
< text v-if = "pallet.volumeCbm != null" class="hint" > {{ pallet.inherited ? ' 来源汇总 ' : ' 已确认 ' }} : {{ pallet.volumeCbm }} m ³ · {{ pallet.grossWeightKg }} kg < / text >
< view v-if = "pallet.inherited && pallet.sourceCartonSpecs?.length" class="source-carton-specs" >
< text class = "source-caption" > 来源箱规 ( 长 × 宽 × 高 ) < / text >
< text v-for = "(spec, index) in pallet.sourceCartonSpecs" :key="index" class="hint" > {{ spec.lengthCm }} × {{ spec.widthCm }} × {{ spec.heightCm }} cm · {{ spec.grossWeightKg }} kg / 箱 · {{ spec.cartons }} 箱 < / text >
< / view >
< text v-if = "pallet.inherited && pallet.lengthCm > 0 && pallet.widthCm > 0 && pallet.heightCm > 0" class="hint" > 原整托尺寸 : {{ pallet.lengthCm }} × {{ pallet.widthCm }} × {{ pallet.heightCm }} cm < / text >
< text v-else-if = "pallet.inherited && !pallet.cartonAggregated" class="hint" > 来源按箱测量 , 未测整托长宽高 。 < / text >
< button v-if = "!pallet.cartonAggregated && pallet.inherited && !pallet.remeasuring" :disabled="busy" @tap="pallet.remeasuring = true" > 包装变化 , 重新测量 < / button >
< view v-if = "!pallet.cartonAggregated && (!pallet.inherited || pallet.remeasuring)" 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 >
@@ -35,32 +32,20 @@
< text v-if = "!busy && !operableSourceRows.length" class="hint" > 当前没有可操作的来源托盘或来源箱 < / text >
< ScanTargetInput v-if = "!isReturnBulk && !unboundSourceMode" v-model="sourceCode" name="relabel-source" label="实际来源托盘" placeholder="扫描实际来源托盘" :active="active === 'source'" @activate="activate('source')" @submit="confirmSourceScan" / >
< ScanTargetInput v-if = "unboundSourceMode" v-model="sourceCode" name="relabel-source-carton" label="来源箱唛" placeholder="扫描来源箱唛确认" :active="active === 'source'" @activate="activate('source')" @submit="confirmSourceScan" / >
< text v-if = "source" class="hint" > 来源已确认 : {{ source.palletCode }} ; 尚未拆出 , 请完成贴标登记 、 核对拆出数量并扫描目标托盘 , 绑定成功后才保存拆托明细 。 < / text >
< text v-if = "source" class="hint" > 来源已确认 : {{ source.palletCode }} < / text >
< view v-if = "selectedSource" >
< text v-if = "isReturnBulk" class="hint" > 本次 {{ wholeSource ? ' 整托换标数量 ' : ' 拆出件数 ' }} ( 可分多个来源托盘 , 逐批提交 ) < / text >
< template v-if = "isReturnBulk" >
< text class = "return-pallet-summary" > { { selectedSource . palletCode } } : { { wholeSource ? '整托换标' : '部分拆箱' } } { { quantity || 0 } } 件 → { { selectedDestination || '目的仓待维护' } } < / text >
< text v-if = "wholeSource" class="hint" > 贴标数量等于本托盘全部 {{ selectedSource.stockPackages }} 件 , 无需填写拆箱数量 ; 直接新增带目的仓的托盘标签并绑定 。 < / text >
< template v-else-if = "allowDisassemble"><input v-model="quantity" type="number" :disabled="busy" :placeholder="`请输入拆箱数量(应为 ${plannedPackages} 件)`" /><text class="hint" > 贴标数量少于本托盘库存 , 请按现场实际选择来源托盘并填写拆箱数量 ; 本托盘应拆 {{ plannedPackages }} 件 。 < / text > < / template >
< text v-else class = "hint" > 退货换标按整托绑定 , 不支持拆箱 ; 托盘仍会展示用于核对 。 < / text >
< text class = "hint" > 退货换标无需扫描箱唛 。 < / text >
< input v-if = "! wholeSource && allowDisassemble" v-model="quantity" type="number" :disabled="busy" :placeholder="`拆出件数(${plannedPackages} 件)`" / >
< / template >
< template v-else > < view class = "split-quantity-row" > < text > 本次拆出箱数 < / text > < input v-model = "quantity" type="number" :disabled="busy" placeholder="箱数" @input="prepareMeasuredBatch" / > < text > 箱 < / text > < / view > < / template >
< / view >
< view v-if = "requiresMeasuredBoxChoice && marks.length" class="source-carton-specs" >
< text class = "hint" > 来源有多种箱规 , 请选择本次移出的实测箱 , 系统自动扣减体积和重量 。 < / text >
< picker v-for = "mark in marks" :key="mark" :range="measuredBoxOptions(mark)" range-key="label" :disabled="busy" @change="chooseMeasuredBox(mark, $event)" >
< view class = "hint" > { { mark } } : { { measuredBoxLabel ( mark ) } } < / view >
< / picker >
< / view >
< view class = "split-progress" > < template v-if = "!isReturnBulk"><text>本次需换标拆 <strong>{{ requiredSplitPackages }}</strong> 箱</text><text>已拆 <strong>{{ splitPackages }}</strong> 箱</text></template><button class="create-pallet-button" :disabled="busy || !!newPallet" @tap="createPallet" > 新增托盘号 < / button > < / view >
< / 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 >
< view v-if = "newPallet" class="hint"><text>托盘 {{ newPallet.palletCode }} 待绑定</text><button @tap="printPallet" > 查看 / 打印托盘标签 < / button > < / view >
< / template >
< text v-if = "message" class="hint">{{ message }}</text><text v-if="error" class="error" > {{ error }} < / text >
@@ -74,11 +59,11 @@ export default {
components : { ScanTargetInput } ,
props : { task : { type : Object , required : true } , allowDisassemble : { type : Boolean , default : true } } ,
emits : [ 'activate' , 'busy' , 'saved' ] ,
data ( ) { return { measurements : [ ] , measuredSelections : { } , 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 : '' } } ,
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 : {
measuredBoxChoices ( ) { return this . measurements . find ( p => p . palletCode === this . selectedSource ? . palletCode ) ? . cartons || [ ] } ,
requiresMeasuredBoxChoice ( ) { return new Set ( this . measuredBoxChoices . map ( box => [ box . lengthCm , box . widthCm , box . heightCm , box . grossWeightKg ] . join ( ':' ) ) ) . size > 1 } ,
requiresMeasuredBoxMapping ( ) { return new Set ( this . measuredBoxChoices . map ( box => [ box . lengthCm , box . widthCm , box . heightCm , box . grossWeightKg ] . join ( ':' ) ) ) . size > 1 } ,
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 ( ) {
@@ -111,8 +96,8 @@ export default {
const pending = this . sourceItems ( ) . filter ( item => Number ( item . relabelledPackages || 0 ) < Number ( item . expectedPackages || 1 ) )
if ( ! pending . length ) return ''
const missingSku = pending . some ( item => item . operationType === 'CARTON_AND_SKU' && Number ( item . skuProcessedPackages || 0 ) < Number ( item . expectedPackages || 1 ) )
if ( missingSku ) return '本托盘还有未 完成的 SKU 贴标登记。混合换标须完成箱唛和 SKU 两项,请返回 SKU 页签处理后再确认绑定。 '
return '本托盘还有未完成的换标明细,请先完成贴标登记,再确认目标托盘绑定。 '
if ( missingSku ) return '请先 完成 SKU 贴标登记'
return '请先完成贴标登记 '
} ,
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' ) ) } ,
@@ -121,16 +106,21 @@ export default {
methods : {
prepareMeasuredBatch ( event ) {
this . quantity = String ( event . detail . value )
this . marks = [ ] ; this . measuredSelections = { }
this . marks = [ ]
const count = Number ( this . quantity )
if ( this . measuredBoxChoices . length && Number . isSafeInteger ( count ) && count > 0 && count <= this . selectedSource . movablePackages && count <= this . eligibleItems . length ) this . fillBatch ( )
} ,
measuredBoxOptions ( mark ) {
const used = Object . entries ( this . measuredSelections || { } ) . filter ( ( [ key ] ) => key !== mark && this . marks . includes ( key ) ) . map ( ( [ , value ] ) => value )
return this . measuredBoxChoices . filter ( box => ! used . includes ( box . cartonMark ) ) . map ( box => ( { ... box , label : ` ${ box . cartonMark } · ${ box . lengthCm } × ${ box . widthCm } × ${ box . heightCm } cm · ${ box . grossWeightKg } kg ` } ) )
// 按申请时的仓库箱唛或客户箱唛对应原实测箱,禁止重新选择其他箱。
measuredCartonBindings ( ) {
if ( ! this . requiresMeasuredBoxMapping ) return [ ]
const used = new Set ( )
return this . marks . map ( mark => {
const matches = this . measuredBoxChoices . filter ( box => box . cartonMark === mark || box . customerCartonMark === mark )
const box = matches . length === 1 && ! used . has ( matches [ 0 ] . cartonMark ) ? matches [ 0 ] : null
if ( box ) used . add ( box . cartonMark )
return { cartonMark : mark , sourceCartonMark : box ? . cartonMark }
} )
} ,
chooseMeasuredBox ( mark , event ) { const box = this . measuredBoxOptions ( mark ) [ Number ( event . detail . value ) ] ; if ( box ) this . measuredSelections = { ... this . measuredSelections , [ mark ] : box . cartonMark } } ,
measuredBoxLabel ( mark ) { return this . measuredBoxOptions ( mark ) . find ( box => box . cartonMark === this . measuredSelections ? . [ mark ] ) ? . label || '点击选择实测箱' } ,
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 }
@@ -171,9 +161,9 @@ export default {
const binding = this . rebindSelection
const targetCode = this . targetCode . trim ( ) . toUpperCase ( )
if ( ! binding || ! targetCode || this . busy ) return
if ( targetCode === binding . pallet . toUpperCase ( ) ) { this . showBindingMessage ( '无需改绑' , '该箱唛已经 绑定此托盘,不会重复扣库存。 ' ) ; return }
if ( targetCode === binding . pallet . toUpperCase ( ) ) { this . showBindingMessage ( '无需改绑' , '该箱唛已绑定此托盘' ) ; return }
this . busy = true ; this . $emit ( 'busy' , true )
const confirmed = await new Promise ( resolve => uni . showModal ( { title : '确认改绑' , content : ` 将 ${ binding . mark } 从 ${ binding . pallet } 移至 ${ targetCode } ? 旧托盘扣箱,新托盘加箱。 ` , confirmText : '是' , cancelText : '否' , success : result => resolve ( result . confirm ) , fail : ( ) => resolve ( false ) } ) )
const confirmed = await new Promise ( resolve => uni . showModal ( { title : '确认改绑' , content : ` 将 ${ binding . mark } 从 ${ binding . pallet } 移至 ${ targetCode } ? ` , confirmText : '是' , cancelText : '否' , success : result => resolve ( result . confirm ) , fail : ( ) => resolve ( false ) } ) )
this . busy = false ; this . $emit ( 'busy' , false )
if ( ! confirmed ) return
await this . run ( async ( ) => {
@@ -194,7 +184,7 @@ export default {
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 . measuredSelections = { } ; this . rebindSelection = null ; this . selectedSource = row ; this . source = null ; this . sourceCode = '' ; this . quantity = '' ; this . marks = [ ]
this . rebindSelection = null ; this . selectedSource = row ; this . source = null ; this . sourceCode = '' ; this . quantity = '' ; this . marks = [ ]
this . selectedDestination = ( this . isReturnBulk ? this . sourceItems ( ) : this . completedItems ( ) ) [ 0 ] ? . destinationFbaWarehouseCode || ''
if ( this . unboundSourceMode ) {
this . marks = this . completedItems ( ) . map ( item => item . cartonMark )
@@ -265,17 +255,17 @@ export default {
let reason = ''
if ( ! this . selectedSource ) reason = '请先选择实际拆出货物的来源托盘或来源箱。'
else if ( this . unboundSourceMode && ! this . bindingSelectionValid ( ) ) reason = '请先完成来源箱换标,再绑定目标托盘。'
else if ( ! this . unboundSourceMode && ! this . samePallet ( this . source , this . selectedSource ) ) reason = this . isReturnBulk ? '来源托盘识别失败,请重新选择。' : '请先 扫描并 确认实际 来源托盘;选择来源列表不等于扫描确认。 '
else if ( this . isReturnBulk && this . completedItems ( ) . length !== this . eligibleItems . length ) reason = '请先完成本托盘的 SKU 贴标登记,再绑定新托盘;整托无需填写拆箱数量。 '
else if ( ! this . unboundSourceMode && ! this . samePallet ( this . source , this . selectedSource ) ) reason = this . isReturnBulk ? '来源托盘识别失败,请重新选择。' : '请扫描确认来源托盘'
else if ( this . isReturnBulk && this . completedItems ( ) . length !== this . eligibleItems . length ) reason = '请先完成 SKU 贴标登记'
else if ( ! Number . isSafeInteger ( Number ( this . quantity ) ) || Number ( this . quantity ) <= 0 || Number ( this . quantity ) > this . selectedSource . movablePackages ) reason = this . isReturnBulk ? '本托盘可操作件数不足,请刷新来源托盘。' : '请填写有效的拆出箱数,不能超过来源托盘可拆数量。'
else if ( ! this . selectedDestination || ! this . bindingSelectionValid ( ) ) reason = this . isReturnBulk ? ` 本托盘拆箱数量必须等于贴标预报分配数量 ${ this . plannedPackages } 件。 ` : '本次拆出箱数必须对应已完成换标且尚未拆出的同目的仓箱唛。'
else if ( this . requiresMeasuredBoxChoice && this . marks . some ( mark => ! this . measuredSelections ? . [ mark ] ) ) reason = '请选择本次移出的实测箱,系统将按所选箱扣减体积和重量。 '
else if ( this . measuredCartonBindings ( ) . some ( row => ! row . sourceCartonMark ) ) reason = '换标箱与来源实测箱不匹配,请核对来源托盘 '
else if ( ! this . targetCode . trim ( ) ) reason = '请扫描或输入目标托盘号。'
if ( reason ) { this . error = reason ; this . showBindingMessage ( '尚未绑定' , reason ) ; return }
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 . samePallet ( this . source , this . target ) ? '整托保留原托盘且货物和包装未变可沿用原测量;包装变化请重新补测。' : '' } ` ) ) { this . target = null ; return }
if ( ! await this . confirmBindingChoice ( ` 是否绑定目标托盘 ${ this . target . palletCode } ( ${ this . quantity } ${ this . isReturnBulk ? '件' : '箱' } )? ` ) ) { this . target = null ; return }
await this . bind ( )
} ,
destination ( mark ) { return this . task . items . find ( item => item . cartonMark === mark ) ? . destinationFbaWarehouseCode || '目的仓待维护' } ,
@@ -309,8 +299,8 @@ export default {
async createPallet ( ) { const payload = { relabelNo : this . task . relabelNo } ; const key = warehouseIdempotencyKey ( 'relabel' , 'new-pallet' , payload ) ; await this . run ( async ( ) => { this . newPallet = await createRelabelPallet ( { ... payload , idempotencyKey : key . value } ) ; this . target = null ; this . targetCode = this . newPallet . palletCode || '' ; this . activate ( 'target' ) } ) } ,
async bind ( ) {
if ( ! this . readyToBind ) { this . error = '请核对来源托盘、拆出箱数、本批箱唛和目标托盘' ; return }
const measuredCartons = this . requiresMeasuredBoxChoice ? this . marks . map ( mark => ( { cartonMark : mark , sourceCartonMark : this . measuredSelections ? . [ mark ] } ) ) : [ ]
if ( measuredCartons . some ( row => ! row . sourceCartonMark ) ) { this . error = '请选择本次移出的实测箱 ' ; return }
const measuredCartons = this . measuredCartonBindings ( )
if ( measuredCartons . some ( row => ! row . sourceCartonMark ) ) { this . error = '换标箱与来源实测箱不匹配,请核对来源托盘 ' ; return }
const expectation = value => ( { palletCode : value . palletCode , expectedVersion : value . palletVersion } )
const payload = { relabelNo : this . task . relabelNo , ... ( this . unboundSourceMode ? { } : { source : expectation ( this . source ) , actualSource : { positionId : this . selectedSource . positionId , expectedPositionVersion : this . selectedSource . positionVersion , packages : Number ( this . quantity ) } } ) , target : expectation ( this . target ) , cartonMarks : [ ... this . marks ] , reason : this . unboundSourceMode ? 'PDA 扫描无来源托盘退货箱并绑定目的托盘' : 'PDA 按原入仓单箱数拆托并按新目的仓合托' }
if ( measuredCartons . length ) payload . measuredCartons = measuredCartons