From 32676797581ffad8bb2e9929ef355212ab24d499 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 20 Oct 2026 10:11:31 +0800 Subject: [PATCH] style(admin): unify dialog layouts and dense readonly presentation --- .../AppointmentCalendarDialogs.native.test.ts | 135 ++++ .../AppointmentCalendarView.test.ts | 3 + .../AppointmentCalendarView.vue | 18 +- .../AppointmentCreatePanel.test.ts | 4 +- .../AppointmentCreatePanel.vue | 39 +- .../WorkCreateAppointmentForm.vue | 21 +- .../WorkDispatchForm.test.ts | 4 +- .../appointment-calendar/WorkDispatchForm.vue | 17 +- .../WorkExistingDispatchForm.test.ts | 2 +- .../WorkExistingDispatchForm.vue | 3 +- .../components/CustomerAuditLogsPanel.test.ts | 124 +++- .../components/CustomerAuditLogsPanel.vue | 108 ++- .../components/ContainerCustomsPanel.test.ts | 50 +- .../components/ContainerCustomsPanel.vue | 32 +- .../ContainerLastLegFeesPanel.test.ts | 65 +- .../components/ContainerLastLegFeesPanel.vue | 41 +- .../ContainerSimulationCreate.test.ts | 43 ++ .../components/ContainerSimulationCreate.vue | 9 +- .../components/OceanBookingLinkDialog.test.ts | 8 + .../components/OceanBookingLinkDialog.vue | 23 +- .../components/SmartLoadingDialog.test.ts | 2 + .../components/SmartLoadingDialog.vue | 5 +- .../containerDialogTitle.test.ts | 16 + .../container-loading/containerDialogTitle.ts | 6 + ...nerDetailView.emptyReturn.behavior.test.ts | 40 +- .../pages/ContainerDetailView.test.ts | 5 +- .../pages/ContainerDetailView.vue | 51 +- .../pages/ContainersView.test.ts | 32 +- .../pages/ContainersView.vue | 57 +- .../pages/GlobalSmartLoadingView.test.ts | 67 +- .../pages/GlobalSmartLoadingView.vue | 95 ++- .../pages/SmartLoadingWorkbenchView.test.ts | 21 +- .../pages/SmartLoadingWorkbenchView.vue | 48 +- .../CustomerOverseasStorageFeePanel.vue | 37 +- .../CustomerBillingRulesPanel.test.ts | 55 +- .../components/CustomerBillingRulesPanel.vue | 46 +- src/modules/customers/customerDensity.css | 50 +- .../customers/customerDialogTitle.test.ts | 16 + src/modules/customers/customerDialogTitle.ts | 9 + .../customers/customerFeeDialogs.test.ts | 145 ++++ .../customers/overseasStorageFee.test.ts | 4 +- .../pages/CustomerDetailView.test.ts | 5 + .../customers/pages/CustomerDetailView.vue | 10 +- .../customers/pages/CustomersView.test.ts | 23 +- src/modules/customers/pages/CustomersView.vue | 214 +++--- .../pages/OverseasStorageFeesView.vue | 29 +- .../dashboard/pages/DashboardView.test.ts | 4 +- src/modules/dashboard/pages/DashboardView.vue | 6 +- .../pages/DomesticContainersView.test.ts | 92 ++- .../pages/DomesticContainersView.vue | 43 +- .../pages/DownloadCenterView.test.ts | 114 ++- .../downloads/pages/DownloadCenterView.vue | 56 +- .../pages/CarrierRateDetailView.vue | 2 +- .../carrier-rates/pages/CarrierRatesView.vue | 37 +- .../pages/PriceBookDialogs.test.ts | 161 ++++ .../fee-items/pages/FeeItemDialogs.test.ts | 155 ++++ .../fee-items/pages/FeeItemsView.test.ts | 3 +- src/modules/fee-items/pages/FeeItemsView.vue | 79 +- .../pages/FinanceAccountDialogs.test.ts | 289 ++++++++ .../pages/FinanceArapStatementDialogs.test.ts | 334 +++++++++ .../pages/FinanceCustomerAccountsView.vue | 98 ++- .../finance/pages/FinanceCustomerArapView.vue | 54 +- .../pages/FinanceCustomerStatementsView.vue | 68 +- .../FinanceFormalSettlementLayout.test.ts | 5 +- .../pages/FinanceFormalSettlementPage.vue | 101 ++- .../finance/pages/FinancePresentation.test.ts | 9 +- .../pages/FinanceSettlementDialogs.test.ts | 283 ++++++++ .../iam/pages/InternalUserDialogs.test.ts | 184 +++++ .../iam/pages/InternalUsersView.test.ts | 7 +- src/modules/iam/pages/InternalUsersView.vue | 129 ++-- .../pages/PermissionsDialog.behavior.test.ts | 8 +- src/modules/iam/pages/PermissionsView.vue | 39 +- .../iam/pages/RolePermissionDialogs.test.ts | 207 ++++++ src/modules/iam/pages/RolesView.test.ts | 4 +- src/modules/iam/pages/RolesView.vue | 45 +- .../components/InboundTaskList.test.ts | 4 + .../inbounds/components/InboundTaskList.vue | 22 +- .../components/InboundWorkflowWorkspace.vue | 2 - .../inbounds/fee/InboundFeePanel.test.ts | 6 +- src/modules/inbounds/fee/InboundFeePanel.vue | 9 +- .../measurement/InboundMeasurementPanel.vue | 61 +- .../measurement/measurementDensity.css | 6 +- .../inbounds/pages/InboundOrdersView.vue | 2 +- .../review/MeasurementReviewPanel.test.ts | 50 +- .../review/MeasurementReviewPanel.vue | 120 +-- .../WeightOcrPanel.behavior.test.ts | 11 + .../weight/components/WeightOcrPanel.vue | 87 +-- .../components/ContainerFeesPanel.test.ts | 54 +- .../components/ContainerFeesPanel.vue | 88 +-- .../components/LoadingListDetailDrawer.vue | 2 - .../components/PalletLabelPrintDialog.test.ts | 37 +- .../components/PalletLabelPrintDialog.vue | 22 +- .../PreloadPlanDetailView.dialogs.test.ts | 131 ++++ .../pages/PreloadPlanDetailView.test.ts | 4 +- .../pages/PreloadPlanDetailView.vue | 61 +- .../notifications/NotificationDialogs.test.ts | 271 +++++++ .../components/MessageComposerDialog.vue | 36 +- .../components/TemplateEditorDialog.vue | 28 +- .../notificationsDensity.behavior.test.ts | 5 +- .../pages/MessageManagementView.vue | 40 +- .../pages/MessageTemplatesView.vue | 2 +- .../pages/PlatformNotificationsView.vue | 36 +- .../OceanBookingDocumentsPanel.test.ts | 56 +- .../components/OceanBookingDocumentsPanel.vue | 11 +- .../OceanBookingPrintDialog.test.ts | 70 +- .../components/OceanBookingPrintDialog.vue | 60 +- .../OceanBookingDetailView.release.test.ts | 18 + .../pages/OceanBookingDetailView.test.ts | 2 +- .../pages/OceanBookingDetailView.vue | 19 +- .../components/FleetQuoteDialogs.test.ts | 173 +++++ .../components/FleetQuoteEditor.test.ts | 6 +- .../components/FleetQuoteEditor.vue | 72 +- .../components/FleetQuoteImport.vue | 34 +- .../pages/ExchangeRatesView.vue | 13 +- .../pages/FleetQuotesView.vue | 2 +- .../pages/OverseasQuotesView.vue | 63 +- .../pages/PricingDialogs.test.ts | 196 +++++ .../pages/TripDetailView.vue | 4 +- .../overseas-warehouse/api/dispatches.test.ts | 5 +- .../overseas-warehouse/api/dispatches.ts | 1 - .../components/DeliveryDocumentsDialog.vue | 44 +- .../components/DispatchArrangeFields.test.ts | 2 +- .../components/DispatchArrangeFields.vue | 43 +- .../components/DispatchEvidenceFiles.vue | 22 +- .../components/DispatchResourceFields.test.ts | 12 +- .../components/DispatchResourceFields.vue | 20 +- .../DispatchWorkflowDialog.layout.test.ts | 217 ++++++ .../components/DispatchWorkflowDialog.test.ts | 24 +- .../components/DispatchWorkflowDialog.vue | 101 ++- .../components/FbaAppointmentFields.vue | 26 +- .../components/RelabelDocumentsPanel.vue | 2 +- .../SmartDispatchDialog.layout.test.ts | 204 ++++++ .../components/SmartDispatchDialog.test.ts | 29 +- .../components/SmartDispatchDialog.vue | 88 ++- .../WarehouseTransferFulfillmentPanel.test.ts | 75 +- .../WarehouseTransferFulfillmentPanel.vue | 31 +- .../WarehouseTransferReceiptPanel.test.ts | 163 ++++- .../WarehouseTransferReceiptPanel.vue | 50 +- .../WarehouseTransferReviewDialog.test.ts | 3 +- .../WarehouseTransferReviewDialog.vue | 4 +- .../overseas-warehouse/dispatchContracts.ts | 56 +- .../dispatchSelectionGroups.test.ts | 43 ++ .../dispatchTestFixtures.ts | 9 +- .../overseas-warehouse/dispatchWire.test.ts | 15 +- .../overseas-warehouse/dispatchWire.ts | 23 +- src/modules/overseas-warehouse/index.ts | 2 +- .../layoutAcceptance.test.ts | 18 +- .../pages/CargoDiscardDialogs.test.ts | 173 +++++ .../pages/CargoDiscardsView.vue | 205 +++++- .../pages/DeliveryDialogs.test.ts | 172 +++++ .../pages/DeliveryOrdersView.test.ts | 32 +- .../pages/DeliveryOrdersView.vue | 29 +- .../pages/DispatchDetailView.test.ts | 8 +- .../pages/DispatchDetailView.vue | 2 +- .../pages/InboundDialogs.behavior.test.ts | 239 ++++++ .../pages/InboundOrderDetailView.vue | 32 +- .../InboundOrdersView.appointment.test.ts | 2 +- .../pages/InboundOrdersView.vue | 25 +- .../pages/InventoryTransactionsView.vue | 3 +- .../pages/LastMileOrderDetailView.test.ts | 72 +- .../pages/LastMileOrderDetailView.vue | 31 +- .../pages/LastMileOrdersView.behavior.test.ts | 136 +++- .../pages/LastMileOrdersView.vue | 193 ++--- .../pages/LocationDialogs.test.ts | 153 ++++ .../pages/LocationsView.vue | 131 +++- .../pages/OutboundDialogs.test.ts | 147 ++++ .../pages/OutboundsView.vue | 186 +++-- .../pages/PalletDialogs.test.ts | 138 ++++ .../overseas-warehouse/pages/PalletsView.vue | 71 +- .../pages/RelabelDialogs.test.ts | 167 +++++ .../pages/RelabelOrderDetailView.test.ts | 3 +- .../pages/RelabelOrderDetailView.vue | 63 +- .../pages/RelabelOrdersView.test.ts | 23 +- .../pages/RelabelOrdersView.vue | 74 +- .../pages/ReturnInventoryDialogs.test.ts | 179 +++++ .../ReturnInventoryManagementView.test.ts | 5 +- .../pages/ReturnInventoryManagementView.vue | 686 +++++++----------- .../pages/prototypeContract.test.ts | 14 +- .../pages/realApiMount.test.ts | 2 +- ...CarrierScheduleQueryPanel.behavior.test.ts | 13 +- .../components/CarrierScheduleQueryPanel.vue | 24 +- .../ContainerPortSchedulePanel.test.ts | 29 +- .../components/ContainerPortSchedulePanel.vue | 73 +- .../pages/SailingCalendarView.test.ts | 115 ++- .../pages/SailingCalendarView.vue | 46 +- .../self-operated/components/SkuTrend.test.ts | 7 +- .../self-operated/components/SkuTrend.vue | 4 +- .../components/SupplierDialogs.test.ts | 166 +++++ .../components/SupplierLicenseDialog.vue | 41 +- .../suppliers/pages/SupplierDetailView.vue | 1 + src/modules/suppliers/pages/SuppliersView.vue | 3 +- .../components/TruckAttachmentDialog.test.ts | 62 +- .../components/TruckAttachmentDialog.vue | 19 +- .../pages/TruckAppointmentDetailView.vue | 4 +- .../components/InboundFeeDetailManager.vue | 47 +- .../OrderDeclarationEditorDialog.test.ts | 9 + .../OrderDeclarationEditorDialog.vue | 124 ++-- .../pages/UnifiedOrdersView.test.ts | 4 +- .../pages/UnifiedOrdersView.vue | 197 +++-- .../unified-orders/styles/orderDensity.css | 2 +- .../locations/pages/LocationsView.test.ts | 1 + .../locations/pages/LocationsView.vue | 10 +- .../pallets/pages/PalletsView.test.ts | 3 +- .../pallets/pages/PalletsView.vue | 10 +- .../warehouses/pages/WarehousesView.test.ts | 198 ++++- .../warehouses/pages/WarehousesView.vue | 172 ++--- ...boundCodeRelationshipDialog.native.test.ts | 113 +++ .../InboundCodeRelationshipDialog.test.ts | 6 +- .../InboundCodeRelationshipDialog.vue | 102 ++- src/shared/components/InlineHelp.test.ts | 56 ++ src/shared/components/InlineHelp.vue | 21 + src/styles/adminDensity.css | 64 +- src/styles/adminDensity.test.ts | 655 ++++++++++++++++- src/styles/adminDensityTokens.css | 7 + 214 files changed, 10931 insertions(+), 2654 deletions(-) create mode 100644 src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts create mode 100644 src/modules/container-loading/components/ContainerSimulationCreate.test.ts create mode 100644 src/modules/container-loading/containerDialogTitle.test.ts create mode 100644 src/modules/container-loading/containerDialogTitle.ts create mode 100644 src/modules/customers/customerDialogTitle.test.ts create mode 100644 src/modules/customers/customerDialogTitle.ts create mode 100644 src/modules/customers/customerFeeDialogs.test.ts create mode 100644 src/modules/fba-pricing/carrier-rates/pages/PriceBookDialogs.test.ts create mode 100644 src/modules/fee-items/pages/FeeItemDialogs.test.ts create mode 100644 src/modules/finance/pages/FinanceAccountDialogs.test.ts create mode 100644 src/modules/finance/pages/FinanceArapStatementDialogs.test.ts create mode 100644 src/modules/finance/pages/FinanceSettlementDialogs.test.ts create mode 100644 src/modules/iam/pages/InternalUserDialogs.test.ts create mode 100644 src/modules/iam/pages/RolePermissionDialogs.test.ts create mode 100644 src/modules/loading-lists/pages/PreloadPlanDetailView.dialogs.test.ts create mode 100644 src/modules/notifications/NotificationDialogs.test.ts create mode 100644 src/modules/overseas-master-data/components/FleetQuoteDialogs.test.ts create mode 100644 src/modules/overseas-master-data/pages/PricingDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchWorkflowDialog.layout.test.ts create mode 100644 src/modules/overseas-warehouse/components/SmartDispatchDialog.layout.test.ts create mode 100644 src/modules/overseas-warehouse/dispatchSelectionGroups.test.ts create mode 100644 src/modules/overseas-warehouse/pages/CargoDiscardDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/DeliveryDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/InboundDialogs.behavior.test.ts create mode 100644 src/modules/overseas-warehouse/pages/LocationDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/OutboundDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/PalletDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/RelabelDialogs.test.ts create mode 100644 src/modules/overseas-warehouse/pages/ReturnInventoryDialogs.test.ts create mode 100644 src/modules/suppliers/components/SupplierDialogs.test.ts create mode 100644 src/shared/components/InboundCodeRelationshipDialog.native.test.ts create mode 100644 src/shared/components/InlineHelp.test.ts create mode 100644 src/shared/components/InlineHelp.vue diff --git a/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts new file mode 100644 index 00000000..753fb4cd --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentCalendarDialogs.native.test.ts @@ -0,0 +1,135 @@ +import ElementPlus from 'element-plus' +import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import AppDialog from '@/shared/components/AppDialog.vue' +import LineClampText from '@/shared/components/LineClampText.vue' +import AppointmentCalendarView from './AppointmentCalendarView.vue' +import { getCalendarSources, getWarehouseArrivals, saveWarehouseArrival, type WarehouseArrival } from './scheduleApi' +import { listTripCalendar } from './tripCalendar' +import { createSourceAppointment, listPlanningCargo, previewPlanningBatch, resolvePlanningSource, type PlanningCargo, type PlanningBatchPreview } from './planningApi' +import { batchContext } from './batch.test-support' +import type { ScheduleEvent } from './schedule' +import { getWorkDetail, type WorkAction } from './workApi' +import { workDetail, workItem } from './work.test-support' + +const routing = vi.hoisted(() => ({ query: { date: '2026-10-19' } as Record, push: vi.fn(), replace: vi.fn(), denied: new Set() })) +vi.mock('vue-router', () => ({ useRoute: () => routing, useRouter: () => routing, onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => !routing.denied.has(permission) }) })) +vi.mock('./scheduleApi', () => ({ getCalendarSources: vi.fn(), getWarehouseArrivals: vi.fn(), saveWarehouseArrival: vi.fn() })) +vi.mock('./tripCalendar', async original => ({ ...await original(), listTripCalendar: vi.fn() })) +vi.mock('./planningApi', async original => ({ ...await original(), listPlanningCargo: vi.fn(), previewPlanningBatch: vi.fn(), resolvePlanningSource: vi.fn(), createSourceAppointment: vi.fn() })) +vi.mock('./workApi', async original => ({ ...await original(), getWorkDetail: vi.fn() })) +enableAutoUnmount(afterEach) +afterEach(() => { vi.unstubAllGlobals() }) +const warehouse: WarehouseArrival = { sourceContainerBookingId: '9007199254740999', containerNo: 'TEST-CONT', warehouseName: '美国仓', warehouseCode: 'US-W', plannedAt: null, actualAt: null, version: 3, planCount: 0, canEdit: true } +const event: ScheduleEvent = { key: 'warehouse-1', type: 'warehouse', date: '2026-10-21', time: '', title: 'TEST-CONT 计划到仓', fields: [], warehouse, canEdit: true } +const cargo: PlanningCargo = { sourceKey: 'ORDER:9007199254740998', orderId: '9007199254740998', customerId: '9007199254740997', customerName: '预约长客户名称'.repeat(12), inboundCode: 'IN-TEST', containerBookingId: 'container-1', containerNo: 'TEST-CONT', originWarehouseId: 'origin-1', originWarehouseCode: 'US-W', originWarehouseName: '美国长仓库名称'.repeat(12), destinationWarehouseCode: 'ONT8', packages: 20, receivedPackages: 0, shippedPackages: 0, sourcePalletCount: null, volumeCbm: null, estimatedArrivalAt: null, estimatedWarehouseAt: null, orderVersion: 1, standardNode: 'DEPARTED', shippingVersion: null, shippingObservedAt: null, shippingProvider: null, hasInboundPlan: false, hasActiveAppointment: false, detained: false, mixedDestinations: false } +const source = { sourceKey: cargo.sourceKey, customerId: cargo.customerId, orderId: cargo.orderId, inboundCode: cargo.inboundCode, bookingNo: null, containerNo: cargo.containerNo, originWarehouseId: cargo.originWarehouseId, originWarehouseName: cargo.originWarehouseName, destinationKey: 'FBA:LAX9', destinationWarehouseCode: 'LAX9', packages: 20, sourceVersion: 'v1', canFollowup: true, blockReasons: [] } +const preview: PlanningBatchPreview = { group: { originWarehouseId: cargo.originWarehouseId, destinationKey: source.destinationKey, packages: 20, sourceCount: 1, customerCount: 1, readinessIncomplete: true, readyFrom: null, readyTo: null, unknownQuantitySources: 0 }, originWarehouseName: cargo.originWarehouseName, destinationWarehouseCode: 'LAX9', destinationTimezone: 'America/Los_Angeles', sources: [source], missingFields: [] } +beforeEach(() => { + vi.clearAllMocks(); routing.denied.clear(); routing.query = { date: '2026-10-19' } + vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) + vi.mocked(getCalendarSources).mockResolvedValue({ containers: [], emptyReturns: [], emptyReturnsReadable: true }) + vi.mocked(getWarehouseArrivals).mockResolvedValue([]) + vi.mocked(listTripCalendar).mockResolvedValue({ records: [], unresolvedRecords: [] }) + vi.mocked(saveWarehouseArrival).mockResolvedValue({}) + vi.mocked(listPlanningCargo).mockResolvedValue({ records: [cargo], total: 1, page: 1, size: 20, pages: 1 }) + vi.mocked(resolvePlanningSource).mockResolvedValue({ source, coverage: { status: 'NONE', missingFields: [], changeReviewRequired: false, appointments: [] } }) + vi.mocked(previewPlanningBatch).mockResolvedValue(preview) + vi.mocked(createSourceAppointment).mockResolvedValue(batchContext().appointment) + vi.mocked(getWorkDetail).mockResolvedValue(workDetail()) +}) +async function open() { + const wrapper = mount(AppointmentCalendarView, { global: { plugins: [ElementPlus], stubs: { + AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', template: '
' }, + } } }) + await flushPromises() + return wrapper +} +async function create(wrapper: Awaited>) { + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '新增FBA送仓预约')! + const content = creationBody() + await content.get('input[type="checkbox"]').setValue(true) + await content.findAll('button').find(button => button.text().startsWith('下一步'))!.trigger('click'); await flushPromises() + return dialog +} +function creationBody() { return new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('新增FBA送仓预约'))!) } +it('keeps the arrival context and typed local time across native fullscreen and submits the original version', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + expect(dialog.props('width')).toBe('520px') + expect(dialog.get('[aria-label="到仓业务信息"]').text()).toContain('TEST-CONT 计划到仓') + expect(dialog.text()).not.toContain(warehouse.sourceContainerBookingId) + await dialog.get('input[type="datetime-local"]').setValue('2026-10-22T09:30') + await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect((dialog.get('input').element as HTMLInputElement).value).toBe('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(saveWarehouseArrival).toHaveBeenCalledExactlyOnceWith(warehouse.sourceContainerBookingId, { estimatedWarehouseArrivalAt: '2026-10-22T09:30:00', expectedVersion: 3 }, expect.any(String)) +}) +it('locks the real arrival input after an unknown outcome and reuses its immutable request', async () => { + vi.mocked(saveWarehouseArrival).mockRejectedValueOnce(new Error('connection lost')) + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('edit', event); await flushPromises() + const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '调整计划到仓')! + await dialog.get('input').setValue('2026-10-22T09:30') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(dialog.get('input').attributes('disabled')).toBeDefined() + expect(dialog.text()).toContain('上次提交结果尚未确认') + expect(dialog.get('.el-dialog__footer .el-button--primary').text()).toBe('重试确认保存结果') + await dialog.get('.el-dialog__footer .el-button--primary').trigger('click'); await flushPromises() + expect(vi.mocked(saveWarehouseArrival).mock.calls[1]).toEqual(vi.mocked(saveWarehouseArrival).mock.calls[0]) +}) +it('keeps complete long cargo names accessible in the selectable table', async () => { + const wrapper = await open() + wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await vi.dynamicImportSettled(); await flushPromises() + const names = wrapper.findAllComponents(LineClampText).map(item => item.props('content')) + expect(names).toContain(cargo.customerName); expect(names).toContain(cargo.originWarehouseName) + expect(wrapper.getComponent({ name: 'DataTable' }).props('maxHeight')).toBe('') + expect(wrapper.text()).not.toMatch(/900719925474099[7-9]/) + expect(resolvePlanningSource).not.toHaveBeenCalled() +}) +it('preserves real FBA fields and entered values when expanding the create dialog', async () => { + const wrapper = await open(); const dialog = await create(wrapper) + const fields = dialog.getComponent({ name: 'FbaAppointmentFields' }) + expect(fields.props('continuous')).toBe(true) + const date = fields.findAllComponents({ name: 'ElDatePicker' })[0]! + date.vm.$emit('update:modelValue', '2026-10-25'); await flushPromises() + await creationBody().get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() + expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true) + expect(date.props('modelValue')).toBe('2026-10-25') + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: 'FBA:LAX9' }], details: expect.objectContaining({ plannedDate: '2026-10-25', plannedAt: null, appointmentStatus: 'PENDING' }), idempotencyKey: expect.any(String) })) + expect(creationBody().get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) + expect(creationBody().text()).toContain('AP-001') +}) +it('shows every saved server field as readonly text including confirmed offsets and long remarks', async () => { + const saved = { ...batchContext().appointment, plannedDate: '2026-10-26', plannedAt: '2026-10-26T08:00:00', plannedEndAt: '2026-10-26T18:00:00', timezone: 'America/Los_Angeles', appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-001', appointmentAt: '2026-11-01T01:30:00', appointmentEndAt: '2026-11-01T01:45:00', appointmentStartUtc: '2026-11-01T08:30:00Z', appointmentEndUtc: '2026-11-01T09:45:00Z', appointmentRemark: '完整预约备注'.repeat(30) } + vi.mocked(createSourceAppointment).mockResolvedValue(saved) + const wrapper = await open(); await create(wrapper) + await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + const receipt = creationBody().get('[aria-label="预约保存回执"]') + for (const text of ['AP-001', saved.plannedDate, saved.plannedAt, saved.plannedEndAt, saved.timezone, '已取得对方正式确认', saved.confirmationNo, saved.appointmentAt, saved.appointmentEndAt, '-07:00', '-08:00', saved.appointmentRemark]) expect(receipt.text()).toContain(text) + expect(receipt.findAll('input, textarea, select')).toHaveLength(0) + expect(receipt.text()).not.toMatch(/item-1|customer-a|FBA_APPOINTMENT:1/) +}) +it('does not open or request creation data without its original write permission', async () => { + routing.denied.add('platform:overseas-warehouse:inbound-order:fba-appointment') + const wrapper = await open(); wrapper.getComponent({ name: 'AppointmentCalendarPanel' }).vm.$emit('create'); await flushPromises() + expect(listPlanningCargo).not.toHaveBeenCalled(); expect(createSourceAppointment).not.toHaveBeenCalled() + expect(wrapper.findAllComponents(AppDialog).some(dialog => dialog.props('title') === '新增FBA送仓预约')).toBe(false) +}) +it('uses the same real continuous form through the original work detail deep link', async () => { + routing.query.workItem = 'work-delivery-1' + const action: WorkAction = { code: 'SAVE_PLAN', label: '安排预约', targetWorkItemKey: null, target: { kind: 'FBA_APPOINTMENT', appointmentId: null, batchKey: null, sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }] } } + vi.mocked(getWorkDetail).mockResolvedValue(workDetail({ item: workItem({ allowedActions: [action], nextAction: action }) })) + const wrapper = await open(); await vi.dynamicImportSettled(); await flushPromises() + const content = new DOMWrapper(Array.from(document.querySelectorAll('.el-dialog')).find(item => item.textContent?.includes('原业务预约办理'))!) + await content.get('.primary-next-action').trigger('click'); await flushPromises() + expect(wrapper.getComponent({ name: 'FbaAppointmentFields' }).props('continuous')).toBe(true) + expect(previewPlanningBatch).toHaveBeenCalledExactlyOnceWith(action.target.kind === 'FBA_APPOINTMENT' ? action.target.sources : []) + await content.findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises() + expect(createSourceAppointment).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sources: [{ sourceKey: source.sourceKey, customerId: source.customerId, destinationKey: source.destinationKey }], details: expect.objectContaining({ plannedAt: null, appointmentStatus: 'PENDING' }) })) + expect(content.text()).toContain('本次保存已完成') +}) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts index ac11323b..98e7c540 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarView.test.ts @@ -21,6 +21,9 @@ export function renderSchedule() { AppointmentCalendarPanel: { name: 'AppointmentCalendarPanel', props: ['events', 'loading', 'incomplete', 'initialDate', 'initialMode', 'canCreate'], template: '
' }, AppDialog: { name: 'AppDialog', props: ['modelValue', 'beforeClose', 'confirmDisabled', 'confirmLoading'], emits: ['confirm'], template: '
' }, WorkDetailPanel: true, SourceAppointmentPanel: true, ApiErrorMeta: true, + ElForm: { template: '
' }, + ElFormItem: { props: ['label'], template: '' }, + ElInput: { props: ['modelValue', 'disabled'], emits: ['update:modelValue'], template: '' }, AppointmentCreatePanel: { name: 'AppointmentCreatePanel', props: ['canWrite', 'canLeave'], emits: ['created'], template: '
' }, LoadErrorState: { props: ['title', 'message'], emits: ['retry'], template: '
{{ title }} {{ message }}
' }, } } }) diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue index 5c58b0c9..044a7ede 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -175,15 +175,21 @@ onBeforeUnmount(() => { ++generation; controller?.abort() }) - - + 返回原事项 @@ -194,7 +200,5 @@ onBeforeUnmount(() => { ++generation; controller?.abort() }) diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts index 134fac61..302e91a2 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.test.ts @@ -89,7 +89,9 @@ it('submits the real form, shows the saved appointment, and prevents another sub expect(wrapper.emitted('created')).toEqual([[batchContext().appointment]]) expect(wrapper.get('[role="status"]').text()).toContain(batchContext().appointment.appointmentNo) expect(wrapper.findAll('button').some(button => button.text() === '保存预约安排')).toBe(false) - expect(wrapper.get('[data-date]').attributes('disabled')).toBeDefined() + expect(wrapper.find('[data-date]').exists()).toBe(false) + expect(wrapper.get('[aria-label="预约保存回执"]').text()).toContain('等待对方确认') + expect(wrapper.get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0) wrapper.unmount() }) it('preserves the original request and blocks returning or closing after an unknown creation result', async () => { diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.vue b/src/modules/appointment-calendar/AppointmentCreatePanel.vue index dce81716..236414f9 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.vue +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.vue @@ -1,11 +1,13 @@ diff --git a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue index 9ad0cf9c..249802e7 100644 --- a/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue +++ b/src/modules/appointment-calendar/WorkCreateAppointmentForm.vue @@ -44,18 +44,23 @@ async function save() { onMounted(load) diff --git a/src/modules/appointment-calendar/WorkDispatchForm.test.ts b/src/modules/appointment-calendar/WorkDispatchForm.test.ts index a36bb194..847c5f69 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.test.ts +++ b/src/modules/appointment-calendar/WorkDispatchForm.test.ts @@ -18,10 +18,10 @@ const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T const batch = 'FBA_APPOINTMENT:1' function receiving(key = batch, vehicles: ReceivingContext['transport']['vehicles'] = []): ReceivingContext { return { batch: batchContext('AP-1', key).appointment, plans: [], agreements: [], pendingChanges: [], transport: { version: 'transport', vehicles, blockReasons: [] }, executionScope: { version: 'execution', members: [] }, allowedActions: [], blockReasons: [] } } function row(id = '9007199254740993', customerId = 'customer-a', key = batch): BatchDispatchCandidate { return { planPalletId: id, palletNo: `PALLET-${id.slice(-2)}`, planId: `plan-${id}`, planNo: 'IP-001', customerId, sourceStage: 'RECEIVED', packageCount: 12, allocations: [{ planPalletId: id, batchKey: key, customerId, sourceKey: customerId === 'customer-a' ? 'ORDER:1' : 'BOOKING:2', destinationKey: 'FBA:ONT8', packageCount: 12, recordVersion: 4 }], blockReasons: [] } } -function candidate(value = row()): Candidate { return { selection: { sourceType: 'INBOUND_PLAN', planPalletId: value.planPalletId, returnReceiptId: null, relabelNo: null, warehousePalletId: `pallet-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}` }, sourceKey: `cargo-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}`, customerId: value.customerId, customerName: value.customerId === 'customer-a' ? '客户甲' : '客户乙', warehouseId: 'origin-1', warehouseCode: 'LAX', warehouseName: '真实起运仓', orderNo: 'LM-1', inboundCode: 'IN-A', planNo: value.planNo, palletNo: value.palletNo, containerNo: null, destination: { key: 'CODE:ONT8', type: 'FBA', warehouseId: null, warehouseCode: 'ONT8', warehouseName: 'ONT8', address: null, postalCode: null, contactName: null, contactPhone: null }, availablePackages: 12, suggestedWeightKg: null, suggestedVolumeCbm: null, measurementSource: null, appointmentBatches: [], blockReasons: [] } } +function candidate(value = row()): Candidate { const base = { selection: { sourceType: 'INBOUND_PLAN', planPalletId: value.planPalletId, returnReceiptId: null, relabelNo: null, warehousePalletId: `pallet-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}` }, sourceKey: `cargo-${value.planPalletId}`, sourceVersion: `source-${value.planPalletId}`, customerId: value.customerId, customerName: value.customerId === 'customer-a' ? '客户甲' : '客户乙', warehouseId: 'origin-1', warehouseCode: 'LAX', warehouseName: '真实起运仓', orderNo: 'LM-1', inboundCode: 'IN-A', planNo: value.planNo, palletNo: value.palletNo, containerNo: null, destination: { key: 'CODE:ONT8', type: 'FBA', warehouseId: null, warehouseCode: 'ONT8', warehouseName: 'ONT8', address: null, postalCode: null, contactName: null, contactPhone: null }, availablePackages: 12, suggestedWeightKg: null, suggestedVolumeCbm: null, measurementSource: null, appointmentBatches: [], blockReasons: [] }; return { ...base, selection: { ...base.selection, sourceType: 'INBOUND_PLAN' }, selectionGroup: { groupKey: base.sourceKey, groupVersion: 'group-1', sources: [{ selection: { ...base.selection, sourceType: 'INBOUND_PLAN' }, sourceKey: base.sourceKey, customerId: base.customerId, customerName: base.customerName, sourceBusinessType: null, sourceBusinessNo: null, packages: base.availablePackages }], pallets: [{ warehousePalletId: base.selection.warehousePalletId, palletNo: base.palletNo, sourceKeys: [base.sourceKey], packageCount: base.availablePackages }], complete: true, blockReasons: [] } } } function detail(no = 'DS/001', version = 1): DispatchDetail { const review = { status: 'NOT_SUBMITTED' as const, reason: null, reviewerId: null, reviewedAt: null, reviewRound: 0, basisHash: null, invalidatedReason: null } - return { dispatchNo: no, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: version, reviewState: 'DRAFT', reviews: { dispatchReview: review, loadingReview: review }, counts: { itemCount: 1, palletCount: 1, packageCount: 12, pickedPackageCount: 0, shippedPackageCount: 0 }, pickingReconciled: false, actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, ADD_CARGO: { allowed: true, blockCode: null, blockReason: null } }, vehicleInfo: { vehicle: null, driver: null }, dispatchInfo: { route: null, plannedDepartureAt: null, departedAt: null, estimatedArrivalAt: null, fbaAppointments: [], vehicleAppointment: null, appointmentBatches: [{ batchKey: batch, batchNo: 'AP-1', recordVersion: 4, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 12 }] }] }, documents: { dispatchNo: no, outbounds: [], deliveries: [], createdAt: '', updatedAt: '', remark: null }, orderDetails: [] } + return { dispatchNo: no, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: version, reviewState: 'DRAFT', reviews: { dispatchReview: review, loadingReview: review }, counts: { itemCount: 1, palletCount: 1, packageCount: 12, pickedPackageCount: 0, shippedPackageCount: 0 }, pickingReconciled: false, actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, ADD_CARGO: { allowed: true, blockCode: null, blockReason: null } }, vehicleInfo: { vehicle: null, driver: null }, dispatchInfo: { route: null, plannedDepartureAt: null, departedAt: null, estimatedArrivalAt: null, fbaAppointments: [], appointmentBatches: [{ batchKey: batch, batchNo: 'AP-1', recordVersion: 4, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 12 }] }] }, documents: { dispatchNo: no, outbounds: [], deliveries: [], createdAt: '', updatedAt: '', remark: null }, orderDetails: [] } } function proof(key: string): CommandResult { return { dispatchNo: 'DS/001', action: 'CREATE', idempotencyKey: key, recordVersion: 1, status: 'ACTIVE', stage: 'BEFORE_LOADING', occurredAt: '2026-10-19T00:00:00Z', effect: 'APPLIED', counts: detail().counts, result: { dispatchNo: 'DS/001', changedItemIds: [] }, replayed: false, alreadyArranged: false } } let guard: LeaveGuard, natives: Candidate[], draft: DispatchDraft diff --git a/src/modules/appointment-calendar/WorkDispatchForm.vue b/src/modules/appointment-calendar/WorkDispatchForm.vue index 8786a0a1..2f23dfb9 100644 --- a/src/modules/appointment-calendar/WorkDispatchForm.vue +++ b/src/modules/appointment-calendar/WorkDispatchForm.vue @@ -1,9 +1,10 @@