From bff9ccbfb6fd2156e878863a92791ceda3686fc6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 22 Aug 2026 10:02:13 +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 --- .../pages/GlobalSmartLoadingView.test.ts | 9 +++++---- .../pages/GlobalSmartLoadingView.vue | 12 ++++++++++-- .../pages/SmartLoadingWorkbenchView.test.ts | 9 +++++---- .../pages/SmartLoadingWorkbenchView.vue | 15 +++++++++++++-- .../pages/TruckAppointmentDetailView.test.ts | 8 ++++++++ .../trucking/pages/TruckAppointmentDetailView.vue | 14 +++++++++++++- .../pages/UnifiedOrdersView.test.ts | 3 ++- .../unified-orders/pages/UnifiedOrdersView.vue | 11 +++++++---- 8 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 3632a9a1..16bcb9c4 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -312,13 +312,13 @@ describe('GlobalSmartLoadingView', () => { const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, version: 3 }) - vi.mocked(recommendSmartLoading).mockRejectedValue(new AxiosError( + vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError( 'conflict', 'ERR_BAD_REQUEST', undefined, undefined, { data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never }, - )) + )).mockResolvedValueOnce({ ...plan, version: 4 }) const button = wrapper.findAll('button').find((item) => item.text().includes('智能推荐')) expect(button).toBeDefined() @@ -326,8 +326,9 @@ describe('GlobalSmartLoadingView', () => { await flushPromises() expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2) - expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2) - expect(warningMessage).toHaveBeenCalledWith('计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作') + expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3) + expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(3) + expect(warningMessage).not.toHaveBeenCalled() expect(wrapper.text()).not.toContain('装柜操作失败') expect(wrapper.text()).not.toContain('request-1') }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 1817ee1b..d38e9abf 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -296,7 +296,15 @@ async function assign() { async function recommend() { if (!plan.value) return await mutate(async () => { - acceptPlan(await recommendSmartLoading(plan.value!.id, plan.value!.version)) + try { + acceptPlan(await recommendSmartLoading(plan.value!.id, plan.value!.version)) + } catch (error) { + const details = getApiErrorDetails(error, '智能推荐失败') + if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error + await loadSelectedData() + if (!plan.value) throw error + acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) + } await loadSelectedData() ElMessage.success('智能推荐已生成') }, '智能推荐失败') @@ -377,7 +385,7 @@ async function mutate( const pickingMessage = recovery.pickingConflict && details.message.includes('拣货') ? '该订单在拣货中不许删除' : null - const message = pickingMessage || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作' + const message = pickingMessage || details.message || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作' if (recovery.preserveAssign) assignOpen.value = true if (pickingMessage) ElMessage.error(pickingMessage) else ElMessage.warning(message) diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts index 1f40f3ac..6464f1b1 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts @@ -75,10 +75,10 @@ describe('SmartLoadingWorkbenchView', () => { vi.mocked(getSmartLoadingPlanByBooking) .mockResolvedValueOnce(planResponse) .mockResolvedValueOnce({ ...planResponse, version: 3 }) - vi.mocked(recommendSmartLoading).mockRejectedValue(new AxiosError( + vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError( 'conflict', 'ERR_BAD_REQUEST', undefined, undefined, { data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never }, - )) + )).mockResolvedValueOnce({ ...planResponse, version: 4 }) const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] }) await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady() const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } }) @@ -87,8 +87,9 @@ describe('SmartLoadingWorkbenchView', () => { await wrapper.findAll('button').find((button) => button.text().includes('智能推荐'))!.trigger('click') await flushPromises() expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2) - expect(wrapper.text()).toContain('已刷新最新版本') - expect(wrapper.text()).toContain('request-1') + expect(recommendSmartLoading).toHaveBeenCalledWith(planResponse.id, 2) + expect(recommendSmartLoading).toHaveBeenCalledWith(planResponse.id, 3) + expect(wrapper.text()).not.toContain('排柜操作失败') }) it('uses bordered row actions and keeps push independent from containerNo', () => { diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue index 599ad0e8..b86791b7 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue @@ -149,7 +149,18 @@ async function recommend() { if (!plan.value) return mutating.value = true mutationError.value = null - try { acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)); ElMessage.success('智能建议已生成,请人工核对后再推送') } + try { + try { + acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) + } catch (error) { + const details = getApiErrorDetails(error, '智能推荐失败') + if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error + await load() + if (!plan.value) throw error + acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) + } + ElMessage.success('智能建议已生成,请人工核对后再推送') + } catch (error) { await handleMutationError(error, '智能推荐失败') } finally { mutating.value = false } } @@ -196,7 +207,7 @@ async function handleMutationError(error: unknown, fallback: string, pickingConf const pickingMessage = pickingConflict && details.code === 'A0409' && details.message.includes('拣货') ? '该订单在拣货中不许删除' : null - mutationError.value = { ...details, message: pickingMessage || (details.code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details.message) } + mutationError.value = { ...details, message: pickingMessage || details.message || (details.code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details.message) } ElMessage.error(mutationError.value.message) } diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts b/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts index 077d2de7..6ba8a2b6 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts @@ -44,4 +44,12 @@ describe('TruckAppointmentDetailView status contract', () => { expect(source).toContain("status === 'DISPATCHED' && dispatchBlockReason.value") expect(source).toContain('attachmentOpen.value = true') }) + + it('keeps the main detail available when attachments or history fail', () => { + expect(source).toContain('Promise.allSettled([') + expect(source).toContain("attachments.status === 'fulfilled'") + expect(source).toContain("histories.status === 'fulfilled'") + expect(source).toContain('主详情数据不受影响') + expect(source).toContain('v-if="relatedError"') + }) }) diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.vue b/src/modules/trucking/pages/TruckAppointmentDetailView.vue index 03cf9d33..809d30ab 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.vue +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.vue @@ -15,9 +15,20 @@ import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue' import { truckAppointmentActionLabel, truckAppointmentStatusLabel, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAppointmentStatusHistory } from '../types' const route = useRoute(); const router = useRouter(); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref(null); const error = ref(null); const form = reactive({ expectedVersion: 0, customerId: null, carrierId: null, plannedLoadingAt: null }) const history = ref([]) +const relatedError = ref('') const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && ['DRAFT', 'PENDING_DISPATCH', 'RESCHEDULED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate')); const canManageAttachments = computed(() => access.can('platform:firstleg:truck-appointment:edit')) const relationSnapshot = computed(() => resource.value ? [resource.value.customerName, resource.value.oceanBookingNo, resource.value.cabinetNo, resource.value.containerNo, resource.value.warehouseName].filter(Boolean).join(' / ') || '无历史关联' : '') -async function loadRelated() { if (!resource.value) return; const [attachments, histories] = await Promise.all([listTruckAppointmentAttachments(id.value), listTruckAppointmentHistory(id.value)]); resource.value.attachments = attachments; history.value = histories } +async function loadRelated() { + if (!resource.value) return + relatedError.value = '' + const [attachments, histories] = await Promise.allSettled([ + listTruckAppointmentAttachments(id.value), + listTruckAppointmentHistory(id.value), + ]) + if (attachments.status === 'fulfilled') resource.value.attachments = attachments.value + if (histories.status === 'fulfilled') history.value = histories.value + if (attachments.status === 'rejected' || histories.status === 'rejected') relatedError.value = '附件或状态历史暂时未能加载,主详情数据不受影响,可稍后刷新重试。' +} async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0 }); await loadRelated() } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } } async function submit() { saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } } const actionByTarget: Partial> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' } @@ -56,6 +67,7 @@ onMounted(load) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index ad9fdd6e..58542937 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -298,9 +298,10 @@ describe('UnifiedOrdersView contract', () => { it('keeps the basic information compact and maps available order fields', () => { expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))') - for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '总净重', '商检验', '提货联系人', '目的地地址']) { + for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '客户申报净重', '客户申报总体积', '商检验', '提货联系人', '目的地地址']) { expect(source).toContain(`
${label}
`) } + expect(source).toContain("['actualVolumeCbm', 'chargeableVolumeCbm'].includes(property)") expect(source).toContain("partyValue(detail.order.parties, 'SENDER', 'phone')") expect(source).toContain("partyValue(detail.order.parties, 'RECEIVER', 'phone')") expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'") diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index b7f2d01e..a4563730 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -401,7 +401,10 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) { if (value === null || value === undefined || value === '') return '—' if (typeof value === 'boolean') return value ? '是' : '否' if (Array.isArray(value)) return value.length ? (property === 'skuNos' ? value.join('、') : `${value.length} 个文件`) : '—' - if (typeof value === 'number') return formatNumber(value) + if (typeof value === 'number') { + if (['actualVolumeCbm', 'chargeableVolumeCbm'].includes(property)) return formatNumber(value, 3) + return formatNumber(value) + } if (property.endsWith('At') || property.endsWith('Date')) return formatDate(String(value)) if (property === 'measurementReviewStatus') return measurementStatusLabel(String(value)) if (property === 'internalOrderStatus') return orderStatusLabel(String(value)) @@ -961,9 +964,9 @@ onBeforeUnmount(() => {
是否整柜
{{ firstLegContainerModeLabel(detail.order.order?.containerMode) }}
商检验
{{ displayBoolean(detail.order.order?.customsInspection) }}
总箱数
{{ detail.summary.totalPackageCount ?? '—' }} 箱
-
总毛重
{{ formatNumber(cargoTotal(detail.order.cargoes, 'grossWeightKg') ?? detail.summary.measurement?.actualGrossWeightKg) }} kg
-
总净重
{{ formatNumber(cargoTotal(detail.order.cargoes, 'netWeightKg')) }} kg
-
总体积
{{ formatNumber(cargoTotal(detail.order.cargoes, 'volumeCbm') ?? detail.summary.measurement?.actualVolumeCbm, 3) }} m³
+
客户申报毛重
{{ formatNumber(cargoTotal(detail.order.cargoes, 'grossWeightKg')) }} kg
+
客户申报净重
{{ formatNumber(cargoTotal(detail.order.cargoes, 'netWeightKg')) }} kg
+
客户申报总体积
{{ formatNumber(cargoTotal(detail.order.cargoes, 'volumeCbm'), 3) }} m³
计费毛重
{{ formatNumber(detail.summary.measurement?.chargeableWeightKg) }} kg
计费体积
{{ formatNumber(detail.summary.chargeableVolumeCbm, 3) }} m³
总材积重
{{ formatNumber(detail.summary.volumetricWeightKg) }} kg