From eedaa2f080bdab0d6aee47fbb80b7dabaa421471 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 18 Aug 2026 11:32:58 +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 --- src/modules/first-leg/api/firstLeg.ts | 10 ++++ .../first-leg/store/firstLegOrders.test.mjs | 5 ++ src/modules/first-leg/store/firstLegOrders.ts | 1 + src/modules/first-leg/types/firstLegOrder.ts | 1 + .../views/FirstLegOrdersView.test.mjs | 8 +++ .../first-leg/views/FirstLegOrdersView.vue | 27 ++++++++- .../first-leg/views/QuickOrderView.test.mjs | 14 +++-- .../first-leg/views/QuickOrderView.vue | 60 +++++++++++++++---- 8 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index 5fbfc89..fac2bbb 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -120,6 +120,7 @@ export interface ProductImageUploadResult { } export interface FirstLegOrderSaveRequest { + inboundCode?: string customerName: string transportMode: ApiTransportMode tradeTerm?: string @@ -148,6 +149,11 @@ export interface FirstLegOrderSaveRequest { referenceId?: string } +export interface InboundCodePolicy { + mode: 'AUTO' | 'LEGACY_MANUAL' + manualEntryRequired: boolean +} + export interface MerchantBatchDraftItem { clientRowId: string draft: FirstLegOrderSaveRequest @@ -442,6 +448,10 @@ export function getMerchantFirstLegOrder(id: number | string) { return apiRequest({ method: 'GET', url: `/merchant/v1/orders/first-leg/${id}` }) } +export function getMerchantInboundCodePolicy() { + return apiRequest({ method: 'GET', url: '/merchant/v1/orders/first-leg/inbound-code-policy' }) +} + export function createMerchantFirstLegOrder(payload: FirstLegOrderSaveRequest) { return apiRequest({ method: 'POST', diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 1956517..6ceaded 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -26,6 +26,7 @@ test('maps the first-leg aggregate list fields without loading detail rows', () test('declares the documented merchant warehouse and first-leg endpoints', () => { assert.match(apiSource, /url: '\/merchant\/v1\/warehouses'/) assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg'/) + assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/inbound-code-policy'/) assert.match(apiSource, /url: '\/merchant\/v1\/orders\/first-leg\/batch'/) assert.match(apiSource, /`\/merchant\/v1\/orders\/first-leg\/\$\{id\}`/) assert.match(apiSource, /`\/merchant\/v1\/orders\/first-leg\/\$\{id\}\/submit`/) @@ -118,6 +119,10 @@ test('maps C02 authoritative status, transport, cargo and pickup snapshots', () assert.match(storeSource, /customerRemark: summary\.customerRemark \?\? ''/) }) +test('sends a manually entered old-system inbound code with the draft payload', () => { + assert.match(storeSource, /inboundCode: draft\.inboundCode\?\.trim\(\) \|\| undefined/) +}) + test('sends backend-required channel, pricing, customs, destination and cargo fields', () => { for (const field of ['pickupMethod', 'channelCode', 'channelName', 'pricingTaxMode', 'taxMode', 'customsInspection', 'destinationType', 'packageType']) { assert.match(storeSource, new RegExp(field)) diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 42c8c56..33ffd63 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -103,6 +103,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust }) : undefined return { + inboundCode: draft.inboundCode?.trim() || undefined, customerName: customerName.trim(), transportMode: apiTransport(draft), tradeTerm: draft.tradeTerm || undefined, diff --git a/src/modules/first-leg/types/firstLegOrder.ts b/src/modules/first-leg/types/firstLegOrder.ts index 3949e31..b3ee077 100644 --- a/src/modules/first-leg/types/firstLegOrder.ts +++ b/src/modules/first-leg/types/firstLegOrder.ts @@ -184,4 +184,5 @@ export type FirstLegOrderDraft = Omit, 'transportMode'> & { transportMode: FirstLegTransportMode + inboundCode?: string } diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index acf2552..c7e5de6 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -483,6 +483,14 @@ test('copies reactive order drafts into plain request data', () => { assert.doesNotMatch(source, /structuredClone\((?:form|order\.)/) }) +test('uses the global inbound-code policy to require old-system codes during parallel running', () => { + assert.match(source, /getMerchantInboundCodePolicy\(\)/) + assert.match(source, /const manualInboundCodeRequired = computed/) + assert.match(source, /v-if="manualInboundCodeRequired" label="旧系统入仓码"/) + assert.match(source, /当前全系统处于旧系统并跑模式/) + assert.match(source, /当前为旧系统并跑模式,请填写旧系统入仓码/) +}) + test('shows authoritative warehouse, loading and sailing fields on desktop and narrow screens', () => { for (const label of ['国内收货仓', '期望出运', '柜号 / 柜次', '船名 / 航次', '预计船期', '更新时间']) { assert.match(source, new RegExp(label.replace('/', '\\/'))) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 3bc4852..0818111 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -16,11 +16,13 @@ import { estimateMerchantFirstLegPrice, downloadMerchantOrderCartonLabelsBatch, downloadMerchantProductImage, + getMerchantInboundCodePolicy, getMerchantPricingChannels, uploadMerchantProductImage, type MerchantPriceEstimate, type MerchantPriceEstimateRequest, type MerchantPricingChannel, + type InboundCodePolicy, } from '@/modules/first-leg/api/firstLeg' import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders' import { useTenantAdministrationStore } from '@/modules/tenant-management/public' @@ -68,6 +70,8 @@ const queryFields = computed(() => [ { key: 'trackingNumber', label: '快递单号', type: 'input', placeholder: '请输入快递单号', props: { 'aria-label': '快递单号' } }, ]) const channelOptions = ref([]) +const inboundCodePolicy = ref(null) +const manualInboundCodeRequired = computed(() => inboundCodePolicy.value?.mode === 'LEGACY_MANUAL') const channelLoading = ref(false) const channelError = ref('') const priceEstimate = ref(null) @@ -331,6 +335,7 @@ function displayValue(value: string | null | undefined) { function emptyDraft(): FirstLegOrderDraft { return { + inboundCode: '', referenceNo: '', transportMode: 'FCL', businessType: 'EXPORT', @@ -512,11 +517,17 @@ async function loadPageData() { const orderPageRequest = isCreatePage.value ? store.loadWarehouseType('DOMESTIC', true) : store.load() - const [ordersResult, companyResult, destinationResult] = await Promise.allSettled([ + const [ordersResult, companyResult, destinationResult, policyResult] = await Promise.allSettled([ orderPageRequest, companyRequest, destinationRequest, + getMerchantInboundCodePolicy(), ]) + if (policyResult.status === 'fulfilled') inboundCodePolicy.value = policyResult.value + else { + inboundCodePolicy.value = null + supportingDataErrors.value.push(`入仓码配置加载失败:${errorMessage(policyResult.reason)}`) + } if (canViewCompanyProfile.value && companyResult.status === 'fulfilled') { companyAuditLoaded.value = true } else if (companyResult.status === 'rejected') { @@ -675,6 +686,7 @@ function removeCargoProductImage(item: FirstLegCargoItem) { function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft { return { + inboundCode: order.businessSummary.inboundCode, referenceNo: order.referenceNo, transportMode: order.transportMode ?? 'FCL', businessType: order.businessType, @@ -1388,6 +1400,15 @@ async function validateEditorForm() { return false } + if (!inboundCodePolicy.value) { + ElMessage.warning('入仓码配置尚未加载,请稍后重试') + return false + } + if (manualInboundCodeRequired.value && !form.inboundCode?.trim()) { + ElMessage.warning('当前为旧系统并跑模式,请填写旧系统入仓码') + return false + } + const errors = validateFirstLegOrderDraft(form) if (errors.length > 0) { ElMessage.warning(errors[0]) @@ -1631,6 +1652,10 @@ async function searchOrders(model: QueryModel) { {{ warehouse.name }}
+ + + 当前全系统处于旧系统并跑模式,请粘贴旧系统生成的入仓码;订单提交后不可修改。 + 渠道加载失败:{{ channelError }} 重试 diff --git a/src/modules/first-leg/views/QuickOrderView.test.mjs b/src/modules/first-leg/views/QuickOrderView.test.mjs index b3a0652..69df221 100644 --- a/src/modules/first-leg/views/QuickOrderView.test.mjs +++ b/src/modules/first-leg/views/QuickOrderView.test.mjs @@ -16,7 +16,7 @@ test('exposes quick order as an active first-leg route with its dedicated permis assert.match(orderViewSource, /store\.loadDraftDetail\(id\)/) }) -test('keeps the primary quick-order table limited to channel, destination, SKU and package count', () => { +test('keeps the primary quick-order table focused on inbound code policy, channel, destination and cargo', () => { for (const label of ['选择渠道', '目的仓', '产品信息:产品 SKU / 箱数', '新增一行', '生成草稿单']) { assert.match(source, new RegExp(label)) } @@ -24,6 +24,10 @@ test('keeps the primary quick-order table limited to channel, destination, SKU a assert.match(source, /destinationType: 'FBA'/) assert.match(source, /function createProduct\(\): QuickOrderProduct/) assert.match(source, /products: \[createProduct\(\)\]/) + assert.match(source, /getMerchantInboundCodePolicy/) + assert.match(source, /manualInboundCodeRequired/) + assert.match(source, /v-if="manualInboundCodeRequired"[^>]*>旧系统入仓码/) + assert.match(source, /inboundCode: row\.inboundCode\.trim\(\) \|\| undefined/) }) test('creates one real backend draft per order row and keeps its products together', () => { @@ -33,7 +37,7 @@ test('creates one real backend draft per order row and keeps its products togeth assert.match(source, /clientRowId: row\.id, draft: buildOrderDraft\(row, channel\)/) assert.match(source, /store\.saveDraftBatch\(drafts, customerName, activeBatchKey\.value\)/) assert.match(source, /response\.items\.flatMap/) - assert.match(source, /item\.inboundCode\?\.trim\(\)/) + assert.match(source, /inboundCode: item\.inboundCode\?\.trim\(\) \?\? ''/) assert.match(source, /已生成 \$\{successes\.length\} 张草稿单/) assert.match(source, /successfulIds\.has\(rows\[index\]\.id\)/) assert.match(source, /response\.items\.filter/) @@ -46,7 +50,9 @@ test('creates one real backend draft per order row and keeps its products togeth assert.doesNotMatch(source, /name: 'first-leg-order-create'/) }) -test('requires only channel, destination, SKU and package count before directly creating drafts', () => { +test('requires the old inbound code globally only while legacy manual mode is active', () => { + assert.match(source, /manualInboundCodeRequired\.value[\s\S]*?rows\.findIndex\(\(row\) => !row\.inboundCode\.trim\(\)\)/) + assert.match(source, /请填写第 \$\{missingInboundCodeIndex \+ 1\} 行的旧系统入仓码/) assert.match(source, /!row\.channelKey \|\| !row\.destinationWarehouseName/) assert.match(source, /!product\.sku\.trim\(\) \|\| product\.packageCount < 1/) assert.match(source, /渠道、目的仓、产品 SKU 和箱数/) @@ -69,7 +75,7 @@ test('requires only channel, destination, SKU and package count before directly test('shows every created draft and keeps failed rows available for retry', () => { assert.match(source, /v-for="result in draftResults"/) - assert.match(source, /入仓码 \{\{ result\.inboundCode \}\}/) + assert.match(source, /入仓码将在提交订单时生成/) assert.match(source, /@click="editDraft\(result\)"/) assert.match(source, /if \(rows\.length === 0\) rows\.push\(createRow\(\)\)/) assert.match(source, /resultDialogOpen\.value = successes\.length > 0/) diff --git a/src/modules/first-leg/views/QuickOrderView.vue b/src/modules/first-leg/views/QuickOrderView.vue index 32bd353..9d2e07a 100644 --- a/src/modules/first-leg/views/QuickOrderView.vue +++ b/src/modules/first-leg/views/QuickOrderView.vue @@ -4,7 +4,12 @@ import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { MapPin, Plus, Send, Trash2 } from '@lucide/vue' import { useAuthStore } from '@/modules/auth/store/auth' -import { getMerchantPricingChannels, type MerchantPricingChannel } from '@/modules/first-leg/api/firstLeg' +import { + getMerchantInboundCodePolicy, + getMerchantPricingChannels, + type InboundCodePolicy, + type MerchantPricingChannel, +} from '@/modules/first-leg/api/firstLeg' import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders' import type { FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder' import { createEmptyCargoItem } from '@/modules/first-leg/utils/firstLegOrder' @@ -19,6 +24,7 @@ interface QuickOrderProduct { interface QuickOrderRow { id: string + inboundCode: string channelKey: string destinationType: DestinationType destinationWarehouseId: string @@ -52,6 +58,7 @@ const store = useFirstLegOrdersStore() const loading = ref(false) const submitting = ref(false) const loadError = ref('') +const inboundCodePolicy = ref(null) const channelOptions = ref([]) const destinationDialogOpen = ref(false) const destinationLoading = ref(false) @@ -85,6 +92,7 @@ function createProduct(): QuickOrderProduct { function createRow(): QuickOrderRow { return reactive({ id: `quick-order-${++rowSequence}`, + inboundCode: '', channelKey: '', destinationType: 'FBA', destinationWarehouseId: '', @@ -102,6 +110,7 @@ function createRow(): QuickOrderRow { const rows = reactive([createRow()]) const hasChannels = computed(() => channelOptions.value.length > 0) +const manualInboundCodeRequired = computed(() => inboundCodePolicy.value?.mode === 'LEGACY_MANUAL') const availableDestinationWarehouses = computed(() => destinationForm.type === 'PRIVATE' ? [] : store.warehouses.filter((warehouse) => warehouse.warehouseType === destinationForm.type)) @@ -163,6 +172,7 @@ function hasCompletePrivateContact(row: QuickOrderRow) { function buildOrderDraft(row: QuickOrderRow, channel: QuickChannelOption): FirstLegOrderDraft { const pricingTaxMode = channel.pricingTaxModes[0] ?? 'TAX_INCLUDED' return { + inboundCode: row.inboundCode.trim() || undefined, referenceNo: '', transportMode: 'FCL', businessType: 'EXPORT', @@ -288,7 +298,11 @@ async function loadChannels() { loading.value = true loadError.value = '' try { - await store.loadWarehouseType('DOMESTIC', true) + const [, policy] = await Promise.all([ + store.loadWarehouseType('DOMESTIC', true), + getMerchantInboundCodePolicy(), + ]) + inboundCodePolicy.value = policy const results = await Promise.allSettled(store.warehouses .filter((warehouse) => warehouse.warehouseType === 'DOMESTIC') .map(async (warehouse) => { @@ -305,6 +319,17 @@ async function loadChannels() { } async function submitQuickOrder() { + if (!inboundCodePolicy.value) { + ElMessage.warning('入仓码规则尚未加载,请刷新页面后重试') + return + } + const missingInboundCodeIndex = manualInboundCodeRequired.value + ? rows.findIndex((row) => !row.inboundCode.trim()) + : -1 + if (missingInboundCodeIndex >= 0) { + ElMessage.warning(`请填写第 ${missingInboundCodeIndex + 1} 行的旧系统入仓码`) + return + } const incompleteIndex = rows.findIndex((row) => !row.channelKey || !row.destinationWarehouseName || row.products.some((product) => !product.sku.trim() || product.packageCount < 1)) if (incompleteIndex >= 0) { @@ -330,7 +355,7 @@ async function submitQuickOrder() { if (!activeBatchKey.value) activeBatchKey.value = createBatchKey() const response = await store.saveDraftBatch(drafts, customerName, activeBatchKey.value) const successes = response.items.flatMap((item): QuickDraftResult[] => { - if (!item.success || item.orderId == null || !item.inboundCode?.trim()) return [] + if (!item.success || item.orderId == null) return [] const row = rows.find((candidate) => candidate.id === item.clientRowId) if (!row) return [] return [{ @@ -338,10 +363,10 @@ async function submitQuickOrder() { sku: row.products.map((product) => product.sku.trim()).join('、'), draftId: String(item.orderId), orderNo: item.orderNo ?? '', - inboundCode: item.inboundCode.trim(), + inboundCode: item.inboundCode?.trim() ?? '', }] }) - const failures = response.items.filter((item) => !item.success || item.orderId == null || !item.inboundCode?.trim()) + const failures = response.items.filter((item) => !item.success || item.orderId == null) draftResults.splice(0, draftResults.length, ...successes) const successfulIds = new Set(successes.map((item) => item.rowId)) for (let index = rows.length - 1; index >= 0; index -= 1) { @@ -374,6 +399,7 @@ onMounted(loadChannels)

客户下单

只需输入渠道、目的仓、产品 SKU 和箱数,即可快速填写订单。

+ 当前全局使用旧系统入仓码,每张草稿都需要填写旧系统生成的入仓码。 每一行会直接生成一张独立草稿单,其他明细可进入草稿后继续编辑并提交。
@@ -385,14 +411,23 @@ onMounted(loadChannels) - - - + + + + +
选择渠道目的仓操作旧系统入仓码选择渠道目的仓操作 产品信息:产品 SKU / 箱数
+ +
-
{{ result.orderNo || `草稿 ${result.draftId}` }}SKU {{ result.sku }}入仓码 {{ result.inboundCode }}
+
{{ result.orderNo || `草稿 ${result.draftId}` }}SKU {{ result.sku }}{{ result.inboundCode ? `入仓码 ${result.inboundCode}` : '入仓码将在提交订单时生成' }}
编辑明细
@@ -494,11 +529,12 @@ onMounted(loadChannels) .quick-order-table-wrap { min-height: 126px; overflow-x: auto; border: 1px solid #e5eaf2; border-radius: 8px; } .quick-order-table { width: 100%; min-width: 900px; border-collapse: collapse; table-layout: fixed; } .quick-order-table th { height: 42px; padding: 0 14px; background: #f4f5fb; color: #637fec; font-size: 12px; font-weight: 650; text-align: left; } -.quick-order-table th:nth-child(1) { width: 220px; } -.quick-order-table th:nth-child(2) { width: 145px; } -.quick-order-table th:nth-child(3) { width: 210px; } +.quick-order-table .inbound-code-heading, .quick-order-table .channel-heading { width: 220px; } +.quick-order-table .destination-heading { width: 145px; } +.quick-order-table .operation-heading { width: 210px; } .quick-order-table td { padding: 12px 14px; border-top: 1px solid #edf0f5; vertical-align: middle; } .channel-cell :deep(.el-select) { width: 100%; } +.inbound-code-cell :deep(.el-input) { width: 100%; } .destination-options { display: flex; align-items: flex-start; flex-direction: column; gap: 4px; } .destination-options :deep(.el-radio) { height: 20px; margin-right: 0; } .destination-binding { display: flex; width: 100%; min-width: 0; align-items: center; gap: 5px; margin-top: 7px; padding: 0; border: 0; background: transparent; color: #637fec; cursor: pointer; font-size: 11px; text-align: left; }