From a3a09d1dec43b121450d0a21a071340e5aa99b01 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 8 Aug 2026 14:13:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/platformOceanBookings.ts | 2 +- .../pages/OceanBookingsView.vue | 2 +- src/modules/trucking/api/platformTrucking.ts | 3 +-- .../components/TruckAppointmentForm.vue | 17 ++++++++++-- .../components/TruckAttachmentDialog.vue | 27 +++++++------------ .../pages/TruckAppointmentCreateView.vue | 9 +++---- .../pages/TruckAppointmentDetailView.vue | 14 +++++++--- .../trucking/pages/TruckAppointmentsView.vue | 25 ++++++++++------- src/modules/trucking/types.ts | 3 ++- 9 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/modules/ocean-bookings/api/platformOceanBookings.ts b/src/modules/ocean-bookings/api/platformOceanBookings.ts index 3114f421..6feb3883 100644 --- a/src/modules/ocean-bookings/api/platformOceanBookings.ts +++ b/src/modules/ocean-bookings/api/platformOceanBookings.ts @@ -3,7 +3,7 @@ import type { OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBook const basePath = '/platform/v1/ocean-bookings' function pageNumber(value: number | string, fallback: number) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback } -function booking(raw: OceanBookingResource): OceanBookingResource { return { ...raw, id: String(raw.id), customerId: raw.customerId == null ? null : String(raw.customerId), containers: (raw.containers || []).map((item) => ({ ...item, id: String(item.id) })) } } +function booking(raw: OceanBookingResource): OceanBookingResource { const item = raw as OceanBookingResource & { departurePortName?: string | null; destinationPortName?: string | null; shippingCompanyName?: string | null; estimatedSailingAt?: string | null; estimatedArrivalAt?: string | null }; return { ...raw, id: String(raw.id), customerId: raw.customerId == null ? null : String(raw.customerId), pol: raw.pol ?? item.departurePortName ?? null, pod: raw.pod ?? item.destinationPortName ?? null, carrierName: raw.carrierName ?? item.shippingCompanyName ?? null, etd: raw.etd ?? item.estimatedSailingAt ?? null, eta: raw.eta ?? item.estimatedArrivalAt ?? null, containers: (raw.containers || []).map((container) => ({ ...container, id: String(container.id ?? '') })) } } export async function listOceanBookings(query: OceanBookingQuery) { const response = await http.get, ApiEnvelope>(basePath, { params: query }) const result = unwrapApiResponse(response) diff --git a/src/modules/ocean-bookings/pages/OceanBookingsView.vue b/src/modules/ocean-bookings/pages/OceanBookingsView.vue index 37c0edee..2f0a4fd4 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingsView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingsView.vue @@ -13,7 +13,7 @@ const router = useRouter(); const rows = ref([]); const const query = reactive({ keyword: '', status: '', pol: '', pod: '', soNo: '' }) const statuses: Array<{ label: string; value: string }> = [{ label: '草稿', value: 'DRAFT' }, { label: '已提交', value: 'SUBMITTED' }, { label: '已确认', value: 'CONFIRMED' }, { label: '已放舱', value: 'RELEASED' }, { label: '失败', value: 'FAILED' }, { label: '变更中', value: 'CHANGE_PENDING' }, { label: '已取消', value: 'CANCELLED' }] const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '订舱号/订单号/客户', width: 220 }, { key: 'status', type: 'select', label: '状态', options: statuses }, { key: 'pol', type: 'input', label: '起运港', width: 150 }, { key: 'pod', type: 'input', label: '目的港', width: 150 }, { key: 'soNo', type: 'input', label: 'S/O号', width: 150 }] -const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false }, { prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'orderNo', label: '关联订单', minWidth: 140 }, { prop: 'customerName', label: '客户', minWidth: 140 }, { prop: 'pol', label: '起运港', minWidth: 110 }, { prop: 'pod', label: '目的港', minWidth: 110 }, { prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'soNo', label: 'S/O号', minWidth: 130 }, { prop: 'cutoffAt', label: '截关时间', minWidth: 150 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' }] +const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false }, { prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'orderNo', label: '关联订单', minWidth: 140 }, { prop: 'customerName', label: '委托单位', minWidth: 160 }, { prop: 'mblShipper', label: 'MB/L货人', minWidth: 150 }, { prop: 'mblConsignee', label: 'MB/L收人', minWidth: 150 }, { prop: 'pol', label: '起运港(POL)', minWidth: 125 }, { prop: 'pod', label: '目的港(POD)', minWidth: 125 }, { prop: 'containerSummary', label: '箱型/箱量', minWidth: 120 }, { prop: 'carrierName', label: '船公司', minWidth: 130 }, { prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'soNo', label: 'S/O号', minWidth: 130 }, { prop: 'cutoffAt', label: '预计截关', minWidth: 150 }, { prop: 'etd', label: 'ETD', minWidth: 140 }, { prop: 'atd', label: 'ATD', minWidth: 140 }, { prop: 'eta', label: 'ETA', minWidth: 140 }, { prop: 'ata', label: 'ATA', minWidth: 140 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' }] async function load() { loading.value = true; error.value = null; try { const result = await listOceanBookings({ page: page.value, size: pageSize.value, keyword: String(query.keyword || '') || undefined, status: (query.status || undefined) as OceanBookingStatus | undefined, pol: String(query.pol || '') || undefined, pod: String(query.pod || '') || undefined, soNo: String(query.soNo || '') || undefined } satisfies OceanBookingQuery); rows.value = result.records; total.value = result.total } catch (e) { error.value = getApiErrorDetails(e) } finally { loading.value = false } } 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/trucking/api/platformTrucking.ts b/src/modules/trucking/api/platformTrucking.ts index 6120d9c7..cebe9840 100644 --- a/src/modules/trucking/api/platformTrucking.ts +++ b/src/modules/trucking/api/platformTrucking.ts @@ -8,8 +8,7 @@ import type { const basePath = '/platform/v1/truck-appointments' function dtoPayload(payload: SaveTruckAppointmentRequest) { - const { attachmentRefs: _attachmentRefs, ...dto } = payload - return dto + return Object.fromEntries(Object.entries(payload).map(([key, value]) => [key, value === '' ? null : value])) as SaveTruckAppointmentRequest } type PagePayload = Omit & { diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index 30703b6f..f4a0a1cd 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -1,7 +1,7 @@ @@ -21,11 +22,19 @@ defineExpose({ validate }) 关联资源:{{ relationText }};柜型由关联柜体返回(只读) + + + + + + + + @@ -33,16 +42,20 @@ defineExpose({ validate }) + + + + -
管理附件引用保存
+
查看附件引用保存
diff --git a/src/modules/trucking/components/TruckAttachmentDialog.vue b/src/modules/trucking/components/TruckAttachmentDialog.vue index 2070c4c7..8669d06b 100644 --- a/src/modules/trucking/components/TruckAttachmentDialog.vue +++ b/src/modules/trucking/components/TruckAttachmentDialog.vue @@ -1,27 +1,20 @@ diff --git a/src/modules/trucking/pages/TruckAppointmentCreateView.vue b/src/modules/trucking/pages/TruckAppointmentCreateView.vue index 3847c3c7..311d9f41 100644 --- a/src/modules/trucking/pages/TruckAppointmentCreateView.vue +++ b/src/modules/trucking/pages/TruckAppointmentCreateView.vue @@ -8,10 +8,9 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import TruckAppointmentForm from '../components/TruckAppointmentForm.vue' import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue' import { createTruckAppointment } from '../api/platformTrucking' -import type { SaveTruckAppointmentRequest, TruckAttachmentReference } from '../types' +import type { SaveTruckAppointmentRequest } from '../types' const router = useRouter(); const open = ref(true); const saving = ref(false); const attachmentOpen = ref(false); const error = ref(null) -const form = reactive({ attachmentRefs: [], expectedVersion: 0, plannedLoadingAt: null, payableAmount: '0.00', receivableAmount: '0.00', currency: 'RMB' }) -async function submit() { saving.value = true; error.value = null; try { await createTruckAppointment({ ...form, attachmentRefs: form.attachmentRefs || [] }); ElMessage.success('拖车预约已创建'); router.push({ name: 'trucking' }) } catch (err) { error.value = getApiErrorDetails(err, '创建失败,请稍后重试') } finally { saving.value = false } } -function saveAttachments(items: TruckAttachmentReference[]) { form.attachmentRefs = items } +const form = reactive({ expectedVersion: 0, plannedLoadingAt: null, payableAmount: '0.00', receivableAmount: '0.00', currency: 'RMB' }) +async function submit() { saving.value = true; error.value = null; try { await createTruckAppointment({ ...form }); ElMessage.success('拖车预约已创建'); router.push({ name: 'trucking' }) } catch (err) { error.value = getApiErrorDetails(err, '创建失败,请稍后重试') } finally { saving.value = false } } - + diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.vue b/src/modules/trucking/pages/TruckAppointmentDetailView.vue index 09fd05f9..8bbbad53 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.vue +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.vue @@ -10,22 +10,28 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview' import { getTruckAppointment, performTruckAppointmentAction, rescheduleTruckAppointment, updateTruckAppointment, type TruckAppointmentAction } from '../api/platformTrucking' 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' +import { truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus } 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({ 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 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 }) } 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 } } const actionByTarget: Partial> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' } +const primaryTargetByStatus: Partial> = { DRAFT: 'PENDING_DISPATCH', PENDING_DISPATCH: 'DISPATCHED', RESCHEDULED: 'DISPATCHED', DISPATCHED: 'PICKUP_IN_PROGRESS', PICKUP_IN_PROGRESS: 'ARRIVED_WAREHOUSE', ARRIVED_WAREHOUSE: 'LOADING', LOADING: 'DEPARTED', DEPARTED: 'COMPLETED' } +const availableTargets = computed(() => { + if (!resource.value || ['COMPLETED', 'CANCELLED'].includes(resource.value.status)) return [] + const primary = primaryTargetByStatus[resource.value.status] + return [...(primary ? [primary] : []), 'RESCHEDULED', 'EXCEPTION', 'CANCELLED'] +}) async function changeStatus(status: TruckAppointmentStatus) { if (!resource.value || status === resource.value.status) return; try { await ElMessageBox.confirm(`确认将状态改为“${truckAppointmentStatusLabels[status]}”?`, '状态变更确认', { type: 'warning' }); statusSaving.value = true; if (status === 'RESCHEDULED') { await rescheduleTruckAppointment(id.value, form.plannedLoadingAt || '', form.expectedVersion || 0, '后台改约') } else { const action = actionByTarget[status]; if (!action) throw new Error('当前状态没有可用动作'); await performTruckAppointmentAction(id.value, action, form.expectedVersion || 0) } ElMessage.success('状态已更新'); await load() } catch (err) { if (err !== 'cancel' && !(err instanceof Error && err.message === 'cancel')) error.value = getApiErrorDetails(err, '状态更新失败,请稍后重试') } finally { statusSaving.value = false } } -function saveAttachments(items: TruckAttachmentReference[]) { form.attachmentRefs = items } onMounted(load) diff --git a/src/modules/trucking/pages/TruckAppointmentsView.vue b/src/modules/trucking/pages/TruckAppointmentsView.vue index 7bdd8739..f475b5d3 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.vue +++ b/src/modules/trucking/pages/TruckAppointmentsView.vue @@ -27,12 +27,13 @@ const columns: DataTableColumn[] = [ { key: 'company', label: '承运商', minWidth: 150, slotName: 'company' }, { key: 'driver', label: '司机/车头', minWidth: 150, slotName: 'driver' }, { key: 'store', label: '门店', minWidth: 130, slotName: 'store' }, - { key: 'plannedLoadingAt', prop: 'plannedLoadingAt', label: '预计装箱时间', minWidth: 170 }, + { key: 'plannedLoadingAt', label: '预计装箱时间', minWidth: 170, formatter: (row: TruckAppointmentResource) => formatDateTime(row.plannedLoadingAt) }, { key: 'payableAmount', label: '应付拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.payableAmount ? `¥${row.payableAmount}` : '—' }, { key: 'receivableAmount', label: '应收拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.receivableAmount ? `¥${row.receivableAmount}` : '—' }, { key: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'attachments', label: '附件', width: 90, formatter: (row: TruckAppointmentResource) => String(row.attachments?.length || 0) }, ] +function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' } function statusLabel(value: unknown) { return typeof value === 'string' && value in truckAppointmentStatusLabels ? truckAppointmentStatusLabels[value as TruckAppointmentStatus] @@ -44,25 +45,29 @@ function reset() { filters.value = { keyword: '', status: '', containerBookingId onMounted(load) diff --git a/src/modules/trucking/types.ts b/src/modules/trucking/types.ts index 84fef700..90026d89 100644 --- a/src/modules/trucking/types.ts +++ b/src/modules/trucking/types.ts @@ -76,6 +76,8 @@ export interface TruckAppointmentResource { orderId?: string | null orderNo?: string | null containerNo?: string | null + relatedContainerType?: string | null + entrustingPartyName?: string | null attachments?: TruckAttachmentReference[] | null createdAt?: string | null updatedAt?: string | null @@ -114,7 +116,6 @@ export interface SaveTruckAppointmentRequest { currency?: string | null poNo?: string | null remark?: string | null - attachmentRefs?: TruckAttachmentReference[] expectedVersion: number }