Files
client-web/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
T

2466 lines
270 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { getRelabelQuantities, type RelabelQuantity } from '../api/relabelQuantities'
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
import TransferPaneDivider from '../components/TransferPaneDivider.vue'
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElForm, ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { ArrowLeft, ChevronDown, Download, Plus, Printer, Trash2, Upload, X } from '@lucide/vue'
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'
import { useAuthStore } from '@/modules/auth/public'
import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting'
import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue'
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import { groupDiscardDocuments } from '../utils/discardDocuments'
import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
getTransferCartonLabel, submitTransfer, updateTransfer, increaseTransferForecastCartons, getTransferSourceCartons, type TransferSourceCarton,
downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel,
uploadRelabelFile,
listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard,
type RelabelInventoryCandidate,
type RelabelCreate,
type RelabelDetail,
type TransferCreate,
} from '../api/warehouseServices'
import {
getMerchantWarehouseDetail,
getMerchantWarehouses,
type MerchantWarehouse,
type MerchantWarehouseDetail,
} from '@/modules/first-leg/api/firstLeg'
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
import { createIdempotencyKey } from '@/utils/idempotencyKey'
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
import { relabelSkuCodes, relabelSkuCount } from '../utils/relabelSkuDisplay'
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'edit' | 'detail'
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
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 }
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string }
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
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 }
const props = withDefaults(defineProps<{ service: ServiceType; createPage?: boolean; detailPage?: boolean; detailNo?: string }>(), { createPage: false, detailPage: false, detailNo: '' })
const auth = useAuthStore()
const router = useRouter()
const route = useRoute()
const transferPaneShare = ref(43)
// 缓存列表不订阅其他详情页的路由参数,避免离开时清空筛选并重复请求。
const detailNumber = computed(() => props.detailPage ? props.detailNo || String(route.params.detailNo || '') : '')
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'))
const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm'))
const completionConfirmOpen = ref(false)
const completionConfirmNo = ref('')
const completionConfirmKind = ref<'disposal' | 'labeling' | 'transfer'>('disposal')
function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling' | 'transfer') {
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()
}
const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel'))
const configs: Record<ServiceType, ServiceConfig> = {
labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '关联业务单号', '海外操作仓', 'SKU 数(种)', '箱数', '贴标件数', '换标类型', '状态', '创建时间'] },
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '预计箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '创建时间'] },
disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '废弃出库单号', '进仓编号', '操作仓', 'SKU 汇总', '申请箱数', '预估费用', '状态', '废弃完成时间', '创建时间'] },
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '审核时间', '测量时间', '出库时间', '创建时间'] },
}
const config = computed(() => configs[props.service])
const listQueryModel = computed<QueryModel>({
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) }),
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] || '') : '',
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] || '') : '',
}),
})
const statusOptions = computed(() => props.service === 'labeling'
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }]
: props.service === 'disposal'
? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待 PDA 废弃', value: 'APPROVED' }, { label: '待客户确认', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }]
: 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' }])
const mode = ref<PageMode>(props.detailPage ? 'detail' : props.createPage ? 'create' : 'list')
const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' })
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 })
const transferDestinationType = ref<'NONE' | 'FBA' | 'PRIVATE'>('NONE')
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(', ')
})
const cargoRows = ref<CargoRow[]>([])
const transferRelabelRows = ref<CargoRow[]>([])
const transferRelabelDefaultDestination = ref('')
const labelingMode = ref<LabelingMode>('carton')
const shippingLabelFile = ref<File | null>(null)
const skuRelabelRows = ref<SkuRelabelRow[]>([])
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included))
const submittedRows = ref<Record<string, string>[]>([])
const page = ref(1)
const pageSize = ref(15)
const total = ref(0)
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> {
reviewRemark?: string
discardNo?: string; photos?: DiscardPhoto[]
transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string
destinationType?: 'NONE' | 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string
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
relabelContactPhone?: string; relabelDeliveryMethod?: string; relabelNo?: string
relabelStatus?: string; relabelFulfillmentStatus?: string; createdAt?: string
inboundCode?: string; sourceOrders?: Array<{ lastlegOrderId?: string | number }>
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 }>
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
}
const selectedDetail = ref<ServiceDetail | null>(null)
/** 仅汇总客户原始预报件数,旧单缺失时明确提示,不用实收件数冒充。 */
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) + ' 件'
})
// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。
const showForecastCartons = ref(false)
const increaseForecastOpen = ref(false)
const increasingForecast = ref(false)
const forecastSnapshot = ref<ServiceDetail | null>(null)
const increaseForecastForm = reactive({ additionalCount: 1 })
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
if (!Number.isSafeInteger(increaseForecastForm.additionalCount) || increaseForecastForm.additionalCount < 1) { ElMessage.warning('请填写有效新增箱数'); return }
increasingForecast.value = true
try {
const result = await increaseTransferForecastCartons(String(snapshot.transferNo), { expectedPackageCount: Number(snapshot.packageCount), packageCount: Number(snapshot.packageCount) + increaseForecastForm.additionalCount })
selectedDetail.value = result as ServiceDetail; increaseForecastOpen.value = false
ElMessage.success('预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点')
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '增加预报箱数失败') }
finally { increasingForecast.value = false }
}
const showTransferCartons = ref(false)
function transferCartonItem(carton: TransferCarton) {
return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId))
}
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) }]
}
function palletCartons(pallet: TransferPallet) {
return countedTransferCartons().filter((carton) => pallet.id != null && carton.transferPalletId != null && String(carton.transferPalletId) === String(pallet.id))
}
function palletSkuRows(pallet: TransferPallet) {
const summary = new Map<string, number>()
for (const carton of palletCartons(pallet)) {
for (const line of transferCartonLines(carton)) summary.set(line.skuCode, (summary.get(line.skuCode) || 0) + Number(line.quantity))
}
return [...summary.entries()].map(([sku, quantity]) => ({ sku, quantity }))
}
function palletQuantity(pallet: TransferPallet) { return palletCartons(pallet).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0) }
/** 保留历史实收,后续占用、废弃与出库不回退签收事实。 */
function countedTransferCartons() { return (selectedDetail.value?.cartons || []).filter((carton) => ['COUNTED', 'DISCARD_PENDING', 'DISCARDED', 'SHIPPED'].includes(carton.status)) }
function receivedItemCartons(itemId: string | number | undefined) {
return countedTransferCartons().filter(carton => itemId != null && transferCartonLines(carton).some(line => String(line.transferItemId) === String(itemId)))
}
function receivedItemQuantity(itemId: string | number | undefined) {
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)
}
const detailError = ref('')
const selectedRelabelDetail = ref<RelabelDetail | null>(null)
const selectedRelabelQuantity = ref<RelabelQuantity | null>(null)
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))
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))
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))) }
const fbaWarehouses = ref<MerchantWarehouse[]>([])
const operationWarehouses = ref<MerchantWarehouse[]>([])
const fbaWarehousesLoading = ref(false)
const fbaWarehousesError = ref('')
const operationWarehousesError = ref('')
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: '请输入仓代码' })
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' },
)
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' }
: { 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,
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 }))
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))))
const discardSelectedBoxKeys = ref<string[]>([])
const discardBoxSearch = ref('')
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
const discardDocuments = computed(() => groupDiscardDocuments(discardCandidates.value))
const discardSelectedCandidate = computed(() => discardDocuments.value.find(item => item.orderId === selectedDiscardOrderId.value))
const discardSelectionSources = computed(() => discardSelectedCandidate.value?.sources || [])
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()])
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() {
const selected = discardSourceBoxes.value.filter(box => discardSelectedBoxKeys.value.includes(box.key))
const groups = new Map<string, CargoRow>()
for (const box of selected) {
const source = box.source
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)
for (const sku of skus) {
const key = `${source.sourceType}:${source.inventoryPositionId || source.returnCartonId || source.orderId}:${sku.skuCode}`
let row = groups.get(key)
if (!row) {
row = { ...createCargoRow(), sku: sku.skuCode, name: sku.productName, quantity: 0, availableQuantity: sku.availablePackages,
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()]
}
const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref(false)
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedRelabelInboundOrderId = ref('')
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' }] : [],
}))
const orderInfoCollapsed = ref(false)
watch([labelingMode, () => form.deliveryMethod, () => form.relabel], () => {
if (isLabelingCreate.value) labelingFormRef.value?.clearValidate(['destination', 'deliveryMethod', 'shippingLabelFile'])
})
const availableBoxesCollapsed = ref(false)
const availableSkusCollapsed = ref(false)
const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
const transferAvailableSourceCounts = ref<Record<string, number>>({})
const selectedTransferInventory = ref('')
const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value))
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
const transferRelabelRequired = computed(() => inventoryTransfer.value && transferDestinationType.value === 'FBA')
watch(transferRelabelRequired, (required) => { if (required) form.relabel = true }, { flush: 'sync' })
const transferSourceCartons = ref<TransferSourceCarton[]>([])
const transferSourceLoading = ref(false)
const transferSourceError = ref('')
let transferSourceRequest = 0
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()
})
async function loadTransferSourceCartons() {
const request = ++transferSourceRequest
transferSourceCartons.value = []; transferSourceError.value = ''
if (!inventoryTransfer.value) return
transferSourceLoading.value = true
try {
// 退货候选本身就是逐箱库存,使用与下拉一致的实物箱数据,不能按普通入仓快照回查。
if (selectedTransferInventory.value.startsWith('RETURN_ORDER:')) {
const returnNo = selectedTransferInventory.value.slice('RETURN_ORDER:'.length)
transferSourceCartons.value = transferInventoryCandidates.value
.filter(item => !['COMPLETED', 'PARTIALLY_COMPLETED'].includes(item.relabelStatus || '')
&& Number(item.relabelledQuantity || item.relabelledPackages || 0) === 0)
.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
}
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 }
}
// 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[]>([])
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
const currentRelabelCandidates = computed(() => relabelCandidates.value.map(item => returnRelabelCandidate(item, labelingMode.value)))
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()))))
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'))
const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && labelingMode.value === 'carton_and_sku')
function inventoryQuantity(value: number | string | null | undefined) {
const quantity = Number(value)
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
}
const relabelInventorySearch = ref('')
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('|')
}
function returnInventoryState(candidate: RelabelInventoryCandidate) {
const labels: Record<string, string> = { AVAILABLE: '可选库存', RELABEL_PENDING: '换标占用', PACKING_RESERVED: '合箱计划预留', OUTBOUND_PENDING: '出库占用', DISCARD_PENDING: '废弃申请占用', OUTBOUNDED: '已出库', MERGED: '已合箱' }
return [...(returnSourceNos(candidate).length > 1 ? ['跨来源单合箱,数量为整箱内容'] : []), ...(candidate.mergedIntoCartonMarks?.length ? ['历史部分转入 ' + candidate.mergedIntoCartonMarks.join('、') + '(不代表当前库存位置)'] : []), labels[candidate.inventoryStatus || ''] || (inventoryQuantity(candidate.availablePackages) > 0 ? '可选库存' : '当前不可选'), candidate.occupiedRelabelNo].filter(Boolean).join(' · ')
}
const relabelInboundOptions = computed(() => {
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件'; stockQuantity: number | null; units: number; hasOutbound: boolean; reasons: string[] }>()
for (const candidate of currentRelabelCandidates.value) {
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)
if (current) {
current.hasOutbound ||= candidate.hasOutbound === true || candidate.inventoryStatus === 'OUTBOUNDED'
current.availablePackages += inventoryQuantity(candidate.availablePackages)
current.units += candidate.returnCartonId && Number(candidate.stockQuantity) === 0 ? 0 : 1
current.stockQuantity = current.stockQuantity == null || candidate.stockQuantity == null ? null : current.stockQuantity + inventoryQuantity(candidate.stockQuantity)
if (candidate.returnCartonId) current.reasons.push(returnInventoryState(candidate))
}
else grouped.set(value, {
value, inboundOrderNo: candidate.returnCartonId ? '退货单 ' + sourceNo : (candidate.inboundOrderNo || '—'),
inboundCode: candidate.returnCartonId ? '已签收退货库存(含合箱)' : (candidate.inboundCode || '—'),
availablePackages: inventoryQuantity(candidate.availablePackages),
quantityUnit: candidate.returnCartonId ? '件' : '箱',
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)] : [],
})
}
}
return [...grouped.values()].map(option => ({ ...option, reasons: [...new Set(option.reasons)] }))
})
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 + ' 箱'
return option.inboundOrderNo + '|在仓 ' + option.units + ' 包裹/箱 · ' + (option.stockQuantity ?? '待查询') + ' 件' + (option.reasons.some(reason => reason.includes('跨来源单')) ? '(关联整箱数量,含其他来源)' : '') + (option.hasOutbound && Number(option.stockQuantity) > 0 ? '|部分出库' : '')
}
const loading = ref(false)
const saving = ref(false)
const unifiedTemplateInput = ref<HTMLInputElement | null>(null)
const transferCartonMarkInput = ref<HTMLInputElement | null>(null)
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)
let cargoSequence = 0
let skuRelabelSequence = 0
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()
})
watch(mode, (value) => { if (value !== 'detail') { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() } })
watch(() => form.relabel, (value) => {
if (props.service === 'transfer' && value) {
labelingMode.value = 'carton_and_sku'
if (!transferRelabelRows.value.length) addTransferRelabelRow()
}
})
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
})
})
watch(labelingMode, (value) => {
if (!isReturnRelabelSource.value && value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow()
})
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
})
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
}
async function loadPage() {
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer' || props.service === 'disposal') await loadWarehouses()
await load()
}
onMounted(() => {
if (props.createPage) void openCreate()
else if (props.detailPage && detailNumber.value) void loadDetailPage()
else void loadPage()
void nextTick(refreshWarehouseMarkLabels)
})
let activatedOnce = false
onActivated(() => {
if (activatedOnce && !props.createPage && !props.detailPage) {
mode.value = 'list'
void loadPage()
}
activatedOnce = true
})
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' }) }
function resetForm() {
labelingFormRef.value?.clearValidate?.()
orderInfoCollapsed.value = false
availableBoxesCollapsed.value = false
availableSkusCollapsed.value = false
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 })
cargoRows.value = []
transferRelabelRows.value = []
transferRelabelDefaultDestination.value = ''
labelingMode.value = 'carton'
skuRelabelRows.value = []
importedRelabelBindings.value = []
discardCandidates.value = []
selectedDiscardOrderId.value = ''
relabelCandidates.value = []
transferInventoryCandidates.value = []
selectedTransferInventory.value = ''
selectedTransferSourceOrderIds.value = []
selectedRelabelInboundOrderId.value = ''
cartonTotalFile.value = null
skuTotalFile.value = null
shippingLabelFile.value = null
if (cartonTotalCreateInput.value) cartonTotalCreateInput.value.value = ''
if (skuTotalCreateInput.value) skuTotalCreateInput.value.value = ''
transferDestinationType.value = 'NONE'
selectedOperationWarehouse.value = null
editingTransferNo.value = ''
}
async function openCreate() {
if (!props.createPage && props.service !== 'relabel') {
await router.push({ name: `${serviceListRouteName(props.service)}-create` })
return
}
resetForm(); mode.value = 'create'
await loadWarehouses()
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
}
if (props.service !== 'disposal') { addCargoRow(); return }
}
function returnToList() {
if (props.createPage || props.detailPage) {
void router.push({ name: serviceListRouteName(props.service) })
return
}
mode.value = 'list'
}
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]
}
async function goToServiceList() {
if (props.createPage || props.detailPage) {
await router.push({ name: serviceListRouteName(props.service) })
return
}
mode.value = 'list'
await loadPage()
}
async function openEdit(transferNo: string) {
try {
resetForm()
await loadWarehouses()
const detail = await getTransfer(transferNo) as ServiceDetail
if (!['DRAFT', 'REJECTED'].includes(detail.status || '')) throw new Error('只有草稿或审核驳回状态的转运单可以编辑')
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 || '',
quantity: Number(item.quantity || 0), expectedPackageCount: Number(item.packageCount || 0), shippingMark: item.shippingMark || '',
customerCartonMarks: item.shippingMark || '',
generatedCartonMarks: (detail.cartons || []).filter(box => String(box.transferItemId) === String(item.id)).map(box => box.cartonMark),
}))
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 || '',
}))
const destinations = [...new Set(bindings.map((binding) => binding.destinationWarehouseCode).filter(Boolean))]
transferRelabelDefaultDestination.value = destinations.length === 1 ? destinations[0]! : ''
}
if (form.warehouse) {
try { selectedOperationWarehouse.value = await getMerchantWarehouseDetail(form.warehouse) } catch { selectedOperationWarehouse.value = null }
}
// 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
await loadTransferSourceCartons()
if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA'
form.inboundNo = sourceInboundCode
selectedTransferSourceOrderIds.value = (detail.sourceOrders || [])
.map((item) => String(item.lastlegOrderId || '')).filter(Boolean)
}
}
mode.value = 'edit'
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '草稿加载失败') }
}
async function selectDiscardWarehouse() {
discardSelectedBoxKeys.value = []
discardBoxSearch.value = ''
selectedDiscardOrderId.value = ''
form.inboundNo = ''
discardCandidates.value = []
cargoRows.value = []
transferRelabelRows.value = []
if (!form.warehouse) return
discardCandidatesLoading.value = true
try {
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) }))
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') }
finally { discardCandidatesLoading.value = false }
}
function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, expectedPackageCount: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } }
function addCargoRow() { cargoRows.value.push(createCargoRow()) }
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 || '' })
}
function clearTransferRelabelRows() {
transferRelabelRows.value = []
}
function addSkuRelabelRow() {
if (isReturnRelabelSource.value) { ElMessage.warning('请从上方实际包裹/箱库存选择 SKU'); return }
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,
cartonMark: '', originalSku: source?.sku || '', newSku: '', quantity: 1,
fbaCartonMark: '', shipmentNo: '', destinationWarehouseId: form.destination, remark: '' })
}
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
function applyExistingSkuBinding(row: SkuRelabelRow) {
if (!row.originalSku.trim() || row.newSku.trim()) return
const mapping = skuRelabelRows.value.find((item) => item.id !== row.id
&& item.originalSku.trim().toUpperCase() === row.originalSku.trim().toUpperCase() && item.newSku.trim())
if (mapping) row.newSku = mapping.newSku
}
function applyExistingCartonBinding(row: SkuRelabelRow) {
if (!row.cartonMark.trim()) return
const cartonMark = row.cartonMark.trim().toUpperCase()
const mapping = cargoRows.value.find((item) => item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim())
|| skuRelabelRows.value.find((item) => item.id !== row.id
&& item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim())
if (!mapping) return
if (!row.fbaCartonMark.trim()) row.fbaCartonMark = mapping.fbaCartonMark
if (!row.shipmentNo.trim()) row.shipmentNo = mapping.shipmentNo
if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId
}
function selectShippingLabel(event: Event) {
shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null
if (isLabelingCreate.value) void nextTick(() => labelingFormRef.value?.validateField('shippingLabelFile').catch(() => undefined))
}
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')
}
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 = ''
}
function chooseCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') {
;(scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput).value?.click()
}
function buildSkuRelabelItems(): RelabelCreate['items'] {
const groups = new Map<string, SkuRelabelRow[]>()
for (const row of skuRelabelRows.value) {
const cartonMark = row.cartonMark.trim().toUpperCase()
if (!cartonMark) throw new Error('请填写每个箱子的我方箱唛')
const rows = groups.get(cartonMark) || []
rows.push(row)
groups.set(cartonMark, rows)
}
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) => {
const source = isReturnRelabelSource.value
? cargoRows.value.find(item => item.id === rows[0].sourceRowId && item.cartonMark === rows[0].cartonMark.trim())
: 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)
if (rows.some(row => row.sourceRowId !== source?.id)) throw new Error('同一换标箱内的 SKU 必须选择相同的实际来源包裹/箱')
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} 超过来源箱可用数量`)
}
}
if (!source?.returnCartonId && (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId)) {
throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
}
const cartonMark = rows[0].cartonMark.trim()
const changeCarton = labelingMode.value === 'carton_and_sku'
const fbaCartonMark = rows[0].fbaCartonMark.trim()
const shipmentNo = rows[0].shipmentNo.trim()
if (changeCarton && (!fbaCartonMark || !form.destination
|| rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark))) {
throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 FBA 目的仓')
}
if (changeCarton && (!shipmentNo || rows.some(row => row.shipmentNo.trim() !== shipmentNo))) {
throw new Error('同一我方箱唛的组合换标必须填写相同的 FBA 货件编号')
}
return {
sourceInventoryBalanceId: source.sourceInventoryBalanceId,
sourceInventoryPositionId: source.sourceInventoryPositionId,
sourceInboundOrderId: source.sourceInboundOrderId,
returnCartonId: source.returnCartonId,
skuCode: source.sku || rows[0].originalSku,
productNameZh: source.name,
totalPackageCount: 1,
notifiedPackageCount: 1,
shipmentNo: changeCarton ? shipmentNo : source.shipmentNo || cartonMark,
trackingCode: form.trackingNo || undefined,
newSkuCode: rows[0].newSku,
labelRequirement: form.requirement || undefined,
cartonMark,
fbaCartonMark: changeCarton ? fbaCartonMark : undefined,
destinationFbaWarehouseCode: changeCarton ? form.destination : undefined,
operationType: changeCarton ? 'CARTON_AND_SKU' as const : 'SKU_LABEL' as const,
skuItems: rows.map((row) => ({ skuCode: row.originalSku.trim(), newSkuCode: row.newSku.trim(), quantity: row.quantity, remark: row.remark || undefined })),
remark: undefined,
}
})
}
function addRelabelCargoRow() {
if (!selectedRelabelInboundOrderId.value) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
const source = cargoRows.value.find((row) => !row.included)
if (!source) { ElMessage.warning('所选入仓单没有更多可添加的库存箱'); return }
source.included = true
}
function removeCargoRow(id: number) {
const item = cargoRows.value.find((row) => row.id === id)
if (props.service === 'labeling' && item?.sourceInventoryBalanceId) {
Object.assign(item, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
return
}
cargoRows.value = cargoRows.value.filter((row) => row.id !== id)
}
function clearRelabelRows() {
for (const row of cargoRows.value) Object.assign(row, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
}
function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function 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', '客户箱唛号']
const rows = cargoRows.value.flatMap((row) => Array.from({ length: Number(row.expectedPackageCount || 0) }, () => [row.sku, '']))
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 }
const headers = parseCsvLine(lines[0]).map((name) => name === '仓库唛头' || name === '旧唛头' ? '旧箱唛' : name)
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} 个客户箱唛`)
}
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() {
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
const templateType = relabelTemplateTypeForMode(labelingMode.value)
const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头'
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])
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)
}
function parseCsvLine(line: string) {
const values: string[] = []; let value = ''; let quoted = false
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 }
values.push(value.trim()); return values
}
async function importRelabelTemplate(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 }
const headers = parseCsvLine(lines[0]).map((name) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name)
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() })
}
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)
}
importedRelabelBindings.value = rows; ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`)
}
function selectAllImportedBindings(value: boolean) {
for (const item of importedRelabelBindings.value) item.selected = value
}
function importSelectedBindingsToDetails() {
const selected = importedRelabelBindings.value.filter((item) => item.selected)
if (!selected.length) { ElMessage.warning('请至少选择一条绑定关系'); return }
const templateType = selected[0].templateType
if (selected.some((item) => item.templateType !== templateType)) { ElMessage.warning('请重新上传同一种绑定模板'); return }
if (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
}
if (templateType === 'carton') {
for (const binding of selected) {
const source = cargoRows.value.find((row) => row.id === binding.sourceRowId)
if (!source) continue
Object.assign(source, { included: true, quantity: 1, shipmentNo: binding.shipmentNo,
cartonMark: binding.cartonMark, fbaCartonMark: binding.fbaCartonMark,
destinationWarehouseId: form.destination, remark: binding.remark })
}
labelingMode.value = 'carton'
} else {
const importedRows = selected.map(({ templateType: _, selected: __, ...item }) => item)
for (const item of importedRows) {
skuRelabelRows.value = skuRelabelRows.value.filter((row) => !(row.cartonMark.trim().toUpperCase() === item.cartonMark.trim().toUpperCase()
&& row.originalSku.trim().toUpperCase() === item.originalSku.trim().toUpperCase()))
item.destinationWarehouseId = form.destination
skuRelabelRows.value.push(item)
if (templateType === 'mixed') {
const source = cargoRows.value.find((row) => row.id === item.sourceRowId)
const shipmentNo = selected.find((binding) => binding.id === item.id)?.shipmentNo || ''
if (source) Object.assign(source, { included: true, cartonMark: item.cartonMark,
fbaCartonMark: item.fbaCartonMark, destinationWarehouseId: form.destination, shipmentNo })
}
}
labelingMode.value = templateType === 'mixed' ? 'carton_and_sku' : 'sku'
}
importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected)
ElMessage.success(`已将 ${selected.length} 条绑定关系导入本次换标明细`)
}
function selectDiscardInbound(orderId: string) {
discardRequestedPackages.value = undefined
discardSelectedBoxKeys.value = []
discardBoxSearch.value = ''
const candidate = discardDocuments.value.find((item) => item.orderId === orderId)
if (!candidate) { cargoRows.value = []; return }
form.inboundNo = candidate.inboundCode || candidate.inboundOrderNo
form.warehouse = candidate.overseasWarehouseId
if (candidate.cartonSelectionSupported) { selectAllDiscardBoxes(true); return }
cargoRows.value = candidate.sources.flatMap(source => source.skus.map((sku) => ({
id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: sku.availablePackages,
availableQuantity: sku.availablePackages, sourceOrderId: source.orderId, sourceType: source.sourceType,
returnCartonId: source.returnCartonId, sourceInventoryPositionId: source.inventoryPositionId, palletNo: source.palletNo, cartonMark: source.cartonMark || '',
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
})))
}
function dateTime(value: string, end = false) { return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined }
function row(values: Array<string | number | undefined | null>) { return Object.fromEntries(values.map((value, index) => [String(index), value == null || value === '' ? '—' : String(value)])) }
function warehouseDisplay(value?: string) {
if (!value) return '—'
return value.split(',').map((code) => {
const warehouse = [...operationWarehouses.value, ...fbaWarehouses.value].find((item) => String(item.id) === code || item.warehouseCode === code)
return warehouse ? warehouseLabel(warehouse) : '—'
}).join('、')
}
function statusLabel(value: string) {
if (props.service === 'labeling') {
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 || '—'
}
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]
}
const labels: Record<string, string> = {
PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认',
READY_FOR_OUTBOUND: '换标完成', COMPLETED: '已完成', CANCELLED: '已取消',
DRAFT: '草稿', PENDING_REVIEW: '待审核', APPROVED: '待 PDA 废弃', PDA_COMPLETED: '待客户确认', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配',
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: '派送完成',
}
return labels[value] || value || '—'
}
type StatusTagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
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)
}
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'
}
function deliveryMethodLabel(value: string) {
const labels: Record<string, string> = {
TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提',
INVENTORY_TRANSFER: '库存转运', WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓',
// Keep the legacy value readable for historical applications created before the four-way split.
PICKUP: '仓库自提',
}
return labels[value] || value || '—'
}
function destinationTypeLabel(value?: string, fbaWarehouseCode?: string) {
if (value === 'PRIVATE') return '私人仓'
if (value === 'FBA' || fbaWarehouseCode) return 'FBA 仓'
return '暂未确定'
}
function formatDateTime(value?: string | null) {
return value ? value.replace('T', ' ').slice(0, 19) : '—'
}
let discardLoadGeneration = 0
async function load() {
const discardRequest = props.service === 'disposal' ? ++discardLoadGeneration : undefined
loading.value = true
try {
if (props.service === 'labeling') {
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)])
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => {
const destinationWarehouses = warehouseDisplay(item.overseasWarehouseId)
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 || '' }
})
} else if (props.service === 'disposal') {
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) }
const result = await listDiscards(discardQuery)
if (discardRequest !== discardLoadGeneration || props.service !== 'disposal') return
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
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 }))
} else {
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)
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
submittedRows.value = result.records.map((item) => {
const operationWarehouse = warehouseDisplay(item.operatingWarehouseId)
const relabelStatus = transferRelabelStatus(item)
return props.service === 'transfer'
? { ...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 }
: row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.reviewedAt), formatDateTime(item.measuredAt), formatDateTime(item.outboundAt), formatDateTime(item.createdAt)])
})
}
} 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 }
}
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) }]
if (canCreate.value && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(item.__status)) actions.push({ key: 'edit', label: '编辑', run: () => openEdit(item['0']) })
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']) })
if (canConfirmDiscard.value && props.service === 'disposal' && item.__status === 'PDA_COMPLETED') actions.push({ key: 'confirm-discard', label: '废弃确认', run: () => openCompletionConfirmation(item['0'], 'disposal') })
return actions
}
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return }
void load()
})
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') {
if (saving.value) return
if (isLabelingCreate.value) {
orderInfoCollapsed.value = false
await nextTick()
if (!await labelingFormRef.value?.validate().catch(() => false)) return
}
if (props.service === 'transfer' && inventoryTransfer.value && action === 'draft') {
ElMessage.warning('选择库存来源后不能保存草稿,请直接提交订单')
return
}
if (isDisposalCreate.value && !await labelingFormRef.value?.validate().catch(() => false)) return
if (transferRelabelRequired.value) form.relabel = true
if (isTransferCreate.value) {
await nextTick()
if (!await labelingFormRef.value?.validate().catch(() => false)) return
}
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
const invalidCargo = props.service === 'disposal'
? !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)
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
: (props.service === 'labeling' || props.service === 'relabel') && (relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton'
? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
|| (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.shipmentNo.trim())))
: (props.service === 'labeling' ? cartonDetailRows.value : cargoRows.value).some((item) => (props.service !== 'labeling' && !item.sku) || !item.name
|| (props.service !== 'labeling' && item.quantity < 1))))
if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) {
ElMessage.warning('请填写必填信息和完整的货物明细'); return
}
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone)) {
ElMessage.warning('请填写贴标截止时间和海外仓联系人信息'); return
}
if (props.service === 'labeling' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) {
ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return
}
if (props.service === 'labeling' && !requiresBothTotalFiles.value && (labelingMode.value === 'sku' ? !skuTotalFile.value : !cartonTotalFile.value)) {
ElMessage.warning(labelingMode.value === 'sku' ? '请上传 SKU 标签总文件' : '请上传箱唛总文件'); return
}
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) {
ElMessage.warning('请选择换标完成后的派送方式'); return
}
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.destination) {
ElMessage.warning('请选择本张贴标申请统一使用的 FBA 目的仓'); return
}
if (props.service === 'labeling' && labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS'
&& form.relabel && !shippingLabelFile.value) {
ElMessage.warning('选择打印面单时必须上传面单文件'); return
}
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) {
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
}
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.returnCartonId && !item.sourceInventoryPositionId)) {
ElMessage.warning('请选择有库存的历史入仓单'); return
}
if (props.service === 'disposal' && !form.reason) {
ElMessage.warning('请选择废弃原因'); return
}
if (props.service === 'disposal' && discardQuantityMode.value && !form.remark.trim()) {
ElMessage.warning('请填写废弃备注'); return
}
if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) {
ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return
}
if (props.service === 'transfer' && !inventoryTransfer.value && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) {
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('、')
ElMessage.warning(`请填写:${missing || '必填信息'}`); return
}
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
}
if (inventoryTransfer.value && (transferSourceLoading.value || transferSourceError.value)) { ElMessage.warning('请等待原单箱明细加载成功后再保存'); return }
if (inventoryTransfer.value && transferDestinationType.value === 'NONE') { ElMessage.warning('库存转运请选择 FBA 仓或私人仓'); return }
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
}
if (props.service === 'transfer' && form.relabel) {
if (!form.labelDeadline || !form.relabelContact || !form.relabelPhone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、换标联系人和换标后派送方式'); return }
if (!transferRelabelDefaultDestination.value) { ElMessage.warning('请选择整票换标货物的 FBA 目的仓'); return }
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 }
const relabelRows = transferRelabelRows.value
const allMarks = relabelRows.flatMap(transferCustomerMarks)
if (inventoryTransfer.value && allMarks.some(mark => !transferSourceCartons.value.some(box => box.cartonMark === mark))) {
ElMessage.error('当前箱子不存在或不属于当前订单'); return
}
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 }
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 }
}
if (props.service !== 'transfer' || action === 'submit') {
try { await ElMessageBox.confirm(`确认提交${config.value.title}吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
}
saving.value = true
try {
const idempotencyKey = createIdempotencyKey()
if (props.service === 'labeling') {
const sourceInboundOrders = [...new Set(cargoRows.value.map((item) => item.sourceInboundOrderNo || item.sourceInboundCode).filter((value): value is string => Boolean(value)))].join('、').slice(0, 128)
const items = labelingMode.value === 'carton'
? 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 }))
: buildSkuRelabelItems()
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('一张申请只能选择一种换标类型,请核对绑定明细')
const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, remark: form.remark || undefined, idempotencyKey, items,
deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP',
printShippingLabel: labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel })
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) {
// Creation already committed: return to the list even when an attachment still needs retrying.
await goToServiceList()
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
return
}
ElMessage.success(`${config.value.title}提交成功`)
await goToServiceList()
return
} else if (props.service === 'disposal') {
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
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)) {
throw new Error(`SKU ${item.sku} 的可废弃库存不足`)
}
return { orderId: item.sourceType === 'DROPSHIP' ? item.sourceOrderId : undefined,
inventoryPositionId: item.sourceType === 'INVENTORY' ? item.sourceInventoryPositionId : undefined,
returnCartonId: item.sourceType === 'RETURN_CARTON' ? item.returnCartonId : undefined,
skuCode: item.sku, discardPackages: item.quantity, cartonIds: item.discardCartonIds }
})
await createDiscard({ items, idempotencyKey, requestedPackages: discardQuantityMode.value ? discardRequestedPackages.value : undefined, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined })
} else {
const packageCount = cargoRows.value.reduce((sum, item) => sum + Number(item.expectedPackageCount || 0), 0)
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'
const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination)
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 }
if (props.service === 'transfer' && selectedTransferInventory.value) {
;(payload as TransferCreate).inboundCode = selectedTransferInventory.value
;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value
}
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') {
const submitted = await submitTransfer(transferNo)
if (form.relabel) {
const relabelNo = String(submitted.relabelNo || created.relabelNo || '')
if (!relabelNo) {
ElMessage.success('转运单已提交审核;PDA 入库确认后将自动生成关联换标单')
await ElMessageBox.alert(`转运单 ${transferNo} 已提交。关联换标单会在 PDA 完成入库、客户确认实测库存后自动生成;届时请进入关联换标单详情上传贴标总文件。`, '转运单已提交', { confirmButtonText: '知道了' })
await goToServiceList()
return
}
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: '知道了' })
await goToServiceList()
return
}
}
ElMessage.success('转运单已提交审核')
await goToServiceList()
return
}
}
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`)
await goToServiceList()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
finally { saving.value = false }
}
async function openDetail(value: Record<string, string>) {
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 = ''
if (props.service === 'labeling') selectedRelabelQuantity.value = null
try {
const detail = props.service === 'labeling' ? await getRelabel(value['0']) : props.service === 'disposal' ? await getDiscard(value['0']) : await getTransfer(value['0'])
const discardSummary = props.service === 'disposal' ? (detail as { summary?: ServiceDetail }).summary : undefined
showTransferCartons.value = false
selectedDetail.value = { ...value, ...(detail as ServiceDetail), ...(discardSummary || {}) }
selectedRelabelDetail.value = props.service === 'labeling' ? detail as RelabelDetail : null
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
}
mode.value = 'detail'
if (props.service === 'labeling') void loadCompletionPhotos()
if (props.service === 'transfer') void loadMeasurementPhotos()
if (props.service === 'disposal') void loadDiscardPhotos()
} 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 })
await loadWarehouses()
}
async function confirmTransferMeasurements() {
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
if (!transferNo) return
openCompletionConfirmation(transferNo, 'transfer')
}
async function printTransferCartons(transferNo: string) {
if (props.service !== 'transfer') return
const printWindow = window.open('', '_blank')
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
printWindow.opener = null
try {
if (!transferNo) { printWindow.close(); return }
const label = await getTransferCartonLabel(transferNo)
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 : '箱唛加载失败') }
}
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]
}
async function confirmTransferFromList(transferNo: string) {
openCompletionConfirmation(transferNo, 'transfer')
}
async function loadCompletionPhoto(fileId: string) {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
if (!relabelNo || completionPhotoLoading[fileId]) return
completionPhotoLoading[fileId] = true
completionPhotoErrors[fileId] = false
try {
const blob = await downloadRelabelCompletionPhoto(relabelNo, fileId)
if (selectedRelabelDetail.value?.application.relabelNo !== relabelNo) return
if (completionPhotoUrls[fileId]) URL.revokeObjectURL(completionPhotoUrls[fileId])
const url = URL.createObjectURL(blob)
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))
}
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)))
}
async function confirmRelabelFromList(relabelNo: string) {
openCompletionConfirmation(relabelNo, 'labeling')
}
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 : '文件上传失败') }
}
async function loadWarehouses() {
fbaWarehousesLoading.value = true
fbaWarehousesError.value = ''
operationWarehousesError.value = ''
try {
const [fbaResult, operationResult] = await Promise.allSettled([
loadAllWarehouseRecords('FBA', getMerchantWarehouses),
loadAllWarehouseRecords('OVERSEAS', getMerchantWarehouses),
])
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 目的仓候选加载失败,请检查仓库主数据或权限'
if (operationResult.status === 'rejected') operationWarehousesError.value = '海外仓候选加载失败,请检查仓库主数据或数据库迁移'
} catch (error) {
fbaWarehouses.value = []
fbaWarehousesError.value = error instanceof Error ? error.message : 'FBA 目的仓加载失败'
ElMessage.error(fbaWarehousesError.value)
} finally { fbaWarehousesLoading.value = false }
}
function warehouseLabel(value: MerchantWarehouse) { return `${value.warehouseName} · ${value.warehouseCode}` }
async function loadTransferInventoryCandidates(warehouseId: string) {
transferInventoryCandidates.value = []
transferAvailableSourceCounts.value = {}
if (!warehouseId) return
relabelCandidatesLoading.value = true
try {
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(isUnlabelledTransferCandidate).filter(item =>
item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
? Number(item.cartonAvailablePackages || 0) > 0
: Number(item.availablePackages || 0) > 0)
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)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '当前海外仓库存来源加载失败')
} finally { relabelCandidatesLoading.value = false }
}
async function selectTransferWarehouse() {
selectedOperationWarehouse.value = null
form.pickupContact = ''
form.pickupPhone = ''
form.relabelContact = ''
form.relabelPhone = ''
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = ''
if (!form.warehouse) return
operationWarehouseDetailLoading.value = true
try {
const detail = await getMerchantWarehouseDetail(form.warehouse)
selectedOperationWarehouse.value = detail
form.relabelContact = detail.warehouse.contactName || ''
form.relabelPhone = detail.warehouse.contactPhone || ''
if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = operationWarehouseAddress.value
await loadTransferInventoryCandidates(form.warehouse)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败')
} finally { operationWarehouseDetailLoading.value = false }
}
async function confirmSelectedRelabel() {
const relabelNo = selectedRelabelDetail.value?.application.relabelNo
if (relabelNo) openCompletionConfirmation(relabelNo, 'labeling')
}
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,
idempotencyKey: createIdempotencyKey(),
})
ElMessage.success('贴标单已重新提交审核')
await goToServiceList()
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '重新提交失败')
}
}
const transferInventoryOptions = computed(() => {
const grouped = new Map<string, { value: string; label: string; packages: number }>()
for (const item of transferInventoryCandidates.value) {
// 退货库存没有普通入仓码,使用带前缀的退货来源编码,避免提交时被当作普通入仓单查询。
const returnNo = String(item.inboundOrderNo || '').trim()
const value = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
? (returnNo ? `RETURN_ORDER:${returnNo}` : '')
: String(item.inboundCode || '')
if (!value) continue
const packages = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL'
? inventoryQuantity(item.cartonAvailablePackages || 0)
: inventoryQuantity(item.availablePackages)
const current = grouped.get(value)
if (current) current.packages += packages
else if ((transferAvailableSourceCounts.value[value] ?? 1) > 0) grouped.set(value, { value, label: value.startsWith('RETURN_ORDER:') ? `退货单|${returnNo}` : `${item.inboundOrderNo || value}|${value}`, packages })
}
return [...grouped.values()]
})
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
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
}
async function selectTransferInventory(value: string) {
selectedTransferInventory.value = value || ''
selectedTransferSourceOrderIds.value = []
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()
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.
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)
const grouped = new Map<string, CargoRow>()
for (const candidate of candidates) {
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 })
}
}
cargoRows.value = [...grouped.values()]
try {
if (isReturnSource) return
const historical = await listRelabelHistoricalOrders(form.warehouse, value)
const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean)
if (selectedTransferInventory.value === value) selectedTransferSourceOrderIds.value = sourceIds
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败')
}
}
function selectTransferInboundMethod(value: string | number | boolean | undefined) {
form.address = value === 'CUSTOMER_DELIVERY' ? operationWarehouseAddress.value : ''
}
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 = '' }
}
async function selectOperationWarehouse() {
cargoRows.value = []
transferRelabelRows.value = []
skuRelabelRows.value = []
importedRelabelBindings.value = []
selectedRelabelInboundOrderId.value = ''
relabelCandidates.value = []
const warehouse = operationWarehouses.value.find((item) => String(item.id) === form.warehouse)
form.contact = warehouse?.contactName || ''
form.phone = warehouse?.contactPhone || ''
if (!form.warehouse) return
relabelCandidatesLoading.value = true
try { relabelCandidates.value = await listRelabelInventoryCandidates(form.warehouse) }
catch (error) { ElMessage.error(error instanceof Error ? error.message : '当前客户库存加载失败') }
finally { relabelCandidatesLoading.value = false }
}
function selectRelabelInventory(inboundOrderId: string) {
const rows: CargoRow[] = []
const selectedCandidates = currentRelabelCandidates.value.filter((item) => {
return item.returnCartonId ? returnSourceNos(item).includes(inboundOrderId.slice('RETURN_ORDER:'.length)) : String(item.inboundOrderId) === inboundOrderId
})
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
for (const candidate of selectedCandidates) rows.push({
id: ++cargoSequence, sku: candidate.skuItems?.[0]?.skuCode || candidate.skuCode || '',
name: candidate.productNameZh || '', included: false, quantity: 1, availableQuantity: 1,
returnCartonId: String(candidate.returnCartonId), sourceInboundOrderNo: candidate.inboundOrderNo,
fnsku: '', shipmentNo: candidate.inboundOrderNo || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '',
cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
})
} else for (const candidate of selectedCandidates) {
for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(inventoryQuantity(position.availablePackages), 500) }, () => ({
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
included: false, quantity: 1, availableQuantity: 1,
sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId),
sourceInboundOrderId: String(candidate.inboundOrderId), sourceInboundOrderNo: candidate.inboundOrderNo,
sourceInboundCode: candidate.inboundCode, sourceWarehouseName: candidate.overseasWarehouseName,
palletNo: position.palletNo, warehouseLocationCode: position.warehouseLocationCode,
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '',
destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '',
})))
}
cargoRows.value = rows.slice(0, 500)
if (cargoRows.value.length && !inboundOrderId.startsWith('RETURN_ORDER:')) cargoRows.value[0].included = true
skuRelabelRows.value = []
importedRelabelBindings.value = []
shippingLabelFile.value = null
cartonTotalFile.value = null
skuTotalFile.value = null
if (!isReturnRelabelSource.value && labelingMode.value !== 'carton') addSkuRelabelRow()
}
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))
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: '' })
}
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
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: '' })
}
</script>
<template>
<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 }">
<template v-if="mode === 'list'">
<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" />
<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>
<template #transferRelabelStatus="{ row }"><el-tag :type="transferRelabelStatusTagType(row.__relabelStatus)" effect="light">{{ row.__relabelStatusLabel }}</el-tag></template>
<template #labelingQuantity="{ row, column }"><span :class="{ 'warehouse-missing-value': row[column.property] === '—' }">{{ row[column.property] }}</span></template>
<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>
<template #labelingStatus="{ row }"><el-tag :type="labelingStatusTagType(row.__status)" size="small" effect="light">{{ statusLabel(row.__status) }}</el-tag></template>
<template #transferStatus="{ row }"><el-tag :type="transferStatusTagType(row.__status)" effect="light">{{ row.__statusLabel }}</el-tag></template>
</DataTable>
</div>
</template>
<template v-else-if="mode === 'create' || mode === 'edit'">
<div class="warehouse-page-heading"><h2>{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}</h2><el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button></div>
<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')">
<div id="transfer-main-pane" class="warehouse-card warehouse-form-section warehouse-transfer-main-card">
<div v-if="isTransferCreate || isDisposalCreate" class="warehouse-order-info-heading"><h3>{{ isTransferCreate ? '转运信息' : '基础信息' }}</h3></div>
<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>
<h3 v-else-if="!isTransferCreate && !isDisposalCreate">{{ props.service === 'transfer' ? '转运信息' : props.service === 'relabel' ? '退货信息' : '基础信息' }}</h3>
<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">
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<div v-if="props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2">
<div><span>海外仓收货信息</span><strong>{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}</strong></div>
<label class="warehouse-span-2"><span><i>*</i>送货地址</span><el-input v-model="form.address" placeholder="" /></label>
<div><span>收货联系人</span><strong>{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}</strong></div>
<div><span>联系电话</span><strong>{{ selectedOperationWarehouse?.warehouse.contactPhone || '未维护' }}</strong></div>
</div>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="" /></label>
<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>
<label class="warehouse-span-2"><span>{{ props.service === 'disposal' && discardQuantityMode ? '备注 *' : '备注' }}</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="" /></label>
<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>
</template>
</div>
</div>
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
<template v-if="props.service === 'labeling'">
<div class="warehouse-section-heading warehouse-labeling-heading">
<div><h3>货物明细</h3></div>
</div>
<div class="warehouse-labeling-body">
<div class="warehouse-file-groups warehouse-file-groups--compact">
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary">
<div class="warehouse-file-group__heading"><div><strong><span>1</span>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : isReturnRelabelSource ? labelingMode === 'sku' ? '必传 · SKU 标签文件' : '必传 · 箱唛文件' : '必传 · 二选一' }}</em></div>
<div class="warehouse-file-options">
<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>
<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>
</section>
<section class="warehouse-file-group">
<div class="warehouse-file-group__heading"><div><strong><span>2</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>
</section>
</div>
<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">
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
<el-input v-model="returnCartonQuery" clearable placeholder="" />
<template v-if="returnLooseCandidates.length">
<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>
</template>
<template v-if="returnBoxCandidates.length">
<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>
<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>
</template>
</div>
<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 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">
<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>
<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>
<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>
</template>
<template v-else>
<div v-if="isTransferCreate || isDisposalCreate" class="warehouse-order-info-heading"><h3>{{ isDisposalCreate && discardQuantityMode ? '废弃数量' : isDisposalCreate && discardSelectedCandidate?.cartonSelectionSupported ? '选择废弃箱子' : '货物明细' }}</h3></div>
<div :class="{ 'warehouse-transfer-create-body': isTransferCreate, 'warehouse-disposal-create-body': isDisposalCreate }">
<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>
</div>
<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>
<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>
<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>
</template>
<el-alert v-else type="info" :closable="false" :title="discardSelectedCandidate.cartonSelectionBlockReason || '当前库存缺少箱唛与托盘对应关系,将预留所选库存,由仓库核对相关托盘并填写实际废弃箱数,未废弃部分释放。'" />
<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>
<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>
<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>
<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>
</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>
<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>
</div>
</template>
</div>
<TransferPaneDivider v-if="props.service === 'transfer' && form.relabel" v-model="transferPaneShare" class="warehouse-transfer-divider" />
<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>
</div>
<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>
<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">
<div class="warehouse-file-group__heading"><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
<div class="warehouse-file-options">
<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>
<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>
<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">
</section>
<section class="warehouse-file-group warehouse-transfer-template-group">
<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>
<input ref="unifiedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate">
</section>
</div>
<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>
<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>
<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>
<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>
</template>
<template v-else>
<header class="warehouse-detail-header">
<button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
<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>
</header>
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
<ReviewRejectionNotice v-if="!detailError" :status="props.service === 'labeling' ? selectedRelabelDetail?.application.status : selectedDetail?.status" :reason="props.service === 'labeling' ? selectedRelabelDetail?.application.reviewRemark : selectedDetail?.reviewRemark" />
<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(selectedDetail?.createdAt) }}</strong></div></div><div v-else-if="props.service === 'disposal'" class="warehouse-detail-grid"><div><span>废弃申请单号</span><strong>{{ selectedDetail?.discardNo || '—' }}</strong></div><div><span>废弃出库单号</span><strong>{{ selectedDetail?.outboundNo || '—' }}</strong></div><div><span>进仓编号</span><strong>{{ selectedDetail?.inboundCodes || '—' }}</strong></div><div><span>操作仓</span><strong>{{ selectedDetail?.overseasWarehouseName || '—' }}</strong></div><div><span>SKU 汇总</span><strong>{{ [...new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean))].join('、') || '未记录' }}</strong></div><div><span>申请箱数</span><strong>{{ selectedDetail?.requestedPackages ?? '未记录' }}</strong></div><div><span>预估费用</span><strong>{{ selectedDetail?.currency || 'USD' }} {{ selectedDetail?.totalFee ?? '未记录' }}</strong></div><div><span>状态</span><strong>{{ statusLabel(selectedDetail?.status || '') }}</strong></div><div><span>废弃完成时间</span><strong>{{ formatDateTime(selectedDetail?.customerConfirmedAt) }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
<TransferPickupProgressPanel v-if="!detailError && props.service === 'transfer' && selectedDetail?.deliveryMethod === 'WAREHOUSE_PICKUP'" :transfer-no="String(selectedDetail.transferNo || '')" />
<div v-if="props.service === 'disposal'" class="warehouse-card warehouse-form-section">
<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>
<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>
<el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" 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>
<el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast">
<el-form label-width="110px"><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>
</el-dialog>
</div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
<div class="warehouse-section-heading"><h3>预报与实际签收</h3></div>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-comparison-table"><colgroup><col style="width:500px" /><col style="width:140px" /><col style="width:130px" /><col style="width:100px" /><col style="width:130px" /><col style="width:130px" /></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>
<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>
</article>
</div>
<div v-else class="warehouse-empty">暂无托盘数据</div>
</details>
<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>
</div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-received-card">
<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="1120px" align-center destroy-on-close append-to-body>
<div class="warehouse-cartons-dialog-body">
<el-collapse>
<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>
<div class="warehouse-carton-measurements">所属托盘:{{ selectedDetail?.pallets?.find((pallet) => carton.transferPalletId != null && String(pallet.id) === String(carton.transferPalletId))?.palletNo || '散箱' }} · 清点时间:{{ formatDateTime(carton.countedAt) }}</div>
<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>
<template #footer><el-button @click="showTransferCartons = false">关闭</el-button></template>
</el-dialog>
</div>
<div v-if="props.service === 'transfer' && selectedDetail?.needRelabel" class="warehouse-card warehouse-form-section">
<h3>换标箱级去向</h3>
<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>
</div>
<div v-if="props.service === 'labeling' && selectedRelabelDetail" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
<div class="warehouse-detail-grid warehouse-fulfillment-grid">
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
<div><span>履约状态</span><el-tag :type="relabelFulfillmentTagType(selectedRelabelDetail?.application.fulfillmentStatus)" effect="light">{{ relabelFulfillmentLabel(selectedRelabelDetail?.application.fulfillmentStatus) }}</el-tag></div>
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
<div v-if="selectedRelabelDetail"><RelabelDocumentsPanel inline :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div>
</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>
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
</div>
<div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div>
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>贴标明细资料</h3><input ref="relabelFileInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="uploadSelectedRelabelFile"></div>
<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>
</tbody></table></div>
</div>
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<h3>换标任务明细</h3>
<div class="warehouse-table-scroll"><table class="warehouse-relabel-detail-table"><thead><tr><th>换标模式</th><th>我方箱唛</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th><th>任务数量/单位</th><th>商品 SKU</th><th>SKU 换标明细</th><th>FBA 货件编号</th><th>贴标要求</th><th>备注</th></tr></thead><tbody>
<tr v-for="item in selectedRelabelDetail?.items || []" :key="item.id"><td>{{ item.operationType === 'CARTON_AND_SKU' ? '箱唛 + SKU 换标' : ['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>
</div>
</template>
</section>
<CompletionConfirmationDialog v-model="completionConfirmOpen" :kind="completionConfirmKind" :document-no="completionConfirmNo" @confirmed="refreshAfterCompletion" />
</template>
<style scoped>
.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; }
.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;
}
.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)}}
.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}}
.warehouse-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}
.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}
.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}
.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}
.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}
.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}
.warehouse-file-clear:hover:not(:disabled){color:#dc2626;background:#fee2e2}
.warehouse-file-clear:disabled{opacity:.5;cursor:not-allowed}
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
.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); }
.warehouse-file-groups--compact .warehouse-template-list > div { grid-template-columns: minmax(0, 1fr) auto; }
}
.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}
.warehouse-measurement-evidence{margin-top:16px}
.warehouse-measurement-photo-grid{flex:1;grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
.warehouse-measurement-photo-item{width:100%;height:120px}
.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}
.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}
.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}
.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}
@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}}
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}}
.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}
.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}
.warehouse-service-page--detail .warehouse-transfer-overview-grid>.warehouse-overview-wide{grid-column:span 2}
.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; }
.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; }
.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,
.warehouse-service-page--disposal-form .warehouse-section-heading,
.warehouse-service-page--disposal-form .warehouse-table-scroll { width:100%; max-width:none; }
@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); }
.warehouse-transfer-cargo-card .warehouse-section-heading, .warehouse-transfer-cargo-card .warehouse-table-scroll { max-width: none; }
.warehouse-transfer-form .warehouse-choice-field--relabel { grid-template-columns: 1fr; align-items: start; }
}
.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,
.warehouse-transfer-form .warehouse-file-groups--compact { width:100%; max-width:none; }
.warehouse-transfer-form--relabel .warehouse-transfer-divider { grid-column:2; grid-row:1 / span 2; }
</style>
<style scoped>
.warehouse-discard-boxes { margin-bottom: 24px; }
.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}
.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}
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-pallet-list{padding:0 16px}
.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}
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
.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: 12px; }
.warehouse-create .warehouse-page-heading h2 { margin: 0; color: #1f2937; font-size: 26px; font-weight: 700; }
.warehouse-create .warehouse-form-section { min-width: 0; margin-bottom: 12px; 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 10px; padding: 0; border: 0; }
.warehouse-create .warehouse-form-section h3 { display: flex; align-items: center; gap: 10px; margin: 0; padding: 0; border: 0; color: #1f2937; font-size: 18px; font-weight: 700; }
.warehouse-create .warehouse-form-section h3::before { content: ''; width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: #2f7bff; }
.warehouse-create .warehouse-labeling-heading h3::before { background: #0ea5a4; }
.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: 16px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
.warehouse-create .warehouse-form-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
.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 6px; padding: 0; color: #374151; font-size: 14px; font-weight: 600; line-height: 20px; }
.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: 20px; }
/* 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% + 6px); right: 0; left: auto; width: auto; max-width: calc(100% - 170px); padding: 0; font-size: 12px; line-height: 20px; 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: 44px; box-sizing: border-box; padding: 10px 14px; border: 1px solid #d5dce8; border-radius: 6px; background: #fff; box-shadow: none; }
.warehouse-create .warehouse-form-grid :deep(.el-input__inner) { height: 22px; line-height: 22px; color: #1f2937; font-size: 14px; }
.warehouse-create .warehouse-form-grid :deep(.el-textarea__inner) { min-height: 110px !important; padding: 11px 14px; border: 1px solid #d5dce8; border-radius: 6px; box-shadow: none; font-size: 14px; }
.warehouse-create .warehouse-form-grid :deep(.el-checkbox) { height: 44px; }
.warehouse-create .warehouse-form-grid :deep(.el-checkbox__label) { font-size: 14px; }
.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: #2f7bff; }
.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: 44px; box-sizing: border-box; padding: 8px 10px; border-color: #d5dce8; border-radius: 6px; }
.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: 12px; margin-bottom: 12px; }
.warehouse-service-page.warehouse-create .warehouse-file-group { border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; box-shadow: none; }
.warehouse-service-page.warehouse-create .warehouse-file-group__heading { padding: 10px 12px; background: #fff; }
.warehouse-service-page.warehouse-create .warehouse-file-group__options { padding: 10px 12px; }
.warehouse-service-page.warehouse-create .warehouse-labeling-mode { width: auto; max-width: none; gap: 12px; margin: 12px 0; padding: 10px 12px; border-color: #e3e8f0; border-radius: 6px; background: #fff; }
.warehouse-service-page.warehouse-create .warehouse-tab-actions { margin: 0 0 10px; }
.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: 1px solid #d5dce8; border-radius: 6px; }
.warehouse-create .warehouse-submit-bar { justify-content: flex-end; gap: 12px; 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: 44px; margin-left: 0; padding: 10px 18px; border: 1px solid #d5dce8; border-radius: 6px; background: #fff; color: #1d5fe0; font-size: 14px; font-weight: 600; }
.warehouse-create .warehouse-submit-bar :deep(.el-button--primary) { padding: 12px 28px; border-color: #2f7bff; background: #2f7bff; color: #fff; }
.warehouse-create .warehouse-submit-bar :deep(.el-button.is-disabled) { opacity: .55; }
:global(.labeling-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
</style>
<style scoped>
.warehouse-transfer-create .warehouse-transfer-form { gap: 12px; }
.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: 44px; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
/* 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, 1fr)); }
.warehouse-transfer-create .warehouse-transfer-create-body { min-width: 0; padding: 16px; border: 1px solid #e3e8f0; border-radius: 10px; 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: 18px; }
.warehouse-transfer-create .warehouse-transfer-cargo-card > .warehouse-order-info-heading h3::before { background: #0ea5a4; }
.warehouse-transfer-create .warehouse-transfer-relabel > .warehouse-section-heading h3::before { background: #0ea5a4; }
.warehouse-transfer-create .warehouse-transfer-relabel > .warehouse-section-heading { margin-bottom: 10px; }
.warehouse-transfer-create .warehouse-transfer-create-body section h3 { font-size: 14px; }
.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, 1fr)); gap: 12px 20px; padding: 12px; border: 1px solid #e3e8f0; border-radius: 6px; background: #f4f7fc; }
.warehouse-transfer-create .warehouse-receiving-card > .el-form-item { display: block; }
.warehouse-transfer-create .warehouse-receiving-card > div > span { font-size: 14px; }
.warehouse-transfer-create :deep(.el-form-item__error) { max-width: calc(100% - 140px); }
.warehouse-transfer-create .warehouse-form-grid :deep(.el-radio-button__inner) { padding: 11px 14px; font-size: 14px; line-height: 20px; box-shadow: none; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-transfer-file-groups { gap: 12px; margin-bottom: 12px; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-file-group { border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; box-shadow: none; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-labeling-mode { margin: 12px 0; padding: 10px 12px; border: 1px solid #e3e8f0; border-radius: 6px; background: #fff; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table { width: 100%; font-size: 13px; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table th { height: 40px; font-size: 13px; }
.warehouse-transfer-create .warehouse-transfer-create-body .warehouse-edit-table td { padding: 8px; }
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-input__wrapper),
.warehouse-transfer-create .warehouse-table-scroll :deep(.el-textarea__inner) { border: 1px solid #d5dce8; border-radius: 6px; 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: #2f7bff; }
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar { justify-content: flex-end; gap: 12px; padding: 0; }
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar :deep(.el-button) { min-height: 44px; padding: 10px 18px; }
.warehouse-transfer-create .warehouse-transfer-form .warehouse-submit-bar :deep(.el-button--primary) { padding: 12px 28px; }
.warehouse-transfer-create .warehouse-form-grid :deep(.el-date-editor.el-input) { height: 44px; }
.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: 2px solid #2f7bff; outline-offset: 0; }
:global(.transfer-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
</style>
<style scoped>
.warehouse-disposal-create .warehouse-form-grid { --el-component-size: 44px; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
.warehouse-disposal-create .warehouse-disposal-create-body { min-width: 0; padding: 16px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
.warehouse-disposal-create .warehouse-transfer-cargo-card > .warehouse-order-info-heading h3::before { background: #0ea5a4; }
.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 12px; }
.warehouse-disposal-create .warehouse-discard-box-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.warehouse-disposal-create .warehouse-discard-box-toolbar :deep(.el-input) { width: calc((100% - 40px) / 3); }
.warehouse-disposal-create .warehouse-edit-table { width: 100%; font-size: 13px; }
.warehouse-disposal-create .warehouse-edit-table th { height: 40px; font-size: 13px; }
.warehouse-disposal-create .warehouse-edit-table td { padding: 8px 12px; }
.warehouse-disposal-create .warehouse-disposal-create-body :deep(.el-input__wrapper) { border: 1px solid #d5dce8; border-radius: 6px; 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: 2px solid #2f7bff; outline-offset: 0; }
.warehouse-disposal-create .warehouse-hint { margin: 10px 0 0; color: #6b7280; font-size: 13px; line-height: 20px; }
.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: 40px; }
.warehouse-disposal-create .warehouse-disposal-quantity > .warehouse-hint { align-self: end; margin-bottom: 12px; }
:global(.disposal-create-popper.el-popper) { border: 1px solid #e3e8f0; border-radius: 6px; box-shadow: none; }
</style>