@@ -21,8 +21,10 @@ import {
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail'
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
interface CargoRow { id : number ; sku : string ; name : string ; quantity : number ; availableQuantity ? : number ; sourceOrderId ? : string ; sourceInventoryBalanceId ? : string ; sourceInventoryPositionId ? : string ; sourceInboundOrderId ? : string ; sourceInboundOrderNo ? : string ; sourceInboundCode ? : string ; sourceWarehouseName ? : string ; palletNo ? : string ; warehouseLocationCode ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
interface CargoRow { id : number ; included ? : boolean ; sku : string ; name : string ; quantity : number ; availableQuantity ? : number ; sourceOrderId ? : string ; sourceInventoryBalanceId ? : string ; sourceInventoryPositionId ? : string ; sourceInboundOrderId ? : string ; sourceInboundOrderNo ? : string ; sourceInboundCode ? : string ; sourceWarehouseName ? : string ; palletNo ? : string ; warehouseLocationCode ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface SkuRelabelRow { id : number ; sourceRowId : number | null ; cartonMark : string ; originalSku : string ; newSku : string ; quantity : number ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface ImportedRelabelBinding extends SkuRelabelRow { templateType : RelabelTemplateType ; selected : boolean ; shipmentNo : string }
interface ServiceConfig { title : string ; createTitle : string ; numberLabel : string ; emptyText : string ; columns : string [ ] }
const props = defineProps < { service : ServiceType } > ( )
@@ -54,6 +56,8 @@ const cargoRows = ref<CargoRow[]>([])
const labelingMode = ref < LabelingMode > ( 'carton' )
const shippingLabelFile = ref < File | null > ( null )
const skuRelabelRows = ref < SkuRelabelRow [ ] > ( [ ] )
const importedRelabelBindings = ref < ImportedRelabelBinding [ ] > ( [ ] )
const cartonDetailRows = computed ( ( ) => cargoRows . value . filter ( ( row ) => row . included ) )
const labelingModeHint = computed ( ( ) => labelingMode . value === 'carton'
? '只替换外箱箱唛,不修改箱内商品 SKU 标签;按箱填写新的 FBA 箱唛和目的仓。'
: labelingMode . value === 'sku'
@@ -89,7 +93,9 @@ const relabelInboundOptions = computed(() => {
} )
const loading = ref ( false )
const saving = ref ( false )
const templateInput = ref < HTMLInputElement | null > ( null )
const cartonTemplateInput = ref < HTMLInputElement | null > ( null )
const skuTemplateInput = ref < HTMLInputElement | null > ( null )
const mixedTemplateInput = ref < HTMLInputElement | null > ( null )
let cargoSequence = 0
let skuRelabelSequence = 0
@@ -102,6 +108,7 @@ function resetForm() {
cargoRows . value = [ ]
labelingMode . value = 'carton'
skuRelabelRows . value = [ ]
importedRelabelBindings . value = [ ]
discardCandidates . value = [ ]
selectedDiscardOrderId . value = ''
relabelCandidates . value = [ ]
@@ -127,6 +134,22 @@ function addSkuRelabelRow() {
fbaCartonMark : '' , destinationWarehouseId : source ? . destinationWarehouseId || '' , remark : '' } )
}
function removeSkuRelabelRow ( id : number ) { skuRelabelRows . value = skuRelabelRows . value . filter ( ( row ) => row . id !== id ) }
function applyExistingSkuBinding ( row : SkuRelabelRow ) {
if ( ! row . originalSku . trim ( ) || row . newSku . trim ( ) ) return
const mapping = skuRelabelRows . value . find ( ( item ) => item . id !== row . id
&& item . originalSku . trim ( ) . toUpperCase ( ) === row . originalSku . trim ( ) . toUpperCase ( ) && item . newSku . trim ( ) )
if ( mapping ) row . newSku = mapping . newSku
}
function applyExistingCartonBinding ( row : SkuRelabelRow ) {
if ( ! row . cartonMark . trim ( ) ) return
const cartonMark = row . cartonMark . trim ( ) . toUpperCase ( )
const mapping = cargoRows . value . find ( ( item ) => item . cartonMark . trim ( ) . toUpperCase ( ) === cartonMark && item . fbaCartonMark . trim ( ) )
|| skuRelabelRows . value . find ( ( item ) => item . id !== row . id
&& item . cartonMark . trim ( ) . toUpperCase ( ) === cartonMark && item . fbaCartonMark . trim ( ) )
if ( ! mapping ) return
if ( ! row . fbaCartonMark . trim ( ) ) row . fbaCartonMark = mapping . fbaCartonMark
if ( ! row . destinationWarehouseId ) row . destinationWarehouseId = mapping . destinationWarehouseId
}
function selectShippingLabel ( event : Event ) { shippingLabelFile . value = ( event . target as HTMLInputElement ) . files ? . [ 0 ] || null }
function buildSkuRelabelItems ( ) : RelabelCreate [ 'items' ] {
const groups = new Map < string , SkuRelabelRow [ ] > ( )
@@ -145,7 +168,7 @@ function buildSkuRelabelItems(): RelabelCreate['items'] {
throw new Error ( '填写的箱唛数量超过当前入仓单可用库存箱数' )
}
return [ ... groups . entries ( ) ] . map ( ( [ _ , rows ] , groupIndex ) => {
const source = anchors [ groupIndex ]
const source = cargoRows . value . find ( ( item ) => item . id === rows [ 0 ] . sourceRowId ) || anchors [ groupIndex ]
if ( ! source ? . sourceInventoryBalanceId || ! source . sourceInventoryPositionId || ! source . sourceInboundOrderId ) {
throw new Error ( '来源库存已变化,请重新选择有库存的历史入仓单' )
}
@@ -180,32 +203,40 @@ function buildSkuRelabelItems(): RelabelCreate['items'] {
}
function addRelabelCargoRow ( ) {
if ( ! selectedRelabelInboundOrderId . value ) { ElMessage . warning ( '请先选择有库存的历史入仓单' ) ; return }
ElMessage . info ( '货物明细已按所选入仓单的可用库存带出,请直接编辑现有明细' )
const source = cargoRows . value . find ( ( row ) => ! row . included )
if ( ! source ) { ElMessage . warning ( '所选入仓单没有更多可添加的库存箱' ) ; return }
source . included = true
}
function removeCargoRow ( id : number ) {
const item = cargoRows . value . find ( ( row ) => row . id === id )
if ( props . service === 'labeling' && item ? . sourceInventoryBalanceId ) {
Object . assign ( item , { included : false , shipmentNo : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } )
return
}
cargoRows . value = cargoRows . value . filter ( ( row ) => row . id !== id )
}
function clearRelabelRows ( ) { cargoRows . value = [ ] ; selectedRelabelInboundOrderId . value = '' }
function clearRelabelRows ( ) {
for ( const row of cargoRows . value ) Object . assign ( row , { included : false , shipmentNo : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } )
}
function csvCell ( value : string | number | undefined ) { return ` " ${ String ( value ? ? '' ) . replaceAll ( '"' , '""' ) } " ` }
function downloadRelabelTemplate ( ) {
function downloadRelabelTemplate ( templateType : RelabelTemplateType ) {
if ( ! cargoRows . value . length ) { ElMessage . warning ( '请先选择有库存的历史入仓单' ) ; return }
const cartonTemplate = labelingMode . value === 'carton'
const combinedTemplate = labelingMode . value === 'carton_and_sku '
const cartonTemplate = templateType === 'carton'
const mixedTemplate = templateType === 'mixed '
const header = cartonTemplate
? [ '明细行标识' , '本次箱数 ' , '商品SKU' , '中文品名' , 'FBA货件编号' , '我方箱唛' , ' FBA箱唛', 'FBA目的仓' , '备注' ]
: [ '我方 箱唛' , '旧SKU标签' , '新SKU标签' , '换标数量(件)' ,
... ( combinedTemplate ? [ '新FBA箱唛' , 'FBA目的仓' ] : [ ] ) , '备注' ]
const rows = cartonTemplate
? cargoRows . value . map ( ( item ) => [ item . id , item . quantity , item . sku , item . name , item . shipmentNo ,
item . cartonMark , item . fbaCartonMark , item . destinationWarehouseId , item . remark ] )
: skuRelabelRows . value . map ( ( item ) => [ item . cartonMark , item . originalSku , item . newSku , item . quantity ,
... ( combinedTemplate ? [ item . fbaCartonMark , item . destinationWarehouseId ] : [ ] ) , item . remark ] )
? [ '明细行标识' , '旧箱唛 ' , '新 FBA箱唛' , 'FBA目的仓' , 'FBA货件编号' , '备注' ]
: [ '明细行标识' , '旧 箱唛', '旧SKU标签' , '新SKU标签' , '换标数量(件)' ,
... ( mixedTemplate ? [ '新FBA箱唛' , 'FBA目的仓' , 'FBA货件编号' ] : [ ] ) , '备注' ]
const rows = cargoRows . value . map ( ( item ) => cartonTemplate
? [ item . id , item . cartonMark , item . fbaCartonMark , item . destinationWarehouseId , item . shipmentNo , item . remark ]
: [ item . id , item . cartonMark , item . sku , '' , 1 ,
... ( mixedTemplate ? [ item . fbaCartonMark , item . destinationWarehouseId , item . shipmentNo ] : [ ] ) , item . remark ] )
const blob = new Blob ( [ ` \ uFEFF ${ [ header , ... rows ] . map ( ( row ) => row . map ( csvCell ) . join ( ',' ) ) . join ( '\r\n' ) } \ r \ n ` ] , { type : 'text/csv;charset=utf-8' } )
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = cartonTemplate ? '贴标申请-箱唛换标 模板.csv'
: combinedTemplate ? '贴标申请-SKU和箱唛 换标模板.csv' : '贴标申请-SKU换标 模板.csv'
anchor . download = cartonTemplate ? '贴标申请-箱号关联 模板.csv'
: mixedTemplate ? '贴标申请-SKU混箱 换标模板.csv' : '贴标申请-SKU关联 模板.csv'
anchor . click ( ) ; URL . revokeObjectURL ( url )
}
function parseCsvLine ( line : string ) {
@@ -218,76 +249,112 @@ function parseCsvLine(line: string) {
}
values . push ( value . trim ( ) ) ; return values
}
async function importRelabelTemplate ( event : Event ) {
async function importRelabelTemplate ( event : Event , templateType : RelabelTemplateType ) {
const input = event . target as HTMLInputElement ; const file = input . files ? . [ 0 ] ; input . value = ''
if ( ! file ) return
const lines = ( await file . text ( ) ) . replace ( /^\uFEFF/ , '' ) . split ( /\r?\n/ ) . filter ( ( line ) => line . trim ( ) )
if ( lines . length < 2 ) { ElMessage . error ( '模板中没有可导入的明细' ) ; return }
const headers = parseCsvLine ( lines [ 0 ] )
if ( labelingMode . value !== 'carton' ) {
const combinedTemplate = labelingMode . value === 'carton_and_sku '
const required = [ '我方 箱唛' , '旧SKU标签' , '新SKU标签' , '换标数量(件)' ,
... ( combinedTemplate ? [ '新FBA箱唛' , 'FBA目的仓' ] : [ ] ) ]
if ( templateType !== 'carton' ) {
const mixedTemplate = templateType === 'mixed '
const required = [ '明细行标识' , '旧 箱唛', '旧SKU标签' , '新SKU标签' , '换标数量(件)' ,
... ( mixedTemplate ? [ '新FBA箱唛' , 'FBA目的仓' , 'FBA货件编号' ] : [ ] ) ]
if ( required . some ( ( name ) => ! headers . includes ( name ) ) ) {
ElMessage . error ( 'SKU 换标 模板格式不正确,请重新下载当前换标类型的 模板' ) ; return
ElMessage . error ( 'SKU 关联 模板格式不正确,请重新下载 SKU 关联 模板' ) ; return
}
const imported = lines . slice ( 1 ) . map ( parseCsvLine )
. map ( ( values ) => Object . fromEntries ( headers . map ( ( key , index ) => [ key , values [ index ] || '' ] ) ) )
. filter ( ( item ) => Object . values ( item ) . some ( Boolean ) )
. filter ( ( item ) => item [ '旧箱唛' ] || item [ '新SKU标签' ] || item [ '新FBA箱唛' ] )
if ( ! imported . length || imported . some ( ( item ) => required . some ( ( key ) => ! item [ key ] ) ) ) {
ElMessage . error ( '我方 箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整' ) ; return
ElMessage . error ( '旧 箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整' ) ; return
}
const rows : SkuRelabelRow [ ] = [ ]
const rows : ImportedRelabelBinding [ ] = [ ]
for ( const item of imported ) {
const quantity = Number ( item [ '换标数量(件)' ] )
if ( ! Number . isInteger ( quantity ) || quantity < 1 ) {
ElMessage . error ( ` 箱唛 ${ item [ '我方 箱唛' ] } 的换标数量必须是大于 0 的整数 ` ) ; return
ElMessage . error ( ` 箱唛 ${ item [ '旧 箱唛' ] } 的换标数量必须是大于 0 的整数 ` ) ; return
}
const source = cargoRows . value . find ( ( row ) => String ( row . id ) === item [ '明细行标识' ] )
if ( ! source ) { ElMessage . error ( ` 明细行 ${ item [ '明细行标识' ] } 不属于当前库存,请重新下载模板 ` ) ; return }
rows . push ( {
id : ++ skuRelabelSequence ,
sourceRowId : null ,
cartonMark : item [ '我方 箱唛' ] . trim ( ) ,
sourceRowId : source . id ,
cartonMark : item [ '旧 箱唛' ] . trim ( ) ,
originalSku : item [ '旧SKU标签' ] . trim ( ) ,
newSku : item [ '新SKU标签' ] . trim ( ) ,
quantity ,
fbaCartonMark : combinedTemplate ? item [ '新FBA箱唛' ] . trim ( ) : '' ,
destinationWarehouseId : combinedTemplate ? item [ 'FBA目的仓' ] . trim ( ) : '' ,
fbaCartonMark : mixedTemplate ? item [ '新FBA箱唛' ] . trim ( ) : '' ,
destinationWarehouseId : mixedTemplate ? item [ 'FBA目的仓' ] . trim ( ) : '' ,
remark : item [ '备注' ] || '' ,
templateType ,
selected : true ,
shipmentNo : mixedTemplate ? item [ 'FBA货件编号' ] . trim ( ) : '' ,
} )
}
skuRelabelRows . value = rows
ElMessage . success ( ` 已导入 ${ rows . length } 条 SKU 换标 明细 ` )
importedRelabelBindings . value = rows
ElMessage . success ( ` 已读取 ${ rows . length } 条绑定关系,请选择后导入到 明细 ` )
return
}
const required = [ '明细行标识' , '本次箱数 ' , 'FBA货件编号' , '我方 箱唛', 'FBA箱唛 ' , 'FBA目的仓 ' ]
if ( required . some ( ( name ) => ! headers . includes ( name ) ) ) { ElMessage . error ( '模板格式不正确,请重新下载模板填写 ' ) ; return }
const required = [ '明细行标识' , '旧箱唛 ' , '新 FBA箱唛' , 'FBA目的仓 ' , 'FBA货件编号 ' ]
if ( required . some ( ( name ) => ! headers . includes ( name ) ) ) { ElMessage . error ( '箱号关联 模板格式不正确,请重新下载箱号关联 模板' ) ; return }
const imported = lines . slice ( 1 ) . map ( parseCsvLine )
. map ( ( values ) => Object . fromEntries ( headers . map ( ( key , index ) => [ key , values [ index ] || '' ] ) ) )
. filter ( ( item ) => item [ '明细行标识 ' ] )
. filter ( ( item ) => item [ '旧箱唛' ] || item [ '新FBA箱唛 '] )
if ( ! imported . length || imported . some ( ( item ) => required . some ( ( key ) => ! item [ key ] ) ) ) {
ElMessage . error ( '模板中的明细行、箱数、货件、箱唛和 FBA 目的仓必须填写完整' ) ; return
ElMessage . error ( '模板中的明细行、旧箱唛、新 FBA 箱唛、 FBA 目的仓和货件编号 必须填写完整' ) ; return
}
if ( new Set ( imported . map ( ( item ) => item [ '明细行标识' ] ) ) . size !== imported . length ) {
ElMessage . error ( '模板中的明细行标识不能重复' ) ; return
}
const importedByRow = new Map ( imported . map ( ( item ) => [ item [ '明细行标识' ] , item ] ) )
if ( importedByRow . size !== cargoRows . value . length || cargoRows . value . some ( ( row ) => ! importedByRow . has ( String ( row . id ) ) ) ) {
ElMessage . error ( '模板明细与当前已选入仓单库存不一致 ,请重新下载模板' ) ; return
if ( importedByRow . size !== imported . length || [ ... importedByRow . keys ( ) ] . some ( ( id ) => ! cargoRows . value . some ( ( row ) => String ( row . id ) === id ) ) ) {
ElMessage . error ( '模板存在重复或不属于当前库存的明细行 ,请重新下载模板' ) ; return
}
for ( const row of cargoRows . value ) {
const item = importedByRow . get ( String ( row . id ) ) !
const quantity = Number ( item [ '本次箱数' ] )
if ( ! Number . isInteger ( quantity ) || quantity < 1 || quantity > Number ( row . availableQuantity || 0 ) ) {
ElMessage . error ( ` 明细行 ${ row . id } 的本次箱数不正确或超过可用库存 ` ) ; return
const bindings : ImportedRelabelBinding [ ] = [ ]
for ( const [ rowId , item ] of importedByRow ) {
const row = cargoRows . value . find ( ( candidate ) => String ( candidate . id ) === rowId ) !
bindings . push ( { id : ++ skuRelabelSequence , sourceRowId : row . id , cartonMark : item [ '旧箱唛' ] ,
originalSku : row . sku , newSku : '' , quantity : 1 , fbaCartonMark : item [ '新FBA箱唛' ] ,
destinationWarehouseId : item [ 'FBA目的仓' ] , remark : item [ '备注' ] || '' , templateType ,
selected : true , shipmentNo : item [ 'FBA货件编号' ] } )
}
importedRelabelBindings . value = bindings
ElMessage . success ( ` 已读取 ${ bindings . length } 条箱号关联,请选择后导入到明细 ` )
}
function selectAllImportedBindings ( value : boolean ) {
for ( const item of importedRelabelBindings . value ) item . selected = value
}
function importSelectedBindingsToDetails ( ) {
const selected = importedRelabelBindings . value . filter ( ( item ) => item . selected )
if ( ! selected . length ) { ElMessage . warning ( '请至少选择一条绑定关系' ) ; return }
const templateType = selected [ 0 ] . templateType
if ( selected . some ( ( item ) => item . templateType !== templateType ) ) { ElMessage . warning ( '请重新上传同一种绑定模板' ) ; return }
if ( templateType === 'carton' ) {
for ( const binding of selected ) {
const source = cargoRows . value . find ( ( row ) => row . id === binding . sourceRowId )
if ( ! source ) continue
Object . assign ( source , { included : true , quantity : 1 , shipmentNo : binding . shipmentNo ,
cartonMark : binding . cartonMark , fbaCartonMark : binding . fbaCartonMark ,
destinationWarehouseId : binding . destinationWarehouseId , remark : binding . remark } )
}
row . quantity = quantity
row . shipmentNo = item [ 'FBA货件编号' ]
row . cartonMark = item [ '我方箱唛' ]
row . fbaCartonMark = item [ 'FBA箱唛' ]
row . destinationWarehouseId = item [ 'FBA目的仓' ]
row . remark = item [ '备注' ] || ''
labelingMode . value = 'carton'
} else {
const importedRows = selected . map ( ( { templateType : _ , selected : _ _ , shipmentNo , ... item } ) => item )
for ( const item of importedRows ) {
skuRelabelRows . value = skuRelabelRows . value . filter ( ( row ) => ! ( row . cartonMark . trim ( ) . toUpperCase ( ) === item . cartonMark . trim ( ) . toUpperCase ( )
&& row . originalSku . trim ( ) . toUpperCase ( ) === item . originalSku . trim ( ) . toUpperCase ( ) ) )
skuRelabelRows . value . push ( item )
if ( templateType === 'mixed' ) {
const source = cargoRows . value . find ( ( row ) => row . id === item . sourceRowId )
const shipmentNo = selected . find ( ( binding ) => binding . id === item . id ) ? . shipmentNo || ''
if ( source ) Object . assign ( source , { included : true , cartonMark : item . cartonMark ,
fbaCartonMark : item . fbaCartonMark , destinationWarehouseId : item . destinationWarehouseId , shipmentNo } )
}
}
labelingMode . value = templateType === 'mixed' ? 'carton_and_sku' : 'sku'
}
ElMessage . success ( ` 已导入 ${ imported . length } 条关联面单明细 ` )
importedRelabelBindings . value = importedRelabelBindings . value . filter ( ( item ) => ! item . selected )
ElMessage . success ( ` 已将 ${ selected . length } 条绑定关系导入本次换标明细 ` )
}
function selectDiscardInbound ( orderId : string ) {
const candidate = discardCandidates . value . find ( ( item ) => item . orderId === orderId )
@@ -356,14 +423,15 @@ async function load() {
function query ( ) { void load ( ) }
async function submit ( ) {
const primaryWarehouse = props . service === 'relabel' ? form . sourceWarehouse : form . warehouse
const relabelRows = labelingMode . value === 'carton' ? cargoRows . value : skuRelabelRows . value
const relabelRows = labelingMode . value === 'carton' ? cartonDetailRows . value : skuRelabelRows . value
const invalidCargo = props . service === 'disposal'
? ! selectedDiscardOrderId . value || cargoRows . value . every ( ( item ) => item . quantity < 1 )
|| cargoRows . value . some ( ( item ) => item . quantity < 0 || item . quantity > Number ( item . availableQuantity || 0 ) )
: relabelRows . length === 0 || ( props . service === 'labeling' && labelingMode . value !== 'carton'
? skuRelabelRows . value . some ( ( item ) => ! item . cartonMark || ! item . originalSku || ! item . newSku || item . quantity < 1
|| ( labelingMode . value === 'carton_and_sku' && ( ! item . fbaCartonMark || ! item . destinationWarehouseId ) ) )
: cargoRows . value . some ( ( item ) => ( props . service !== 'labeling' && ! item . sku ) || ! item . name || item . quantity < 1 ) )
: cargoRows . value . some ( ( item ) => ( props . service !== 'labeling' && ! item . sku ) || ! item . name
|| ( props . service !== 'labeling' && item . quantity < 1 ) ) )
if ( ( props . service === 'relabel' && ! form . inboundNo ) || ( props . service !== 'disposal' && ! primaryWarehouse ) || invalidCargo ) {
ElMessage . warning ( '请填写必填信息和完整的货物明细' ) ; return
}
@@ -377,10 +445,10 @@ async function submit() {
&& form . relabel && ! shippingLabelFile . value ) {
ElMessage . warning ( '选择打印面单时必须上传面单文件' ) ; return
}
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cargoRows . value . some ( ( item ) => ! item . shipmentNo || ! item . cartonMark || ! item . fbaCartonMark ) ) {
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cartonDetailRows . value . some ( ( item ) => ! item . shipmentNo || ! item . cartonMark || ! item . fbaCartonMark ) ) {
ElMessage . warning ( '请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整' ) ; return
}
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cargoRows . value . some ( ( item ) => ! item . destinationWarehouseId || ! item . sourceInventoryPositionId ) ) {
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cartonDetailRows . value . some ( ( item ) => ! item . destinationWarehouseId || ! item . sourceInventoryPositionId ) ) {
ElMessage . warning ( '请选择有库存的历史入仓单,并为每项填写 FBA 目的仓' ) ; return
}
if ( props . service === 'disposal' && ! form . reason ) {
@@ -402,7 +470,7 @@ async function submit() {
if ( props . service === 'labeling' ) {
const sourceInboundOrders = [ ... new Set ( cargoRows . value . map ( ( item ) => item . sourceInboundOrderNo || item . sourceInboundCode ) . filter ( ( value ) : value is string => Boolean ( value ) ) ) ] . join ( '、' ) . slice ( 0 , 128 )
const items = labelingMode . value === 'carton'
? cargoRows . value . map ( ( item ) => ( { sourceInventoryBalanceId : item . sourceInventoryBalanceId ! , sourceInventoryPositionId : item . sourceInventoryPositionId ! , sourceInboundOrderId : item . sourceInboundOrderId ! , skuCode : item . sku , productNameZh : item . name , totalPackageCount : item . quantity , notifiedPackageCount : item . quantity , shipmentNo : item . shipmentNo , trackingCode : form . trackingNo || undefined , newSkuCode : undefined , labelRequirement : form . requirement || undefined , cartonMark : item . cartonMark , fbaCartonMark : item . fbaCartonMark , destinationFbaWarehouseCode : item . destinationWarehouseId , operationType : 'CARTON_LABEL' as const , skuItems : [ ] , remark : item . remark || undefined } ) )
? cartonDetailRows . value . map ( ( item ) => ( { sourceInventoryBalanceId : item . sourceInventoryBalanceId ! , sourceInventoryPositionId : item . sourceInventoryPositionId ! , sourceInboundOrderId : item . sourceInboundOrderId ! , skuCode : item . sku , productNameZh : item . name , totalPackageCount : 1 , notifiedPackageCount : 1 , shipmentNo : item . shipmentNo , trackingCode : form . trackingNo || undefined , newSkuCode : undefined , labelRequirement : form . requirement || undefined , cartonMark : item . cartonMark , fbaCartonMark : item . fbaCartonMark , destinationFbaWarehouseCode : item . destinationWarehouseId , operationType : 'CARTON_LABEL' as const , skuItems : [ ] , remark : item . remark || undefined } ) )
: buildSkuRelabelItems ( )
const created = await createRelabel ( { customerOrderNo : sourceInboundOrders || '贴标申请' , operationWarehouseId : form . warehouse , labelDeadline : form . labelDeadline , contactName : form . contact , contactPhone : form . phone , chargeAmount : form . chargeAmount , remark : form . remark || undefined , idempotencyKey , items ,
deliveryMethod : labelingMode . value === 'sku' ? undefined : form . deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' ,
@@ -496,6 +564,7 @@ function selectTransferFbaAvailability(value: string | number | boolean | undefi
async function selectOperationWarehouse ( ) {
cargoRows . value = [ ]
skuRelabelRows . value = [ ]
importedRelabelBindings . value = [ ]
selectedRelabelInboundOrderId . value = ''
relabelCandidates . value = [ ]
const warehouse = operationWarehouses . value . find ( ( item ) => String ( item . id ) === form . warehouse )
@@ -512,7 +581,7 @@ function selectRelabelInventory(inboundOrderId: string) {
. filter ( ( candidate ) => String ( candidate . inboundOrderId ) === inboundOrderId )
. flatMap ( ( candidate ) => candidate . positions . flatMap ( ( position ) => Array . from ( { length : Math . min ( position . availablePackages , 500 ) } , ( ) => ( {
id : ++ cargoSequence , sku : candidate . skuCode || '' , name : candidate . productNameZh || '库存货物' ,
quantity : 1 , availableQuantity : 1 ,
included : false , quantity : 1 , availableQuantity : 1 ,
sourceInventoryBalanceId : String ( candidate . inventoryBalanceId ) , sourceInventoryPositionId : String ( position . positionId ) ,
sourceInboundOrderId : String ( candidate . inboundOrderId ) , sourceInboundOrderNo : candidate . inboundOrderNo ,
sourceInboundCode : candidate . inboundCode , sourceWarehouseName : candidate . overseasWarehouseName ,
@@ -520,7 +589,9 @@ function selectRelabelInventory(inboundOrderId: string) {
fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , cartonMark : '' , fbaCartonMark : '' ,
destinationWarehouseId : candidate . destinationFbaWarehouseCode || '' , remark : '' ,
} ) ) ) ) . slice ( 0 , 500 )
if ( cargoRows . value . length ) cargoRows . value [ 0 ] . included = true
skuRelabelRows . value = [ ]
importedRelabelBindings . value = [ ]
shippingLabelFile . value = null
if ( labelingMode . value !== 'carton' ) addSkuRelabelRow ( )
}
@@ -597,9 +668,15 @@ function selectRelabelInventory(inboundOrderId: string) {
< div class = "warehouse-section-heading warehouse-labeling-heading" >
< h3 > 货物明细 < / h3 >
< div class = "warehouse-template-actions" >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="downloadRelabelTemplate" > < Download :size = "15" / > 下载 { { labelingMode === 'carton' ? '箱唛' : 'SKU' } } 模板 < / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="t emplateInput?.click()" > < Upload :size = "15" / > 上传 { { labelingMode === 'carton' ? '箱唛' : 'SKU' } } 模板 < / el-button >
< input ref = "templateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate" >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="downloadRelabelTemplate('sku') " > < Download :size = "15" / > 下载 SKU 关联 模板< / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="skuT emplateInput?.click()" > < Upload :size = "15" / > 上传 SKU 关联 < / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="downloadRelabelTemplate('carton')" > < Download :size = "15" / > 下载箱号关联模板 < / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="cartonTemplateInput?.click()" > < Upload :size = "15" / > 上传箱号关联 < / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="downloadRelabelTemplate('mixed')" > < Download :size = "15" / > 下载 SKU 混箱模板 < / el-button >
< el-button size = "small" :disabled = "!cargoRows.length" @click ="mixedTemplateInput?.click()" > < Upload :size = "15" / > 上传 SKU 混箱 < / el-button >
< input ref = "skuTemplateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate($event, 'sku')" >
< input ref = "cartonTemplateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate($event, 'carton')" >
< input ref = "mixedTemplateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate($event, 'mixed')" >
< / div >
< / div >
< div class = "warehouse-labeling-mode" >
@@ -611,13 +688,17 @@ function selectRelabelInventory(inboundOrderId: string) {
< / el-radio-group >
< p > { { labelingModeHint } } < / p >
< / div >
< div v-if = "importedRelabelBindings.length" class="warehouse-import-preview" >
< div class = "warehouse-section-heading" > < div > < strong > 待导入绑定关系 < / strong > < p > 可只勾选本次需要换标的部分 , 也可以全选后一次导入 。 < / p > < / div > < div > < el-checkbox : model -value = " importedRelabelBindings.every ( ( item ) = > item . selected ) " @change=" selectAllImportedBindings ( Boolean ( $event ) ) ">全选</el-checkbox><el-button type=" primary " size=" small " @click=" importSelectedBindingsToDetails ">导入到明细</el-button></div></div>
<div class=" warehouse - table - scroll "><table class=" warehouse - binding - preview - table "><thead><tr><th>选择</th><th>关系类型</th><th>旧箱唛</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th></tr></thead><tbody><tr v-for=" item in importedRelabelBindings " :key=" item . id "><td><el-checkbox v-model=" item . selected " /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td><td>{{ warehouseDisplay(item.destinationWarehouseId) }}</td></tr></tbody></table></div>
</div>
<div class=" warehouse - section - heading warehouse - tab - actions ">
< p > 系统只提供历史入仓单的 大概库存位置, 不代表箱唛已绑定具体托盘 。 < / p >
<div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;SKU 与箱号关联通过旧箱唛合并。系统只提供 大概库存位置, 不代表箱唛已绑定具体托盘。</p></div>
<div v-if=" labelingMode === 'carton' "><el-button type=" primary " size=" small " @click=" addRelabelCargoRow "><Plus :size=" 15 " />手动添加行</el-button><el-button size=" small " @click=" clearRelabelRows ">清空</el-button></div>
<div v-else><el-button type=" primary " size=" small " @click=" addSkuRelabelRow "><Plus :size=" 15 " />添加 SKU 明细</el-button><el-button size=" small " @click=" skuRelabelRows = [ ] ">清空</el-button></div>
</div>
< div v-if = "labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table "><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数 * </th><th>我方 箱唛 *</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cargoRows " :key="row.id "><td><el-input v-model="row.sku " placeholder="选填 " /></td><td><el-input v-model="row.name " placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo " placeholder="请输入FBA货件编号" /></td><td><el-input-number v-model="row.quantity" :min="1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" / ></td><td><el-input v-model="row.cartonMark " placeholder="请输入我方箱唛 " /></td><td><el-input v-model="row.fbaCartonMark " placeholder="请输入新的 FBA 箱唛 " /></td><td><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError) " placeholder="请选择 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode " /></el-select></td><td><el-input v-model="row.remark" type="textarea " :rows="2 " maxlength="1000 " show-word-limit placeholder="请输入明细备注" /></td><td><button type="button " class="warehouse-delete" title="删除" @click="removeCargoRow(row.id) "><Trash2 :size="16 " />删除 </button></td></tr><tr v-if="cargoRows.length === 0 "><td colspan="9 "><div class="warehouse-empty" > 请先选择仍有库存的历史入仓单 , 系统将带出可用库存明细 < / div > < / td > < / tr > < / tbody > < / table > < / div >
< div v-else class = "warehouse-table-scroll" > < table class = "warehouse-edit-table warehouse-sku-label-table" > < thead > < tr > < th > 我方箱唛 * < / th > < th > 原 SKU * < / th > < th > 换标后 SKU * < / th > < th > 换标数量 ( 件 ) * < / th > < th v-if = "labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th v-if="labelingMode === 'carton_and_sku'">FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows " :key="row.id "><td><el-input v-model="row.cartonMark " placeholder="请输入我方箱唛 " /></td><td><el-input v-model="row.originalSku " placeholder="请输入原 SKU " /></td><td><el-input v-model="row.newSku " placeholder="请输入换标后 SKU " /></td><td><el-input-number v-model="row.quantity " :min="1 " :precision="0 " controls-position="right " placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark " placeholder="请输入新的 FBA 箱唛 " /></td><td v-if="labelingMode === 'carton_and_sku'"><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError) " placeholder="请选择 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode " /></el-select></td><td><el-input v-model="row.remark " placeholder="请输入备注" /></td><td><button type="button " class="warehouse-delete " title="删除 " @click="removeSkuRelabelRow(row.id) "><Trash2 :size="16 " />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 8 : 6"><div class="warehouse-empty" > 暂无换标明细 , 请点击 “ 添加 SKU 明细 ” < / div > < / td > < / tr > < / tbody > < / table > < / div >
<div v-if=" labelingMode === 'carton' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - label - table "><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>旧 箱唛 *</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in cartonDetailRows " :key=" row . id "><td><el-input v-model=" row . sku " placeholder=" 选填 " /></td><td><el-input v-model=" row . name " placeholder=" 请输入中文品名 " /></td><td><el-input v-model=" row . shipmentNo " placeholder=" 请输入FBA货件编号 " /></td><td><strong>1 箱</strong ></td><td><el-input v-model=" row . cartonMark " placeholder=" 请输入旧箱唛 " /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td><el-select v-model=" row . destinationWarehouseId " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " placeholder=" 请选择 FBA 目的仓 "><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " maxlength=" 1000 " show-word-limit placeholder=" 请输入明细备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 移出本次明细 " @click=" removeCargoRow ( row . id ) "><Trash2 :size=" 16 " />移出 </button></td></tr><tr v-if=" cartonDetailRows . length === 0 "><td colspan=" 9 "><div class=" warehouse - empty ">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
<div v-else class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - sku - label - table "><thead><tr><th>旧箱唛 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if=" labelingMode === 'carton_and_sku' ">新 FBA 箱唛 *</th><th v-if=" labelingMode === 'carton_and_sku' ">FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in skuRelabelRows " :key=" row . id "><td><el-input v-model=" row . cartonMark " placeholder=" 请输入旧箱唛 " @change=" applyExistingCartonBinding ( row ) " /></td><td><el-input v-model=" row . originalSku " placeholder=" 请输入旧 SKU " @change=" applyExistingSkuBinding ( row ) " /></td><td><el-input v-model=" row . newSku " placeholder=" 请输入新 SKU " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入件数 " /></td><td v-if=" labelingMode === 'carton_and_sku' "><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td v-if=" labelingMode === 'carton_and_sku' "><el-select v-model=" row . destinationWarehouseId " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " placeholder=" 请选择 FBA 目的仓 "><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select></td><td><el-input v-model=" row . remark " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除 " @click=" removeSkuRelabelRow ( row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" skuRelabelRows . length === 0 "><td :colspan=" labelingMode === 'carton_and_sku' ? 8 : 6 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
</template>
<template v-else>
<div class=" warehouse - section - heading "><h3>货物明细</h3><div v-if=" props . service !== 'disposal' "><el-button type=" primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [ ] ">清空</el-button></div></div>
@@ -662,4 +743,5 @@ function selectRelabelInventory(inboundOrderId: string) {
. warehouse - labeling - tabs { margin - top : - 8 px } . warehouse - labeling - heading { align - items : flex - start } . warehouse - labeling - heading h3 { margin - bottom : 16 px } . warehouse - template - actions { display : flex ; gap : 8 px ; padding - bottom : 12 px } . warehouse - tab - actions { align - items : center ; margin : 2 px 0 14 px } . warehouse - tab - actions p { margin : 0 ; color : # 7 d8797 ; font - size : 12 px } . warehouse - tab - actions > div { display : flex ; gap : 8 px } . warehouse - sku - notice { margin : 0 0 14 px ; padding : 10 px 12 px ; border : 1 px solid # f2d39b ; border - radius : 4 px ; background : # fff8e8 ; color : # 8 a6116 ; font - size : 12 px } . warehouse - sku - label - table { min - width : 980 px } . warehouse - sku - label - table : deep ( . el - input - number ) { width : 160 px } : deep ( . warehouse - labeling - tabs > . el - tabs _ _header ) { margin - bottom : 18 px } : deep ( . warehouse - labeling - tabs . el - tabs _ _item ) { height : 42 px ; padding : 0 26 px ; font - weight : 600 } . warehouse - labeling - mode { display : flex ; align - items : center ; gap : 14 px ; margin : 0 0 16 px ; padding : 14 px 16 px ; border : 1 px solid # dce5f5 ; border - radius : 5 px ; background : # f7f9ff } . warehouse - labeling - mode > span { color : # 34445 c ; font - size : 13 px ; font - weight : 650 ; white - space : nowrap } . warehouse - labeling - mode p { margin : 0 ; color : # 71809 a ; font - size : 12 px } . warehouse - labeling - mode : deep ( . el - radio - group ) { display : flex ; flex - shrink : 0 } . warehouse - labeling - mode : deep ( . el - radio - button _ _inner ) { padding : 8 px 14 px }
. warehouse - relabel - detail - table { min - width : 1420 px } . warehouse - relabel - detail - table td strong { color : # 253 f67 }
. warehouse - native - file { min - height : 32 px ; padding : 5 px 8 px ; border : 1 px solid # dcdfe6 ; border - radius : 4 px ; background : # fff ; color : # 606266 } . warehouse - fulfillment - grid { margin - bottom : 18 px } . warehouse - evidence { display : flex ; align - items : center ; gap : 16 px ; padding : 14 px ; border : 1 px solid # e7ebf1 ; border - radius : 4 px ; background : # f8fafc } . warehouse - evidence > span { color : # 71809 a ; font - size : 12 px } . warehouse - evidence > strong { color : # 8 a95a5 ; font - size : 12 px ; font - weight : 400 } . warehouse - photo - actions { display : flex ; flex - wrap : wrap ; gap : 8 px } . warehouse - completion - remark { margin : 10 px 0 0 ; color : # 596579 ; font - size : 12 px }
. warehouse - template - actions { flex - wrap : wrap ; justify - content : flex - end } . warehouse - import - preview { margin : 0 0 16 px ; padding : 14 px ; border : 1 px solid # cfe0f5 ; border - radius : 5 px ; background : # f8fbff } . warehouse - import - preview . warehouse - section - heading { margin - bottom : 10 px } . warehouse - import - preview . warehouse - section - heading > div { display : flex ; align - items : center ; gap : 10 px } . warehouse - import - preview p { margin : 0 ; color : # 7 d8797 ; font - size : 12 px } . warehouse - binding - preview - table { min - width : 980 px ; background : # fff }
< / style >