diff --git a/src/modules/overseas-business/pages/PurchaseOrdersView.vue b/src/modules/overseas-business/pages/PurchaseOrdersView.vue index cc138e1e..c641a741 100644 --- a/src/modules/overseas-business/pages/PurchaseOrdersView.vue +++ b/src/modules/overseas-business/pages/PurchaseOrdersView.vue @@ -4,7 +4,7 @@ import '@/styles/adminDensity.css' import { computed, onActivated, onMounted, onBeforeUnmount, ref, watch } from 'vue' import { useRouter } from 'vue-router' import { useAuthStore } from '@/core/auth/auth' -import { procurementStatuses, statusLabel, money } from '../procurementPresentation' +import { procurementStatuses, statusLabel, statusTone, money } from '../procurementPresentation' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' @@ -39,7 +39,7 @@ const columns: DataTableColumn[] = [ { key: 'productSpecification', prop: 'productSpecification', label: '商品规格', minWidth: 180, singleLine: false, formatter: (row: OverseasPurchaseSummary) => row.productSpecification || '—' }, { key: 'budgetUnitPrice', label: '预算单价', width: 120, align: 'right', formatter: (row: OverseasPurchaseSummary) => money(row.budgetUnitPrice, row.currency) }, { key: 'purchaseQuantity', prop: 'purchaseQuantity', label: '采购数量', width: 100, align: 'right' }, - { key: 'status', label: '状态', minWidth: 145, formatter: (row: OverseasPurchaseSummary) => statusLabel(row.status) }, + { key: 'status', label: '状态', minWidth: 145, slotName: 'status' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: OverseasPurchaseSummary) => formatDateTime(row.createdAt) }, ] function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' } @@ -80,6 +80,7 @@ onActivated(() => { if (activatedOnce) void load(); else activatedOnce = true }) + diff --git a/src/modules/overseas-business/pages/TransportOrdersView.vue b/src/modules/overseas-business/pages/TransportOrdersView.vue index fe2b0fdd..9bbe1792 100644 --- a/src/modules/overseas-business/pages/TransportOrdersView.vue +++ b/src/modules/overseas-business/pages/TransportOrdersView.vue @@ -42,6 +42,13 @@ function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arra const filters = ref({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value }) const looseboardStatusLabels: Record = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels } const transportTypeLabels: Record = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } +const statusLabels = computed>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels) +const statusTones: Record = { + PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', WAITING_INBOUND: 'warning', + RECEIVING: 'primary', WAITING_CONSOLIDATION: 'warning', PALLET_OVERAGE_REVIEW: 'warning', + PARTIALLY_ALLOCATED: 'primary', ALLOCATED: 'primary', PENDING_DISPATCH: 'warning', + DISPATCHED: 'primary', IN_TRANSIT: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info', +} const fields = computed(() => [ { key: 'orderNo', type: 'input', label: '商业单号', placeholder: '请输入商业单号', width: 230 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 170, options: Object.entries(isLooseboard.value ? looseboardStatusLabels : truckStatusLabels).map(([value, label]) => ({ value, label })) }, @@ -56,7 +63,7 @@ const truckColumns: DataTableColumn[] = [ { key: 'packageCount', prop: 'packageCount', label: '件数', width: 80, align: 'right' }, { key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' }, { key: 'budgetAmount', label: '预算总价', width: 120, align: 'right', formatter: (row: LocalTransportOrder) => row.budgetAmount == null ? '—' : `${row.budgetAmount} ${row.currency}` }, { key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.pickupAt) }, - { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 120, formatter: (row: LocalTransportOrder) => truckStatusLabels[row.status] }, + { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 120, slotName: 'status' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.createdAt) }, ] const looseboardColumns: DataTableColumn[] = [ @@ -65,7 +72,7 @@ const looseboardColumns: DataTableColumn[] = [ { key: 'destination', prop: 'destination', label: '目的地', minWidth: 145 }, { key: 'originContactName', prop: 'originContactName', label: '起运地联系人', minWidth: 125 }, { key: 'originContactPhone', prop: 'originContactPhone', label: '起运地联系电话', minWidth: 155 }, { key: 'destinationContactName', prop: 'destinationContactName', label: '目的地联系人', minWidth: 125 }, { key: 'destinationContactPhone', prop: 'destinationContactPhone', label: '目的地联系电话', minWidth: 155 }, { key: 'packageCount', prop: 'packageCount', label: '件数', width: 80, align: 'right' }, { key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' }, { key: 'budgetAmount', label: '预算总价', width: 115, align: 'right', formatter: (row: LocalLooseboardOrder) => row.budgetAmount == null ? '—' : `${row.budgetAmount} ${row.currency}` }, - { key: 'productName', prop: 'productName', label: '商品名称', minWidth: 150 }, { key: 'estimatedPalletCount', prop: 'estimatedPalletCount', label: '预计托数', width: 100 }, { key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.pickupAt) }, { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 105, formatter: (row: LocalLooseboardOrder) => looseboardStatusLabels[row.status] }, + { key: 'productName', prop: 'productName', label: '商品名称', minWidth: 150 }, { key: 'estimatedPalletCount', prop: 'estimatedPalletCount', label: '预计托数', width: 100 }, { key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.pickupAt) }, { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 105, slotName: 'status' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.createdAt) }, ] const columns = computed(() => { @@ -128,6 +135,7 @@ watch([page, pageSize], () => { void load() }); watch([isLooseboard, () => route + diff --git a/src/modules/overseas-master-data/api/trips.test.ts b/src/modules/overseas-master-data/api/trips.test.ts index 1da9087f..b97d1dc0 100644 --- a/src/modules/overseas-master-data/api/trips.test.ts +++ b/src/modules/overseas-master-data/api/trips.test.ts @@ -147,3 +147,13 @@ it('rejects missing stable-reference arrays instead of silently falling back to await expect(getTrip(trip.tripNo)).rejects.toThrow() } }) + +it('keeps unknown actual load measurements unknown when reading the trip', async () => { + const unknown = structuredClone(confirmed) as unknown as { pallets: Array<{ weightKg: unknown; volumeCbm: unknown }>; segments: Array<{ occupied: { weightKg: unknown; volumeCbm: unknown }; remaining: { weightKg: unknown; volumeCbm: unknown } }> } + unknown.pallets.forEach(row => { row.weightKg = null; row.volumeCbm = null }) + unknown.segments.forEach(row => { row.occupied.weightKg = null; row.occupied.volumeCbm = null; row.remaining.weightKg = null; row.remaining.volumeCbm = null }) + vi.mocked(http.get).mockResolvedValueOnce(envelope({ tripNo: trip.tripNo, recordVersion: '3', cargo: [cargo], manifestHash: confirmed.manifestHash, confirmedLoad: unknown, blockReason: null })) + const result = await getTripLoading(trip.tripNo) + expect(result.confirmedLoad?.pallets[0]?.weightKg).toBeNull() + expect(result.confirmedLoad?.segments[0]?.remaining.volumeCbm).toBeNull() +}) diff --git a/src/modules/overseas-master-data/api/trips.ts b/src/modules/overseas-master-data/api/trips.ts index 521057f4..397d7e72 100644 --- a/src/modules/overseas-master-data/api/trips.ts +++ b/src/modules/overseas-master-data/api/trips.ts @@ -19,7 +19,7 @@ type WirePreviousDispatch = Omit & { reco type WireDetail = Omit & { trip: WireTrip; dispatches: WireBinding[]; loadingReview: WireReview; previousDispatches: WirePreviousDispatch[] } type WirePage = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric } type WireCargo = Omit & { packages: Numeric } -type WireLoad = { [K in keyof TripLoad]: Numeric } +type WireLoad = { palletSpaces: Numeric; weightKg: Numeric | null; volumeCbm: Numeric | null } type WireMeasurement = Omit & WireLoad & { deliveryIndex: Numeric } type WireConfirmed = Omit & { cargo: WireCargo[]; pallets: WireMeasurement[] @@ -56,7 +56,7 @@ export const normalizeTripDetail = (value: WireDetail): TripDetail => ({ ...valu const cargo = (value: WireCargo): TripCargo => ({ ...value, packages: integer(value.packages) }) const outcome = (value: WireOutcome): TripOutcome => ({ ...value, packages: integer(value.packages), signedPackages: integer(value.signedPackages), returnedPackages: integer(value.returnedPackages), remainingPackages: integer(value.remainingPackages) }) -const load = (value: WireLoad): TripLoad => ({ palletSpaces: integer(value.palletSpaces), weightKg: number(value.weightKg), volumeCbm: number(value.volumeCbm) }) +const load = (value: WireLoad): TripLoad => ({ palletSpaces: integer(value.palletSpaces), weightKg: value.weightKg == null ? null : number(value.weightKg), volumeCbm: value.volumeCbm == null ? null : number(value.volumeCbm) }) function confirmed(value: WireConfirmed | null): TripConfirmedLoad | null { return value == null ? null : { ...value, cargo: value.cargo.map(cargo), pallets: value.pallets.map(row => ({ ...row, ...load(row), deliveryIndex: integer(row.deliveryIndex) })), diff --git a/src/modules/overseas-master-data/components/TripLoadingPanel.vue b/src/modules/overseas-master-data/components/TripLoadingPanel.vue index eeeac374..1564d606 100644 --- a/src/modules/overseas-master-data/components/TripLoadingPanel.vue +++ b/src/modules/overseas-master-data/components/TripLoadingPanel.vue @@ -104,7 +104,7 @@ onBeforeUnmount(() => { sequence++ }) -
行驶区间占用托位重量(kg)体积(m³)剩余托位 / kg / m³
{{ trip.stops[segment.fromIndex]?.name }} → {{ trip.stops[segment.toIndex]?.name }}{{ segment.occupied.palletSpaces }} / {{ trip.maxPalletSpaces }}{{ segment.occupied.weightKg }} / {{ trip.maxLoadKg }}{{ segment.occupied.volumeCbm }} / {{ trip.maxVolumeCbm }}{{ segment.remaining.palletSpaces }} / {{ segment.remaining.weightKg }} / {{ segment.remaining.volumeCbm }}
+
行驶区间占用托位重量(kg)体积(m³)剩余托位 / kg / m³
{{ trip.stops[segment.fromIndex]?.name }} → {{ trip.stops[segment.toIndex]?.name }}{{ segment.occupied.palletSpaces }} / {{ trip.maxPalletSpaces }}{{ segment.occupied.weightKg ?? '—' }} / {{ trip.maxLoadKg }}{{ segment.occupied.volumeCbm ?? '—' }} / {{ trip.maxVolumeCbm }}{{ segment.remaining.palletSpaces }} / {{ segment.remaining.weightKg }} / {{ segment.remaining.volumeCbm }}
diff --git a/src/modules/overseas-master-data/tripTypes.ts b/src/modules/overseas-master-data/tripTypes.ts index 49310bbc..fecc3542 100644 --- a/src/modules/overseas-master-data/tripTypes.ts +++ b/src/modules/overseas-master-data/tripTypes.ts @@ -47,8 +47,8 @@ export interface TripCargo { dispatchNo: string; customerId: string; dispatchItemId: string; warehousePalletId: string | null palletNo: string | null; packages: number; destinationKey: string | null; destinationLabel: string | null } -export interface TripMeasurement { warehousePalletId: string; palletSpaces: number; weightKg: number; volumeCbm: number; deliveryIndex: number } -export interface TripLoad { palletSpaces: number; weightKg: number; volumeCbm: number } +export interface TripMeasurement { warehousePalletId: string; palletSpaces: number; weightKg: number | null; volumeCbm: number | null; deliveryIndex: number } +export interface TripLoad { palletSpaces: number; weightKg: number | null; volumeCbm: number | null } export interface TripSegment { fromIndex: number; toIndex: number; occupied: TripLoad; remaining: TripLoad } export interface TripConfirmedLoad { manifestHash: string; cargo: TripCargo[]; pallets: TripMeasurement[]; segments: TripSegment[]; operatorId: string; confirmedAt: string } export interface TripPlannedLoad { manifestHash: string; cargo: TripCargo[]; pallets: TripMeasurement[]; segments: TripSegment[]; operatorId: string; confirmedAt: string } diff --git a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue index 9ed27a5a..d0b47456 100644 --- a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue +++ b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue @@ -28,7 +28,7 @@ const error = ref(null) const conflict = ref(false) const canEdit = computed(() => access.can(permission.deliveryDocuments) && Boolean(snapshot.value?.allowedActions.includes('MANAGE_DOCUMENTS'))) const busy = computed(() => saving.value || uploading.value) -const uploadedCount = computed(() => draft.value.filter(item => item.files.length > 0).length) +const uploadedCount = computed(() => visibleGroups.value.filter(item => item.files.length > 0).length) const visibleGroups = computed(() => draft.value.filter(item => !props.categories || props.categories.includes(item.category))) const dirty = computed(() => Boolean(snapshot.value) && (bolNo.value !== (snapshot.value?.bolNo || '') || draft.value.some(group => JSON.stringify(group.files.map(file => file.fileId)) !== JSON.stringify(snapshot.value?.categories.find(item => item.category === group.category)?.files.map(file => file.fileId) || [])))) defineExpose({ busy, dirty, canEdit, loading, conflict, save }) @@ -118,7 +118,7 @@ async function save() {
-

资料核对

派送单号{{ deliveryNo || '—' }}资料类别{{ snapshot ? uploadedCount + '/3 已上传' : loading ? '正在读取…' : '资料未返回' }}

{{ canEdit ? '上传、替换和删除将在保存资料后生效;关闭将放弃未保存修改。' : '当前账号仅可查看已保存资料。' }}

+

资料核对

派送单号{{ deliveryNo || '—' }}资料类别{{ snapshot ? uploadedCount + '/' + visibleGroups.length + ' 已上传' : loading ? '正在读取…' : '资料未返回' }}

{{ canEdit ? '上传、替换和删除将在保存资料后生效;关闭将放弃未保存修改。' : '当前账号仅可查看已保存资料。' }}