From ecd0317b58d9ce6af6760d91cdde7feb3d566422 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 14 Sep 2026 16:51:43 +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 | 11 +- .../views/WarehouseServiceView.vue | 123 +++++++++++++----- .../views/warehouseDisposalForm.test.mjs | 12 ++ .../views/warehouseTransferForm.test.mjs | 5 +- 4 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 2dd6047..a0b0d16 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -5,7 +5,7 @@ export type RelabelDeliveryMethod = 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPR export interface PageResult { records: T[]; page: number; size: number; total: number; pages: number } export interface LastlegOrderSummary { id: string; orderNo: string; inboundCode: string; orderType: string; packageCount: number; overseasWarehouseId?: string } export interface TransferSummary { id: string; transferNo: string; inboundCode: string; lastlegOrderId: string; lastlegOrderNo: string; operatingWarehouseId: string; fbaWarehouseCode?: string; packageCount: number; deliveryMethod: string; needRelabel: boolean; relabelNo?: string; status: string; estimatedArrivalAt: string; estimatedShipAt?: string; actualShipAt?: string; createdAt: string } -export interface RelabelSummary { id: string; relabelNo: string; lastlegOrderId: string; lastlegOrderNo: string; customerOrderNo: string; overseasWarehouseId: string; status: string; itemCount: number; totalPackageCount: number; notifiedPackageCount: number; skuCodes: string; createdAt: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean; shippingLabelFileId?: string; shippingLabelFileName?: string; shippingLabelFileUrl?: string; fulfillmentStatus?: string; outboundOrderNo?: string; customerConfirmedAt?: string } +export interface RelabelSummary { id: string; relabelNo: string; lastlegOrderId: string; lastlegOrderNo: string; customerOrderNo: string; overseasWarehouseId: string; status: string; itemCount: number; totalPackageCount: number; notifiedPackageCount: number; skuCodes: string; createdAt: string; totalFileId?: string; totalFileName?: string; totalFileUrl?: string; cartonTotalFileId?: string; cartonTotalFileName?: string; cartonTotalFileUrl?: string; skuTotalFileId?: string; skuTotalFileName?: string; skuTotalFileUrl?: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean; shippingLabelFileId?: string; shippingLabelFileName?: string; shippingLabelFileUrl?: string; fulfillmentStatus?: string; outboundOrderNo?: string; customerConfirmedAt?: string } export interface RelabelItemDetail { id: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string; remark?: string @@ -42,7 +42,7 @@ export interface TransferCreate { 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 { - customerOrderNo: string; operationWarehouseId: string; labelDeadline: string; contactName: string; contactPhone: string; chargeAmount: number + customerOrderNo: string; operationWarehouseId: string; labelDeadline: string; contactName: string; contactPhone: string; chargeAmount?: number remark?: string; idempotencyKey: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean items: Array<{ sourceInventoryBalanceId: string; sourceInventoryPositionId: string; sourceInboundOrderId: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number; shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string cartonMark: string; fbaCartonMark?: string; destinationFbaWarehouseCode?: string; operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'; skuItems?: Array<{ skuCode: string; newSkuCode: string; quantity: number; remark?: string }>; remark?: string }> @@ -87,6 +87,10 @@ export function uploadRelabelShippingLabel(relabelNo: string, file: File) { const data = new FormData(); data.append('file', file) return overseasApiRequest({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/shipping-label`, data, headers: { 'Content-Type': 'multipart/form-data' } }) } +export function uploadRelabelFile(relabelNo: string, scope: 'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM', file: File, itemId?: string) { + const data = new FormData(); data.append('file', file) + return overseasApiRequest({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, params: { scope, itemId }, data, headers: { 'Content-Type': 'multipart/form-data' } }) +} export function confirmRelabelCompletion(relabelNo: string, remark?: string) { return overseasApiRequest({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`, data: { idempotencyKey: crypto.randomUUID(), remark } }) } @@ -105,3 +109,6 @@ export function listDiscardCandidates(inboundCode?: string) { export function createDiscard(payload: { items: Array<{ orderId: string; skuCode: string; discardPackages: number }>; idempotencyKey: string; remark?: string }) { return overseasApiRequest>({ method: 'POST', url: '/merchant/v1/last-leg/cargo-discards', data: payload }) } +export function cancelDiscard(discardNo: string) { + return overseasApiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/cancel` }) +} diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 543553a..00eca07 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -7,7 +7,8 @@ import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, getTransferCartonLabel, confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, uploadRelabelShippingLabel, - listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabels, listTransfers, + uploadRelabelFile, + listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabels, listTransfers, cancelDiscard, type RelabelInventoryCandidate, type RelabelCreate, type RelabelDetail, @@ -33,7 +34,7 @@ interface TransferCarton { cartonMark: string; customerCartonMark?: string; cart const props = defineProps<{ service: ServiceType }>() 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 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] }, } @@ -41,11 +42,11 @@ const config = computed(() => configs[props.service]) const statusOptions = computed(() => props.service === 'labeling' ? [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '待客户确认', value: 'CUSTOMER_CONFIRM_PENDING' }, { label: '待出库', value: 'READY_FOR_OUTBOUND' }, { label: '已完成', value: 'COMPLETED' }, { label: '已拒绝', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] : props.service === 'disposal' - ? [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已通过', value: 'APPROVED' }, { label: '已拒绝', value: 'REJECTED' }] + ? [{ 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: '已通过', 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('list') const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) -const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 }) +const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 }) const transferHasFbaDestination = ref(false) const selectedOperationWarehouse = ref(null) const operationWarehouseDetailLoading = ref(false) @@ -103,10 +104,17 @@ const cartonTemplateInput = ref(null) const skuTemplateInput = ref(null) const mixedTemplateInput = ref(null) const transferCartonMarkInput = ref(null) +const relabelFileInput = ref(null) +const relabelFileScope = ref<'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM'>('CARTON_TOTAL') +const relabelFileItemId = ref() +const cartonTotalFile = ref(null) +const skuTotalFile = ref(null) +const cartonTotalCreateInput = ref(null) +const skuTotalCreateInput = ref(null) let cargoSequence = 0 let skuRelabelSequence = 0 -watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void load() }) +watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void loadPage() }) watch(() => form.relabel, (value) => { if (props.service === 'transfer' && value) { labelingMode.value = 'carton_and_sku' @@ -124,10 +132,14 @@ function refreshWarehouseMarkLabels() { while ((node = walker.nextNode())) nodes.push(node as Text) for (const text of nodes) text.nodeValue = text.nodeValue?.replaceAll('旧箱唛', '仓库唛头').replaceAll('旧唛头', '仓库唛头') || text.nodeValue } -onMounted(() => { void load(); if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer' || props.service === 'disposal') void loadWarehouses(); void nextTick(refreshWarehouseMarkLabels) }) +async function loadPage() { + if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer' || props.service === 'disposal') await loadWarehouses() + await load() +} +onMounted(() => { void loadPage(); void nextTick(refreshWarehouseMarkLabels) }) function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) } function resetForm() { - Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 }) + Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 }) cargoRows.value = [] labelingMode.value = 'carton' skuRelabelRows.value = [] @@ -144,9 +156,18 @@ async function openCreate() { if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') void loadWarehouses() if (props.service === 'labeling') return if (props.service !== 'disposal') { addCargoRow(); return } +} +async function selectDiscardWarehouse() { + selectedDiscardOrderId.value = '' + form.inboundNo = '' + discardCandidates.value = [] + cargoRows.value = [] + if (!form.warehouse) return discardCandidatesLoading.value = true - try { discardCandidates.value = await listDiscardCandidates() } - catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') } + 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: '' } } @@ -179,6 +200,11 @@ function applyExistingCartonBinding(row: SkuRelabelRow) { 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 } +function selectSkuTotalFile(event: Event) { skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null } +function chooseCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') { + ;(scope === 'CARTON_TOTAL' ? cartonTotalCreateInput : skuTotalCreateInput).value?.click() +} function buildSkuRelabelItems(): RelabelCreate['items'] { const groups = new Map() for (const row of skuRelabelRows.value) { @@ -443,7 +469,7 @@ function statusLabel(value: string) { PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '待出库', COMPLETED: '已完成', CANCELLED: '已取消', - PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配', + PENDING_REVIEW: '待审核', APPROVED: '待 PDA 废弃', PDA_COMPLETED: '待仓库确认完成', REJECTED: '已拒绝', PENDING_ASSIGNMENT: '待分配', ALLOCATED: '已分配', OUTBOUNDED: '已出库', PARTIALLY_RECEIVED: '部分收货', RECEIVED: '已收货', } @@ -476,7 +502,7 @@ async function load() { } else { const result = await listTransfers({ page: 1, size: 20, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, ...(props.service === 'relabel' ? { needRelabel: true } : {}), createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) }) submittedRows.value = result.records.map((item) => props.service === 'transfer' - ? row([item.transferNo, item.packageCount, '—', item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)]) + ? row([item.transferNo, item.packageCount, warehouseDisplay(item.operatingWarehouseId), item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)]) : row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)])) } } catch (error) { ElMessage.error(error instanceof Error ? error.message : '列表加载失败') } @@ -494,11 +520,11 @@ async function submit() { || (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.destinationWarehouseId))) : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || (props.service !== 'labeling' && item.quantity < 1)))) - if ((props.service === 'relabel' && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) { + if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) { ElMessage.warning('请填写必填信息和完整的货物明细'); return } - if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) { - ElMessage.warning('请填写贴标截止时间、联系人和贴标费用'); return + if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone)) { + ElMessage.warning('请填写贴标截止时间和海外仓联系人信息'); return } if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) { ElMessage.warning('请选择换标完成后的派送方式'); return @@ -520,14 +546,14 @@ async function submit() { ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return } if (props.service === 'transfer' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || !form.estimatedShipAt || form.volumeCbm < 0 || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.contact || !form.phone)))) { - const missing = [!form.warehouse && '中转仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.contact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.phone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间', !form.estimatedShipAt && '预计发货时间', form.volumeCbm < 0 && '预估总体积'].filter(Boolean).join('、') + const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.contact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.phone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间', !form.estimatedShipAt && '预计发货时间', form.volumeCbm < 0 && '预估总体积'].filter(Boolean).join('、') ElMessage.warning(`请填写:${missing || '必填信息'}`); return } if (props.service === 'transfer' && transferHasFbaDestination.value && (!form.destination || !form.fbaShipmentNo)) { ElMessage.warning('已选择有 FBA 去向,请填写 FBA 目的仓和货件编号'); return } if (props.service === 'transfer' && form.relabel) { - if (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount < 0.01 || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、联系人、费用和换标后派送方式'); return } + if (!form.labelDeadline || !form.contact || !form.phone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、联系人和换标后派送方式'); return } const relabelRows = transferRelabelRows.value const allMarks = relabelRows.flatMap(transferCustomerMarks) if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return } @@ -544,9 +570,11 @@ async function submit() { const items = labelingMode.value === 'carton' ? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, 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: item.destinationWarehouseId, 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, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items, + 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 }) + 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) } else if (props.service === 'disposal') { const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => { @@ -561,7 +589,7 @@ async function submit() { 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' - await createTransfer({ operatingWarehouseId: primaryWarehouse, fbaWarehouseCode: form.destination || undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: form.fbaShipmentNo || 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, relabelContactName: (form.relabel || form.deliveryMethod === 'WAREHOUSE_PICKUP') ? form.contact : undefined, relabelContactPhone: (form.relabel || form.deliveryMethod === 'WAREHOUSE_PICKUP') ? form.phone : undefined, relabelChargeAmount: form.relabel ? form.chargeAmount : 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: item.destinationWarehouseId || undefined, remark: item.remark || undefined })) : undefined }) + await createTransfer({ operatingWarehouseId: primaryWarehouse, fbaWarehouseCode: form.destination || undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: form.fbaShipmentNo || 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, relabelContactName: (form.relabel || form.deliveryMethod === 'WAREHOUSE_PICKUP') ? form.contact : undefined, relabelContactPhone: (form.relabel || form.deliveryMethod === 'WAREHOUSE_PICKUP') ? form.phone : 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: item.destinationWarehouseId || undefined, remark: item.remark || undefined })) : undefined }) } ElMessage.success(`${config.value.title}提交成功`); mode.value = 'list'; await load() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') } @@ -570,7 +598,8 @@ async function submit() { 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']) - selectedDetail.value = { ...value, ...(detail as ServiceDetail) } + 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' } catch (error) { ElMessage.error(error instanceof Error ? error.message : '详情加载失败') } @@ -613,6 +642,24 @@ async function previewCompletionPhoto(fileId: string) { window.setTimeout(() => URL.revokeObjectURL(url), 60_000) } catch (error) { 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 = '' @@ -620,10 +667,7 @@ async function loadWarehouses() { try { const [fbaResult, operationResult] = await Promise.allSettled([ loadAllWarehouseRecords('FBA', getMerchantWarehouses), - // The warehouse master uses OVERSEAS_PARTNER for managed overseas transit warehouses - // (including Los Angeles and New York). Query the active catalogue without a type - // restriction, then accept both supported overseas warehouse types. - loadAllWarehouseRecords('OVERSEAS', (query) => getMerchantWarehouses({ page: query.page, size: query.size })), + loadAllWarehouseRecords('OVERSEAS', getMerchantWarehouses), ]) fbaWarehouses.value = fbaResult.status === 'fulfilled' ? fbaResult.value.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'FBA') : [] operationWarehouses.value = operationResult.status === 'fulfilled' @@ -696,6 +740,8 @@ function selectRelabelInventory(inboundOrderId: string) { skuRelabelRows.value = [] importedRelabelBindings.value = [] shippingLabelFile.value = null + cartonTotalFile.value = null + skuTotalFile.value = null if (labelingMode.value !== 'carton') addSkuRelabelRow() } @@ -729,19 +775,19 @@ function selectRelabelInventory(inboundOrderId: string) {

{{ props.service === 'transfer' ? '入库与转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}

- + + - + - -
*货物如何送到中转仓仓库上门提货客户送货到仓仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示中转仓收货信息。
+
*货物如何送到海外仓仓库上门提货客户送货到仓仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示海外仓收货信息。
-
仓库收货信息{{ operationWarehouseAddress || '该中转仓暂未维护收货地址' }}
+
海外仓收货信息{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}
收货联系人{{ form.contact || '未维护' }}
联系电话{{ form.phone || '未维护' }}
@@ -752,7 +798,6 @@ function selectRelabelInventory(inboundOrderId: string) { -
是否打印面单需要仓库打印面单
@@ -760,7 +805,7 @@ function selectRelabelInventory(inboundOrderId: string) { -
*是否已有 FBA 去向暂未确定已有 FBA 去向暂未确定也可以提交,货物先正常入中转仓,后续再补充转运指令。
+
*是否已有 FBA 去向暂未确定已有 FBA 去向暂未确定也可以提交,货物先正常入海外仓,后续再补充转运指令。
@@ -775,6 +820,8 @@ function selectRelabelInventory(inboundOrderId: string) {

货物明细

+ 上传箱唛总文件 + 上传 SKU 总文件 下载 SKU 关联模板 上传 SKU 关联 下载箱号关联模板 @@ -784,8 +831,11 @@ function selectRelabelInventory(inboundOrderId: string) { + +
+
箱唛总文件:{{ cartonTotalFile?.name || '未选择' }}SKU 总文件:{{ skuTotalFile?.name || '未选择' }}
换标类型 @@ -814,7 +864,7 @@ function selectRelabelInventory(inboundOrderId: string) {

货物明细

换标明细与贴标申请保持一致;客户只填写自己的箱唛,我方箱唛由系统生成。

下载 SKU 关联模板上传 SKU 关联下载箱号关联模板上传箱号关联下载 SKU 混箱模板上传 SKU 混箱
-
+
换标类型只换箱唛只换 SKU 标签SKU 和箱唛都换

{{ labelingModeHint }}

待导入绑定关系

可只勾选本次需要换标的部分,也可以全选后一次导入。

全选导入到明细
选择关系类型仓库唛头旧 SKU新 SKU数量新 FBA 箱唛FBA 目的仓
{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}{{ item.cartonMark }}{{ item.originalSku || '—' }}{{ item.newSku || '—' }}{{ item.quantity }}{{ item.fbaCartonMark || '—' }}{{ warehouseDisplay(item.destinationWarehouseId) }}
绑定明细

上传后在下方回显,也可以手动添加;客户箱唛作为仓库唛头来源,系统我方箱唛不需要手填。

添加 SKU 明细清空
@@ -828,7 +878,7 @@ function selectRelabelInventory(inboundOrderId: string) {