1408 lines
154 KiB
Vue
1408 lines
154 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { ArrowLeft, 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 {
|
||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
|
||
getTransferCartonLabel, submitTransfer, updateTransfer,
|
||
confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, uploadRelabelShippingLabel,
|
||
uploadRelabelFile,
|
||
listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, 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 { createReturnRelabel, uploadReturnRelabelFile } from '../api/returnOrders'
|
||
|
||
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 { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; 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; destinationWarehouseId: string; remark: string }
|
||
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
|
||
interface ReturnRelabelSkuRow { skuCode: string; productNameZh: string; availableQuantity: number; selected: boolean; quantity: number }
|
||
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
|
||
interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: 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 detailNumber = computed(() => 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 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) }),
|
||
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] || '') : '',
|
||
}),
|
||
})
|
||
const statusOptions = computed(() => props.service === 'labeling'
|
||
? [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }]
|
||
: 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: '' })
|
||
const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', 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> {
|
||
transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string
|
||
destinationType?: '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
|
||
items?: Array<{ skuCode?: string; productNameZh?: string; 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 detailError = ref('')
|
||
const selectedRelabelDetail = ref<RelabelDetail | 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 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 },
|
||
{ key: 'createdDates', 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: index === 1 ? 190 : 130 }))
|
||
const discardCandidates = ref<import('../api/warehouseServices').DiscardCandidate[]>([])
|
||
const selectedDiscardOrderId = ref('')
|
||
const discardCandidatesLoading = ref(false)
|
||
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
|
||
const selectedRelabelInboundOrderId = ref('')
|
||
const relabelCandidatesLoading = ref(false)
|
||
const returnRelabelSkuRows = ref<ReturnRelabelSkuRow[]>([])
|
||
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
|
||
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
|
||
const requiresBothTotalFiles = computed(() => props.service === 'labeling' && !isReturnRelabelSource.value && 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 relabelInboundOptions = computed(() => {
|
||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件' }>()
|
||
for (const candidate of relabelCandidates.value) {
|
||
const returnOrderNo = candidate.inboundOrderNo || String(candidate.returnCartonId || '')
|
||
const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId)
|
||
const current = grouped.get(value)
|
||
if (current) current.availablePackages += inventoryQuantity(candidate.availablePackages)
|
||
else grouped.set(value, {
|
||
value,
|
||
inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'),
|
||
inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '已签收退货库存' : (candidate.inboundCode || '—'),
|
||
availablePackages: inventoryQuantity(candidate.availablePackages),
|
||
quantityUnit: candidate.sourceType === 'RETURN_CARTON' ? '件' : '箱',
|
||
})
|
||
}
|
||
return [...grouped.values()]
|
||
})
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const cartonTemplateInput = ref<HTMLInputElement | null>(null)
|
||
const skuTemplateInput = ref<HTMLInputElement | null>(null)
|
||
const mixedTemplateInput = 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() } })
|
||
watch(() => form.relabel, (value) => {
|
||
if (props.service === 'transfer' && value) {
|
||
labelingMode.value = 'carton_and_sku'
|
||
if (!transferRelabelRows.value.length) addTransferRelabelRow()
|
||
}
|
||
})
|
||
watch(labelingMode, (value) => { if (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)
|
||
})
|
||
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos() })
|
||
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) }
|
||
function resetForm() {
|
||
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', 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 = []
|
||
selectedRelabelInboundOrderId.value = ''
|
||
transferDestinationType.value = 'NONE'
|
||
selectedOperationWarehouse.value = null
|
||
editingTransferNo.value = ''
|
||
}
|
||
async function openCreate() {
|
||
resetForm(); mode.value = 'create'
|
||
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') 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) {
|
||
const routeName = ({ labeling: 'overseas-labeling', transfer: 'overseas-storage-transfer', disposal: 'overseas-cargo-disposal', relabel: 'overseas-return-relabel' } as Record<ServiceType, string>)[props.service]
|
||
void router.push({ name: routeName })
|
||
return
|
||
}
|
||
mode.value = 'list'
|
||
}
|
||
|
||
async function openEdit(transferNo: string) {
|
||
try {
|
||
resetForm()
|
||
await loadWarehouses()
|
||
const detail = await getTransfer(transferNo) as ServiceDetail
|
||
if (detail.status !== 'DRAFT') 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.packageCount || 0), shippingMark: item.shippingMark || '',
|
||
customerCartonMarks: item.shippingMark || '',
|
||
}))
|
||
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 }
|
||
}
|
||
mode.value = 'edit'
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '草稿加载失败') }
|
||
}
|
||
async function selectDiscardWarehouse() {
|
||
selectedDiscardOrderId.value = ''
|
||
form.inboundNo = ''
|
||
discardCandidates.value = []
|
||
cargoRows.value = []
|
||
transferRelabelRows.value = []
|
||
if (!form.warehouse) return
|
||
discardCandidatesLoading.value = true
|
||
try {
|
||
const candidates = await listDiscardCandidates()
|
||
discardCandidates.value = candidates.filter((candidate) => String(candidate.overseasWarehouseId) === form.warehouse)
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') }
|
||
finally { discardCandidatesLoading.value = false }
|
||
}
|
||
function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } }
|
||
function addCargoRow() { cargoRows.value.push(createCargoRow()) }
|
||
function escapePrintHtml(value: string) {
|
||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''')
|
||
}
|
||
function printTransferDraftCartonMarks() {
|
||
if (props.service !== 'transfer') return
|
||
const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.quantity) > 0)
|
||
if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.quantity)))) {
|
||
ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return
|
||
}
|
||
const totalCartons = validRows.reduce((sum, row) => sum + Number(row.quantity), 0)
|
||
if (totalCartons > 500) { ElMessage.warning('单次最多打印 500 张箱唛'); return }
|
||
const batch = new Date().toISOString().replace(/\D/g, '').slice(2, 14)
|
||
const labels: Array<{ mark: string; sku: string; index: number }> = []
|
||
let cartonIndex = 0
|
||
for (const [rowIndex, row] of validRows.entries()) {
|
||
let marks = transferCustomerMarks(row)
|
||
if (marks.length !== Number(row.quantity)) {
|
||
marks = Array.from({ length: Number(row.quantity) }, (_, index) => `CT-${batch}-${String(rowIndex + 1).padStart(2, '0')}-${String(index + 1).padStart(3, '0')}`)
|
||
row.customerCartonMarks = marks.join('\n')
|
||
}
|
||
for (const mark of marks) labels.push({ mark, sku: row.sku.trim(), index: ++cartonIndex })
|
||
}
|
||
const printWindow = window.open('', '_blank')
|
||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||
printWindow.opener = null
|
||
const sections = labels.map((label) => `<section class="label"><div class="title">仓储转运客户箱唛</div><div class="mark">${escapePrintHtml(label.mark)}</div><div class="sku">SKU:${escapePrintHtml(label.sku)}</div><div class="meta">${label.index} / ${labels.length}</div></section>`).join('')
|
||
printWindow.document.open()
|
||
printWindow.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body>${sections}</body></html>`)
|
||
printWindow.document.close(); printWindow.focus(); printWindow.print()
|
||
}
|
||
function addTransferRelabelRow() { transferRelabelRows.value.push(createCargoRow()) }
|
||
function clearTransferRelabelRows() {
|
||
transferRelabelRows.value = []
|
||
}
|
||
function addSkuRelabelRow() {
|
||
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: '', 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.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId
|
||
}
|
||
function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null }
|
||
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 = cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex]
|
||
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()
|
||
if (changeCarton && (!fbaCartonMark || !form.destination
|
||
|| rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark))) {
|
||
throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 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: 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: row.quantity }, () => [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 downloadRelabelTemplate(templateType: RelabelTemplateType) {
|
||
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
|
||
const cartonTemplate = templateType === 'carton'
|
||
const mixedTemplate = templateType === 'mixed'
|
||
const includeDestination = false
|
||
const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头'
|
||
const header = cartonTemplate
|
||
? ['明细行标识', cartonMarkHeader, '新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号', '备注']
|
||
: ['明细行标识', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)',
|
||
...(mixedTemplate ? ['新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号'] : []), '备注']
|
||
const rows = cargoRows.value.map((item) => cartonTemplate
|
||
? [item.id, item.cartonMark, item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo, item.remark]
|
||
: [item.id, item.cartonMark, item.sku, '', 1,
|
||
...(mixedTemplate ? [item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo] : []), item.remark])
|
||
const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
|
||
const url = URL.createObjectURL(blob)
|
||
const anchor = document.createElement('a')
|
||
anchor.href = url
|
||
anchor.download = cartonTemplate ? '贴标申请-箱号关联模板.csv'
|
||
: mixedTemplate ? '贴标申请-SKU混箱换标模板.csv' : '贴标申请-SKU关联模板.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, templateType: RelabelTemplateType) {
|
||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||
if (!file) return
|
||
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
|
||
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
|
||
// Accept both the current display name and legacy template header while keeping the internal key stable.
|
||
const headers = parseCsvLine(lines[0]).map((name) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name)
|
||
if (templateType !== 'carton') {
|
||
const mixedTemplate = templateType === 'mixed'
|
||
const required = ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
|
||
...(mixedTemplate ? ['新FBA箱唛', 'FBA货件编号'] : [])]
|
||
if (required.some((name) => !headers.includes(name))) {
|
||
ElMessage.error('SKU 关联模板格式不正确,请重新下载 SKU 关联模板'); return
|
||
}
|
||
const imported = lines.slice(1).map(parseCsvLine)
|
||
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
|
||
.filter((item) => item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛'])
|
||
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
|
||
ElMessage.error('仓库唛头、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return
|
||
}
|
||
const rows: ImportedRelabelBinding[] = []
|
||
for (const item of imported) {
|
||
const quantity = Number(item['换标数量(件)'])
|
||
if (!Number.isInteger(quantity) || quantity < 1) {
|
||
ElMessage.error(`仓库唛头 ${item['旧箱唛']} 的换标数量必须是大于 0 的整数`); return
|
||
}
|
||
const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识'])
|
||
if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return }
|
||
rows.push({
|
||
id: ++skuRelabelSequence,
|
||
sourceRowId: source.id,
|
||
cartonMark: item['旧箱唛'].trim(),
|
||
originalSku: item['旧SKU标签'].trim(),
|
||
newSku: item['新SKU标签'].trim(),
|
||
quantity,
|
||
fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '',
|
||
destinationWarehouseId: form.destination,
|
||
remark: item['备注'] || '',
|
||
templateType,
|
||
selected: true,
|
||
shipmentNo: mixedTemplate ? item['FBA货件编号'].trim() : '',
|
||
})
|
||
}
|
||
importedRelabelBindings.value = rows
|
||
ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`)
|
||
return
|
||
}
|
||
const required = ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA货件编号']
|
||
if (required.some((name) => !headers.includes(name))) { ElMessage.error('箱号关联模板格式不正确,请重新下载箱号关联模板'); return }
|
||
const imported = lines.slice(1).map(parseCsvLine)
|
||
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
|
||
.filter((item) => item['旧箱唛'] || item['新FBA箱唛'])
|
||
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
|
||
ElMessage.error('模板中的明细行、仓库唛头、新 FBA 箱唛和货件编号必须填写完整'); return
|
||
}
|
||
if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) {
|
||
ElMessage.error('模板中的明细行标识不能重复'); return
|
||
}
|
||
const importedByRow = new Map(imported.map((item) => [item['明细行标识'], item]))
|
||
if (importedByRow.size !== imported.length || [...importedByRow.keys()].some((id) => !cargoRows.value.some((row) => String(row.id) === id))) {
|
||
ElMessage.error('模板存在重复或不属于当前库存的明细行,请重新下载模板'); return
|
||
}
|
||
const bindings: ImportedRelabelBinding[] = []
|
||
for (const [rowId, item] of importedByRow) {
|
||
const row = cargoRows.value.find((candidate) => String(candidate.id) === rowId)!
|
||
bindings.push({ id: ++skuRelabelSequence, sourceRowId: row.id, cartonMark: item['旧箱唛'],
|
||
originalSku: row.sku, newSku: '', quantity: 1, fbaCartonMark: item['新FBA箱唛'],
|
||
destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType,
|
||
selected: true, shipmentNo: item['FBA货件编号'] })
|
||
}
|
||
importedRelabelBindings.value = bindings
|
||
ElMessage.success(`已读取 ${bindings.length} 条箱号关联,请选择后导入到明细`)
|
||
}
|
||
function selectAllImportedBindings(value: boolean) {
|
||
for (const item of importedRelabelBindings.value) item.selected = value
|
||
}
|
||
function importSelectedBindingsToDetails() {
|
||
const selected = importedRelabelBindings.value.filter((item) => item.selected)
|
||
if (!selected.length) { ElMessage.warning('请至少选择一条绑定关系'); return }
|
||
const templateType = selected[0].templateType
|
||
if (selected.some((item) => item.templateType !== templateType)) { ElMessage.warning('请重新上传同一种绑定模板'); return }
|
||
if (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: __, shipmentNo, ...item }) => item)
|
||
for (const item of importedRows) {
|
||
skuRelabelRows.value = skuRelabelRows.value.filter((row) => !(row.cartonMark.trim().toUpperCase() === item.cartonMark.trim().toUpperCase()
|
||
&& row.originalSku.trim().toUpperCase() === item.originalSku.trim().toUpperCase()))
|
||
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) {
|
||
const candidate = discardCandidates.value.find((item) => item.orderId === orderId)
|
||
if (!candidate) { cargoRows.value = []; return }
|
||
form.inboundNo = candidate.inboundCode
|
||
form.warehouse = candidate.overseasWarehouseId
|
||
cargoRows.value = candidate.skus.map((sku) => ({
|
||
id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: 0,
|
||
availableQuantity: sku.availablePackages, sourceOrderId: candidate.orderId,
|
||
fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', 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) : code
|
||
}).join('、')
|
||
}
|
||
function statusLabel(value: string) {
|
||
if (props.service === 'labeling') {
|
||
if (value === 'PROCESSING') return '处理中'
|
||
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认'
|
||
return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(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 || '—'
|
||
}
|
||
function deliveryMethodLabel(value: string) {
|
||
const labels: Record<string, string> = {
|
||
TRUCK: '卡车派送', EXPRESS: '快递派送', TRUCK_PICKUP: '卡车自提', EXPRESS_PICKUP: '快递自提',
|
||
WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓',
|
||
// Keep the legacy value readable for historical applications created before the four-way split.
|
||
PICKUP: '仓库自提',
|
||
}
|
||
return labels[value] || value || '—'
|
||
}
|
||
function formatDateTime(value?: string | null) {
|
||
return value ? value.replace('T', ' ').slice(0, 19) : '—'
|
||
}
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
if (props.service === 'labeling') {
|
||
const result = await listRelabels({ 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) })
|
||
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.customerOrderNo, destinationWarehouses, '—', item.itemCount, item.totalPackageCount, statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status, __confirmedAt: item.customerConfirmedAt || '' }
|
||
})
|
||
} else if (props.service === 'disposal') {
|
||
const result = await listDiscards({ 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) })
|
||
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
|
||
submittedRows.value = result.records.map((item) => row([item.discardNo, item.inboundCodes, item.overseasWarehouseName || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.createdAt)]))
|
||
} else {
|
||
const result = await listTransfers({ page: 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) })
|
||
total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length
|
||
submittedRows.value = result.records.map((item) => {
|
||
const operationWarehouse = warehouseDisplay(item.operatingWarehouseId)
|
||
return props.service === 'transfer'
|
||
? { ...row([item.transferNo, item.packageCount, operationWarehouse, item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status }
|
||
: row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)])
|
||
})
|
||
}
|
||
} catch (error) { submittedRows.value = []; total.value = 0; ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
|
||
finally { 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' && item.__status === 'DRAFT') 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']) })
|
||
return actions
|
||
}
|
||
async function submitReturnRelabel() {
|
||
const items = returnRelabelSkuRows.value.filter(item => item.selected)
|
||
.map(item => ({ skuCode: item.skuCode, quantity: Number(item.quantity) }))
|
||
if (!selectedReturnOrderNo.value || !items.length) { ElMessage.warning('请至少选择一个需要贴标的 SKU'); return }
|
||
if (items.some(item => item.quantity < 1 || item.quantity > (returnRelabelSkuRows.value.find(row => row.skuCode === item.skuCode)?.availableQuantity || 0))) {
|
||
ElMessage.warning('本次贴标数量必须大于 0,且不能超过该 SKU 的库存总数'); return
|
||
}
|
||
const destination = fbaWarehouses.value.find(warehouse => warehouse.warehouseCode === form.destination)
|
||
if (!destination) { ElMessage.warning('请选择 FBA 目的仓'); return }
|
||
const selectedFile = cartonTotalFile.value || skuTotalFile.value
|
||
const fileScope = cartonTotalFile.value ? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const
|
||
if (!selectedFile) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
|
||
try { await ElMessageBox.confirm(`确认提交退货单 ${selectedReturnOrderNo.value} 的贴标申请吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
|
||
saving.value = true
|
||
try {
|
||
const created = await createReturnRelabel(selectedReturnOrderNo.value, {
|
||
items,
|
||
fileScope,
|
||
destinationFbaWarehouseId: String(destination.id),
|
||
destinationFbaWarehouseCode: destination.warehouseCode,
|
||
cartonRemark: form.remark.trim() || undefined,
|
||
idempotencyKey: crypto.randomUUID(),
|
||
})
|
||
await uploadReturnRelabelFile(created.relabelNo, fileScope, selectedFile)
|
||
ElMessage.success(`贴标申请 ${created.relabelNo} 已提交,等待审核`)
|
||
await router.push({ name: 'overseas-labeling' })
|
||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货贴标申请提交失败,请重试') }
|
||
finally { saving.value = false }
|
||
}
|
||
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||
if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return }
|
||
void load()
|
||
})
|
||
async function submit(action: 'draft' | 'submit' = 'submit') {
|
||
if (isReturnRelabelSource.value) { await submitReturnRelabel(); 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 || 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))
|
||
: 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 && !cartonTotalFile.value && !skuTotalFile.value) {
|
||
ElMessage.warning('请上传箱唛总文件或 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.deliveryMethod === 'EXPRESS_PICKUP')
|
||
&& 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.sourceInventoryPositionId)) {
|
||
ElMessage.warning('请选择有库存的历史入仓单'); return
|
||
}
|
||
if (props.service === 'disposal' && !form.reason) {
|
||
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' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || form.volumeCbm < 0 || (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 && '预计入仓时间', form.volumeCbm < 0 && '预估总体积'].filter(Boolean).join('、')
|
||
ElMessage.warning(`请填写:${missing || '必填信息'}`); 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 }
|
||
const relabelRows = transferRelabelRows.value
|
||
const allMarks = relabelRows.flatMap(transferCustomerMarks)
|
||
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 = crypto.randomUUID()
|
||
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 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' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP',
|
||
printShippingLabel: labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && 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: leave the creation form even if the detail reload fails.
|
||
if (props.createPage) await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||
else { mode.value = 'list'; await load(); await openDetail({ '0': created.application.relabelNo }) }
|
||
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
|
||
return
|
||
}
|
||
ElMessage.success(`${config.value.title}提交成功`)
|
||
await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||
return
|
||
} else if (props.service === 'disposal') {
|
||
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
|
||
if (!item.sourceOrderId || item.sourceOrderId !== selectedDiscardOrderId.value || item.quantity > Number(item.availableQuantity || 0)) {
|
||
throw new Error(`SKU ${item.sku} 的可废弃库存不足`)
|
||
}
|
||
return { orderId: item.sourceOrderId, skuCode: item.sku, discardPackages: item.quantity }
|
||
})
|
||
await createDiscard({ items, idempotencyKey, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined })
|
||
} else {
|
||
const packageCount = cargoRows.value.reduce((sum, item) => sum + item.quantity, 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: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: 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: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: 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' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), 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 }
|
||
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') {
|
||
await submitTransfer(transferNo)
|
||
ElMessage.success('转运单已提交审核')
|
||
await openDetail({ '0': transferNo })
|
||
return
|
||
}
|
||
}
|
||
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`)
|
||
if (props.createPage) await router.push({ name: props.service === 'relabel' ? 'overseas-return-relabel' : 'overseas-labeling' })
|
||
else { mode.value = 'list'; await load() }
|
||
} 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 = ''
|
||
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
|
||
selectedDetail.value = { ...value, ...(detail as ServiceDetail), ...(discardSummary || {}) }
|
||
selectedRelabelDetail.value = props.service === 'labeling' ? detail as RelabelDetail : null
|
||
mode.value = 'detail'
|
||
if (props.service === 'labeling') void loadCompletionPhotos()
|
||
if (props.service === 'transfer') void loadMeasurementPhotos()
|
||
} 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 loadWarehouses()
|
||
await openDetail({ '0': detailNumber.value })
|
||
}
|
||
async function confirmTransferMeasurements() {
|
||
const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '')
|
||
if (!transferNo) return
|
||
try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return }
|
||
try { selectedDetail.value = await confirmTransferMeasurement(transferNo) as ServiceDetail; ElMessage.success('仓库实测数据已确认') }
|
||
catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') }
|
||
}
|
||
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) {
|
||
try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return }
|
||
try { await confirmTransferMeasurement(transferNo); ElMessage.success('仓库实测数据已确认'); await load() }
|
||
catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') }
|
||
}
|
||
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) {
|
||
try {
|
||
await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续出库流程吗?', '换标完成确认', { confirmButtonText: '确认并继续', cancelButtonText: '取消' })
|
||
await confirmRelabelCompletion(relabelNo)
|
||
ElMessage.success('已确认换标完成,订单已进入后续出库流程')
|
||
await load()
|
||
} catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '确认失败') }
|
||
}
|
||
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 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
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败')
|
||
} finally { operationWarehouseDetailLoading.value = false }
|
||
}
|
||
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 = relabelCandidates.value.filter((item) => {
|
||
const returnOrderNo = item.inboundOrderNo || String(item.returnCartonId || '')
|
||
return (item.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(item.inboundOrderId)) === inboundOrderId
|
||
})
|
||
returnRelabelSkuRows.value = []
|
||
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
|
||
const grouped = new Map<string, ReturnRelabelSkuRow>()
|
||
for (const candidate of selectedCandidates) {
|
||
const skuItems = candidate.skuItems?.length
|
||
? candidate.skuItems
|
||
: candidate.skuCode ? [{ skuCode: candidate.skuCode, quantity: inventoryQuantity(candidate.availablePackages) }] : []
|
||
for (const item of skuItems) {
|
||
const skuCode = item.skuCode.trim()
|
||
if (!skuCode) continue
|
||
const key = skuCode.toUpperCase()
|
||
const current = grouped.get(key)
|
||
const quantity = inventoryQuantity(item.quantity)
|
||
if (current) current.availableQuantity += quantity
|
||
else grouped.set(key, { skuCode, productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '', availableQuantity: quantity, selected: false, quantity })
|
||
}
|
||
}
|
||
returnRelabelSkuRows.value = [...grouped.values()]
|
||
labelingMode.value = 'sku'
|
||
} 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) 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()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="warehouse-service-page" :class="{ 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--return-relabel': isReturnRelabelSource }">
|
||
<template v-if="mode === 'list'">
|
||
<QueryToolbar v-model="listQueryModel" 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 v-if="props.service === 'transfer' && submittedRows.some((item) => item.__status === 'WAIT_CUSTOMER_CONFIRMATION')" #before-table><div class="warehouse-list-notice">部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。</div></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>
|
||
<form :class="{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" @submit.prevent="submit('submit')">
|
||
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
|
||
<div class="warehouse-form-grid">
|
||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><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><small v-else class="warehouse-hint">请先选择本次废弃货物所在的海外仓。</small></label>
|
||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><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 discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}|${candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint">仅显示当前操作仓下一件代发中仍有可用箱数的入库单。</small></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="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
|
||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @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'" class="warehouse-span-2"><span><i>*</i>选择有库存的入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}`" :value="option.value" /></el-select></label>
|
||
<label v-if="props.service === 'labeling' && (isReturnRelabelSource || labelingMode !== 'sku')"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><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'" 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' && 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' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></label>
|
||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" /></label>
|
||
<div v-if="props.service === 'transfer' && 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="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-if="props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择"><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" /></el-select></label>
|
||
<label v-if="props.service === 'relabel' || props.service === 'disposal'"><span><i>*</i>{{ props.service === 'relabel' ? '退货原因' : '废弃原因' }}</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
|
||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<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="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label>
|
||
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP')" 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.deliveryMethod === 'EXPRESS_PICKUP') && 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 === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预估体积(CBM)' : '预估总体积(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' || 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><i>*</i>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button 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="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
|
||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'" class="warehouse-span-2"><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="请输入 FBA 货件编号" /></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="请输入物流跟踪号(选填)" /></label>
|
||
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" /></label>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>换标服务</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要</el-radio-button><el-radio-button :value="true">需要</el-radio-button></el-radio-group></div>
|
||
</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-file-groups" :class="{ 'warehouse-file-groups--single': isReturnRelabelSource }">
|
||
<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 ? '必传 · 两份文件' : '必传 · 二选一' }}</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 :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 :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 v-if="!isReturnRelabelSource" 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>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||
<div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||
<div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><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">
|
||
<table class="warehouse-edit-table warehouse-return-relabel-table">
|
||
<thead><tr><th>选择</th><th>SKU</th><th>中文品名</th><th>库存总数</th><th>本次贴标数量</th></tr></thead>
|
||
<tbody><tr v-for="row in returnRelabelSkuRows" :key="row.skuCode"><td><el-checkbox v-model="row.selected" /></td><td><strong>{{ row.skuCode }}</strong></td><td>{{ row.productNameZh || '—' }}</td><td><strong>{{ row.availableQuantity }}</strong></td><td><el-input-number v-model="row.quantity" :disabled="!row.selected" :min="1" :max="row.availableQuantity" :precision="0" controls-position="right" placeholder="请输入贴标数量" /></td></tr><tr v-if="returnRelabelSkuRows.length === 0"><td colspan="5"><div class="warehouse-empty">该退货单暂无可申请贴标的 SKU 库存</div></td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
<div v-if="!isReturnRelabelSource" 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="!isReturnRelabelSource && importedRelabelBindings.length" class="warehouse-import-preview">
|
||
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div>
|
||
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></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 v-if="!isReturnRelabelSource" 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="!isReturnRelabelSource && 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="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-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>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><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' ? 7 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||
</template>
|
||
<template v-else>
|
||
<div 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" :disabled="!cargoRows.some((row) => row.sku.trim() && row.quantity > 0)" @click="printTransferDraftCartonMarks"><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 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>{{ 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="props.service === 'transfer' ? '选填' : '请输入商品 SKU'" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入中文品名'" /></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><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><button type="button" class="warehouse-delete" title="删除" @click="removeCargoRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="cargoRows.length === 0"><td :colspan="props.service === 'disposal' ? 4 : props.service === 'relabel' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
|
||
</template>
|
||
</div>
|
||
<div 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-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="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_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><small class="warehouse-hint">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></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>
|
||
<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>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||
</section>
|
||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>客户箱唛号</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></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" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></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" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></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">暂无绑定明细,请点击“添加 SKU 明细”</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" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入数量" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></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">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||
</div>
|
||
<div class="warehouse-submit-bar warehouse-transfer-submit-bar"><el-button v-if="mode === 'create'" native-type="button" @click="resetForm">重置</el-button><el-button v-if="props.service === 'transfer'" 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>
|
||
</form>
|
||
</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><span>{{ config.title }}详情</span></div>
|
||
<div class="warehouse-detail-actions"><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' && selectedDetail?.status === 'DRAFT'" 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 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>
|
||
<div v-else 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?.application.itemCount ?? '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</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 class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
|
||
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||
<div class="warehouse-section-heading"><h3>货物明细</h3><el-button v-if="selectedDetail?.status !== 'DRAFT'" native-type="button" type="primary" @click="printTransferCartons(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))"><Printer :size="16" />打印箱唛</el-button></div>
|
||
<div 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="(item, index) in selectedDetail?.items || []" :key="`${item.skuCode}-${index}`"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ item.shippingMark || '—' }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
|
||
</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 === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-times"><h3>发货时间</h3><div class="warehouse-detail-grid"><div><span>预计发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div></div><p>实际发货时间由仓库出库或 PDA 完成后回填,客户不能手动修改。</p></div>
|
||
<div v-if="props.service === 'labeling' && canManageRelabelFiles" 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><strong>{{ statusLabel(selectedRelabelDetail?.application.fulfillmentStatus || '') }}</strong></div>
|
||
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
|
||
</div>
|
||
<div 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><strong v-else>仓库尚未上传完工照片</strong></div>
|
||
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
|
||
</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-hint">需要补充或替换资料时,可在对应明细行上传 SKU 或箱唛文件。</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 换标' : item.operationType === 'SKU_LABEL' ? 'SKU 换标' : '箱唛换标' }}</td><td>{{ item.cartonMark || '—' }}</td><td>{{ item.skuCode || '—' }}</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 === 'transfer'" class="warehouse-card warehouse-form-section">
|
||
<div class="warehouse-section-heading"><h3>PDA 打托实测数据</h3><span class="warehouse-hint">PDA 作业完成即显示入库完成,客户可确认仓库实测数据</span></div>
|
||
<div v-if="selectedDetail?.pallets?.length" class="warehouse-table-scroll"><table><thead><tr><th>托盘号</th><th>库位</th><th>箱数</th><th>长 × 宽 × 高(cm)</th><th>毛重(kg)</th><th>体积(m³)</th><th>状态</th><th>完成时间</th></tr></thead><tbody><tr v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)"><td>{{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}</td><td>{{ pallet.warehouseLocationCode || '—' }}</td><td>{{ pallet.cartonCount ?? '—' }}</td><td>{{ [pallet.lengthCm, pallet.widthCm, pallet.heightCm].every((value) => value != null) ? `${pallet.lengthCm} × ${pallet.widthCm} × ${pallet.heightCm}` : '—' }}</td><td>{{ pallet.grossWeightKg ?? '—' }}</td><td>{{ pallet.volumeCbm ?? '—' }}</td><td>{{ statusLabel(pallet.status || '') }}</td><td>{{ formatDateTime(pallet.completedAt) }}</td></tr></tbody></table></div>
|
||
<div v-else class="warehouse-empty">暂无 PDA 打托测量数据</div>
|
||
<div class="warehouse-evidence warehouse-measurement-evidence"><span>测量打托现场照片</span><div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-photo-grid"><div v-for="(photo, index) in selectedDetail.measurementPhotos" :key="photo.fileId" class="warehouse-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><strong v-else>仓库尚未上传测量打托照片</strong></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 换标' : item.operationType === 'SKU_LABEL' ? 'SKU 换标' : '箱唛换标' }}</td><td><strong>{{ item.cartonMark || '—' }}</strong></td><td><strong>{{ item.fbaCartonMark || '—' }}</strong></td><td>{{ warehouseDisplay(item.destinationFbaWarehouseCode) }}</td><td>{{ item.totalPackageCount }} 箱</td><td>{{ item.skuCode || '—' }}</td><td><template v-if="item.skuItems?.length"><div v-for="skuItem in item.skuItems" :key="`${skuItem.skuCode}-${skuItem.newSkuCode}-${skuItem.quantity}`">{{ skuItem.skuCode }} → {{ skuItem.newSkuCode }} × {{ skuItem.quantity }} 件</div></template><span v-else>{{ item.newSkuCode || '—' }}</span></td><td>{{ item.shipmentNo || '—' }}</td><td>{{ item.labelRequirement || '—' }}</td><td>{{ item.remark || '—' }}</td></tr>
|
||
<tr v-if="!selectedRelabelDetail?.items?.length"><td colspan="10"><div class="warehouse-empty">暂无换标任务明细</div></td></tr>
|
||
</tbody></table></div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.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-service-page--return-relabel .warehouse-table-scroll:has(> .warehouse-return-relabel-table)~.warehouse-table-scroll{display:none}
|
||
.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-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-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,1fr) minmax(0,1.35fr);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:2;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}
|
||
@media(max-width:1250px){.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
|
||
.warehouse-transfer-form .pickup-address-field{grid-column:span 1}
|
||
/* 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; }
|
||
@media (max-width: 720px) {
|
||
.warehouse-pagination :deep(.el-pagination) { width: 100%; justify-content: flex-end; }
|
||
}
|
||
</style>
|