diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts index 6f0d730e..67500466 100644 --- a/src/modules/overseas-warehouse/api/dispatches.test.ts +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches' +import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches' vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } })) @@ -38,6 +38,12 @@ describe('dispatch overseas API', () => { expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1') }) + it('requests the generated BOL as a PDF blob', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue(new Blob(['%PDF-test'])) + await getDispatchBolPdf('PC/1') + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/prints/bol', { responseType: 'blob' }) + }) + it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } }) diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index fc653ccf..72d8c9e0 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -145,6 +145,13 @@ export async function getDispatch(dispatchNo: string) { return unwrapApiResponse(response) } +export async function getDispatchBolPdf(dispatchNo: string) { + return overseasHttp.get( + `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/prints/bol`, + { responseType: 'blob' }, + ) +} + export async function updateDispatch(dispatchNo: string, payload: UpdateDispatchPayload) { const response = await overseasHttp.put, ApiEnvelope>( `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, payload, diff --git a/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.test.ts deleted file mode 100644 index 0074cc37..00000000 --- a/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { mount } from '@vue/test-utils' -import { afterEach, describe, expect, it, vi } from 'vitest' -import DispatchBolPrintDialog from './DispatchBolPrintDialog.vue' - -afterEach(() => { - document.body.classList.remove('printing-dispatch-bol') - vi.restoreAllMocks() -}) - -describe('DispatchBolPrintDialog', () => { - it('groups print sheets by FBA warehouse and totals each A4 sheet', () => { - const wrapper = mount(DispatchBolPrintDialog, { - props: { - modelValue: true, - detail: { - id: '1', dispatchNo: 'DSP-1', overseasWarehouseName: 'NOBLE LOGISTICS INC', carrierName: 'Harbor', - bolLines: [ - { containerNo: 'D2144', palletNo: 'D2144-112-1', fbaWarehouseCode: 'LBE1', warehouseNo: 'NPHK1', fbaShipmentId: 'FBA1', poReferenceNo: 'PO1', packageCount: 138 }, - { containerNo: 'D2144', palletNo: 'D2144-112-2', fbaWarehouseCode: 'LBE1', warehouseNo: 'NPHK2', fbaShipmentId: 'FBA2', poReferenceNo: 'PO2', packageCount: 2 }, - { containerNo: 'D2145', palletNo: 'D2145-113-1', fbaWarehouseCode: 'ONT8', warehouseNo: 'NPHK3', packageCount: 5 }, - ], - }, - }, - global: { - stubs: { - ElDialog: { template: '
' }, - ElButton: { template: '' }, - }, - }, - }) - - const sheets = wrapper.findAll('.dispatch-bol-sheet') - expect(sheets).toHaveLength(2) - expect(sheets[0]!.text()).toContain('LBE1') - expect(sheets[0]!.find('.bol-total').text()).toContain('140') - expect(sheets[1]!.find('.bol-total').text()).toContain('5') - }) - - it('opens the native browser print flow', async () => { - vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { callback(0); return 1 }) - const print = vi.spyOn(window, 'print').mockImplementation(() => {}) - const wrapper = mount(DispatchBolPrintDialog, { - props: { modelValue: true, detail: { id: '1', dispatchNo: 'DSP-1' } }, - global: { stubs: { ElDialog: { template: '
' }, ElButton: { template: '' } } }, - }) - - await wrapper.findAll('button').at(-1)!.trigger('click') - expect(print).toHaveBeenCalled() - expect(document.body.classList.contains('printing-dispatch-bol')).toBe(true) - }) -}) diff --git a/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.vue b/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.vue deleted file mode 100644 index 56519c9c..00000000 --- a/src/modules/overseas-warehouse/components/DispatchBolPrintDialog.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts index 8e61c52a..3a87a02d 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts +++ b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts @@ -4,12 +4,12 @@ import { resolve } from 'node:path' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import QueryToolbar, { type QueryModel } from '@/shared/components/QueryToolbar.vue' -import { cancelDispatch, getDispatch, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches' +import { cancelDispatch, getDispatchBolPdf, listDispatches, transferDispatchToDelivery, updateDispatch } from '../api/dispatches' import DispatchesView from './DispatchesView.vue' const canPermission = vi.hoisted(() => vi.fn(() => true)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) -vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatch: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() })) +vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatchBolPdf: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() })) class ResizeObserverStub implements ResizeObserver { observe() {} @@ -47,7 +47,7 @@ describe('DispatchesView', () => { page: 1, size: 20, total: 1, pages: 1, }) vi.mocked(updateDispatch).mockReset() - vi.mocked(getDispatch).mockReset().mockResolvedValue({ id: '190000000000000099', dispatchNo: 'PC-1', bolLines: [] }) + vi.mocked(getDispatchBolPdf).mockReset().mockResolvedValue(new Blob(['%PDF-test'], { type: 'application/pdf' })) vi.mocked(cancelDispatch).mockReset() vi.mocked(transferDispatchToDelivery).mockReset() canPermission.mockReset().mockReturnValue(true) @@ -85,17 +85,20 @@ describe('DispatchesView', () => { expect(wrapper.html()).toContain('190000000000000099') }) - it('loads one detail request only when BOL preview is opened', async () => { + it('opens the server-generated BOL PDF with one request', async () => { + const pdfWindow = { location: { href: '' }, close: vi.fn() } + vi.spyOn(window, 'open').mockReturnValue(pdfWindow as unknown as Window) + vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:dispatch-bol'), revokeObjectURL: vi.fn() }) const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } }) await flushPromises() - expect(getDispatch).not.toHaveBeenCalled() + expect(getDispatchBolPdf).not.toHaveBeenCalled() await wrapper.findAll('.el-table__body-wrapper button').find((button) => button.text().trim() === '打印BOL')!.trigger('click') await flushPromises() - expect(getDispatch).toHaveBeenCalledOnce() - expect(getDispatch).toHaveBeenCalledWith('PC-1') - expect(document.body.textContent).toContain('BOL 打印预览') + expect(getDispatchBolPdf).toHaveBeenCalledOnce() + expect(getDispatchBolPdf).toHaveBeenCalledWith('PC-1') + expect(pdfWindow.location.href).toBe('blob:dispatch-bol') }) it('documents the backend feature flag required by a missing endpoint', () => { diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index 32bdbf41..ee0291d7 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -13,9 +13,8 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone import StatusBadge from '@/shared/components/StatusBadge.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' -import { cancelDispatch, getDispatch, listDispatches, transferDispatchToDelivery, updateDispatch, type DispatchSummaryDto } from '../api/dispatches' +import { cancelDispatch, getDispatchBolPdf, listDispatches, transferDispatchToDelivery, updateDispatch, type DispatchSummaryDto } from '../api/dispatches' import AppDialog from '@/shared/components/AppDialog.vue' -import DispatchBolPrintDialog from '../components/DispatchBolPrintDialog.vue' import type { DispatchListItem } from '../types' import { formatOverseasDateTime } from '../lastMilePresentation' import { overseasWarehousePermissions as permission } from '../permissions' @@ -32,7 +31,7 @@ const filters = ref({ dispatchNo: '', vehiclePlate: '', status: '', const transferOpen = ref(false); const transferRow = ref(null); const transferSaving = ref(false) const mutationError = ref(null) const editOpen = ref(false); const editRow = ref(null); const editSaving = ref(false) -const bolOpen = ref(false); const bolDetail = ref(null); const bolLoadingNo = ref('') +const bolLoadingNo = ref('') const dispatchSnapshots = new Map() const editForm = reactive({ vehiclePlate: '', driverName: '', driverPhone: '', carrierName: '', route: '', departAt: '', fbaAppointmentNo: '', fbaAppointmentStatus: '', vehicleAppointmentStatus: '', estimatedArrivalAt: '' }) const transferForm = reactive({ plannedDeliveryAt: '', deliveryMethod: '', waybillNo: '', carrierName: '', pickupMethod: '', loadingType: '', vehicleType: '', vehicleSize: '', driverName: '', driverPhone: '', vehiclePlate: '', appointmentAccount: '', sellerName: '', deliveryFee: undefined as number | undefined, loadingFee: undefined as number | undefined, appointmentFee: undefined as number | undefined, cartonMarksJson: '', remark: '' }) @@ -189,13 +188,18 @@ function reset(model: QueryModel) { page.value = 1; void load(model) } function openDetail(row: DispatchListItem) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) } async function openBol(row: DispatchListItem) { if (bolLoadingNo.value) return + const pdfWindow = window.open('', '_blank') + if (!pdfWindow) { ElMessage.warning('浏览器拦截了 BOL PDF 窗口,请允许本站打开新窗口'); return } bolLoadingNo.value = row.dispatchNo mutationError.value = null try { - bolDetail.value = await getDispatch(row.dispatchNo) - bolOpen.value = true + const pdf = await getDispatchBolPdf(row.dispatchNo) + const url = URL.createObjectURL(pdf) + pdfWindow.location.href = url + window.setTimeout(() => URL.revokeObjectURL(url), 60_000) } catch (cause) { - mutationError.value = getApiErrorDetails(cause, 'BOL 打印数据加载失败,请重试') + pdfWindow.close() + mutationError.value = getApiErrorDetails(cause, 'BOL PDF 生成失败,请重试') } finally { bolLoadingNo.value = '' } @@ -238,7 +242,6 @@ onMounted(load) -

派送信息

入库未逐箱绑定时,在出库前确认箱唛属于哪个实际托盘。

费用信息(USD)