test(admin): harden booking document workflows
This commit is contained in:
@@ -66,4 +66,30 @@ describe('OceanBookingDocumentsPanel contract', () => {
|
||||
expect(source).toContain('订舱资料上传失败,请稍后重试')
|
||||
expect(source).toContain('订舱资料下载失败,请稍后重试')
|
||||
})
|
||||
|
||||
it('suppresses duplicate file mutations and protects list-only access', () => {
|
||||
expect(source).toContain('if (busyDocumentId.value || busyType.value) return')
|
||||
expect(source).toContain('if (busyDocumentId.value || busyType.value || !editable.value) return')
|
||||
expect(source).toContain('!file || !editable.value || busyType.value || busyDocumentId.value')
|
||||
expect(source).toContain(':disabled="Boolean(busyType || busyDocumentId)"')
|
||||
})
|
||||
|
||||
it('keeps long Unicode active and invalidated file names readable', () => {
|
||||
const fileName = '客户资料-中文-🚢-é-'.repeat(16) + '.数据包'
|
||||
const document = {
|
||||
id: 'doc-1', oceanBookingId: '123', fileId: 'file-1', documentType: 'BILL_OF_LADING' as const,
|
||||
fileName, fileHash: null, sizeBytes: 12, contentType: null, valid: true,
|
||||
uploadedBy: '7', uploadedByName: '操作员', uploadedAt: '2026-08-31',
|
||||
invalidatedBy: null, invalidatedByName: null, invalidatedAt: null,
|
||||
}
|
||||
const wrapper = mount(OceanBookingDocumentsPanel, {
|
||||
props: { bookingId: '123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [document], canManage: false, manageReason: '暂无订舱资料维护权限' },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
expect(wrapper.get('.booking-document-list strong').attributes('title')).toBe(fileName)
|
||||
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('作废')
|
||||
expect(source).toContain('.booking-document-history li span{min-width:0;overflow-wrap:anywhere}')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,6 +37,7 @@ function saveBlob(blob: Blob, name: string) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
async function download(item: OceanBookingDocument) {
|
||||
if (busyDocumentId.value || busyType.value) return
|
||||
busyDocumentId.value = item.id
|
||||
error.value = null
|
||||
try {
|
||||
@@ -54,18 +55,15 @@ async function invalidate(item: OceanBookingDocument, expectedVersion = props.bo
|
||||
return { documents: nextDocuments, version: result.bookingVersion }
|
||||
}
|
||||
async function remove(item: OceanBookingDocument) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认作废“${item.fileName}”?文件历史仍会保留。`, '作废订舱资料', { confirmButtonText: '确认作废', cancelButtonText: '取消', type: 'warning' })
|
||||
} catch (cause) {
|
||||
if (cause === 'cancel' || cause === 'close') return
|
||||
throw cause
|
||||
}
|
||||
if (busyDocumentId.value || busyType.value || !editable.value) return
|
||||
busyDocumentId.value = item.id
|
||||
error.value = null
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认作废“${item.fileName}”?文件历史仍会保留。`, '作废订舱资料', { confirmButtonText: '确认作废', cancelButtonText: '取消', type: 'warning' })
|
||||
await invalidate(item)
|
||||
ElMessage.success('订舱资料已作废')
|
||||
} catch (cause) {
|
||||
if (cause === 'cancel' || cause === 'close') return
|
||||
error.value = getApiErrorDetails(cause, '订舱资料作废失败,请稍后重试')
|
||||
} finally {
|
||||
busyDocumentId.value = ''
|
||||
@@ -75,23 +73,18 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file || !editable.value) return
|
||||
if (!file || !editable.value || busyType.value || busyDocumentId.value) return
|
||||
if (file.size > maxFileBytes) {
|
||||
error.value = { message: '单个订舱资料文件不能超过 50MB,请重新选择' }
|
||||
return
|
||||
}
|
||||
const previous = documentsOfType(type)
|
||||
if (previous.length) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`将上传“${file.name}”并作废当前 ${previous.length} 份同类型文件,是否继续?`, '替换订舱资料', { confirmButtonText: '确认替换', cancelButtonText: '取消', type: 'warning' })
|
||||
} catch (cause) {
|
||||
if (cause === 'cancel' || cause === 'close') return
|
||||
throw cause
|
||||
}
|
||||
}
|
||||
busyType.value = type
|
||||
error.value = null
|
||||
try {
|
||||
if (previous.length) {
|
||||
await ElMessageBox.confirm(`将上传“${file.name}”并作废当前 ${previous.length} 份同类型文件,是否继续?`, '替换订舱资料', { confirmButtonText: '确认替换', cancelButtonText: '取消', type: 'warning' })
|
||||
}
|
||||
const uploaded = await uploadOceanBookingDocument(props.bookingId, props.bookingVersion, type, file)
|
||||
let documents = [...props.documents, uploaded.document]
|
||||
let version = uploaded.bookingVersion
|
||||
@@ -104,6 +97,7 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) {
|
||||
}
|
||||
ElMessage.success(previous.length ? '订舱资料已替换' : '订舱资料已上传')
|
||||
} catch (cause) {
|
||||
if (cause === 'cancel' || cause === 'close') return
|
||||
error.value = getApiErrorDetails(cause, '订舱资料上传失败,请稍后重试')
|
||||
} finally {
|
||||
busyType.value = ''
|
||||
@@ -129,4 +123,5 @@ async function selectFile(event: Event, type: OceanBookingDocumentType) {
|
||||
|
||||
<style scoped>
|
||||
.booking-documents{padding:16px}.booking-documents__heading,.booking-document-card__heading,.booking-document-list li,.booking-document-actions{display:flex;align-items:center}.booking-documents__heading,.booking-document-card__heading,.booking-document-list li{justify-content:space-between;gap:16px}.booking-documents__heading{margin-bottom:14px}.booking-documents h2,.booking-document-card h3{margin:0}.booking-documents__heading h2{font-size:16px}.booking-documents__heading p,.booking-document-card__heading p{margin:4px 0 0;color:var(--cc-muted);font-size:12px}.booking-document-limit{display:block;margin-top:5px;color:var(--cc-muted);font-size:11px}.booking-document-types{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.booking-document-card{min-width:0;padding:14px;border:1px solid var(--cc-border);border-radius:8px}.booking-document-card h3{font-size:14px}.booking-document-upload{display:flex;flex:0 0 auto;align-items:center;gap:6px;min-height:32px;padding:0 12px;border:1px solid var(--cc-primary);border-radius:6px;color:var(--cc-primary);font-size:12px;cursor:pointer}.booking-document-upload.is-disabled{cursor:not-allowed;opacity:.55}.booking-document-upload input{position:absolute;width:1px;height:1px;overflow:hidden;opacity:0}.booking-document-list,.booking-document-history ul{display:grid;gap:8px;margin:12px 0 0;padding:0;list-style:none}.booking-document-list li{min-width:0;padding:9px 10px;border-radius:6px;background:var(--el-fill-color-lighter)}.booking-document-list li>div:first-child{display:grid;min-width:0;gap:3px}.booking-document-list strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.booking-document-list small,.booking-document-history small{color:var(--cc-muted);font-size:11px}.booking-document-actions{flex:0 0 auto;gap:6px}.booking-document-actions :deep(.el-button){margin-left:0}.booking-document-history{margin-top:14px;color:var(--cc-muted);font-size:12px}.booking-document-history summary{cursor:pointer}.booking-document-history li{display:flex;justify-content:space-between;gap:12px;padding:7px 10px;border-bottom:1px solid var(--cc-border)}@media(max-width:900px){.booking-document-types{grid-template-columns:1fr}}@media(max-width:560px){.booking-documents__heading,.booking-document-card__heading,.booking-document-list li{align-items:flex-start;flex-direction:column}.booking-document-actions{width:100%}}
|
||||
.booking-document-history li span{min-width:0;overflow-wrap:anywhere}
|
||||
</style>
|
||||
|
||||
@@ -54,4 +54,26 @@ describe('OceanBookingPrintDialog', () => {
|
||||
expect(document.body.textContent).toContain('trace-print')
|
||||
failed.unmount()
|
||||
})
|
||||
|
||||
it('suppresses duplicate preview clicks and restores loading after completion', async () => {
|
||||
let resolvePrint!: (blob: Blob) => void
|
||||
vi.mocked(downloadOceanBookingPrint).mockImplementation(() => new Promise<Blob>((resolve) => { resolvePrint = resolve }))
|
||||
const wrapper = mount(OceanBookingPrintDialog, {
|
||||
props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true },
|
||||
global: { plugins: [ElementPlus] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.booking-print-option')[0]!.trigger('click')
|
||||
const preview = Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))!
|
||||
preview.click()
|
||||
preview.click()
|
||||
await flushPromises()
|
||||
expect(downloadOceanBookingPrint).toHaveBeenCalledTimes(1)
|
||||
expect(preview.disabled).toBe(true)
|
||||
resolvePrint(new Blob(['pdf'], { type: 'application/pdf' }))
|
||||
await flushPromises()
|
||||
expect(document.body.querySelector('iframe')).not.toBeNull()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user