From b769d2479c2906b5fbc8cce46260d1e462c5eef9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 15 Oct 2026 17:11:03 +0800 Subject: [PATCH 1/4] feat(appointment): integrate work calendar and original business actions --- .../AppointmentBatchForm.test.ts | 73 ++ .../AppointmentBatchForm.vue | 108 +++ .../AppointmentCalendarView.test.ts | 470 +++---------- .../AppointmentCalendarView.vue | 646 +++++------------- .../CargoVerificationForm.test.ts | 54 ++ .../CargoVerificationForm.vue | 74 ++ .../appointment-calendar/PROTOTYPE_MATRIX.md | 62 +- .../SourceAppointmentPanel.test.ts | 25 + .../SourceAppointmentPanel.vue | 41 ++ .../WorkAppointmentForm.test.ts | 27 + .../WorkAppointmentForm.vue | 65 ++ .../WorkCalendarPanel.test.ts | 29 + .../WorkCalendarPanel.vue | 61 ++ .../WorkCommandForm.test.ts | 109 +++ .../appointment-calendar/WorkCommandForm.vue | 104 +++ .../WorkCreateAppointmentForm.test.ts | 46 ++ .../WorkCreateAppointmentForm.vue | 60 ++ .../WorkDetailPanel.test.ts | 88 +++ .../appointment-calendar/WorkDetailPanel.vue | 116 ++++ .../WorkDispatchForm.test.ts | 63 ++ .../appointment-calendar/WorkDispatchForm.vue | 92 +++ .../WorkExistingDispatchForm.test.ts | 29 + .../WorkExistingDispatchForm.vue | 38 ++ .../WorkReceiptPlans.test.ts | 49 ++ .../appointment-calendar/WorkReceiptPlans.vue | 53 ++ src/modules/appointment-calendar/api.ts | 2 +- .../batch.test-support.ts | 10 + .../appointment-calendar/batchApi.test.ts | 41 ++ src/modules/appointment-calendar/batchApi.ts | 59 ++ .../cargoVerificationApi.ts | 13 + src/modules/appointment-calendar/index.ts | 1 + .../appointment-calendar/planningApi.test.ts | 9 +- .../appointment-calendar/planningApi.ts | 6 + .../appointment-calendar/tripCalendar.test.ts | 21 +- .../appointment-calendar/tripCalendar.ts | 11 +- .../appointment-calendar/useWorkDraft.ts | 21 + .../appointment-calendar/work.test-support.ts | 8 + .../appointment-calendar/workApi.test.ts | 50 ++ src/modules/appointment-calendar/workApi.ts | 88 +++ .../workNavigation.test.ts | 13 + .../appointment-calendar/workNavigation.ts | 22 + .../workPresentation.test.ts | 44 ++ .../appointment-calendar/workPresentation.ts | 61 ++ .../api/terminalDrayages.test.ts | 17 + .../container-loading/api/terminalDrayages.ts | 35 +- ...nerDetailView.emptyReturn.behavior.test.ts | 102 ++- .../pages/ContainerDetailView.vue | 212 +++++- .../pages/TripDetailView.vue | 3 +- .../pages/TripsBehavior.test.ts | 13 +- .../overseas-master-data/pages/TripsView.vue | 9 +- .../overseas-warehouse/api/dispatches.test.ts | 12 +- .../overseas-warehouse/api/dispatches.ts | 8 + .../overseas-warehouse/api/inboundOrders.ts | 6 +- src/modules/overseas-warehouse/index.ts | 5 + .../pages/DispatchesView.test.ts | 2 +- .../pages/DispatchesView.vue | 11 +- .../pages/InboundOrderDetailView.vue | 5 +- .../InboundOrdersView.appointment.test.ts | 24 +- .../pages/InboundOrdersView.vue | 19 +- 59 files changed, 2604 insertions(+), 941 deletions(-) create mode 100644 src/modules/appointment-calendar/AppointmentBatchForm.test.ts create mode 100644 src/modules/appointment-calendar/AppointmentBatchForm.vue create mode 100644 src/modules/appointment-calendar/CargoVerificationForm.test.ts create mode 100644 src/modules/appointment-calendar/CargoVerificationForm.vue create mode 100644 src/modules/appointment-calendar/SourceAppointmentPanel.test.ts create mode 100644 src/modules/appointment-calendar/SourceAppointmentPanel.vue create mode 100644 src/modules/appointment-calendar/WorkAppointmentForm.test.ts create mode 100644 src/modules/appointment-calendar/WorkAppointmentForm.vue create mode 100644 src/modules/appointment-calendar/WorkCalendarPanel.test.ts create mode 100644 src/modules/appointment-calendar/WorkCalendarPanel.vue create mode 100644 src/modules/appointment-calendar/WorkCommandForm.test.ts create mode 100644 src/modules/appointment-calendar/WorkCommandForm.vue create mode 100644 src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts create mode 100644 src/modules/appointment-calendar/WorkCreateAppointmentForm.vue create mode 100644 src/modules/appointment-calendar/WorkDetailPanel.test.ts create mode 100644 src/modules/appointment-calendar/WorkDetailPanel.vue create mode 100644 src/modules/appointment-calendar/WorkDispatchForm.test.ts create mode 100644 src/modules/appointment-calendar/WorkDispatchForm.vue create mode 100644 src/modules/appointment-calendar/WorkExistingDispatchForm.test.ts create mode 100644 src/modules/appointment-calendar/WorkExistingDispatchForm.vue create mode 100644 src/modules/appointment-calendar/WorkReceiptPlans.test.ts create mode 100644 src/modules/appointment-calendar/WorkReceiptPlans.vue create mode 100644 src/modules/appointment-calendar/batch.test-support.ts create mode 100644 src/modules/appointment-calendar/batchApi.test.ts create mode 100644 src/modules/appointment-calendar/batchApi.ts create mode 100644 src/modules/appointment-calendar/cargoVerificationApi.ts create mode 100644 src/modules/appointment-calendar/index.ts create mode 100644 src/modules/appointment-calendar/useWorkDraft.ts create mode 100644 src/modules/appointment-calendar/work.test-support.ts create mode 100644 src/modules/appointment-calendar/workApi.test.ts create mode 100644 src/modules/appointment-calendar/workApi.ts create mode 100644 src/modules/appointment-calendar/workNavigation.test.ts create mode 100644 src/modules/appointment-calendar/workNavigation.ts create mode 100644 src/modules/appointment-calendar/workPresentation.test.ts create mode 100644 src/modules/appointment-calendar/workPresentation.ts diff --git a/src/modules/appointment-calendar/AppointmentBatchForm.test.ts b/src/modules/appointment-calendar/AppointmentBatchForm.test.ts new file mode 100644 index 00000000..32a6df7b --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentBatchForm.test.ts @@ -0,0 +1,73 @@ +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, expect, it, vi } from 'vitest' +import AppointmentBatchForm from './AppointmentBatchForm.vue' +import { getAppointmentBatch, repartitionAppointments } from './batchApi' +import { getWorkItems } from './workApi' +import { ApiResponseError } from '@/shared/api/http' +import { batchContext } from './batch.test-support' +import { workItem } from './work.test-support' +import type { LeaveGuard } from './useWorkDraft' +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() }, ElMessageBox: { confirm: vi.fn().mockResolvedValue(true) } })) +vi.mock('./batchApi', async original => ({ ...await original(), getAppointmentBatch: vi.fn(), repartitionAppointments: vi.fn() })) +vi.mock('./workApi', async original => ({ ...await original(), getWorkItems: vi.fn() })) +let guard: LeaveGuard +function render() { return mount(AppointmentBatchForm, { props: { batchKey: 'FBA_APPOINTMENT:1' }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { + ElButton: { props: ['nativeType', 'disabled'], template: '' }, + LoadErrorState: { props: ['message'], template: '

{{ message }}

' }, ApiErrorMeta: true, CargoVerificationForm: true, +} } }) } +beforeEach(() => { + vi.clearAllMocks(); vi.mocked(getAppointmentBatch).mockResolvedValue(batchContext()) + vi.mocked(repartitionAppointments).mockResolvedValue({ replacedBatchKeys: ['FBA_APPOINTMENT:1'], replacedAppointmentNos: ['AP-001'], appointments: [batchContext('AP-NEW-1', 'FBA_APPOINTMENT:11').appointment, batchContext('AP-NEW-2', 'FBA_APPOINTMENT:12').appointment] }) + vi.mocked(getWorkItems).mockResolvedValue({ records: [workItem({ batchKey: 'FBA_APPOINTMENT:2' })], page: 1, size: 8, total: 1, pages: 1 }) +}) +it('submits two conservative allocations without fake confirmation times and renders replacement business numbers', async () => { + const wrapper = render(); await flushPromises() + await wrapper.get('[aria-label="来源1分出箱数"]').setValue(20) + await wrapper.findAll('textarea')[0]!.setValue('分两次送仓'); await wrapper.get('form').trigger('submit'); await flushPromises() + const request = vi.mocked(repartitionAppointments).mock.calls[0]![0] + expect(request.originals).toEqual([{ appointmentNo: 'AP-001', version: 3 }]) + expect(request.batches.map(row => row.plannedDate)).toEqual([null, null]) + expect(request.batches[0]!.allocations.map(row => row.packageCount)).toEqual([50, 30]) + expect(request.batches[1]!.allocations.map(row => row.packageCount)).toEqual([20]) + expect(request).not.toHaveProperty('appointmentAt') + expect(wrapper.text()).toContain('AP-NEW-1'); expect(wrapper.text()).not.toContain('FBA_APPOINTMENT:') + expect(wrapper.emitted('updated')).toHaveLength(1); wrapper.unmount() +}) +it('locks an unknown outcome and retries the same immutable repartition command', async () => { + vi.mocked(repartitionAppointments).mockRejectedValueOnce(new Error('unknown outcome')) + const wrapper = render(); await flushPromises() + await wrapper.get('[aria-label="来源1分出箱数"]').setValue(20); await wrapper.findAll('textarea')[0]!.setValue('分批') + await wrapper.get('form').trigger('submit'); await flushPromises() + const first = vi.mocked(repartitionAppointments).mock.calls[0]![0] + expect(await guard()).toBe(false); expect(wrapper.get('fieldset').attributes('disabled')).toBeDefined() + await wrapper.get('form').trigger('submit'); await flushPromises() + expect(repartitionAppointments).toHaveBeenNthCalledWith(2, first); wrapper.unmount() +}) +it('keeps edited allocations after a version conflict and requires a fresh explicit submit', async () => { + vi.mocked(repartitionAppointments).mockRejectedValueOnce(new ApiResponseError('版本变化', 'A0409', 409)) + const wrapper = render(); await flushPromises() + await wrapper.get('[aria-label="来源1分出箱数"]').setValue(20); await wrapper.findAll('textarea')[0]!.setValue('保留分批原因') + await wrapper.get('form').trigger('submit'); await flushPromises() + expect(getAppointmentBatch).toHaveBeenCalledTimes(2); expect(repartitionAppointments).toHaveBeenCalledTimes(1) + expect((wrapper.get('[aria-label="来源1分出箱数"]').element as HTMLInputElement).value).toBe('20') + expect((wrapper.findAll('textarea')[0]!.element as HTMLTextAreaElement).value).toBe('保留分批原因'); wrapper.unmount() +}) +it('shows full appointment cargo when confirmed or replaced batches have no repartition sources', async () => { + const context = batchContext(); context.sources = []; context.allowedActions = []; context.blockReasons = ['原批次已有正式确认']; context.appointment.appointmentStatus = 'SUCCESS' + vi.mocked(getAppointmentBatch).mockResolvedValue(context) + const wrapper = render(); await flushPromises() + expect(wrapper.get('[aria-label="原批次货物快照"]').text()).toContain('IN-A') + expect(wrapper.text()).toContain('FBA-B'); expect(wrapper.text()).toContain('当前批次无分批资格') + await wrapper.get('form').trigger('submit'); expect(repartitionAppointments).not.toHaveBeenCalled(); wrapper.unmount() +}) +it('loads a candidate context before merging and preserves both original versions', async () => { + const other = batchContext('AP-002', 'FBA_APPOINTMENT:2'); other.appointment.version = 6 + vi.mocked(getAppointmentBatch).mockImplementation(async key => key === 'FBA_APPOINTMENT:2' ? other : batchContext()) + const wrapper = render(); await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '同仓合并安排')!.trigger('click'); await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '核对并选择')!.trigger('click'); await flushPromises() + await wrapper.findAll('textarea')[0]!.setValue('同仓合并'); await wrapper.get('form').trigger('submit'); await flushPromises() + expect(repartitionAppointments).toHaveBeenCalledWith(expect.objectContaining({ originals: [{ appointmentNo: 'AP-001', version: 3 }, { appointmentNo: 'AP-002', version: 6 }], batches: [expect.objectContaining({ allocations: [expect.objectContaining({ customerId: 'customer-a', packageCount: 140 }), expect.objectContaining({ customerId: 'customer-b', packageCount: 60 })] })] })) + wrapper.unmount() +}) diff --git a/src/modules/appointment-calendar/AppointmentBatchForm.vue b/src/modules/appointment-calendar/AppointmentBatchForm.vue new file mode 100644 index 00000000..1c8d6d99 --- /dev/null +++ b/src/modules/appointment-calendar/AppointmentBatchForm.vue @@ -0,0 +1,108 @@ + +