From 09579ea285d9fc810040e0b0d351bd530b681260 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 19:59:17 +0800 Subject: [PATCH] fix(admin): align booking files with upload contract --- .../OceanBookingDocumentsPanel.test.ts | 38 ++++++++++++++++++- .../components/OceanBookingDocumentsPanel.vue | 9 ++++- .../pages/OceanBookingDetailView.test.ts | 3 ++ .../pages/OceanBookingDetailView.vue | 3 ++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.test.ts b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.test.ts index ace5970e..f210a114 100644 --- a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.test.ts +++ b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.test.ts @@ -1,10 +1,21 @@ -import { describe, expect, it } from 'vitest' +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' +import OceanBookingDocumentsPanel from './OceanBookingDocumentsPanel.vue' +import { uploadOceanBookingDocument } from '../api/platformOceanBookings' + +vi.mock('../api/platformOceanBookings', () => ({ + downloadOceanBookingDocument: vi.fn(), + invalidateOceanBookingDocument: vi.fn(), + uploadOceanBookingDocument: vi.fn(), +})) const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue'), 'utf8') describe('OceanBookingDocumentsPanel contract', () => { + beforeEach(() => { vi.clearAllMocks() }) it('owns the two confirmed document types under the booking', () => { expect(source).toContain("value: 'BOOKING_CONFIRMATION', label: '订舱确认文件'") expect(source).toContain("value: 'BILL_OF_LADING', label: '提单文件'") @@ -22,6 +33,31 @@ describe('OceanBookingDocumentsPanel contract', () => { expect(source).toContain('URL.createObjectURL(blob)') }) + it('accepts every file format while rejecting a single file above 50 MiB before upload', () => { + expect(source).toContain('const maxFileBytes = 50 * 1024 * 1024') + expect(source).toContain('file.size > maxFileBytes') + expect(source).toContain('单个订舱资料文件不能超过 50MB,请重新选择') + expect(source).toContain('任意格式,单文件不超过 50MB') + expect(source).not.toContain('accept="') + }) + + it('rejects an oversized arbitrary-format file before calling the backend', async () => { + const wrapper = mount(OceanBookingDocumentsPanel, { + props: { bookingId: '123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [], canManage: true }, + global: { plugins: [ElementPlus] }, + }) + const file = new File(['data'], 'archive.custom', { type: 'application/x-custom' }) + Object.defineProperty(file, 'size', { value: 50 * 1024 * 1024 + 1 }) + const input = wrapper.get('input[type="file"]') + Object.defineProperty(input.element, 'files', { configurable: true, value: [file] }) + await input.trigger('change') + await flushPromises() + + expect(wrapper.text()).toContain('单个订舱资料文件不能超过 50MB') + expect(uploadOceanBookingDocument).not.toHaveBeenCalled() + wrapper.unmount() + }) + it('makes released booking files permanently read-only and preserves traced errors', () => { expect(source).toContain("props.bookingStatus === 'DRAFT' && props.canManage") expect(source).toContain('已放舱 · 永久只读') diff --git a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue index 84c89256..215a9414 100644 --- a/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue +++ b/src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue @@ -12,6 +12,7 @@ const emit = defineEmits<{ updated: [documents: OceanBookingDocument[], bookingV const busyType = ref('') const busyDocumentId = ref('') const error = ref(null) +const maxFileBytes = 50 * 1024 * 1024 const types: Array<{ value: OceanBookingDocumentType; label: string; description: string }> = [ { value: 'BOOKING_CONFIRMATION', label: '订舱确认文件', description: '用于确认已订妥舱位及放舱前资料完整性' }, { value: 'BILL_OF_LADING', label: '提单文件', description: '用于提单号和拖车派车前资料校验' }, @@ -75,6 +76,10 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) { const file = input.files?.[0] input.value = '' if (!file || !editable.value) return + if (file.size > maxFileBytes) { + error.value = { message: '单个订舱资料文件不能超过 50MB,请重新选择' } + return + } const previous = documentsOfType(type) if (previous.length) { try { @@ -113,7 +118,7 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) {
-

{{ type.label }}

{{ type.description }}

+

{{ type.label }}

{{ type.description }}

任意格式,单文件不超过 50MB
  • {{ item.fileName }}{{ fileSize(item.sizeBytes) }} · {{ item.uploadedByName || item.uploadedBy }} · {{ item.uploadedAt }}
    下载作废
@@ -123,5 +128,5 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) { diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts index de0bf628..09b8e539 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts @@ -54,6 +54,9 @@ describe('OceanBookingDetailView draft and release contract', () => { it('performs the complete release gate and presents the irreversible warning', () => { expect(source).toContain('function releaseValidationError()') expect(source).toContain('预计到港时间不能早于预计开船时间') + expect(source).toContain('放舱前请填写起运港代码或名称') + expect(source).toContain('放舱前请填写目的港代码或名称') + expect(source).toContain("[form.data.placeOfDelivery, '交货地点']") expect(source).toContain("[form.data.customerId, '客户']") expect(source).toContain("if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'") expect(source).not.toContain('请先保存草稿后再放舱') diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index be9eaadc..c11f3362 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -299,11 +299,14 @@ async function openReleaseDialog() { } function releaseValidationError() { if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码' + if (!form.data.departurePortCode?.trim() && !form.data.departurePortName?.trim()) return '放舱前请填写起运港代码或名称' + if (!form.data.destinationPortCode?.trim() && !form.data.destinationPortName?.trim()) return '放舱前请填写目的港代码或名称' const requiredFields: Array<[unknown, string]> = [ [form.data.customerId, '客户'], [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'], [form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'], [form.data.estimatedSailingAt, '预计开船时间'], [form.data.estimatedArrivalAt, '预计到港时间'], [form.data.serviceTerm, 'Service Term'], [form.data.freightTerm, 'Freight Term'], + [form.data.placeOfDelivery, '交货地点'], [form.data.shippingCompanyId, '船公司'], [form.data.bookingAgentId, '订舱代理'], ] const missing = requiredFields.find(([value]) => !String(value || '').trim())