From 5b320a3293c13444e96ceac9105cfa797133cf09 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 15 Oct 2026 15:45:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/warehouseServices.ts | 9 +- .../CompletionConfirmationDialog.test.mjs | 81 +++++++ .../CompletionConfirmationDialog.vue | 105 +++++++++ .../views/WarehouseServiceView.vue | 212 +++++++++++++----- .../views/returnRelabelSources.test.mjs | 20 ++ .../views/warehouseTransferForm.test.mjs | 64 +++++- src/modules/tenant-management/contracts.ts | 2 + 7 files changed, 430 insertions(+), 63 deletions(-) create mode 100644 src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs create mode 100644 src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index d8eaf02..2c86df0 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -23,9 +23,9 @@ export interface RelabelDetail { remark?: string; items: RelabelItemDetail[] history: Array<{ fromStatus?: string; toStatus?: string; operatorId?: string; remark?: string; createdAt: string }> tailOrders?: Array<{ id: string; orderNo: string; overseasWarehouseId: string; packageCount: number; status: string }> - completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string } + completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string; customerConfirmedAt?: string } } -export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string } +export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string; customerConfirmedAt?: string } export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } export interface DiscardCandidate { cartons?: DiscardBox[]; cartonSelectionSupported?: boolean; cartonSelectionBlockReason?: string; orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'INVENTORY'; inventoryPositionId?: string; palletNo?: string; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } export interface RelabelInventoryCandidate { @@ -48,7 +48,7 @@ export interface TransferCreate { deliveryAddress: string; packageCount: number; volumeCbm: number; deliveryMethod: string; estimatedArrivalAt: string; estimatedShipAt?: string fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string labelDeadline?: string; pickupContactName?: string; pickupContactPhone?: string; relabelContactName?: string; relabelContactPhone?: string; relabelChargeAmount?: number; relabelDeliveryMethod?: RelabelDeliveryMethod - items: Array<{ skuCode?: string; productNameZh?: string; packageCount: number; shippingMark?: string }> + items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string }> relabelItems?: Array<{ operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'; customerCartonMarks: string[]; originalSku?: string; newSku?: string; quantity: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }> } export interface RelabelCreate { @@ -134,6 +134,9 @@ export function createDiscard(payload: { items: Array<{ orderId?: string; return export function cancelDiscard(discardNo: string) { return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/cancel` }) } +export function confirmDiscardCompletion(discardNo: string) { + return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/confirm-completion` }) +} export interface DiscardPhoto { fileId: string; palletNo: string; fileName: string; createdAt: string; submittedAt: string } export function downloadDiscardPhoto(discardNo: string, fileId: string) { diff --git a/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs new file mode 100644 index 0000000..bc8c078 --- /dev/null +++ b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs @@ -0,0 +1,81 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import ts from 'typescript' + +const source = readFileSync(new URL('./CompletionConfirmationDialog.vue', import.meta.url), 'utf8') +const script = source.match(/ + + + + diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 375f4a3..ab41c46 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -2,16 +2,17 @@ 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 { ArrowLeft, ChevronDown, Download, Plus, Printer, Trash2, Upload, X } from '@lucide/vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue' import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue' import type { TableAction } from '@/components/common/tableActions' import { useAuthStore } from '@/modules/auth/public' +import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue' import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, getTransferCartonLabel, submitTransfer, updateTransfer, - confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, + confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, uploadRelabelFile, listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, type RelabelInventoryCandidate, @@ -33,11 +34,11 @@ 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 { discardCartonIds?: string[]; id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } +interface CargoRow { discardCartonIds?: string[]; id: number; included?: boolean; sku: string; name: string; quantity: number; expectedPackageCount?: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string } interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } -interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string } +interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string; transferItemId?: string | number; transferPalletId?: string | number; length?: number; width?: number; height?: number; weight?: number; quantity?: number; discrepancyReason?: string; countedAt?: string } const props = withDefaults(defineProps<{ service: ServiceType; createPage?: boolean; detailPage?: boolean; detailNo?: string }>(), { createPage: false, detailPage: false, detailNo: '' }) const auth = useAuthStore() @@ -53,10 +54,22 @@ const canCreate = computed(() => auth.hasPermission(({ const canManageRelabelFiles = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:files')) const canConfirmRelabel = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:confirm')) const canConfirmTransfer = computed(() => auth.hasPermission('merchant:overseas-warehouse:transfer:confirm')) +const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm')) +const completionConfirmOpen = ref(false) +const completionConfirmNo = ref('') +const completionConfirmKind = ref<'disposal' | 'labeling'>('disposal') +function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling') { + if (!no) return + completionConfirmNo.value = no; completionConfirmKind.value = kind; completionConfirmOpen.value = true +} +async function refreshAfterCompletion(no: string) { + if (mode.value === 'detail') await openDetail({ '0': no }) + else await load() +} const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel')) const configs: Record = { labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '关联业务单号', '海外操作仓', 'SKU 数(种)', '总箱数/包裹', '换标类型', '状态', '审核时间', '贴标完成时间', '贴标出库时间', '创建时间'] }, - transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '审核时间', '测量时间', '出库时间', '创建时间'] }, + transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '预计箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '审核时间', '测量时间', '出库时间', '创建时间'] }, disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '废弃出库单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '废弃完成时间', '创建时间'] }, relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '审核时间', '测量时间', '出库时间', '创建时间'] }, } @@ -81,7 +94,7 @@ const listQueryModel = computed({ const statusOptions = computed(() => props.service === 'labeling' ? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }] : props.service === 'disposal' - ? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待 PDA 废弃', value: 'APPROVED' }, { label: '待仓库确认完成', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] + ? [{ 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' }, @@ -130,11 +143,35 @@ interface ServiceDetail extends Record { relabelContactPhone?: string; relabelDeliveryMethod?: string; relabelNo?: string relabelStatus?: string; relabelFulfillmentStatus?: string; createdAt?: string inboundCode?: string; sourceOrders?: Array<{ lastlegOrderId?: string | number }> - items?: Array<{ cartonMarks?: string[]; inboundCode?: string; productName?: string; discardPackages?: number; skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }> + items?: Array<{ id?: string | number; cartonMarks?: string[]; inboundCode?: string; productName?: string; discardPackages?: number; skuCode?: string; productNameZh?: string; quantity?: number | null; packageCount?: number; shippingMark?: string }> relabelBindings?: Array<{ operationType?: string; customerCartonMark?: string; originalSku?: string; newSku?: string; quantity?: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }> cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string } const selectedDetail = ref(null) +const showTransferCartons = ref(false) +function transferCartonItem(carton: TransferCarton) { + return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId)) +} +function palletCartons(pallet: TransferPallet) { + return (selectedDetail.value?.cartons || []).filter((carton) => carton.status === 'COUNTED' && pallet.id != null && carton.transferPalletId != null && String(carton.transferPalletId) === String(pallet.id)) +} +function palletSkuRows(pallet: TransferPallet) { + const summary = new Map() + for (const carton of palletCartons(pallet)) { + const item = transferCartonItem(carton); const sku = item?.skuCode || '未标记 SKU' + summary.set(sku, (summary.get(sku) || 0) + Number(carton.quantity || 0)) + } + return [...summary.entries()].map(([sku, quantity]) => ({ sku, quantity })) +} +function palletQuantity(pallet: TransferPallet) { return palletCartons(pallet).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0) } +function countedTransferCartons() { return (selectedDetail.value?.cartons || []).filter((carton) => carton.status === 'COUNTED') } +function receivedItemCartons(itemId: string | number | undefined) { + return countedTransferCartons().filter(carton => itemId != null && carton.transferItemId != null && String(carton.transferItemId) === String(itemId)) +} +function receivedItemQuantity(itemId: string | number | undefined) { + return receivedItemCartons(itemId).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0) +} + const detailError = ref('') const selectedRelabelDetail = ref(null) const completionPhotoUrls = reactive>({}) @@ -463,7 +500,7 @@ async function openEdit(transferNo: string) { 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 || '', + quantity: Number(item.quantity || 0), expectedPackageCount: Number(item.packageCount || 0), shippingMark: item.shippingMark || '', customerCartonMarks: item.shippingMark || '', })) const bindings = detail.relabelBindings || [] @@ -520,26 +557,26 @@ async function selectDiscardWarehouse() { } 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 createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, expectedPackageCount: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } } function addCargoRow() { cargoRows.value.push(createCargoRow()) } function 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)))) { + const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.expectedPackageCount) > 0) + if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.expectedPackageCount)))) { ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return } - const totalCartons = validRows.reduce((sum, row) => sum + Number(row.quantity), 0) + const totalCartons = validRows.reduce((sum, row) => sum + Number(row.expectedPackageCount), 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')}`) + if (marks.length !== Number(row.expectedPackageCount)) { + marks = Array.from({ length: Number(row.expectedPackageCount) }, (_, 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 }) @@ -696,7 +733,7 @@ function downloadTransferCartonMarkTemplate() { ElMessage.warning('请先填写完整的 SKU 明细'); return } const header = ['SKU', '客户箱唛号'] - const rows = cargoRows.value.flatMap((row) => Array.from({ length: row.quantity }, () => [row.sku, ''])) + const rows = cargoRows.value.flatMap((row) => Array.from({ length: Number(row.expectedPackageCount || 0) }, () => [row.sku, ''])) const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' }) const url = URL.createObjectURL(blob); const anchor = document.createElement('a') anchor.href = url; anchor.download = '仓储转运-客户箱唛模板.csv'; anchor.click(); URL.revokeObjectURL(url) @@ -932,7 +969,7 @@ function statusLabel(value: string) { PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '换标完成', COMPLETED: '已完成', CANCELLED: '已取消', - DRAFT: '草稿', PENDING_REVIEW: '待审核', APPROVED: '待 PDA 废弃', PDA_COMPLETED: '待仓库确认完成', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配', + 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: '派送完成', @@ -1031,7 +1068,7 @@ async function load() { } 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), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true) }) total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length - submittedRows.value = result.records.map((item) => row([item.discardNo, item.outboundNo || '—', item.inboundCodes, item.overseasWarehouseName || warehouseDisplay(item.overseasWarehouseId) || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.completedAt), formatDateTime(item.createdAt)])) + submittedRows.value = result.records.map((item) => ({ ...row([item.discardNo, item.outboundNo || '—', item.inboundCodes, item.overseasWarehouseName || warehouseDisplay(item.overseasWarehouseId) || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.customerConfirmedAt), formatDateTime(item.createdAt)]), __status: item.status })) } 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), reviewedFrom: dateTime(filters.reviewStart), reviewedTo: dateTime(filters.reviewEnd, true), measuredFrom: props.service === 'relabel' ? dateTime(filters.measuredStart) : dateTime(filters.completedStart), measuredTo: props.service === 'relabel' ? dateTime(filters.measuredEnd, true) : dateTime(filters.completedEnd, true), outboundFrom: dateTime(filters.outboundStart), outboundTo: dateTime(filters.outboundEnd, true) }) total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length @@ -1054,6 +1091,7 @@ function listRowActions(candidate: object): TableAction[] { if (canCreate.value && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(item.__status)) actions.push({ key: 'edit', label: '编辑', run: () => openEdit(item['0']) }) if (canConfirmTransfer.value && props.service === 'transfer' && item.__status === 'WAIT_CUSTOMER_CONFIRMATION') actions.push({ key: 'confirm-measurement', label: '确认测量', run: () => confirmTransferFromList(item['0']) }) if (canConfirmRelabel.value && props.service === 'labeling' && item.__status === 'CUSTOMER_CONFIRM_PENDING' && !item.__confirmedAt) actions.push({ key: 'confirm-relabel', label: '确认换标', run: () => confirmRelabelFromList(item['0']) }) + if (canConfirmDiscard.value && props.service === 'disposal' && item.__status === 'PDA_COMPLETED') actions.push({ key: 'confirm-discard', label: '废弃确认', run: () => openCompletionConfirmation(item['0'], 'disposal') }) return actions } watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => { @@ -1069,7 +1107,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { : (props.service === 'labeling' || props.service === 'relabel') && (relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton' ? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1 || (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.shipmentNo.trim()))) - : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name + : (props.service === 'labeling' ? cartonDetailRows.value : cargoRows.value).some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || (props.service !== 'labeling' && item.quantity < 1)))) if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) { ElMessage.warning('请填写必填信息和完整的货物明细'); return @@ -1105,10 +1143,14 @@ async function submit(action: 'draft' | 'submit' = 'submit') { 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('、') + if (props.service === 'transfer' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) { + const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupContact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupPhone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间'].filter(Boolean).join('、') ElMessage.warning(`请填写:${missing || '必填信息'}`); return } + if (props.service === 'transfer' && (!cargoRows.value.length || cargoRows.value.some((row) => + !Number.isSafeInteger(row.quantity) || row.quantity < 1 || !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1))) { + ElMessage.warning('请填写每个 SKU 的数量(件)和预计箱数,均须为正整数'); return + } if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) { ElMessage.warning('请选择 FBA 目的仓'); return } @@ -1169,12 +1211,12 @@ async function submit(action: 'draft' | 'submit' = 'submit') { }) 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 packageCount = cargoRows.value.reduce((sum, item) => sum + Number(item.expectedPackageCount || 0), 0) const customerCartonMarks = (form.relabel ? transferRelabelRows.value : cargoRows.value).flatMap(transferCustomerMarks) const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';') const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) - const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: 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' | 'AMAZON_PICKUP' | 'CUSTOMER_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 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: 0, 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' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } if (props.service === 'transfer' && selectedTransferInventory.value) { ;(payload as TransferCreate).inboundCode = selectedTransferInventory.value ;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value @@ -1223,6 +1265,7 @@ async function openDetail(value: Record) { try { const detail = props.service === 'labeling' ? await getRelabel(value['0']) : props.service === 'disposal' ? await getDiscard(value['0']) : await getTransfer(value['0']) const discardSummary = props.service === 'disposal' ? (detail as { summary?: ServiceDetail }).summary : undefined + showTransferCartons.value = false selectedDetail.value = { ...value, ...(detail as ServiceDetail), ...(discardSummary || {}) } selectedRelabelDetail.value = props.service === 'labeling' ? detail as RelabelDetail : null mode.value = 'detail' @@ -1307,12 +1350,7 @@ async function loadMeasurementPhotos() { 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 : '确认失败') } + openCompletionConfirmation(relabelNo, 'labeling') } async function cancelSelectedDiscard() { const no = String(selectedDetail.value?.discardNo || selectedDetail.value?.['0'] || '') @@ -1386,13 +1424,7 @@ async function selectTransferWarehouse() { } async function confirmSelectedRelabel() { const relabelNo = selectedRelabelDetail.value?.application.relabelNo - if (!relabelNo) return - try { - await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续出库流程吗?', '换标完成确认', { confirmButtonText: '确认并继续', cancelButtonText: '取消' }) - await confirmRelabelCompletion(relabelNo) - ElMessage.success('已确认换标完成,订单已进入后续出库流程') - await openDetail({ '0': relabelNo }) - } catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '确认失败') } + if (relabelNo) openCompletionConfirmation(relabelNo, 'labeling') } async function resubmitRejectedRelabel() { const application = selectedRelabelDetail.value?.application @@ -1524,12 +1556,12 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { return } if (cartonDetailRows.value.some(item => item.cartonMark === source.cartonMark)) return - cargoRows.value.push({ id: ++cargoSequence, included: true, sku: source.skuItems?.map(item => item.skuCode).join(',') || '', name: source.productNameZh || '', shipmentNo: '', cartonMark: source.cartonMark || '', fbaCartonMark: '', remark: '', sourceInventoryBalanceId: undefined, sourceInventoryPositionId: undefined, sourceInboundOrderId: undefined, returnCartonId: String(source.returnCartonId), quantity: 1, fnsku: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', destinationWarehouseId: '' }) + cargoRows.value.push({ id: ++cargoSequence, included: true, sku: source.skuItems?.map(item => item.skuCode).join(',') || '', name: source.productNameZh || '', shipmentNo: '', cartonMark: source.cartonMark || '', fbaCartonMark: '', remark: '', sourceInventoryBalanceId: undefined, sourceInventoryPositionId: undefined, sourceInboundOrderId: undefined, returnCartonId: String(source.returnCartonId), quantity: 1, expectedPackageCount: 1, fnsku: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', destinationWarehouseId: '' }) }
@@ -1708,24 +1740,57 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}{{ config.title }}详情
-
修改后重新提交确认换标完成取消申请编辑确认测量数据{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}
+
修改后重新提交确认换标完成废弃确认取消申请编辑确认测量数据{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}
-

订单概览

操作仓{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}
客户参考号{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}
SKU 数(种){{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}
总箱数{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}
贴标截止时间{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}
仓库联系人{{ selectedRelabelDetail?.application.contactName || '—' }}
联系电话{{ selectedRelabelDetail?.application.contactPhone || '—' }}
创建时间{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}
状态{{ statusLabel(selectedDetail?.status || '') }}
目的去向{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}
FBA 目的仓{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}
FBA 货件号{{ selectedDetail?.fbaShipmentNo || '—' }}
私人仓地址{{ selectedDetail?.privateDestinationAddress || '—' }}
邮编{{ selectedDetail?.privateDestinationPostalCode || '—' }}
私人仓联系人{{ selectedDetail?.privateDestinationContactName || '—' }}
联系电话{{ selectedDetail?.privateDestinationContactPhone || '—' }}
海外仓{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}
箱数{{ selectedDetail?.packageCount ?? '—' }}
入仓方式{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}
期望发货时间{{ formatDateTime(selectedDetail?.estimatedShipAt) }}
实际发货时间{{ formatDateTime(selectedDetail?.actualShipAt) }}
是否换标{{ selectedDetail?.needRelabel ? '是' : '否' }}
换标状态{{ transferRelabelStatus(selectedDetail || {}).label }}
换标单号{{ selectedDetail?.relabelNo || '—' }}
创建时间{{ formatDateTime(selectedDetail?.createdAt) }}
转运单号{{ selectedDetail?.transferNo || '—' }}
{{ column }}{{ selectedDetail?.[String(index)] || '—' }}
+

订单概览

操作仓{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}
客户参考号{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}
SKU 数(种){{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}
总箱数{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}
贴标截止时间{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}
仓库联系人{{ selectedRelabelDetail?.application.contactName || '—' }}
联系电话{{ selectedRelabelDetail?.application.contactPhone || '—' }}
创建时间{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}
状态{{ statusLabel(selectedDetail?.status || '') }}
目的去向{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}
FBA 目的仓{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}
FBA 货件号{{ selectedDetail?.fbaShipmentNo || '—' }}
私人仓地址{{ selectedDetail?.privateDestinationAddress || '—' }}
邮编{{ selectedDetail?.privateDestinationPostalCode || '—' }}
私人仓联系人{{ selectedDetail?.privateDestinationContactName || '—' }}
联系电话{{ selectedDetail?.privateDestinationContactPhone || '—' }}
海外仓{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}
预报 SKU 种数{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}
预计箱数{{ selectedDetail?.packageCount ?? '—' }}
实际签收箱数{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').length ?? '—' }}
入仓方式{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}
期望发货时间{{ formatDateTime(selectedDetail?.estimatedShipAt) }}
实际发货时间{{ formatDateTime(selectedDetail?.actualShipAt) }}
是否换标{{ selectedDetail?.needRelabel ? '是' : '否' }}
换标状态{{ transferRelabelStatus(selectedDetail || {}).label }}
换标单号{{ selectedDetail?.relabelNo || '—' }}
创建时间{{ formatDateTime(selectedDetail?.createdAt) }}
{{ column }}{{ selectedDetail?.[String(index)] || '—' }}

本次废弃箱子

入仓码SKU品名申请数量所选箱唛
{{ item.inboundCode || '—' }}{{ item.skuCode || '—' }}{{ item.productName || '—' }}{{ item.discardPackages ?? '—' }}{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}

废弃现场照片

照片加载中…
{{ formatDateTime(photo.submittedAt) }}
暂无已提交的废弃现场照片
-
-

PDA 打托实测数据

PDA 作业完成即显示入库完成,客户可确认仓库实测数据
-
托盘号库位箱数长 × 宽 × 高(cm)毛重(kg)体积(m³)状态完成时间
{{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }}{{ pallet.warehouseLocationCode || '—' }}{{ pallet.cartonCount ?? '—' }}{{ [pallet.lengthCm, pallet.widthCm, pallet.heightCm].every((value) => value != null) ? `${pallet.lengthCm} × ${pallet.widthCm} × ${pallet.heightCm}` : '—' }}{{ pallet.grossWeightKg ?? '—' }}{{ pallet.volumeCbm ?? '—' }}{{ statusLabel(pallet.status || '') }}{{ formatDateTime(pallet.completedAt) }}
-
暂无 PDA 打托测量数据
+
+

预报与实际签收

+
商品 SKU中文品名预报数量(件)预计箱数实际签收件数实际签收箱数
{{ item.skuCode || '—' }}{{ item.productNameZh || '—' }}{{ item.quantity ?? '—' }}{{ item.packageCount ?? '—' }}{{ receivedItemQuantity(item.id) }}{{ receivedItemCartons(item.id).length }}
暂无货物明细
+
+

PDA 打托实测数据

{{ selectedDetail?.pallets?.length || 0 }} 托展开托盘收起托盘
+
+
+
+ 托盘号 {{ pallet.palletNo || `第${pallet.sequenceNo || '—'}托` }} +
+
箱数
{{ palletCartons(pallet).length }} 箱
件数
{{ palletQuantity(pallet) }} 件
SKU 种数
{{ palletSkuRows(pallet).length }} 种
毛重(kg)
{{ pallet.grossWeightKg ?? '—' }}
体积(m³)
{{ pallet.volumeCbm ?? '—' }}
+
+
+
SKU 汇总
{{ item.sku }}{{ item.quantity }} 件
暂无已清点 SKU
+
库位:{{ pallet.warehouseLocationCode || '—' }}完成时间:{{ formatDateTime(pallet.completedAt) }}
+
+
+
暂无 PDA 打托测量数据
+
测量打托现场照片
照片 {{ index + 1 }} 加载中…
仓库尚未上传测量打托照片
-
-

货物明细

打印箱唛
-
商品 SKU中文品名预计箱数客户箱唛号
{{ item.skuCode || '—' }}{{ item.productNameZh || '—' }}{{ item.packageCount ?? '—' }}{{ item.shippingMark || '—' }}
暂无货物明细
+
+

签收箱号明细({{ countedTransferCartons().length }} 箱)

打印箱唛查看箱号明细
+
暂无实际签收箱,等待 PDA 清点。
+ +
+ + + +
所属托盘:{{ selectedDetail?.pallets?.find((pallet) => carton.transferPalletId != null && String(pallet.id) === String(carton.transferPalletId))?.palletNo || '散箱' }} · 清点时间:{{ formatDateTime(carton.countedAt) }}
+
长 {{ carton.length ?? '—' }} cm · 宽 {{ carton.width ?? '—' }} cm · 高 {{ carton.height ?? '—' }} cm · 重量 {{ carton.weight ?? '—' }} kg · 体积 {{ [carton.length, carton.width, carton.height].every(value => value != null) ? (Number(carton.length) * Number(carton.width) * Number(carton.height) / 1000000).toFixed(6) : '—' }} m³
+ + + + + +

差异说明:{{ carton.discrepancyReason }}

+
+
+
+ +

换标箱级去向

@@ -1758,6 +1823,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
+ diff --git a/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs b/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs index 7a15e48..9772d92 100644 --- a/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs +++ b/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs @@ -5,6 +5,26 @@ import ts from 'typescript' const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8') const source = view.slice(view.indexOf('function returnSourceNos'), view.indexOf('const relabelInboundOptions')) const compiled = ts.transpileModule(source + '\nexports.sourceNos = returnSourceNos; exports.progress = returnRelabelProgress', { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText +test('carton validation ignores unselected stock after SKU relabel and consolidation', () => { + const start = view.indexOf('async function submit(') + const validation = view.slice(view.indexOf(' const invalidCargo =', start), view.indexOf(" if ((props.service === 'relabel'", start)) + const compiledValidation = ts.transpileModule(validation, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText + const validate = new Function('props', 'labelingMode', 'relabelRows', 'cartonDetailRows', 'cargoRows', 'skuRelabelRows', compiledValidation + '; return invalidCargo') + const stockRows = [ + { included: false, name: '', sku: 'OLD-SKU', quantity: 1 }, + { included: false, name: '', sku: 'OLD-SKU,NEW-SKU', quantity: 1 }, + ] + const selected = { included: true, name: 'Product', sku: 'OLD-SKU,NEW-SKU', quantity: 1 } + function invalid(details) { + return validate({ service: 'labeling' }, { value: 'carton' }, details, + { value: details }, { value: [...stockRows, ...details] }, { value: [] }) + } + assert.equal(invalid([selected]), false) + assert.equal(invalid([{ ...selected, name: '' }]), true) + assert.equal(invalid([]), true) + assert.equal(invalid([{ ...selected, sku: '' }]), false, 'SKU is optional for carton labels') +}) + const exports = {} new Function('exports', compiled)(exports) test('合箱来源按单号集合精确匹配并去重,不能用子串匹配', () => { diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index 6ba891f..ed9399d 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -44,7 +44,8 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否 test('换标详情待客户确认时提供确认入口并刷新详情', () => { assert.match(view, /application\.status === 'CUSTOMER_CONFIRM_PENDING'[\s\S]*@click="confirmSelectedRelabel">确认换标完成/) - assert.match(view, /async function confirmSelectedRelabel\(\)[\s\S]*await confirmRelabelCompletion\(relabelNo\)[\s\S]*await openDetail\(\{ '0': relabelNo \}\)/) + assert.match(view, /async function confirmSelectedRelabel\(\)[\s\S]*openCompletionConfirmation\(relabelNo, 'labeling'\)/) + assert.match(view, /async function refreshAfterCompletion\(no: string\)[\s\S]*await openDetail\(\{ '0': no \}\)/) }) test('仓储转运换标绑定明细自动同步只读的旧 SKU', () => { @@ -104,7 +105,7 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换 test('仓储转运货物明细可按 SKU 和箱数打印客户预贴箱唛', () => { assert.match(view, /printTransferDraftCartonMarks/) assert.match(view, /请先填写商品 SKU 和对应的整数箱数/) - assert.match(view, /Array\.from\(\{ length: Number\(row\.quantity\) \}/) + assert.match(view, /Array\.from\(\{ length: Number\(row\.expectedPackageCount\) \}/) assert.match(view, /row\.customerCartonMarks = marks\.join\('\\n'\)/) assert.match(view, /仓储转运客户箱唛/) assert.match(view, /@click="printTransferDraftCartonMarks"/) @@ -210,7 +211,7 @@ test('仓储转运列表展示换标单号、换标状态并为两类状态着 test('仓储转运详情订单概览使用详情接口命名字段回填', () => { assert.match(view, /v-else-if="props\.service === 'transfer'" class="warehouse-detail-grid warehouse-transfer-overview-grid"/) assert.doesNotMatch(view, /入库箱数/) - assert.match(view, /'转运单号', '箱数', '海外仓'/) + assert.match(view, /'转运单号', '预计箱数', '海外仓'/) for (const field of ['transferNo', 'packageCount', 'operatingWarehouseId', 'deliveryMethod', 'needRelabel', 'relabelNo', 'status', 'createdAt']) { assert.match(view, new RegExp(`selectedDetail\\?\\.${field}`)) } @@ -258,7 +259,8 @@ test('贴标申请详情使用紧凑订单概览并中文展示换标状态', () test('仓储转运详情按接口具名字段展示订单概览', () => { const overview = view.split('

订单概览

')[1]?.split('warehouse-transfer-cargo-card')[0] || '' assert.match(overview, /v-if="props\.service === 'transfer'"/) - assert.match(overview, /selectedDetail\?\.transferNo/) + assert.doesNotMatch(overview, /转运单号<\/span>/) + assert.match(view.split('
')[1]?.split('
')[0] || '', /selectedDetail\?\.transferNo/) assert.match(overview, /selectedDetail\?\.packageCount \?\? '—'/) assert.match(overview, /warehouseDisplay\(selectedDetail\?\.operatingWarehouseId\)/) assert.match(overview, /selectedDetail\?\.destinationWarehouseName \|\| warehouseDisplay\(selectedDetail\?\.destinationWarehouseCode \|\| selectedDetail\?\.fbaWarehouseCode\)/) @@ -272,26 +274,26 @@ test('PDA 打托完成后客户可从列表或详情确认并查看实测数据' assert.match(view, /confirmTransferFromList\(item\['0'\]\)/) assert.match(view, /selectedDetail\?\.status === 'WAIT_CUSTOMER_CONFIRMATION'/) assert.match(view, /PDA 打托实测数据/) - for (const label of ['托盘号', '库位', '箱数', '长 × 宽 × 高(cm)', '毛重(kg)', '体积(m³)', '完成时间']) assert.equal(view.includes(label), true) + for (const label of ['托盘号', '库位', '箱数', 'SKU 汇总', '毛重(kg)', '体积(m³)', '完成时间']) assert.equal(view.includes(label), true) assert.match(view, /pallet\.warehouseLocationCode/) - assert.match(view, /pallet\.cartonCount/) + assert.match(view, /palletCartons\(pallet\)\.length/) assert.match(view, /pallet\.grossWeightKg/) assert.match(view, /pallet\.volumeCbm/) assert.doesNotMatch(view, /pallet\.items/) }) -test('仓储转运详情按订单概览、PDA 实测数据、货物明细排序并放大测量照片', () => { +test('仓储转运详情按订单概览、货物预报、PDA 实测数据排序并放大测量照片', () => { const detailStart = view.indexOf('
') const detailSource = view.slice(detailStart) const overviewIndex = detailSource.indexOf('

订单概览

') const measurementIndex = detailSource.indexOf('

PDA 打托实测数据

') - const cargoIndex = detailSource.indexOf('

货物明细

') + const cargoIndex = detailSource.indexOf('

预报与实际签收

') assert.ok(overviewIndex >= 0 && overviewIndex < measurementIndex) - assert.ok(measurementIndex < cargoIndex) + assert.ok(cargoIndex < measurementIndex) assert.match(view, /class="warehouse-photo-grid warehouse-measurement-photo-grid"/) assert.match(view, /class="warehouse-photo-item warehouse-measurement-photo-item"/) assert.match(view, /warehouse-measurement-photo-grid\{flex:1;grid-template-columns:repeat\(auto-fill,minmax\(240px,1fr\)\)\}/) - assert.match(view, /warehouse-measurement-photo-item\{width:100%;height:180px\}/) + assert.match(view, /warehouse-measurement-photo-item\{width:100%;height:120px\}/) }) test('仓储转运详情自动加载 PDA 测量照片并释放 Blob URL', () => { @@ -367,3 +369,45 @@ test('仓储转运基础货物不强制 SKU 和中文品名,只换箱唛不强 assert.match(api, /skuCode\?: string; productNameZh\?: string/) assert.match(api, /originalSku\?: string/) }) + + test('SKU 件数和预计箱数独立提交,签收展示实际清点箱数', () => { + assert.match(view, /quantity: item.quantity, packageCount: Number\(item.expectedPackageCount\)/) + assert.match(view, /sum \+ Number\(item.expectedPackageCount \|\| 0\)/) + assert.match(view, /v-model="row.expectedPackageCount"/) + assert.match(view, /Number.isSafeInteger\(row.quantity\)/) + assert.match(view, /Number.isSafeInteger\(row.expectedPackageCount\)/) + assert.match(view, /carton.status === 'COUNTED'/) + assert.match(view, /实际签收箱数/) +}) + +test('转运客户详情保留预报并显示实际签收箱内明细和托盘汇总', () => { + for (const label of ['预报数量(件)', '预报 SKU 种数', '签收箱号明细', '查看箱号明细', 'SKU 汇总', '毛重(kg)', '体积(m³)']) assert.ok(view.includes(label), label) + assert.match(view, /carton.status === 'COUNTED' && pallet.id != null/) + assert.match(view, /String\(carton.transferPalletId\) === String\(pallet.id\)/) + assert.match(view, /carton.transferItemId != null && String\(item.id\) === String\(carton.transferItemId\)/) + assert.match(view, /@click="showTransferCartons = true"/) + assert.match(view, /')), view.indexOf('v-if="props.service === \'transfer\' && selectedDetail?.needRelabel"')) + assert.doesNotMatch(receiptSection.split('\{\{ \[pallet.lengthCm/) +}) + + +test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题', () => { + const start = view.indexOf('class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card"', view.indexOf('
')) + const section = view.slice(start, view.indexOf('warehouse-transfer-received-card', start)) + assert.ok(section.includes('预报与实际签收')) + assert.ok(section.includes('PDA 打托实测数据')) + assert.ok(section.includes('receivedItemQuantity(item.id)')) + assert.ok(section.includes('receivedItemCartons(item.id).length')) + assert.ok(section.includes('class="warehouse-transfer-pallet-card"')) + assert.ok(section.includes('class="warehouse-transfer-pallet-sku"')) + assert.doesNotMatch(section, /打印箱唛/) + const receipt = view.slice(view.indexOf('warehouse-transfer-received-card', start)).split('