From 28d97d2221b1f5adbc8653b0968d1c53a2d71cb5 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 9 Sep 2026 13:35:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overseas-warehouse/api/outbounds.ts | 11 ++++- .../overseas-warehouse/api/relabelOrders.ts | 21 ++++++++++ .../overseas-warehouse/api/returnInventory.ts | 2 +- .../pages/OutboundsView.vue | 18 +++++---- .../pages/RelabelOrderDetailView.vue | 40 +++++++++++++++++-- .../pages/ReturnInventoryManagementView.vue | 4 +- 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/modules/overseas-warehouse/api/outbounds.ts b/src/modules/overseas-warehouse/api/outbounds.ts index e5eec373..3d9c5206 100644 --- a/src/modules/overseas-warehouse/api/outbounds.ts +++ b/src/modules/overseas-warehouse/api/outbounds.ts @@ -3,7 +3,7 @@ import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import type { DeliveryOrderRow } from './deliveryOrders' -export interface OutboundRow { id: string; outboundNo: string; dispatchNo?: string | null; overseasWarehouseId?: string | null; lxCodes?: string | null; status?: string | null; palletCount?: number | null; packageCount?: number | null; createdAt?: string | null } +export interface OutboundRow { id: string; outboundNo: string; dispatchNo?: string | null; overseasWarehouseId?: string | null; lxCodes?: string | null; status?: string | null; palletCount?: number | null; packageCount?: number | null; createdAt?: string | null; outboundType?: string | null } export interface OutboundPalletRow { id?: string | null palletNo?: string | null @@ -18,6 +18,7 @@ export interface OutboundPalletRow { } export interface OutboundDetail { outbound: OutboundRow; pallets?: OutboundPalletRow[]; delivery?: DeliveryOrderRow | null } export interface OutboundQuery { page: number; size: number; documentNo?: string; lxCode?: string; warehouseId?: string; status?: string } +export interface ReturnOutboundCandidate { returnNo?: string | null; overseasWarehouseId?: string | null; cartonMark?: string | null; fbaCartonMark?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; totalQuantity?: number | null; status?: string | null } export async function listOutbounds(query: OutboundQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/outbound-orders', { params: query }) return normalizeOverseasPage(unwrapApiResponse(response)) @@ -30,6 +31,14 @@ export async function createOneClickDropshipOutbound(customerId: string, payload const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } }) return unwrapApiResponse(response) } +export async function listReturnOutboundCandidates(customerId: string, warehouseId: string) { + const response = await overseasHttp.get, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/return/candidates', { params: { customerId, warehouseId } }) + return unwrapApiResponse(response) +} +export async function createReturnOutbound(customerId: string, payload: { recipient: string; overseasWarehouseId: string; returnNo: string; cartonMarks: string[]; remark?: string; idempotencyKey: string }) { + const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/return', payload, { params: { customerId } }) + return unwrapApiResponse(response) +} export async function cancelOutbound(outboundNo: string, reason: string) { const response = await overseasHttp.post, ApiEnvelope>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason }) return unwrapApiResponse(response) diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index 6232a768..d083b922 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -64,6 +64,9 @@ export interface RelabelSummaryDto { totalPackageCount?: number | null notifiedPackageCount?: number | null skuCodes?: string | null + totalFileId?: string | null + totalFileName?: string | null + totalFileUrl?: string | null chargeAmount?: number | string | null chargeCurrency?: string | null walletStatus?: string | null @@ -79,6 +82,7 @@ export interface RelabelItemDto { notifiedPackageCount?: number | null newSkuCode?: string | null labelRequirement?: string | null + relabelFileId?: string | null relabelFileName?: string | null relabelFileUrl?: string | null shipmentNo?: string | null @@ -144,3 +148,20 @@ export async function changeRelabelOrderStatus(relabelNo: string, status: string ) return unwrapApiResponse(response) } + +export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TOTAL' | 'ITEM', itemId?: string) { + const body = new FormData() + body.append('file', file) + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, body, + { params: { scope, itemId }, headers: { 'Content-Type': 'multipart/form-data' } }, + ) + return unwrapApiResponse(response) +} + +export async function downloadRelabelPdf(relabelNo: string, fileId: string, scope: 'TOTAL' | 'ITEM', itemId?: string) { + return overseasHttp.get( + `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files/${encodeURIComponent(fileId)}/download`, + { params: { scope, itemId }, responseType: 'blob' }, + ) +} diff --git a/src/modules/overseas-warehouse/api/returnInventory.ts b/src/modules/overseas-warehouse/api/returnInventory.ts index 6ed7d881..05b1ef14 100644 --- a/src/modules/overseas-warehouse/api/returnInventory.ts +++ b/src/modules/overseas-warehouse/api/returnInventory.ts @@ -3,7 +3,7 @@ import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' export interface ReturnStockRow { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; totalQuantity: number; availableQuantity: number; cartonQuantity: number; updatedAt: string } -export interface ReturnCartonRow { customerId: string; overseasWarehouseId: string; cartonMark: string; sourceReturnNo?: string; sourceBatchNo?: string; status: string; skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; createdAt: string } +export interface ReturnCartonRow { customerId: string; overseasWarehouseId: string; cartonMark: string; sourceReturnNo?: string; sourceBatchNo?: string; status: string; fbaCartonMark?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string; relabelNo?: string; relabelStatus?: string; skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; createdAt: string } export interface ReturnCartonDetail extends ReturnCartonRow { customerId: string; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> } export interface ReturnCartonLabel extends ReturnCartonDetail { printableHtml: string } export interface ReturnInventoryMovement { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; movementType: 'RECEIPT' | 'MERGE' | 'OUTBOUND'; referenceNo: string; totalDelta: number; availableDelta: number; occurredAt: string } diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 542bac4b..244a6685 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -14,27 +14,31 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' -import { cancelOutbound, createOneClickDropshipOutbound, listOutbounds, type OutboundRow } from '../api/outbounds' +import { cancelOutbound, createOneClickDropshipOutbound, createReturnOutbound, listReturnOutboundCandidates, listOutbounds, type OutboundRow, type ReturnOutboundCandidate } from '../api/outbounds' import { listInventoryBalances, listInventoryPositions, type InventoryBalanceDto, type InventoryPositionDto } from '../api/inventory' import { formatOverseasDateTime } from '../lastMilePresentation' import { overseasWarehousePermissions as permission } from '../permissions' const router = useRouter(); const access = useAccessPreviewStore() const rows = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const error = ref(null); const mutationError = ref(null) -const filters = ref({ documentNo: '', lxCode: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref(); const cartonMarks = ref(''); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref>([]); const inventoryLoading = ref(false) +const filters = ref({ documentNo: '', lxCode: '', warehouseId: '', status: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref(); const cartonMarks = ref(''); const selectedReturnMarks = ref([]); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref>([]); const returnCandidates = ref([]); const selectedWarehouseId = ref(''); const selectedReturnNo = ref(''); const outboundType = ref<'ONE_CLICK_DROPSHIP' | 'RETURN_OUTBOUND'>('ONE_CLICK_DROPSHIP'); const inventoryLoading = ref(false) const customerOptions = computed(() => { const customers = new Map() positionOptions.value.forEach(({ balance }) => customers.set(balance.customerId, balance.customerName?.trim() || '客户名称待补充')) return [...customers].map(([value, label]) => ({ value, label })) }) const availablePositionOptions = computed(() => positionOptions.value.filter(({ balance }) => balance.customerId === selectedCustomerId.value)) +const warehouseOptions = computed(() => [...new Map(positionOptions.value.filter(({ balance }) => balance.customerId === selectedCustomerId.value).map(({ balance }) => [balance.overseasWarehouseId, balance.overseasWarehouseName || balance.overseasWarehouseId])).entries()].map(([value, label]) => ({ value, label: label || value }))) +const returnMarksModel = computed({ get: () => selectedReturnMarks.value, set: (value: string[]) => { selectedReturnMarks.value = value } }) +const oneClickMarks = computed(() => typeof cartonMarks.value === 'string' ? cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean) : []) const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'lxCodes', prop: 'lxCodes', label: 'LX代码', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }] const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 220 }, { key: 'lxCode', type: 'input', label: 'LX代码', placeholder: '请输入领星入库单代码', width: 220 }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓', width: 220 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '全部', value: '' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }] }] async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const result = await listOutbounds({ page: page.value, size: pageSize.value, documentNo: String(filters.value.documentNo || '').trim() || undefined, lxCode: String(filters.value.lxCode || '').trim() || undefined, warehouseId: String(filters.value.warehouseId || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }); rows.value = result.records.map((row) => ({ ...row, createdAt: formatOverseasDateTime(row.createdAt) })); total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause, '出库单列表加载失败') } finally { loading.value = false } } -async function openCreate() { createOpen.value = true; selectedCustomerId.value = ''; selectedPositionId.value = ''; quantity.value = undefined; inventoryLoading.value = true; try { const balances = await listInventoryBalances({ page: 1, size: 200, inventoryStatus: 'AVAILABLE' }); const groups = await Promise.all(balances.records.map(async balance => (await listInventoryPositions(balance.id, { inventoryStatus: 'AVAILABLE' })).map(position => ({ balance, position })))); positionOptions.value = groups.flat().filter(item => item.position.availablePackages > 0) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '可用库存加载失败').message) } finally { inventoryLoading.value = false } } -function selectCustomer() { selectedPositionId.value = ''; quantity.value = undefined } -async function submit() { const selected = availablePositionOptions.value.find(item => item.position.id === selectedPositionId.value); const marks = cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean); if (!selectedCustomerId.value || !recipient.value.trim() || !selected || !quantity.value || quantity.value < 1) { ElMessage.warning('请选择客户,填写收件信息并选择托盘和出库数量'); return }; if (quantity.value > selected.position.availablePackages) { ElMessage.warning('出库数量不能超过可用库存'); return }; if (marks.length && marks.length !== quantity.value) { ElMessage.warning('指定箱唛数量必须等于出库数量'); return }; saving.value = true; try { await createOneClickDropshipOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), items: [{ inventoryPositionId: selected.position.id, packages: quantity.value, sku: selected.balance.skuCode || undefined, cartonMarks: marks.length ? marks : undefined }], remark: remark.value.trim() || undefined, idempotencyKey: `web-dropship-${Date.now()}` }); ElMessage.success('出库单已创建并推送PDA拣货任务'); createOpen.value = false; await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '一件代发出库失败').message) } finally { saving.value = false } } +async function openCreate() { createOpen.value = true; outboundType.value = 'ONE_CLICK_DROPSHIP'; selectedCustomerId.value = ''; selectedPositionId.value = ''; selectedWarehouseId.value = ''; selectedReturnNo.value = ''; returnCandidates.value = []; selectedReturnMarks.value = []; quantity.value = undefined; cartonMarks.value = ''; recipient.value = ''; remark.value = ''; inventoryLoading.value = true; try { const balances = await listInventoryBalances({ page: 1, size: 200, inventoryStatus: 'AVAILABLE' }); const groups = await Promise.all(balances.records.map(async balance => (await listInventoryPositions(balance.id, { inventoryStatus: 'AVAILABLE' })).map(position => ({ balance, position })))); positionOptions.value = groups.flat().filter(item => item.position.availablePackages > 0) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '可用库存加载失败').message) } finally { inventoryLoading.value = false } } +function selectCustomer() { selectedPositionId.value = ''; quantity.value = undefined; selectedWarehouseId.value = ''; selectedReturnNo.value = ''; selectedReturnMarks.value = []; returnCandidates.value = [] } +async function loadReturnCandidates() { if (!selectedCustomerId.value || !selectedWarehouseId.value) return; inventoryLoading.value = true; try { returnCandidates.value = await listReturnOutboundCandidates(selectedCustomerId.value, selectedWarehouseId.value) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '退货箱唛加载失败').message) } finally { inventoryLoading.value = false } } +async function submit() { const marks = String(cartonMarks.value).split(/[,\n,]+/).map(value => value.trim()).filter(Boolean); if (!selectedCustomerId.value || !recipient.value.trim()) { ElMessage.warning('请选择客户并填写收件信息'); return }; saving.value = true; try { if (outboundType.value === 'RETURN_OUTBOUND') { if (!selectedWarehouseId.value || !selectedReturnNo.value || !selectedReturnMarks.value.length) { ElMessage.warning('请选择海外仓、退货单和至少一个箱唛'); return }; await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, remark: remark.value.trim() || undefined, idempotencyKey: `web-return-${Date.now()}` }); ElMessage.success('退货出库单已创建并推送PDA箱唛拣货任务') } else { const selected = availablePositionOptions.value.find(item => item.position.id === selectedPositionId.value); if (!selected || !quantity.value || quantity.value < 1) { ElMessage.warning('请选择托盘和出库数量'); return }; if (quantity.value > selected.position.availablePackages) { ElMessage.warning('出库数量不能超过可用库存'); return }; if (marks.length && marks.length !== quantity.value) { ElMessage.warning('指定箱唛数量必须等于出库数量'); return }; await createOneClickDropshipOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), items: [{ inventoryPositionId: selected.position.id, packages: quantity.value, sku: selected.balance.skuCode || undefined, cartonMarks: marks.length ? marks : undefined }], remark: remark.value.trim() || undefined, idempotencyKey: `web-dropship-${Date.now()}` }); ElMessage.success('一件代发出库单已创建并推送PDA拣货任务') } createOpen.value = false; await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '出库单创建失败').message) } finally { saving.value = false } } function openDetail(row: OutboundRow) { void router.push({ name: 'overseas-outbound-detail', params: { outboundNo: row.outboundNo } }) } function canCancel(row: OutboundRow) { return access.can(permission.outboundCancel) && row.status === 'PICKED' } async function cancel(row: OutboundRow) { if (!canCancel(row)) return; let reason = ''; try { const result = await ElMessageBox.prompt(`出库单 ${row.outboundNo} · 出库数量 ${row.packageCount ?? 0}`, '取消出库单', { inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return }; mutationError.value = null; try { await cancelOutbound(row.outboundNo, reason); ElMessage.success('出库单已取消,库存及关联单据已回滚'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败') } } @@ -48,12 +52,12 @@ onMounted(load)
- +
-
填写后按指定箱唛出库,数量必须与出库数量一致。
+ 一件代发出库退货出库 diff --git a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue index e0f23423..61b19c94 100644 --- a/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue +++ b/src/modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue @@ -48,8 +48,8 @@ onMounted(() => { void load(); void loadOptions() })
查询
- +
- +