From 1076315945fdb947b076ed67d8d0766bc15a7753 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 8 Aug 2026 15:15:04 +0800 Subject: [PATCH] fix(admin): validate booking and trucking forms --- .../pages/OceanBookingDetailView.test.ts | 22 ++++++++ .../pages/OceanBookingDetailView.vue | 52 +++++++++++++++---- .../components/TruckAppointmentForm.vue | 50 +++++++++++++----- .../trucking/pages/TruckAppointmentsView.vue | 3 +- 4 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts new file mode 100644 index 00000000..608c5a2c --- /dev/null +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingDetailView.vue'), 'utf8') + +describe('OceanBookingDetailView form contract', () => { + it('keeps prototype parties, sailing and cargo fields', () => { + for (const field of ['SHIPPER', 'CONSIGNEE', 'NOTIFY_PARTY', 'departurePortName', 'destinationPortName', 'vesselName', 'voyageNo', 'containerRequirements', 'goodsDescription']) { + expect(source).toContain(field) + } + }) + + it('validates required booking fields and protects edit-only fields', () => { + expect(source).toContain("customerId: [{ required: true") + expect(source).toContain("departurePortName: [{ required: true") + expect(source).toContain("bookingAgentName: [{ required: true") + expect(source).toContain(':disabled="!isNew"') + expect(source).toContain(':disabled="!editable"') + expect(source).toContain("containerType: '40HC', quantity: 1") + }) +}) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index 7c5ed510..00e256ff 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -1,21 +1,53 @@ -返回列表{{ detail?.bookingNo || '新增订舱' }}{{ detail.customerName || '—' }} · {{ detail.pol || '—' }} → {{ detail.pod || '—' }}{{ actionError.message }}订舱信息{{ nextStatus }}保存状态历史{{ event.fromStatus || '—' }} → {{ event.toStatus }} · {{ event.operatorName || '—' }} - + + 返回列表{{ detail?.bookingNo || '新增订舱' }}{{ detail.customerName || '—' }} · {{ detail.pol || '—' }} → {{ detail.pod || '—' }} + + {{ actionError.message }} + 订舱信息{{ nextStatus }}保存 + + + + + + + + + + + 状态历史{{ event.fromStatus || '—' }} → {{ event.toStatus }} · {{ event.operatorName || '—' }} + + + + diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index c8630f3d..030beada 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -6,11 +6,27 @@ import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../t const props = defineProps<{ modelValue: SaveTruckAppointmentRequest; resource?: TruckAppointmentResource | null; readonly?: boolean; submitting?: boolean }>() const emit = defineEmits<{ 'update:modelValue': [SaveTruckAppointmentRequest]; submit: []; attachments: [] }>() const formRef = ref() -const draft = reactive({ ...props.modelValue }) -watch(() => props.modelValue, (value) => Object.assign(draft, value), { deep: true }) +const defaults: Partial = { expectedVersion: 0, currency: 'RMB', payableAmount: '0.00', receivableAmount: '0.00', packageCount: 0, palletCount: 0, transitMinutes: 0 } +function normalizeDraft(value: SaveTruckAppointmentRequest): SaveTruckAppointmentRequest { return { ...defaults, ...value, currency: value.currency?.trim().toUpperCase() || 'RMB' } } +const draft = reactive(normalizeDraft(props.modelValue)) +watch(() => props.modelValue, (value) => Object.assign(draft, normalizeDraft(value)), { deep: true }) watch(draft, (value) => emit('update:modelValue', { ...value }), { deep: true }) +const nonNegative = (label: string, integer = false) => ({ validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => { + if (value === null || value === undefined || value === '') return callback() + const text = String(value).trim(); const valid = integer ? /^\d+$/.test(text) : /^(?:\d+)(?:\.\d{1,3})?$/.test(text) + callback(valid ? undefined : new Error(`${label}请输入${integer ? '非负整数' : '非负数字'}`)) +}, trigger: ['blur', 'change'] }) +const phoneRule = (label: string) => ({ validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => { + if (!value || /^[0-9+()\-\s]{6,32}$/.test(String(value).trim())) callback() + else callback(new Error(`${label}格式不正确`)) +}, trigger: 'blur' }) const rules: FormRules = { + carrierId: [{ required: true, message: '请输入运输公司 ID', trigger: 'blur' }, { pattern: /^[1-9]\d*$/, message: '运输公司 ID 必须为正整数', trigger: 'blur' }], plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }], + fleetPhone: [phoneRule('车队电话')], driverPhone: [phoneRule('司机电话')], storePhone: [phoneRule('门店电话')], + packageCount: [nonNegative('件数', true)], palletCount: [nonNegative('托盘数量', true)], transitMinutes: [nonNegative('在途分钟', true)], + grossWeightKg: [nonNegative('毛重')], volumeCbm: [nonNegative('体积')], payableAmount: [nonNegative('应付拖车费')], receivableAmount: [nonNegative('应收拖车费')], + currency: [{ pattern: /^[A-Z]{3,8}$/, message: '币种请输入 3-8 位大写字母(如 RMB)', trigger: 'blur' }], } const relationText = computed(() => [props.resource?.customerName, props.resource?.outboundOrderId, props.resource?.containerBookingId].filter(Boolean).join(' / ') || '由服务端关联资源返回') async function validate() { return formRef.value?.validate() } @@ -34,27 +50,27 @@ defineExpose({ validate }) - + - + - - - - - + + + + + 费用信息 - RMB - RMB - + RMB + RMB + 门店信息与备注 - + @@ -68,4 +84,12 @@ defineExpose({ validate }) .truck-form { padding-bottom: 12px; } .relation-alert { margin-bottom: 16px; } .form-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; } + :deep(.el-divider) { margin: 18px 0 12px; } + :deep(.el-divider__text) { color: var(--el-text-color-primary); font-size: 14px; font-weight: 600; } + :deep(.el-input), :deep(.el-textarea), :deep(.el-date-editor) { max-width: 100%; } +@media (max-width: 480px) { + .truck-form { padding-inline: 2px; } + .form-actions { position: sticky; bottom: 0; z-index: 2; margin: 8px -2px -4px; padding: 10px 2px; background: var(--el-bg-color); border-top: 1px solid var(--el-border-color-lighter); } + .form-actions :deep(.el-button) { flex: 1; min-height: 40px; } +} diff --git a/src/modules/trucking/pages/TruckAppointmentsView.vue b/src/modules/trucking/pages/TruckAppointmentsView.vue index 68797086..4c769194 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.vue +++ b/src/modules/trucking/pages/TruckAppointmentsView.vue @@ -66,8 +66,7 @@ onMounted(load)
{{ detail.customerName || '—' }} · {{ detail.pol || '—' }} → {{ detail.pod || '—' }}