diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index c9e4656..590cd74 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -4,6 +4,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 HistoricalOrderSummary { id: string | null; orderNo: string; sourceOrderNo?: string; outboundOrderNo?: string; relabelOrderNo?: string; orderType?: string; status?: string; packageCount?: number } 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; 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 { @@ -36,7 +37,7 @@ export interface RelabelInventoryCandidate { } export interface TransferCreate { - operatingWarehouseId: string; fbaWarehouseCode?: string; destinationType?: 'NONE' | 'FBA' | 'PRIVATE' + inboundCode?: string; sourceLastlegOrderIds?: number[]; operatingWarehouseId: string; fbaWarehouseCode?: string; destinationType?: 'NONE' | 'FBA' | 'PRIVATE' destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string deliveryAddress: string; packageCount: number; volumeCbm: number; deliveryMethod: string; estimatedArrivalAt: string; estimatedShipAt?: string @@ -52,8 +53,8 @@ export interface RelabelCreate { 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 }> } -export function listLastlegOrders(inboundCode: string) { - return overseasApiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/orders', params: { page: 1, size: 200, inboundCode } }) +export function listRelabelHistoricalOrders(operationWarehouseId: string, keyword?: string) { + return overseasApiRequest({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications/historical-orders', params: { operationWarehouseId, keyword: keyword || undefined } }) } export function listTransfers(params: Record) { return overseasApiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/warehouse-transfers', params }) diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 6c84011..c6cf869 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -12,7 +12,7 @@ import { getTransferCartonLabel, submitTransfer, updateTransfer, confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, uploadRelabelShippingLabel, uploadRelabelFile, - listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabels, listTransfers, cancelDiscard, + listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, type RelabelInventoryCandidate, type RelabelCreate, type RelabelDetail, @@ -165,6 +165,9 @@ const discardCandidatesLoading = ref(false) const relabelCandidates = ref([]) const selectedRelabelInboundOrderId = ref('') const relabelCandidatesLoading = ref(false) +const transferInventoryCandidates = ref([]) +const selectedTransferInventory = ref('') +const selectedTransferSourceOrderIds = ref([]) const returnRelabelSkuRows = ref([]) const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:')) const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '') @@ -262,6 +265,9 @@ function resetForm() { discardCandidates.value = [] selectedDiscardOrderId.value = '' relabelCandidates.value = [] + transferInventoryCandidates.value = [] + selectedTransferInventory.value = '' + selectedTransferSourceOrderIds.value = [] selectedRelabelInboundOrderId.value = '' cartonTotalFile.value = null skuTotalFile.value = null @@ -884,6 +890,9 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) { ElMessage.warning('请选择 FBA 目的仓'); return } + if (props.service === 'transfer' && transferDestinationType.value === 'NONE') { + ElMessage.warning('请选择 FBA 仓或私人仓'); return + } if (props.service === 'transfer' && transferDestinationType.value === 'PRIVATE' && (!form.privateDestinationAddress || !form.privateDestinationPostalCode || !form.privateDestinationContact || !form.privateDestinationPhone)) { ElMessage.warning('请完整填写私人仓地址、邮编、联系人和电话'); return } @@ -942,6 +951,10 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } + if (props.service === 'transfer' && selectedTransferInventory.value) { + ;(payload as TransferCreate).inboundCode = selectedTransferInventory.value + ;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value + } const created = editingTransferNo.value ? await updateTransfer(editingTransferNo.value, payload) : await createTransfer(payload) @@ -1127,10 +1140,51 @@ async function selectTransferWarehouse() { form.relabelContact = detail.warehouse.contactName || '' form.relabelPhone = detail.warehouse.contactPhone || '' if (form.deliveryMethod === 'CUSTOMER_DELIVERY') form.address = operationWarehouseAddress.value + transferInventoryCandidates.value = await listRelabelInventoryCandidates(form.warehouse) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '中转仓联系信息加载失败') } finally { operationWarehouseDetailLoading.value = false } } +const transferInventoryOptions = computed(() => { + const grouped = new Map() + for (const item of transferInventoryCandidates.value) { + const value = String(item.inboundCode || '') + if (!value) continue + const packages = inventoryQuantity(item.availablePackages) + const current = grouped.get(value) + if (current) current.packages += packages + else grouped.set(value, { value, label: `${item.inboundOrderNo || value}|${value}`, packages }) + } + return [...grouped.values()] +}) +async function selectTransferInventory(value: string) { + selectedTransferInventory.value = value || '' + selectedTransferSourceOrderIds.value = [] + if (!value) return + form.inboundNo = value + // The inventory-candidates response already contains the merchant-visible + // SKU/name/package data. Do not call the platform-only last-leg orders API + // here: merchants are not authorized for that endpoint (A0403), and the + // transfer service can derive source orders from inboundCode when IDs are + // omitted. Aggregate candidates for the selected inbound into cargo rows. + const candidates = transferInventoryCandidates.value.filter((item) => String(item.inboundCode || '') === value) + const grouped = new Map() + for (const candidate of candidates) { + const sku = String(candidate.skuCode || '') + const current = grouped.get(sku) + const quantity = inventoryQuantity(candidate.availablePackages) + if (current) current.quantity += quantity + else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, availableQuantity: quantity }) + } + cargoRows.value = [...grouped.values()] + try { + const historical = await listRelabelHistoricalOrders(form.warehouse, value) + const sourceIds = historical.map((item) => Number(item.id)).filter((id) => Number.isInteger(id) && id > 0) + selectedTransferSourceOrderIds.value = sourceIds + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败') + } +} function selectTransferInboundMethod(value: string | number | boolean | undefined) { form.address = value === 'CUSTOMER_DELIVERY' ? operationWarehouseAddress.value : '' } @@ -1226,6 +1280,7 @@ function selectRelabelInventory(inboundOrderId: string) { +
*入仓方式仓库上门提货客户送货到仓
@@ -1253,7 +1308,7 @@ function selectRelabelInventory(inboundOrderId: string) {
*目的去向暂未确定FBA 仓私人仓
- + @@ -1261,7 +1316,7 @@ function selectRelabelInventory(inboundOrderId: string) { -
*换标服务不需要需要
+
*是否贴标不需要贴标需要贴标{{ form.relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' }}
@@ -1422,9 +1477,11 @@ function selectRelabelInventory(inboundOrderId: string) { .warehouse-template-actions{flex-wrap:wrap;justify-content:flex-end}.warehouse-import-preview{margin:0 0 16px;padding:14px;border:1px solid #cfe0f5;border-radius:5px;background:#f8fbff}.warehouse-import-preview .warehouse-section-heading{margin-bottom:10px}.warehouse-import-preview .warehouse-section-heading>div{display:flex;align-items:center;gap:10px}.warehouse-import-preview p{margin:0;color:#7d8797;font-size:12px}.warehouse-binding-preview-table{min-width:980px;background:#fff} .warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-file-groups{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);align-items:stretch;gap:14px;margin-bottom:14px}.warehouse-transfer-total-files,.warehouse-transfer-template-group{height:100%;margin:0}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545} .warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px minmax(0,1fr);gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}.warehouse-step-heading{align-items:flex-start}.warehouse-step-heading h3{display:flex;align-items:center;gap:9px}.warehouse-step-number{display:inline-grid;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-heading-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.warehouse-transfer-template-group{margin:0 0 14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#fbfcfe}.warehouse-binding-heading{margin-top:14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#f8faff}.warehouse-binding-heading strong{display:flex;align-items:center;gap:8px;color:#34445c;font-size:13px} +.warehouse-transfer-form .warehouse-choice-field--relabel{grid-template-columns:150px auto minmax(240px,1fr)}.warehouse-choice-field--relabel :deep(.el-radio-group){display:flex;flex-wrap:nowrap}.warehouse-choice-field--relabel>small{min-width:0;margin:0;line-height:1.5} @media(max-width:1250px){.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}} @media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}} .warehouse-transfer-form .pickup-address-field{grid-column:span 1} +.warehouse-transfer-main-card .warehouse-form-grid{grid-template-columns:repeat(2,minmax(280px,640px))}.warehouse-transfer-cargo-card .warehouse-section-heading,.warehouse-transfer-cargo-card .warehouse-table-scroll{width:100%;max-width:1296px} /* Keep the count and controls visible together on narrow list pages. */ .warehouse-pagination { justify-content: flex-end; } .warehouse-pagination-total { color: #526176; font-size: 14px; } @@ -1434,5 +1491,8 @@ function selectRelabelInventory(inboundOrderId: string) { .warehouse-list-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; } @media (max-width: 720px) { .warehouse-pagination :deep(.el-pagination) { width: 100%; justify-content: flex-end; } + .warehouse-transfer-main-card .warehouse-form-grid { grid-template-columns: minmax(0, 1fr); } + .warehouse-transfer-cargo-card .warehouse-section-heading, .warehouse-transfer-cargo-card .warehouse-table-scroll { max-width: none; } + .warehouse-transfer-form .warehouse-choice-field--relabel { grid-template-columns: 1fr; align-items: start; } } diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index b54ab9a..25675e9 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -25,9 +25,14 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否 assert.match(view, /value="FBA">FBA 仓/) assert.match(view, /value="PRIVATE">私人仓/) assert.match(view, /transferDestinationType === 'PRIVATE'.*私人仓地址.*邮编.*私人仓联系人.*联系电话/s) - assert.match(view, />换标服务不需要/) - assert.match(view, /:value="true">需要/) + assert.match(view, />是否贴标不需要贴标/) + assert.match(view, /:value="true">需要贴标/) + assert.match(view, /FBA 仓和私人仓均可选择是否贴标/) + assert.doesNotMatch(view, /v-model="form\.relabel" disabled/) + assert.doesNotMatch(view, /value === 'FBA'\) form\.relabel = true/) + assert.doesNotMatch(view, /value === 'PRIVATE'.*form\.relabel = false/) + assert.match(view, /v-if="props\.service === 'transfer' && form\.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel"/) assert.match(api, /fbaWarehouseCode\?: string/) assert.match(api, /privateDestinationAddress\?: string; privateDestinationPostalCode\?: string/) assert.match(api, /fbaShipmentNo\?: string/)