优化
This commit is contained in:
@@ -76,3 +76,11 @@ describe('owned business HTTP contract', () => {
|
||||
await expect(uploadOwnedBusinessPhoto('DS-OWN-1','FUEL_START',new File(['text'],'bad.txt',{type:'text/plain'}),'key')).rejects.toThrow('JPG')
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts unknown transport ownership without guessing current master data', async () => {
|
||||
const detail = ownedBusinessFixture()
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { ...detail, row: { ...detail.row, taskType: 'LOCAL_TRUCK', actualPalletCount: null, vehicle: { ...detail.row.vehicle, ownership: null } } } })
|
||||
const result = await getOwnedBusiness(detail.row.dispatchNo)
|
||||
expect(result.row.vehicle.ownership).toBeNull()
|
||||
expect(result.row.actualPalletCount).toBeNull()
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ function operations(value: OwnedBusinessOperations): OwnedBusinessOperations {
|
||||
return { ...value, money: value.money ? { ...value.money, amount: decimal(value.money.amount, 2, 18)! } : null, actualDistanceMiles: decimal(value.actualDistanceMiles,3,12), fuelConsumptionLitresPer100Km: decimal(value.fuelConsumptionLitresPer100Km,3,12) }
|
||||
}
|
||||
function row(value: OwnedBusinessRow): OwnedBusinessRow {
|
||||
if (!value.dispatchNo || !['AFTER_LOADING','DELIVERING','DELIVERED'].includes(value.stage) || !['OWNED','DISPATCHED'].includes(value.vehicle?.ownership)) throw new Error('自有排车来源或阶段不完整,请重新读取')
|
||||
if (!value.dispatchNo || !['AFTER_LOADING','DELIVERING','DELIVERED'].includes(value.stage) || !(['OWNED','DISPATCHED'].includes(value.vehicle?.ownership || '') || (value.vehicle?.ownership === null && ['LOCAL_TRUCK','PICKUP'].includes(value.taskType || '')))) throw new Error('自有排车来源或阶段不完整,请重新读取')
|
||||
return { ...value, operations: operations(value.operations), unitPrice: decimal(value.unitPrice,2,18), recordVersion: ownedBusinessVersion(value.recordVersion), dispatchRecordVersion: ownedBusinessVersion(value.dispatchRecordVersion), actualPalletCount: value.actualPalletCount == null ? null : count(value.actualPalletCount), photoSummary: { loadingCount: count(value.photoSummary.loadingCount), fuelCount: count(value.photoSummary.fuelCount) } }
|
||||
}
|
||||
function detail(value: OwnedBusinessDetail): OwnedBusinessDetail { if (value.row.dispatchNo !== value.dispatch.dispatchNo) throw new Error('运营资料与原排车单不一致'); return { ...value, row: row(value.row) } }
|
||||
|
||||
@@ -46,3 +46,11 @@ it('rejects JSON download errors and preserves correlation evidence instead of p
|
||||
vi.mocked(http.get).mockResolvedValue({ code: 'A0403', message: '无权读取', requestId: 'request-evidence', traceId: 'trace-evidence' })
|
||||
await expect(downloadTripOperationsEvidence('TR1', '81')).rejects.toMatchObject({ code: 'A0403', requestId: 'request-evidence', traceId: 'trace-evidence' })
|
||||
})
|
||||
|
||||
it('keeps transport pallet facts unknown while retaining actual shipment quantities', async () => {
|
||||
vi.clearAllMocks()
|
||||
const counts = { physicalPallets: null, palletSpaces: null, ownPackages: '10', externalPackages: '0', externalAgreedAmounts: [] }
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { page: { records: [{ ...counts, tripNo: 'LOCAL-1', destinations: [], externalPalletRates: [], measured: null, costs: null }], total: '1', page: '1', size: '20', pages: '1' }, summary: { ...counts, physicalPallets: '0', palletSpaces: '0', trips: '1', inTransit: '1', completed: '0', costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }, charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] } } })
|
||||
const result = await listTripOperations({ from: '2026-10-26T00:00:00Z', to: '2026-10-28T00:00:00Z' })
|
||||
expect(result.page.records[0]).toMatchObject({ physicalPallets: null, palletSpaces: null, ownPackages: 10, externalPackages: 0 })
|
||||
})
|
||||
|
||||
@@ -15,12 +15,12 @@ export interface TripOperationsSeries { label: string; unit: string; points: { l
|
||||
export interface TripOperationsCharts { physicalPallets: TripOperationsSeries[]; destinationTrips: TripOperationsSeries[]; externalAgreedAmounts: TripOperationsSeries[]; externalPalletRates: TripOperationsSeries[] }
|
||||
export interface TripOperationsRow {
|
||||
tripNo: string; status: string; vehiclePlate: string | null; driverName: string | null; warehouseName: string | null; departedAt: string; completedAt: string | null; destinations: { key: string; label: string }[]
|
||||
physicalPallets: number; palletSpaces: number; ownPackages: number; externalPackages: number; externalAgreedAmounts: TripOperationsMoney[]; financeStatus: string; costStatus: string
|
||||
physicalPallets: number | null; palletSpaces: number | null; ownPackages: number; externalPackages: number; externalAgreedAmounts: TripOperationsMoney[]; financeStatus: string; costStatus: string
|
||||
externalPalletRates: { currency: string; pricedPalletSpaces: number; agreedAmount: number }[]; measured: TripOperationsMeasured | null; costs: OperationsRowCosts | null
|
||||
}
|
||||
export interface TripOperationsSummary { trips: number; inTransit: number; completed: number; physicalPallets: number; palletSpaces: number; ownPackages: number; externalPackages: number; externalAgreedAmounts: TripOperationsMoney[]; costs: OperationsCosts }
|
||||
export interface TripOperationsResult { page: PageResult<TripOperationsRow>; summary: TripOperationsSummary; charts: TripOperationsCharts }
|
||||
type WireNumbers<T> = { [K in keyof T]: T[K] extends number ? number | string : T[K] }
|
||||
type WireNumbers<T> = { [K in keyof T]: T[K] extends number | null ? number | string | Extract<T[K], null> : T[K] }
|
||||
type WireMoney = { currency: string; amount: string | number }
|
||||
type WireEvidence = Omit<TripOperationsEvidence, 'sizeBytes'> & { sizeBytes: number | string }
|
||||
type WireMeasured = { actualDistanceMiles: string | number | null; actualFuelLitres: string | number | null; remark: string | null; evidenceFiles?: WireEvidence[] }
|
||||
@@ -29,7 +29,7 @@ type WireRow = Omit<WireNumbers<TripOperationsRow>, 'externalAgreedAmounts' | 'm
|
||||
type WireSummary = Omit<WireNumbers<TripOperationsSummary>, 'externalAgreedAmounts' | 'costs'> & { externalAgreedAmounts: WireMoney[]; costs: { visible: boolean; unrecordedTrips: string | number | null; unallocatedTrips: string | number | null; totals: WireCostTotal[] } }
|
||||
const costTotals = (rows: WireCostTotal[]): OperationsCostTotal[] => rows.map(r => ({ ...r, directCost: fleetMoney(r.directCost), ownCost: r.ownCost == null ? null : fleetMoney(r.ownCost), externalCost: r.externalCost == null ? null : fleetMoney(r.externalCost) }))
|
||||
const money = (rows: WireMoney[]) => rows.map(r => ({ ...r, amount: fleetMoney(r.amount) }))
|
||||
const counts = (r: WireRow | WireSummary) => ({ physicalPallets: fleetNumber(r.physicalPallets), palletSpaces: fleetNumber(r.palletSpaces), ownPackages: fleetNumber(r.ownPackages), externalPackages: fleetNumber(r.externalPackages), externalAgreedAmounts: money(r.externalAgreedAmounts) })
|
||||
const counts = (r: WireRow | WireSummary) => ({ physicalPallets: r.physicalPallets == null ? null : fleetNumber(r.physicalPallets), palletSpaces: r.palletSpaces == null ? null : fleetNumber(r.palletSpaces), ownPackages: fleetNumber(r.ownPackages), externalPackages: fleetNumber(r.externalPackages), externalAgreedAmounts: money(r.externalAgreedAmounts) })
|
||||
const evidence = (r: WireEvidence): TripOperationsEvidence => {
|
||||
if (typeof r.fileId !== 'string' || !/^[1-9]\d*$/.test(r.fileId) || !r.fileName || !['application/pdf', 'image/jpeg', 'image/png'].includes(r.contentType)) throw new Error('实测凭证信息无效')
|
||||
return { ...r, sizeBytes: fleetNumber(r.sizeBytes) }
|
||||
@@ -40,7 +40,7 @@ type WireCharts = { [K in keyof TripOperationsCharts]: WireSeries[] }
|
||||
const series = (values: WireSeries[]) => values.map(s => ({ ...s, points: s.points.map(p => ({ ...p, value: fleetNumber(p.value) })) }))
|
||||
export async function listTripOperations(query: TripOperationsQuery): Promise<TripOperationsResult> {
|
||||
const result = unwrapApiResponse(await http.get<ApiEnvelope<{ page: PageResult<WireRow>; summary: WireSummary; charts: WireCharts }>, ApiEnvelope<{ page: PageResult<WireRow>; summary: WireSummary; charts: WireCharts }>>('/platform/v1/last-leg/fleet/operations', { params: query }))
|
||||
return { page: fleetPage({ ...result.page, records: result.page.records.map(r => ({ ...r, ...counts(r), measured: r.measured == null ? null : measured(r.measured), externalPalletRates: r.externalPalletRates.map(p => ({ ...p, pricedPalletSpaces: fleetNumber(p.pricedPalletSpaces), agreedAmount: fleetMoney(p.agreedAmount) })), costs: r.costs == null ? null : { allocationBlockReason: r.costs.allocationBlockReason, totals: costTotals(r.costs.totals) } })) }), summary: { ...result.summary, ...counts(result.summary), trips: fleetNumber(result.summary.trips), inTransit: fleetNumber(result.summary.inTransit), completed: fleetNumber(result.summary.completed), costs: { ...result.summary.costs, unrecordedTrips: result.summary.costs.unrecordedTrips == null ? null : fleetNumber(result.summary.costs.unrecordedTrips), unallocatedTrips: result.summary.costs.unallocatedTrips == null ? null : fleetNumber(result.summary.costs.unallocatedTrips), totals: costTotals(result.summary.costs.totals) } }, charts: { physicalPallets: series(result.charts.physicalPallets), destinationTrips: series(result.charts.destinationTrips), externalAgreedAmounts: series(result.charts.externalAgreedAmounts), externalPalletRates: series(result.charts.externalPalletRates) } }
|
||||
return { page: fleetPage({ ...result.page, records: result.page.records.map(r => ({ ...r, ...counts(r), measured: r.measured == null ? null : measured(r.measured), externalPalletRates: r.externalPalletRates.map(p => ({ ...p, pricedPalletSpaces: fleetNumber(p.pricedPalletSpaces), agreedAmount: fleetMoney(p.agreedAmount) })), costs: r.costs == null ? null : { allocationBlockReason: r.costs.allocationBlockReason, totals: costTotals(r.costs.totals) } })) }), summary: { ...result.summary, ...counts(result.summary), physicalPallets: fleetNumber(result.summary.physicalPallets), palletSpaces: fleetNumber(result.summary.palletSpaces), trips: fleetNumber(result.summary.trips), inTransit: fleetNumber(result.summary.inTransit), completed: fleetNumber(result.summary.completed), costs: { ...result.summary.costs, unrecordedTrips: result.summary.costs.unrecordedTrips == null ? null : fleetNumber(result.summary.costs.unrecordedTrips), unallocatedTrips: result.summary.costs.unallocatedTrips == null ? null : fleetNumber(result.summary.costs.unallocatedTrips), totals: costTotals(result.summary.costs.totals) } }, charts: { physicalPallets: series(result.charts.physicalPallets), destinationTrips: series(result.charts.destinationTrips), externalAgreedAmounts: series(result.charts.externalAgreedAmounts), externalPalletRates: series(result.charts.externalPalletRates) } }
|
||||
}
|
||||
export interface TripOperationsRecord { tripNo: string; recordVersion: string; details: TripOperationsMeasured; updatedAt: string | null }
|
||||
export interface TripOperationsRecordSave { recordVersion: string; idempotencyKey: string; details: TripOperationsMeasuredInput; reason: string | null }
|
||||
|
||||
@@ -10,7 +10,8 @@ export interface OwnedBusinessDestination { destinationKey: string; warehouseCod
|
||||
export interface OwnedBusinessRow {
|
||||
dispatchNo: string; stage: OwnedBusinessStage; plannedDepartureAt: string; loadingCompletedAt: string; departedAt: string | null; deliveryCompletedAt: string | null
|
||||
origin: { warehouseId: string; warehouseCode: string; warehouseName: string }
|
||||
vehicle: { vehicleId: string | null; plate: string | null; ownership: 'OWNED' | 'DISPATCHED' }; driver: { name: string | null; phone: string | null }
|
||||
taskType?: 'DELIVERY' | 'LOCAL_TRUCK' | 'PICKUP'
|
||||
vehicle: { vehicleId: string | null; plate: string | null; ownership: 'OWNED' | 'DISPATCHED' | null }; driver: { name: string | null; phone: string | null }
|
||||
destinations: OwnedBusinessDestination[]; actualPalletCount: number | null; operations: OwnedBusinessOperations; unitPrice: string | null; recordVersion: string; dispatchRecordVersion: string
|
||||
photoSummary: { loadingCount: number; fuelCount: number }; actions: { edit: OwnedBusinessGate; upload: OwnedBusinessGate }; sourceIssues: string[]
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const cargoColumns: DataTableColumn[] = [{ prop: 'customerName', label: '客户'
|
||||
const historyColumns: DataTableColumn[] = [{ key: 'source', label: '事件来源', minWidth: 110, slotName: 'source' }, { key: 'revision', label: '运营版本', minWidth: 90, prop: 'recordVersion' }, { prop: 'dispatchRecordVersion', label: '排车版本', minWidth: 90 }, { key: 'action', label: '操作', minWidth: 150, slotName: 'action' }, { prop: 'operatorName', label: '操作人', minWidth: 140 }, { key: 'occurredAt', label: '操作时间', minWidth: 180, slotName: 'occurredAt' }, { prop: 'reason', label: '说明', minWidth: 160 }, { key: 'snapshot', label: '当时运营信息', minWidth: 250, slotName: 'snapshot' }, { key: 'photos', label: '当时照片', minWidth: 190, slotName: 'photos' }]
|
||||
const taskFields = computed(() => {
|
||||
const row = detail.value?.row
|
||||
return [['计划发车', time(row?.plannedDepartureAt)], ['装车完成', time(row?.loadingCompletedAt)], ['实际发车', time(row?.departedAt)], ['派送完成', time(row?.deliveryCompletedAt)], ['车牌', text(row?.vehicle.plate)], ['司机', text(row?.driver.name)], ['司机电话', text(row?.driver.phone)], ['提货仓库', text(row?.origin.warehouseName)], ['实际物理托数', text(row?.actualPalletCount)], ['整车金额', row?.operations.money ? `${row.operations.money.currency} ${row.operations.money.amount}` : '未登记'], ['单价', row?.unitPrice == null ? '—' : `${row.operations.money?.currency || ''} ${row.unitPrice}`], ['实际里程(mile)', row?.operations.actualDistanceMiles ?? '未登记'], ['油耗(L/100km)', row?.operations.fuelConsumptionLitresPer100Km ?? '未登记']]
|
||||
return [['计划发车', time(row?.plannedDepartureAt)], ['装车完成', time(row?.loadingCompletedAt)], ['实际发车', time(row?.departedAt)], ['派送完成', time(row?.deliveryCompletedAt)], ['车牌', text(row?.vehicle.plate)], ['司机', text(row?.driver.name)], ['司机电话', text(row?.driver.phone)], [row?.taskType === 'LOCAL_TRUCK' ? '起点' : '提货仓库', text(row?.origin.warehouseName)], ['实际物理托数', text(row?.actualPalletCount)], ['整车金额', row?.operations.money ? `${row.operations.money.currency} ${row.operations.money.amount}` : '未登记'], ['单价', row?.unitPrice == null ? '—' : `${row.operations.money?.currency || ''} ${row.unitPrice}`], ['实际里程(mile)', row?.operations.actualDistanceMiles ?? '未登记'], ['油耗(L/100km)', row?.operations.fuelConsumptionLitresPer100Km ?? '未登记']]
|
||||
})
|
||||
function time(value: string | null | undefined) { return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—' }
|
||||
function destinationLabel(key: string) { const value = detail.value?.row.destinations.find(destination => destination.destinationKey === key); return value?.warehouseCode || value?.warehouseName || value?.address || '—' }
|
||||
|
||||
@@ -36,9 +36,9 @@ const summary = computed(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 245, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'appointments', label: '预约号', minWidth: 195, slotName: 'appointments' }, { prop: 'dispatchNo', label: '排车单号', minWidth: 300 },
|
||||
{ prop: 'vehicle.plate', label: '车牌', minWidth: 130 }, { key: 'driver', label: '司机', minWidth: 150, slotName: 'driver' }, { prop: 'origin.warehouseName', label: '提货仓库', minWidth: 150 },
|
||||
{ key: 'destinations', label: '目的仓', minWidth: 150, slotName: 'destinations' },
|
||||
{ key: 'deliveryWarehouses', label: '派送仓库', minWidth: 150, slotName: 'destinations' },
|
||||
{ prop: 'vehicle.plate', label: '车牌', minWidth: 130 }, { key: 'driver', label: '司机', minWidth: 150, slotName: 'driver' }, { prop: 'origin.warehouseName', label: '起点', minWidth: 150 },
|
||||
{ key: 'destinations', label: '目的地', minWidth: 150, slotName: 'destinations' },
|
||||
|
||||
{ key: 'fuel', label: '油耗(L/100km)', minWidth: 145, slotName: 'fuel' }, { prop: 'actualPalletCount', label: '实际托数', minWidth: 105 },
|
||||
{ key: 'money', label: '总价', minWidth: 170, align: 'right', slotName: 'money' },
|
||||
{ key: 'unitPrice', label: '单价(/托)', minWidth: 160, align: 'right', slotName: 'unitPrice' },
|
||||
@@ -50,6 +50,10 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'stage', label: '状态', minWidth: 110, slotName: 'stage' },
|
||||
]
|
||||
function time(value: string | null | undefined) { return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—' }
|
||||
// 全列无值时只显示一个占位;多站有值时保留顺序,目的地通过悬停说明。
|
||||
function hasAppointments(row: OwnedBusinessRow) { return row.destinations.some(destination => destination.appointments.length > 0) }
|
||||
function hasScheduledArrival(row: OwnedBusinessRow) { return row.destinations.some(destination => destination.appointments.some(appointment => !!appointment.scheduledAt)) }
|
||||
function hasActualArrival(row: OwnedBusinessRow) { return row.destinations.some(destination => !!destination.actualArrivedAt) }
|
||||
async function load() {
|
||||
if (!active) return
|
||||
const request = ++generation
|
||||
@@ -102,10 +106,10 @@ onBeforeUnmount(() => { active = false; generation++; openGeneration++ })
|
||||
<template #notices><LoadErrorState v-if="error" title="自有业务读取未完成" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState><p v-if="warehouseError">{{ warehouseError.message }} <el-button link @click="loadWarehouses">重试目录</el-button></p><p v-if="!access.can('platform:warehouse:list')">没有公共仓库目录读取权限,仓库筛选暂不可选。</p></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" link @click="router.push(`/overseas-master-data/owned-business/${encodeURIComponent(row.dispatchNo)}`)">查看明细</el-button><el-button v-if="access.can(permission.ownedBusinessEdit)" class="density-action" link :disabled="opening || mutating || !row.actions.edit.allowed" @click="openAction(row as OwnedBusinessRow, 'operations')">登记运营信息</el-button><el-button class="density-action" link :disabled="opening || mutating" @click="openAction(row as OwnedBusinessRow, 'photos')">{{ access.can(permission.ownedBusinessUpload) && row.actions.upload.allowed ? '上传照片' : '查看照片' }}</el-button></div></template>
|
||||
<template #driver="{ row }">{{ text(row.driver.name) }}<br>{{ text(row.driver.phone) }}</template>
|
||||
<template #appointments="{ row }"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination"><strong>{{ destination.warehouseCode || destination.warehouseName || '—' }}</strong><div v-for="appointment in destination.appointments" :key="appointment.appointmentNo">{{ appointment.appointmentNo }}<small v-if="appointment.confirmationNo">确认号 {{ appointment.confirmationNo }}</small></div><span v-if="!destination.appointments.length">—</span></div></template>
|
||||
<template #appointments="{ row }"><template v-if="hasAppointments(row as OwnedBusinessRow)"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination" :title="destination.warehouseCode || destination.warehouseName || destination.address || undefined"><div v-for="appointment in destination.appointments" :key="appointment.appointmentNo">{{ appointment.appointmentNo }}<small v-if="appointment.confirmationNo">确认号 {{ appointment.confirmationNo }}</small></div><span v-if="!destination.appointments.length">—</span></div></template><span v-else>—</span></template>
|
||||
<template #destinations="{ row }"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination">{{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}</div><span v-if="!row.destinations.length">—</span></template>
|
||||
<template #scheduledArrival="{ row }"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination"><strong>{{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}</strong><div v-for="appointment in destination.appointments" :key="appointment.appointmentNo">{{ time(appointment.scheduledAt) }}</div><span v-if="!destination.appointments.length">—</span></div><span v-if="!row.destinations.length">—</span></template>
|
||||
<template #actualArrival="{ row }"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination"><strong>{{ destination.warehouseCode || destination.warehouseName || destination.address || '—' }}</strong><div>{{ time(destination.actualArrivedAt) }}</div></div><span v-if="!row.destinations.length">—</span></template>
|
||||
<template #scheduledArrival="{ row }"><template v-if="hasScheduledArrival(row as OwnedBusinessRow)"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination" :title="destination.warehouseCode || destination.warehouseName || destination.address || undefined"><div v-for="appointment in destination.appointments" :key="appointment.appointmentNo">{{ time(appointment.scheduledAt) }}</div><span v-if="!destination.appointments.length">—</span></div></template><span v-else>—</span></template>
|
||||
<template #actualArrival="{ row }"><template v-if="hasActualArrival(row as OwnedBusinessRow)"><div v-for="destination in row.destinations" :key="destination.destinationKey" class="owned-business-destination" :title="destination.warehouseCode || destination.warehouseName || destination.address || undefined">{{ time(destination.actualArrivedAt) }}</div></template><span v-else>—</span></template>
|
||||
<template #fuel="{ row }">{{ row.operations.fuelConsumptionLitresPer100Km ?? '未登记' }}</template>
|
||||
<template #money="{ row }"><template v-if="row.operations.money">{{ row.operations.money.currency }} {{ row.operations.money.amount }}</template><span v-else>未登记</span></template>
|
||||
<template #unitPrice="{ row }"><template v-if="row.operations.money && row.unitPrice != null">{{ row.operations.money.currency }} {{ row.unitPrice }}</template><span v-else>{{ row.operations.money ? '—' : '未登记' }}</span></template>
|
||||
|
||||
@@ -38,19 +38,35 @@ describe('owned business views', () => {
|
||||
calls.list.mockResolvedValue({ records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } })
|
||||
const wrapper = mount(OwnedBusinessView, { global }); await flushPromises()
|
||||
const columns = wrapper.findComponent(Table).props('columns') as { key?: string; label: string; slotName?: string }[]
|
||||
expect(columns.map(column => column.label)).toEqual(expect.arrayContaining(['目的仓', '派送仓库', '总价', '单价(/托)', '最晚装车完成时间', '预约到达时间', '实际到达时间', '状态']))
|
||||
expect(columns.find(column => column.key === 'deliveryWarehouses')!.slotName).toBe('destinations')
|
||||
expect(columns.map(column => column.label)).toEqual(expect.arrayContaining(['目的地', '总价', '单价(/托)', '最晚装车完成时间', '预约到达时间', '实际到达时间', '状态']))
|
||||
expect(columns.filter(column => column.slotName === 'destinations')).toHaveLength(1)
|
||||
expect(wrapper.get('[data-column="money"]').text()).toBe('USD 0.00')
|
||||
expect(wrapper.get('[data-column="unitPrice"]').text()).toBe('USD 0.00')
|
||||
expect(wrapper.get('[data-column="latestLoaded"]').text()).toBe('—')
|
||||
const scheduled = wrapper.findAll('[data-column="scheduledArrival"] .owned-business-destination')
|
||||
expect(scheduled.map(cell => cell.text())).toEqual(['FBA-1' + dayjs('2026-10-21T09:00:00Z').format('YYYY-MM-DD HH:mm'), 'FBA-2—'])
|
||||
expect(scheduled.map(cell => cell.text())).toEqual([dayjs('2026-10-21T09:00:00Z').format('YYYY-MM-DD HH:mm'), '—'])
|
||||
expect(scheduled.map(cell => cell.attributes('title'))).toEqual(['FBA-1', 'FBA-2'])
|
||||
const actual = wrapper.findAll('[data-column="actualArrival"] .owned-business-destination')
|
||||
expect(actual.map(cell => cell.text())).toEqual(['FBA-1—', 'FBA-2' + dayjs('2026-10-21T10:00:00Z').format('YYYY-MM-DD HH:mm')])
|
||||
expect(actual.map(cell => cell.text())).toEqual(['—', dayjs('2026-10-21T10:00:00Z').format('YYYY-MM-DD HH:mm')])
|
||||
expect(calls.detail).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('shows one placeholder for missing appointment and arrival values without warehouse names', async () => {
|
||||
const detail = ownedBusinessFixture()
|
||||
detail.row.destinations = [
|
||||
{ destinationKey: 'one', warehouseCode: null, warehouseName: '洛杉矶仓', address: null, appointments: [], actualArrivedAt: null },
|
||||
{ destinationKey: 'two', warehouseCode: null, warehouseName: '龙华', address: null, appointments: [], actualArrivedAt: null },
|
||||
]
|
||||
calls.list.mockResolvedValue({ records: [detail.row], page: 1, size: 20, total: 1, summary: {} })
|
||||
const wrapper = mount(OwnedBusinessView, { global }); await flushPromises()
|
||||
for (const column of ['appointments', 'scheduledArrival', 'actualArrival']) {
|
||||
expect(wrapper.get(`[data-column="${column}"]`).text()).toBe('—')
|
||||
}
|
||||
expect(wrapper.get('[data-column="destinations"]').text()).toContain('洛杉矶仓')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps a non-pool external vehicle returned by the server without filtering or requiring a vehicle id', async () => {
|
||||
const detail = ownedBusinessFixture()
|
||||
detail.row.vehicle = { vehicleId: null, plate: 'EXTERNAL-NO-POOL', ownership: 'DISPATCHED' }
|
||||
|
||||
@@ -36,10 +36,10 @@ const fields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 220 },
|
||||
{ key: 'status', label: '状态', width: 125, slotName: 'status' }, { key: 'vehiclePlate', label: '发车车牌', minWidth: 125 }, { key: 'driverName', label: '发车司机', minWidth: 100 },
|
||||
{ key: 'warehouseName', label: '起运仓', minWidth: 150 }, { key: 'destinations', label: '目的地', minWidth: 180, slotName: 'destinations' },
|
||||
{ key: 'departedAt', label: '实际发车(UTC)', minWidth: 190 }, { key: 'physicalPallets', label: '实物托盘', width: 100 },
|
||||
{ key: 'departedAt', label: '实际发车(UTC)', minWidth: 190 }, { key: 'physicalPallets', label: '实物托盘', width: 100, formatter: (row: TripOperationsRow) => row.physicalPallets ?? '—' },
|
||||
{ key: 'agreed', label: '外接约定金额', minWidth: 170, slotName: 'agreed' },
|
||||
{ key: 'completedAt', label: '货物结案(UTC)', minWidth: 190, defaultVisible: false },
|
||||
{ key: 'palletSpaces', label: '标准托位', width: 100, defaultVisible: false }, { key: 'ownPackages', label: '自有箱数', width: 100, defaultVisible: false }, { key: 'externalPackages', label: '外接箱数', width: 100, defaultVisible: false },
|
||||
{ key: 'palletSpaces', label: '标准托位', width: 100, defaultVisible: false, formatter: (row: TripOperationsRow) => row.palletSpaces ?? '—' }, { key: 'ownPackages', label: '自有箱数', width: 100, defaultVisible: false }, { key: 'externalPackages', label: '外接箱数', width: 100, defaultVisible: false },
|
||||
{ key: 'distance', label: '实际里程(mile)', width: 140, slotName: 'distance', defaultVisible: false }, { key: 'fuel', label: '实际油耗(L)', width: 130, slotName: 'fuel', defaultVisible: false },
|
||||
{ key: 'finance', label: '应收 / 实收', minWidth: 260, slotName: 'finance', defaultVisible: false }, { key: 'cost', label: '已登记直接成本', minWidth: 200, slotName: 'cost', defaultVisible: false }, { key: 'allocated', label: '自有 / 外接分摊', minWidth: 240, slotName: 'allocated', defaultVisible: false }]
|
||||
function range(): TripOperationsQuery {
|
||||
|
||||
Reference in New Issue
Block a user