2026-08-21 17:20:51 +08:00
< script setup lang = "ts" >
2026-10-20 11:54:44 +08:00
import { getRelabelQuantities , type RelabelQuantity } from '../api/relabelQuantities'
2026-10-19 10:23:47 +08:00
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
2026-10-20 11:54:44 +08:00
import TransferPaneDivider from '../components/TransferPaneDivider.vue'
2026-10-15 18:25:32 +08:00
import { computed , nextTick , onActivated , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
2026-09-18 16:48:35 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-10-29 16:13:22 +08:00
import { ElForm , ElMessage , ElMessageBox , type FormInstance , type FormRules } from 'element-plus'
2026-10-15 15:45:22 +08:00
import { ArrowLeft , ChevronDown , Download , Plus , Printer , Trash2 , Upload , X } from '@lucide/vue'
2026-09-18 16:48:35 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import type { TableAction } from '@/components/common/tableActions'
2026-09-17 18:25:08 +08:00
import { useAuthStore } from '@/modules/auth/public'
2026-10-16 17:43:42 +08:00
import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting'
2026-10-15 15:45:22 +08:00
import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue'
2026-10-16 18:25:56 +08:00
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
2026-10-13 14:46:59 +08:00
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
2026-10-19 15:59:04 +08:00
import { groupDiscardDocuments } from '../utils/discardDocuments'
2026-08-21 17:20:51 +08:00
import {
2026-09-23 11:25:35 +08:00
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer , resubmitRelabel ,
2026-10-16 16:19:31 +08:00
getTransferCartonLabel , submitTransfer , updateTransfer , increaseTransferForecastCartons , getTransferSourceCartons , type TransferSourceCarton ,
2026-10-16 11:42:59 +08:00
downloadRelabelCompletionPhoto , downloadTransferMeasurementPhoto , downloadDiscardPhoto , type DiscardPhoto , uploadRelabelShippingLabel ,
2026-09-14 16:51:43 +08:00
uploadRelabelFile ,
2026-10-14 17:41:04 +08:00
listDiscards , listDiscardCandidates , listRelabelInventoryCandidates , listRelabelHistoricalOrders , listRelabels , listTransfers , cancelDiscard ,
2026-09-08 20:28:05 +08:00
type RelabelInventoryCandidate ,
type RelabelCreate ,
2026-09-06 18:11:42 +08:00
type RelabelDetail ,
2026-09-17 13:36:20 +08:00
type TransferCreate ,
2026-08-21 17:20:51 +08:00
} from '../api/warehouseServices'
2026-09-10 15:34:03 +08:00
import {
getMerchantWarehouseDetail ,
getMerchantWarehouses ,
type MerchantWarehouse ,
type MerchantWarehouseDetail ,
} from '@/modules/first-leg/api/firstLeg'
2026-09-14 12:01:13 +08:00
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
2026-09-24 10:11:16 +08:00
import { createIdempotencyKey } from '@/utils/idempotencyKey'
2026-10-16 16:52:49 +08:00
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
2026-10-13 13:47:03 +08:00
import { relabelSkuCodes , relabelSkuCount } from '../utils/relabelSkuDisplay'
2026-08-21 17:20:51 +08:00
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
2026-09-17 13:36:20 +08:00
type PageMode = 'list' | 'create' | 'edit' | 'detail'
2026-09-09 13:35:51 +08:00
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
2026-09-10 17:12:52 +08:00
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
2026-10-16 17:43:42 +08:00
interface CargoRow { generatedCartonMarks ?: string []; discardCartonIds ?: string []; id : number ; included ?: boolean ; sku : string ; name : string ; quantity : number ; expectedPackageCount ?: number ; availableQuantity ?: number ; sourceOrderId ?: string ; sourceType ?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY' ; sourceInventoryBalanceId ?: string ; sourceInventoryPositionId ?: string ; sourceInboundOrderId ?: string ; returnCartonId ?: string ; sourceInboundOrderNo ?: string ; sourceInboundCode ?: string ; sourceWarehouseName ?: string ; palletNo ?: string ; warehouseLocationCode ?: string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; customerCartonMarks : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
2026-10-12 21:48:28 +08:00
interface SkuRelabelRow { id : number ; sourceRowId : number | null ; cartonMark : string ; originalSku : string ; newSku : string ; quantity : number ; fbaCartonMark : string ; shipmentNo : string ; destinationWarehouseId : string ; remark : string }
2026-09-10 17:12:52 +08:00
interface ImportedRelabelBinding extends SkuRelabelRow { templateType : RelabelTemplateType ; selected : boolean ; shipmentNo : string }
2026-08-21 17:20:51 +08:00
interface ServiceConfig { title : string ; createTitle : string ; numberLabel : string ; emptyText : string ; columns : string [] }
2026-10-15 18:25:32 +08:00
interface TransferCarton { items ?: Array < { transferItemId : string | number ; skuCode : string ; quantity : number } > ; cartonMark : string ; customerCartonMark ?: string ; cartonIndex : number ; totalCartons : number ; status : string ; transferItemId ?: string | number ; transferPalletId ?: string | number ; length ?: number ; width ?: number ; height ?: number ; weight ?: number ; quantity ?: number ; discrepancyReason ?: string ; countedAt ?: string }
2026-08-21 17:20:51 +08:00
2026-09-18 16:48:35 +08:00
const props = withDefaults ( defineProps < { service : ServiceType ; createPage ?: boolean ; detailPage ?: boolean ; detailNo ?: string } > (), { createPage : false , detailPage : false , detailNo : '' })
2026-09-17 18:25:08 +08:00
const auth = useAuthStore ()
2026-09-18 16:48:35 +08:00
const router = useRouter ()
const route = useRoute ()
2026-10-20 11:54:44 +08:00
const transferPaneShare = ref ( 43 )
2026-10-16 16:28:49 +08:00
// 缓存列表不订阅其他详情页的路由参数,避免离开时清空筛选并重复请求。
const detailNumber = computed (() => props . detailPage ? props . detailNo || String ( route . params . detailNo || '' ) : '' )
2026-09-17 18:25:08 +08:00
const canCreate = computed (() => auth . hasPermission (({
labeling : 'merchant:overseas-warehouse:relabel:create' ,
transfer : 'merchant:overseas-warehouse:transfer:create' ,
disposal : 'merchant:overseas-warehouse:discard:create' ,
relabel : 'merchant:overseas-warehouse:return-order:create' ,
} as Record < ServiceType , string >)[ props . service ]))
const canManageRelabelFiles = computed (() => auth . hasPermission ( 'merchant:overseas-warehouse:relabel:files' ))
const canConfirmRelabel = computed (() => auth . hasPermission ( 'merchant:overseas-warehouse:relabel:confirm' ))
const canConfirmTransfer = computed (() => auth . hasPermission ( 'merchant:overseas-warehouse:transfer:confirm' ))
2026-10-15 15:45:22 +08:00
const canConfirmDiscard = computed (() => auth . hasPermission ( 'merchant:overseas-warehouse:discard:confirm' ))
const completionConfirmOpen = ref ( false )
const completionConfirmNo = ref ( '' )
2026-10-16 11:42:59 +08:00
const completionConfirmKind = ref < 'disposal' | 'labeling' | 'transfer' > ( 'disposal' )
function openCompletionConfirmation ( no : string , kind : 'disposal' | 'labeling' | 'transfer' ) {
2026-10-15 15:45:22 +08:00
if ( ! no ) return
completionConfirmNo . value = no ; completionConfirmKind . value = kind ; completionConfirmOpen . value = true
}
async function refreshAfterCompletion ( no : string ) {
if ( mode . value === 'detail' ) await openDetail ({ '0' : no })
else await load ()
}
2026-09-17 18:25:08 +08:00
const canCancelDiscard = computed (() => auth . hasPermission ( 'merchant:overseas-warehouse:discard:cancel' ))
2026-08-21 17:20:51 +08:00
const configs : Record < ServiceType , ServiceConfig > = {
2026-10-20 11:54:44 +08:00
labeling : { title : '贴标申请' , createTitle : '新增贴标申请' , numberLabel : '贴标申请单号' , emptyText : '暂无贴标申请' , columns : [ '操作' , '贴标申请单号' , '关联业务单号' , '海外操作仓' , 'SKU 数(种)' , '箱数' , '贴标件数' , '换标类型' , '状态' , '创建时间' ] },
2026-10-19 15:59:04 +08:00
transfer : { title : '仓储转运单' , createTitle : '新增仓储转运单' , numberLabel : '转运单号' , emptyText : '暂无仓储转运订单' , columns : [ '操作' , '转运单号' , '预计箱数' , '海外仓' , 'FBA 目的仓' , '入仓方式' , '是否换标' , '换标单号' , '换标状态' , '状态' , '创建时间' ] },
2026-10-20 11:54:44 +08:00
disposal : { title : '货物废弃' , createTitle : '新增货物废弃申请' , numberLabel : '废弃申请单号' , emptyText : '暂无货物废弃申请' , columns : [ '操作' , '废弃申请单号' , '废弃出库单号' , '进仓编号' , '操作仓' , 'SKU 汇总' , '申请箱数' , '预估费用' , '状态' , '废弃完成时间' , '创建时间' ] },
2026-09-28 17:26:37 +08:00
relabel : { title : '退货换标' , createTitle : '新建退货换标单' , numberLabel : '退货换标单号' , emptyText : '暂无退货换标订单' , columns : [ '操作' , '退货换标单号' , '来源 FBA 仓' , '目的 FBA 仓' , 'SKU 汇总' , '退件总数' , '是否换标' , '换标状态' , '尾程状态' , '审核时间' , '测量时间' , '出库时间' , '创建时间' ] },
2026-08-21 17:20:51 +08:00
}
const config = computed (() => configs [ props . service ])
2026-09-18 16:48:35 +08:00
const listQueryModel = computed < QueryModel >({
2026-09-28 17:26:37 +08:00
get : () => ({ ... filters , createdDates : [ filters . start , filters . end ]. filter ( Boolean ), reviewDates : [ filters . reviewStart , filters . reviewEnd ]. filter ( Boolean ), completedDates : [ filters . completedStart , filters . completedEnd ]. filter ( Boolean ), measuredDates : [ filters . measuredStart , filters . measuredEnd ]. filter ( Boolean ), outboundDates : [ filters . outboundStart , filters . outboundEnd ]. filter ( Boolean ) }),
2026-09-18 16:48:35 +08:00
set : ( value ) => Object . assign ( filters , {
number : String ( value . number || '' ), inboundNo : String ( value . inboundNo || '' ), warehouse : String ( value . warehouse || '' ),
destination : String ( value . destination || '' ), status : String ( value . status || '' ),
start : Array . isArray ( value . createdDates ) ? String ( value . createdDates [ 0 ] || '' ) : '' ,
end : Array . isArray ( value . createdDates ) ? String ( value . createdDates [ 1 ] || '' ) : '' ,
2026-09-28 17:26:37 +08:00
reviewStart : Array . isArray ( value . reviewDates ) ? String ( value . reviewDates [ 0 ] || '' ) : '' ,
reviewEnd : Array . isArray ( value . reviewDates ) ? String ( value . reviewDates [ 1 ] || '' ) : '' ,
completedStart : Array . isArray ( value . completedDates ) ? String ( value . completedDates [ 0 ] || '' ) : '' ,
completedEnd : Array . isArray ( value . completedDates ) ? String ( value . completedDates [ 1 ] || '' ) : '' ,
measuredStart : Array . isArray ( value . measuredDates ) ? String ( value . measuredDates [ 0 ] || '' ) : '' ,
measuredEnd : Array . isArray ( value . measuredDates ) ? String ( value . measuredDates [ 1 ] || '' ) : '' ,
outboundStart : Array . isArray ( value . outboundDates ) ? String ( value . outboundDates [ 0 ] || '' ) : '' ,
outboundEnd : Array . isArray ( value . outboundDates ) ? String ( value . outboundDates [ 1 ] || '' ) : '' ,
2026-09-18 16:48:35 +08:00
}),
})
2026-08-21 17:20:51 +08:00
const statusOptions = computed (() => props . service === 'labeling'
2026-09-23 11:25:35 +08:00
? [{ label : '待审核' , value : 'PENDING_REVIEW' }, { label : '待处理' , value : 'PENDING_PROCESSING' }, { label : '处理中' , value : 'PROCESSING' }, { label : '换标完成' , value : 'COMPLETED' }, { label : '已驳回' , value : 'REJECTED' }]
2026-08-21 17:20:51 +08:00
: props . service === 'disposal'
2026-10-15 15:45:22 +08:00
? [{ label : '待审核' , value : 'PENDING_REVIEW' }, { label : '待 PDA 废弃' , value : 'APPROVED' }, { label : '待客户确认' , value : 'PDA_COMPLETED' }, { label : '已完成' , value : 'COMPLETED' }, { label : '已拒绝' , value : 'REJECTED' }, { label : '已取消' , value : 'CANCELLED' }]
2026-09-17 13:36:20 +08:00
: props . service === 'transfer'
? [{ label : '草稿' , value : 'DRAFT' }, { label : '待审核' , value : 'PENDING_REVIEW' },
{ label : '待PDA收货' , value : 'WAIT_PDA_RECEIVING' }, { label : '入库测量中' , value : 'INBOUND_MEASURING' },
{ label : '待客户确认实测' , value : 'WAIT_CUSTOMER_CONFIRMATION' }, { label : '待换标' , value : 'WAIT_RELABEL' },
{ label : '待确认目的地' , value : 'WAIT_FBA_DESTINATION' }, { label : '待排车' , value : 'READY_FOR_DISPATCH' },
{ label : '待出库' , value : 'WAIT_OUTBOUND' }, { label : '已出库' , value : 'OUTBOUNDED' },
{ label : '部分收货' , value : 'PARTIALLY_RECEIVED' }, { label : '已收货' , value : 'RECEIVED' },
{ label : '审核驳回' , value : 'REJECTED' }]
: [{ label : '待审核' , value : 'PENDING_REVIEW' }, { label : '已通过' , value : 'APPROVED' }, { label : '待分配' , value : 'PENDING_ASSIGNMENT' }, { label : '已分配' , value : 'ALLOCATED' }, { label : '已出库' , value : 'OUTBOUNDED' }, { label : '部分收货' , value : 'PARTIALLY_RECEIVED' }, { label : '已收货' , value : 'RECEIVED' }, { label : '已拒绝' , value : 'REJECTED' }])
2026-09-18 16:48:35 +08:00
const mode = ref < PageMode >( props . detailPage ? 'detail' : props . createPage ? 'create' : 'list' )
2026-09-28 17:26:37 +08:00
const filters = reactive ({ number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' , reviewStart : '' , reviewEnd : '' , completedStart : '' , completedEnd : '' , measuredStart : '' , measuredEnd : '' , outboundStart : '' , outboundEnd : '' })
2026-10-29 16:13:22 +08:00
const form = reactive ({ customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , privateDestinationAddress : '' , privateDestinationPostalCode : '' , privateDestinationContact : '' , privateDestinationPhone : '' , address : '' , contact : '' , phone : '' , pickupContact : '' , pickupPhone : '' , relabelContact : '' , relabelPhone : '' , deliveryMethod : props . service === 'transfer' ? 'WAREHOUSE_PICKUP' : '' , relabelDeliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , estimatedShipAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 })
2026-09-17 13:36:20 +08:00
const transferDestinationType = ref < 'NONE' | 'FBA' | 'PRIVATE' > ( 'NONE' )
2026-09-10 15:34:03 +08:00
const selectedOperationWarehouse = ref < MerchantWarehouseDetail | null >( null )
const operationWarehouseDetailLoading = ref ( false )
const operationWarehouseAddress = computed (() => {
const address = selectedOperationWarehouse . value ? . addresses [ 0 ]
if ( ! address ) return ''
return [ address . addressLine1 , address . addressLine2 , address . city , address . stateProvince , address . postalCode , address . countryCode ]
. filter ( Boolean ). join ( ', ' )
})
2026-08-21 17:20:51 +08:00
const cargoRows = ref < CargoRow [] >([])
2026-09-13 21:10:14 +08:00
const transferRelabelRows = ref < CargoRow [] >([])
2026-09-17 16:34:51 +08:00
const transferRelabelDefaultDestination = ref ( '' )
2026-09-08 20:28:05 +08:00
const labelingMode = ref < LabelingMode >( 'carton' )
2026-09-10 15:34:03 +08:00
const shippingLabelFile = ref < File | null >( null )
2026-09-08 20:28:05 +08:00
const skuRelabelRows = ref < SkuRelabelRow [] >([])
2026-09-10 17:12:52 +08:00
const importedRelabelBindings = ref < ImportedRelabelBinding [] >([])
const cartonDetailRows = computed (() => cargoRows . value . filter (( row ) => row . included ))
2026-08-21 17:20:51 +08:00
const submittedRows = ref < Record < string , string >[] > ([])
2026-09-15 15:20:35 +08:00
const page = ref ( 1 )
const pageSize = ref ( 15 )
const total = ref ( 0 )
2026-09-17 13:36:20 +08:00
const editingTransferNo = ref ( '' )
interface TransferPallet { id ?: string | number ; sequenceNo ?: number ; palletNo ?: string ; warehouseLocationCode ?: string ; cartonCount ?: number ; lengthCm ?: number ; widthCm ?: number ; heightCm ?: number ; grossWeightKg ?: number ; volumeCbm ?: number ; status ?: string ; completedAt ?: string }
interface TransferMeasurementPhoto { fileId : string | number ; fileName ?: string ; contentType ?: string ; sizeBytes ?: number ; uploadedAt ?: string }
interface ServiceDetail extends Record < string , unknown > {
2026-10-16 18:25:56 +08:00
reviewRemark ?: string
2026-10-14 11:43:38 +08:00
discardNo ?: string ; photos ?: DiscardPhoto []
2026-09-17 13:36:20 +08:00
transferNo ?: string ; status ?: string ; operatingWarehouseId ?: string ; fbaWarehouseCode ?: string
2026-09-23 11:25:35 +08:00
destinationType ?: 'NONE' | 'FBA' | 'PRIVATE' ; destinationWarehouseCode ?: string ; destinationWarehouseName ?: string
2026-09-17 13:36:20 +08:00
privateDestinationAddress ?: string ; privateDestinationPostalCode ?: string ; privateDestinationContactName ?: string ; privateDestinationContactPhone ?: string
deliveryAddress ?: string ; pickupContactName ?: string ; pickupContactPhone ?: string
packageCount ?: number ; volumeCbm ?: number ; deliveryMethod ?: string ; estimatedArrivalAt ?: string
estimatedShipAt ?: string ; actualShipAt ?: string ; fbaShipmentNo ?: string ; trackingNo ?: string
remark ?: string ; needRelabel ?: boolean ; labelDeadline ?: string ; relabelContactName ?: string
2026-09-23 11:25:35 +08:00
relabelContactPhone ?: string ; relabelDeliveryMethod ?: string ; relabelNo ?: string
relabelStatus ?: string ; relabelFulfillmentStatus ?: string ; createdAt ?: string
2026-09-28 17:26:37 +08:00
inboundCode ?: string ; sourceOrders ?: Array < { lastlegOrderId ?: string | number } >
2026-10-29 15:49:59 +08:00
items ?: Array < { id ?: string | number ; actualCartonMarks ?: string []; palletNo ?: string ; cartonMarks ?: string []; inboundCode ?: string ; productName ?: string ; discardPackages ?: number ; quantityUnit ?: string ; actualDiscardPackages ?: number | null ; skuCode ?: string ; productNameZh ?: string ; quantity ?: number | null ; packageCount ?: number ; shippingMark ?: string } >
2026-09-17 13:36:20 +08:00
relabelBindings ?: Array < { operationType ?: string ; customerCartonMark ?: string ; originalSku ?: string ; newSku ?: string ; quantity ?: number ; fbaCartonMark ?: string ; destinationWarehouseCode ?: string ; remark ?: string } >
cartons ?: TransferCarton []; pallets ?: TransferPallet []; measurementPhotos ?: TransferMeasurementPhoto []; customerConfirmedAt ?: string
}
2026-09-14 11:05:47 +08:00
const selectedDetail = ref < ServiceDetail | null >( null )
2026-10-16 16:52:49 +08:00
/** 仅汇总客户原始预报件数,旧单缺失时明确提示,不用实收件数冒充。 */
const transferForecastQuantity = computed (() => {
const items = selectedDetail . value ? . items || []
if ( ! items . length || items . some ( item => item . quantity == null )) return '未记录'
return items . reduce (( sum , item ) => sum + Number ( item . quantity ), 0 ) + ' 件'
})
2026-10-16 16:19:31 +08:00
// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。
const showForecastCartons = ref ( false )
const increaseForecastOpen = ref ( false )
const increasingForecast = ref ( false )
const forecastSnapshot = ref < ServiceDetail | null >( null )
2026-10-16 16:52:49 +08:00
const increaseForecastForm = reactive ({ additionalCount : 1 })
2026-10-16 16:19:31 +08:00
const canIncreaseForecast = computed (() => canCreate . value && selectedDetail . value ? . transferNo === selectedDetail . value ? . inboundCode && [ 'PENDING_REVIEW' , 'WAIT_PDA_RECEIVING' ]. includes ( selectedDetail . value ? . status || '' ))
/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */
function openIncreaseForecast () {
if ( ! canIncreaseForecast . value || ! selectedDetail . value ) return
forecastSnapshot . value = selectedDetail . value
increaseForecastForm . additionalCount = 1 ; increaseForecastOpen . value = true
}
/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */
async function saveIncreaseForecast () {
if ( increasingForecast . value || ! forecastSnapshot . value ? . transferNo ) return
const snapshot = forecastSnapshot . value
2026-10-16 16:52:49 +08:00
if ( ! Number . isSafeInteger ( increaseForecastForm . additionalCount ) || increaseForecastForm . additionalCount < 1 ) { ElMessage . warning ( '请填写有效新增箱数' ); return }
2026-10-16 16:19:31 +08:00
increasingForecast . value = true
try {
2026-10-16 16:52:49 +08:00
const result = await increaseTransferForecastCartons ( String ( snapshot . transferNo ), { expectedPackageCount : Number ( snapshot . packageCount ), packageCount : Number ( snapshot . packageCount ) + increaseForecastForm . additionalCount })
2026-10-16 16:19:31 +08:00
selectedDetail . value = result as ServiceDetail ; increaseForecastOpen . value = false
ElMessage . success ( '预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点' )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '增加预报箱数失败' ) }
finally { increasingForecast . value = false }
}
2026-10-15 15:45:22 +08:00
const showTransferCartons = ref ( false )
function transferCartonItem ( carton : TransferCarton ) {
return selectedDetail . value ? . items ? . find (( item ) => carton . transferItemId != null && String ( item . id ) === String ( carton . transferItemId ))
}
2026-10-15 18:25:32 +08:00
function transferCartonLines ( carton : TransferCarton ) {
if ( carton . items ? . length ) return carton . items . map ( line => ({ ... line , productNameZh : selectedDetail . value ? . items ? . find ( item => String ( item . id ) === String ( line . transferItemId )) ? . productNameZh || '—' }))
const item = transferCartonItem ( carton )
return [{ transferItemId : carton . transferItemId , skuCode : item ? . skuCode || '—' , productNameZh : item ? . productNameZh || '—' , quantity : Number ( carton . quantity || 0 ) }]
}
2026-10-15 15:45:22 +08:00
function palletCartons ( pallet : TransferPallet ) {
2026-10-20 11:54:44 +08:00
return countedTransferCartons (). filter (( carton ) => pallet . id != null && carton . transferPalletId != null && String ( carton . transferPalletId ) === String ( pallet . id ))
2026-10-15 15:45:22 +08:00
}
function palletSkuRows ( pallet : TransferPallet ) {
const summary = new Map < string , number >()
for ( const carton of palletCartons ( pallet )) {
2026-10-15 18:25:32 +08:00
for ( const line of transferCartonLines ( carton )) summary . set ( line . skuCode , ( summary . get ( line . skuCode ) || 0 ) + Number ( line . quantity ))
2026-10-15 15:45:22 +08:00
}
return [... summary . entries ()]. map (([ sku , quantity ]) => ({ sku , quantity }))
}
function palletQuantity ( pallet : TransferPallet ) { return palletCartons ( pallet ). reduce (( sum , carton ) => sum + Number ( carton . quantity || 0 ), 0 ) }
2026-10-20 11:54:44 +08:00
/** 保留历史实收,后续占用、废弃与出库不回退签收事实。 */
function countedTransferCartons () { return ( selectedDetail . value ? . cartons || []). filter (( carton ) => [ 'COUNTED' , 'DISCARD_PENDING' , 'DISCARDED' , 'SHIPPED' ]. includes ( carton . status )) }
2026-10-15 15:45:22 +08:00
function receivedItemCartons ( itemId : string | number | undefined ) {
2026-10-15 18:25:32 +08:00
return countedTransferCartons (). filter ( carton => itemId != null && transferCartonLines ( carton ). some ( line => String ( line . transferItemId ) === String ( itemId )))
2026-10-15 15:45:22 +08:00
}
function receivedItemQuantity ( itemId : string | number | undefined ) {
2026-10-15 18:25:32 +08:00
return receivedItemCartons ( itemId ). reduce (( sum , carton ) => sum + transferCartonLines ( carton ). filter ( line => String ( line . transferItemId ) === String ( itemId )). reduce (( total , line ) => total + Number ( line . quantity ), 0 ), 0 )
2026-10-15 15:45:22 +08:00
}
2026-09-18 16:48:35 +08:00
const detailError = ref ( '' )
2026-09-06 18:11:42 +08:00
const selectedRelabelDetail = ref < RelabelDetail | null >( null )
2026-10-20 11:54:44 +08:00
const selectedRelabelQuantity = ref < RelabelQuantity | null >( null )
2026-09-15 15:20:35 +08:00
const completionPhotoUrls = reactive < Record < string , string > > ({})
const completionPhotoLoading = reactive < Record < string , boolean > > ({})
const completionPhotoErrors = reactive < Record < string , boolean > > ({})
const completionPhotoPreviewUrls = computed (() => ( selectedRelabelDetail . value ? . completionEvidence ? . photoFileIds || [])
. map (( fileId ) => completionPhotoUrls [ fileId ]). filter ( Boolean ))
2026-09-17 13:36:20 +08:00
const measurementPhotoUrls = reactive < Record < string , string > > ({})
const measurementPhotoLoading = reactive < Record < string , boolean > > ({})
const measurementPhotoErrors = reactive < Record < string , boolean > > ({})
const measurementPhotoPreviewUrls = computed (() => ( selectedDetail . value ? . measurementPhotos || [])
. map (( photo ) => measurementPhotoUrls [ photo . fileId ]). filter ( Boolean ))
2026-10-14 11:43:38 +08:00
const discardPhotoUrls = reactive < Record < string , string > > ({})
const discardPhotoLoading = reactive < Record < string , boolean > > ({})
const discardPhotoErrors = reactive < Record < string , boolean > > ({})
const discardPhotoPreviewUrls = computed (() => ( selectedDetail . value ? . photos || []). map ( photo => discardPhotoUrls [ photo . fileId ]). filter ( Boolean ))
let discardPhotoGeneration = 0
function clearDiscardPhotos () { discardPhotoGeneration ++ ; Object . values ( discardPhotoUrls ). forEach ( URL . revokeObjectURL ); for ( const map of [ discardPhotoUrls , discardPhotoLoading , discardPhotoErrors ]) Object . keys ( map ). forEach ( key => delete map [ key ]) }
async function loadDiscardPhoto ( fileId : string ) {
const current = selectedDetail . value ; const generation = discardPhotoGeneration
if ( ! current ? . discardNo || discardPhotoLoading [ fileId ]) return
discardPhotoLoading [ fileId ] = true ; discardPhotoErrors [ fileId ] = false
try { const blob = await downloadDiscardPhoto ( current . discardNo , fileId ); if ( generation !== discardPhotoGeneration || selectedDetail . value !== current || mode . value !== 'detail' ) return ; if ( discardPhotoUrls [ fileId ]) URL . revokeObjectURL ( discardPhotoUrls [ fileId ]); discardPhotoUrls [ fileId ] = URL . createObjectURL ( blob ) }
catch { if ( generation === discardPhotoGeneration ) discardPhotoErrors [ fileId ] = true }
finally { if ( generation === discardPhotoGeneration ) discardPhotoLoading [ fileId ] = false }
}
async function loadDiscardPhotos () { clearDiscardPhotos (); await Promise . all (( selectedDetail . value ? . photos || []). map ( photo => loadDiscardPhoto ( photo . fileId ))) }
2026-09-08 20:28:05 +08:00
const fbaWarehouses = ref < MerchantWarehouse [] >([])
const operationWarehouses = ref < MerchantWarehouse [] >([])
const fbaWarehousesLoading = ref ( false )
const fbaWarehousesError = ref ( '' )
2026-09-13 21:10:14 +08:00
const operationWarehousesError = ref ( '' )
2026-09-18 16:48:35 +08:00
const listQueryFields = computed < QueryField [] >(() => {
const fields : QueryField [] = [
{ key : 'number' , label : config . value . numberLabel , type : 'input' , placeholder : `请输入 ${ config . value . numberLabel } ` },
]
if ( props . service !== 'labeling' && props . service !== 'relabel' ) fields . push ({ key : 'inboundNo' , label : '进仓编号' , type : 'input' , placeholder : '请输入进仓编号' })
fields . push ({
key : 'warehouse' , label : props . service === 'relabel' ? '来源 FBA 仓' : '操作仓' , type : 'select' ,
options : ( props . service === 'relabel' ? fbaWarehouses . value : operationWarehouses . value ). map (( warehouse ) => ({ label : warehouseLabel ( warehouse ), value : String ( warehouse . id ) })),
})
if ( props . service === 'transfer' || props . service === 'relabel' ) fields . push ({ key : 'destination' , label : '目的 FBA 仓' , type : 'input' , placeholder : '请输入仓代码' })
2026-09-28 17:26:37 +08:00
fields . push ({ key : 'status' , label : '订单状态' , type : 'select' , options : statusOptions . value })
fields . push ({ key : 'createdDates' , label : '创建时间' , type : 'daterange' })
if ( props . service === 'relabel' ) fields . push (
{ key : 'reviewDates' , label : '审核时间' , type : 'daterange' },
{ key : 'measuredDates' , label : '测量时间' , type : 'daterange' },
{ key : 'outboundDates' , label : '出库时间' , type : 'daterange' },
)
2026-09-18 16:48:35 +08:00
return fields
})
const listColumns = computed < DataTableColumn [] >(() => config . value . columns . map (( label , index ) => index === 0
? { key : 'actions' , label , slotName : 'actions' , width : props . service === 'transfer' ? 210 : 160 , fixed : 'left' }
2026-10-13 18:06:28 +08:00
: { key : String ( index - 1 ), prop : String ( index - 1 ), label , minWidth : label === '关联业务单号' ? 300 : index === 1 || label === '换标单号' ? 190 : 130 ,
singleLine : label !== '关联业务单号' , className : label === '关联业务单号' ? 'related-business-number-cell' : undefined ,
2026-10-20 15:07:39 +08:00
slotName : props . service === 'labeling' && [ 'SKU 数(种)' , '箱数' , '贴标件数' ]. includes ( label ) ? 'labelingQuantity' : props . service === 'labeling' && label === '换标类型' ? 'labelingType' : props . service === 'labeling' && label === '状态' ? 'labelingStatus' : props . service === 'transfer' && label === '换标状态' ? 'transferRelabelStatus' : props . service === 'transfer' && label === '状态' ? 'transferStatus' : undefined }))
2026-10-29 15:49:59 +08:00
const discardRequestedPackages = ref < number | undefined >()
const discardQuantityMode = computed (() => Boolean ( discardSelectedCandidate . value ? . sources . length && discardSelectedCandidate . value . sources . every ( source => source . sourceType === 'INVENTORY' && ! source . cartonSelectionSupported && Boolean ( source . palletNo ))))
2026-10-14 17:41:04 +08:00
const discardSelectedBoxKeys = ref < string [] >([])
const discardBoxSearch = ref ( '' )
2026-08-21 17:20:51 +08:00
const discardCandidates = ref < import (' .. / api / warehouseServices ') .DiscardCandidate [] >([])
2026-10-19 15:59:04 +08:00
const discardDocuments = computed (() => groupDiscardDocuments ( discardCandidates . value ))
const discardSelectedCandidate = computed (() => discardDocuments . value . find ( item => item . orderId === selectedDiscardOrderId . value ))
const discardSelectionSources = computed (() => discardSelectedCandidate . value ? . sources || [])
2026-10-20 14:00:59 +08:00
const discardSourceBoxes = computed (() => discardSelectionSources . value . flatMap ( source => ( source . cartons || []). map ( box => ({ ... box , key : ` ${ source . sourceType } : ${ box . cartonId } ` , source }))))
const discardSelectableBoxes = computed (() => [... new Map ( discardSourceBoxes . value . map ( box => [ box . key , box ])). values ()])
2026-10-14 17:41:04 +08:00
const discardVisibleBoxes = computed (() => discardSelectableBoxes . value . filter ( box => ! discardBoxSearch . value . trim () || box . cartonMark . includes ( discardBoxSearch . value . trim ()) || box . skus . some ( sku => sku . skuCode . includes ( discardBoxSearch . value . trim ()))))
function toggleDiscardBox ( key : string , selected : boolean ) {
discardSelectedBoxKeys . value = selected ? [... new Set ([... discardSelectedBoxKeys . value , key ])] : discardSelectedBoxKeys . value . filter ( value => value !== key )
syncDiscardSelection ()
}
function selectAllDiscardBoxes ( selected : boolean ) {
discardSelectedBoxKeys . value = selected ? discardSelectableBoxes . value . map ( box => box . key ) : []
syncDiscardSelection ()
}
function syncDiscardSelection () {
2026-10-20 14:00:59 +08:00
const selected = discardSourceBoxes . value . filter ( box => discardSelectedBoxKeys . value . includes ( box . key ))
2026-10-14 17:41:04 +08:00
const groups = new Map < string , CargoRow >()
for ( const box of selected ) {
const source = box . source
2026-10-20 14:00:59 +08:00
const skus = source . sourceType === 'RETURN_CARTON' ? source . skus : source . sourceType === 'INVENTORY' ? source . skus . filter ( sku => box . skus . some ( content => content . skuCode === sku . skuCode )) : box . skus . slice ( 0 , 1 )
2026-10-14 17:41:04 +08:00
for ( const sku of skus ) {
2026-10-20 14:00:59 +08:00
const key = ` ${ source . sourceType } : ${ source . inventoryPositionId || source . returnCartonId || source . orderId } : ${ sku . skuCode } `
2026-10-14 17:41:04 +08:00
let row = groups . get ( key )
if ( ! row ) {
2026-10-20 14:00:59 +08:00
row = { ... createCargoRow (), sku : sku . skuCode , name : sku . productName , quantity : 0 , availableQuantity : sku . availablePackages ,
2026-10-14 17:41:04 +08:00
sourceOrderId : source . orderId , sourceType : source . sourceType , sourceInventoryPositionId : source . inventoryPositionId , returnCartonId : source . returnCartonId ,
palletNo : source . palletNo , discardCartonIds : source . sourceType === 'RETURN_CARTON' ? undefined : [] }
groups . set ( key , row )
}
row . quantity += source . sourceType === 'RETURN_CARTON' ? sku . availablePackages : 1
row . discardCartonIds ? . push ( box . cartonId )
}
}
cargoRows . value = [... groups . values ()]
}
2026-08-21 17:20:51 +08:00
const selectedDiscardOrderId = ref ( '' )
const discardCandidatesLoading = ref ( false )
2026-09-08 20:28:05 +08:00
const relabelCandidates = ref < RelabelInventoryCandidate [] >([])
const selectedRelabelInboundOrderId = ref ( '' )
2026-10-29 16:13:22 +08:00
const isLabelingCreate = computed (() => props . service === 'labeling' && mode . value === 'create' )
const isTransferCreate = computed (() => props . service === 'transfer' && mode . value === 'create' )
const isDisposalCreate = computed (() => props . service === 'disposal' && mode . value === 'create' )
const isServiceCreate = computed (() => isLabelingCreate . value || isTransferCreate . value || isDisposalCreate . value )
const disposalFormModel = computed (() => ({ ... form , sourceOrder : selectedDiscardOrderId . value , requestedPackages : discardRequestedPackages . value }))
const disposalFormRules : FormRules = {
warehouse : [{ required : true , message : '请选择操作仓' , trigger : 'change' }],
sourceOrder : [{ required : true , message : '请选择入库单' , trigger : 'change' }],
reason : [{ required : true , whitespace : true , message : '请选择废弃原因' , trigger : 'change' }],
}
const labelingFormRef = ref < FormInstance >()
const labelingFormModel = computed (() => ({ ... form , sourceOrder : selectedRelabelInboundOrderId . value , shippingLabelFile : shippingLabelFile . value }))
const labelingFormRules = computed < FormRules >(() => ({
warehouse : [{ required : true , message : '请选择操作仓' , trigger : 'change' }],
sourceOrder : [{ required : true , message : '请选择入仓单或退货单' , trigger : 'change' }],
contact : [{ required : true , whitespace : true , message : '请输入签收联系人' , trigger : 'blur' }],
phone : [{ required : true , whitespace : true , message : '请输入联系电话' , trigger : 'blur' }],
labelDeadline : [{ required : true , message : '请选择贴标截止时间' , trigger : 'change' }],
destination : labelingMode . value === 'sku' ? [] : [{ required : true , message : '请选择 FBA 目的仓' , trigger : 'change' }],
deliveryMethod : labelingMode . value === 'sku' ? [] : [{ required : true , message : '请选择派送方式' , trigger : 'change' }],
shippingLabelFile : labelingMode . value !== 'sku' && form . deliveryMethod === 'EXPRESS' && form . relabel
? [{ required : true , type : 'object' , message : '请上传面单文件' , trigger : 'change' }] : [],
}))
2026-10-09 11:48:52 +08:00
const orderInfoCollapsed = ref ( false )
2026-10-29 16:13:22 +08:00
watch ([ labelingMode , () => form . deliveryMethod , () => form . relabel ], () => {
if ( isLabelingCreate . value ) labelingFormRef . value ? . clearValidate ([ 'destination' , 'deliveryMethod' , 'shippingLabelFile' ])
})
2026-10-09 15:25:27 +08:00
const availableBoxesCollapsed = ref ( false )
const availableSkusCollapsed = ref ( false )
2026-09-08 20:28:05 +08:00
const relabelCandidatesLoading = ref ( false )
2026-09-23 09:06:20 +08:00
const transferInventoryCandidates = ref < RelabelInventoryCandidate [] >([])
2026-10-19 14:53:57 +08:00
const transferAvailableSourceCounts = ref < Record < string , number > > ({})
2026-09-23 09:06:20 +08:00
const selectedTransferInventory = ref ( '' )
2026-10-16 11:42:59 +08:00
const inventoryTransfer = computed (() => props . service === 'transfer' && Boolean ( selectedTransferInventory . value ))
2026-10-19 14:30:52 +08:00
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
const transferRelabelRequired = computed (() => inventoryTransfer . value && transferDestinationType . value === 'FBA' )
watch ( transferRelabelRequired , ( required ) => { if ( required ) form . relabel = true }, { flush : 'sync' })
2026-10-16 11:42:59 +08:00
const transferSourceCartons = ref < TransferSourceCarton [] >([])
const transferSourceLoading = ref ( false )
const transferSourceError = ref ( '' )
let transferSourceRequest = 0
2026-10-29 16:13:22 +08:00
const transferFormModel = computed (() => ({ ... form , relabelDestination : transferRelabelDefaultDestination . value }))
const transferFormRules = computed < FormRules >(() => {
const required = ( message : string , enabled = true , trigger = 'change' ) => enabled ? [{ required : true , whitespace : true , message , trigger }] : []
const incoming = ! inventoryTransfer . value
const pickup = incoming && form . deliveryMethod === 'WAREHOUSE_PICKUP'
const privateDestination = transferDestinationType . value === 'PRIVATE'
return {
warehouse : required ( '请选择入库海外仓' ),
deliveryMethod : required ( '请选择入仓方式' , incoming ),
address : required ( form . deliveryMethod === 'WAREHOUSE_PICKUP' ? '请输入提货地址' : '请输入送货地址' , incoming , 'blur' ),
pickupContact : required ( '请输入提货联系人' , pickup , 'blur' ),
pickupPhone : required ( '请输入联系电话' , pickup , 'blur' ),
estimatedArrivalAt : required ( '请选择预计入仓时间' , incoming ),
destination : required ( '请选择 FBA 目的仓' , transferDestinationType . value === 'FBA' ),
privateDestinationAddress : required ( '请输入私人仓地址' , privateDestination , 'blur' ),
privateDestinationPostalCode : required ( '请输入邮编' , privateDestination , 'blur' ),
privateDestinationContact : required ( '请输入联系人' , privateDestination , 'blur' ),
privateDestinationPhone : required ( '请输入联系电话' , privateDestination , 'blur' ),
labelDeadline : required ( '请选择换标截止时间' , form . relabel ),
relabelContact : required ( '请输入换标联系人' , form . relabel , 'blur' ),
relabelPhone : required ( '请输入联系电话' , form . relabel , 'blur' ),
relabelDeliveryMethod : required ( '请选择派送方式' , form . relabel && labelingMode . value !== 'sku' ),
relabelDestination : required ( '请选择 FBA 目的仓' , form . relabel ),
}
})
watch ([ inventoryTransfer , transferDestinationType , labelingMode , () => form . deliveryMethod , () => form . relabel ], () => {
if ( isTransferCreate . value ) labelingFormRef . value ? . clearValidate ()
})
2026-10-16 11:42:59 +08:00
async function loadTransferSourceCartons () {
const request = ++ transferSourceRequest
transferSourceCartons . value = []; transferSourceError . value = ''
if ( ! inventoryTransfer . value ) return
transferSourceLoading . value = true
try {
2026-10-16 14:22:00 +08:00
// 退货候选本身就是逐箱库存,使用与下拉一致的实物箱数据,不能按普通入仓快照回查。
if ( selectedTransferInventory . value . startsWith ( 'RETURN_ORDER:' )) {
const returnNo = selectedTransferInventory . value . slice ( 'RETURN_ORDER:' . length )
transferSourceCartons . value = transferInventoryCandidates . value
2026-10-19 14:30:52 +08:00
. filter ( item => ! [ 'COMPLETED' , 'PARTIALLY_COMPLETED' ]. includes ( item . relabelStatus || '' )
&& Number ( item . relabelledQuantity || item . relabelledPackages || 0 ) === 0 )
2026-10-16 14:22:00 +08:00
. filter ( item => ( item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL' )
&& String ( item . inboundOrderNo || '' ). trim () === returnNo && Number ( item . cartonAvailablePackages || 0 ) > 0 )
. filter ( item => Boolean ( item . cartonMark ))
. map ( item => {
const contents = item . cartonSkuItems ? . length ? item . cartonSkuItems : item . skuItems || []
return { cartonMark : item . cartonMark ! , productNameZh : item . productNameZh ,
quantity : contents . reduce (( total , sku ) => total + Number ( sku . quantity || 0 ), 0 ),
destinationWarehouseCode : item . destinationFbaWarehouseCode ,
destinationWarehouseName : item . destinationFbaWarehouseName ,
items : contents . map ( sku => ({ skuCode : sku . skuCode , quantity : Number ( sku . quantity || 0 ) })) }
})
if ( ! transferSourceCartons . value . length ) throw new Error ( '当前退货单没有可转运箱,请刷新库存后重试' )
return
}
2026-10-16 11:42:59 +08:00
const cartons = await getTransferSourceCartons ( form . warehouse , selectedTransferInventory . value )
if ( request === transferSourceRequest ) transferSourceCartons . value = cartons
} catch ( error ) { if ( request === transferSourceRequest ) transferSourceError . value = error instanceof Error ? error . message : '原单箱明细加载失败' }
finally { if ( request === transferSourceRequest ) transferSourceLoading . value = false }
}
2026-09-28 17:26:37 +08:00
// Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings
// all the way to JSON so the backend receives the exact source order ID.
const selectedTransferSourceOrderIds = ref < string [] >([])
2026-09-20 16:55:27 +08:00
const isReturnRelabelSource = computed (() => props . service === 'labeling' && selectedRelabelInboundOrderId . value . startsWith ( 'RETURN_ORDER:' ))
const selectedReturnOrderNo = computed (() => isReturnRelabelSource . value ? selectedRelabelInboundOrderId . value . slice ( 'RETURN_ORDER:' . length ) : '' )
2026-10-13 14:46:59 +08:00
const currentRelabelCandidates = computed (() => relabelCandidates . value . map ( item => returnRelabelCandidate ( item , labelingMode . value )))
2026-10-14 11:43:38 +08:00
const returnCartonQuery = ref ( String ( route . query . cartonMark || '' ))
const selectedReturnCandidates = computed (() => currentRelabelCandidates . value . filter ( item => Boolean ( item . returnCartonId )
&& returnSourceNos ( item ). includes ( selectedReturnOrderNo . value )
&& ( ! returnCartonQuery . value . trim () || ( item . cartonMark || '' ). toUpperCase (). includes ( returnCartonQuery . value . trim (). toUpperCase ()))))
2026-10-12 15:56:07 +08:00
const availableReturnCandidates = computed (() => selectedReturnCandidates . value . filter ( item => inventoryQuantity ( item . availablePackages ) > 0 ). map ( item => ({ ... item , skuItems : ( item . skuItems || []). filter ( sku => inventoryQuantity ( sku . quantity ) > 0 ) })))
const returnBoxCandidates = computed (() => availableReturnCandidates . value . filter ( item => item . sourceType === 'RETURN_CARTON' ))
const returnLooseCandidates = computed (() => availableReturnCandidates . value . filter ( item => item . sourceType === 'RETURN_PARCEL' ))
2026-10-09 11:48:52 +08:00
const requiresBothTotalFiles = computed (() => ( props . service === 'labeling' || ( props . service === 'transfer' && form . relabel )) && labelingMode . value === 'carton_and_sku' )
2026-09-20 16:55:27 +08:00
function inventoryQuantity ( value : number | string | null | undefined ) {
const quantity = Number ( value )
return Number . isFinite ( quantity ) && quantity > 0 ? quantity : 0
}
2026-10-12 15:56:07 +08:00
const relabelInventorySearch = ref ( '' )
2026-10-12 13:43:29 +08:00
function returnSourceNos ( candidate : RelabelInventoryCandidate ) : string [] {
return [... new Set (( candidate . inboundOrderNo || '' ). split ( ',' ). map ( value => value . trim ()). filter ( Boolean ))]
}
function returnRelabelProgress ( candidate : RelabelInventoryCandidate ) {
const labels : Record < string , string > = { UNLABELLED : '未换标' , BOUND : '处理中' , PROCESSING : '处理中' , PARTIALLY_COMPLETED : '部分完成' , COMPLETED : '全部完成' }
return [ labels [ candidate . relabelStatus || '' ] || '—' , '实际完成 ' + ( candidate . relabelledQuantity ?? '—' ) + ' 件' , '处理中占用 ' + ( candidate . relabelReservedQuantity ?? '—' ) + ' 件' ]. join ( '| ' )
}
2026-10-12 15:56:07 +08:00
function returnInventoryState ( candidate : RelabelInventoryCandidate ) {
const labels : Record < string , string > = { AVAILABLE : '可选库存' , RELABEL_PENDING : '换标占用' , PACKING_RESERVED : '合箱计划预留' , OUTBOUND_PENDING : '出库占用' , DISCARD_PENDING : '废弃申请占用' , OUTBOUNDED : '已出库' , MERGED : '已合箱' }
2026-10-14 11:43:38 +08:00
return [...( returnSourceNos ( candidate ). length > 1 ? [ '跨来源单合箱,数量为整箱内容' ] : []), ...( candidate . mergedIntoCartonMarks ? . length ? [ '历史部分转入 ' + candidate . mergedIntoCartonMarks . join ( '、' ) + '(不代表当前库存位置)' ] : []), labels [ candidate . inventoryStatus || '' ] || ( inventoryQuantity ( candidate . availablePackages ) > 0 ? '可选库存' : '当前不可选' ), candidate . occupiedRelabelNo ]. filter ( Boolean ). join ( ' · ' )
2026-10-12 15:56:07 +08:00
}
2026-09-08 20:28:05 +08:00
const relabelInboundOptions = computed (() => {
2026-10-12 15:56:07 +08:00
const grouped = new Map < string , { value : string ; inboundOrderNo : string ; inboundCode : string ; availablePackages : number ; quantityUnit : ' 箱 ' | ' 件 '; stockQuantity : number | null ; units : number ; hasOutbound : boolean ; reasons : string [] }>()
2026-10-13 14:46:59 +08:00
for ( const candidate of currentRelabelCandidates . value ) {
2026-10-12 13:43:29 +08:00
const sources = candidate . returnCartonId ? returnSourceNos ( candidate ) : [ String ( candidate . inboundOrderId )]
for ( const sourceNo of sources ) {
const value = candidate . returnCartonId ? 'RETURN_ORDER:' + sourceNo : sourceNo
const current = grouped . get ( value )
2026-10-12 15:56:07 +08:00
if ( current ) {
current . hasOutbound ||= candidate . hasOutbound === true || candidate . inventoryStatus === 'OUTBOUNDED'
current . availablePackages += inventoryQuantity ( candidate . availablePackages )
2026-10-14 12:14:37 +08:00
current . units += candidate . returnCartonId && Number ( candidate . stockQuantity ) === 0 ? 0 : 1
2026-10-12 15:56:07 +08:00
current . stockQuantity = current . stockQuantity == null || candidate . stockQuantity == null ? null : current . stockQuantity + inventoryQuantity ( candidate . stockQuantity )
if ( candidate . returnCartonId ) current . reasons . push ( returnInventoryState ( candidate ))
}
2026-10-12 13:43:29 +08:00
else grouped . set ( value , {
value , inboundOrderNo : candidate . returnCartonId ? '退货单 ' + sourceNo : ( candidate . inboundOrderNo || '—' ),
inboundCode : candidate . returnCartonId ? '已签收退货库存(含合箱)' : ( candidate . inboundCode || '—' ),
availablePackages : inventoryQuantity ( candidate . availablePackages ),
quantityUnit : candidate . returnCartonId ? '件' : '箱' ,
2026-10-14 12:14:37 +08:00
stockQuantity : candidate . stockQuantity == null ? null : inventoryQuantity ( candidate . stockQuantity ), units : candidate . returnCartonId && Number ( candidate . stockQuantity ) === 0 ? 0 : 1 , hasOutbound : candidate . hasOutbound === true || candidate . inventoryStatus === 'OUTBOUNDED' , reasons : candidate . returnCartonId ? [ returnInventoryState ( candidate )] : [],
2026-10-12 13:43:29 +08:00
})
2026-09-22 08:47:31 +08:00
}
2026-09-08 20:28:05 +08:00
}
2026-10-12 15:56:07 +08:00
return [... grouped . values ()]. map ( option => ({ ... option , reasons : [... new Set ( option . reasons )] }))
2026-09-08 20:28:05 +08:00
})
2026-10-12 15:56:07 +08:00
const visibleRelabelInboundOptions = computed (() => relabelInboundOptions . value . filter ( option => option . availablePackages > 0 ))
function relabelOptionLabel ( option : typeof relabelInboundOptions . value [ number ]) {
if ( option . quantityUnit === '箱' ) return option . inboundOrderNo + '| ' + option . inboundCode + '|可选 ' + option . availablePackages + ' 箱'
2026-10-14 12:14:37 +08:00
return option . inboundOrderNo + '|在仓 ' + option . units + ' 包裹/箱 · ' + ( option . stockQuantity ?? '待查询' ) + ' 件' + ( option . reasons . some ( reason => reason . includes ( '跨来源单' )) ? '(关联整箱数量,含其他来源)' : '' ) + ( option . hasOutbound && Number ( option . stockQuantity ) > 0 ? '|部分出库' : '' )
2026-10-12 15:56:07 +08:00
}
2026-08-21 17:20:51 +08:00
const loading = ref ( false )
const saving = ref ( false )
2026-10-28 16:33:35 +08:00
const unifiedTemplateInput = ref < HTMLInputElement | null >( null )
2026-09-13 21:10:14 +08:00
const transferCartonMarkInput = ref < HTMLInputElement | null >( null )
2026-09-14 16:51:43 +08:00
const relabelFileInput = ref < HTMLInputElement | null >( null )
const relabelFileScope = ref < 'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM' > ( 'CARTON_TOTAL' )
const relabelFileItemId = ref < string >()
const cartonTotalFile = ref < File | null >( null )
const skuTotalFile = ref < File | null >( null )
const cartonTotalCreateInput = ref < HTMLInputElement | null >( null )
const skuTotalCreateInput = ref < HTMLInputElement | null >( null )
2026-08-21 17:20:51 +08:00
let cargoSequence = 0
2026-09-08 20:28:05 +08:00
let skuRelabelSequence = 0
2026-08-21 17:20:51 +08:00
2026-09-18 16:48:35 +08:00
watch (() => [ props . service , props . createPage , props . detailPage , detailNumber . value ] as const , () => {
mode . value = props . detailPage ? 'detail' : props . createPage ? 'create' : 'list'
page . value = 1
resetFilters ()
resetForm ()
if ( props . createPage ) void openCreate ()
else if ( props . detailPage && detailNumber . value ) void loadDetailPage ()
else void loadPage ()
})
2026-10-14 11:43:38 +08:00
watch ( mode , ( value ) => { if ( value !== 'detail' ) { clearCompletionPhotos (); clearMeasurementPhotos (); clearDiscardPhotos () } })
2026-09-13 21:10:14 +08:00
watch (() => form . relabel , ( value ) => {
if ( props . service === 'transfer' && value ) {
labelingMode . value = 'carton_and_sku'
if ( ! transferRelabelRows . value . length ) addTransferRelabelRow ()
}
})
2026-09-23 11:25:35 +08:00
watch (() => cargoRows . value . map (( row ) => row . sku ), ( skus ) => {
if ( props . service !== 'transfer' || ! form . relabel ) return
transferRelabelRows . value . forEach (( row , index ) => {
const sourceSku = skus [ Math . min ( index , Math . max ( 0 , skus . length - 1 ))] ? . trim () || ''
if ( sourceSku ) row . sku = sourceSku
})
})
2026-10-12 13:43:29 +08:00
watch ( labelingMode , ( value ) => {
if ( ! isReturnRelabelSource . value && value !== 'carton' && ! skuRelabelRows . value . length ) addSkuRelabelRow ()
2026-10-09 11:48:52 +08:00
})
2026-09-17 16:34:51 +08:00
watch (() => form . destination , ( value ) => {
if ( props . service !== 'labeling' ) return
for ( const row of cargoRows . value ) row . destinationWarehouseId = value
for ( const row of skuRelabelRows . value ) row . destinationWarehouseId = value
for ( const row of importedRelabelBindings . value ) row . destinationWarehouseId = value
})
2026-09-14 12:01:13 +08:00
function refreshWarehouseMarkLabels () {
if ( props . service !== 'labeling' ) return
const root = document . querySelector ( '.warehouse-service-page' )
if ( ! root ) return
const walker = document . createTreeWalker ( root , NodeFilter . SHOW_TEXT )
const nodes : Text [] = []
let node : Node | null
while (( node = walker . nextNode ())) nodes . push ( node as Text )
for ( const text of nodes ) text . nodeValue = text . nodeValue ? . replaceAll ( '旧箱唛' , '仓库唛头' ). replaceAll ( '旧唛头' , '仓库唛头' ) || text . nodeValue
}
2026-09-14 16:51:43 +08:00
async function loadPage () {
if ( props . service === 'labeling' || props . service === 'relabel' || props . service === 'transfer' || props . service === 'disposal' ) await loadWarehouses ()
await load ()
}
2026-09-18 16:48:35 +08:00
onMounted (() => {
if ( props . createPage ) void openCreate ()
else if ( props . detailPage && detailNumber . value ) void loadDetailPage ()
else void loadPage ()
void nextTick ( refreshWarehouseMarkLabels )
})
2026-10-15 18:25:32 +08:00
let activatedOnce = false
onActivated (() => {
2026-10-20 11:54:44 +08:00
if ( activatedOnce && ! props . createPage && ! props . detailPage ) {
mode . value = 'list'
void loadPage ()
}
2026-10-15 18:25:32 +08:00
activatedOnce = true
})
2026-10-14 11:43:38 +08:00
onBeforeUnmount (() => { clearCompletionPhotos (); clearMeasurementPhotos (); clearDiscardPhotos () })
2026-09-28 17:26:37 +08:00
function resetFilters () { Object . assign ( filters , { number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' , reviewStart : '' , reviewEnd : '' , completedStart : '' , completedEnd : '' , measuredStart : '' , measuredEnd : '' , outboundStart : '' , outboundEnd : '' }) }
2026-08-21 17:20:51 +08:00
function resetForm () {
2026-10-29 16:13:22 +08:00
labelingFormRef . value ? . clearValidate ? .()
2026-10-09 11:48:52 +08:00
orderInfoCollapsed . value = false
2026-10-09 15:25:27 +08:00
availableBoxesCollapsed . value = false
availableSkusCollapsed . value = false
2026-10-29 16:13:22 +08:00
Object . assign ( form , { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , privateDestinationAddress : '' , privateDestinationPostalCode : '' , privateDestinationContact : '' , privateDestinationPhone : '' , address : '' , contact : '' , phone : '' , pickupContact : '' , pickupPhone : '' , relabelContact : '' , relabelPhone : '' , deliveryMethod : props . service === 'transfer' ? 'WAREHOUSE_PICKUP' : '' , relabelDeliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , estimatedShipAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 })
2026-08-21 17:20:51 +08:00
cargoRows . value = []
2026-09-17 13:36:20 +08:00
transferRelabelRows . value = []
2026-09-17 16:34:51 +08:00
transferRelabelDefaultDestination . value = ''
2026-09-08 20:28:05 +08:00
labelingMode . value = 'carton'
skuRelabelRows . value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings . value = []
2026-08-21 17:20:51 +08:00
discardCandidates . value = []
selectedDiscardOrderId . value = ''
2026-09-08 20:28:05 +08:00
relabelCandidates . value = []
2026-09-23 09:06:20 +08:00
transferInventoryCandidates . value = []
selectedTransferInventory . value = ''
selectedTransferSourceOrderIds . value = []
2026-09-08 20:28:05 +08:00
selectedRelabelInboundOrderId . value = ''
2026-09-20 17:53:41 +08:00
cartonTotalFile . value = null
skuTotalFile . value = null
shippingLabelFile . value = null
if ( cartonTotalCreateInput . value ) cartonTotalCreateInput . value . value = ''
if ( skuTotalCreateInput . value ) skuTotalCreateInput . value . value = ''
2026-09-17 13:36:20 +08:00
transferDestinationType . value = 'NONE'
2026-09-10 15:34:03 +08:00
selectedOperationWarehouse . value = null
2026-09-17 13:36:20 +08:00
editingTransferNo . value = ''
2026-08-21 17:20:51 +08:00
}
async function openCreate () {
2026-10-20 11:54:44 +08:00
if ( ! props . createPage && props . service !== 'relabel' ) {
await router . push ({ name : ` ${ serviceListRouteName ( props . service ) } -create` })
return
}
2026-08-21 17:20:51 +08:00
resetForm (); mode . value = 'create'
2026-10-20 11:54:44 +08:00
await loadWarehouses ()
2026-09-20 16:55:27 +08:00
if ( props . service === 'labeling' ) {
const requestedReturnNo = String ( route . query . returnNo || '' ). trim ()
const requestedWarehouseId = String ( route . query . warehouseId || '' ). trim ()
if ( requestedReturnNo && requestedWarehouseId && operationWarehouses . value . some ( warehouse => String ( warehouse . id ) === requestedWarehouseId )) {
form . warehouse = requestedWarehouseId
await selectOperationWarehouse ()
const sourceValue = `RETURN_ORDER: ${ requestedReturnNo } `
if ( relabelInboundOptions . value . some ( option => option . value === sourceValue )) {
selectedRelabelInboundOrderId . value = sourceValue
selectRelabelInventory ( sourceValue )
} else ElMessage . warning ( `退货单 ${ requestedReturnNo } 暂无可申请贴标的库存` )
}
return
}
2026-08-21 17:20:51 +08:00
if ( props . service !== 'disposal' ) { addCargoRow (); return }
2026-09-14 16:51:43 +08:00
}
2026-09-17 13:36:20 +08:00
2026-09-18 16:48:35 +08:00
function returnToList () {
if ( props . createPage || props . detailPage ) {
2026-09-28 17:26:37 +08:00
void router . push ({ name : serviceListRouteName ( props . service ) })
2026-09-18 16:48:35 +08:00
return
}
mode . value = 'list'
}
2026-09-28 17:26:37 +08:00
function serviceListRouteName ( service : ServiceType ) {
return ({ labeling : 'overseas-labeling' , transfer : 'overseas-storage-transfer' , disposal : 'overseas-cargo-disposal' , relabel : 'overseas-return-relabel' } as Record < ServiceType , string >)[ service ]
}
2026-10-15 18:25:32 +08:00
async function goToServiceList () {
if ( props . createPage || props . detailPage ) {
await router . push ({ name : serviceListRouteName ( props . service ) })
return
}
mode . value = 'list'
await loadPage ()
}
2026-09-18 16:48:35 +08:00
2026-09-17 13:36:20 +08:00
async function openEdit ( transferNo : string ) {
try {
resetForm ()
await loadWarehouses ()
const detail = await getTransfer ( transferNo ) as ServiceDetail
2026-09-28 17:26:37 +08:00
if ( ! [ 'DRAFT' , 'REJECTED' ]. includes ( detail . status || '' )) throw new Error ( '只有草稿或审核驳回状态的转运单可以编辑' )
2026-09-17 13:36:20 +08:00
editingTransferNo . value = transferNo
Object . assign ( form , {
warehouse : detail . operatingWarehouseId || '' , destination : detail . destinationWarehouseCode || detail . fbaWarehouseCode || '' ,
privateDestinationAddress : detail . privateDestinationAddress || '' , privateDestinationPostalCode : detail . privateDestinationPostalCode || '' ,
privateDestinationContact : detail . privateDestinationContactName || '' , privateDestinationPhone : detail . privateDestinationContactPhone || '' ,
address : detail . deliveryAddress || '' , pickupContact : detail . pickupContactName || '' ,
pickupPhone : detail . pickupContactPhone || '' , deliveryMethod : detail . deliveryMethod || '' ,
estimatedArrivalAt : detail . estimatedArrivalAt || '' , estimatedShipAt : detail . estimatedShipAt || '' ,
fbaShipmentNo : detail . fbaShipmentNo || '' , trackingNo : detail . trackingNo || '' , remark : detail . remark || '' ,
volumeCbm : Number ( detail . volumeCbm ?? 0 ), relabel : Boolean ( detail . needRelabel ),
labelDeadline : detail . labelDeadline || '' , relabelContact : detail . relabelContactName || '' ,
relabelPhone : detail . relabelContactPhone || '' , relabelDeliveryMethod : detail . relabelDeliveryMethod || '' ,
})
transferDestinationType . value = detail . destinationType || ( detail . fbaWarehouseCode ? 'FBA' : 'NONE' )
cargoRows . value = ( detail . items || []). map (( item ) => ({
... createCargoRow (), sku : item . skuCode || '' , name : item . productNameZh || '' ,
2026-10-15 15:45:22 +08:00
quantity : Number ( item . quantity || 0 ), expectedPackageCount : Number ( item . packageCount || 0 ), shippingMark : item . shippingMark || '' ,
2026-09-17 13:36:20 +08:00
customerCartonMarks : item . shippingMark || '' ,
2026-10-16 17:43:42 +08:00
generatedCartonMarks : ( detail . cartons || []). filter ( box => String ( box . transferItemId ) === String ( item . id )). map ( box => box . cartonMark ),
2026-09-17 13:36:20 +08:00
}))
const bindings = detail . relabelBindings || []
if ( bindings . length ) {
const operationType = bindings [ 0 ]. operationType
labelingMode . value = operationType === 'SKU_LABEL' ? 'sku' : operationType === 'CARTON_LABEL' ? 'carton' : 'carton_and_sku'
transferRelabelRows . value = bindings . map (( binding ) => ({
... createCargoRow (), sku : binding . originalSku || '' , newSkuCode : binding . newSku || '' ,
quantity : Number ( binding . quantity || 1 ), customerCartonMarks : binding . customerCartonMark || '' ,
fbaCartonMark : binding . fbaCartonMark || '' , destinationWarehouseId : binding . destinationWarehouseCode || '' ,
remark : binding . remark || '' ,
}))
2026-09-17 16:34:51 +08:00
const destinations = [... new Set ( bindings . map (( binding ) => binding . destinationWarehouseCode ). filter ( Boolean ))]
transferRelabelDefaultDestination . value = destinations . length === 1 ? destinations [ 0 ] ! : ''
2026-09-17 13:36:20 +08:00
}
if ( form . warehouse ) {
try { selectedOperationWarehouse . value = await getMerchantWarehouseDetail ( form . warehouse ) } catch { selectedOperationWarehouse . value = null }
}
2026-09-28 17:26:37 +08:00
// The warehouse selector loads inventory candidates for a new transfer, but
// edit mode starts with an already populated warehouse and never emits the
// selector change event. Load the same candidates explicitly so the source
// inventory dropdown is usable when editing a draft.
if ( props . service === 'transfer' && form . warehouse ) {
await loadTransferInventoryCandidates ( form . warehouse )
const sourceInboundCode = String ( detail . inboundCode || '' )
if ( transferInventoryOptions . value . some (( option ) => option . value === sourceInboundCode )) {
selectedTransferInventory . value = sourceInboundCode
2026-10-16 11:42:59 +08:00
await loadTransferSourceCartons ()
if ( transferDestinationType . value === 'NONE' ) transferDestinationType . value = 'FBA'
2026-09-28 17:26:37 +08:00
form . inboundNo = sourceInboundCode
selectedTransferSourceOrderIds . value = ( detail . sourceOrders || [])
. map (( item ) => String ( item . lastlegOrderId || '' )). filter ( Boolean )
}
}
2026-09-17 13:36:20 +08:00
mode . value = 'edit'
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '草稿加载失败' ) }
}
2026-09-14 16:51:43 +08:00
async function selectDiscardWarehouse () {
2026-10-14 17:41:04 +08:00
discardSelectedBoxKeys . value = []
discardBoxSearch . value = ''
2026-09-14 16:51:43 +08:00
selectedDiscardOrderId . value = ''
form . inboundNo = ''
discardCandidates . value = []
cargoRows . value = []
2026-09-17 13:36:20 +08:00
transferRelabelRows . value = []
2026-09-14 16:51:43 +08:00
if ( ! form . warehouse ) return
2026-08-21 17:20:51 +08:00
discardCandidatesLoading . value = true
2026-09-14 16:51:43 +08:00
try {
2026-10-14 17:41:04 +08:00
const warehouse = form . warehouse
const candidates = await listDiscardCandidates ()
if ( form . warehouse !== warehouse ) return
discardCandidates . value = candidates
. filter (( candidate ) => String ( candidate . overseasWarehouseId ) === warehouse )
. filter (( candidate ) => candidate . sourceType === 'DROPSHIP' ? Boolean ( candidate . orderId ) : candidate . sourceType === 'RETURN_CARTON' ? Boolean ( candidate . returnCartonId ) : candidate . sourceType === 'INVENTORY' && Boolean ( candidate . inventoryPositionId ))
. map (( candidate ) => ({ ... candidate , orderId : candidate . sourceType === 'RETURN_CARTON' ? `return: ${ candidate . returnCartonId } ` : candidate . sourceType === 'INVENTORY' ? `inventory: ${ candidate . inventoryPositionId } ` : String ( candidate . orderId ) }))
2026-09-14 16:51:43 +08:00
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '可废弃入库单加载失败' ) }
2026-08-21 17:20:51 +08:00
finally { discardCandidatesLoading . value = false }
}
2026-10-15 15:45:22 +08:00
function createCargoRow () : CargoRow { return { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , expectedPackageCount : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } }
2026-09-13 21:10:14 +08:00
function addCargoRow () { cargoRows . value . push ( createCargoRow ()) }
2026-09-23 11:25:35 +08:00
function addTransferRelabelRow () {
const source = cargoRows . value [ Math . min ( transferRelabelRows . value . length , Math . max ( 0 , cargoRows . value . length - 1 ))]
transferRelabelRows . value . push ({ ... createCargoRow (), sku : source ? . sku || '' })
}
2026-09-13 21:10:14 +08:00
function clearTransferRelabelRows () {
transferRelabelRows . value = []
}
2026-09-09 13:35:51 +08:00
function addSkuRelabelRow () {
2026-10-12 13:43:29 +08:00
if ( isReturnRelabelSource . value ) { ElMessage . warning ( '请从上方实际包裹/箱库存选择 SKU' ); return }
2026-09-09 13:35:51 +08:00
const source = cargoRows . value [ Math . min ( skuRelabelRows . value . length , Math . max ( 0 , cargoRows . value . length - 1 ))]
skuRelabelRows . value . push ({ id : ++ skuRelabelSequence , sourceRowId : source ? . id || null ,
2026-10-12 13:43:29 +08:00
cartonMark : '' , originalSku : source ? . sku || '' , newSku : '' , quantity : 1 ,
2026-10-12 21:48:28 +08:00
fbaCartonMark : '' , shipmentNo : '' , destinationWarehouseId : form . destination , remark : '' })
2026-09-08 20:28:05 +08:00
}
2026-09-09 13:35:51 +08:00
function removeSkuRelabelRow ( id : number ) { skuRelabelRows . value = skuRelabelRows . value . filter (( row ) => row . id !== id ) }
2026-09-10 17:12:52 +08:00
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
2026-10-12 21:48:28 +08:00
if ( ! row . shipmentNo . trim ()) row . shipmentNo = mapping . shipmentNo
2026-09-10 17:12:52 +08:00
if ( ! row . destinationWarehouseId ) row . destinationWarehouseId = mapping . destinationWarehouseId
}
2026-10-29 16:13:22 +08:00
function selectShippingLabel ( event : Event ) {
shippingLabelFile . value = ( event . target as HTMLInputElement ). files ? .[ 0 ] || null
if ( isLabelingCreate . value ) void nextTick (() => labelingFormRef . value ? . validateField ( 'shippingLabelFile' ). catch (() => undefined ))
}
2026-09-20 16:55:27 +08:00
function selectCartonTotalFile ( event : Event ) {
cartonTotalFile . value = ( event . target as HTMLInputElement ). files ? .[ 0 ] || null
if ( cartonTotalFile . value && ! requiresBothTotalFiles . value ) clearCreateTotalFile ( 'SKU_TOTAL' )
}
function selectSkuTotalFile ( event : Event ) {
skuTotalFile . value = ( event . target as HTMLInputElement ). files ? .[ 0 ] || null
if ( skuTotalFile . value && ! requiresBothTotalFiles . value ) clearCreateTotalFile ( 'CARTON_TOTAL' )
}
2026-09-17 13:36:20 +08:00
function clearCreateTotalFile ( scope : 'CARTON_TOTAL' | 'SKU_TOTAL' ) {
if ( saving . value ) return
const file = scope === 'CARTON_TOTAL' ? cartonTotalFile : skuTotalFile
const input = scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput
file . value = null
if ( input . value ) input . value . value = ''
}
2026-09-14 16:51:43 +08:00
function chooseCreateTotalFile ( scope : 'CARTON_TOTAL' | 'SKU_TOTAL' ) {
;( scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput ). value ? . click ()
}
2026-09-08 20:28:05 +08:00
function buildSkuRelabelItems () : RelabelCreate [ 'items' ] {
2026-09-09 13:35:51 +08:00
const groups = new Map < string , SkuRelabelRow [] >()
2026-09-08 20:28:05 +08:00
for ( const row of skuRelabelRows . value ) {
2026-09-09 13:35:51 +08:00
const cartonMark = row . cartonMark . trim (). toUpperCase ()
if ( ! cartonMark ) throw new Error ( '请填写每个箱子的我方箱唛' )
const rows = groups . get ( cartonMark ) || []
2026-09-08 20:28:05 +08:00
rows . push ( row )
2026-09-09 13:35:51 +08:00
groups . set ( cartonMark , rows )
2026-09-08 20:28:05 +08:00
}
2026-09-09 13:35:51 +08:00
const anchors = cargoRows . value . flatMap (( item ) => Array . from (
{ length : Number ( item . availableQuantity || 0 ) }, () => item ,
))
if ( ! anchors . length ) throw new Error ( '请先选择有库存的历史入仓单' )
if ( groups . size > anchors . length ) {
throw new Error ( '填写的箱唛数量超过当前入仓单可用库存箱数' )
}
return [... groups . entries ()]. map (([ _ , rows ], groupIndex ) => {
2026-10-09 11:48:52 +08:00
const source = isReturnRelabelSource . value
2026-10-12 13:43:29 +08:00
? cargoRows . value . find ( item => item . id === rows [ 0 ]. sourceRowId && item . cartonMark === rows [ 0 ]. cartonMark . trim ())
2026-10-09 11:48:52 +08:00
: cargoRows . value . find (( item ) => item . id === rows [ 0 ]. sourceRowId ) || anchors [ groupIndex ]
if ( isReturnRelabelSource . value && ! source ) throw new Error ( '请选择该退货单实际可用的来源箱号' )
if ( isReturnRelabelSource . value ) {
const candidate = selectedReturnCandidates . value . find ( item => String ( item . returnCartonId ) === source ? . returnCartonId )
2026-10-12 13:43:29 +08:00
if ( rows . some ( row => row . sourceRowId !== source ? . id )) throw new Error ( '同一换标箱内的 SKU 必须选择相同的实际来源包裹/箱' )
2026-10-09 11:48:52 +08:00
const requested = new Map < string , number >()
for ( const row of rows ) {
const quantity = ( requested . get ( row . originalSku ) || 0 ) + row . quantity
requested . set ( row . originalSku , quantity )
if ( quantity > Number ( candidate ? . skuItems ? . find ( item => item . skuCode === row . originalSku ) ? . quantity || 0 )) throw new Error ( `SKU ${ row . originalSku } 超过来源箱可用数量` )
}
}
2026-09-17 16:34:51 +08:00
if ( ! source ? . returnCartonId && ( ! source ? . sourceInventoryBalanceId || ! source . sourceInventoryPositionId || ! source . sourceInboundOrderId )) {
2026-09-08 20:28:05 +08:00
throw new Error ( '来源库存已变化,请重新选择有库存的历史入仓单' )
}
const cartonMark = rows [ 0 ]. cartonMark . trim ()
2026-09-09 13:35:51 +08:00
const changeCarton = labelingMode . value === 'carton_and_sku'
2026-09-08 20:28:05 +08:00
const fbaCartonMark = rows [ 0 ]. fbaCartonMark . trim ()
2026-10-12 21:48:28 +08:00
const shipmentNo = rows [ 0 ]. shipmentNo . trim ()
2026-09-17 16:34:51 +08:00
if ( changeCarton && ( ! fbaCartonMark || ! form . destination
|| rows . some (( row ) => row . fbaCartonMark . trim () !== fbaCartonMark ))) {
throw new Error ( '同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 FBA 目的仓' )
2026-09-08 20:28:05 +08:00
}
2026-10-12 21:48:28 +08:00
if ( changeCarton && ( ! shipmentNo || rows . some ( row => row . shipmentNo . trim () !== shipmentNo ))) {
throw new Error ( '同一我方箱唛的组合换标必须填写相同的 FBA 货件编号' )
}
2026-09-08 20:28:05 +08:00
return {
sourceInventoryBalanceId : source . sourceInventoryBalanceId ,
sourceInventoryPositionId : source . sourceInventoryPositionId ,
sourceInboundOrderId : source . sourceInboundOrderId ,
2026-09-17 16:34:51 +08:00
returnCartonId : source . returnCartonId ,
2026-09-08 20:28:05 +08:00
skuCode : source . sku || rows [ 0 ]. originalSku ,
productNameZh : source . name ,
2026-09-09 13:35:51 +08:00
totalPackageCount : 1 ,
notifiedPackageCount : 1 ,
2026-10-12 21:48:28 +08:00
shipmentNo : changeCarton ? shipmentNo : source . shipmentNo || cartonMark ,
2026-09-08 20:28:05 +08:00
trackingCode : form . trackingNo || undefined ,
newSkuCode : rows [ 0 ]. newSku ,
labelRequirement : form . requirement || undefined ,
cartonMark ,
fbaCartonMark : changeCarton ? fbaCartonMark : undefined ,
2026-09-17 16:34:51 +08:00
destinationFbaWarehouseCode : changeCarton ? form . destination : undefined ,
2026-09-08 20:28:05 +08:00
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 }
2026-09-10 17:12:52 +08:00
const source = cargoRows . value . find (( row ) => ! row . included )
if ( ! source ) { ElMessage . warning ( '所选入仓单没有更多可添加的库存箱' ); return }
source . included = true
2026-09-08 20:28:05 +08:00
}
function removeCargoRow ( id : number ) {
2026-09-10 17:12:52 +08:00
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
}
2026-09-08 20:28:05 +08:00
cargoRows . value = cargoRows . value . filter (( row ) => row . id !== id )
}
2026-09-10 17:12:52 +08:00
function clearRelabelRows () {
for ( const row of cargoRows . value ) Object . assign ( row , { included : false , shipmentNo : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' })
}
2026-09-08 20:28:05 +08:00
function csvCell ( value : string | number | undefined ) { return `" ${ String ( value ?? '' ). replaceAll ( '"' , '""' ) } "` }
2026-09-13 21:10:14 +08:00
function transferCustomerMarks ( row : CargoRow ) {
return row . customerCartonMarks . split ( /[\r\n,, ]+/ ). map (( value ) => value . trim ()). filter ( Boolean )
}
function downloadTransferCartonMarkTemplate () {
if ( ! cargoRows . value . length || cargoRows . value . some (( row ) => ! row . sku . trim ())) {
ElMessage . warning ( '请先填写完整的 SKU 明细' ); return
}
const header = [ 'SKU' , '客户箱唛号' ]
2026-10-15 15:45:22 +08:00
const rows = cargoRows . value . flatMap (( row ) => Array . from ({ length : Number ( row . expectedPackageCount || 0 ) }, () => [ row . sku , '' ]))
2026-09-13 21:10:14 +08:00
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 )
}
async function importTransferCartonMarks ( event : Event ) {
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 }
2026-09-14 12:01:13 +08:00
const headers = parseCsvLine ( lines [ 0 ]). map (( name ) => name === '仓库唛头' || name === '旧唛头' ? '旧箱唛' : name )
2026-09-13 21:10:14 +08:00
if ( ! headers . includes ( 'SKU' ) || ! headers . includes ( '客户箱唛号' )) { ElMessage . error ( '客户箱唛模板格式不正确,请重新下载模板' ); return }
const imported = lines . slice ( 1 ). map ( parseCsvLine ). map (( values ) => Object . fromEntries ( headers . map (( key , index ) => [ key , values [ index ] || '' ]))). filter (( item ) => item . SKU || item [ '客户箱唛号' ])
if ( ! imported . length || imported . some (( item ) => ! item . SKU || ! item [ '客户箱唛号' ])) { ElMessage . error ( 'SKU 和客户箱唛号必须填写完整' ); return }
const marks = new Set < string >()
for ( const item of imported ) {
const row = cargoRows . value . find (( candidate ) => candidate . sku . trim (). toUpperCase () === item . SKU . trim (). toUpperCase ())
if ( ! row ) { ElMessage . error ( `SKU ${ item . SKU } 不属于当前转运单` ); return }
const mark = item [ '客户箱唛号' ]. trim ()
if ( marks . has ( mark . toUpperCase ())) { ElMessage . error ( `客户箱唛 ${ mark } 不能重复` ); return }
marks . add ( mark . toUpperCase ())
}
for ( const row of cargoRows . value ) row . customerCartonMarks = imported . filter (( item ) => item . SKU . trim (). toUpperCase () === row . sku . trim (). toUpperCase ()). map (( item ) => item [ '客户箱唛号' ]. trim ()). join ( '\n' )
ElMessage . success ( `已导入 ${ imported . length } 个客户箱唛` )
}
2026-10-28 16:33:35 +08:00
function relabelTemplateTypeForMode ( mode : LabelingMode ) : RelabelTemplateType {
return mode === 'carton' ? 'carton' : mode === 'sku' ? 'sku' : 'mixed'
}
function relabelTemplateTypeLabel ( type : RelabelTemplateType ) {
return type === 'carton' ? '只换箱唛' : type === 'sku' ? '只换 SKU 标签' : 'SKU 和箱唛都换'
}
function parseRelabelTemplateType ( value : string ) : RelabelTemplateType | null {
const normalized = value . trim (). toUpperCase (). replaceAll ( ' ' , '' )
if ([ '只换箱唛' , '箱号关联' , '箱唛换标' , 'CARTON' , 'CARTON_LABEL' ]. includes ( normalized )) return 'carton'
if ([ '只换SKU标签' , 'SKU关联' , 'SKU换标' , 'SKU' , 'SKU_LABEL' ]. includes ( normalized )) return 'sku'
if ([ 'SKU和箱唛都换' , 'SKU混箱' , 'SKU混箱换标' , '混箱' , 'MIXED' , 'CARTON_AND_SKU' ]. includes ( normalized )) return 'mixed'
return null
}
function downloadRelabelTemplate () {
2026-09-08 20:28:05 +08:00
if ( ! cargoRows . value . length ) { ElMessage . warning ( '请先选择有库存的历史入仓单' ); return }
2026-10-28 16:33:35 +08:00
const templateType = relabelTemplateTypeForMode ( labelingMode . value )
2026-09-17 16:34:51 +08:00
const cartonMarkHeader = props . service === 'transfer' ? '客户箱唛号' : '仓库唛头'
2026-10-28 16:33:35 +08:00
const header = [ '明细行标识' , '贴标类型' , cartonMarkHeader , '旧SKU标签' , '新SKU标签' , '换标数量(件)' , '新FBA箱唛' , 'FBA货件编号' , '备注' ]
const rows = cargoRows . value . map (( item ) => [ item . id , relabelTemplateTypeLabel ( templateType ), props . service === 'transfer' ? item . customerCartonMarks : item . cartonMark , item . sku , '' , 1 , '' , item . shipmentNo , item . remark ])
2026-09-08 20:28:05 +08:00
const blob = new Blob ([ ` \ uFEFF ${ [ header , ... rows ]. map (( row ) => row . map ( csvCell ). join ( ',' )). join ( '\r\n' ) } \ r \ n` ], { type : 'text/csv;charset=utf-8' })
2026-10-28 16:33:35 +08:00
const url = URL . createObjectURL ( blob ); const anchor = document . createElement ( 'a' ); anchor . href = url ; anchor . download = '贴标申请-统一明细关联模板.csv' ; anchor . click (); URL . revokeObjectURL ( url )
2026-09-06 15:53:56 +08:00
}
function parseCsvLine ( line : string ) {
const values : string [] = []; let value = '' ; let quoted = false
2026-10-28 16:33:35 +08:00
for ( let index = 0 ; index < line . length ; index += 1 ) { const char = line [ index ]; if ( char === '"' ) { if ( quoted && line [ index + 1 ] === '"' ) { value += '"' ; index += 1 } else quoted = ! quoted } else if ( char === ',' && ! quoted ) { values . push ( value . trim ()); value = '' } else value += char }
2026-09-06 15:53:56 +08:00
values . push ( value . trim ()); return values
}
2026-10-28 16:33:35 +08:00
async function importRelabelTemplate ( event : Event ) {
const input = event . target as HTMLInputElement ; const file = input . files ? .[ 0 ]; input . value = '' ; if ( ! file ) return
2026-09-06 15:53:56 +08:00
const lines = ( await file . text ()). replace ( /^\uFEFF/ , '' ). split ( /\r?\n/ ). filter (( line ) => line . trim ())
if ( lines . length < 2 ) { ElMessage . error ( '模板中没有可导入的明细' ); return }
2026-09-17 16:34:51 +08:00
const headers = parseCsvLine ( lines [ 0 ]). map (( name ) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name )
2026-10-28 16:33:35 +08:00
const requiredHeaders = [ '明细行标识' , '贴标类型' , '旧箱唛' , '旧SKU标签' , '新SKU标签' , '换标数量(件)' , '新FBA箱唛' , 'FBA货件编号' , '备注' ]
if ( requiredHeaders . 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 [ '明细行标识' ] || item [ '贴标类型' ] || item [ '旧箱唛' ] || item [ '新SKU标签' ] || item [ '新FBA箱唛' ])
if ( ! imported . length ) { ElMessage . error ( '模板中没有可导入的明细' ); return }
const rows : ImportedRelabelBinding [] = []; let detectedType : RelabelTemplateType | null = null
for ( const item of imported ) {
const templateType = parseRelabelTemplateType ( item [ '贴标类型' ]); if ( ! templateType ) { ElMessage . error ( `明细行 ${ item [ '明细行标识' ] } 的贴标类型无效` ); return }
if ( detectedType && detectedType !== templateType ) { ElMessage . error ( '同一份明细文件只能包含一种贴标类型,请拆分后分别提交换标单' ); return }
detectedType = templateType
const source = cargoRows . value . find (( row ) => String ( row . id ) === item [ '明细行标识' ]); if ( ! source ) { ElMessage . error ( `明细行 ${ item [ '明细行标识' ] } 不属于当前库存,请重新下载模板` ); return }
const quantity = Number ( item [ '换标数量(件)' ]); const requiresSku = templateType !== 'carton' ; const requiresCarton = templateType !== 'sku'
if ( ! item [ '旧箱唛' ] || ( requiresSku && ( ! item [ '旧SKU标签' ] || ! item [ '新SKU标签' ])) || ! Number . isInteger ( quantity ) || quantity < 1 || ( requiresCarton && ( ! item [ '新FBA箱唛' ] || ! item [ 'FBA货件编号' ]))) { ElMessage . error ( `明细行 ${ item [ '明细行标识' ] } 未按“ ${ relabelTemplateTypeLabel ( templateType ) } ”填写完整` ); return }
rows . push ({ id : ++ skuRelabelSequence , sourceRowId : source . id , cartonMark : item [ '旧箱唛' ]. trim (), originalSku : ( item [ '旧SKU标签' ] || source . sku ). trim (), newSku : item [ '新SKU标签' ]. trim (), quantity : templateType === 'carton' ? 1 : quantity , fbaCartonMark : item [ '新FBA箱唛' ]. trim (), destinationWarehouseId : form . destination , remark : item [ '备注' ] || '' , templateType , selected : true , shipmentNo : item [ 'FBA货件编号' ]. trim () })
2026-09-10 15:56:39 +08:00
}
2026-10-28 16:33:35 +08:00
const keys = rows . map (( row ) => detectedType === 'carton' ? String ( row . sourceRowId ) : ` ${ row . cartonMark . toUpperCase () } | ${ row . originalSku . toUpperCase () } ` )
if ( new Set ( keys ). size !== keys . length ) { ElMessage . error ( '存在重复箱明细或重复的箱内 SKU,请合并后重新上传' ); return }
const cartonTargets = new Map < string , string >()
for ( const row of rows ) {
const key = row . cartonMark . toUpperCase ()
const target = ` ${ row . fbaCartonMark } | ${ row . shipmentNo } `
if ( detectedType === 'mixed' && cartonTargets . has ( key ) && cartonTargets . get ( key ) !== target ) { ElMessage . error ( '同一箱内的 SKU 必须填写相同的新 FBA 箱唛和货件编号' ); return }
cartonTargets . set ( key , target )
2026-09-08 20:28:05 +08:00
}
2026-10-28 16:33:35 +08:00
importedRelabelBindings . value = rows ; ElMessage . success ( `已读取 ${ rows . length } 条绑定关系,请选择后导入到明细` )
2026-09-10 17:12:52 +08:00
}
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 }
2026-09-17 16:34:51 +08:00
if ( props . service === 'transfer' ) {
const importedRows = selected . map (( binding ) => ({
... createCargoRow (),
sku : binding . originalSku ,
newSkuCode : binding . newSku ,
quantity : binding . quantity ,
customerCartonMarks : binding . cartonMark ,
fbaCartonMark : binding . fbaCartonMark ,
destinationWarehouseId : '' ,
remark : binding . remark ,
}))
const importedMarks = new Set ( importedRows . flatMap ( transferCustomerMarks ). map (( mark ) => mark . toUpperCase ()))
transferRelabelRows . value = [
... transferRelabelRows . value . filter (( row ) => ! transferCustomerMarks ( row ). some (( mark ) => importedMarks . has ( mark . toUpperCase ()))),
... importedRows ,
]
labelingMode . value = templateType === 'carton' ? 'carton' : templateType === 'sku' ? 'sku' : 'carton_and_sku'
importedRelabelBindings . value = importedRelabelBindings . value . filter (( item ) => ! item . selected )
ElMessage . success ( `已导入 ${ importedRows . length } 条仓储转运换标明细` )
return
}
2026-09-10 17:12:52 +08:00
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 ,
2026-09-17 16:34:51 +08:00
destinationWarehouseId : form . destination , remark : binding . remark })
2026-09-08 20:28:05 +08:00
}
2026-09-10 17:12:52 +08:00
labelingMode . value = 'carton'
} else {
2026-10-12 21:48:28 +08:00
const importedRows = selected . map (({ templateType : _ , selected : __ , ... item }) => item )
2026-09-10 17:12:52 +08:00
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 ()))
2026-09-17 16:34:51 +08:00
item . destinationWarehouseId = form . destination
2026-09-10 17:12:52 +08:00
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 ,
2026-09-17 16:34:51 +08:00
fbaCartonMark : item . fbaCartonMark , destinationWarehouseId : form . destination , shipmentNo })
2026-09-10 17:12:52 +08:00
}
}
labelingMode . value = templateType === 'mixed' ? 'carton_and_sku' : 'sku'
2026-09-08 20:28:05 +08:00
}
2026-09-10 17:12:52 +08:00
importedRelabelBindings . value = importedRelabelBindings . value . filter (( item ) => ! item . selected )
ElMessage . success ( `已将 ${ selected . length } 条绑定关系导入本次换标明细` )
2026-09-06 15:53:56 +08:00
}
2026-08-21 17:20:51 +08:00
function selectDiscardInbound ( orderId : string ) {
2026-10-29 15:49:59 +08:00
discardRequestedPackages . value = undefined
2026-10-14 17:41:04 +08:00
discardSelectedBoxKeys . value = []
discardBoxSearch . value = ''
2026-10-19 15:59:04 +08:00
const candidate = discardDocuments . value . find (( item ) => item . orderId === orderId )
2026-08-21 17:20:51 +08:00
if ( ! candidate ) { cargoRows . value = []; return }
2026-09-28 17:26:37 +08:00
form . inboundNo = candidate . inboundCode || candidate . inboundOrderNo
2026-08-21 17:20:51 +08:00
form . warehouse = candidate . overseasWarehouseId
2026-10-14 17:41:04 +08:00
if ( candidate . cartonSelectionSupported ) { selectAllDiscardBoxes ( true ); return }
2026-10-19 15:59:04 +08:00
cargoRows . value = candidate . sources . flatMap ( source => source . skus . map (( sku ) => ({
2026-10-14 17:41:04 +08:00
id : ++ cargoSequence , sku : sku . skuCode , name : sku . productName , quantity : sku . availablePackages ,
2026-10-19 15:59:04 +08:00
availableQuantity : sku . availablePackages , sourceOrderId : source . orderId , sourceType : source . sourceType ,
returnCartonId : source . returnCartonId , sourceInventoryPositionId : source . inventoryPositionId , palletNo : source . palletNo , cartonMark : source . cartonMark || '' ,
2026-09-28 17:26:37 +08:00
fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' ,
2026-10-19 15:59:04 +08:00
})))
2026-08-21 17:20:51 +08:00
}
function dateTime ( value : string , end = false ) { return value ? ` ${ value } T ${ end ? '23:59:59' : '00:00:00' } ` : undefined }
2026-08-30 17:35:52 +08:00
function row ( values : Array < string | number | undefined | null >) { return Object . fromEntries ( values . map (( value , index ) => [ String ( index ), value == null || value === '' ? '—' : String ( value )])) }
2026-09-06 15:53:56 +08:00
function warehouseDisplay ( value ?: string ) {
if ( ! value ) return '—'
2026-09-08 20:28:05 +08:00
return value . split ( ',' ). map (( code ) => {
const warehouse = [... operationWarehouses . value , ... fbaWarehouses . value ]. find (( item ) => String ( item . id ) === code || item . warehouseCode === code )
2026-09-28 17:01:36 +01:00
return warehouse ? warehouseLabel ( warehouse ) : '—'
2026-09-08 20:28:05 +08:00
}). join ( '、' )
2026-09-06 15:53:56 +08:00
}
2026-09-07 19:04:00 +08:00
function statusLabel ( value : string ) {
2026-09-15 15:20:35 +08:00
if ( props . service === 'labeling' ) {
2026-09-23 11:25:35 +08:00
const relabelLabels : Record < string , string > = {
WAIT_APPROVAL : '待审核' , PENDING_REVIEW : '待审核' , PENDING_PROCESSING : '待仓库处理' , PROCESSING : '处理中' ,
CUSTOMER_CONFIRM_PENDING : '待客户确认' , READY_FOR_OUTBOUND : '换标完成' , COMPLETED : '已完成' ,
REJECTED : '已驳回' , CANCELLED : '已取消' ,
}
return relabelLabels [ value ] || value || '—'
2026-09-17 13:36:20 +08:00
}
if ( props . service === 'transfer' ) {
const transferLabels : Record < string , string > = {
DRAFT : '草稿' , PENDING_REVIEW : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' ,
WAIT_PDA_RECEIVING : '待PDA收货' , INBOUND_MEASURING : '入库测量中' ,
WAIT_CUSTOMER_CONFIRMATION : '待客户确认实测' , WAIT_RELABEL : '待换标' ,
WAIT_FBA_DESTINATION : '待确认目的地' , READY_FOR_DISPATCH : '待排车' ,
WAIT_OUTBOUND : '待出库' , PENDING_ASSIGNMENT : '待分配' , ALLOCATED : '已分配库存' ,
OUTBOUNDED : '已出库' , PARTIALLY_RECEIVED : '部分收货' , RECEIVED : '已收货' ,
INBOUND_COMPLETED : '入库完成' , COMPLETED : '已完成' ,
}
if ( transferLabels [ value ]) return transferLabels [ value ]
2026-09-15 15:20:35 +08:00
}
2026-09-07 19:04:00 +08:00
const labels : Record < string , string > = {
2026-09-08 17:24:51 +08:00
2026-09-10 15:34:03 +08:00
PENDING_PROCESSING : '待处理' , PROCESSING : '处理中' , CUSTOMER_CONFIRM_PENDING : '待客户确认' ,
2026-09-15 15:20:35 +08:00
READY_FOR_OUTBOUND : '换标完成' , COMPLETED : '已完成' , CANCELLED : '已取消' ,
2026-10-15 15:45:22 +08:00
DRAFT : '草稿' , PENDING_REVIEW : '待审核' , APPROVED : '待 PDA 废弃' , PDA_COMPLETED : '待客户确认' , REJECTED : '已拒绝' , PENDING_ASSIGNMENT : '待分配' ,
2026-09-17 13:36:20 +08:00
WAIT_PDA_RECEIVING : '待PDA收货' , WAIT_CUSTOMER_CONFIRMATION : '待客户确认实测' , WAIT_FBA_DESTINATION : '待确认目的地' , WAIT_RELABEL : '待换标' , READY_FOR_DISPATCH : '待排车' , WAIT_OUTBOUND : '待出库' ,
ALLOCATED : '已分配' , OUTBOUNDED : '已出库' , PARTIALLY_RECEIVED : '部分收货' , RECEIVED : '已收货' , INBOUND_COMPLETED : '入库完成' , PENDING_DELIVERY : '待派送' , DELIVERING : '派送中' , DELIVERED : '派送完成' ,
2026-09-08 17:24:51 +08:00
2026-09-07 19:04:00 +08:00
}
return labels [ value ] || value || '—'
}
2026-09-23 11:25:35 +08:00
type StatusTagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
2026-10-13 17:43:05 +08:00
function labelingTypeLabels ( value : unknown ) : string [] {
return typeof value === 'string' && value !== '—' ? [... new Set ( value . split ( '、' ))] : []
}
function labelingTypeTagType ( label : string ) : StatusTagType {
if ( label === '只换 SKU' ) return 'primary'
if ( label === '只换箱唛' ) return 'warning'
return 'success'
}
function labelingStatusTagType ( status : string ) : StatusTagType {
if ( status === 'CANCELLED' ) return 'info'
if ( ! [ 'READY_FOR_OUTBOUND' , 'COMPLETED' , 'REJECTED' , 'WAIT_APPROVAL' , 'PENDING_REVIEW' , 'PENDING_PROCESSING' , 'CUSTOMER_CONFIRM_PENDING' , 'PROCESSING' ]. includes ( status )) return 'info'
return relabelStatusTagType ( status )
}
2026-09-23 11:25:35 +08:00
function relabelStatusTagType ( status : string ) : StatusTagType {
if ([ 'READY_FOR_OUTBOUND' , 'COMPLETED' ]. includes ( status )) return 'success'
if ([ 'REJECTED' , 'CANCELLED' ]. includes ( status )) return 'danger'
if ([ 'WAIT_APPROVAL' , 'PENDING_REVIEW' , 'PENDING_PROCESSING' , 'CUSTOMER_CONFIRM_PENDING' ]. includes ( status )) return 'warning'
return 'primary'
}
function relabelFulfillmentLabel ( value ?: string ) {
const labels : Record < string , string > = {
WAIT_APPROVAL : '待审核' , WAIT_PROCESSING : '待仓库处理' ,
WAIT_RELABEL : '待换标' , WAIT_PRE_DISPATCH : '待排车' , READY_FOR_DISPATCH : '待排车' ,
OUTBOUND_CREATED : '换标出库单已创建' , OUTBOUNDED : '已出库' , COMPLETED : '已完成' ,
}
return labels [ value || '' ] || value || '—'
}
function relabelFulfillmentTagType ( status ?: string ) : StatusTagType {
if ([ 'OUTBOUNDED' , 'COMPLETED' ]. includes ( status || '' )) return 'success'
if ([ 'WAIT_APPROVAL' , 'WAIT_PROCESSING' , 'WAIT_RELABEL' , 'WAIT_PRE_DISPATCH' , 'READY_FOR_DISPATCH' ]. includes ( status || '' )) return 'warning'
return 'primary'
}
function transferStatusTagType ( status : string ) : StatusTagType {
if ([ 'RECEIVED' , 'COMPLETED' ]. includes ( status )) return 'success'
if ([ 'REJECTED' , 'CANCELLED' ]. includes ( status )) return 'danger'
if ([ 'DRAFT' ]. includes ( status )) return 'info'
if ([ 'PENDING_REVIEW' , 'WAIT_CUSTOMER_CONFIRMATION' , 'WAIT_RELABEL' , 'WAIT_FBA_DESTINATION' , 'READY_FOR_DISPATCH' , 'WAIT_OUTBOUND' , 'PARTIALLY_RECEIVED' ]. includes ( status )) return 'warning'
return 'primary'
}
function transferRelabelStatus ( item : { needRelabel ?: boolean ; relabelNo ?: string ; relabelStatus ?: string ; relabelFulfillmentStatus ?: string }) {
if ( ! item . needRelabel ) return { code : 'NOT_REQUIRED' , label : '无需换标' }
if ( ! item . relabelNo ) return { code : 'WAIT_RELABEL' , label : '待换标' }
const code = item . relabelStatus || 'PENDING_PROCESSING'
const labels : Record < string , string > = {
PENDING_REVIEW : '待审核' , PENDING_PROCESSING : '待处理' , PROCESSING : '换标中' ,
CUSTOMER_CONFIRM_PENDING : '待客户确认' , READY_FOR_OUTBOUND : '已换标' , COMPLETED : '已换标' ,
REJECTED : '已驳回' , CANCELLED : '已取消' ,
}
const fulfillmentLabels : Record < string , string > = {
WAIT_RELABEL : '待换标' , WAIT_CUSTOMER_CONFIRMATION : '待客户确认' , READY_FOR_DISPATCH : '已换标' ,
OUTBOUND_CREATED : '已换标' , OUTBOUNDED : '已换标' , NOT_REQUIRED : '无需换标' ,
}
return { code , label : labels [ code ] || fulfillmentLabels [ item . relabelFulfillmentStatus || '' ] || statusLabel ( code ) }
}
function transferRelabelStatusTagType ( status : string ) : StatusTagType {
if ([ 'READY_FOR_OUTBOUND' , 'COMPLETED' ]. includes ( status )) return 'success'
if ([ 'REJECTED' , 'CANCELLED' ]. includes ( status )) return 'danger'
if ( status === 'NOT_REQUIRED' ) return 'info'
if ([ 'WAIT_RELABEL' , 'PENDING_REVIEW' , 'CUSTOMER_CONFIRM_PENDING' ]. includes ( status )) return 'warning'
return 'primary'
}
2026-09-07 19:04:00 +08:00
function deliveryMethodLabel ( value : string ) {
2026-09-10 15:34:03 +08:00
const labels : Record < string , string > = {
2026-10-14 17:41:04 +08:00
TRUCK : '卡车派送' , EXPRESS : '快递派送' , AMAZON_PICKUP : '亚马逊自提' , CUSTOMER_PICKUP : '客户自提' ,
2026-10-16 11:42:59 +08:00
INVENTORY_TRANSFER : '库存转运' , WAREHOUSE_PICKUP : '仓库上门提货' , CUSTOMER_DELIVERY : '客户送货到仓' ,
2026-09-10 15:34:03 +08:00
// Keep the legacy value readable for historical applications created before the four-way split.
PICKUP : '仓库自提' ,
}
2026-09-07 19:04:00 +08:00
return labels [ value ] || value || '—'
}
2026-09-23 11:25:35 +08:00
function destinationTypeLabel ( value ?: string , fbaWarehouseCode ?: string ) {
if ( value === 'PRIVATE' ) return '私人仓'
if ( value === 'FBA' || fbaWarehouseCode ) return 'FBA 仓'
return '暂未确定'
}
2026-09-10 15:34:03 +08:00
function formatDateTime ( value ?: string | null ) {
2026-09-07 19:04:00 +08:00
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 19 ) : '—'
}
2026-10-20 14:00:59 +08:00
let discardLoadGeneration = 0
2026-08-21 17:20:51 +08:00
async function load () {
2026-10-20 14:00:59 +08:00
const discardRequest = props . service === 'disposal' ? ++ discardLoadGeneration : undefined
2026-08-21 17:20:51 +08:00
loading . value = true
try {
if ( props . service === 'labeling' ) {
2026-10-20 11:54:44 +08:00
const query = { page : page . value , size : pageSize . value , relabelNo : filters . number || undefined , overseasWarehouseId : filters . warehouse || undefined , status : filters . status || undefined , createdFrom : dateTime ( filters . start ), createdTo : dateTime ( filters . end , true ), reviewedFrom : dateTime ( filters . reviewStart ), reviewedTo : dateTime ( filters . reviewEnd , true ), completedFrom : dateTime ( filters . completedStart ), completedTo : dateTime ( filters . completedEnd , true ), outboundFrom : dateTime ( filters . outboundStart ), outboundTo : dateTime ( filters . outboundEnd , true ) }
const [ result , dimensionResult ] = await Promise . all ([ listRelabels ( query ), getRelabelQuantities ( query ). catch (() => null )])
2026-09-15 15:20:35 +08:00
total . value = Number . isFinite ( Number ( result . total )) ? Number ( result . total ) : result . records . length
2026-09-06 15:53:56 +08:00
submittedRows . value = result . records . map (( item ) => {
const destinationWarehouses = warehouseDisplay ( item . overseasWarehouseId )
2026-10-20 15:07:39 +08:00
return { ... row ([ item . relabelNo , ( item . relatedBusinessOrderNos || []). join ( '、' ) || '—' , destinationWarehouses , dimensionResult ? . orders [ item . relabelNo ] ? . skuCount ?? '—' , dimensionResult ? . orders [ item . relabelNo ] ? . cartonCount ?? '—' , dimensionResult ? . orders [ item . relabelNo ] ? . labelPieceCount ?? '—' , ( item . operationTypes || []). map ( type => ({ CARTON_LABEL : '只换箱唛' , SKU_LABEL : '只换 SKU' , RETURN_BULK_SKU_LABEL : '只换 SKU' , CARTON_AND_SKU : 'SKU 和箱唛都换' }[ type ] || type )). join ( '、' ) || '—' , statusLabel ( item . status ), formatDateTime ( item . createdAt )]), __status : item . status , __confirmedAt : item . customerConfirmedAt || '' }
2026-09-06 15:53:56 +08:00
})
2026-08-21 17:20:51 +08:00
} else if ( props . service === 'disposal' ) {
2026-10-20 11:54:44 +08:00
const discardQuery = { page : page . value , size : pageSize . value , 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 ), completedFrom : dateTime ( filters . completedStart ), completedTo : dateTime ( filters . completedEnd , true ) }
2026-10-20 14:00:59 +08:00
const result = await listDiscards ( discardQuery )
if ( discardRequest !== discardLoadGeneration || props . service !== 'disposal' ) return
2026-09-15 15:20:35 +08:00
total . value = Number . isFinite ( Number ( result . total )) ? Number ( result . total ) : result . records . length
2026-10-20 14:00:59 +08:00
submittedRows . value = result . records . map (( item ) => ({ ... row ([ item . discardNo , item . outboundNo || '—' , item . inboundCodes , item . overseasWarehouseName || warehouseDisplay ( item . overseasWarehouseId ) || '—' , item . skuCodes ? . length ? ` ${ item . skuCount } 种| ${ item . skuCodes . join ( '、' ) } ` : '未记录' , item . requestedPackages , ` ${ item . currency } ${ item . totalFee } ` , statusLabel ( item . status ), formatDateTime ( item . customerConfirmedAt ), formatDateTime ( item . createdAt )]), __status : item . status }))
2026-08-21 17:20:51 +08:00
} else {
2026-10-20 14:00:59 +08:00
const query = { page : page . value , size : pageSize . value , transferNo : filters . number || undefined , inboundCode : filters . inboundNo || undefined , operatingWarehouseId : filters . warehouse || undefined , fbaWarehouseCode : filters . destination || undefined , status : filters . status || undefined , ...( props . service === 'relabel' ? { needRelabel : true } : {}), createdFrom : dateTime ( filters . start ), createdTo : dateTime ( filters . end , true ), reviewedFrom : dateTime ( filters . reviewStart ), reviewedTo : dateTime ( filters . reviewEnd , true ), measuredFrom : props . service === 'relabel' ? dateTime ( filters . measuredStart ) : dateTime ( filters . completedStart ), measuredTo : props . service === 'relabel' ? dateTime ( filters . measuredEnd , true ) : dateTime ( filters . completedEnd , true ), outboundFrom : dateTime ( filters . outboundStart ), outboundTo : dateTime ( filters . outboundEnd , true ) }
const result = await listTransfers ( query )
2026-09-15 15:20:35 +08:00
total . value = Number . isFinite ( Number ( result . total )) ? Number ( result . total ) : result . records . length
submittedRows . value = result . records . map (( item ) => {
const operationWarehouse = warehouseDisplay ( item . operatingWarehouseId )
2026-09-23 11:25:35 +08:00
const relabelStatus = transferRelabelStatus ( item )
2026-09-15 15:20:35 +08:00
return props . service === 'transfer'
2026-10-19 15:59:04 +08:00
? { ... row ([ item . transferNo , item . packageCount , operationWarehouse , item . fbaWarehouseCode , deliveryMethodLabel ( item . deliveryMethod ), item . needRelabel ? '是' : '否' , item . relabelNo , relabelStatus . label , statusLabel ( item . status ), formatDateTime ( item . createdAt )]), __status : item . status , __statusLabel : statusLabel ( item . status ), __relabelStatus : relabelStatus . code , __relabelStatusLabel : relabelStatus . label }
2026-09-28 17:26:37 +08:00
: row ([ item . transferNo , '—' , item . fbaWarehouseCode , '—' , item . packageCount , '是' , item . relabelNo ? '已生成' : '待生成' , statusLabel ( item . status ), formatDateTime ( item . reviewedAt ), formatDateTime ( item . measuredAt ), formatDateTime ( item . outboundAt ), formatDateTime ( item . createdAt )])
2026-09-15 15:20:35 +08:00
})
2026-08-21 17:20:51 +08:00
}
2026-10-20 14:00:59 +08:00
} catch ( error ) { if ( discardRequest !== undefined && discardRequest !== discardLoadGeneration ) return ; submittedRows . value = []; total . value = 0 ; ElMessage . error ( error instanceof Error ? error . message : '列表加载失败' ) }
finally { if ( discardRequest === undefined || discardRequest === discardLoadGeneration ) loading . value = false }
2026-08-21 17:20:51 +08:00
}
2026-09-18 16:48:35 +08:00
function query () { if ( page . value === 1 ) void load (); else page . value = 1 }
function resetListQuery () { resetFilters (); query () }
function listRowActions ( candidate : object ) : TableAction [] {
const item = candidate as Record < string , string >
const actions : TableAction [] = [{ key : 'detail' , label : '详情' , run : () => openDetail ( item ) }]
2026-09-28 17:26:37 +08:00
if ( canCreate . value && props . service === 'transfer' && [ 'DRAFT' , 'REJECTED' ]. includes ( item . __status )) actions . push ({ key : 'edit' , label : '编辑' , run : () => openEdit ( item [ '0' ]) })
2026-09-18 16:48:35 +08:00
if ( canConfirmTransfer . value && props . service === 'transfer' && item . __status === 'WAIT_CUSTOMER_CONFIRMATION' ) actions . push ({ key : 'confirm-measurement' , label : '确认测量' , run : () => confirmTransferFromList ( item [ '0' ]) })
if ( canConfirmRelabel . value && props . service === 'labeling' && item . __status === 'CUSTOMER_CONFIRM_PENDING' && ! item . __confirmedAt ) actions . push ({ key : 'confirm-relabel' , label : '确认换标' , run : () => confirmRelabelFromList ( item [ '0' ]) })
2026-10-15 15:45:22 +08:00
if ( canConfirmDiscard . value && props . service === 'disposal' && item . __status === 'PDA_COMPLETED' ) actions . push ({ key : 'confirm-discard' , label : '废弃确认' , run : () => openCompletionConfirmation ( item [ '0' ], 'disposal' ) })
2026-09-18 16:48:35 +08:00
return actions
}
watch ([ page , pageSize ], ([ nextPage , nextSize ], [ previousPage , previousSize ]) => {
if ( nextSize !== previousSize && nextPage !== 1 ) { page . value = 1 ; return }
void load ()
})
2026-10-16 17:43:42 +08:00
function printForecastCartons () {
if ( saving . value ) return
let html : string
try { html = prepareTransferCartonLabels ( cargoRows . value ) }
catch ( error ) { ElMessage . warning ( error instanceof Error ? error . message : '箱唛生成失败' ); return }
const printWindow = window . open ( '' , '_blank' )
if ( ! printWindow ) { ElMessage . warning ( '请允许浏览器打开打印窗口' ); return }
printWindow . opener = null
printWindow . document . open (); printWindow . document . write ( html ); printWindow . document . close ()
printWindow . focus (); printWindow . print ()
}
async function submit ( action : 'draft' | 'submit' = 'submit' ) {
2026-10-16 16:52:49 +08:00
if ( saving . value ) return
2026-10-29 16:13:22 +08:00
if ( isLabelingCreate . value ) {
orderInfoCollapsed . value = false
await nextTick ()
if ( ! await labelingFormRef . value ? . validate (). catch (() => false )) return
}
2026-10-19 14:53:57 +08:00
if ( props . service === 'transfer' && inventoryTransfer . value && action === 'draft' ) {
ElMessage . warning ( '选择库存来源后不能保存草稿,请直接提交订单' )
return
}
2026-10-29 16:13:22 +08:00
if ( isDisposalCreate . value && ! await labelingFormRef . value ? . validate (). catch (() => false )) return
2026-10-19 14:30:52 +08:00
if ( transferRelabelRequired . value ) form . relabel = true
2026-10-29 16:13:22 +08:00
if ( isTransferCreate . value ) {
await nextTick ()
if ( ! await labelingFormRef . value ? . validate (). catch (() => false )) return
}
2026-08-21 17:20:51 +08:00
const primaryWarehouse = props . service === 'relabel' ? form . sourceWarehouse : form . warehouse
2026-09-10 17:12:52 +08:00
const relabelRows = labelingMode . value === 'carton' ? cartonDetailRows . value : skuRelabelRows . value
2026-08-21 17:20:51 +08:00
const invalidCargo = props . service === 'disposal'
2026-10-29 15:49:59 +08:00
? ! selectedDiscardOrderId . value || ( discardQuantityMode . value && ( ! Number . isInteger ( discardRequestedPackages . value ) || Number ( discardRequestedPackages . value ) < 1 || Number ( discardRequestedPackages . value ) > Number ( discardSelectedCandidate . value ? . availablePackages || 0 ))) || cargoRows . value . every (( item ) => item . quantity < 1 )
2026-08-21 17:20:51 +08:00
|| cargoRows . value . some (( item ) => item . quantity < 0 || item . quantity > Number ( item . availableQuantity || 0 ))
2026-09-13 21:10:14 +08:00
: ( props . service === 'labeling' || props . service === 'relabel' ) && ( relabelRows . length === 0 || ( props . service === 'labeling' && labelingMode . value !== 'carton'
2026-10-12 13:43:29 +08:00
? skuRelabelRows . value . some (( item ) => ! item . cartonMark || ! item . originalSku || ! item . newSku || item . quantity < 1
2026-10-12 21:48:28 +08:00
|| ( labelingMode . value === 'carton_and_sku' && ( ! item . fbaCartonMark || ! item . shipmentNo . trim ())))
2026-10-15 15:45:22 +08:00
: ( props . service === 'labeling' ? cartonDetailRows . value : cargoRows . value ). some (( item ) => ( props . service !== 'labeling' && ! item . sku ) || ! item . name
2026-09-13 21:10:14 +08:00
|| ( props . service !== 'labeling' && item . quantity < 1 ))))
2026-09-14 16:51:43 +08:00
if (( props . service === 'relabel' && ! form . inboundNo ) || ! primaryWarehouse || invalidCargo ) {
2026-08-21 17:20:51 +08:00
ElMessage . warning ( '请填写必填信息和完整的货物明细' ); return
}
2026-09-14 16:51:43 +08:00
if ( props . service === 'labeling' && ( ! form . labelDeadline || ! form . contact || ! form . phone )) {
ElMessage . warning ( '请填写贴标截止时间和海外仓联系人信息' ); return
2026-09-06 15:53:56 +08:00
}
2026-09-20 16:55:27 +08:00
if ( props . service === 'labeling' && requiresBothTotalFiles . value && ( ! cartonTotalFile . value || ! skuTotalFile . value )) {
ElMessage . warning ( 'SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件' ); return
}
2026-10-12 13:43:29 +08:00
if ( props . service === 'labeling' && ! requiresBothTotalFiles . value && ( labelingMode . value === 'sku' ? ! skuTotalFile . value : ! cartonTotalFile . value )) {
ElMessage . warning ( labelingMode . value === 'sku' ? '请上传 SKU 标签总文件' : '请上传箱唛总文件' ); return
2026-09-20 16:55:27 +08:00
}
2026-09-10 15:34:03 +08:00
if ( props . service === 'labeling' && labelingMode . value !== 'sku' && ! form . deliveryMethod ) {
ElMessage . warning ( '请选择换标完成后的派送方式' ); return
}
2026-09-17 16:34:51 +08:00
if ( props . service === 'labeling' && labelingMode . value !== 'sku' && ! form . destination ) {
ElMessage . warning ( '请选择本张贴标申请统一使用的 FBA 目的仓' ); return
}
2026-10-14 17:41:04 +08:00
if ( props . service === 'labeling' && labelingMode . value !== 'sku' && form . deliveryMethod === 'EXPRESS'
2026-09-10 15:34:03 +08:00
&& form . relabel && ! shippingLabelFile . value ) {
ElMessage . warning ( '选择打印面单时必须上传面单文件' ); return
}
2026-09-10 17:12:52 +08:00
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cartonDetailRows . value . some (( item ) => ! item . shipmentNo || ! item . cartonMark || ! item . fbaCartonMark )) {
2026-09-06 15:53:56 +08:00
ElMessage . warning ( '请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整' ); return
}
2026-10-09 11:48:52 +08:00
if ( props . service === 'labeling' && labelingMode . value === 'carton' && cartonDetailRows . value . some (( item ) => ! item . returnCartonId && ! item . sourceInventoryPositionId )) {
2026-09-17 16:34:51 +08:00
ElMessage . warning ( '请选择有库存的历史入仓单' ); return
2026-08-21 17:20:51 +08:00
}
if ( props . service === 'disposal' && ! form . reason ) {
ElMessage . warning ( '请选择废弃原因' ); return
}
2026-10-29 15:49:59 +08:00
if ( props . service === 'disposal' && discardQuantityMode . value && ! form . remark . trim ()) {
ElMessage . warning ( '请填写废弃备注' ); return
}
2026-09-10 15:34:03 +08:00
if ( props . service === 'relabel' && ( ! form . destination || ! form . address || ! form . deliveryMethod || ! form . estimatedArrivalAt || ! form . fbaShipmentNo || form . volumeCbm <= 0 )) {
2026-08-21 17:20:51 +08:00
ElMessage . warning ( '请填写完整的派送信息、体积、预计到达时间和 FBA 货件号' ); return
}
2026-10-16 11:42:59 +08:00
if ( props . service === 'transfer' && ! inventoryTransfer . value && ( ! form . warehouse || ! form . deliveryMethod || ! form . address || ! form . estimatedArrivalAt || ( form . deliveryMethod === 'WAREHOUSE_PICKUP' && ( ! form . pickupContact || ! form . pickupPhone )))) {
2026-10-15 15:45:22 +08:00
const missing = [ ! form . warehouse && '海外仓' , ! form . deliveryMethod && '送货方式' , ! form . address && '地址' , form . deliveryMethod === 'WAREHOUSE_PICKUP' && ! form . pickupContact && '提货联系人' , form . deliveryMethod === 'WAREHOUSE_PICKUP' && ! form . pickupPhone && '联系人电话' , ! form . estimatedArrivalAt && '预计入仓时间' ]. filter ( Boolean ). join ( '、' )
2026-09-13 21:10:14 +08:00
ElMessage . warning ( `请填写: ${ missing || '必填信息' } ` ); return
2026-09-10 15:34:03 +08:00
}
2026-10-15 15:45:22 +08:00
if ( props . service === 'transfer' && ( ! cargoRows . value . length || cargoRows . value . some (( row ) =>
! Number . isSafeInteger ( row . quantity ) || row . quantity < 1 || ! Number . isSafeInteger ( row . expectedPackageCount ) || Number ( row . expectedPackageCount ) < 1 ))) {
ElMessage . warning ( '请填写每个 SKU 的数量(件)和预计箱数,均须为正整数' ); return
}
2026-10-16 11:42:59 +08:00
if ( inventoryTransfer . value && ( transferSourceLoading . value || transferSourceError . value )) { ElMessage . warning ( '请等待原单箱明细加载成功后再保存' ); return }
if ( inventoryTransfer . value && transferDestinationType . value === 'NONE' ) { ElMessage . warning ( '库存转运请选择 FBA 仓或私人仓' ); return }
2026-09-17 13:36:20 +08:00
if ( props . service === 'transfer' && transferDestinationType . value === 'FBA' && ! form . destination ) {
ElMessage . warning ( '请选择 FBA 目的仓' ); return
}
if ( props . service === 'transfer' && transferDestinationType . value === 'PRIVATE' && ( ! form . privateDestinationAddress || ! form . privateDestinationPostalCode || ! form . privateDestinationContact || ! form . privateDestinationPhone )) {
ElMessage . warning ( '请完整填写私人仓地址、邮编、联系人和电话' ); return
2026-09-10 15:34:03 +08:00
}
2026-09-13 21:10:14 +08:00
if ( props . service === 'transfer' && form . relabel ) {
2026-09-17 13:36:20 +08:00
if ( ! form . labelDeadline || ! form . relabelContact || ! form . relabelPhone || ( labelingMode . value !== 'sku' && ! form . relabelDeliveryMethod )) { ElMessage . warning ( '请填写换标截止时间、换标联系人和换标后派送方式' ); return }
2026-09-17 16:34:51 +08:00
if ( ! transferRelabelDefaultDestination . value ) { ElMessage . warning ( '请选择整票换标货物的 FBA 目的仓' ); return }
2026-09-20 17:53:41 +08:00
if ( action === 'submit' && requiresBothTotalFiles . value && ( ! cartonTotalFile . value || ! skuTotalFile . value )) { ElMessage . warning ( 'SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件' ); return }
if ( action === 'submit' && ! requiresBothTotalFiles . value && ! cartonTotalFile . value && ! skuTotalFile . value ) { ElMessage . warning ( '请上传箱唛总文件或 SKU 总文件(二选一)' ); return }
2026-09-13 21:10:14 +08:00
const relabelRows = transferRelabelRows . value
const allMarks = relabelRows . flatMap ( transferCustomerMarks )
2026-10-16 11:42:59 +08:00
if ( inventoryTransfer . value && allMarks . some ( mark => ! transferSourceCartons . value . some ( box => box . cartonMark === mark ))) {
ElMessage . error ( '当前箱子不存在或不属于当前订单' ); return
}
2026-09-13 21:10:14 +08:00
if ( ! relabelRows . length || relabelRows . some (( row ) => transferCustomerMarks ( row ). length !== row . quantity )) { ElMessage . warning ( '每个换标明细的客户箱唛数量必须与换标数量一致' ); return }
if ( new Set ( allMarks . map (( mark ) => mark . toUpperCase ())). size !== allMarks . length ) { ElMessage . warning ( '客户箱唛号不能重复' ); return }
if ( allMarks . join ( ',' ). length > 2000 || relabelRows . some (( row ) => transferCustomerMarks ( row ). join ( ',' ). length > 255 )) { ElMessage . warning ( '客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号' ); return }
2026-09-17 16:34:51 +08:00
if ( relabelRows . some (( row ) => row . quantity < 1 || ( labelingMode . value !== 'carton' && ( ! row . sku . trim () || ! row . newSkuCode . trim ())) || ( labelingMode . value !== 'sku' && ! row . fbaCartonMark . trim ()))) { ElMessage . warning ( '请按当前换标类型填写完整明细' ); return }
2026-09-17 13:36:20 +08:00
}
if ( props . service !== 'transfer' || action === 'submit' ) {
try { await ElMessageBox . confirm ( `确认提交 ${ config . value . title } 吗?` , '提交确认' , { confirmButtonText : '提交' , cancelButtonText : '取消' }) } catch { return }
2026-09-13 21:10:14 +08:00
}
2026-08-21 17:20:51 +08:00
saving . value = true
try {
2026-09-24 10:11:16 +08:00
const idempotencyKey = createIdempotencyKey ()
2026-08-21 17:20:51 +08:00
if ( props . service === 'labeling' ) {
2026-09-08 20:28:05 +08:00
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'
2026-09-17 16:34:51 +08:00
? cartonDetailRows . value . map (( item ) => ({ sourceInventoryBalanceId : item . sourceInventoryBalanceId , sourceInventoryPositionId : item . sourceInventoryPositionId , sourceInboundOrderId : item . sourceInboundOrderId , returnCartonId : item . returnCartonId , 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 : form . destination , operationType : 'CARTON_LABEL' as const , skuItems : [], remark : item . remark || undefined }))
2026-09-08 20:28:05 +08:00
: buildSkuRelabelItems ()
2026-10-09 15:25:27 +08:00
const selectedOperationType = labelingMode . value === 'carton' ? 'CARTON_LABEL' : labelingMode . value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
if ( items . some ( item => item . operationType !== selectedOperationType )) throw new Error ( '一张申请只能选择一种换标类型,请核对绑定明细' )
2026-09-14 16:51:43 +08:00
const created = await createRelabel ({ customerOrderNo : sourceInboundOrders || '贴标申请' , operationWarehouseId : form . warehouse , labelDeadline : form . labelDeadline , contactName : form . contact , contactPhone : form . phone , remark : form . remark || undefined , idempotencyKey , items ,
2026-10-14 17:41:04 +08:00
deliveryMethod : labelingMode . value === 'sku' ? undefined : form . deliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' ,
printShippingLabel : labelingMode . value !== 'sku' && form . deliveryMethod === 'EXPRESS' && form . relabel })
2026-09-17 13:36:20 +08:00
try {
if ( cartonTotalFile . value ) await uploadRelabelFile ( created . application . relabelNo , 'CARTON_TOTAL' , cartonTotalFile . value )
if ( skuTotalFile . value ) await uploadRelabelFile ( created . application . relabelNo , 'SKU_TOTAL' , skuTotalFile . value )
if ( shippingLabelFile . value ) await uploadRelabelShippingLabel ( created . application . relabelNo , shippingLabelFile . value )
} catch ( error ) {
2026-09-22 08:47:31 +08:00
// Creation already committed: return to the list even when an attachment still needs retrying.
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-17 13:36:20 +08:00
await ElMessageBox . alert ( `申请 ${ created . application . relabelNo } 已创建,部分附件上传失败: ${ error instanceof Error ? error . message : '上传失败' } 。请在原单详情补传失败附件,不要重复下单。` , '申请已创建,附件待补传' , { confirmButtonText : '知道了' })
return
}
2026-09-18 16:48:35 +08:00
ElMessage . success ( ` ${ config . value . title } 提交成功` )
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-18 16:48:35 +08:00
return
2026-08-21 17:20:51 +08:00
} else if ( props . service === 'disposal' ) {
const items = cargoRows . value . filter (( item ) => item . quantity > 0 ). map (( item ) => {
2026-10-20 14:00:59 +08:00
const belongs = discardSelectionSources . value . some ( source => item . sourceType === 'INVENTORY'
? !! item . sourceInventoryPositionId && source . inventoryPositionId === item . sourceInventoryPositionId
: item . sourceType === 'RETURN_CARTON' ? !! item . returnCartonId && source . returnCartonId === item . returnCartonId
: !! item . sourceOrderId && source . orderId === item . sourceOrderId )
if ( ! belongs || item . quantity > Number ( item . availableQuantity || 0 )) {
2026-08-21 17:20:51 +08:00
throw new Error ( `SKU ${ item . sku } 的可废弃库存不足` )
}
2026-10-14 17:41:04 +08:00
return { orderId : item . sourceType === 'DROPSHIP' ? item . sourceOrderId : undefined ,
inventoryPositionId : item . sourceType === 'INVENTORY' ? item . sourceInventoryPositionId : undefined ,
2026-09-28 17:26:37 +08:00
returnCartonId : item . sourceType === 'RETURN_CARTON' ? item . returnCartonId : undefined ,
2026-10-14 17:41:04 +08:00
skuCode : item . sku , discardPackages : item . quantity , cartonIds : item . discardCartonIds }
2026-08-21 17:20:51 +08:00
})
2026-10-29 15:49:59 +08:00
await createDiscard ({ items , idempotencyKey , requestedPackages : discardQuantityMode . value ? discardRequestedPackages . value : undefined , remark : [ form . reason , form . remark ]. filter ( Boolean ). join ( '; ' ) || undefined })
2026-08-21 17:20:51 +08:00
} else {
2026-10-15 15:45:22 +08:00
const packageCount = cargoRows . value . reduce (( sum , item ) => sum + Number ( item . expectedPackageCount || 0 ), 0 )
2026-09-13 21:10:14 +08:00
const customerCartonMarks = ( form . relabel ? transferRelabelRows . value : cargoRows . value ). flatMap ( transferCustomerMarks )
const transferRemark = [ form . remark , form . relabel && form . requirement ? `换标要求: ${ form . requirement } ` : '' ]. filter ( Boolean ). join ( '; ' )
const operationType = labelingMode . value === 'carton' ? 'CARTON_LABEL' : labelingMode . value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
2026-09-17 13:36:20 +08:00
const selectedFba = fbaWarehouses . value . find (( warehouse ) => warehouse . warehouseCode === form . destination )
2026-10-16 17:43:42 +08:00
const payload : TransferCreate = { operatingWarehouseId : primaryWarehouse , destinationType : transferDestinationType . value , fbaWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseId : transferDestinationType . value === 'FBA' ? String ( selectedFba ? . id || '' ) || undefined : undefined , destinationWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseName : transferDestinationType . value === 'FBA' ? selectedFba ? . warehouseName : undefined , privateDestinationAddress : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationAddress : undefined , privateDestinationPostalCode : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPostalCode : undefined , privateDestinationContactName : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationContact : undefined , privateDestinationContactPhone : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPhone : undefined , deliveryAddress : inventoryTransfer . value ? undefined : form . address , packageCount , volumeCbm : 0 , deliveryMethod : inventoryTransfer . value ? undefined : form . deliveryMethod , estimatedArrivalAt : inventoryTransfer . value ? undefined : form . estimatedArrivalAt , estimatedShipAt : form . estimatedShipAt || undefined , fbaShipmentNo : transferDestinationType . value === 'FBA' ? form . fbaShipmentNo || undefined : undefined , trackingNo : ! inventoryTransfer . value && form . deliveryMethod === 'CUSTOMER_DELIVERY' ? form . trackingNo || undefined : undefined , fbaBoxNos : customerCartonMarks . length ? customerCartonMarks . join ( ',' ) : undefined , remark : transferRemark || undefined , needRelabel : props . service === 'relabel' || form . relabel , labelDeadline : form . relabel ? form . labelDeadline : undefined , pickupContactName : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupContact : undefined , pickupContactPhone : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupPhone : undefined , relabelContactName : form . relabel ? form . relabelContact : undefined , relabelContactPhone : form . relabel ? form . relabelPhone : undefined , relabelDeliveryMethod : form . relabel && labelingMode . value !== 'sku' ? form . relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined , idempotencyKey , items : cargoRows . value . map (( item ) => ({ skuCode : item . sku , productNameZh : item . name , quantity : item . quantity , packageCount : Number ( item . expectedPackageCount ), shippingMark : transferCustomerMarks ( item ). join ( ',' ) || undefined , cartonMarks : item . generatedCartonMarks ? . slice ( 0 , Number ( item . expectedPackageCount )) })), relabelItems : form . relabel ? transferRelabelRows . value . map (( item ) => ({ operationType , customerCartonMarks : transferCustomerMarks ( item ), originalSku : item . sku , newSku : item . newSkuCode || undefined , quantity : item . quantity , fbaCartonMark : item . fbaCartonMark || undefined , destinationWarehouseCode : transferRelabelDefaultDestination . value || undefined , remark : item . remark || undefined })) : undefined }
2026-09-23 09:06:20 +08:00
if ( props . service === 'transfer' && selectedTransferInventory . value ) {
;( payload as TransferCreate ). inboundCode = selectedTransferInventory . value
;( payload as TransferCreate ). sourceLastlegOrderIds = selectedTransferSourceOrderIds . value
}
2026-09-17 13:36:20 +08:00
const created = editingTransferNo . value
? await updateTransfer ( editingTransferNo . value , payload )
: await createTransfer ( payload )
const transferNo = String ( created . transferNo || '' )
if ( ! transferNo ) throw new Error ( '转运草稿已保存,但未返回转运单号' )
if ( action === 'submit' ) {
2026-09-20 17:53:41 +08:00
const submitted = await submitTransfer ( transferNo )
if ( form . relabel ) {
const relabelNo = String ( submitted . relabelNo || created . relabelNo || '' )
2026-09-23 11:25:35 +08:00
if ( ! relabelNo ) {
ElMessage . success ( '转运单已提交审核;PDA 入库确认后将自动生成关联换标单' )
await ElMessageBox . alert ( `转运单 ${ transferNo } 已提交。关联换标单会在 PDA 完成入库、客户确认实测库存后自动生成;届时请进入关联换标单详情上传贴标总文件。` , '转运单已提交' , { confirmButtonText : '知道了' })
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-23 11:25:35 +08:00
return
}
2026-09-20 17:53:41 +08:00
try {
if ( cartonTotalFile . value ) await uploadRelabelFile ( relabelNo , 'CARTON_TOTAL' , cartonTotalFile . value )
if ( skuTotalFile . value ) await uploadRelabelFile ( relabelNo , 'SKU_TOTAL' , skuTotalFile . value )
} catch ( error ) {
await ElMessageBox . alert ( `转运单 ${ transferNo } 已提交,但换标总文件上传失败: ${ error instanceof Error ? error . message : '上传失败' } 。请进入关联换标单详情补传,不要重复提交转运单。` , '转运单已提交,附件待补传' , { confirmButtonText : '知道了' })
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-20 17:53:41 +08:00
return
}
}
2026-09-17 13:36:20 +08:00
ElMessage . success ( '转运单已提交审核' )
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-17 13:36:20 +08:00
return
}
2026-08-21 17:20:51 +08:00
}
2026-09-18 16:48:35 +08:00
ElMessage . success ( props . service === 'transfer' ? '转运单草稿保存成功' : ` ${ config . value . title } 提交成功` )
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-10-16 17:43:42 +08:00
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '提交失败' ) }
2026-08-21 17:20:51 +08:00
finally { saving . value = false }
}
async function openDetail ( value : Record < string , string >) {
2026-09-18 16:48:35 +08:00
if ([ 'labeling' , 'transfer' , 'disposal' ]. includes ( props . service ) && ! props . detailPage ) {
const routeName = ({ labeling : 'overseas-labeling-detail' , transfer : 'overseas-storage-transfer-detail' , disposal : 'overseas-cargo-disposal-detail' } as Partial < Record < ServiceType , string > > )[ props . service ]
if ( routeName ) void router . push ({ name : routeName , params : { detailNo : value [ '0' ] } })
return
}
detailError . value = ''
2026-10-20 11:54:44 +08:00
if ( props . service === 'labeling' ) selectedRelabelQuantity . value = null
2026-08-21 17:20:51 +08:00
try {
const detail = props . service === 'labeling' ? await getRelabel ( value [ '0' ]) : props . service === 'disposal' ? await getDiscard ( value [ '0' ]) : await getTransfer ( value [ '0' ])
2026-09-14 16:51:43 +08:00
const discardSummary = props . service === 'disposal' ? ( detail as { summary ?: ServiceDetail }). summary : undefined
2026-10-15 15:45:22 +08:00
showTransferCartons . value = false
2026-09-14 16:51:43 +08:00
selectedDetail . value = { ... value , ...( detail as ServiceDetail ), ...( discardSummary || {}) }
2026-09-06 18:11:42 +08:00
selectedRelabelDetail . value = props . service === 'labeling' ? detail as RelabelDetail : null
2026-10-20 11:54:44 +08:00
if ( props . service === 'labeling' ) {
const quantities = await getRelabelQuantities ({ relabelNo : value [ '0' ] }). catch (() => null )
if ( selectedRelabelDetail . value ? . application . relabelNo === value [ '0' ]) selectedRelabelQuantity . value = quantities ? . orders [ value [ '0' ]] || null
}
2026-09-06 18:11:42 +08:00
mode . value = 'detail'
2026-09-15 15:20:35 +08:00
if ( props . service === 'labeling' ) void loadCompletionPhotos ()
2026-09-17 13:36:20 +08:00
if ( props . service === 'transfer' ) void loadMeasurementPhotos ()
2026-10-14 11:43:38 +08:00
if ( props . service === 'disposal' ) void loadDiscardPhotos ()
2026-09-18 16:48:35 +08:00
} catch ( error ) { selectedDetail . value = null ; selectedRelabelDetail . value = null ; detailError . value = error instanceof Error ? error . message : '详情加载失败' ; mode . value = 'detail' ; ElMessage . error ( detailError . value ) }
}
async function loadDetailPage () {
await openDetail ({ '0' : detailNumber . value })
2026-09-23 11:25:35 +08:00
await loadWarehouses ()
2026-08-21 17:20:51 +08:00
}
2026-09-13 21:10:14 +08:00
async function confirmTransferMeasurements () {
const transferNo = String ( selectedDetail . value ? . transferNo || selectedDetail . value ? .[ '0' ] || '' )
if ( ! transferNo ) return
2026-10-16 11:42:59 +08:00
openCompletionConfirmation ( transferNo , 'transfer' )
2026-09-13 21:10:14 +08:00
}
2026-09-17 13:36:20 +08:00
async function printTransferCartons ( transferNo : string ) {
2026-09-13 21:10:14 +08:00
if ( props . service !== 'transfer' ) return
2026-09-17 13:36:20 +08:00
const printWindow = window . open ( '' , '_blank' )
if ( ! printWindow ) { ElMessage . warning ( '浏览器阻止了打印窗口,请允许弹窗后重试' ); return }
printWindow . opener = null
2026-09-13 21:10:14 +08:00
try {
2026-09-17 13:36:20 +08:00
if ( ! transferNo ) { printWindow . close (); return }
2026-09-13 21:10:14 +08:00
const label = await getTransferCartonLabel ( transferNo )
2026-09-17 13:36:20 +08:00
if ( ! label . cartons . length ) { printWindow . close (); ElMessage . warning ( '暂无可打印的箱唛' ); return }
printWindow . document . open (); printWindow . document . write ( label . printableHtml ); printWindow . document . close (); printWindow . focus (); printWindow . print ()
} catch ( error ) { printWindow . close (); ElMessage . error ( error instanceof Error ? error . message : '箱唛加载失败' ) }
2026-09-13 21:10:14 +08:00
}
2026-09-15 15:20:35 +08:00
function clearCompletionPhotos () {
for ( const url of Object . values ( completionPhotoUrls )) URL . revokeObjectURL ( url )
for ( const key of Object . keys ( completionPhotoUrls )) delete completionPhotoUrls [ key ]
for ( const key of Object . keys ( completionPhotoLoading )) delete completionPhotoLoading [ key ]
for ( const key of Object . keys ( completionPhotoErrors )) delete completionPhotoErrors [ key ]
2026-09-10 15:34:03 +08:00
}
2026-09-17 13:36:20 +08:00
async function confirmTransferFromList ( transferNo : string ) {
2026-10-16 11:42:59 +08:00
openCompletionConfirmation ( transferNo , 'transfer' )
2026-09-17 13:36:20 +08:00
}
2026-09-15 15:20:35 +08:00
async function loadCompletionPhoto ( fileId : string ) {
2026-09-10 15:34:03 +08:00
const relabelNo = selectedRelabelDetail . value ? . application . relabelNo
2026-09-15 15:20:35 +08:00
if ( ! relabelNo || completionPhotoLoading [ fileId ]) return
completionPhotoLoading [ fileId ] = true
completionPhotoErrors [ fileId ] = false
2026-09-10 15:34:03 +08:00
try {
const blob = await downloadRelabelCompletionPhoto ( relabelNo , fileId )
2026-09-15 15:20:35 +08:00
if ( selectedRelabelDetail . value ? . application . relabelNo !== relabelNo ) return
if ( completionPhotoUrls [ fileId ]) URL . revokeObjectURL ( completionPhotoUrls [ fileId ])
2026-09-10 15:34:03 +08:00
const url = URL . createObjectURL ( blob )
2026-09-15 15:20:35 +08:00
completionPhotoUrls [ fileId ] = url
} catch { if ( selectedRelabelDetail . value ? . application . relabelNo === relabelNo ) completionPhotoErrors [ fileId ] = true }
finally { completionPhotoLoading [ fileId ] = false }
}
async function loadCompletionPhotos () {
clearCompletionPhotos ()
await Promise . all (( selectedRelabelDetail . value ? . completionEvidence ? . photoFileIds || []). map ( loadCompletionPhoto ))
}
2026-09-17 13:36:20 +08:00
function clearMeasurementPhotos () {
for ( const url of Object . values ( measurementPhotoUrls )) URL . revokeObjectURL ( url )
for ( const key of Object . keys ( measurementPhotoUrls )) delete measurementPhotoUrls [ key ]
for ( const key of Object . keys ( measurementPhotoLoading )) delete measurementPhotoLoading [ key ]
for ( const key of Object . keys ( measurementPhotoErrors )) delete measurementPhotoErrors [ key ]
}
async function loadMeasurementPhoto ( fileId : string | number ) {
const transferNo = selectedDetail . value ? . transferNo
if ( ! transferNo || measurementPhotoLoading [ fileId ]) return
measurementPhotoLoading [ fileId ] = true
measurementPhotoErrors [ fileId ] = false
try {
const blob = await downloadTransferMeasurementPhoto ( transferNo , fileId )
if ( mode . value !== 'detail' || props . service !== 'transfer' || selectedDetail . value ? . transferNo !== transferNo ) return
if ( measurementPhotoUrls [ fileId ]) URL . revokeObjectURL ( measurementPhotoUrls [ fileId ])
measurementPhotoUrls [ fileId ] = URL . createObjectURL ( blob )
} catch { if ( mode . value === 'detail' && props . service === 'transfer' && selectedDetail . value ? . transferNo === transferNo ) measurementPhotoErrors [ fileId ] = true }
finally { measurementPhotoLoading [ fileId ] = false }
}
async function loadMeasurementPhotos () {
clearMeasurementPhotos ()
await Promise . all (( selectedDetail . value ? . measurementPhotos || []). map (( photo ) => loadMeasurementPhoto ( photo . fileId )))
}
2026-09-15 15:20:35 +08:00
async function confirmRelabelFromList ( relabelNo : string ) {
2026-10-15 15:45:22 +08:00
openCompletionConfirmation ( relabelNo , 'labeling' )
2026-09-10 15:34:03 +08:00
}
2026-09-14 16:51:43 +08:00
async function cancelSelectedDiscard () {
const no = String ( selectedDetail . value ? . discardNo || selectedDetail . value ? .[ '0' ] || '' )
if ( ! no ) return
try { await ElMessageBox . confirm ( '确认取消该废弃申请吗?' , '取消确认' , { confirmButtonText : '取消申请' , cancelButtonText : '返回' }); await cancelDiscard ( no ); ElMessage . success ( '废弃申请已取消' ); mode . value = 'list' ; await load () }
catch ( error ) { if ( error !== 'cancel' ) ElMessage . error ( error instanceof Error ? error . message : '取消失败' ) }
}
function chooseRelabelFile ( scope : 'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM' , itemId ?: string ) {
relabelFileScope . value = scope ; relabelFileItemId . value = itemId ; relabelFileInput . value ? . click ()
}
async function uploadSelectedRelabelFile ( event : Event ) {
const input = event . target as HTMLInputElement ; const file = input . files ? .[ 0 ]; input . value = ''
const relabelNo = selectedRelabelDetail . value ? . application . relabelNo
if ( ! file || ! relabelNo ) return
try {
selectedRelabelDetail . value = await uploadRelabelFile ( relabelNo , relabelFileScope . value , file , relabelFileItemId . value )
ElMessage . success ( '文件上传成功' )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '文件上传失败' ) }
}
2026-09-08 20:28:05 +08:00
async function loadWarehouses () {
fbaWarehousesLoading . value = true
fbaWarehousesError . value = ''
2026-09-13 21:10:14 +08:00
operationWarehousesError . value = ''
2026-09-08 20:28:05 +08:00
try {
2026-09-13 21:10:14 +08:00
const [ fbaResult , operationResult ] = await Promise . allSettled ([
2026-09-14 12:01:13 +08:00
loadAllWarehouseRecords ( 'FBA' , getMerchantWarehouses ),
2026-09-14 16:51:43 +08:00
loadAllWarehouseRecords ( 'OVERSEAS' , getMerchantWarehouses ),
2026-09-08 20:28:05 +08:00
])
2026-09-14 12:01:13 +08:00
fbaWarehouses . value = fbaResult . status === 'fulfilled' ? fbaResult . value . filter (( item ) => item . status === 'ACTIVE' && item . warehouseType === 'FBA' ) : []
operationWarehouses . value = operationResult . status === 'fulfilled'
? operationResult . value . filter (( item ) => item . status === 'ACTIVE' && [ 'OVERSEAS' , 'OVERSEAS_PARTNER' ]. includes ( item . warehouseType ))
: []
if ( fbaResult . status === 'rejected' ) fbaWarehousesError . value = 'FBA 目的仓候选加载失败,请检查仓库主数据或权限'
2026-09-13 21:10:14 +08:00
if ( operationResult . status === 'rejected' ) operationWarehousesError . value = '海外仓候选加载失败,请检查仓库主数据或数据库迁移'
2026-09-08 20:28:05 +08:00
} 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 } ` }
2026-09-28 17:26:37 +08:00
async function loadTransferInventoryCandidates ( warehouseId : string ) {
transferInventoryCandidates . value = []
2026-10-19 14:53:57 +08:00
transferAvailableSourceCounts . value = {}
2026-09-28 17:26:37 +08:00
if ( ! warehouseId ) return
relabelCandidatesLoading . value = true
try {
2026-10-16 13:58:08 +08:00
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
2026-10-19 14:30:52 +08:00
transferInventoryCandidates . value = ( await listRelabelInventoryCandidates ( warehouseId )). filter ( isUnlabelledTransferCandidate ). filter ( item =>
2026-10-16 13:58:08 +08:00
item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? Number ( item . cartonAvailablePackages || 0 ) > 0
: Number ( item . availablePackages || 0 ) > 0 )
2026-10-19 14:53:57 +08:00
const sourceValues = [... new Set ( transferInventoryCandidates . value . map ( item => {
const returnNo = String ( item . inboundOrderNo || '' ). trim ()
return item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? ( returnNo ? `RETURN_ORDER: ${ returnNo } ` : '' ) : String ( item . inboundCode || '' )
}). filter ( Boolean ))]
const results = await Promise . all ( sourceValues . map ( async ( value ) => {
try {
const cartons = await getTransferSourceCartons ( warehouseId , value )
return [ value , cartons . length ] as const
} catch { return [ value , 0 ] as const }
}))
transferAvailableSourceCounts . value = Object . fromEntries ( results )
2026-09-28 17:26:37 +08:00
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '当前海外仓库存来源加载失败' )
} finally { relabelCandidatesLoading . value = false }
}
2026-09-10 15:34:03 +08:00
async function selectTransferWarehouse () {
selectedOperationWarehouse . value = null
2026-09-17 13:36:20 +08:00
form . pickupContact = ''
form . pickupPhone = ''
form . relabelContact = ''
form . relabelPhone = ''
2026-09-13 21:10:14 +08:00
if ( form . deliveryMethod === 'CUSTOMER_DELIVERY' ) form . address = ''
2026-09-10 15:34:03 +08:00
if ( ! form . warehouse ) return
operationWarehouseDetailLoading . value = true
try {
const detail = await getMerchantWarehouseDetail ( form . warehouse )
selectedOperationWarehouse . value = detail
2026-09-17 13:36:20 +08:00
form . relabelContact = detail . warehouse . contactName || ''
form . relabelPhone = detail . warehouse . contactPhone || ''
2026-09-10 15:34:03 +08:00
if ( form . deliveryMethod === 'CUSTOMER_DELIVERY' ) form . address = operationWarehouseAddress . value
2026-09-28 17:26:37 +08:00
await loadTransferInventoryCandidates ( form . warehouse )
2026-09-10 15:34:03 +08:00
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '中转仓联系信息加载失败' )
} finally { operationWarehouseDetailLoading . value = false }
}
2026-09-23 11:25:35 +08:00
async function confirmSelectedRelabel () {
const relabelNo = selectedRelabelDetail . value ? . application . relabelNo
2026-10-15 15:45:22 +08:00
if ( relabelNo ) openCompletionConfirmation ( relabelNo , 'labeling' )
2026-09-23 11:25:35 +08:00
}
async function resubmitRejectedRelabel () {
const application = selectedRelabelDetail . value ? . application
if ( ! application || application . status !== 'REJECTED' ) return
try {
const { value } = await ElMessageBox . prompt ( '可先在下方替换贴标文件,再补充本次修改说明。' , '修改后重新提交审核' , {
inputType : 'textarea' , inputValue : selectedRelabelDetail . value ? . remark || '' ,
inputPlaceholder : '修改说明(选填)' , confirmButtonText : '重新提交' , cancelButtonText : '取消' ,
})
selectedRelabelDetail . value = await resubmitRelabel ( application . relabelNo , {
labelDeadline : application . labelDeadline || '' , contactName : application . contactName || '' ,
contactPhone : application . contactPhone || '' , remark : value ? . trim () || undefined ,
2026-09-24 10:11:16 +08:00
idempotencyKey : createIdempotencyKey (),
2026-09-23 11:25:35 +08:00
})
ElMessage . success ( '贴标单已重新提交审核' )
2026-10-15 18:25:32 +08:00
await goToServiceList ()
2026-09-23 11:25:35 +08:00
} catch ( error ) {
if ( error !== 'cancel' && error !== 'close' ) ElMessage . error ( error instanceof Error ? error . message : '重新提交失败' )
}
}
2026-09-23 09:06:20 +08:00
const transferInventoryOptions = computed (() => {
const grouped = new Map < string , { value : string ; label : string ; packages : number }>()
for ( const item of transferInventoryCandidates . value ) {
2026-10-16 13:58:08 +08:00
// 退货库存没有普通入仓码,使用带前缀的退货来源编码,避免提交时被当作普通入仓单查询。
const returnNo = String ( item . inboundOrderNo || '' ). trim ()
const value = item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? ( returnNo ? `RETURN_ORDER: ${ returnNo } ` : '' )
: String ( item . inboundCode || '' )
2026-09-23 09:06:20 +08:00
if ( ! value ) continue
2026-10-16 13:58:08 +08:00
const packages = item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? inventoryQuantity ( item . cartonAvailablePackages || 0 )
: inventoryQuantity ( item . availablePackages )
2026-09-23 09:06:20 +08:00
const current = grouped . get ( value )
if ( current ) current . packages += packages
2026-10-19 14:53:57 +08:00
else if (( transferAvailableSourceCounts . value [ value ] ?? 1 ) > 0 ) grouped . set ( value , { value , label : value . startsWith ( 'RETURN_ORDER:' ) ? `退货单| ${ returnNo } ` : ` ${ item . inboundOrderNo || value } | ${ value } ` , packages })
2026-09-23 09:06:20 +08:00
}
return [... grouped . values ()]
})
2026-10-19 14:30:52 +08:00
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
function isUnlabelledTransferCandidate ( item : RelabelInventoryCandidate ) {
if ( item . sourceType !== 'RETURN_CARTON' && item . sourceType !== 'RETURN_PARCEL' ) return true
return ! [ 'COMPLETED' , 'PARTIALLY_COMPLETED' ]. includes ( item . relabelStatus || '' )
&& inventoryQuantity ( item . relabelledQuantity || item . relabelledPackages ) === 0
}
2026-09-23 09:06:20 +08:00
async function selectTransferInventory ( value : string ) {
selectedTransferInventory . value = value || ''
selectedTransferSourceOrderIds . value = []
2026-10-16 11:42:59 +08:00
transferSourceCartons . value = []; transferSourceError . value = '' ; ++ transferSourceRequest
if ( ! value ) { form . inboundNo = '' ; cargoRows . value = [ createCargoRow ()]; transferSourceLoading . value = false ; return }
if ( transferDestinationType . value === 'NONE' ) transferDestinationType . value = 'FBA'
void loadTransferSourceCartons ()
2026-09-23 09:06:20 +08:00
form . inboundNo = value
// The inventory-candidates response already contains the merchant-visible
// SKU/name/package data. Do not call the platform-only last-leg orders API
// here: merchants are not authorized for that endpoint (A0403), and the
// transfer service can derive source orders from inboundCode when IDs are
// omitted. Aggregate candidates for the selected inbound into cargo rows.
2026-10-16 13:58:08 +08:00
const isReturnSource = value . startsWith ( 'RETURN_ORDER:' )
const returnNo = isReturnSource ? value . slice ( 'RETURN_ORDER:' . length ) : ''
const candidates = transferInventoryCandidates . value . filter (( item ) => isReturnSource
? ( item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL' ) && String ( item . inboundOrderNo || '' ) === returnNo
: String ( item . inboundCode || '' ) === value )
2026-09-23 09:06:20 +08:00
const grouped = new Map < string , CargoRow >()
for ( const candidate of candidates ) {
2026-10-16 13:58:08 +08:00
const candidateRows = isReturnSource
? ( candidate . cartonSkuItems ? . length ? candidate . cartonSkuItems : candidate . skuItems || []). map ( item => ({ sku : item . skuCode , name : candidate . productNameZh || '' , quantity : inventoryQuantity ( item . quantity ), packageCount : inventoryQuantity ( candidate . cartonAvailablePackages || 0 ) }))
: [{ sku : String ( candidate . skuCode || '' ), name : candidate . productNameZh || '' , quantity : inventoryQuantity ( candidate . availablePackages ), packageCount : inventoryQuantity ( candidate . availablePackages ) }]
for ( const row of candidateRows ) {
if ( ! row . sku ) continue
const current = grouped . get ( row . sku )
if ( current ) { current . quantity += row . quantity ; current . expectedPackageCount = Number ( current . expectedPackageCount || 0 ) + row . packageCount ; current . availableQuantity = Number ( current . availableQuantity || 0 ) + row . quantity }
else grouped . set ( row . sku , { ... createCargoRow (), sku : row . sku , name : row . name , quantity : row . quantity , expectedPackageCount : row . packageCount , availableQuantity : row . quantity })
}
2026-09-23 09:06:20 +08:00
}
cargoRows . value = [... grouped . values ()]
try {
2026-10-16 13:58:08 +08:00
if ( isReturnSource ) return
2026-09-23 09:06:20 +08:00
const historical = await listRelabelHistoricalOrders ( form . warehouse , value )
2026-09-28 17:26:37 +08:00
const sourceIds = historical . map (( item ) => String ( item . id || '' )). filter ( Boolean )
2026-10-16 11:42:59 +08:00
if ( selectedTransferInventory . value === value ) selectedTransferSourceOrderIds . value = sourceIds
2026-09-23 09:06:20 +08:00
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '库存来源订单加载失败' )
}
}
2026-09-10 15:34:03 +08:00
function selectTransferInboundMethod ( value : string | number | boolean | undefined ) {
form . address = value === 'CUSTOMER_DELIVERY' ? operationWarehouseAddress . value : ''
}
2026-09-17 13:36:20 +08:00
function selectTransferDestinationType ( value : string | number | boolean | undefined ) {
if ( value !== 'FBA' ) { form . destination = '' ; form . fbaShipmentNo = '' }
if ( value !== 'PRIVATE' ) { form . privateDestinationAddress = '' ; form . privateDestinationPostalCode = '' ; form . privateDestinationContact = '' ; form . privateDestinationPhone = '' }
2026-09-10 15:34:03 +08:00
}
2026-09-08 20:28:05 +08:00
async function selectOperationWarehouse () {
cargoRows . value = []
2026-09-13 21:10:14 +08:00
transferRelabelRows . value = []
2026-09-08 20:28:05 +08:00
skuRelabelRows . value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings . value = []
2026-09-08 20:28:05 +08:00
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 ) {
2026-09-17 16:34:51 +08:00
const rows : CargoRow [] = []
2026-10-13 14:46:59 +08:00
const selectedCandidates = currentRelabelCandidates . value . filter (( item ) => {
2026-10-12 13:43:29 +08:00
return item . returnCartonId ? returnSourceNos ( item ). includes ( inboundOrderId . slice ( 'RETURN_ORDER:' . length )) : String ( item . inboundOrderId ) === inboundOrderId
2026-09-20 16:55:27 +08:00
})
if ( inboundOrderId . startsWith ( 'RETURN_ORDER:' )) {
2026-10-09 11:48:52 +08:00
for ( const candidate of selectedCandidates ) rows . push ({
id : ++ cargoSequence , sku : candidate . skuItems ? .[ 0 ] ? . skuCode || candidate . skuCode || '' ,
2026-10-12 14:33:17 +08:00
name : candidate . productNameZh || '' , included : false , quantity : 1 , availableQuantity : 1 ,
2026-10-09 11:48:52 +08:00
returnCartonId : String ( candidate . returnCartonId ), sourceInboundOrderNo : candidate . inboundOrderNo ,
fnsku : '' , shipmentNo : candidate . inboundOrderNo || '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' ,
cartonMark : candidate . cartonMark || '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' ,
})
2026-09-20 16:55:27 +08:00
} else for ( const candidate of selectedCandidates ) {
for ( const position of candidate . positions ) rows . push (... Array . from ({ length : Math . min ( inventoryQuantity ( position . availablePackages ), 500 ) }, () => ({
2026-09-10 15:34:03 +08:00
id : ++ cargoSequence , sku : candidate . skuCode || '' , name : candidate . productNameZh || '库存货物' ,
2026-09-10 17:12:52 +08:00
included : false , quantity : 1 , availableQuantity : 1 ,
2026-09-08 20:28:05 +08:00
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 ,
2026-09-13 21:10:14 +08:00
fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , cartonMark : '' , fbaCartonMark : '' ,
2026-09-10 15:34:03 +08:00
destinationWarehouseId : candidate . destinationFbaWarehouseCode || '' , remark : '' ,
2026-09-17 16:34:51 +08:00
})))
}
cargoRows . value = rows . slice ( 0 , 500 )
2026-10-09 11:48:52 +08:00
if ( cargoRows . value . length && ! inboundOrderId . startsWith ( 'RETURN_ORDER:' )) cargoRows . value [ 0 ]. included = true
2026-09-08 20:28:05 +08:00
skuRelabelRows . value = []
2026-09-10 17:12:52 +08:00
importedRelabelBindings . value = []
2026-09-10 15:34:03 +08:00
shippingLabelFile . value = null
2026-09-14 16:51:43 +08:00
cartonTotalFile . value = null
skuTotalFile . value = null
2026-09-20 16:55:27 +08:00
if ( ! isReturnRelabelSource . value && labelingMode . value !== 'carton' ) addSkuRelabelRow ()
2026-09-08 20:28:05 +08:00
}
2026-10-09 11:48:52 +08:00
function addReturnSkuBinding ( source : RelabelInventoryCandidate , skuCode : string ) {
const sku = source . skuItems ? . find ( item => item . skuCode === skuCode )
if ( ! sku ) return
const row = skuRelabelRows . value . find ( item => item . cartonMark === source . cartonMark && item . originalSku === skuCode )
if ( row ) return
const sourceRow = cargoRows . value . find ( item => item . returnCartonId === String ( source . returnCartonId ))
2026-10-12 21:48:28 +08:00
skuRelabelRows . value . push ({ id : ++ skuRelabelSequence , sourceRowId : sourceRow ? . id || null , cartonMark : source . cartonMark || '' , originalSku : skuCode , newSku : '' , quantity : Math . max ( 1 , Number ( sku . quantity )), fbaCartonMark : '' , shipmentNo : '' , destinationWarehouseId : '' , remark : '' })
2026-10-09 11:48:52 +08:00
}
function addReturnBoxBinding ( source : RelabelInventoryCandidate ) {
if ( labelingMode . value !== 'carton' ) {
for ( const sku of source . skuItems || []) addReturnSkuBinding ( source , sku . skuCode )
return
}
if ( cartonDetailRows . value . some ( item => item . cartonMark === source . cartonMark )) return
2026-10-15 15:45:22 +08:00
cargoRows . value . push ({ id : ++ cargoSequence , included : true , sku : source . skuItems ? . map ( item => item . skuCode ). join ( ',' ) || '' , name : source . productNameZh || '' , shipmentNo : '' , cartonMark : source . cartonMark || '' , fbaCartonMark : '' , remark : '' , sourceInventoryBalanceId : undefined , sourceInventoryPositionId : undefined , sourceInboundOrderId : undefined , returnCartonId : String ( source . returnCartonId ), quantity : 1 , expectedPackageCount : 1 , fnsku : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , destinationWarehouseId : '' })
2026-10-09 11:48:52 +08:00
}
2026-08-21 17:20:51 +08:00
</ script >
< template >
2026-10-29 16:13:22 +08:00
< section class = "warehouse-service-page" : class = "{ 'warehouse-create client-ui': isServiceCreate, 'warehouse-labeling-create': isLabelingCreate, 'warehouse-transfer-create': isTransferCreate, 'warehouse-disposal-create': isDisposalCreate, 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--transfer': props.service === 'transfer', 'warehouse-service-page--disposal-form': props.service === 'disposal' && (mode === 'create' || mode === 'edit'), 'warehouse-service-page--return-relabel': isReturnRelabelSource }" >
2026-08-21 17:20:51 +08:00
< template v-if = "mode === 'list'" >
2026-10-13 17:43:05 +08:00
< QueryToolbar v-model = "listQueryModel" :class="{ 'labeling-query': props.service === 'labeling', 'transfer-query': props.service === 'transfer' || props.service === 'disposal', 'disposal-query': props.service === 'disposal' }" layout="grid" :ariaLabel="`${config.title}筛选`" :fields="listQueryFields" :search-on-reset="false" @search="query" @reset="resetListQuery" />
2026-09-18 16:48:35 +08:00
< div class = "warehouse-card warehouse-table-card" >
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "submittedRows" :columns = "listColumns" :loading = "loading" :total = "total" : page -sizes ="[ 15 , 30 , 50 , 100 ]" :row-actions = "listRowActions" :empty-text = "config.emptyText" >
< template # toolbar -actions >< el-button v-if = "canCreate" type="primary" @click="openCreate"><Plus :size="16" />{{ props . service === 'relabel' ? '新建退货换标单' : props . service === 'transfer' ? '新增订单' : `新增 ${ config . title } ` }}</ el-button ></ template >
2026-09-23 11:25:35 +08:00
< template # transferRelabelStatus = "{ row }" >< el-tag :type = "transferRelabelStatusTagType(row.__relabelStatus)" effect = "light" >{{ row . __relabelStatusLabel }}</ el-tag ></ template >
2026-10-20 15:07:39 +08:00
< template # labelingQuantity = "{ row, column }" >< span : class = "{ 'warehouse-missing-value': row[column.property] === '—' }" >{{ row [ column . property ] }}</ span ></ template >
2026-10-20 11:54:44 +08:00
< template # labelingType = "{ row }" >< span v-if = "row['6'] === '—'">—</span><span v-else class="labeling-type-tags"><el-tag v-for="label in labelingTypeLabels(row['6'])" :key="label" :type="labelingTypeTagType(label)" size="small" effect="light" >{{ label }} </ el -tag ></ span ></ template >
2026-10-13 17:43:05 +08:00
< template # labelingStatus = "{ row }" >< el-tag :type = "labelingStatusTagType(row.__status)" size = "small" effect = "light" >{{ statusLabel ( row . __status ) }}</ el-tag ></ template >
2026-09-23 11:25:35 +08:00
< template # transferStatus = "{ row }" >< el-tag :type = "transferStatusTagType(row.__status)" effect = "light" >{{ row . __statusLabel }}</ el-tag ></ template >
2026-09-18 16:48:35 +08:00
</ DataTable >
2026-08-21 17:20:51 +08:00
</ div >
</ template >
2026-09-17 13:36:20 +08:00
< template v-else-if = "mode === 'create' || mode === 'edit'" >
2026-09-18 16:48:35 +08:00
< div class = "warehouse-page-heading" >< h2 >{{ mode === 'edit' ? `编辑转运草稿 ${ editingTransferNo } ` : config . createTitle }}</ h2 >< el-button @click ="returnToList" >< ArrowLeft :size = "16" /> 返回列表 </ el-button ></ div >
2026-10-29 16:13:22 +08:00
< component : is = "isServiceCreate ? ElForm : 'form'" ref = "labelingFormRef" : model = "isLabelingCreate ? labelingFormModel : isTransferCreate ? transferFormModel : isDisposalCreate ? disposalFormModel : undefined" : rules = "isLabelingCreate ? labelingFormRules : isTransferCreate ? transferFormRules : isDisposalCreate ? disposalFormRules : undefined" : disabled = "isServiceCreate ? saving : undefined" label -position =" top " :validate-on-rule-change = "false" :scroll-to-error = "isServiceCreate" : class = "{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" : style = "props.service === 'transfer' ? { '--transfer-left': `${transferPaneShare}fr`, '--transfer-right': `${100 - transferPaneShare}fr` } : undefined" @submit.prevent ="submit('submit')" >
2026-10-20 11:54:44 +08:00
< div id = "transfer-main-pane" class = "warehouse-card warehouse-form-section warehouse-transfer-main-card" >
2026-10-29 16:13:22 +08:00
< div v-if = "isTransferCreate || isDisposalCreate" class="warehouse-order-info-heading" >< h3 >{{ isTransferCreate ? '转运信息' : '基础信息' }}</ h3 ></ div >
2026-10-09 11:48:52 +08:00
< div v-if = "props.service === 'labeling'" class="warehouse-order-info-heading" >
< h3 > 订单信息 </ h3 >
< el-button text type = "primary" native -type =" button " :aria-expanded = "!orderInfoCollapsed" @click ="orderInfoCollapsed = !orderInfoCollapsed" >{{ orderInfoCollapsed ? ' 展开信息 ' : ' 收起信息 ' }} </ el -button >
</ div >
2026-10-29 16:13:22 +08:00
< h3 v-else-if = "!isTransferCreate && !isDisposalCreate" >{{ props.service === ' transfer ' ? ' 转运信息 ' : props.service === ' relabel ' ? ' 退货信息 ' : ' 基础信息 ' }}</ h3 >
2026-10-09 11:48:52 +08:00
< div v-if = "props.service === 'labeling' && orderInfoCollapsed" class="warehouse-order-info-summary" >
< span >{{ operationWarehouses . find ( item => String ( item . id ) === form . warehouse ) ? . warehouseName || '未选择操作仓' }}</ span >
< span >{{ relabelInboundOptions . find ( item => item . value === selectedRelabelInboundOrderId ) ? . inboundOrderNo || '未选择来源单据' }}</ span >
</ div >
< div v-show = "props.service !== 'labeling' || !orderInfoCollapsed" class="warehouse-form-grid" >
2026-10-29 16:13:22 +08:00
< template v-if = "isLabelingCreate" >
< el-form-item label = "海外操作仓(中转仓)" prop = "warehouse" >
< el-select v-model = "form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="请选择海外操作仓" popper-class="labeling-create-popper" @change="selectOperationWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></ el-select >
< small v-if = "operationWarehousesError" class="warehouse-hint" >{{ operationWarehousesError }}</ small >
</ el -form-item >
< el-form-item label = "选择入仓单或退货单" prop = "sourceOrder" >
< el-select v-model = "selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="请选择入仓单或退货单" popper-class="labeling-create-popper" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></ el-select >
</ el-form-item >
< el-form-item v-if = "labelingMode !== 'sku'" label="FBA 目的仓" prop="destination"><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 目的仓" popper-class="labeling-create-popper"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></ el-select ></ el-form-item >
< el-form-item label = "海外仓签收联系人" prop = "contact" >< el-input v-model = "form.contact" :placeholder="form.warehouse && !form.contact ? '仓库未维护,请填写签收联系人' : '请输入海外仓签收联系人'" /></ el-form-item >
< el-form-item label = "海外仓联系电话" prop = "phone" >< el-input v-model = "form.phone" type="tel" :placeholder="form.warehouse && !form.phone ? '仓库未维护,请填写联系电话' : '请输入海外仓联系电话'" /></ el-form-item >
< el-form-item label = "贴标截止时间" prop = "labelDeadline" >< el-date-picker v-model = "form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择贴标截止时间" popper-class="labeling-create-popper" /></ el-form-item >
< el-form-item v-if = "labelingMode !== 'sku'" label="换标后派送方式" prop="deliveryMethod"><el-select v-model="form.deliveryMethod" placeholder="请选择换标后派送方式" popper-class="labeling-create-popper"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></ el-select ></ el-form-item >
< el-form-item v-if = "labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS'" label="是否打印面单"><el-checkbox v-model="form.relabel" > 需要仓库打印面单 </ el -checkbox ></ el-form-item >
< el-form-item v-if = "canManageRelabelFiles && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel" label="上传面单文件" prop="shippingLabelFile"><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel" /></ el-form-item >
< el-form-item label = "贴标要求" >< el-input v-model = "form.requirement" placeholder="请输入贴标要求(选填)" /></ el-form-item >
< el-form-item class = "warehouse-span-2" label = "备注" >< el-input v-model = "form.remark" type="textarea" :rows="4" resize="vertical" placeholder="请输入其他补充信息(选填)" /></ el-form-item >
</ template >
< template v-else-if = "isTransferCreate" >
< el-form-item label = "入库海外仓" prop = "warehouse" >< el-select v-model = "form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="请选择入库海外仓" popper-class="transfer-create-popper" @change="selectTransferWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint" >{{ operationWarehousesError }}</ small > </ el -form-item >
< el-form-item label = "库存转运来源(可选)" >< el-select v-model = "selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="请选择库存来源(选填)" popper-class="transfer-create-popper" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></ el-select ></ el-form-item >
< el-form-item v-if = "!inventoryTransfer" class="warehouse-transfer-choice" label="入仓方式" prop="deliveryMethod"><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY" > 客户送货到仓 </ el -radio-button ></ el-radio-group ></ el-form-item >
< template v-if = "!inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'" >
< el-form-item class = "warehouse-span-2" label = "提货地址" prop = "address" >< el-input v-model = "form.address" placeholder="请输入详细提货地址" /></ el-form-item >
< el-form-item label = "提货联系人" prop = "pickupContact" >< el-input v-model = "form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></ el-form-item >
< el-form-item label = "提货联系人电话" prop = "pickupPhone" >< el-input v-model = "form.pickupPhone" type="tel" placeholder="请输入提货联系电话" maxlength="64" /></ el-form-item >
</ template >
< div v-if = "!inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2" >
< div class = "warehouse-span-2" >< span > 海外仓收货信息 </ span >< strong >{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</ strong ></ div >
< el-form-item class = "warehouse-span-2" label = "送货地址" prop = "address" >< el-input v-model = "form.address" placeholder="请输入详细送货地址" /></ el-form-item >
< div >< span > 收货联系人 </ span >< strong >{{ selectedOperationWarehouse ? . warehouse . contactName || '未维护' }}</ strong ></ div >< div >< span > 联系电话 </ span >< strong >{{ selectedOperationWarehouse ? . warehouse . contactPhone || '未维护' }}</ strong ></ div >
</ div >
< el-form-item v-if = "!inventoryTransfer" label="预计入仓时间" prop="estimatedArrivalAt"><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计入仓时间" popper-class="transfer-create-popper" /></ el-form-item >
< el-form-item label = "期望发货时间" >< el-date-picker v-model = "form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择期望发货时间(选填)" popper-class="transfer-create-popper" /></ el-form-item >
< el-form-item class = "warehouse-transfer-choice" label = "目的去向" >< el-radio-group v-model = "transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 </ el -radio-button ></ el-radio-group ></ el-form-item >
< template v-if = "transferDestinationType === 'FBA'" >
< el-form-item label = "FBA 目的仓" prop = "destination" >< el-select v-model = "form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 目的仓" popper-class="transfer-create-popper"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></ el-select ></ el-form-item >
< el-form-item label = "FBA 货件编号" >< el-input v-model = "form.fbaShipmentNo" placeholder="请输入 FBA 货件编号(选填)" /></ el-form-item >
</ template >
< template v-if = "transferDestinationType === 'PRIVATE'" >
< el-form-item class = "warehouse-span-2" label = "私人仓地址" prop = "privateDestinationAddress" >< el-input v-model = "form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓详细地址" /></ el-form-item >
< el-form-item label = "邮编" prop = "privateDestinationPostalCode" >< el-input v-model = "form.privateDestinationPostalCode" maxlength="32" placeholder="请输入邮编" /></ el-form-item >
< el-form-item label = "私人仓联系人" prop = "privateDestinationContact" >< el-input v-model = "form.privateDestinationContact" maxlength="128" placeholder="请输入私人仓联系人" /></ el-form-item >
< el-form-item label = "联系电话" prop = "privateDestinationPhone" >< el-input v-model = "form.privateDestinationPhone" type="tel" maxlength="64" placeholder="请输入联系电话" /></ el-form-item >
</ template >
< el-form-item v-if = "!inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" label="物流跟踪号"><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></ el-form-item >
< el-form-item class = "warehouse-transfer-choice" label = "是否贴标" required >< el-radio-group v-model = "form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true" > 需要贴标 </ el -radio-button ></ el-radio-group ></ el-form-item >
< el-form-item class = "warehouse-span-2" label = "备注" >< el-input v-model = "form.remark" type="textarea" :rows="3" resize="vertical" placeholder="请输入转运要求或其他补充信息(选填)" /></ el-form-item >
</ template >
< template v-else-if = "isDisposalCreate" >
< el-form-item label = "操作仓" prop = "warehouse" >< el-select v-model = "form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择操作仓" popper-class="disposal-create-popper" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint" >{{ operationWarehousesError }}</ small > </ el -form-item >
< el-form-item label = "选择入库单" prop = "sourceOrder" >< el-select v-model = "selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="请选择入库单" popper-class="disposal-create-popper" @change="selectDiscardInbound"><el-option v-for="candidate in discardDocuments" :key="candidate.orderId" :label="`${candidate.sourceType === 'RETURN_CARTON' ? '退货|' : ''}${candidate.inboundOrderNo || candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></ el-select ></ el-form-item >
< el-form-item label = "废弃原因" prop = "reason" >< el-select v-model = "form.reason" placeholder="请选择废弃原因" popper-class="disposal-create-popper"><el-option label="残损不可售" value="残损不可售" /><el-option label="其他" value="其他" /></ el-select ></ el-form-item >
< el-form-item class = "warehouse-span-2" label = "备注" prop = "remark" : rules = "discardQuantityMode ? [{ required: true, whitespace: true, message: '请填写废弃备注', trigger: 'blur' }] : []" >< el-input v-model = "form.remark" type="textarea" :rows="3" resize="vertical" :placeholder="discardQuantityMode ? '请填写废弃备注' : '请输入废弃要求或其他补充信息(选填)'" /></ el-form-item >
</ template >
< template v-else >
2026-10-19 16:40:30 +08:00
< label v-if = "props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>操作仓</span><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint" >{{ operationWarehousesError }}</ small ></ label >
< label v-if = "props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="" @change="selectDiscardInbound"><el-option v-for="candidate in discardDocuments" :key="candidate.orderId" :label="`${candidate.sourceType === 'RETURN_CARTON' ? '退货|' : ''}${candidate.inboundOrderNo || candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></ el-select ></ label >
2026-10-20 14:52:22 +08:00
< label v-if = "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" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></ el-select ></ label >
2026-10-20 11:54:44 +08:00
< label v-else-if = "props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder=""><el-option label="残损不可售" value="残损不可售" /><el-option label="其他" value="其他" /></ el-select ></ label >
2026-10-19 16:40:30 +08:00
< label v-else >< span >< i > * </ i >{{ props . service === 'labeling' ? '海外操作仓(中转仓)' : props . service === 'transfer' ? '入库海外仓' : '操作仓' }}</ span >< el-select v-model = "form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable placeholder="" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint" >{{ operationWarehousesError }}</ small ></ label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></ el-select ></ label >
< label v-if = "props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></ el-select ></ label >
< label v-if = "props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></ el-select ></ label >
2026-10-16 11:42:59 +08:00
< div v-if = "props.service === 'transfer' && !inventoryTransfer" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY" > 客户送货到仓 </ el -radio-button ></ el-radio-group ></ div >
2026-10-19 16:40:30 +08:00
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="" /></ label >
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="" maxlength="128" /></ label >
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="" maxlength="64" /></ label >
2026-10-16 11:42:59 +08:00
< div v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2" >
2026-09-14 16:51:43 +08:00
< div >< span > 海外仓收货信息 </ span >< strong >{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</ strong ></ div >
2026-10-19 16:40:30 +08:00
< label class = "warehouse-span-2" >< span >< i > * </ i > 送货地址 </ span >< el-input v-model = "form.address" placeholder="" /></ label >
2026-09-17 13:36:20 +08:00
< div >< span > 收货联系人 </ span >< strong >{{ selectedOperationWarehouse ? . warehouse . contactName || '未维护' }}</ strong ></ div >
< div >< span > 联系电话 </ span >< strong >{{ selectedOperationWarehouse ? . warehouse . contactPhone || '未维护' }}</ strong ></ div >
2026-09-10 15:34:03 +08:00
</ div >
2026-10-20 14:52:22 +08:00
< label v-if = "props.service === 'relabel'"><span><i>*</i>目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder=""><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></ el-select ></ label >
< label v-if = "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'"><span><i>*</i>退货原因</span><el-select v-model="form.reason" placeholder=""><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></ el-select ></ label >
2026-10-19 16:40:30 +08:00
< 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' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder=""><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="亚马逊自提" value="AMAZON_PICKUP" /><el-option label="客户自提" value="CUSTOMER_PICKUP" /></ el-select ></ label >
2026-10-14 17:41:04 +08:00
< div v-if = "props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS'" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel" > 需要仓库打印面单 </ el -checkbox ></ div >
< label v-if = "canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel" ></ label >
2026-10-20 14:52:22 +08:00
< label v-if = "props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="" /></ label >
< label v-if = "props.service === 'relabel'"><span><i>*</i>预估总体积(CBM) </span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="" /></ label >
2026-10-19 16:40:30 +08:00
< label v-if = "(props.service === 'transfer' && !inventoryTransfer) || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '' : '请选择预计到达时间'" /></ label >
< label v-if = "props.service === 'transfer'"><span>期望发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></ label >
2026-10-16 11:42:59 +08:00
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 </ el -radio-button ></ el-radio-group ></ div >
2026-10-19 16:40:30 +08:00
< label v-if = "props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder=""><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' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></ label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="" /></ label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="" /></ label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓联系人</span><el-input v-model="form.privateDestinationContact" maxlength="128" placeholder="" /></ label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>联系电话</span><el-input v-model="form.privateDestinationPhone" maxlength="64" placeholder="" /></ label >
2026-10-20 14:52:22 +08:00
< label v-if = "props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></ label >
2026-10-19 16:40:30 +08:00
< label v-if = "props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="" /></ label >
< label v-else-if = "props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" :placeholder="props.service === 'transfer' ? '' : '请输入物流跟踪号(选填)'" /></ label >
2026-10-29 15:49:59 +08:00
< label class = "warehouse-span-2" >< span >{{ props . service === 'disposal' && discardQuantityMode ? '备注 *' : '备注' }}</ span >< el-input v-model = "form.remark" type="textarea" :rows="2" placeholder="" /></ label >
2026-10-19 16:40:30 +08:00
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true" > 需要贴标 </ el -radio-button ></ el-radio-group ></ div >
2026-10-29 16:13:22 +08:00
</ template >
2026-08-21 17:20:51 +08:00
</ div >
</ div >
2026-09-13 21:10:14 +08:00
< div class = "warehouse-card warehouse-form-section warehouse-transfer-cargo-card" >
2026-09-08 20:28:05 +08:00
< template v-if = "props.service === 'labeling'" >
2026-09-10 15:56:39 +08:00
< div class = "warehouse-section-heading warehouse-labeling-heading" >
2026-09-20 16:55:27 +08:00
< div >< h3 > 货物明细 </ h3 ></ div >
2026-09-10 15:56:39 +08:00
</ div >
2026-10-29 16:13:22 +08:00
< div class = "warehouse-labeling-body" >
2026-10-09 11:48:52 +08:00
< div class = "warehouse-file-groups warehouse-file-groups--compact" >
2026-09-20 16:55:27 +08:00
< section v-if = "canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary" >
2026-10-12 13:43:29 +08:00
< div class = "warehouse-file-group__heading" >< div >< strong >< span > 1 </ span > 贴标总文件 </ strong ></ div >< em >{{ requiresBothTotalFiles ? '必传 · 两份文件' : isReturnRelabelSource ? labelingMode === 'sku' ? '必传 · SKU 标签文件' : '必传 · 箱唛文件' : '必传 · 二选一' }}</ em ></ div >
2026-09-20 16:55:27 +08:00
< div class = "warehouse-file-options" >
2026-09-23 11:25:35 +08:00
< div class = "warehouse-file-option" : class = "{ 'is-selected': Boolean(cartonTotalFile) }" >< div class = "warehouse-file-option__name" >< strong > 箱唛总文件 </ strong ></ div >< el-button native -type =" button " : type = "cartonTotalFile ? 'primary' : 'default'" plain @click ="chooseCreateTotalFile('CARTON_TOTAL')" >< Upload :size = "15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</ el-button >< div class = "warehouse-file-option__status" : class = "{ 'is-empty': !cartonTotalFile }" >< span >{{ cartonTotalFile ? . name || '尚未选择文件' }}</ span >< button v-if = "cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></ button ></ div ></ div >
2026-09-20 16:55:27 +08:00
< div class = "warehouse-file-choice-or" : class = "{ 'is-both': requiresBothTotalFiles }" >{{ requiresBothTotalFiles ? '和' : '或' }}</ div >
2026-09-23 11:25:35 +08:00
< div class = "warehouse-file-option" : class = "{ 'is-selected': Boolean(skuTotalFile) }" >< div class = "warehouse-file-option__name" >< strong > SKU 总文件 </ strong ></ div >< el-button native -type =" button " : type = "skuTotalFile ? 'primary' : 'default'" plain @click ="chooseCreateTotalFile('SKU_TOTAL')" >< Upload :size = "15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</ el-button >< div class = "warehouse-file-option__status" : class = "{ 'is-empty': !skuTotalFile }" >< span >{{ skuTotalFile ? . name || '尚未选择文件' }}</ span >< button v-if = "skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></ button ></ div ></ div >
2026-09-20 16:55:27 +08:00
</ div >
</ section >
2026-10-09 11:48:52 +08:00
< section class = "warehouse-file-group" >
2026-09-20 16:55:27 +08:00
< div class = "warehouse-file-group__heading" >< div >< strong >< span > 2 </ span > 明细关联模板 </ strong ></ div >< em > 按需使用 </ em ></ div >
< div class = "warehouse-template-list" >
2026-10-28 16:33:35 +08:00
< div >< span >< strong > 统一贴标明细 </ strong ></ span >< el-button size = "small" :disabled = "!cargoRows.length" @click ="downloadRelabelTemplate()" >< Download :size = "15" /> 下载模板 </ el-button >< el-button type = "primary" plain size = "small" :disabled = "!cargoRows.length" @click ="unifiedTemplateInput?.click()" >< Upload :size = "15" /> 上传文件 </ el-button ></ div >
2026-09-20 16:55:27 +08:00
</ div >
</ section >
2026-09-17 13:36:20 +08:00
</ div >
2026-10-28 16:33:35 +08:00
< input ref = "unifiedTemplateInput" class = "warehouse-hidden-input" type = "file" accept = ".csv,text/csv" @change ="importRelabelTemplate" >< input ref = "cartonTotalCreateInput" class = "warehouse-hidden-input" type = "file" accept = "application/pdf,.pdf" @change ="selectCartonTotalFile" >< input ref = "skuTotalCreateInput" class = "warehouse-hidden-input" type = "file" accept = "application/pdf,.pdf" @change ="selectSkuTotalFile" >
2026-10-19 16:40:30 +08:00
< div v-if = "isReturnRelabelSource" class="warehouse-table-scroll" >
2026-10-12 13:43:29 +08:00
< el-input v-model = "returnCartonQuery" clearable placeholder="" />
2026-10-12 21:19:13 +08:00
< template v-if = "returnLooseCandidates.length" >
2026-10-09 11:48:52 +08:00
< div class = "warehouse-section-heading" >< h3 > 可用包裹 </ h3 >< el-button text type = "primary" native -type =" button " :aria-expanded = "!availableSkusCollapsed" aria -controls =" return -available-skus " @click ="availableSkusCollapsed = !availableSkusCollapsed" >{{ availableSkusCollapsed ? ' 展开明细 ' : ' 收起明细 ' }} </ el -button ></ div >
< table v-show = "!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</th><th>SKU 换标结果</th><th>剩余可换(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ source.cartonMark }}</td><td>{{ source.inboundOrderNo }}</td><td>{{ sku.skuCode }}</td><td><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="labelingMode === 'sku' ? addReturnSkuBinding(source, sku.skuCode) : addReturnBoxBinding(source)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="6" > 暂无可用包裹库存 </ td ></ tr ></ tbody ></ table >
2026-10-09 15:25:27 +08:00
</ template >
2026-10-12 21:19:13 +08:00
< template v-if = "returnBoxCandidates.length" >
2026-10-09 11:48:52 +08:00
< div class = "warehouse-section-heading" >< h3 > 可用箱子 ( {{ returnBoxCandidates . length }} 箱 ) </ h3 >< el-button text type = "primary" native -type =" button " :aria-expanded = "!availableBoxesCollapsed" aria -controls =" return -available-boxes " @click ="availableBoxesCollapsed = !availableBoxesCollapsed" >{{ availableBoxesCollapsed ? ' 展开明细 ' : ' 收起明细 ' }} </ el -button ></ div >
2026-09-20 16:55:27 +08:00
< table v-show = "!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>仓库箱识别码</th><th>来源退货单</th><th>箱内 SKU</th><th>SKU 换标结果</th><th>剩余可换(件)</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td>{{ source.inboundOrderNo }}</td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="6" > 暂无可用箱子库存 </ td ></ tr ></ tbody ></ table >
2026-10-09 11:48:52 +08:00
</ template >
2026-09-09 13:35:51 +08:00
</ div >
< div class = "warehouse-labeling-mode" >
2026-10-12 13:43:29 +08:00
< span > 换标类型 </ span >
2026-09-09 13:35:51 +08:00
< el-radio-group v-model = "labelingMode" aria-label="换标类型" >
2026-10-12 13:43:29 +08:00
< el-radio-button value = "carton" > 只换箱唛 </ el-radio-button >
2026-09-09 13:35:51 +08:00
< el-radio-button value = "sku" > 只换 SKU 标签 </ el-radio-button >
< el-radio-button value = "carton_and_sku" > SKU 和箱唛都换 </ el-radio-button >
2026-10-09 11:48:52 +08:00
</ el-radio-group >
2026-10-19 16:40:30 +08:00
</ div >
2026-09-17 16:34:51 +08:00
< div v-if = "importedRelabelBindings.length" class="warehouse-import-preview" >
2026-09-10 17:12:52 +08:00
< div class = "warehouse-section-heading" >< div >< strong > 待导入绑定关系 </ strong ></ 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>
2026-10-09 11:48:52 +08:00
<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></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></tr></tbody></table></div>
2026-09-20 16:55:27 +08:00
</div>
2026-09-10 15:56:39 +08:00
<div class=" warehouse - section - heading warehouse - tab - actions ">
2026-09-09 13:35:51 +08:00
<div><strong>绑定明细</strong></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>
2026-10-29 16:13:22 +08:00
<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>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in cartonDetailRows " :key=" row . id "><td><el-input v-model=" row . sku " placeholder=" 请输入 SKU " /></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 " :readonly=" isReturnRelabelSource " placeholder=" 请输入仓库唛头 " /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新 FBA 箱唛 " /></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=" 8 "><div class=" warehouse - empty ">暂无绑定明细</div></td></tr></tbody></table></div>
2026-09-08 20:28:05 +08:00
<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 " :readonly=" isReturnRelabelSource " placeholder=" 请输入仓库唛头 " @change=" applyExistingCartonBinding ( row ) " /></td><td><el-input v-model=" row . originalSku " :readonly=" isReturnRelabelSource " 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-input v-model=" row . shipmentNo " placeholder=" 请输入 FBA 货件编号 " /></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 ">暂无绑定明细</div></td></tr></tbody></table></div>
</div>
2026-10-29 16:13:22 +08:00
</template>
<template v-else>
2026-10-14 17:41:04 +08:00
<div v-if=" isTransferCreate || isDisposalCreate " class=" warehouse - order - info - heading "><h3>{{ isDisposalCreate && discardQuantityMode ? '废弃数量' : isDisposalCreate && discardSelectedCandidate?.cartonSelectionSupported ? '选择废弃箱子' : '货物明细' }}</h3></div>
2026-10-29 15:49:59 +08:00
<div :class=" { 'warehouse-transfer-create-body' : isTransferCreate , 'warehouse-disposal-create-body' : isDisposalCreate } ">
2026-10-29 16:13:22 +08:00
<section v-if=" props . service === 'disposal' && discardSelectedCandidate " class=" warehouse - discard - boxes ">
<div class=" warehouse - section - heading "><div><h3>{{ discardQuantityMode ? '废弃数量' : '选择废弃箱子' }}</h3></div><div v-if=" discardSelectedCandidate . cartonSelectionSupported " class=" warehouse - heading - actions "><el-button size=" small " @click=" selectAllDiscardBoxes ( true ) ">全选</el-button><el-button size=" small " @click=" selectAllDiscardBoxes ( false ) ">清空选择</el-button></div></div>
<div v-if=" isDisposalCreate && discardQuantityMode " class=" warehouse - form - grid warehouse - disposal - quantity ">
<el-form-item label=" 本次废弃箱数 " prop=" requestedPackages " :rules=" [{ required : true , type : 'integer' , min : 1 , max : Number ( discardSelectedCandidate . availablePackages ), message : '请填写可用范围内的整数箱数' , trigger : 'change' }] "><el-input-number v-model=" discardRequestedPackages " :min=" 1 " :max=" Number ( discardSelectedCandidate . availablePackages ) " :precision=" 0 " controls-position=" right " placeholder=" 请输入废弃箱数 " /></el-form-item>
<span class=" warehouse - hint ">可用 {{ discardSelectedCandidate.availablePackages }} 箱</span>
2026-10-29 15:49:59 +08:00
</div>
2026-10-29 16:13:22 +08:00
<div v-else-if=" discardQuantityMode " class=" warehouse - disposal - field "><span>本次废弃箱数 *</span><el-input-number v-model=" discardRequestedPackages " :min=" 1 " :max=" Number ( discardSelectedCandidate . availablePackages ) " :precision=" 0 " controls-position=" right " placeholder=" 请输入废弃箱数 " /><span>可用 {{ discardSelectedCandidate.availablePackages }} 箱</span></div>
2026-10-14 17:41:04 +08:00
<template v-else-if=" discardSelectedCandidate . cartonSelectionSupported ">
<div class=" warehouse - discard - box - toolbar "><el-input v-model=" discardBoxSearch " clearable :placeholder=" isDisposalCreate ? '搜索箱唛或 SKU' : '' " /><strong>已选 {{ discardSelectedBoxKeys.length }} / {{ discardSelectableBoxes.length }} 箱</strong></div>
2026-10-20 14:00:59 +08:00
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>选择</th><th>箱唛</th><th>所属托盘</th><th>箱内 SKU</th><th>中文品名</th></tr></thead><tbody><tr v-for=" box in discardVisibleBoxes " :key=" box . key "><td><el-checkbox :model-value=" discardSelectedBoxKeys . includes ( box . key ) " :aria-label=" `选择废弃箱子 ${ box . cartonMark } ` " @change=" toggleDiscardBox ( box . key , Boolean ( $event )) " /></td><td><strong>{{ box.cartonMark }}</strong></td><td>{{ box.palletNo || '散箱' }}</td><td>{{ box.skus.map(sku => sku.skuCode).join('、') }}</td><td>{{ box.skus.map(sku => sku.productName || '—').join('、') }}</td></tr><tr v-if=" ! discardVisibleBoxes . length "><td colspan=" 5 "><div class=" warehouse - empty ">暂无匹配箱唛</div></td></tr></tbody></table></div>
2026-10-29 15:49:59 +08:00
</template>
2026-10-14 17:41:04 +08:00
<el-alert v-else type=" info " :closable=" false " :title=" discardSelectedCandidate . cartonSelectionBlockReason || '当前库存缺少箱唛与托盘对应关系,将预留所选库存,由仓库核对相关托盘并填写实际废弃箱数,未废弃部分释放。' " />
2026-10-16 11:42:59 +08:00
<div v-if=" ! discardQuantityMode && ! discardSelectedCandidate . cartonSelectionSupported " class=" warehouse - hint ">相关托盘:{{ [...new Set(discardSelectionSources.map(source => source.palletNo).filter(Boolean))].join('、') || '散箱/托盘信息未记录' }}</div>
</section>
<section v-if=" inventoryTransfer " v-loading=" transferSourceLoading ">
<div class=" warehouse - section - heading "><h3>原单箱明细 <small>共 {{ transferSourceCartons.length }} 箱 · 只读</small></h3></div>
<el-alert v-if=" transferSourceError " :title=" transferSourceError " type=" error " :closable=" false "><el-button link @click=" loadTransferSourceCartons ">重新加载</el-button></el-alert>
2026-10-19 14:30:52 +08:00
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>箱号</th><th>商品 SKU</th><th>中文品名</th><th>箱内数量(件)</th><th>原目的仓</th></tr></thead><tbody>
2026-10-16 11:42:59 +08:00
<tr v-for=" box in transferSourceCartons " :key=" box . cartonMark "><td>{{ box.cartonMark }}</td><td>{{ box.items.map(item => item.skuCode || '—').join(' / ') }}</td><td>{{ box.productNameZh || '—' }}</td><td>{{ box.quantity ?? '未记录' }}</td><td>{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}</td></tr>
<tr v-if=" ! transferSourceLoading && ! transferSourceCartons . length "><td colspan=" 5 "><div class=" warehouse - empty ">{{ transferSourceError ? '加载失败,请重试' : '当前来源单暂无未换标的箱明细' }}</div></td></tr>
</tbody></table></div>
2026-10-29 15:49:59 +08:00
<div v-if=" ! transferSourceLoading && ! transferSourceError && ! transferSourceCartons . length " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU</th><th>中文品名</th><th>库存箱数</th><th>原目的仓</th></tr></thead><tbody><tr v-for=" row in cargoRows " :key=" row . id "><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
2026-10-29 16:13:22 +08:00
</section>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ( ! discardQuantityMode && ! discardSelectedCandidate ? . cartonSelectionSupported )) " class=" warehouse - section - heading " :class=" { 'warehouse-step-heading' : props . service === 'transfer' } "><div><h3><span v-if=" props . service === 'transfer' " class=" warehouse - step - number ">1</span>货物明细</h3></div><div v-if=" props . service !== 'disposal' " class=" warehouse - heading - actions "><el-button v-if=" props . service === 'transfer' " size=" small " :loading=" saving " @click=" printForecastCartons "><Printer :size=" 15 " />打印唛头</el-button><el-button type=" primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [] ">清空</el-button></div></div>
2026-09-08 20:28:05 +08:00
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ( ! discardQuantityMode && ! discardSelectedCandidate ? . cartonSelectionSupported )) " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if=" props . service === 'disposal' ">可用箱数</th><th v-if=" props . service === 'relabel' ">FNSKU / 箱唛</th><th v-if=" props . service === 'transfer' ">SKU 数量(件)*</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=" isTransferCreate ? '请输入商品 SKU(选填)' : props . service === 'relabel' ? '请输入商品 SKU' : '' " /></td><td><el-input v-model=" row . name " :readonly=" props . service === 'disposal' " :placeholder=" isTransferCreate ? '请输入中文品名(选填)' : props . service === 'relabel' ? '请输入中文品名' : '' " /></td><td v-if=" props . service === 'disposal' "><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if=" props . service === 'relabel' "><el-input v-model=" row . shippingMark " placeholder=" 请输入FNSKU或箱唛 " /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder="" /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . expectedPackageCount " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder="" /></td><td v-else><el-input-number v-model=" row . quantity " :disabled=" props . service === 'disposal' " :min=" props . service === 'disposal' ? 0 : 1 " :max=" row . availableQuantity " controls-position=" right " :placeholder=" props . service === 'relabel' ? '请输入箱数' : '' " /></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 : props . service === 'relabel' || props . service === 'transfer' ? 5 : 4 "><div class=" warehouse - empty ">暂无货物明细</div></td></tr></tbody></table></div>
2026-08-21 17:20:51 +08:00
</div>
2026-10-20 11:54:44 +08:00
</template>
</div>
2026-09-20 16:55:27 +08:00
<TransferPaneDivider v-if=" props . service === 'transfer' && form . relabel " v-model=" transferPaneShare " class=" warehouse - transfer - divider " />
2026-10-29 16:13:22 +08:00
<div id=" transfer - relabel - pane " v-if=" props . service === 'transfer' && form . relabel " class=" warehouse - card warehouse - form - section warehouse - transfer - relabel ">
<div class=" warehouse - section - heading warehouse - labeling - heading warehouse - step - heading "><div><h3><span class=" warehouse - step - number ">2</span>换标申请</h3></div></div>
<div :class=" { 'warehouse-transfer-create-body' : isTransferCreate , 'warehouse-disposal-create-body' : isDisposalCreate } ">
<div v-if=" isTransferCreate " class=" warehouse - form - grid warehouse - transfer - relabel - fields ">
<el-form-item label=" 换标截止时间 " prop=" labelDeadline "><el-date-picker v-model=" form . labelDeadline " type=" datetime " value-format=" YYYY - MM - DDTHH : mm : ss " placeholder=" 请选择换标截止时间 " popper-class=" transfer - create - popper " /></el-form-item>
<el-form-item label=" 换标联系人 " prop=" relabelContact "><el-input v-model=" form . relabelContact " maxlength=" 128 " placeholder=" 请输入换标联系人 " /></el-form-item>
<el-form-item label=" 换标联系人电话 " prop=" relabelPhone "><el-input v-model=" form . relabelPhone " type=" tel " maxlength=" 64 " placeholder=" 请输入换标联系电话 " /></el-form-item>
<el-form-item v-if=" labelingMode !== 'sku' " label=" 换标后派送方式 " prop=" relabelDeliveryMethod "><el-select v-model=" form . relabelDeliveryMethod " placeholder=" 请选择派送方式 " popper-class=" transfer - create - popper "><el-option label=" 卡车派送 " value=" TRUCK " /><el-option label=" 快递派送 " value=" EXPRESS " /><el-option label=" 亚马逊自提 " value=" AMAZON_PICKUP " /><el-option label=" 客户自提 " value=" CUSTOMER_PICKUP " /></el-select></el-form-item>
<el-form-item class=" warehouse - span - 2 " label=" 整票换标货物目的 FBA 仓 " prop=" relabelDestination "><el-select v-model=" transferRelabelDefaultDestination " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " filterable placeholder=" 请选择整票换标货物目的 FBA 仓 " popper-class=" transfer - create - popper "><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select></el-form-item>
<el-form-item label=" FBA 货件编号 "><el-input v-model=" form . fbaShipmentNo " placeholder=" 请输入 FBA 货件编号 ( 选填 ) " /></el-form-item>
<el-form-item class=" warehouse - span - 2 " label=" 贴标要求 "><el-input v-model=" form . requirement " type=" textarea " :rows=" 3 " maxlength=" 1000 " show-word-limit placeholder=" 请输入贴标要求 ( 选填 ) " /></el-form-item>
2026-09-20 16:55:27 +08:00
</div>
2026-10-09 11:48:52 +08:00
<div v-else class=" warehouse - form - grid warehouse - transfer - relabel - fields "><label><span><i>*</i>换标截止时间</span><el-date-picker v-model=" form . labelDeadline " type=" datetime " value-format=" YYYY - MM - DDTHH : mm : ss " placeholder="" /></label><label><span><i>*</i>换标联系人</span><el-input v-model=" form . relabelContact " placeholder="" maxlength=" 128 " /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model=" form . relabelPhone " placeholder="" maxlength=" 64 " /></label><label v-if=" labelingMode !== 'sku' "><span><i>*</i>换标后派送方式</span><el-select v-model=" form . relabelDeliveryMethod " placeholder=""><el-option label=" 卡车派送 " value=" TRUCK " /><el-option label=" 快递派送 " value=" EXPRESS " /><el-option label=" 亚马逊自提 " value=" AMAZON_PICKUP " /><el-option label=" 客户自提 " value=" CUSTOMER_PICKUP " /></el-select></label><label class=" warehouse - span - 2 "><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model=" transferRelabelDefaultDestination " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " filterable placeholder=""><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select></label><label><span>FBA 货件编号</span><el-input v-model=" form . fbaShipmentNo " placeholder="" /></label><label class=" warehouse - span - 2 "><span>贴标要求</span><el-input v-model=" form . requirement " type=" textarea " :rows=" 2 " maxlength=" 1000 " show-word-limit placeholder="" /></label></div>
2026-09-20 17:53:41 +08:00
<div class=" warehouse - labeling - mode "><span>换标类型</span><el-radio-group v-model=" labelingMode " aria-label=" 换标类型 "><el-radio-button value=" carton ">只换箱唛</el-radio-button><el-radio-button value=" sku ">只换 SKU 标签</el-radio-button><el-radio-button value=" carton_and_sku ">SKU 和箱唛都换</el-radio-button></el-radio-group></div>
<div class=" warehouse - transfer - file - groups warehouse - file - groups -- compact ">
<section v-if=" canManageRelabelFiles " class=" warehouse - file - group warehouse - transfer - total - files ">
2026-09-23 11:25:35 +08:00
<div class=" warehouse - file - group__heading "><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
2026-09-20 17:53:41 +08:00
<div class=" warehouse - file - options ">
2026-09-23 11:25:35 +08:00
<div class=" warehouse - file - option " :class=" { 'is-selected' : Boolean ( cartonTotalFile ) } "><div class=" warehouse - file - option__name "><strong>箱唛总文件</strong></div><el-button native-type=" button " :type=" cartonTotalFile ? 'primary' : 'default' " plain @click=" chooseCreateTotalFile ( 'CARTON_TOTAL' ) "><Upload :size=" 15 " />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class=" warehouse - file - option__status " :class=" { 'is-empty' : ! cartonTotalFile } "><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if=" cartonTotalFile " type=" button " class=" warehouse - file - clear " title=" 清除箱唛总文件 " aria-label=" 清除箱唛总文件 " :disabled=" saving " @click=" clearCreateTotalFile ( 'CARTON_TOTAL' ) "><X :size=" 14 " /></button></div></div>
2026-09-20 17:53:41 +08:00
<div class=" warehouse - file - choice - or " :class=" { 'is-both' : requiresBothTotalFiles } ">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
<div class=" warehouse - file - option " :class=" { 'is-selected' : Boolean ( skuTotalFile ) } "><div class=" warehouse - file - option__name "><strong>SKU 总文件</strong></div><el-button native-type=" button " :type=" skuTotalFile ? 'primary' : 'default' " plain @click=" chooseCreateTotalFile ( 'SKU_TOTAL' ) "><Upload :size=" 15 " />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class=" warehouse - file - option__status " :class=" { 'is-empty' : ! skuTotalFile } "><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if=" skuTotalFile " type=" button " class=" warehouse - file - clear " title=" 清除 SKU 总文件 " aria-label=" 清除 SKU 总文件 " :disabled=" saving " @click=" clearCreateTotalFile ( 'SKU_TOTAL' ) "><X :size=" 14 " /></button></div></div>
</div>
2026-09-20 16:55:27 +08:00
<input ref=" cartonTotalCreateInput " class=" warehouse - hidden - input " type=" file " accept=" application / pdf ,. pdf " @change=" selectCartonTotalFile "><input ref=" skuTotalCreateInput " class=" warehouse - hidden - input " type=" file " accept=" application / pdf ,. pdf " @change=" selectSkuTotalFile ">
2026-10-28 16:33:35 +08:00
</section>
<section class=" warehouse - file - group warehouse - transfer - template - group ">
2026-09-20 16:55:27 +08:00
<div class=" warehouse - file - group__heading "><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class=" warehouse - template - list "><div><span><strong>统一贴标明细</strong></span><el-button size=" small " :disabled=" ! cargoRows . length " @click=" downloadRelabelTemplate () "><Download :size=" 15 " />下载模板</el-button><el-button type=" primary " plain size=" small " :disabled=" ! cargoRows . length " @click=" unifiedTemplateInput ? . click () "><Upload :size=" 15 " />上传文件</el-button></div></div>
2026-09-20 17:53:41 +08:00
<input ref=" unifiedTemplateInput " class=" warehouse - hidden - input " type=" file " accept=" . csv , text / csv " @change=" importRelabelTemplate ">
2026-10-19 16:40:30 +08:00
</section>
2026-09-20 16:55:27 +08:00
</div>
2026-10-29 16:13:22 +08:00
<div v-if=" importedRelabelBindings . length " class=" warehouse - import - preview "><div class=" warehouse - section - heading "><div><strong>待导入绑定关系</strong></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></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></tr></tbody></table></div></div>
<div class=" warehouse - section - heading warehouse - tab - actions warehouse - binding - heading "><div><strong><span class=" warehouse - step - number ">4</span>绑定明细</strong></div><div><el-button type=" primary " size=" small " @click=" addTransferRelabelRow "><Plus :size=" 15 " />添加 SKU 明细</el-button><el-button size=" small " @click=" clearTransferRelabelRows ">清空</el-button></div></div>
<div v-if=" labelingMode === 'carton' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " :placeholder=" isTransferCreate ? '请输入仓库唛头' : '' " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . fbaCartonMark " :placeholder=" isTransferCreate ? '请输入新 FBA 箱唛' : '' " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " :placeholder=" isTransferCreate ? '选填' : '' " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter (( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 5 "><div class=" warehouse - empty ">暂无绑定明细</div></td></tr></tbody></table></div>
2026-09-13 21:10:14 +08:00
<div v-else-if=" labelingMode === 'sku' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " :placeholder=" isTransferCreate ? '请输入仓库唛头' : '' " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " :placeholder=" isTransferCreate ? '请输入新 SKU' : '' " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " :placeholder=" isTransferCreate ? '请输入数量' : '' " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " :placeholder=" isTransferCreate ? '选填' : '' " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter (( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 6 "><div class=" warehouse - empty ">暂无绑定明细</div></td></tr></tbody></table></div>
2026-10-29 16:13:22 +08:00
<div v-else class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " :placeholder=" isTransferCreate ? '请输入仓库唛头' : '' " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " :placeholder=" isTransferCreate ? '请输入新 SKU' : '' " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " :placeholder=" isTransferCreate ? '请输入数量' : '' " /></td><td><el-input v-model=" row . fbaCartonMark " :placeholder=" isTransferCreate ? '请输入新 FBA 箱唛' : '' " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " :placeholder=" isTransferCreate ? '选填' : '' " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter (( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 7 "><div class=" warehouse - empty ">暂无绑定明细</div></td></tr></tbody></table></div>
</div>
</div>
2026-08-21 17:20:51 +08:00
<div class=" warehouse - submit - bar warehouse - transfer - submit - bar "><el-button v-if=" isServiceCreate " native-type=" button " :disabled=" saving " @click=" returnToList ">返回列表</el-button><el-button v-if=" mode === 'create' " native-type=" button " @click=" resetForm ">重置</el-button><el-button v-if=" props . service === 'transfer' && ! inventoryTransfer " native-type=" button " :loading=" saving " @click=" submit ( 'draft' ) ">保存草稿</el-button><el-button v-if=" props . service === 'transfer' " type=" primary " native-type=" submit " :loading=" saving ">提交订单</el-button><el-button v-if=" props . service !== 'transfer' " type=" primary " native-type=" submit " :loading=" saving ">提交{{ config.title }}</el-button></div>
</component>
2026-09-06 18:11:42 +08:00
</template>
2026-09-18 16:48:35 +08:00
<template v-else>
2026-10-28 10:55:19 +08:00
<header class=" warehouse - detail - header ">
2026-10-15 15:45:22 +08:00
<button type=" button " class=" warehouse - detail - back " @click=" returnToList "><ArrowLeft :size=" 16 " />返回列表</button>
2026-09-18 16:48:35 +08:00
<div class=" warehouse - detail - title "><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong></div>
<div class=" warehouse - detail - actions "><el-button v-if=" canCreate && props . service === 'labeling' && selectedRelabelDetail ? . application . status === 'REJECTED' " type=" primary " @click=" resubmitRejectedRelabel ">修改后重新提交</el-button><el-button v-if=" canConfirmRelabel && props . service === 'labeling' && selectedRelabelDetail ? . application . status === 'CUSTOMER_CONFIRM_PENDING' && ! selectedRelabelDetail ? . application . customerConfirmedAt " type=" primary " @click=" confirmSelectedRelabel ">确认换标完成</el-button><el-button v-if=" canConfirmDiscard && props . service === 'disposal' && selectedDetail ? . status === 'PDA_COMPLETED' " type=" primary " @click=" openCompletionConfirmation ( String ( selectedDetail ? . discardNo || '' ), 'disposal' ) ">废弃确认</el-button><el-button v-if=" canCancelDiscard && props . service === 'disposal' && selectedDetail ? . status === 'PENDING_REVIEW' " type=" danger " @click=" cancelSelectedDiscard ">取消申请</el-button><el-button v-if=" canCreate && props . service === 'transfer' && [ 'DRAFT' , 'REJECTED' ]. includes ( selectedDetail ? . status || '' ) " type=" primary " @click=" openEdit ( String ( selectedDetail ? . transferNo || selectedDetail ? .[ '0' ] || '' )) ">编辑</el-button><el-button v-if=" canConfirmTransfer && props . service === 'transfer' && selectedDetail ? . status === 'WAIT_CUSTOMER_CONFIRMATION' " type=" primary " @click=" confirmTransferMeasurements ">确认测量数据</el-button><el-tag v-if=" props . service !== 'transfer' " :type=" props . service === 'labeling' ? relabelStatusTagType ( selectedRelabelDetail ? . application . status || '' ) : undefined " effect=" light ">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
2026-10-16 18:25:56 +08:00
</header>
2026-10-20 15:07:39 +08:00
<div v-if=" detailError " class=" warehouse - detail - error " role=" alert "><strong>{{ detailError }}</strong><el-button @click=" loadDetailPage ">重新加载</el-button></div>
2026-10-19 10:23:47 +08:00
<ReviewRejectionNotice v-if=" ! detailError " :status=" props . service === 'labeling' ? selectedRelabelDetail ? . application . status : selectedDetail ? . status " :reason=" props . service === 'labeling' ? selectedRelabelDetail ? . application . reviewRemark : selectedDetail ? . reviewRemark " />
2026-10-14 11:43:38 +08:00
<div v-if=" ! detailError " class=" warehouse - card warehouse - form - section "><h3>订单概览</h3><div v-if=" props . service === 'labeling' " class=" warehouse - detail - grid "><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>关联单号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>箱数</span><strong :class=" { 'warehouse-missing-value' : selectedRelabelQuantity ? . cartonCount == null } ">{{ selectedRelabelQuantity?.cartonCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if=" props . service === 'transfer' " class=" warehouse - detail - grid warehouse - transfer - overview - grid "><div><span>状态</span><el-tag :type=" transferStatusTagType ( selectedDetail ? . status || '' ) " effect=" light ">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' " class=" warehouse - overview - wide "><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预报 SKU 总件数</span><strong>{{ transferForecastQuantity }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ countedTransferCartons().length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type=" transferRelabelStatusTagType ( transferRelabelStatus ( selectedDetail || {}). code ) " effect=" light ">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(se
2026-10-29 15:49:59 +08:00
<TransferPickupProgressPanel v-if=" ! detailError && props . service === 'transfer' && selectedDetail ? . deliveryMethod === 'WAREHOUSE_PICKUP' " :transfer-no=" String ( selectedDetail . transferNo || '' ) " />
2026-10-28 10:55:19 +08:00
<div v-if=" props . service === 'disposal' " class=" warehouse - card warehouse - form - section ">
2026-10-14 11:43:38 +08:00
<p class=" warehouse - completion - remark ">废弃备注:{{ selectedDetail?.remark || '—' }}</p><h3>本次废弃箱子</h3><div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th><th>废弃托盘</th><th>实际废弃箱唛</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [] " :key=" index "><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }} {{ item.quantityUnit || '单位未记录' }}<br>实际:{{ item.actualDiscardPackages == null ? '未记录' : `${item.actualDiscardPackages} ${item.quantityUnit || '单位未记录'}` }}</td><td>{{ item.cartonMarks?.join('、') || '未关联箱唛' }}</td><td>{{ item.palletNo || '—' }}</td><td>{{ item.actualCartonMarks?.join('、') || '未记录' }}</td></tr></tbody></table></div>
2026-10-16 16:19:31 +08:00
<template v-if=" selectedDetail ? . photos ? . length "><h3>废弃现场照片</h3><div class=" warehouse - photo - grid "><div v-for=" photo in selectedDetail . photos " :key=" photo . fileId " class=" warehouse - photo - item "><el-image v-if=" discardPhotoUrls [ photo . fileId ] " :src=" discardPhotoUrls [ photo . fileId ] " :preview-src-list=" discardPhotoPreviewUrls " :initial-index=" discardPhotoPreviewUrls . indexOf ( discardPhotoUrls [ photo . fileId ]) " fit=" cover " preview-teleported /><div v-else-if=" discardPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadDiscardPhoto ( photo . fileId ) ">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div></template>
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . status !== 'DRAFT' " class=" warehouse - card warehouse - form - section ">
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button v-if=" canIncreaseForecast " type=" primary " size=" small " @click=" openIncreaseForecast "><Plus :size=" 14 " />增加预报箱数</el-button><el-button type=" primary " size=" small " @click=" printTransferCartons ( String ( selectedDetail ? . transferNo || '' )) "><Printer :size=" 14 " />打印预报箱唛</el-button><el-button size=" small " @click=" showForecastCartons = true ">查看预报箱唛</el-button></div></div>
2026-10-28 10:55:19 +08:00
<el-dialog v-model=" showForecastCartons " title=" 预报箱唛明细 " width=" 900 px " align-center destroy-on-close append-to-body><el-table :data=" selectedDetail ? . cartons || [] " size=" small " border><el-table-column prop=" cartonMark " label=" 仓库唛头 " min-width=" 300 " /><el-table-column label=" 预报SKU " min-width=" 240 "><template #default=" { row } ">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label=" 清点状态 " width=" 130 "><template #default=" { row } ">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click=" showForecastCartons = false ">关闭</el-button></template></el-dialog>
2026-10-16 16:19:31 +08:00
<el-dialog v-model=" increaseForecastOpen " title=" 增加预报箱数 " width=" 560 px " align-center destroy-on-close append-to-body :close-on-click-modal=" ! increasingForecast " :show-close=" ! increasingForecast ">
<el-form label-width=" 110 px "><el-form-item label=" 新增箱数 "><el-input-number v-model=" increaseForecastForm . additionalCount " placeholder="" :disabled=" increasingForecast " :min=" 1 " :max=" Math . max ( 1 , 5000 - Number ( forecastSnapshot ? . packageCount || 0 )) " :precision=" 0 " /></el-form-item></el-form>
<template #footer><el-button :disabled=" increasingForecast " @click=" increaseForecastOpen = false ">取消</el-button><el-button type=" primary " :loading=" increasingForecast " @click=" saveIncreaseForecast ">确认增加</el-button></template>
2026-10-15 15:45:22 +08:00
</el-dialog>
</div>
2026-10-16 16:52:49 +08:00
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - cargo - card ">
2026-10-15 15:45:22 +08:00
<div class=" warehouse - section - heading "><h3>预报与实际签收</h3></div>
2026-10-28 10:55:19 +08:00
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - comparison - table "><colgroup><col style=" width : 500 px " /><col style=" width : 140 px " /><col style=" width : 130 px " /><col style=" width : 100 px " /><col style=" width : 130 px " /><col style=" width : 130 px " /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [] " :key=" String ( item . id || index ) "><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '未记录' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if=" ! selectedDetail ? . items ? . length "><td colspan=" 6 "><div class=" warehouse - empty ">暂无货物明细</div></td></tr></tbody></table></div>
2026-10-15 15:45:22 +08:00
<details class=" warehouse - transfer - pallet - group " open>
<summary class=" warehouse - section - heading warehouse - transfer - pallet - heading warehouse - transfer - pallet - summary "><h3>托盘实测数据</h3><span class=" warehouse - transfer - pallet - count ">{{ selectedDetail?.pallets?.length || 0 }} 托</span><span class=" warehouse - transfer - pallet - toggle "><span class=" when - collapsed ">展开托盘</span><span class=" when - expanded ">收起托盘</span><ChevronDown :size=" 14 " /></span></summary>
<div v-if=" selectedDetail ? . pallets ? . length " class=" warehouse - transfer - pallet - list ">
<article v-for=" pallet in selectedDetail . pallets " :key=" String ( pallet . id || pallet . sequenceNo ) " class=" warehouse - transfer - pallet - card ">
<header class=" warehouse - transfer - pallet - header ">
<strong><span>托盘号</span> {{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}</strong>
<dl class=" warehouse - transfer - pallet - metrics ">
<div><dt>箱数</dt><dd>{{ palletCartons(pallet).length }} 箱</dd></div><div><dt>件数</dt><dd>{{ palletQuantity(pallet) }} 件</dd></div><div><dt>SKU 种数</dt><dd>{{ palletSkuRows(pallet).length }} 种</dd></div><div><dt>毛重(kg)</dt><dd>{{ pallet.grossWeightKg ?? '—' }}</dd></div><div><dt>体积(m³)</dt><dd>{{ pallet.volumeCbm ?? '—' }}</dd></div>
</dl>
</header>
<div class=" warehouse - transfer - pallet - skus "><span>SKU 汇总</span><div><div v-for=" item in palletSkuRows ( pallet ) " :key=" item . sku " class=" warehouse - transfer - pallet - sku "><strong>{{ item.sku }}</strong><span>{{ item.quantity }} 件</span></div><span v-if=" ! palletSkuRows ( pallet ). length ">暂无已清点 SKU</span></div></div>
<div class=" warehouse - transfer - pallet - meta "><span>库位:{{ pallet.warehouseLocationCode || '—' }}</span><span>完成时间:{{ formatDateTime(pallet.completedAt) }}</span></div>
2026-10-28 10:55:19 +08:00
</article>
2026-10-15 15:45:22 +08:00
</div>
2026-10-28 10:55:19 +08:00
<div v-else class=" warehouse - empty ">暂无托盘数据</div>
2026-09-23 11:25:35 +08:00
</details>
2026-10-15 15:45:22 +08:00
<div v-if=" selectedDetail ? . measurementPhotos ? . length " class=" warehouse - evidence warehouse - measurement - evidence "><span>现场照片</span><div v-if=" selectedDetail ? . measurementPhotos ? . length " class=" warehouse - photo - grid warehouse - measurement - photo - grid "><div v-for=" ( photo , index ) in selectedDetail . measurementPhotos " :key=" photo . fileId " class=" warehouse - photo - item warehouse - measurement - photo - item " :title=" photo . fileName || `测量照片 ${ index + 1 } ` "><el-image v-if=" measurementPhotoUrls [ photo . fileId ] " :src=" measurementPhotoUrls [ photo . fileId ] " :preview-src-list=" measurementPhotoPreviewUrls " :initial-index=" measurementPhotoPreviewUrls . indexOf ( measurementPhotoUrls [ photo . fileId ]) " :alt=" photo . fileName || `测量照片 ${ index + 1 } ` " fit=" cover " preview-teleported /><div v-else-if=" measurementPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片 {{ index + 1 }} 加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadMeasurementPhoto ( photo . fileId ) ">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
2026-10-16 16:19:31 +08:00
</div>
2026-10-28 10:55:19 +08:00
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - received - card ">
2026-10-15 15:45:22 +08:00
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button type=" primary " size=" small " :disabled=" ! countedTransferCartons (). length " @click=" showTransferCartons = true ">查看箱号明细</el-button></div></div>
<div v-if=" ! countedTransferCartons (). length " class=" warehouse - empty ">暂无签收箱号</div>
<el-dialog v-model=" showTransferCartons " title=" 签收箱号明细 " width=" 1120 px " align-center destroy-on-close append-to-body>
<div class=" warehouse - cartons - dialog - body ">
2026-10-15 18:25:32 +08:00
<el-collapse>
2026-10-15 15:45:22 +08:00
<el-collapse-item v-for=" carton in countedTransferCartons () " :key=" carton . cartonMark " :name=" carton . cartonMark ">
<template #title><strong>{{ carton.cartonMark }}</strong><span class=" warehouse - carton - summary ">已确认 · {{ transferCartonLines(carton).length }} 种 SKU · {{ carton.quantity ?? '—' }} 件</span></template>
2026-10-15 18:25:32 +08:00
<div class=" warehouse - carton - measurements ">所属托盘:{{ selectedDetail?.pallets?.find((pallet) => carton.transferPalletId != null && String(pallet.id) === String(carton.transferPalletId))?.palletNo || '散箱' }} · 清点时间:{{ formatDateTime(carton.countedAt) }}</div>
2026-10-15 15:45:22 +08:00
<div class=" warehouse - carton - measurements ">长 {{ carton.length ?? '—' }} cm · 宽 {{ carton.width ?? '—' }} cm · 高 {{ carton.height ?? '—' }} cm · 重量 {{ carton.weight ?? '—' }} kg · 体积 {{ [carton.length, carton.width, carton.height].every(value => value != null) ? (Number(carton.length) * Number(carton.width) * Number(carton.height) / 1000000).toFixed(6) : '—' }} m³</div>
<el-table :data=" transferCartonLines ( carton ) " size=" small " border>
<el-table-column prop=" skuCode " label=" SKU " min-width=" 220 " />
<el-table-column prop=" productNameZh " label=" 中文品名 " min-width=" 180 " />
<el-table-column prop=" quantity " label=" 实际签收数量 " width=" 130 " />
</el-table>
<p v-if=" carton . discrepancyReason " class=" warehouse - carton - measurements ">差异说明:{{ carton.discrepancyReason }}</p>
</el-collapse-item>
</el-collapse>
</div>
2026-09-13 21:10:14 +08:00
<template #footer><el-button @click=" showTransferCartons = false ">关闭</el-button></template>
2026-09-17 16:34:51 +08:00
</el-dialog>
</div>
2026-10-16 14:22:00 +08:00
<div v-if=" props . service === 'transfer' && selectedDetail ? . needRelabel " class=" warehouse - card warehouse - form - section ">
2026-09-17 16:34:51 +08:00
<h3>换标箱级去向</h3>
2026-10-16 18:25:56 +08:00
<div class=" warehouse - table - scroll "><table><thead><tr><th>换标类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>新 FBA 箱唛</th><th>实际 FBA 目的仓</th><th>数量</th><th>备注</th></tr></thead><tbody><tr v-for=" ( binding , index ) in selectedDetail ? . relabelBindings || [] " :key=" ` ${ binding . customerCartonMark } - ${ index } ` "><td>{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}</td><td>{{ binding.customerCartonMark || '—' }}</td><td>{{ binding.originalSku || '—' }}</td><td>{{ binding.newSku || '—' }}</td><td>{{ binding.fbaCartonMark || '—' }}</td><td>{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}</td><td>{{ binding.quantity ?? '—' }}</td><td>{{ binding.remark || '—' }}</td></tr><tr v-if=" ! selectedDetail ? . relabelBindings ? . length "><td colspan=" 8 "><div class=" warehouse - empty ">暂无换标绑定明细</div></td></tr></tbody></table></div>
2026-09-15 15:20:35 +08:00
</div>
2026-09-10 15:34:03 +08:00
<div v-if=" props . service === 'labeling' && selectedRelabelDetail " class=" warehouse - card warehouse - form - section ">
<div class=" warehouse - section - heading "><h3>换标完成与出库</h3></div>
2026-09-23 11:25:35 +08:00
<div class=" warehouse - detail - grid warehouse - fulfillment - grid ">
2026-09-10 15:34:03 +08:00
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
2026-10-16 18:25:56 +08:00
<div><span>履约状态</span><el-tag :type=" relabelFulfillmentTagType ( selectedRelabelDetail ? . application . fulfillmentStatus ) " effect=" light ">{{ relabelFulfillmentLabel(selectedRelabelDetail?.application.fulfillmentStatus) }}</el-tag></div>
2026-09-10 15:34:03 +08:00
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
2026-10-28 10:55:19 +08:00
<div v-if=" selectedRelabelDetail "><RelabelDocumentsPanel inline :relabel-no=" selectedRelabelDetail . application . relabelNo " :detail=" selectedRelabelDetail " /></div>
2026-09-10 15:34:03 +08:00
</div>
<div v-if=" selectedRelabelDetail ? . completionEvidence ? . photoFileIds ? . length " class=" warehouse - evidence "><span>仓库完工照片</span><div v-if=" selectedRelabelDetail ? . completionEvidence ? . photoFileIds ? . length " class=" warehouse - photo - grid "><div v-for=" ( fileId , index ) in selectedRelabelDetail . completionEvidence . photoFileIds " :key=" fileId " class=" warehouse - photo - item "><el-image v-if=" completionPhotoUrls [ fileId ] " :src=" completionPhotoUrls [ fileId ] " :preview-src-list=" completionPhotoPreviewUrls " :initial-index=" completionPhotoPreviewUrls . indexOf ( completionPhotoUrls [ fileId ]) " fit=" cover " preview-teleported /><div v-else-if=" completionPhotoLoading [ fileId ] " class=" warehouse - photo - placeholder ">照片 {{ index + 1 }} 加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadCompletionPhoto ( fileId ) ">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
2026-10-16 16:52:49 +08:00
<p v-if=" selectedRelabelDetail ? . completionEvidence ? . completionRemark " class=" warehouse - completion - remark ">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
2026-09-14 16:51:43 +08:00
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . relabelNo " class=" warehouse - card warehouse - form - section "><RelabelDocumentsPanel :relabel-no=" selectedDetail . relabelNo " /></div>
2026-10-20 14:52:22 +08:00
<div v-if=" props . service === 'labeling' " class=" warehouse - card warehouse - form - section ">
2026-09-14 16:51:43 +08:00
<div class=" warehouse - section - heading "><h3>贴标明细资料</h3><input ref=" relabelFileInput " class=" warehouse - hidden - input " type=" file " accept=" application / pdf ,. pdf " @change=" uploadSelectedRelabelFile "></div>
2026-10-20 11:54:44 +08:00
2026-09-14 16:51:43 +08:00
<div class=" warehouse - table - scroll "><table class=" warehouse - relabel - detail - table "><thead><tr><th>换标模式</th><th>仓库唛头</th><th>SKU</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 换标' : ['SKU_LABEL', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType || '') ? 'SKU 换标' : '箱唛换标' }}</td><td>{{ item.cartonMark || '—' }}</td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</td><td>{{ item.relabelFileName || '未上传' }}</td><td><el-button size=" small " @click=" chooseRelabelFile ( 'ITEM' , String ( item . id )) "><Upload :size=" 14 " />上传明细文件</el-button></td></tr>
<tr v-if=" ! selectedRelabelDetail ? . items ? . length "><td colspan=" 5 "><div class=" warehouse - empty ">暂无贴标明细</div></td></tr>
2026-09-06 18:11:42 +08:00
</tbody></table></div>
2026-10-28 11:30:12 +08:00
</div>
2026-10-20 11:54:44 +08:00
<div v-if=" props . service === 'labeling' " class=" warehouse - card warehouse - form - section ">
2026-10-20 15:07:39 +08:00
<h3>换标任务明细</h3>
2026-09-08 20:28:05 +08:00
<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>
2026-09-06 18:11:42 +08:00
<tr v-for=" item in selectedRelabelDetail ? . items || [] " :key=" item . id "><td>{{ item.operationType === 'CARTON_AND_SKU' ? '箱唛 + SKU 换标' : ['SKU_LABEL', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType || '') ? 'SKU 换标' : '箱唛换标' }}</td><td><strong>{{ item.cartonMark || '—' }}</strong></td><td><strong>{{ item.fbaCartonMark || '—' }}</strong></td><td>{{ warehouseDisplay(item.destinationFbaWarehouseCode) }}</td><td><span v-if=" item . totalPackageCount == null " class=" warehouse - missing - value ">—</span><template v-else>{{ item.totalPackageCount }} {{ String(item.operationType) === 'RETURN_BULK_SKU_LABEL' ? '件' : '箱任务' }}</template></td><td>{{ relabelSkuCodes(item).join('、') || '—' }}</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>
2026-08-21 17:20:51 +08:00
</div>
2026-10-15 15:45:22 +08:00
</template>
2026-08-21 17:20:51 +08:00
</section>
<CompletionConfirmationDialog v-model=" completionConfirmOpen " :kind=" completionConfirmKind " :document-no=" completionConfirmNo " @confirmed=" refreshAfterCompletion " />
</template>
2026-10-20 15:07:39 +08:00
2026-10-13 18:06:28 +08:00
<style scoped>
2026-10-13 17:43:05 +08:00
.warehouse-service-page .warehouse-missing-value { color: #a8abb2; font-weight: 400; }
.warehouse-table-card :deep(.related-business-number-cell .cell) { white-space:normal; overflow:visible; text-overflow:clip; overflow-wrap:anywhere; padding-top:6px; padding-bottom:6px; }
.labeling-type-tags { display:inline-flex; flex-wrap:wrap; gap:4px; }
.labeling-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,320px)); gap:14px 18px; align-items:end; }
.labeling-query :deep(.query-toolbar__filters) { display:contents; }
.labeling-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
.labeling-query :deep(.query-toolbar__field:nth-child(-n+3)) { grid-row:1; }
.labeling-query :deep(.query-toolbar__field:nth-child(n+4)) { grid-row:2; }
.labeling-query :deep(.query-toolbar__label) { line-height:18px; }
.labeling-query :deep(.query-toolbar__actions) { grid-column:4; grid-row:1; margin:0; padding:0; border:0; align-self:end; }
.transfer-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(5,minmax(0,280px)); gap:14px 18px; align-items:end; }
.transfer-query :deep(.query-toolbar__filters) { display:contents; }
.transfer-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
.transfer-query :deep(.query-toolbar__field:nth-child(-n+5)) { grid-row:1; }
.transfer-query :deep(.query-toolbar__field:nth-child(n+6)) { grid-row:2; }
.transfer-query :deep(.query-toolbar__label) { line-height:18px; }
.transfer-query :deep(.query-toolbar__actions) { grid-column:5; grid-row:2; margin:0; padding:0; border:0; align-self:end; }
.disposal-query :deep(.query-toolbar__field:nth-child(5)) { grid-column:1; grid-row:2; }
.disposal-query :deep(.query-toolbar__actions) { grid-column:2; }
@media(max-width:1200px) {
.labeling-query.query-toolbar--grid { grid-template-columns:repeat(2,minmax(0,320px)); }
.labeling-query :deep(.query-toolbar__field:nth-child(n)) { grid-row:auto; }
.labeling-query :deep(.query-toolbar__actions) { grid-column:auto; grid-row:auto; }
.transfer-query.query-toolbar--grid { grid-template-columns:repeat(2,minmax(0,320px)); }
.transfer-query :deep(.query-toolbar__field:nth-child(n)) { grid-row:auto; }
.disposal-query :deep(.query-toolbar__field:nth-child(5)) { grid-column:auto; }
2026-10-09 11:48:52 +08:00
.transfer-query :deep(.query-toolbar__actions) { grid-column:auto; grid-row:auto; }
}
.warehouse-order-info-heading { display: flex; align-items: center; justify-content: space-between; max-width: 1296px; margin-bottom: 16px; border-bottom: 1px solid #edf0f4; padding-bottom: 10px; }
.warehouse-form-section .warehouse-order-info-heading h3 { margin: 0; padding: 0; border: 0; }
.warehouse-order-info-summary { display: flex; flex-wrap: wrap; gap: 10px 24px; color: #64748b; font-size: 12px; }
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-labeling-mode {
width: fit-content;
max-width: 1296px;
box-sizing: border-box;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
padding: 8px 12px;
gap: 14px;
margin: 20px 0 22px;
}
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-labeling-mode > span {
color: #475569;
font-size: 12px;
}
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll th {
background: #f0f5fc;
color: #385579;
border-bottom: 1px solid #dce6f2;
height: 38px;
font-weight: 600;
}
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button) {
--el-color-primary: #2563eb;
--el-radio-button-checked-bg-color: #2563eb;
--el-radio-button-checked-border-color: #2563eb;
}
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button:not(.is-disabled) .el-radio-button__inner) {
border-color: #cbdaf0;
}
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-group) { gap: 6px; }
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button .el-radio-button__inner) {
padding: 8px 12px;
border: 1px solid #dce3ec;
border-radius: 5px;
box-shadow: none;
font-size: 12px;
}
.warehouse-service-page--return-relabel .warehouse-labeling-mode :deep(.el-radio-button.is-disabled .el-radio-button__inner) {
color: #a3adbb;
background: #f1f4f8;
border-color: #e5eaf0;
}
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll td { padding: 10px 12px; }
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-table-scroll h3 {
font-size: 14px;
margin-bottom: 12px;
padding-bottom: 10px;
}
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-sku-label-table :deep(.el-input__wrapper) {
min-height: 30px;
box-shadow: 0 0 0 1px #dce3ec inset;
border-radius: 5px;
}
2026-09-18 16:48:35 +08:00
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-sku-label-table :deep(.el-input-number) { width: 120px; }
.warehouse-service-page--return-relabel .warehouse-transfer-cargo-card .warehouse-tab-actions { margin-bottom: 10px; }
.warehouse-service-page{display:grid;gap:12px;color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:0;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff;box-shadow:none}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-list-notice{padding:8px 12px;border-bottom:1px solid var(--client-border);background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:72px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;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:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}}
2026-09-09 13:35:51 +08:00
.warehouse-service-page--detail{display:block;overflow:hidden;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff}.warehouse-detail-header{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;min-height:64px;padding:10px 16px;border-bottom:1px solid var(--client-border)}.warehouse-detail-back{display:inline-flex;align-items:center;gap:5px;padding:5px 0;border:0;background:transparent;color:var(--el-color-primary);cursor:pointer}.warehouse-detail-title{display:flex;min-width:0;flex-direction:column;gap:3px}.warehouse-detail-title strong{overflow:hidden;color:#1f2d45;font-size:18px;text-overflow:ellipsis;white-space:nowrap}.warehouse-detail-title span{color:#7c899c;font-size:12px}.warehouse-detail-actions{display:flex;align-items:center;gap:8px}.warehouse-service-page--detail>.warehouse-card{border:0;border-top:1px solid var(--client-border);border-radius:0}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card{border-top:0}.warehouse-service-page--detail>.warehouse-form-section{padding:0}.warehouse-service-page--detail .warehouse-form-section h3{min-height:40px;margin:0;padding:11px 16px 9px;border-bottom:1px solid #edf1f5;font-size:14px}.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin:0}.warehouse-service-page--detail .warehouse-detail-grid>div{min-height:48px;flex-direction:row;align-items:center;gap:10px;padding:8px 14px;border-right:1px solid #edf1f5;border-bottom:1px solid #edf1f5}.warehouse-service-page--detail .warehouse-detail-grid span{flex:0 0 90px}.warehouse-service-page--detail .warehouse-detail-grid strong{min-width:0;overflow-wrap:anywhere}.warehouse-service-page--detail .warehouse-fulfillment-grid{margin-bottom:0}.warehouse-service-page--detail .warehouse-evidence{margin:12px 14px}.warehouse-service-page--detail .warehouse-completion-remark,.warehouse-service-page--detail .warehouse-hint{margin:10px 14px}.warehouse-service-page--detail .warehouse-table-scroll table{margin:0}.warehouse-service-page--detail .warehouse-empty{height:64px}@media(max-width:1100px){.warehouse-service-page--detail .warehouse-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.warehouse-detail-header{grid-template-columns:1fr auto}.warehouse-detail-back{grid-column:1/-1}.warehouse-detail-actions{justify-content:flex-end}}
2026-09-10 15:34:03 +08:00
.warehouse-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}
2026-09-08 20:28:05 +08:00
.warehouse-checkbox-field{display:flex;min-height:32px;align-items:center;justify-content:flex-start;gap:16px}.warehouse-checkbox-field>span{color:#596579;font-size:12px}.warehouse-checkbox-field :deep(.el-checkbox){margin-right:0}
2026-09-14 16:51:43 +08:00
.warehouse-choice-field{display:grid;grid-template-columns:150px auto 1fr;align-items:center;gap:14px;min-height:40px;padding:12px 14px;border:1px solid #e1e8f2;border-radius:5px;background:#f8faff}.warehouse-choice-field>span{color:#34445c;font-size:13px;font-weight:650}.warehouse-choice-field>span i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-choice-field>small{color:#7a879a;font-size:12px}.warehouse-receiving-card{display:grid;grid-template-columns:2fr 1fr 1fr;gap:22px;padding:15px 16px;border:1px solid #cfe0f5;border-radius:5px;background:#f6faff}.warehouse-receiving-card div{display:flex;min-width:0;flex-direction:column;gap:6px}.warehouse-receiving-card span{color:#7c899c;font-size:12px}.warehouse-receiving-card strong{overflow-wrap:anywhere;color:#314561;font-size:13px;font-weight:600}
2026-09-17 13:36:20 +08:00
.warehouse-hidden-input{display:none}.warehouse-label-table{min-width:1120px}.warehouse-attachment-tip{margin:10px 0 12px;color:#7d8797;font-size:12px}.warehouse-mark-mapping{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 8px;padding:12px;border:1px solid #dce7f5;border-radius:5px;background:#f7faff}.warehouse-mark-mapping strong{width:100%;color:#34445c}.warehouse-mark-mapping span{padding:5px 9px;border-radius:3px;background:#eaf2fc;color:#3e6593;font-size:12px}
2026-09-20 16:55:27 +08:00
.warehouse-upload-summary{display:flex;flex-wrap:wrap;gap:8px 24px;margin:-4px 0 14px;padding:9px 12px;border:1px solid #dce7f5;border-radius:4px;background:#f8fbff;color:#596579;font-size:12px}
2026-09-17 13:36:20 +08:00
.warehouse-upload-summary>span{display:inline-flex;align-items:center;gap:6px;overflow-wrap:anywhere}
.warehouse-file-groups{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;margin:0 0 18px}.warehouse-file-groups--single{grid-template-columns:minmax(0,1fr)}.warehouse-file-group{min-width:0;overflow:hidden;border:1px solid #dde5f0;border-radius:8px;background:#fbfcfe;box-shadow:0 1px 2px rgba(31,54,86,.04)}.warehouse-file-group--primary{border-color:#cbdcf2;background:linear-gradient(180deg,#f7faff 0,#fbfdff 100%)}.warehouse-file-group__heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px 16px;border-bottom:1px solid #e7edf5;background:rgba(255,255,255,.68)}.warehouse-file-group__heading strong{display:flex;align-items:center;gap:8px;color:#2d405c;font-size:14px}.warehouse-file-group__heading strong>span{display:inline-grid;width:22px;height:22px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-file-group__heading em{flex-shrink:0;padding:4px 8px;border-radius:10px;background:#edf4fd;color:#4774aa;font-size:11px;font-style:normal}.warehouse-file-options{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:stretch;padding:14px}.warehouse-file-option{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px 12px;align-items:center;min-width:0;padding:12px;border:1px dashed #ced8e6;border-radius:6px;background:#fff}.warehouse-file-option.is-selected{border-style:solid;border-color:#79a8df;background:#f5f9ff}.warehouse-file-option__name strong{color:#34445a;font-size:13px}.warehouse-file-option__status{display:flex;grid-column:1/-1;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:7px 8px;border-radius:4px;background:#eaf3ff;color:#376da9;font-size:11px}.warehouse-file-option__status>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.warehouse-file-option__status.is-empty{background:#f4f6f8;color:#9aa4b2}.warehouse-file-choice-or{display:grid;color:#9ba6b5;font-size:11px;place-items:center}.warehouse-file-choice-or.is-both{color:#3478c8;font-weight:700}.warehouse-template-list{display:flex;flex-direction:column;padding:7px 14px}.warehouse-template-list>div{display:grid;grid-template-columns:minmax(150px,1fr) auto auto;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid #edf1f6}.warehouse-template-list>div:last-child{border-bottom:0}.warehouse-template-list strong{color:#3f4e63;font-size:12px}.warehouse-transfer-template-group{margin:0 0 14px}@media(max-width:1180px){.warehouse-file-groups{grid-template-columns:minmax(0,1fr)}}@media(max-width:720px){.warehouse-file-options{grid-template-columns:minmax(0,1fr)}.warehouse-file-choice-or{height:24px}.warehouse-template-list>div{grid-template-columns:minmax(0,1fr) auto}.warehouse-template-list>div>span{grid-column:1/-1}}
.warehouse-file-clear{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:24px;height:24px;border:0;border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
2026-09-08 20:28:05 +08:00
.warehouse-file-clear:hover:not(:disabled){color:#dc2626;background:#fee2e2}
2026-09-10 15:56:39 +08:00
.warehouse-file-clear:disabled{opacity:.5;cursor:not-allowed}
2026-09-06 18:11:42 +08:00
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
2026-10-09 11:48:52 +08:00
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-labeling-heading{align-items:flex-start}.warehouse-labeling-heading h3{margin-bottom:16px}.warehouse-template-actions{display:flex;gap:8px;padding-bottom:12px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
.warehouse-file-groups--compact {
width: 100%;
max-width: 1296px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
align-items: stretch;
}
.warehouse-file-groups--compact .warehouse-file-group__heading {
padding: 10px 12px;
align-items: center;
}
.warehouse-file-groups--compact .warehouse-file-options {
padding: 10px;
grid-template-columns: minmax(0, 1fr) 20px minmax(0, 1fr);
}
.warehouse-file-groups--compact .warehouse-file-option {
padding: 8px;
gap: 8px;
}
.warehouse-file-groups--compact .warehouse-file-option__name strong { font-size: 12px; }
.warehouse-file-groups--compact .warehouse-file-option :deep(.el-button) {
padding: 6px 8px;
font-size: 12px;
}
.warehouse-file-groups--compact .warehouse-template-list { padding: 4px 12px; }
.warehouse-file-groups--compact .warehouse-template-list > div {
grid-template-columns: minmax(0, 1fr) auto auto;
padding: 7px 0;
}
@media (max-width: 1180px) {
.warehouse-file-groups--compact { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 720px) {
.warehouse-file-groups--compact .warehouse-file-options { grid-template-columns: minmax(0, 1fr); }
2026-09-15 15:20:35 +08:00
.warehouse-file-groups--compact .warehouse-template-list > div { grid-template-columns: minmax(0, 1fr) auto; }
2026-09-17 13:36:20 +08:00
}
2026-09-23 11:25:35 +08:00
.warehouse-native-file{min-height:32px;padding:5px 8px;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266}.warehouse-fulfillment-grid{margin-bottom:18px}.warehouse-evidence{display:flex;align-items:flex-start;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{padding-top:6px;color:#71809a;font-size:12px;white-space:nowrap}.warehouse-evidence>strong{padding-top:6px;color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-grid{display:grid;grid-template-columns:repeat(4,140px);gap:12px}.warehouse-photo-item{width:140px;height:105px;overflow:hidden;border:1px solid #dce3ed;border-radius:5px;background:#fff}.warehouse-photo-item :deep(.el-image){display:block;width:100%;height:100%;cursor:zoom-in}.warehouse-photo-placeholder{display:flex;width:100%;height:100%;align-items:center;justify-content:center;padding:10px;border:0;background:#f4f7fb;color:#8792a2;font-size:12px;text-align:center}.warehouse-photo-retry{cursor:pointer}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
2026-10-15 15:45:22 +08:00
.warehouse-measurement-evidence{margin-top:16px}
2026-09-10 17:12:52 +08:00
.warehouse-measurement-photo-grid{flex:1;grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
2026-09-20 17:53:41 +08:00
.warehouse-measurement-photo-item{width:100%;height:120px}
2026-10-20 11:54:44 +08:00
.warehouse-template-actions{flex-wrap:wrap;justify-content:flex-end}.warehouse-import-preview{margin:0 0 16px;padding:14px;border:1px solid #cfe0f5;border-radius:5px;background:#f8fbff}.warehouse-import-preview .warehouse-section-heading{margin-bottom:10px}.warehouse-import-preview .warehouse-section-heading>div{display:flex;align-items:center;gap:10px}.warehouse-import-preview p{margin:0;color:#7d8797;font-size:12px}.warehouse-binding-preview-table{min-width:980px;background:#fff}
2026-09-23 09:06:20 +08:00
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-file-groups{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);align-items:stretch;gap:14px;margin-bottom:14px}.warehouse-transfer-total-files,.warehouse-transfer-template-group{height:100%;margin:0}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
2026-10-20 11:54:44 +08:00
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,var(--transfer-left,43fr)) 16px minmax(0,var(--transfer-right,57fr));column-gap:0;align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:3;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px minmax(0,1fr);gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}.warehouse-step-heading{align-items:flex-start}.warehouse-step-heading h3{display:flex;align-items:center;gap:9px}.warehouse-step-number{display:inline-grid;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-heading-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.warehouse-transfer-template-group{margin:0 0 14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#fbfcfe}.warehouse-binding-heading{margin-top:14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#f8faff}.warehouse-binding-heading strong{display:flex;align-items:center;gap:8px;color:#34445c;font-size:13px}
2026-09-20 17:53:41 +08:00
.warehouse-transfer-form .warehouse-choice-field--relabel{grid-template-columns:150px auto minmax(240px,1fr)}.warehouse-choice-field--relabel :deep(.el-radio-group){display:flex;flex-wrap:nowrap}.warehouse-choice-field--relabel>small{min-width:0;margin:0;line-height:1.5}
2026-09-14 12:01:13 +08:00
@media(max-width:1250px){.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
2026-09-23 09:06:20 +08:00
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}}
2026-09-23 11:25:35 +08:00
.warehouse-transfer-form .pickup-address-field{grid-column:span 1}
.warehouse-transfer-main-card .warehouse-form-grid{grid-template-columns:repeat(2,minmax(280px,640px))}.warehouse-transfer-cargo-card .warehouse-section-heading,.warehouse-transfer-cargo-card .warehouse-table-scroll{width:100%;max-width:1296px}
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-section-heading,.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-table-scroll{max-width:none}
.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:repeat(4,minmax(180px,1fr));gap:6px 28px;padding:10px 16px 12px}.warehouse-service-page--detail .warehouse-transfer-overview-grid>div{min-height:26px;padding:2px 0;border:0}.warehouse-service-page--detail .warehouse-transfer-overview-grid span{flex:0 0 auto;min-width:76px}.warehouse-service-page--detail .warehouse-transfer-overview-grid :deep(.el-tag){height:24px}
2026-10-15 15:45:22 +08:00
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid{grid-template-columns:repeat(4,minmax(180px,1fr));gap:6px 28px;padding:10px 16px 12px}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid>div{min-height:26px;padding:2px 0;border:0}.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-detail-grid span{flex:0 0 auto;min-width:76px}
2026-09-23 11:25:35 +08:00
.warehouse-service-page--detail .warehouse-transfer-overview-grid>.warehouse-overview-wide{grid-column:span 2}
2026-09-15 15:20:35 +08:00
.warehouse-transfer-received-card{margin-top:14px}.warehouse-cartons-dialog-body{max-height:65vh;overflow:auto}.warehouse-carton-summary{margin-left:16px;color:var(--muted);font-size:12px}.warehouse-carton-measurements{margin:10px 0;color:var(--muted);font-size:13px;line-height:1.8}
@media(max-width:1100px){.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:repeat(2,minmax(180px,1fr))}}@media(max-width:720px){.warehouse-service-page--detail .warehouse-transfer-overview-grid{grid-template-columns:1fr;gap:4px;padding:10px 14px}}
/* Keep the count and controls visible together on narrow list pages. */
.warehouse-pagination { justify-content: flex-end; }
.warehouse-pagination-total { color: #526176; font-size: 14px; }
.warehouse-pagination :deep(.el-pagination) { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; margin: 0; }
.warehouse-page-size-select { width: 112px; margin-left: 8px; }
2026-09-28 17:26:37 +08:00
.warehouse-page-size-select :deep(.el-input__inner), .warehouse-page-size-select :deep(.el-select__selected-item) { color: #526176 !important; opacity: 1 !important; -webkit-text-fill-color: #526176; }
2026-10-20 11:54:44 +08:00
.warehouse-list-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.warehouse-disposal-field { max-width: 520px; }
.warehouse-service-page--disposal-form .warehouse-form-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
.warehouse-service-page--disposal-form .warehouse-disposal-field,
.warehouse-service-page--disposal-form .warehouse-order-info-heading,
2026-09-15 15:20:35 +08:00
.warehouse-service-page--disposal-form .warehouse-section-heading,
.warehouse-service-page--disposal-form .warehouse-table-scroll { width:100%; max-width:none; }
2026-09-23 09:06:20 +08:00
@media (max-width: 720px) {
.warehouse-pagination :deep(.el-pagination) { width: 100%; justify-content: flex-end; }
.warehouse-transfer-main-card .warehouse-form-grid { grid-template-columns: minmax(0, 1fr); }
2026-09-15 15:20:35 +08:00
.warehouse-transfer-cargo-card .warehouse-section-heading, .warehouse-transfer-cargo-card .warehouse-table-scroll { max-width: none; }
2026-10-15 15:45:22 +08:00
.warehouse-transfer-form .warehouse-choice-field--relabel { grid-template-columns: 1fr; align-items: start; }
2026-10-20 11:54:44 +08:00
}
.warehouse-transfer-form .warehouse-form-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
.warehouse-transfer-form .warehouse-form-section { min-width:0; }
.warehouse-transfer-form .warehouse-section-heading,
.warehouse-transfer-form .warehouse-table-scroll,
.warehouse-transfer-form .warehouse-order-info-heading,
2026-08-21 17:20:51 +08:00
.warehouse-transfer-form .warehouse-file-groups--compact { width:100%; max-width:none; }
2026-10-14 17:41:04 +08:00
.warehouse-transfer-form--relabel .warehouse-transfer-divider { grid-column:2; grid-row:1 / span 2; }
</style>
<style scoped>
.warehouse-discard-boxes { margin-bottom: 24px; }
2026-10-15 15:45:22 +08:00
.warehouse-discard-box-toolbar { display: flex; align-items: center; gap: 20px; margin-bottom: 16px; }
.warehouse-discard-box-toolbar .el-input { max-width: 340px; }
.warehouse-service-page--detail .warehouse-table-scroll .warehouse-transfer-comparison-table{width:1130px;min-width:1130px;table-layout:fixed}
.warehouse-transfer-comparison-table td{white-space:normal;overflow-wrap:anywhere}
.warehouse-service-page--detail .warehouse-transfer-comparison-table th{height:36px;padding:0 10px}
.warehouse-service-page--detail .warehouse-transfer-comparison-table td{height:38px;padding:7px 10px}
.warehouse-transfer-comparison-table th:nth-child(n+3),.warehouse-transfer-comparison-table td:nth-child(n+3){text-align:center}
.warehouse-transfer-comparison-table td:first-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-transfer-pallet-heading{margin-top:12px}
.warehouse-transfer-pallet-list{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;padding:0 14px;margin-bottom:12px;align-items:start}
.warehouse-transfer-pallet-card{padding:14px;border:1px solid var(--client-border);border-radius:10px;background:#fff;min-width:0}
.warehouse-transfer-pallet-header{display:flex;align-items:center;flex-wrap:wrap;gap:10px 20px}
.warehouse-transfer-pallet-header>strong{font-size:13px;color:var(--navy);overflow-wrap:anywhere;line-height:1.6}
.warehouse-transfer-pallet-header strong>span{display:inline-block;margin-right:8px;padding:2px 7px;border-radius:4px;background:#eef1ff;color:var(--primary);font-size:12px;font-weight:500}
.warehouse-transfer-pallet-metrics{display:flex;flex-wrap:wrap;gap:8px;margin:0}
.warehouse-transfer-pallet-metrics>div{display:flex;align-items:baseline;gap:8px;padding:6px 10px;background:#f5f7fc;border:1px solid #e9edf5;border-radius:6px}
.warehouse-transfer-pallet-metrics dt{font-size:12px;color:var(--muted)}
.warehouse-transfer-pallet-metrics dd{margin:0;font-size:13px;font-weight:600;color:var(--navy)}
.warehouse-transfer-pallet-summary{cursor:pointer;list-style:none;border-radius:4px}
.warehouse-transfer-pallet-summary::-webkit-details-marker{display:none}
.warehouse-transfer-pallet-summary:focus-visible{outline:2px solid var(--primary);outline-offset:4px}
.warehouse-transfer-pallet-toggle{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:6px}
.warehouse-transfer-pallet-count{font-size:12px;color:var(--muted)}
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-detail-header{grid-template-columns:auto minmax(0,max-content) auto;justify-content:start;column-gap:14px}
.warehouse-service-page--detail .warehouse-transfer-pallet-heading h3{flex:0 0 auto}
.warehouse-service-page--detail .warehouse-transfer-pallet-heading .warehouse-transfer-pallet-count{flex:0 0 auto}
.warehouse-service-page--detail .warehouse-transfer-cargo-card .warehouse-transfer-pallet-heading{display:flex;align-items:center;justify-content:flex-start;gap:10px}
.warehouse-transfer-pallet-toggle{color:var(--primary);font-size:12px;gap:4px}
.warehouse-transfer-pallet-group .when-expanded{display:none}
.warehouse-transfer-pallet-group[open] .when-expanded{display:inline}
.warehouse-transfer-pallet-group[open] .when-collapsed{display:none}
.warehouse-transfer-pallet-group[open] .warehouse-transfer-pallet-toggle svg{transform:rotate(180deg)}
.warehouse-transfer-pallet-meta{display:flex;flex-wrap:wrap;gap:4px 20px;margin-top:8px;color:var(--muted);font-size:12px;line-height:1.7;overflow-wrap:anywhere}
.warehouse-transfer-pallet-skus{display:grid;grid-template-columns:64px minmax(0,1fr);align-items:start;gap:10px;margin-top:10px;padding-top:10px;border-top:1px solid var(--client-border)}
.warehouse-transfer-pallet-skus>span{font-size:12px;color:var(--muted);padding-top:5px}
.warehouse-transfer-pallet-skus>div{display:flex;flex-wrap:wrap;gap:6px;min-width:0}
.warehouse-transfer-pallet-sku{display:inline-flex;align-items:center;gap:8px;max-width:100%;box-sizing:border-box;padding:4px 8px;background:#f5f7ff;border:1px solid #e5e9f8;border-radius:5px;font-size:12px;line-height:1.6}
2026-10-16 18:25:56 +08:00
.warehouse-transfer-pallet-sku strong{font-weight:500;overflow-wrap:anywhere;min-width:0;color:var(--navy)}
.warehouse-transfer-pallet-sku>span{flex-shrink:0;padding:0 7px;border:1px solid #e0e6ff;border-radius:4px;background:#eef1ff;color:var(--primary);font-weight:600;white-space:nowrap}
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading{box-sizing:border-box;min-height:44px;justify-content:flex-start;flex-wrap:wrap;gap:10px 16px;padding:8px 16px;border-bottom:1px solid #edf1f5}
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading h3{flex:0 0 auto;min-height:0;margin:0;padding:0;border:0;line-height:24px}
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-cargo-card>.warehouse-table-scroll{box-sizing:border-box;padding:0 16px}
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table th:first-child,.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table td:first-child{padding-left:0}
2026-10-15 15:45:22 +08:00
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-pallet-list{padding:0 16px}
2026-10-16 18:25:56 +08:00
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-measurement-evidence{margin:12px 16px}
.warehouse-transfer-receipt-actions{display:flex;flex-wrap:wrap;gap:8px}.warehouse-transfer-receipt-actions :deep(.el-button+.el-button){margin-left:0}
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid{width:100%;max-width:1440px;box-sizing:border-box;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:18px;row-gap:4px;padding:8px 16px 10px}
2026-10-14 17:41:04 +08:00
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
2026-10-29 16:13:22 +08:00
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div>span{flex:0 0 84px;min-width:0}
</style>
<style scoped>
.warehouse-create {
--el-color-primary: #2f7bff;
--el-text-color-regular: #1f2937;
--el-text-color-placeholder: #b0b7c3;
--client-border: #e3e8f0;
width: 100%; min-width: 0; gap: 0; background: #f4f7fc; color: #1f2937;
font: 14px/1.5 " PingFang SC ", " Microsoft YaHei ", " Noto Sans SC " , sans - serif ;
}
. warehouse - create . warehouse - page - heading { margin - bottom : 12 px ; }
. warehouse - create . warehouse - page - heading h2 { margin : 0 ; color : # 1 f2937 ; font - size : 26 px ; font - weight : 700 ; }
. warehouse - create . warehouse - form - section { min - width : 0 ; margin - bottom : 12 px ; padding : 0 ; border : 0 ; background : transparent ; box - shadow : none ; }
. warehouse - create . warehouse - order - info - heading ,
. warehouse - create . warehouse - labeling - heading { width : 100 % ; max - width : none ; margin : 0 0 10 px ; padding : 0 ; border : 0 ; }
. warehouse - create . warehouse - form - section h3 { display : flex ; align - items : center ; gap : 10 px ; margin : 0 ; padding : 0 ; border : 0 ; color : # 1 f2937 ; font - size : 18 px ; font - weight : 700 ; }
. warehouse - create . warehouse - form - section h3 :: before { content : '' ; width : 9 px ; height : 9 px ; flex : 0 0 9 px ; border - radius : 50 % ; background : # 2 f7bff ; }
. warehouse - create . warehouse - labeling - heading h3 :: before { background : # 0 ea5a4 ; }
. warehouse - create . warehouse - form - grid ,
. warehouse - create . warehouse - labeling - body ,
. warehouse - create . warehouse - order - info - summary { width : 100 % ; max - width : none ; min - width : 0 ; box - sizing : border - box ; padding : 16 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
. warehouse - create . warehouse - form - grid { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 12 px 20 px ; }
. warehouse - create : deep (. el - form - item ) { min - width : 0 ; margin : 0 ; }
. warehouse - create : deep ( label . el - form - item__label ) { display : flex ; flex - direction : row ; gap : 0 ; height : auto ; margin : 0 0 6 px ; padding : 0 ; color : # 374151 ; font - size : 14 px ; font - weight : 600 ; line - height : 20 px ; }
. warehouse - create : deep (. el - form - item . is - required . el - form - item__label :: before ) { color : # e5484d ; }
. warehouse - create : deep (. el - form - item__content ) { min - width : 0 ; line - height : 20 px ; }
/* Validation shares the label line so showing and clearing it cannot move the next row. */
. warehouse - create : deep (. el - form - item__error ) { position : absolute ; top : auto ; bottom : calc ( 100 % + 6 px ); right : 0 ; left : auto ; width : auto ; max - width : calc ( 100 % - 170 px ); padding : 0 ; font - size : 12 px ; line - height : 20 px ; text - align : right ; }
. warehouse - create . warehouse - form - grid : deep (. el - input ),
. warehouse - create . warehouse - form - grid : deep (. el - select ),
. warehouse - create . warehouse - form - grid : deep (. el - date - editor ) { width : 100 % ; }
. warehouse - create . warehouse - form - grid : deep (. el - input__wrapper ),
. warehouse - create . warehouse - form - grid : deep (. el - select__wrapper ) { min - height : 44 px ; box - sizing : border - box ; padding : 10 px 14 px ; border : 1 px solid # d5dce8 ; border - radius : 6 px ; background : # fff ; box - shadow : none ; }
. warehouse - create . warehouse - form - grid : deep (. el - input__inner ) { height : 22 px ; line - height : 22 px ; color : # 1 f2937 ; font - size : 14 px ; }
. warehouse - create . warehouse - form - grid : deep (. el - textarea__inner ) { min - height : 110 px ! important ; padding : 11 px 14 px ; border : 1 px solid # d5dce8 ; border - radius : 6 px ; box - shadow : none ; font - size : 14 px ; }
. warehouse - create . warehouse - form - grid : deep (. el - checkbox ) { height : 44 px ; }
. warehouse - create . warehouse - form - grid : deep (. el - checkbox__label ) { font - size : 14 px ; }
. warehouse - create . warehouse - form - grid : deep (. el - input__wrapper . is - focus ),
. warehouse - create . warehouse - form - grid : deep (. el - select__wrapper . is - focused ),
. warehouse - create . warehouse - form - grid : deep (. el - textarea__inner : focus ) { border - color : # 2 f7bff ; }
. warehouse - create . warehouse - form - grid : deep (. is - error . el - input__wrapper ),
. warehouse - create . warehouse - form - grid : deep (. is - error . el - select__wrapper ) { border - color : # e5484d ; box - shadow : none ; }
. warehouse - create : deep ( input :: placeholder ),
. warehouse - create : deep ( textarea :: placeholder ),
. warehouse - create : deep (. el - select__placeholder . is - transparent ) { color : # b0b7c3 ; opacity : 1 ; }
. warehouse - create . warehouse - native - file { width : 100 % ; min - height : 44 px ; box - sizing : border - box ; padding : 8 px 10 px ; border - color : # d5dce8 ; border - radius : 6 px ; }
. warehouse - service - page . warehouse - create . warehouse - transfer - cargo - card . warehouse - section - heading ,
. warehouse - service - page . warehouse - create . warehouse - transfer - cargo - card . warehouse - table - scroll ,
. warehouse - service - page . warehouse - create . warehouse - file - groups -- compact { width : 100 % ; max - width : none ; }
. warehouse - service - page . warehouse - create . warehouse - file - groups { gap : 12 px ; margin - bottom : 12 px ; }
. warehouse - service - page . warehouse - create . warehouse - file - group { border : 1 px solid # e3e8f0 ; border - radius : 6 px ; background : # fff ; box - shadow : none ; }
. warehouse - service - page . warehouse - create . warehouse - file - group__heading { padding : 10 px 12 px ; background : # fff ; }
. warehouse - service - page . warehouse - create . warehouse - file - group__options { padding : 10 px 12 px ; }
. warehouse - service - page . warehouse - create . warehouse - labeling - mode { width : auto ; max - width : none ; gap : 12 px ; margin : 12 px 0 ; padding : 10 px 12 px ; border - color : # e3e8f0 ; border - radius : 6 px ; background : # fff ; }
. warehouse - service - page . warehouse - create . warehouse - tab - actions { margin : 0 0 10 px ; }
. warehouse - service - page . warehouse - create : deep (. el - radio - button__inner ),
. warehouse - service - page . warehouse - create . warehouse - labeling - body : deep (. el - input__wrapper ),
. warehouse - service - page . warehouse - create . warehouse - labeling - body : deep (. el - textarea__inner ) { box - shadow : none ; }
. warehouse - service - page . warehouse - create . warehouse - labeling - body : deep (. el - input__wrapper ),
. warehouse - service - page . warehouse - create . warehouse - labeling - body : deep (. el - textarea__inner ) { border : 1 px solid # d5dce8 ; border - radius : 6 px ; }
. warehouse - create . warehouse - submit - bar { justify - content : flex - end ; gap : 12 px ; padding : 0 ; }
. warehouse - create : deep (. el - button ) { box - shadow : none ; }
. warehouse - create . warehouse - page - heading : deep (. el - button ),
. warehouse - create . warehouse - submit - bar : deep (. el - button ) { min - height : 44 px ; margin - left : 0 ; padding : 10 px 18 px ; border : 1 px solid # d5dce8 ; border - radius : 6 px ; background : # fff ; color : # 1 d5fe0 ; font - size : 14 px ; font - weight : 600 ; }
. warehouse - create . warehouse - submit - bar : deep (. el - button -- primary ) { padding : 12 px 28 px ; border - color : # 2 f7bff ; background : # 2 f7bff ; color : # fff ; }
. warehouse - create . warehouse - submit - bar : deep (. el - button . is - disabled ) { opacity : .55 ; }
: global (. labeling - create - popper . el - popper ) { border : 1 px solid # e3e8f0 ; border - radius : 6 px ; box - shadow : none ; }
</ style >
< style scoped >
. warehouse - transfer - create . warehouse - transfer - form { gap : 12 px ; }
. warehouse - transfer - create . warehouse - transfer - form -- relabel { column - gap : 0 ; }
. warehouse - transfer - create . warehouse - form - section { margin - bottom : 0 ; padding : 0 ; }
. warehouse - transfer - create . warehouse - transfer - form . warehouse - form - grid { -- el - component - size : 44 px ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 12 px 20 px ; }
/* Preserve the existing resizable two-pane workflow when relabeling is selected. */
. warehouse - transfer - create . warehouse - transfer - form -- relabel . warehouse - form - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. warehouse - transfer - create . warehouse - transfer - create - body { min - width : 0 ; padding : 16 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
. warehouse - transfer - create . warehouse - transfer - create - body > . warehouse - section - heading h3 { display : none ; }
. warehouse - transfer - create . warehouse - transfer - create - body > . warehouse - section - heading { justify - content : flex - end ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - form - grid { padding : 0 ; border : 0 ; border - radius : 0 ; }
. warehouse - transfer - create . warehouse - transfer - relabel { overflow : visible ; }
. warehouse - transfer - create . warehouse - step - number { display : none ; }
. warehouse - transfer - create . warehouse - transfer - form . warehouse - form - section h3 { margin : 0 ; padding : 0 ; font - size : 18 px ; }
. warehouse - transfer - create . warehouse - transfer - cargo - card > . warehouse - order - info - heading h3 :: before { background : # 0 ea5a4 ; }
. warehouse - transfer - create . warehouse - transfer - relabel > . warehouse - section - heading h3 :: before { background : # 0 ea5a4 ; }
. warehouse - transfer - create . warehouse - transfer - relabel > . warehouse - section - heading { margin - bottom : 10 px ; }
. warehouse - transfer - create . warehouse - transfer - create - body section h3 { font - size : 14 px ; }
. warehouse - transfer - create . warehouse - transfer - create - body section h3 :: before { display : none ; }
. warehouse - transfer - create . warehouse - receiving - card { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px 20 px ; padding : 12 px ; border : 1 px solid # e3e8f0 ; border - radius : 6 px ; background : # f4f7fc ; }
. warehouse - transfer - create . warehouse - receiving - card > . el - form - item { display : block ; }
. warehouse - transfer - create . warehouse - receiving - card > div > span { font - size : 14 px ; }
. warehouse - transfer - create : deep (. el - form - item__error ) { max - width : calc ( 100 % - 140 px ); }
. warehouse - transfer - create . warehouse - form - grid : deep (. el - radio - button__inner ) { padding : 11 px 14 px ; font - size : 14 px ; line - height : 20 px ; box - shadow : none ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - transfer - file - groups { gap : 12 px ; margin - bottom : 12 px ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - file - group { border : 1 px solid # e3e8f0 ; border - radius : 6 px ; background : # fff ; box - shadow : none ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - labeling - mode { margin : 12 px 0 ; padding : 10 px 12 px ; border : 1 px solid # e3e8f0 ; border - radius : 6 px ; background : # fff ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - edit - table { width : 100 % ; font - size : 13 px ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - edit - table th { height : 40 px ; font - size : 13 px ; }
. warehouse - transfer - create . warehouse - transfer - create - body . warehouse - edit - table td { padding : 8 px ; }
. warehouse - transfer - create . warehouse - table - scroll : deep (. el - input__wrapper ),
. warehouse - transfer - create . warehouse - table - scroll : deep (. el - textarea__inner ) { border : 1 px solid # d5dce8 ; border - radius : 6 px ; box - shadow : none ; }
. warehouse - transfer - create . warehouse - table - scroll : deep (. el - input__wrapper . is - focus ),
. warehouse - transfer - create . warehouse - table - scroll : deep (. el - textarea__inner : focus ) { border - color : # 2 f7bff ; }
. warehouse - transfer - create . warehouse - transfer - form . warehouse - submit - bar { justify - content : flex - end ; gap : 12 px ; padding : 0 ; }
. warehouse - transfer - create . warehouse - transfer - form . warehouse - submit - bar : deep (. el - button ) { min - height : 44 px ; padding : 10 px 18 px ; }
. warehouse - transfer - create . warehouse - transfer - form . warehouse - submit - bar : deep (. el - button -- primary ) { padding : 12 px 28 px ; }
. warehouse - transfer - create . warehouse - form - grid : deep (. el - date - editor . el - input ) { height : 44 px ; }
. warehouse - transfer - create . warehouse - transfer - form -- relabel . warehouse - transfer - choice { grid - column : 1 / - 1 ; }
. warehouse - transfer - create . warehouse - form - grid : deep (. el - input__wrapper . is - focus ),
. warehouse - transfer - create . warehouse - form - grid : deep (. el - select__wrapper . is - focused ),
. warehouse - transfer - create . warehouse - form - grid : deep (. el - textarea__inner : focus ) { outline : 2 px solid # 2 f7bff ; outline - offset : 0 ; }
: global (. transfer - create - popper . el - popper ) { border : 1 px solid # e3e8f0 ; border - radius : 6 px ; box - shadow : none ; }
</ style >
< style scoped >
. warehouse - disposal - create . warehouse - form - grid { -- el - component - size : 44 px ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 12 px 20 px ; }
. warehouse - disposal - create . warehouse - disposal - create - body { min - width : 0 ; padding : 16 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
. warehouse - disposal - create . warehouse - transfer - cargo - card > . warehouse - order - info - heading h3 :: before { background : # 0 ea5a4 ; }
. warehouse - disposal - create . warehouse - disposal - create - body > . warehouse - section - heading { display : none ; }
. warehouse - disposal - create . warehouse - discard - boxes > . warehouse - section - heading h3 { display : none ; }
. warehouse - disposal - create . warehouse - discard - boxes > . warehouse - section - heading { justify - content : flex - end ; margin : 0 0 12 px ; }
. warehouse - disposal - create . warehouse - discard - box - toolbar { display : flex ; align - items : center ; gap : 12 px ; margin - bottom : 12 px ; }
. warehouse - disposal - create . warehouse - discard - box - toolbar : deep (. el - input ) { width : calc (( 100 % - 40 px ) / 3 ); }
. warehouse - disposal - create . warehouse - edit - table { width : 100 % ; font - size : 13 px ; }
. warehouse - disposal - create . warehouse - edit - table th { height : 40 px ; font - size : 13 px ; }
. warehouse - disposal - create . warehouse - edit - table td { padding : 8 px 12 px ; }
. warehouse - disposal - create . warehouse - disposal - create - body : deep (. el - input__wrapper ) { border : 1 px solid # d5dce8 ; border - radius : 6 px ; box - shadow : none ; }
. warehouse - disposal - create . warehouse - form - grid : deep (. el - input__wrapper . is - focus ),
. warehouse - disposal - create . warehouse - form - grid : deep (. el - select__wrapper . is - focused ),
. warehouse - disposal - create . warehouse - form - grid : deep (. el - textarea__inner : focus ),
. warehouse - disposal - create . warehouse - disposal - create - body : deep (. el - input__wrapper . is - focus ) { outline : 2 px solid # 2 f7bff ; outline - offset : 0 ; }
. warehouse - disposal - create . warehouse - hint { margin : 10 px 0 0 ; color : # 6 b7280 ; font - size : 13 px ; line - height : 20 px ; }
. warehouse - disposal - create . warehouse - disposal - quantity { padding : 0 ; border : 0 ; }
. warehouse - disposal - create . warehouse - disposal - quantity : deep (. el - input - number ) { width : 100 % ; }
. warehouse - disposal - create . warehouse - disposal - quantity : deep (. el - input - number . el - input__wrapper ) { padding - right : 40 px ; }
. warehouse - disposal - create . warehouse - disposal - quantity > . warehouse - hint { align - self : end ; margin - bottom : 12 px ; }