@@ -4,21 +4,26 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft , Download , Eye , Plus , Search , Trash2 , Upload } from '@lucide/vue'
import {
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer ,
listDiscardCandidates , listDiscards , listLastlegOrders , listRelabels , listTransfers ,
listDiscardCandidates , listDiscards , listLastlegOrders , listRelabelInventoryCandidates , listRelabels , listTransfers ,
type RelabelInventoryCandidate ,
type RelabelCreate ,
type RelabelDetail ,
} from '../api/warehouseServices'
import { getMerchantWarehouses , type MerchantWarehouse } from '@/modules/first-leg/api/firstLeg'
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail'
interface CargoRow { id : number ; sku : string ; name : string ; quantity : number ; availableQuantity ? : number ; sourceOrderId ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
type LabelingMode = 'carton' | '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 }
interface SkuRelabelRow { id : number ; sourceRowId : number | null ; cartonMark : string ; originalSku : string ; newSku : string ; quantity : number ; changeCarton : boolean ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface ServiceConfig { title : string ; createTitle : string ; numberLabel : string ; emptyText : string ; columns : string [ ] }
const props = defineProps < { service : ServiceType } > ( )
const configs : Record < ServiceType , ServiceConfig > = {
labeling : { title : '贴标申请' , createTitle : '新增贴标申请' , numberLabel : '贴标申请单号' , emptyText : '暂无贴标申请' , columns : [ '贴标申请单号' , '客户单号' , '目的 海外仓' , '贴标要求' , 'SKU 数' , '总箱数' , '状态' , '创建时间' , '操作' ] } ,
transfer : { title : '仓储转运单' , createTitle : '新增仓储转运单' , numberLabel : '转运单号' , emptyText : '暂无仓储转运订单' , columns : [ '转运单号' , '进仓编号' , '发货箱数' , '操作仓' , 'FBA 目的仓' , '派送方式' , '是否换标' , '状态' , '创建时间' , '操作' ] } ,
disposal : { title : '货物废弃' , createTitle : '新增货物废弃申请' , numberLabel : '废弃申请单号' , emptyText : '暂无货物废弃申请' , columns : [ '废弃申请单号' , '进仓编号' , '操作仓' , 'SKU 汇总' , '废弃数量' , '预估费用' , '状态' , '创建时间' , '操作' ] } ,
relabel : { title : '退货换标' , createTitle : '新建退货换标单' , numberLabel : '退货换标单号' , emptyText : '暂无退货换标订单' , columns : [ '退货换标单号' , '来源 FBA 仓' , '目的 FBA 仓' , 'SKU 汇总' , '退件总数' , '是否换标' , '换标状态' , '尾程状态' , '创建时间' , '操作' ] } ,
labeling : { title : '贴标申请' , createTitle : '新增贴标申请' , numberLabel : '贴标申请单号' , emptyText : '暂无贴标申请' , columns : [ '操作' , ' 贴标申请单号', '客户单号' , '海外操作 仓' , '贴标要求' , 'SKU 数' , '总箱数' , '状态' , '创建时间' ] } ,
transfer : { title : '仓储转运单' , createTitle : '新增仓储转运单' , numberLabel : '转运单号' , emptyText : '暂无仓储转运订单' , columns : [ '操作' , ' 转运单号', '进仓编号' , '发货箱数' , '操作仓' , 'FBA 目的仓' , '派送方式' , '是否换标' , '状态' , '创建时间' ] } ,
disposal : { title : '货物废弃' , createTitle : '新增货物废弃申请' , numberLabel : '废弃申请单号' , emptyText : '暂无货物废弃申请' , columns : [ '操作' , ' 废弃申请单号', '进仓编号' , '操作仓' , 'SKU 汇总' , '废弃数量' , '预估费用' , '状态' , '创建时间' ] } ,
relabel : { title : '退货换标' , createTitle : '新建退货换标单' , numberLabel : '退货换标单号' , emptyText : '暂无退货换标订单' , columns : [ '操作' , ' 退货换标单号', '来源 FBA 仓' , '目的 FBA 仓' , 'SKU 汇总' , '退件总数' , '是否换标' , '换标状态' , '尾程状态' , '创建时间' ] } ,
}
const config = computed ( ( ) => configs [ props . service ] )
const statusOptions = computed ( ( ) => props . service === 'labeling'
@@ -30,28 +35,64 @@ const mode = ref<PageMode>('list')
const filters = reactive ( { number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' } )
const form = reactive ( { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 , chargeAmount : 0 } )
const cargoRows = ref < CargoRow [ ] > ( [ ] )
const labelingMode = ref < LabelingMode > ( 'carton' )
const skuRelabelRows = ref < SkuRelabelRow [ ] > ( [ ] )
const cartonMarkOptions = computed ( ( ) => cargoRows . value . map ( ( item ) => ( {
value : item . id ,
label : ` ${ item . cartonMark || item . sku || '库存货物' } |可用 ${ item . availableQuantity || item . quantity } 箱 ` ,
} ) ) )
const submittedRows = ref < Record < string , string > [ ] > ( [ ] )
const selectedDetail = ref < Record < string , string > | null > ( null )
const selectedRelabelDetail = ref < RelabelDetail | null > ( null )
const fbaWarehouses = ref < MerchantWarehouse [ ] > ( [ ] )
const operationWarehouses = ref < MerchantWarehouse [ ] > ( [ ] )
const fbaWarehousesLoading = ref ( false )
const fbaWarehousesError = ref ( '' )
const discardCandidates = ref < import ( ' .. / api / warehouseServices ' ) .DiscardCandidate [ ] > ( [ ] )
const selectedDiscardOrderId = ref ( '' )
const discardCandidatesLoading = ref ( false )
const relabelCandidates = ref < RelabelInventoryCandidate [ ] > ( [ ] )
const selectedRelabelInboundOrderId = ref ( '' )
const relabelCandidatesLoading = ref ( false )
const relabelInboundOptions = computed ( ( ) => {
const grouped = new Map < string , { value : string ; inboundOrderNo : string ; inboundCode : string ; availablePackages : number } > ( )
for ( const candidate of relabelCandidates . value ) {
const value = String ( candidate . inboundOrderId )
const current = grouped . get ( value )
if ( current ) current . availablePackages += candidate . availablePackages
else grouped . set ( value , {
value ,
inboundOrderNo : candidate . inboundOrderNo || '—' ,
inboundCode : candidate . inboundCode || '—' ,
availablePackages : candidate . availablePackages ,
} )
}
return [ ... grouped . values ( ) ]
} )
const loading = ref ( false )
const saving = ref ( false )
const templateInput = ref < HTMLInputElement | null > ( null )
let cargoSequence = 0
let skuRelabelSequence = 0
watch ( ( ) => props . service , ( ) => { mode . value = 'list' ; resetFilters ( ) ; resetForm ( ) ; void load ( ) } )
watch ( labelingMode , ( value ) => { if ( value === 'sku' && ! skuRelabelRows . value . length ) addSkuRelabelRow ( ) } )
onMounted ( ( ) => { void load ( ) } )
function resetFilters ( ) { Object . assign ( filters , { number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' } ) }
function resetForm ( ) {
Object . assign ( form , { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 , chargeAmount : 0 } )
cargoRows . value = [ ]
labelingMode . value = 'carton'
skuRelabelRows . value = [ ]
discardCandidates . value = [ ]
selectedDiscardOrderId . value = ''
relabelCandidates . value = [ ]
selectedRelabelInboundOrderId . value = ''
}
async function openCreate ( ) {
resetForm ( ) ; mode . value = 'create'
if ( props . service === 'labeling' || props . service === 'relabel' || props . service === 'transfer' ) void loadWarehouses ( )
if ( props . service === 'labeling' ) return
if ( props . service !== 'disposal' ) { addCargoRow ( ) ; return }
discardCandidatesLoading . value = true
try { discardCandidates . value = await listDiscardCandidates ( ) }
@@ -59,12 +100,78 @@ async function openCreate() {
finally { discardCandidatesLoading . value = false }
}
function addCargoRow ( ) { cargoRows . value . push ( { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } ) }
function removeCargoRow ( id : number ) { cargoRows . value = cargoRows . value . filter ( ( row ) => row . id !== id ) }
function csvCell ( value : string ) { return ` " ${ value . replaceAll ( '"' , '""' ) } " ` }
function addSkuRelabelRow ( ) { skuRelabelRows . value . push ( { id : ++ skuRelabelSequence , sourceRowId : null , cartonMark : '' , originalSku : '' , newSku : '' , quantity : 1 , changeCarton : false , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } ) }
function removeSkuRelabelRow ( id : number ) { skuRelabelRows . value = skuRelabelRows . value . filter ( ( row ) => row . id !== id ) }
function selectSkuSource ( row : SkuRelabelRow ) {
const source = cargoRows . value . find ( ( item ) => item . id === row . sourceRowId )
if ( ! source ) return
row . cartonMark = source . cartonMark || ''
row . originalSku = row . originalSku || source . sku
row . destinationWarehouseId = row . destinationWarehouseId || source . destinationWarehouseId
}
function buildSkuRelabelItems ( ) : RelabelCreate [ 'items' ] {
const groups = new Map < number , SkuRelabelRow [ ] > ( )
for ( const row of skuRelabelRows . value ) {
if ( row . sourceRowId == null ) throw new Error ( '请选择每条 SKU 明细对应的来源库存箱' )
const rows = groups . get ( row . sourceRowId ) || [ ]
rows . push ( row )
groups . set ( row . sourceRowId , rows )
}
return [ ... groups . entries ( ) ] . map ( ( [ sourceRowId , rows ] ) => {
const source = cargoRows . value . find ( ( item ) => item . id === sourceRowId )
if ( ! source ? . sourceInventoryBalanceId || ! source . sourceInventoryPositionId || ! source . sourceInboundOrderId ) {
throw new Error ( '来源库存已变化,请重新选择有库存的历史入仓单' )
}
const cartonMark = rows [ 0 ] . cartonMark . trim ( )
if ( ! cartonMark || rows . some ( ( row ) => row . cartonMark . trim ( ) !== cartonMark ) ) {
throw new Error ( '同一来源库存箱的 SKU 明细必须使用同一个我方箱唛' )
}
const changeCarton = rows [ 0 ] . changeCarton
if ( rows . some ( ( row ) => row . changeCarton !== changeCarton ) ) {
throw new Error ( '同一我方箱唛的 SKU 明细必须统一是否同时更换箱唛' )
}
const fbaCartonMark = rows [ 0 ] . fbaCartonMark . trim ( )
const destinationWarehouseId = rows [ 0 ] . destinationWarehouseId
if ( changeCarton && ( ! fbaCartonMark || ! destinationWarehouseId
|| rows . some ( ( row ) => row . fbaCartonMark . trim ( ) !== fbaCartonMark || row . destinationWarehouseId !== destinationWarehouseId ) ) ) {
throw new Error ( '同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛和目的仓' )
}
return {
sourceInventoryBalanceId : source . sourceInventoryBalanceId ,
sourceInventoryPositionId : source . sourceInventoryPositionId ,
sourceInboundOrderId : source . sourceInboundOrderId ,
skuCode : source . sku || rows [ 0 ] . originalSku ,
productNameZh : source . name ,
totalPackageCount : source . quantity ,
notifiedPackageCount : source . quantity ,
shipmentNo : source . shipmentNo || cartonMark ,
trackingCode : form . trackingNo || undefined ,
newSkuCode : rows [ 0 ] . newSku ,
labelRequirement : form . requirement || undefined ,
cartonMark ,
fbaCartonMark : changeCarton ? fbaCartonMark : undefined ,
destinationFbaWarehouseCode : changeCarton ? destinationWarehouseId : undefined ,
operationType : changeCarton ? 'CARTON_AND_SKU' as const : 'SKU_LABEL' as const ,
skuItems : rows . map ( ( row ) => ( { skuCode : row . originalSku . trim ( ) , newSkuCode : row . newSku . trim ( ) , quantity : row . quantity , remark : row . remark || undefined } ) ) ,
remark : undefined ,
}
} )
}
function addRelabelCargoRow ( ) {
if ( ! selectedRelabelInboundOrderId . value ) { ElMessage . warning ( '请先选择有库存的历史入仓单' ) ; return }
ElMessage . info ( '货物明细已按所选入仓单的可用库存带出,请直接编辑现有明细' )
}
function removeCargoRow ( id : number ) {
cargoRows . value = cargoRows . value . filter ( ( row ) => row . id !== id )
}
function clearRelabelRows ( ) { cargoRows . value = [ ] ; selectedRelabelInboundOrderId . value = '' }
function csvCell ( value : string | number | undefined ) { return ` " ${ String ( value ? ? '' ) . replaceAll ( '"' , '""' ) } " ` }
function downloadRelabelTemplate ( ) {
const header = [ '商品SKU(选填)' , '中文品名' , 'FBA货件编号' , '换标新SKU' , '我方箱唛' , 'FBA箱唛' , '目的仓' , '备注' ]
const example = [ '' , '示例商品' , 'FBA-SHIPMENT-001' , 'LX1000-NEW ', 'LX1000 箱唛' , 'FBA-LX1000 箱唛' , 'ONT8 ' , '' ]
const blob = new Blob ( [ ` \ uFEFF ${ header . map ( csvCell ) . join ( ',' ) } \ r \ n ${ example . map ( csvCell ) . join ( ',' ) } \ r \ n ` ] , { type : 'text/csv;charset=utf-8' } )
if ( ! cargoRows . value . length ) { ElMessage . warning ( '请先选择有库存的历史入仓单' ) ; return }
const header = [ '明细行标识 ' , '本次箱数' , '商品SKU' , '中文品名' , 'FBA货件编号' , '换标新SKU ', '我方 箱唛' , 'FBA箱唛' , 'FBA目的仓 ' , '备注 ' ]
const rows = cargoRows . value . map ( ( item ) => [ item . id , item . quantity , item . sku , item . name , item . shipmentNo , item . newSkuCode ,
item . cartonMark , item . fbaCartonMark , item . destinationWarehouseId , 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 = '贴标申请-关联面单模板.csv' ; anchor . click ( ) ; URL . revokeObjectURL ( url )
@@ -84,20 +191,37 @@ async function importRelabelTemplate(event: Event) {
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 ] ) ; const required = [ '中文品名' , 'FBA货件编号' , '换标新SKU' , '我方箱唛' , 'FBA箱唛' ]
const skuHeader = headers . includes ( '商品SKU(选填) ') ? '商品SKU(选填)' : '商品SKU'
if ( ! headers . includes ( skuHeader ) ) { ElMessage . error ( '模板格式不正确,请重新下载模板填写' ) ; return }
const headers = parseCsvLine ( lines [ 0 ] )
const required = [ '明细行标识' , '本次箱数 ', 'FBA货件编号' , '换标新SKU' , '我方箱唛' , '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 ) => required . some ( ( key ) => item [ key ] ) )
if ( ! imported . length || imported . some ( ( item ) => required . some ( ( key ) => ! item [ key ] ) ) ) { ElMessage . error ( '模板中的商品、货件、换标 SKU 和箱唛信息必须填写完整' ) ; return }
if ( new Set ( imported . map ( ( item ) => item [ '我方箱唛 ' ] ) ) . size !== imported . length ) { ElMessage . error ( '模板中的我方箱唛不能重复' ) ; return }
cargoRows . value = imported . map ( ( item ) => ( {
id : ++ cargoSequence , sku : item [ skuHeader ] , name : item [ '中文品名 '] , quantity : 1 , fnsku : '' ,
shipmentNo : item [ 'FBA货件编号' ] , newSkuCode : item [ '换标新SKU' ] , shippingMark : '' ,
cartonMark : item [ '我方箱唛 ' ] , fbaCartonMark : item [ 'FBA箱唛' ] ,
destinationWarehouseId : item [ '目的仓' ] || '' , remark : item [ '备注' ] || '' ,
} ) )
ElMessage . success ( ` 已导入 ${ cargoRows . value . length } 箱货物明细 ` )
const imported = lines . slice ( 1 ) . map ( parseCsvLine )
. map ( ( values ) => Object . fromEntries ( headers . map ( ( key , index ) => [ key , values [ index ] || '' ] ) ) )
. filter ( ( item ) => item [ '明细行标识 ' ] )
if ( ! imported . length || imported . some ( ( item ) => required . some ( ( key ) => ! item [ key ] ) ) ) {
ElMessage . error ( '模板中的明细行、箱数、货件、换标 SKU、箱唛和 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
}
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
}
row . quantity = quantity
row . shipmentNo = item [ 'FBA货件编号' ]
row . newSkuCode = item [ '换标新SKU' ]
row . cartonMark = item [ '我方箱唛' ]
row . fbaCartonMark = item [ 'FBA箱唛' ]
row . destinationWarehouseId = item [ 'FBA目的仓' ]
row . remark = item [ '备注' ] || ''
}
ElMessage . success ( ` 已导入 ${ imported . length } 条关联面单明细 ` )
}
function selectDiscardInbound ( orderId : string ) {
const candidate = discardCandidates . value . find ( ( item ) => item . orderId === orderId )
@@ -112,10 +236,21 @@ function selectDiscardInbound(orderId: string) {
}
function dateTime ( value : string , end = false ) { return value ? ` ${ value } T ${ end ? '23:59:59' : '00:00:00' } ` : undefined }
function row ( values : Array < string | number | undefined | null > ) { return Object . fromEntries ( values . map ( ( value , index ) => [ String ( index ) , value == null || value === '' ? '—' : String ( value ) ] ) ) }
function statusLabel ( value : string ) {
return statusOptions . value . find ( ( option ) => option . value === value ) ? . label || value
}
function deliveryMethodLabel ( value : string ) {
return ( { TRUCK : '卡车派送' , EXPRESS : '快递派送' , '卡车派送' : '卡车派送' , '快递派送' : '快递派送' } as Record < string , string > ) [ value ] || value
}
function formatDateTime ( value : string ) {
return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) : '—'
}
function warehouseDisplay ( value ? : string ) {
if ( ! value ) return '—'
const labels : Record < string , string > = { ONT8 : '美西海外仓-ONT8' , ABE8 : '美东海外仓-ABE8' , MDW2 : '美中海外仓-MDW2' }
return value . split ( ',' ) . map ( ( warehouse ) => labels [ warehouse ] || warehouse ) . join ( '、' )
return value . split ( ',' ) . map ( ( code ) => {
const warehouse = [ ... operationWarehouses . value , ... fbaWarehouses . value ] . find ( ( item ) => String ( item . id ) === code || item . warehouseCode === code )
return warehouse ? warehouseLabel ( warehouse ) : code
} ) . join ( '、' )
}
async function load ( ) {
loading . value = true
@@ -124,16 +259,16 @@ async function load() {
const result = await listRelabels ( { page : 1 , size : 20 , relabelNo : filters . number || undefined , overseasWarehouseId : filters . warehouse || undefined , status : filters . status || undefined , createdFrom : dateTime ( filters . start ) , createdTo : dateTime ( filters . end , true ) } )
submittedRows . value = result . records . map ( ( item ) => {
const destinationWarehouses = warehouseDisplay ( item . overseasWarehouseId )
return row ( [ item . relabelNo , item . customerOrderNo , destinationWarehouses , '—' , item . itemCount , item . totalPackageCount , item . status , item . createdAt ] )
return row ( [ item . relabelNo , item . customerOrderNo , destinationWarehouses , '—' , item . itemCount , item . totalPackageCount , statusLabel ( item . status ) , formatDateTime ( item . createdAt ) ] )
} )
} else if ( props . service === 'disposal' ) {
const result = await listDiscards ( { page : 1 , size : 20 , discardNo : filters . number || undefined , inboundCode : filters . inboundNo || undefined , overseasWarehouseId : filters . warehouse || undefined , status : filters . status || undefined , createdFrom : dateTime ( filters . start ) , createdTo : dateTime ( filters . end , true ) } )
submittedRows . value = result . records . map ( ( item ) => row ( [ item . discardNo , item . inboundCodes , item . overseasWarehouseName || '—' , '—' , item . totalPackages , ` ${ item . currency } ${ item . totalFee } ` , item . status , item . createdAt ] ) )
submittedRows . value = result . records . map ( ( item ) => row ( [ item . discardNo , item . inboundCodes , item . overseasWarehouseName || '—' , '—' , item . totalPackages , ` ${ item . currency } ${ item . totalFee } ` , statusLabel ( item . status ) , formatDateTime ( item . createdAt ) ] ) )
} else {
const result = await listTransfers ( { page : 1 , size : 20 , transferNo : filters . number || undefined , inboundCode : filters . inboundNo || undefined , operatingWarehouseId : filters . warehouse || undefined , fbaWarehouseCode : filters . destination || undefined , status : filters . status || undefined , needRelabel : props . service === 'relabel' , createdFrom : dateTime ( filters . start ) , createdTo : dateTime ( filters . end , true ) } )
submittedRows . value = result . records . map ( ( item ) => props . service === 'transfer'
? row ( [ item . transferNo , item . inboundCode , item . packageCount , '—' , item . fbaWarehouseCode , item . deliveryMethod , item . needRelabel ? '是' : '否' , item . status , item . createdAt ] )
: row ( [ item . transferNo , '—' , item . fbaWarehouseCode , '—' , item . packageCount , '是' , item . relabelNo ? '已生成' : '待生成' , item . status , item . createdAt ] ) )
? row ( [ item . transferNo , item . inboundCode , item . packageCount , '—' , item . fbaWarehouseCode , deliveryMethodLabel ( item . deliveryMethod ) , item . needRelabel ? '是' : '否' , statusLabel ( item . status ) , formatDateTime ( item . createdAt ) ] )
: row ( [ item . transferNo , '—' , item . fbaWarehouseCode , '—' , item . packageCount , '是' , item . relabelNo ? '已生成' : '待生成' , statusLabel ( item . status ) , formatDateTime ( item . createdAt ) ] ) )
}
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '列表加载失败' ) }
finally { loading . value = false }
@@ -141,21 +276,25 @@ async function load() {
function query ( ) { void load ( ) }
async function submit ( ) {
const primaryWarehouse = props . service === 'relabel' ? form . sourceWarehouse : form . warehouse
const relabelRows = labelingMode . value === 'sku' ? skuRelabelRows . value : cargoRows . 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 ) )
: cargoRows . value . length === 0 || cargoRows . value . some ( ( item ) => ( props . service ! == 'labeling' && ! item . sku ) || ! item . name || item . quantity < 1 )
if ( ( ! form . customerNo && ! form . inboundNo ) || ( props . service !== 'labeling' && ! primaryWarehouse ) || invalidCargo ) {
: relabelRows . length === 0 || ( props . service = == 'labeling' && labelingMode . value === 'sku'
? skuRelabelRows . value . some ( ( item ) => ! item . sourceRowId || ! item . cartonMark || ! item . originalSku || ! item . newSku || item . quantity < 1
|| ( item . changeCarton && ( ! item . fbaCartonMark || ! item . destinationWarehouseId ) ) )
: cargoRows . value . some ( ( item ) => ( props . service !== 'labeling' && ! item . sku ) || ! item . name || item . quantity < 1 ) )
if ( ( props . service !== 'labeling' && ! form . customerNo && ! form . inboundNo ) || ( props . service !== 'disposal' && ! primaryWarehouse ) || invalidCargo ) {
ElMessage . warning ( '请填写必填信息和完整的货物明细' ) ; return
}
if ( props . service === 'labeling' && ( ! form . labelDeadline || ! form . contact || ! form . phone || form . chargeAmount <= 0 || cargoRows . value . some ( ( item ) => ! item . shipmentNo || ! item . newSkuCode ) ) ) {
ElMessage . warning ( '请填写贴标截止时间、联系人、 贴标费用以及货物的货件编号和新 SKU ' ) ; return
if ( props . service === 'labeling' && ( ! form . labelDeadline || ! form . contact || ! form . phone || form . chargeAmount <= 0 ) ) {
ElMessage . warning ( '请填写贴标截止时间、联系人和 贴标费用' ) ; return
}
if ( props . service === 'labeling' && cargoRows . value . some ( ( item ) => ! item . cartonMark || ! item . fbaCartonMark ) ) {
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cargoRows . value . some ( ( item ) => ! item . shipmentNo || ! item . newSkuCode || ! item . cartonMark || ! item . fbaCartonMark ) ) {
ElMessage . warning ( '请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整' ) ; return
}
if ( props . service === 'labeling' && cargoRows . value . some ( ( item ) => ! item . destinationWarehouseId && ! form . warehouse ) ) {
ElMessage . warning ( '请填写统一目的海外仓,或为每箱货物填写 目的仓' ) ; return
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cargoRows . value . some ( ( item ) => ! item . destinationWarehouseId || ! item . sourceInventoryPositionId ) ) {
ElMessage . warning ( '请选择有库存的历史入仓单,并为每项填写 FBA 目的仓' ) ; return
}
if ( props . service === 'disposal' && ! form . reason ) {
ElMessage . warning ( '请选择废弃原因' ) ; return
@@ -168,7 +307,11 @@ async function submit() {
try {
const idempotencyKey = crypto . randomUUID ( )
if ( props . service === 'labeling' ) {
await createRelabel ( { customerOrderNo : form . customerNo , overseasWarehouseId : form . warehouse || undefined , labelDeadline : form . labelDeadline , contactName : form . contact , contactPhone : form . phone , chargeAmount : form . chargeAmount , remark : form . remark || undefined , idempotencyKey , items : cargoRows . value . map ( ( item ) => ( { skuCode : item . sku , productNameZh : item . name , totalPackageCount : 1 , notifiedPackageCount : 1 , shipmentNo : item . shipmentNo , trackingCode : form . trackingNo || undefined , newSkuCode : item . newSkuCode , labelRequirement : form . requirement || undefined , cartonMark : item . cartonMark , fbaCartonMark : item . fbaCartonMark , destinationWarehouseId : item . destinationWarehouseId || undefined , remark : item . remark || undefined } ) ) } )
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 : item . newSkuCode , labelRequirement : form . requirement || undefined , cartonMark : item . cartonMark , fbaCartonMark : item . fbaCartonMark , destinationFbaWarehouseCode : item . destinationWarehouseId , operationType : 'CARTON_LABEL' as const , skuItems : [ ] , remark : item . remark || undefined } ) )
: buildSkuRelabelItems ( )
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 } )
} else if ( props . service === 'disposal' ) {
const items = cargoRows . value . filter ( ( item ) => item . quantity > 0 ) . map ( ( item ) => {
if ( ! item . sourceOrderId || item . sourceOrderId !== selectedDiscardOrderId . value || item . quantity > Number ( item . availableQuantity || 0 ) ) {
@@ -195,6 +338,57 @@ async function openDetail(value: Record<string, string>) {
mode . value = 'detail'
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '详情加载失败' ) }
}
async function loadWarehouses ( ) {
fbaWarehousesLoading . value = true
fbaWarehousesError . value = ''
try {
const [ fbaResult , operationResult ] = await Promise . all ( [
getMerchantWarehouses ( { warehouseType : 'FBA' } ) ,
getMerchantWarehouses ( { warehouseType : 'OVERSEAS' } ) ,
] )
fbaWarehouses . value = fbaResult . records
operationWarehouses . value = operationResult . records
} catch ( error ) {
fbaWarehouses . value = [ ]
fbaWarehousesError . value = error instanceof Error ? error . message : 'FBA 目的仓加载失败'
ElMessage . error ( fbaWarehousesError . value )
} finally { fbaWarehousesLoading . value = false }
}
function warehouseLabel ( value : MerchantWarehouse ) { return ` ${ value . warehouseName } · ${ value . warehouseCode } ` }
async function selectOperationWarehouse ( ) {
cargoRows . value = [ ]
skuRelabelRows . value = [ ]
selectedRelabelInboundOrderId . value = ''
relabelCandidates . value = [ ]
const warehouse = operationWarehouses . value . find ( ( item ) => String ( item . id ) === form . warehouse )
form . contact = warehouse ? . contactName || ''
form . phone = warehouse ? . contactPhone || ''
if ( ! form . warehouse ) return
relabelCandidatesLoading . value = true
try { relabelCandidates . value = await listRelabelInventoryCandidates ( form . warehouse ) }
catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '当前客户库存加载失败' ) }
finally { relabelCandidatesLoading . value = false }
}
function selectRelabelInventory ( inboundOrderId : string ) {
const existing = new Map ( cargoRows . value . map ( ( row ) => [ row . sourceInventoryPositionId , row ] ) )
cargoRows . value = relabelCandidates . value
. filter ( ( candidate ) => String ( candidate . inboundOrderId ) === inboundOrderId )
. flatMap ( ( candidate ) => candidate . positions . map ( ( position ) => {
const previous = existing . get ( String ( position . positionId ) )
return {
id : previous ? . id || ++ cargoSequence , sku : candidate . skuCode || '' , name : candidate . productNameZh || '库存货物' ,
quantity : Math . min ( previous ? . quantity || 1 , position . availablePackages ) , availableQuantity : position . availablePackages ,
sourceInventoryBalanceId : String ( candidate . inventoryBalanceId ) , sourceInventoryPositionId : String ( position . positionId ) ,
sourceInboundOrderId : String ( candidate . inboundOrderId ) , sourceInboundOrderNo : candidate . inboundOrderNo ,
sourceInboundCode : candidate . inboundCode , sourceWarehouseName : candidate . overseasWarehouseName ,
palletNo : position . palletNo , warehouseLocationCode : position . warehouseLocationCode ,
fnsku : '' , shipmentNo : previous ? . shipmentNo || '' , newSkuCode : previous ? . newSkuCode || '' , shippingMark : '' ,
cartonMark : previous ? . cartonMark || '' , fbaCartonMark : previous ? . fbaCartonMark || '' ,
destinationWarehouseId : previous ? . destinationWarehouseId || candidate . destinationFbaWarehouseCode || '' , remark : previous ? . remark || '' ,
}
} ) )
skuRelabelRows . value = [ ]
}
< / script >
< template >
@@ -215,7 +409,7 @@ async function openDetail(value: Record<string, string>) {
< div class = "warehouse-filter-actions" > < el-button type = "primary" @click ="query" > < Search :size = "16" / > 查询 < / el-button > < el-button @click ="resetFilters(); query()" > 重置 < / el -button > < / div >
< / div >
< div v-loading = "loading" class="warehouse-card warehouse-table-card" >
< div class = "warehouse-table-scroll" > < table > < thead > < tr > < th v-for = "column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td v-for="(_, index) in config.columns.slice(0, -1)" :key="index">{{ row[String(index)] }}</td><td ><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button></td></tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty" > {{ config.emptyText }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "warehouse-table-scroll" > < table > < thead > < tr > < th v-for = "column in config.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="(row, rowIndex) in submittedRows" :key="rowIndex"><td><button class="warehouse-link" type="button" @click="openDetail(row)"><Eye :size="15" />查看</button></td><td v-for="(_, index) in config.columns.slice(1)" :key="index">{{ row[String(index)] }}</td>< /tr><tr v-if="submittedRows.length === 0"><td :colspan="config.columns.length"><div class="warehouse-empty" > {{ config.emptyText }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "warehouse-pagination" > < span > 共 < strong > { { submittedRows . length } } < / strong > 条 < / span > < el-button size = "small" disabled > 上一页 < / el-button > < button type = "button" class = "warehouse-page-number" > 1 < / button > < el-button size = "small" disabled > 下一页 < / el-button > < / div >
< / div >
< / template >
@@ -226,32 +420,47 @@ async function openDetail(value: Record<string, string>) {
< form @submit.prevent ="submit" >
< div class = "warehouse-card warehouse-form-section" > < h3 > { { props . service === 'relabel' ? '退货信息' : props . service === 'labeling' ? '订单信息' : '基础与派送信息' } } < / h3 >
< div class = "warehouse-form-grid" >
< label v-if = "props.service === 'labeling'"><span><i>*</i>客户单号</span><el-input v-model="form.customerNo" placeholder="请输入客户单号" / > < / label >
< label v-if = "props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable :loading="discardCandidatesLoading" placeholder="请选择有可用库存的入库单" @change="selectDiscardInbound"><el-option v-for="candidate in discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}| ${candidate.inboundCode || '—'}| ${candidate.overseasWarehouseName || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint" > 仅显示一件代发中仍有可用箱数的入库单 。 < / small > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" / > < / label >
< label v-if = "props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" placeholder="请选择"><el-option label="ONT8" value="ONT8" /><el-option label="ABE8" value="ABE8" /><el-option label="CLT2 " value="CLT2 " / > < / el-select > < / label >
< label v-if = "props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :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 > < / label >
< label v-else-if = "props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly / > < / label >
< label v-else > < span > < i v-if = "props.service !== 'labeling'">*</i>{{ props.service === 'labeling' ? '统一目的海外仓' : '操作仓' }}</span><el-select v-model="form.w arehouse" clearable placeholder="留空则按货物明细目的仓"><el-option label="美西海外仓-ONT8" value="ONT8" /><el-option label="美东海外仓-ABE8" value="ABE8" /><el-option label="美中海外仓-MDW2 " value="MDW2 " /></el-select><small v-if="props.service === 'labeling'" class="warehouse-hint"> 填写后 , 未单独指定目的仓的货物将使用此仓库 。 < / small > < / label >
< label v-else > < span > < i > * < / i > { { props . service === 'labeling' ? '海外操作仓(中转仓)' : '操作仓' } } < / span > < el-select v-model = "form.warehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaW arehousesError) " clearable placeholder="请选择国外中转仓" @change="props.service === 'labeling' && selectOperationWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse) " : value="String(warehouse.id) " /></el-select><small v-if="props.service === 'labeling'" class="warehouse-hint"> 选择后仅查询当前客户在该中转仓的可用库存 。 < / small > < / label >
< label v-if = "props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的历史入仓单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无已入库可用库存" placeholder="请选择曾经下单且当前仍有库存的入仓单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|入仓码 ${option.inboundCode}|可用 ${option.availablePackages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint" > 仅显示当前客户在所选中转仓已经完成入库且仍有可用库存的历史入仓单 。 < / small > < / label >
< label v-if = "props.service === 'disposal'"><span>操作仓</span><el-input :model-value="discardCandidates.find((item) => item.orderId === selectedDiscardOrderId)?.overseasWarehouseName || form.warehouse" readonly / > < / label >
< label v-if = "props.service === 'relabel' || props.service === 'transfer'"><span><i>*</i>目的 FBA 仓</span><el-inpu t v-model="form.destination" placeholder="请输入仓代码" / > < / label >
< label v-if = "props.service === 'relabel' || props.service === 'transfer'"><span><i>*</i>目的 FBA 仓</span><el-selec t v-model="form.destination" :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 > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择"><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" / > < / el-select > < / label >
< label v-if = "props.service === 'relabel' || props.service === 'disposal'"><span><i>*</i>{{ props.service === 'relabel' ? '退货原因' : '废弃原因' }}</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" / > < / el-select > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>联系人 </span><el-input v-model="form.contact" placeholder="请输入联系人 " / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>联系电话</span><el-input v-model="form.phone" placeholder="请输入联系电话 " / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<em v-if="form.warehouse && !form.contact" class="warehouse-unmaintained">未维护</em> </span><el-input v-model="form.contact" placeholder="选择海外操作仓后自动带出 " / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>海外仓 联系电话<em v-if="form.warehouse && !form.phone" class="warehouse-unmaintained">未维护</em>< /span><el-input v-model="form.phone" placeholder="选择海外操作仓后自动带出 " / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>贴标费用(CNY) </span><el-input-number v-model="form.chargeAmount" :min="0" :precision="2" controls-position="right" / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>贴标费用(CNY) </span><el-input-number v-model="form.chargeAmount" :min="0" :precision="2" controls-position="right" placeholder="请输入贴标费用" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>总体积(CBM) </span><el-input-number v-model="form.volumeCbm" :min="0.0001" :precision="4" controls-position="right" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>总体积(CBM) </span><el-input-number v-model="form.volumeCbm" :min="0.0001" :precision="4" controls-position="right" placeholder="请输入总体积" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>预计到达时间</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到达时间" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" / > < / label >
< label v-if = "props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" / > < / label >
< label v-else-if = "props.service !== 'disposal'"><span>物流跟踪号</span><el-input v-model="form.trackingNo" / > < / label >
< label v-if = "props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" / > < / label >
< label v-else-if = "props.service !== 'disposal'"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号" / > < / label >
< label class = "warehouse-span-2" > < span > 订单备注 < / span > < el-input v-model = "form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" / > < / label >
< / div >
< / div >
< div class = "warehouse-card warehouse-form-section" > < div class = "warehouse-section-heading" > < h3 > 货物明细 < / h3 > < div v-if = "props.service !== 'disposal'"><template v-if="props.service === 'labeling'"><el-button size="small" @click="downloadRelabelTemplate"><Download :size="15" />下载关联模板</el-button><el-button size="small" @click="templateInput?.click()"><Upload :size="15" />上传关联面单</el-button><input ref="templateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate"></template><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []" > 清空 < / el -button > < / div > < / div >
< p v-if = "props.service === 'labeling'" class="warehouse-attachment-tip" > 模板每行代表一箱 ; 上传后直接回显到下方明细 。 明细目的仓优先 , 留空时使用顶部统一目的海外仓 。 < / p >
< div class = "warehouse-table-scroll" > < table class = "warehouse-edit-table" : class = "{ 'warehouse-label-table': props.service === 'labeling' }" > < thead > < tr > < th > 商品 SKU < span v-if = "props.service !== 'labeling'"> *</span></th><th>中文品名 *</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-else>{{ props.service === 'labeling' ? 'FBA 货件编号 *' : 'FNSKU / 箱唛' }}</th><th v-if="props.service === 'labeling'">换标新 SKU *</th><th v-if="props.service !== 'labeling'">{{ props.service === 'disposal' ? '本次废弃箱数' : props.service === 'relabel' ? '退件数量' : '箱数' }} *</th><template v-if="props.service === 'labeling'"><th>我方箱唛</th><th>FBA 箱唛</th><th>目的仓</th><th>备注</th></template><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" placeholder="选填" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-else><el-input v-if="props.service === 'labeling'" v-model="row.shipmentNo" /><el-input v-else v-model="row.shippingMark" /></td><td v-if="props.service === 'labeling'"><el-input v-model="row.newSkuCode" /></td><td v-if="props.service !== 'labeling'"><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="props.service === 'disposal' ? row.availableQuantity : undefined" controls-position="right" /></td><template v-if="props.service === 'labeling'"><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" clearable placeholder="使用顶部统一目的仓"><el-option label="美西海外仓-ONT8" value="ONT8" /><el-option label="美东海外仓-ABE8" value="ABE8" /><el-option label="美中海外仓-MDW2" value="MDW2" /></el-select></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td></template><td v-if="props.service !== 'disposal'"><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="props.service === 'labeling' ? 8 : 5"><div class="warehouse-empty" > {{ props.service = = = ' disposal ' ? ' 请先选择入库单 , 系统将自动带出 SKU 和可用箱数 ' : ' 暂无货物明细 , 请点击 “ 添加行 ” ' }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "warehouse-card warehouse-form-section" >
< template v-if = "props.service === 'labeling'" >
< h3 > 货物明细 < / h3 >
< el-tabs v-model = "labelingMode" class="warehouse-labeling-tabs" >
< el -tab -pane label = "箱唛换标" name = "carton" >
< div class = "warehouse-section-heading warehouse-tab-actions" > < p > 按箱关联我方箱唛与新的 FBA 箱唛 , 提交后按所选库存箱数冻结 。 < / p > < div > < el-button size = "small" @click ="downloadRelabelTemplate" > < Download :size = "15" / > 下载关联模板 < / el-button > < el-button size = "small" :disabled = "!cargoRows.length" @click ="templateInput?.click()" > < Upload :size = "15" / > 上传关联面单 < / el-button > < input ref = "templateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate" > < el -button type = "primary" size = "small" @click ="addRelabelCargoRow" > < Plus :size = "15" / > 手动添加行 < / el-button > < el-button size = "small" @click ="clearRelabelRows" > 清空 < / el -button > < / div > < / div >
< div class = "warehouse-table-scroll" > < table class = "warehouse-edit-table warehouse-label-table" > < thead > < tr > < th > 商品 SKU < / th > < th > 中文品名 * < / th > < th > FBA 货件编号 * < / th > < th > 换标新 SKU * < / 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 v-model="row.newSkuCode" placeholder="请输入换标新SKU" /></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="10"><div class="warehouse-empty" > 请先选择仍有库存的历史入仓单 , 系统将带出可用库存明细 < / div > < / td > < / tr > < / tbody > < / table > < / div >
< / el -tab -pane >
< el-tab-pane label = "SKU 换标" name = "sku" >
< div class = "warehouse-section-heading warehouse-tab-actions" > < p > 指定我方箱唛后 , 可为同一箱唛添加多条 SKU 换标明细 , 数量按件填写 。 < / p > < div > < 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 class = "warehouse-table-scroll" > < table class = "warehouse-edit-table warehouse-sku-label-table" > < thead > < tr > < th > 来源库存箱 * < / th > < th > 我方箱唛 * < / th > < th > 原 SKU * < / th > < th > 换标后 SKU * < / th > < th > 换标数量 ( 件 ) * < / th > < th > 同时更换箱唛 < / th > < th > 新 FBA 箱唛 < / th > < th > FBA 目的仓 < / th > < th > 备注 < / th > < th > 操作 < / th > < / tr > < / thead > < tbody > < tr v-for = "row in skuRelabelRows" :key="row.id"><td><el-select v-model="row.sourceRowId" filterable placeholder="选择来源库存箱" @change="selectSkuSource(row)"><el-option v-for="source in cartonMarkOptions" :key="source.value" :label="source.label" :value="source.value" /></el-select></td><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><el-checkbox v-model="row.changeCarton">换箱唛</el-checkbox></td><td><el-input v-model="row.fbaCartonMark" :disabled="!row.changeCarton" placeholder="组合换标时填写" /></td><td><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="!row.changeCarton || Boolean(fbaWarehousesError)" placeholder="组合换标时选择"><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="10"><div class="warehouse-empty" > 暂无 SKU 换标明细 , 请点击 “ 添加 SKU 明细 ” < / div > < / td > < / tr > < / tbody > < / table > < / div >
< / el -tab -pane >
< / el-tabs >
< / 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 >
< div class = "warehouse-table-scroll" > < table class = "warehouse-edit-table" > < thead > < tr > < th > 商品 SKU * < / th > < th > 中文品名 * < / th > < th v-if = "props.service === 'disposal'">可用箱数</th><th v-else>FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" placeholder="请输入商品 SKU" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" placeholder="请输入中文品名" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-else><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><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="props.service === 'disposal' ? 4 : 5"><div class="warehouse-empty" > {{ props.service = = = ' disposal ' ? ' 请先选择入库单 , 系统将自动带出 SKU 和可用箱数 ' : ' 暂无货物明细 , 请点击 “ 添加行 ” ' }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< / template >
< / div >
< div class = "warehouse-submit-bar" > < el-button @click ="resetForm" > 重置 < / el -button > < el-button type = "primary" native -type = " submit " :loading = "saving" > 提交 { { config . title } } < / el-button > < / div >
< / form >
@@ -260,12 +469,12 @@ async function openDetail(value: Record<string, string>) {
< template v-else >
< div class = "warehouse-breadcrumb" > < button type = "button" @click ="mode = 'list'" > {{ config.title }} < / button > < b > / < / b > < strong > 订单详情 < / strong > < / div >
< div class = "warehouse-page-heading" > < h2 > { { config . title } } 详情 < / h2 > < el-button @click ="mode = 'list'" > < ArrowLeft :size = "16" / > 返回列表 < / el-button > < / div >
< div class = "warehouse-card warehouse-form-section" > < h3 > 订单概览 < / h3 > < div class = "warehouse-detail-grid" > < div v-for = "(column, index) in config.columns.slice(0, - 1)" :key="column" > < span > { { column } } < / span > < strong > { { selectedDetail ? . [ String ( index ) ] || '—' } } < / strong > < / div > < / div > < / div >
< div class = "warehouse-card warehouse-form-section" > < h3 > 订单概览 < / h3 > < div class = "warehouse-detail-grid" > < div v-for = "(column, index) in config.columns.slice(1)" :key="column" > < span > { { column } } < / span > < strong > { { selectedDetail ? . [ String ( index ) ] || '—' } } < / strong > < / div > < / div > < / div >
< div v-if = "props.service === 'labeling'" class="warehouse-card warehouse-form-section" >
< h3 > 箱唛 换标明细< / h3 >
< div class = "warehouse-table-scroll" > < table class = "warehouse-relabel-detail-table" > < thead > < tr > < th > 我方箱唛 < / th > < th > FBA 箱唛 < / th > < th > 商品 SKU < / th > < th > 中文品名 < / th > < th > FBA 货件编号 < / th > < th > 换标新 SKU < / th > < th > 目的仓 < / th > < th > 贴标要求 < / th > < th > 备注 < / th > < / tr > < / thead > < tbody >
< tr v-for = "item in selectedRelabelDetail?.items || []" :key="item.id" > < td > < strong > { { item . cartonMark || '—' } } < / strong > < / td > < td > < strong > { { item . fbaCartonMark || '—' } } < / strong > < / td > < td > { { item . skuCode || '—' } } < / td > < td > { { item . productNameZh || '—' } } < / td > < td > { { item . shipmentNo || '—' } } < / td > < td > { { item . newSkuCode || '—' } } < / td > < td > { { warehouseDisplay ( item . destinationWarehouseId ) } } < / td > < td > { { item . labelRequirement || '—' } } < / td > < td > { { item . remark || '—' } } < / td > < / tr >
< tr v-if = "!selectedRelabelDetail?.items?.length"><td colspan="9 "><div class="warehouse-empty" > 暂无箱唛 换标明细 < / div > < / td > < / tr >
< h3 > 换标任务 明细 < / h3 >
< div class = "warehouse-table-scroll" > < table class = "warehouse-relabel-detail-table" > < thead > < tr > < th > 换标模式 < / th > < th > 我方箱唛 < / th > < th > 新 FBA 箱唛 < / th > < th > FBA 目的仓 < / th > < th > 任务箱数 < / th > < th > 商品 SKU < / th > < th > SKU 换标明细 < / th > < th > FBA 货件编号 < / th > < th > 贴标要求 < / th > < th > 备注 < / th > < / tr > < / thead > < tbody >
< tr v-for = "item in selectedRelabelDetail?.items || []" :key="item.id"><td>{{ item.operationType === 'CARTON_AND_SKU' ? '箱唛 + SKU 换标' : item.operationType === 'SKU_LABEL' ? 'SKU 换标' : '箱唛换标' }}</td><td><strong>{{ item.cartonMark || '—' }}</strong></td><td><strong>{{ item.fbaCartonMark || '—' }}</strong></td><td>{{ warehouseDisplay(item.destinationFbaWarehouseCode) }}</td><td>{{ item.totalPackageCount }} 箱</td><td>{{ item.skuCode || '—' }}</td><td><template v-if="item.skuItems?.length"><div v-for="skuItem in item.skuItems" :key="`${skuItem.skuCode}-${skuItem.newSkuCode}-${skuItem.quantity}`" > {{ skuItem.skuCode }} → {{ skuItem.newSkuCode }} × {{ skuItem.quantity }} 件 < / div > < / template > < span v-else > {{ item.newSkuCode | | ' — ' }} < / span > < / td > < td > { { item . shipmentNo || '—' } } < / td > < td > { { item . labelRequirement || '—' } } < / td > < td > { { item . remark || '—' } } < / td > < / tr >
< tr v-if = "!selectedRelabelDetail?.items?.length"><td colspan="10 "><div class="warehouse-empty" > 暂无换标任务 明细 < / div > < / td > < / tr >
< / tbody > < / table > < / div >
< / div >
< / template >
@@ -274,6 +483,8 @@ async function openDetail(value: Record<string, string>) {
< style scoped >
. warehouse - service - page { color : # 25324 a } . warehouse - breadcrumb { display : flex ; align - items : center ; gap : 9 px ; margin : 0 0 14 px ; color : # 9 aa3b2 ; font - size : 12 px } . warehouse - breadcrumb b { font - weight : 400 } . warehouse - breadcrumb strong { color : # 606 b7c } . warehouse - breadcrumb button { padding : 0 ; border : 0 ; background : transparent ; color : # 4376 b8 ; cursor : pointer } . warehouse - page - heading { display : flex ; align - items : center ; justify - content : space - between ; margin - bottom : 16 px } . warehouse - page - heading h2 { margin : 0 ; color : # 17233 a ; font - size : 22 px ; letter - spacing : 0 } . warehouse - card { margin - bottom : 12 px ; border : 1 px solid # e7ebf1 ; border - radius : 4 px ; background : # fff ; box - shadow : 0 1 px 3 px rgba ( 25 , 42 , 70 , .04 ) } . warehouse - filter - card { padding : 20 px 22 px 16 px } . warehouse - filter - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 180 px , 1 fr ) ) ; gap : 16 px 20 px } . warehouse - filter - grid label , . warehouse - form - grid label { display : flex ; min - width : 0 ; flex - direction : column ; gap : 7 px } . warehouse - filter - grid label > span , . warehouse - form - grid label > span { color : # 596579 ; font - size : 12 px } . warehouse - filter - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; margin - top : 18 px } . warehouse - table - card { overflow : hidden } . warehouse - table - scroll { overflow - x : auto } table { width : 100 % ; min - width : 940 px ; border - collapse : collapse ; font - size : 12 px } th { height : 44 px ; padding : 0 14 px ; border - bottom : 1 px solid # e8edf3 ; background : # f7f9fc ; color : # 4 d5a6e ; font - weight : 650 ; text - align : left ; white - space : nowrap } td { height : 48 px ; padding : 8 px 14 px ; border - bottom : 1 px solid # eef1f5 ; color : # 526176 ; white - space : nowrap } . warehouse - empty { display : grid ; height : 150 px ; color : # a0a9b7 ; place - items : center } . warehouse - link , . warehouse - delete { display : inline - flex ; align - items : center ; gap : 5 px ; padding : 4 px ; border : 0 ; background : transparent ; color : # 2176 d2 ; cursor : pointer } . warehouse - delete { color : # d64545 } . warehouse - pagination { display : flex ; align - items : center ; justify - content : flex - end ; gap : 8 px ; padding : 14 px 18 px ; color : # 778398 ; font - size : 12 px } . warehouse - pagination span { margin - right : 8 px } . warehouse - page - number { width : 28 px ; height : 28 px ; border : 1 px solid # 2176 d2 ; border - radius : 3 px ; background : # 2176 d2 ; color : # fff } . warehouse - form - section { padding : 22 px } . warehouse - form - section h3 { margin : 0 0 18 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; color : # 26364 f ; font - size : 15 px ; letter - spacing : 0 } . warehouse - form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 240 px , 1 fr ) ) ; gap : 18 px 28 px } . warehouse - form - grid i { margin - right : 3 px ; color : # e24b4b ; font - style : normal } . warehouse - span - 2 { grid - column : 1 / - 1 } . warehouse - section - heading { display : flex ; align - items : center ; justify - content : space - between } . warehouse - section - heading h3 { flex : 1 } . warehouse - edit - table td { padding : 9 px 10 px } . warehouse - edit - table : deep ( . el - input - number ) { width : 130 px } . warehouse - hint { margin : 12 px 0 0 ; color : # 8792 a2 ; font - size : 12 px } . warehouse - submit - bar { display : flex ; justify - content : center ; gap : 10 px ; padding : 12 px 0 30 px } . warehouse - detail - grid { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ) ; gap : 24 px } . warehouse - detail - grid div { display : flex ; flex - direction : column ; gap : 7 px } . warehouse - detail - grid span { color : # 8 a95a5 ; font - size : 12 px } . warehouse - detail - grid strong { color : # 334157 ; font - size : 13 px } : deep ( . el - button ) { border - radius : 4 px } : deep ( . el - button span ) { display : inline - flex ; align - items : center ; gap : 6 px } : deep ( . el - select ) , : deep ( . el - date - editor . el - input ) { width : 100 % } @ media ( max - width : 1100 px ) { . warehouse - filter - grid { grid - template - columns : repeat ( 2 , 1 fr ) } . warehouse - detail - grid { grid - template - columns : repeat ( 2 , 1 fr ) } }
. warehouse - hidden - input { display : none } . warehouse - label - table { min - width : 1320 px } . warehouse - attachment - tip { margin : 10 px 0 12 px ; color : # 7 d8797 ; font - size : 12 px } . warehouse - mark - mapping { display : flex ; flex - wrap : wrap ; gap : 8 px ; margin : 4 px 0 8 px ; padding : 12 px ; border : 1 px solid # dce7f5 ; border - radius : 5 px ; background : # f7faff } . warehouse - mark - mapping strong { width : 100 % ; color : # 34445 c } . warehouse - mark - mapping span { padding : 5 px 9 px ; border - radius : 3 px ; background : # eaf2fc ; color : # 3 e6593 ; font - size : 12 px }
. warehouse - hidden - input { display : none } . warehouse - label - table { min - width : 1120 px } . warehouse - attachment - tip { margin : 10 px 0 12 px ; color : # 7 d8797 ; font - size : 12 px } . warehouse - mark - mapping { display : flex ; flex - wrap : wrap ; gap : 8 px ; margin : 4 px 0 8 px ; padding : 12 px ; border : 1 px solid # dce7f5 ; border - radius : 5 px ; background : # f7faff } . warehouse - mark - mapping strong { width : 100 % ; color : # 34445 c } . warehouse - mark - mapping span { padding : 5 px 9 px ; border - radius : 3 px ; background : # eaf2fc ; color : # 3 e6593 ; font - size : 12 px }
. warehouse - unmaintained { margin - left : 8 px ; color : # e6a23c ; font - size : 12 px ; font - style : normal }
. warehouse - labeling - tabs { margin - top : - 8 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 - relabel - detail - table { min - width : 1420 px } . warehouse - relabel - detail - table td strong { color : # 253 f67 }
< / style >