diff --git a/src/modules/customers/overseasStorageFee.ts b/src/modules/customers/overseasStorageFee.ts index 833711d1..5dac43d7 100644 --- a/src/modules/customers/overseasStorageFee.ts +++ b/src/modules/customers/overseasStorageFee.ts @@ -62,5 +62,5 @@ export async function updateCustomerOverseasStorageFees(tenantId: string, reques export async function listOverseasStorageFees(page = 1, size = 20, customerName?: string) { const response = await http.get>('/platform/v1/overseas-storage-fees', { params: { page, size, customerName } }) const result = unwrapApiResponse(response) - return { ...result, records: result.records.map(normalize) } + return { ...result, records: (result.records || []).map(normalize), total: Number(result.total || 0), page: Number(result.page || page), size: Number(result.size || size) } } diff --git a/src/modules/customers/pages/OverseasStorageFeesView.vue b/src/modules/customers/pages/OverseasStorageFeesView.vue index 84065606..8bf7a8de 100644 --- a/src/modules/customers/pages/OverseasStorageFeesView.vue +++ b/src/modules/customers/pages/OverseasStorageFeesView.vue @@ -1,8 +1,10 @@ - - + + diff --git a/src/modules/ocean-bookings/pages/OceanBookingsView.vue b/src/modules/ocean-bookings/pages/OceanBookingsView.vue index b65868ba..37c0edee 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingsView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingsView.vue @@ -18,5 +18,5 @@ async function load() { loading.value = true; error.value = null; try { const re function search() { page.value = 1; void load() }; function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') } watch([page, pageSize], () => void load()); onMounted(load) - + diff --git a/src/modules/ocean-bookings/types.ts b/src/modules/ocean-bookings/types.ts index 8f0bf9e1..512c206b 100644 --- a/src/modules/ocean-bookings/types.ts +++ b/src/modules/ocean-bookings/types.ts @@ -1,6 +1,16 @@ export type OceanBookingStatus = 'DRAFT' | 'SUBMITTED' | 'CONFIRMED' | 'RELEASED' | 'FAILED' | 'CHANGE_PENDING' | 'CANCELLED' -export interface OceanBookingContainer { id: string; containerType: string; quantity: number; containerKind?: string | null } +export interface OceanBookingParty { partyType: string; companyName: string; contactName?: string | null; phone?: string | null; email?: string | null; address?: string | null } +export interface OceanBookingContainer { id?: string; containerType: string; quantity: number; containerKind?: string | null } +export interface OceanBookingContainerAllocation extends OceanBookingContainer { allocatedQuantity?: number | null } +export interface OceanBookingDraftData { + customerId: string; customerName: string; parties: OceanBookingParty[]; containerRequirements: OceanBookingContainer[] + shippingCompanyId?: string | null; shippingCompanyName?: string | null; bookingAgentId?: string | null; bookingAgentName?: string | null + departurePortCode?: string | null; departurePortName?: string | null; destinationPortCode?: string | null; destinationPortName?: string | null + estimatedSailingAt?: string | null; estimatedArrivalAt?: string | null; soNo?: string | null; vesselName?: string | null; voyageNo?: string | null + cutoffAt?: string | null; goodsDescription?: string | null; packageCount?: number | null; grossWeight?: string | null; volume?: string | null + amount?: string | null; currency?: string | null; remark?: string | null; orderNo?: string | null +} export interface OceanBookingResource { id: string; bookingNo: string | null; orderNo: string | null; customerId: string | null; customerName: string | null status: OceanBookingStatus; carrierName: string | null; bookingAgent: string | null; vesselName: string | null; voyageNo: string | null @@ -9,8 +19,8 @@ export interface OceanBookingResource { containerSummary: string | null; containers: OceanBookingContainer[]; amount: string | null; currency: string | null remark: string | null; createdAt: string | null; updatedAt: string | null } -export interface OceanBookingDetail extends OceanBookingResource { goodsDescription: string | null; packageCount: number | null; grossWeight: string | null; volume: string | null; statusHistory: OceanBookingStatusEvent[] } +export interface OceanBookingDetail extends OceanBookingResource { oceanBooking?: OceanBookingResource; parties: OceanBookingParty[]; containerRequirements: OceanBookingContainer[]; containerAllocations: OceanBookingContainerAllocation[]; histories: OceanBookingStatusEvent[]; goodsDescription: string | null; packageCount: number | null; grossWeight: string | null; volume: string | null; statusHistory: OceanBookingStatusEvent[] } export interface OceanBookingStatusEvent { id: string; fromStatus: OceanBookingStatus | null; toStatus: OceanBookingStatus; action: string; operatorName: string | null; occurredAt: string } export interface OceanBookingPage { records: OceanBookingResource[]; page: number; size: number; total: number; pages: number } export interface OceanBookingQuery { page: number; size: number; keyword?: string; status?: OceanBookingStatus; pol?: string; pod?: string; soNo?: string; customerId?: string } -export interface OceanBookingSaveRequest { customerId?: string | null; orderNo?: string | null; carrierName?: string | null; bookingAgent?: string | null; vesselName?: string | null; voyageNo?: string | null; pol?: string | null; pod?: string | null; mblShipper?: string | null; mblConsignee?: string | null; soNo?: string | null; cutoffAt?: string | null; etd?: string | null; eta?: string | null; goodsDescription?: string | null; packageCount?: number | null; grossWeight?: string | null; volume?: string | null; amount?: string | null; currency?: string | null; remark?: string | null; containers: Array<{ containerType: string; quantity: number; containerKind?: string | null }> } +export interface OceanBookingSaveRequest { expectedVersion: number | null; data: OceanBookingDraftData } diff --git a/src/modules/trucking/api/platformTrucking.test.ts b/src/modules/trucking/api/platformTrucking.test.ts index 5e9b99cd..040f2eca 100644 --- a/src/modules/trucking/api/platformTrucking.test.ts +++ b/src/modules/trucking/api/platformTrucking.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { changeTruckAppointmentStatus, createTruckAppointment, getTruckAppointment, listTruckAppointments } from './platformTrucking' +import { createTruckAppointment, getTruckAppointment, listTruckAppointments, performTruckAppointmentAction } from './platformTrucking' vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() }, unwrapApiResponse: (response: { data: unknown }) => response.data })) @@ -9,17 +9,17 @@ describe('truck appointment API', () => { it('normalizes page values and maps filters', async () => { vi.mocked(http.get).mockResolvedValue({ data: { records: [], page: '2', size: '20', total: '21', pages: '2' } }) - await expect(listTruckAppointments({ page: 2, size: 20, keyword: ' BY ', status: 'DISPATCHED', containerType: '40HC' })).resolves.toMatchObject({ page: 2, total: 21, pages: 2 }) - expect(http.get).toHaveBeenCalledWith('/platform/v1/truck-appointments', { params: { page: 2, size: 20, keyword: 'BY', status: 'DISPATCHED', containerType: '40HC' } }) + await expect(listTruckAppointments({ page: 2, size: 20, keyword: ' BY ', status: 'DISPATCHED', containerBookingId: '77' })).resolves.toMatchObject({ page: 2, total: 21, pages: 2 }) + expect(http.get).toHaveBeenCalledWith('/platform/v1/truck-appointments', { params: { page: 2, size: 20, keyword: 'BY', status: 'DISPATCHED', containerBookingId: '77' } }) }) it('uses detail, create, and status contracts', async () => { vi.mocked(http.get).mockResolvedValue({ data: { id: '9001', status: 'DRAFT' } }) vi.mocked(http.post).mockResolvedValue({ data: '9001' }) - vi.mocked(http.patch).mockResolvedValue({ data: null }) + vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'DISPATCHED' } }) await expect(getTruckAppointment('9001')).resolves.toMatchObject({ id: '9001' }) - await expect(createTruckAppointment({ expectedLoadingDate: '2026-08-10' })).resolves.toBe('9001') - await expect(changeTruckAppointmentStatus('9001', 'DISPATCHED')).resolves.toBeNull() - expect(http.patch).toHaveBeenCalledWith('/platform/v1/truck-appointments/9001/status', { status: 'DISPATCHED' }) + await expect(createTruckAppointment({ plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0 })).resolves.toMatchObject({ id: '9001' }) + await expect(performTruckAppointmentAction('9001', 'DISPATCH', 2)).resolves.toMatchObject({ status: 'DISPATCHED' }) + expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/9001/dispatch', { expectedVersion: 2, reason: null }, { headers: {} }) }) }) diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index 57ed8f17..30703b6f 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -11,7 +11,6 @@ watch(() => props.modelValue, (value) => Object.assign(draft, value), { deep: tr watch(draft, (value) => emit('update:modelValue', { ...value }), { deep: true }) const rules: FormRules = { plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }], - carrierId: [{ required: true, message: '请输入承运商 ID', trigger: 'blur' }], } const relationText = computed(() => [props.resource?.customerName, props.resource?.outboundOrderId, props.resource?.containerBookingId].filter(Boolean).join(' / ') || '由服务端关联资源返回') async function validate() { return formRef.value?.validate() } diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.vue b/src/modules/trucking/pages/TruckAppointmentDetailView.vue index 714f611c..09fd05f9 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.vue +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.vue @@ -11,7 +11,7 @@ import { getTruckAppointment, performTruckAppointmentAction, rescheduleTruckAppo import TruckAppointmentForm from '../components/TruckAppointmentForm.vue' import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue' import { TRUCK_APPOINTMENT_STATUSES, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAttachmentReference } from '../types' -const route = useRoute(); const router = useRouter(); const open = ref(true); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref(null); const error = ref(null); const form = reactive({}) +const route = useRoute(); const router = useRouter(); const open = ref(true); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref(null); const error = ref(null); const form = reactive({ expectedVersion: 0 }) const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && !['COMPLETED','CANCELLED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate')) async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0, attachmentRefs: result.attachments || [] }) } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } } async function submit() { saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } } diff --git a/src/modules/trucking/pages/TruckAppointmentsView.test.ts b/src/modules/trucking/pages/TruckAppointmentsView.test.ts index 83c954c7..808079fc 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentsView.test.ts @@ -8,13 +8,12 @@ import type { TruckAppointmentResource } from '../types' vi.mock('../api/platformTrucking', () => ({ listTruckAppointments: vi.fn(), - exportTruckAppointments: vi.fn(), })) const row: TruckAppointmentResource = { - id: '9001', appointmentNo: 'TA-9001', status: 'DISPATCHED', customerName: '示例企业', orderNo: 'ORD-1', - containerNo: 'CONT-1', containerType: '40HC', transportCompanyName: '宁波运输', driverName: '王强', tractorNo: '浙B12345', - expectedLoadingDate: '2026-08-10', payableAmount: '3200.00', receivableAmount: '3800.00', attachments: [], + id: '9001', appointmentNo: 'TA-9001', status: 'DISPATCHED', customerName: '示例企业', orderNo: 'ORD-1', containerBookingId: '77', + containerNo: 'CONT-1', carrierName: '宁波运输', driverName: '王强', vehicleNo: '浙B12345', + plannedLoadingAt: '2026-08-10T10:00:00', payableAmount: '3200.00', receivableAmount: '3800.00', attachments: [], } describe('TruckAppointmentsView', () => { @@ -26,9 +25,9 @@ describe('TruckAppointmentsView', () => { it('loads the real list and renders relationship snapshots', async () => { const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } }) await flushPromises() - expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerType: '' }) + expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerBookingId: '' }) expect(wrapper.text()).toContain('TA-9001') - expect(wrapper.text()).toContain('40HC') + expect(wrapper.text()).toContain('柜体 77') expect(wrapper.text()).toContain('宁波运输') expect(wrapper.find('button[aria-label="查看 TA-9001"]').exists()).toBe(true) }) diff --git a/src/modules/trucking/types.ts b/src/modules/trucking/types.ts index 768f02eb..84fef700 100644 --- a/src/modules/trucking/types.ts +++ b/src/modules/trucking/types.ts @@ -62,6 +62,7 @@ export interface TruckAppointmentResource { storeCity?: string | null storeZipcode?: string | null plannedLoadingAt?: string | null + actualLoadingAt?: string | null transitMinutes?: number | string | null packageCount?: number | string | null grossWeightKg?: string | null