diff --git a/src/components.d.ts b/src/components.d.ts index f15b3f0..c3c224a 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -23,8 +23,6 @@ declare module 'vue' { ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] - ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] - ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] diff --git a/src/modules/first-leg/routes.ts b/src/modules/first-leg/routes.ts index 9357aae..3a460a9 100644 --- a/src/modules/first-leg/routes.ts +++ b/src/modules/first-leg/routes.ts @@ -19,7 +19,7 @@ export const firstLegModule = { name: 'first-leg-order-create', component: () => import('./views/FirstLegOrderCreateHubView.vue'), release: 'active', - meta: { title: '创建订单', workspaceTitle: '头程订单 · 新建', description: '填写发货、目的地和货物信息', permission: 'order:first-leg:create' }, + meta: { title: '创建订单', workspaceTitle: '头程订单 · 新建', description: '填写发货、目的地和货物信息', permission: 'order:first-leg:create', keepAlive: false }, }, { path: 'orders/first-leg/:orderId/detail', diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 9dd7d9c..dc90cbc 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -19,7 +19,7 @@ test('routes the confirmed first-leg path to the implemented order view', () => }) test('opens order creation as a dedicated protected page while keeping draft editing in a large dialog', () => { - assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/) + assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'[\s\S]*?keepAlive: false/) assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?FirstLegOrderCreateHubView\.vue/) assert.match(createHubSource, //) assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/) @@ -48,6 +48,17 @@ test('exposes real list, draft, submit, and detail workflow entries without canc assert.doesNotMatch(source, /cancelOrder|取消订单/) }) +test('copies an order into the dedicated create page without reusing order identity', () => { + assert.match(source, /key: 'copy', label: '一键复制'/) + assert.match(source, /name: 'first-leg-order-create'[\s\S]*copyFrom: order\.id/) + assert.match(source, /order\.status === 'DRAFT' \? \{ copyDraft: '1' \}/) + assert.match(source, /sourceIsDraft \? await store\.loadDraftDetail\(id\) : await store\.loadDetail\(id\)/) + assert.match(source, /const draft = draftFromOrder\(order\)[\s\S]*inboundCode: ''/) + assert.match(source, /id: `copied-cargo-\$\{\+\+cargoSequence\}`/) + assert.match(source, /editingId\.value = undefined/) + assert.match(source, /订单数据已复制,请核对后提交/) +}) + test('keeps create-page support errors scoped to create-page requests', () => { assert.match(source, /if \(!isCreatePage\.value && store\.draftLoadError\)/) }) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index d45c95d..b67cee3 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -71,6 +71,8 @@ const isCreatePage = computed(() => props.createPage) const isDetailPage = computed(() => props.detailPage) const routeDraftId = computed(() => typeof route.params.draftId === 'string' ? route.params.draftId : '') const routeDetailId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '') +const routeCopySourceId = computed(() => typeof route.query.copyFrom === 'string' ? route.query.copyFrom : '') +const routeCopySourceIsDraft = computed(() => route.query.copyDraft === '1') const canViewCompanyProfile = computed(() => auth.hasPermission('company:profile:view')) const companyAuditLoaded = ref(false) const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE') @@ -197,6 +199,7 @@ const editorOpen = ref(false) const orderFormRef = ref() const editingId = ref() const openingEditor = ref(false) +const copyingOrderId = ref('') const detailId = ref() const saving = ref(false) const detailLoading = ref(false) @@ -616,6 +619,7 @@ async function loadPageData() { if (addressResult.status === 'rejected') supportingDataErrors.value.push(`企业地址簿加载失败:${errorMessage(addressResult.reason)}`) await autofillWarehouseSelections() if (isCreatePage.value && routeDraftId.value) await loadRouteDraft(routeDraftId.value) + else if (isCreatePage.value && routeCopySourceId.value) await loadCopiedOrder(routeCopySourceId.value, routeCopySourceIsDraft.value) if (isDetailPage.value && routeDetailId.value) await loadDetailPage(routeDetailId.value) supportingDataLoading.value = false } @@ -633,6 +637,39 @@ async function loadRouteDraft(id: string) { markEditorClean() } +async function loadCopiedOrder(id: string, sourceIsDraft: boolean) { + try { + const detailedOrder = sourceIsDraft ? await store.loadDraftDetail(id) : await store.loadDetail(id) + const draft = copyDraftFromOrder(detailedOrder) + if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType) + + const warehouseAvailable = availableWarehouses.value.some((item) => item.id === draft.receivingWarehouseId) + if (!warehouseAvailable) { + draft.receivingWarehouseId = '' + draft.channelCode = '' + draft.channelName = '' + } + if (draft.destinationType !== 'PRIVATE') { + const destinationAvailable = store.warehouses.some((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType) + if (!destinationAvailable) { + draft.destinationWarehouseId = '' + draft.destinationWarehouseName = '' + draft.amazonWarehouseCode = '' + } + } + + editingId.value = undefined + replaceForm(draft) + await Promise.all(form.items.map(ensureProductImagePreview)) + if (warehouseAvailable) await loadSelectedWarehouse(draft.receivingWarehouseId) + resetEditorValidation() + markEditorClean() + ElMessage.success('订单数据已复制,请核对后提交') + } catch (error) { + supportingDataErrors.value.push(`复制订单数据失败:${errorMessage(error)}`) + } +} + async function loadDestinationWarehouses(type: 'FBA' | 'OVERSEAS' | 'PRIVATE') { if (type === 'PRIVATE') return destinationWarehousesLoading.value = true @@ -833,6 +870,21 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft { } } +function copyDraftFromOrder(order: FirstLegOrder): FirstLegOrderDraft { + const draft = draftFromOrder(order) + return { + ...draft, + inboundCode: '', + items: draft.items.map((item) => ({ + ...item, + id: `copied-cargo-${++cargoSequence}`, + productImagePreviewUrl: '', + productImageUploading: false, + productImageError: '', + })), + } +} + function selectChannel(code: string) { const channel = channelOptions.value.find((item) => item.channelCode === code) form.channelName = channel?.channelName ?? '' @@ -1225,6 +1277,9 @@ function getOrderRowActions(row: object): TableAction[] { const actions: TableAction[] = [ { key: 'detail', label: '详情', run: () => openDetail(order) }, ] + if (canCreateOrder.value) { + actions.push({ key: 'copy', label: '一键复制', loading: copyingOrderId.value === order.id, run: () => copyOrder(order) }) + } if (canConfirmInboundFee.value && order.businessSummary.customerFeeConfirmationRequired) { actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) }) } @@ -1237,6 +1292,22 @@ function getOrderRowActions(row: object): TableAction[] { return actions } +async function copyOrder(order: FirstLegOrder) { + if (copyingOrderId.value || !canCreateOrder.value) return + copyingOrderId.value = order.id + try { + await router.push({ + name: 'first-leg-order-create', + query: { + copyFrom: order.id, + ...(order.status === 'DRAFT' ? { copyDraft: '1' } : {}), + }, + }) + } finally { + copyingOrderId.value = '' + } +} + async function openCreate() { if (availableWarehouses.value.length === 0) { ElMessage.warning(supportingDataLoading.value @@ -2061,7 +2132,7 @@ async function searchOrders(model: QueryModel) { - +

客户下单

先选择货物如何送到国内收货仓,并填写本次运输安排。