feat(fleet): show vehicle delivery and reassignment records
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 海外通用资料与车辆管理
|
||||
|
||||
- 车辆概览中的派送与更换记录由`components/VehicleFulfillmentTable.vue`、`api/vehicleFulfillment.ts`读取,替代原提示文字。派送要求vehicle:list与delivery:list,更换要求vehicle:list与trip:list;无对应权限不请求接口。两个可见分区各一次分页请求,不逐行加载;换车/换页忽略旧响应,错误保留重试及诊断。更换显示原/新车辆与司机、历史车次及排车业务号;现有快照没有操作人姓名时显示“—”,不用内部操作者ID回退。
|
||||
|
||||
- 适用应用规则见 `../../../AGENTS.md`;原型与交互矩阵为本目录 `PROTOTYPE_MATRIX.md`,新增司机页面使用其中用户提供的新版原型入口。
|
||||
- 报价、汇率、车辆现有 API 在 `api/overseasMasterData.ts`;司机 API 在 `api/drivers.ts`,字段与标签在 `driverTypes.ts`,后端合同为 Lastleg `fleet-drivers-v1.yaml`。
|
||||
- 共享车次接口在 `api/trips.ts`,类型在 `tripTypes.ts`,后端合同为 `fleet-trips-v1.yaml`;覆盖计划/分配、客户排车关联、实际装载、货物结案和分别结束车辆/司机任务。接口保留稳定幂等键、版本与货物hash,长ID按字符串透传;数量和容量展示服务端结果,不在浏览器推算整车可发或可结案。新页面承载按 `PROTOTYPE_MATRIX.md` 确定,不因API存在推导页面能力。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
| 汇率管理 `gen-exchange-rate` | 列表/筛选 | `10-general.html:21-33`、`app.js:3673-3699` | 独立列表页 | 四项摘要;月份筛选;8 列表格 | 已接摘要与真实分页 | 统一展示 `1 USD = X CNY` |
|
||||
| 汇率管理 | 更新/编辑 | `overlays.html:1695-1704`、`app.js:3701-3728` | 居中弹框 `modal-exchange-rate-edit` | 560px;月份、平均汇率、备注;已有月份覆盖提示 | 已接创建/覆盖、状态与导出 | recordVersion 冲突保留服务端错误 |
|
||||
| 车辆 `vehicle` | 列表/筛选 | `10-general.html:299-351`、`app.js:10437-10456`;当前对话于 2026-08-19 修正双页签问题,并指定复用统一订单列表 `UnifiedOrdersView.vue` 的 `status-tabs` 样式 | 独立列表页 | 原型同时使用国内/海外区域页签与自有/外派归属页签,形成重复主切换;原型操作列在末尾 | 已接真实分页;区域降为查询下拉框,仅保留自有/外派归属页签;操作列按全局约定移到第一列 | 当前对话确认高于原型;查询提交 ownership/region/status,不拼装数据 |
|
||||
| 车辆 | 详情 | `10-general.html:421-448`、`app.js:10546-10591` | 独立页面 `vehicle-detail` | 紧凑页头;车辆信息、派送订单明细、更换记录 | 已接真实车辆详情;派送区说明从现有排车/派送管理查看,更换区明确尚未接入 | 不拼装订单/派送数据;用户提示不展示内部接口说明 |
|
||||
| 车辆 | 详情 | `10-general.html`的`page-vehicle-detail`、`vehicle-detail-orders-title`(455)、`vehicle-detail-history-title`(459) | 独立页面 `vehicle-detail` | 紧凑页头;车辆信息、派送订单明细、更换记录;只读表不添加操作列 | 已接车辆详情和两个分页只读投影;派送保留单号、BOL/来源、客户、目的地、预约日期、状态和关联关系;车次统一改派按事件展示原/新车辆及司机和关联排车,不虚构逐派送改派事件。操作人姓名未记录时为“—” | 按稳定车辆身份及不可变历史关联,不按车牌猜测;使用派送/车次各自只读权限。真实1920×1080页面及请求量待新接口发布后验收 |
|
||||
| 车辆 | 新增/编辑 | `10-general.html:363-394`、`app.js:10458-10529`;本次车辆容量设计见车辆管理规划第4.1节 | 居中弹框 `modal-vehicle-edit` | 原760px两列;新增托位/载重/容积后按高密度规范扩为1080px三列 | 已接创建/编辑、独立状态与导出;三项容量同填或全空,不从车型文本推算 | create/edit/status 权限独立;占用中关键变更由服务端拒绝 |
|
||||
| 车辆 | 删除/导出 | `10-general.html:302-351`、`app.js:10437-10456` | 删除为列表行操作;导出在工具栏 | 原型列表操作 | 已接真实 CSV 导出;服务端无删除端点,未展示删除 | vehicle:export 独立校验;不推断 DELETE |
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { listVehicleDeliveries, listVehicleAssignmentChanges } from './vehicleFulfillment'
|
||||
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data }))
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
it('encodes the stable vehicle key and adapts serialized pagination without a plate or customer override', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { records: [{ deliveryNo: 'DL-1' }], page: '1', size: '10', total: '1', pages: '1' } })
|
||||
const result=await listVehicleDeliveries('900719925474099301')
|
||||
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/fleet/vehicles/900719925474099301/deliveries', { params: { page: 1, size: 10 } })
|
||||
expect(result.total).toBe(1)
|
||||
})
|
||||
it('history preserves large serialized versions and rejects invalid pagination', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { records: [{ tripNo:'TR-1', recordVersion:'900719925474099301' }], page:'1',size:'10',total:'1',pages:'1' } })
|
||||
expect((await listVehicleAssignmentChanges('8')).records[0]?.recordVersion).toBe('900719925474099301')
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { records: [], page:'1',size:'10',total:'not-a-number',pages:'1' } })
|
||||
await expect(listVehicleAssignmentChanges('8')).rejects.toThrow('分页数值')
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { PageResult } from './overseasMasterData'
|
||||
|
||||
export interface VehicleDelivery {
|
||||
deliveryNo: string; dispatchNo: string; tripNo: string; bolNo: string | null; customerName: string | null
|
||||
destinationType: string; destinationWarehouseCode: string | null; destinationWarehouseName: string | null
|
||||
deliveryAddress: string | null; plannedDeliveryAt: string | null; status: string; relationship: 'PLANNED' | 'DEPARTED'
|
||||
}
|
||||
export interface VehicleAssignmentChange {
|
||||
tripNo: string; recordVersion: string; occurredAt: string; reason: string | null
|
||||
previousVehiclePlate: string | null; vehiclePlate: string | null; previousDriverName: string | null; driverName: string | null
|
||||
relationship: 'REPLACED_OUT' | 'REPLACED_IN' | 'DRIVER_CHANGED'; dispatchNos: string[]
|
||||
}
|
||||
type Numeric = string | number
|
||||
type WirePage<T> = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }
|
||||
type WireChange = Omit<VehicleAssignmentChange, 'recordVersion'> & { recordVersion: Numeric }
|
||||
function integer(value: Numeric) {
|
||||
const number = Number(value)
|
||||
if (value == null || value === '' || !Number.isSafeInteger(number) || number < 0) throw new Error('车辆履约接口返回无效分页数值')
|
||||
return number
|
||||
}
|
||||
function page<T, W>(value: WirePage<W>, normalize: (row: W) => T): PageResult<T> {
|
||||
return { records: value.records.map(normalize), page: integer(value.page), size: integer(value.size), total: integer(value.total), pages: integer(value.pages) }
|
||||
}
|
||||
const base = (vehicleId: string) => `/platform/v1/last-leg/fleet/vehicles/${encodeURIComponent(vehicleId)}`
|
||||
export async function listVehicleDeliveries(vehicleId: string, currentPage = 1, size = 10) {
|
||||
const result = unwrapApiResponse(await http.get<ApiEnvelope<WirePage<VehicleDelivery>>, ApiEnvelope<WirePage<VehicleDelivery>>>(`${base(vehicleId)}/deliveries`, { params: { page: currentPage, size } }))
|
||||
return page(result, row => row)
|
||||
}
|
||||
export async function listVehicleAssignmentChanges(vehicleId: string, currentPage = 1, size = 10) {
|
||||
const result = unwrapApiResponse(await http.get<ApiEnvelope<WirePage<WireChange>>, ApiEnvelope<WirePage<WireChange>>>(`${base(vehicleId)}/assignment-history`, { params: { page: currentPage, size } }))
|
||||
return page(result, row => ({ ...row, recordVersion: String(row.recordVersion) }))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import VehicleFulfillmentTable from './VehicleFulfillmentTable.vue'
|
||||
import * as api from '../api/vehicleFulfillment'
|
||||
const permissions = vi.hoisted(() => ({ allowed: true }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => permissions.allowed }) }))
|
||||
vi.mock('../api/vehicleFulfillment', () => ({ listVehicleDeliveries: vi.fn(), listVehicleAssignmentChanges: vi.fn() }))
|
||||
const wrappers: VueWrapper[] = []
|
||||
const result = (number: string) => ({ records: [{ deliveryNo: number }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
function mount(kind: 'deliveries' | 'changes' = 'deliveries') {
|
||||
const wrapper = shallowMount(VehicleFulfillmentTable, { props: { vehicleId: '900719925474099301', kind }, global: { stubs: {
|
||||
DataTable: { name: 'DataTable', props: ['data','columns','page','pageSize','total','loading'], emits: ['update:page','update:pageSize'], template: '<div><slot name="toolbar-actions"/></div>' },
|
||||
ElButton: { template: '<button><slot/></button>' }, LoadErrorState: { name: 'LoadErrorState', props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true,
|
||||
} } }); wrappers.push(wrapper); return wrapper
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); permissions.allowed = true
|
||||
vi.mocked(api.listVehicleDeliveries).mockResolvedValue(result('DL-1') as Awaited<ReturnType<typeof api.listVehicleDeliveries>>)
|
||||
vi.mocked(api.listVehicleAssignmentChanges).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
||||
})
|
||||
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()) })
|
||||
it('loads only its displayed projection once and does not fetch per-row details', async () => {
|
||||
const wrapper = mount(); await flushPromises()
|
||||
expect(api.listVehicleDeliveries).toHaveBeenCalledExactlyOnceWith('900719925474099301',1,10)
|
||||
expect(api.listVehicleAssignmentChanges).not.toHaveBeenCalled()
|
||||
expect(wrapper.findComponent({ name: 'DataTable' }).props('data')).toEqual(result('DL-1').records)
|
||||
})
|
||||
it('does not call either endpoint without the required viewing permissions', async () => {
|
||||
permissions.allowed = false; const wrapper = mount(); await flushPromises()
|
||||
expect(api.listVehicleDeliveries).not.toHaveBeenCalled(); expect(api.listVehicleAssignmentChanges).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('暂无派送记录查看权限')
|
||||
})
|
||||
it('changing vehicle from a later page resets pagination and sends one scoped request', async () => {
|
||||
const wrapper = mount(); await flushPromises()
|
||||
wrapper.findComponent({ name: 'DataTable' }).vm.$emit('update:page', 3); await flushPromises()
|
||||
vi.mocked(api.listVehicleDeliveries).mockClear()
|
||||
await wrapper.setProps({ vehicleId: '900719925474099302' }); await flushPromises()
|
||||
expect(api.listVehicleDeliveries).toHaveBeenCalledExactlyOnceWith('900719925474099302',1,10)
|
||||
})
|
||||
it('a late response for the previous vehicle cannot replace the new vehicle records', async () => {
|
||||
let resolveOld!: (value: Awaited<ReturnType<typeof api.listVehicleDeliveries>>) => void
|
||||
vi.mocked(api.listVehicleDeliveries).mockImplementationOnce(() => new Promise(resolve => { resolveOld=resolve }))
|
||||
const wrapper = mount()
|
||||
await wrapper.setProps({ vehicleId: '900719925474099302' }); await flushPromises()
|
||||
resolveOld(result('DL-OLD') as Awaited<ReturnType<typeof api.listVehicleDeliveries>>); await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'DataTable' }).props('data')).toEqual(result('DL-1').records)
|
||||
})
|
||||
it('changes use the history endpoint and do not reveal current customer delivery data', async () => {
|
||||
mount('changes'); await flushPromises()
|
||||
expect(api.listVehicleAssignmentChanges).toHaveBeenCalledExactlyOnceWith('900719925474099301',1,10)
|
||||
expect(api.listVehicleDeliveries).not.toHaveBeenCalled()
|
||||
})
|
||||
it('failed reads show an error and clear stale rows instead of showing unrelated prior data', async () => {
|
||||
const wrapper = mount(); await flushPromises()
|
||||
vi.mocked(api.listVehicleDeliveries).mockRejectedValueOnce(new Error('读取失败'))
|
||||
await wrapper.find('button').trigger('click'); await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'DataTable' }).props('data')).toEqual([])
|
||||
expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasMasterDataPermissions as permission } from '../permissions'
|
||||
import { listVehicleDeliveries, listVehicleAssignmentChanges, type VehicleDelivery, type VehicleAssignmentChange } from '../api/vehicleFulfillment'
|
||||
|
||||
const props = defineProps<{ vehicleId: string; kind: 'deliveries' | 'changes' }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const allowed = computed(() => access.can(permission.vehicleList) && access.can(props.kind === 'deliveries' ? 'platform:overseas-warehouse:delivery:list' : permission.tripList))
|
||||
const rows = ref<(VehicleDelivery | VehicleAssignmentChange)[]>([]), page = ref(1), size = ref(10), total = ref(0), loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const title = computed(() => props.kind === 'deliveries' ? '派送订单明细' : '派送更换记录')
|
||||
let generation = 0
|
||||
const columns = computed<DataTableColumn[]>(() => props.kind === 'deliveries' ? [
|
||||
{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 210 },
|
||||
{ key: 'source', label: 'BOL / 来源排车', minWidth: 215, slotName: 'source' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 160 },
|
||||
{ key: 'destination', label: '目的仓 / 地址', minWidth: 180, slotName: 'destination' },
|
||||
{ key: 'plannedDeliveryAt', label: '预约日期', minWidth: 165, slotName: 'date' },
|
||||
{ key: 'status', label: '派送状态', width: 115, slotName: 'status' },
|
||||
{ key: 'relationship', label: '关联关系', minWidth: 220, slotName: 'relationship' },
|
||||
] : [
|
||||
{ key: 'occurredAt', label: '时间', minWidth: 165, slotName: 'date' },
|
||||
{ key: 'tripNo', prop: 'tripNo', label: '车次', minWidth: 210 },
|
||||
{ key: 'dispatchNos', label: '关联排车', minWidth: 210, slotName: 'dispatches' },
|
||||
{ key: 'relationship', label: '动作', width: 100, slotName: 'relationship' },
|
||||
{ key: 'vehicle', label: '原车辆 → 新车辆', minWidth: 230, slotName: 'vehicles' },
|
||||
{ key: 'driver', label: '原司机 → 新司机', minWidth: 180, slotName: 'drivers' },
|
||||
{ key: 'reason', prop: 'reason', label: '原因', minWidth: 200 },
|
||||
{ key: 'operator', label: '操作人', width: 100, slotName: 'operator' },
|
||||
])
|
||||
const statuses: Record<string, string> = { WAIT_DELIVERY: '待派送', DISPATCHED: '已派送', IN_TRANSIT: '运输中', DELIVERED: '已签收', RETURNED: '已退回', CANCELLED: '已取消', DELIVERY_FAILED: '派送异常' }
|
||||
const relations: Record<string, string> = { PLANNED: '当前计划车辆', DEPARTED: '实际发车车辆', REPLACED_OUT: '换出本车', REPLACED_IN: '换入本车', DRIVER_CHANGED: '更换司机' }
|
||||
function date(value: string | null | undefined) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||
function rowKey(row: VehicleDelivery | VehicleAssignmentChange) { return 'deliveryNo' in row ? row.deliveryNo : `${row.tripNo}:${row.recordVersion}` }
|
||||
async function load() {
|
||||
const request = ++generation
|
||||
rows.value = []; total.value = 0; error.value = null
|
||||
if (!allowed.value || !props.vehicleId) { loading.value = false; return }
|
||||
loading.value = true
|
||||
try {
|
||||
const result = props.kind === 'deliveries'
|
||||
? await listVehicleDeliveries(props.vehicleId, page.value, size.value)
|
||||
: await listVehicleAssignmentChanges(props.vehicleId, page.value, size.value)
|
||||
if (request === generation) { rows.value = result.records; total.value = result.total }
|
||||
} catch (cause) { if (request === generation) error.value = getApiErrorDetails(cause, `${title.value}加载失败`) }
|
||||
finally { if (request === generation) loading.value = false }
|
||||
}
|
||||
watch([() => props.vehicleId, () => props.kind, allowed, page, size], (current, prior) => {
|
||||
if (prior?.length && (current[0] !== prior[0] || current[1] !== prior[1] || current[4] !== prior[4]) && page.value !== 1) { page.value = 1; return }
|
||||
void load()
|
||||
}, { immediate: true })
|
||||
onBeforeUnmount(() => { generation++ })
|
||||
</script>
|
||||
<template>
|
||||
<section :aria-label="title">
|
||||
<p v-if="!allowed" class="fulfillment-note">暂无{{ kind === 'deliveries' ? '派送记录' : '车次历史' }}查看权限。</p>
|
||||
<template v-else>
|
||||
<p class="fulfillment-note">{{ kind === 'deliveries' ? '计划中的派送按当前分配车辆展示,实际发车后按发车记录归属车辆。' : '按车次保存更换前后的真实记录;同车次可能关联多张排车单。' }}</p>
|
||||
<LoadErrorState v-if="error" :title="`${title}加载失败`" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<DataTable v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" :row-key="rowKey" :empty-text="`暂无${title}`">
|
||||
<template #toolbar-actions><el-button :disabled="loading" @click="load">刷新</el-button></template>
|
||||
<template #source="{ row }"><div>{{ row.bolNo || '—' }}</div><div>{{ row.dispatchNo || '未关联排车' }}</div></template>
|
||||
<template #destination="{ row }">{{ row.destinationType === 'PRIVATE' ? row.deliveryAddress || '—' : row.destinationWarehouseCode || row.destinationWarehouseName || '—' }}</template>
|
||||
<template #date="{ row }">{{ date(kind === 'deliveries' ? row.plannedDeliveryAt : row.occurredAt) }}</template>
|
||||
<template #status="{ row }">{{ statuses[row.status] || row.status || '—' }}</template>
|
||||
<template #relationship="{ row }"><div>{{ relations[row.relationship] || '—' }}</div><div v-if="kind === 'deliveries'">{{ row.tripNo }}</div></template>
|
||||
<template #dispatches="{ row }"><div v-for="number in row.dispatchNos" :key="number">{{ number }}</div><span v-if="!row.dispatchNos.length">未关联排车</span></template>
|
||||
<template #vehicles="{ row }">{{ row.previousVehiclePlate || '—' }} → {{ row.vehiclePlate || '—' }}</template>
|
||||
<template #drivers="{ row }">{{ row.previousDriverName || '—' }} → {{ row.driverName || '—' }}</template>
|
||||
<template #operator>—</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>.fulfillment-note{margin:0 0 var(--admin-space-4);color:var(--cc-muted)}</style>
|
||||
@@ -37,5 +37,15 @@ describe('overseas master data real page mounts', () => {
|
||||
expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('mounts API-backed vehicle detail without fabricating fulfillment history', async () => { const wrapper = mountView(VehicleDetailView); await flushPromises(); expect(wrapper.get('.density-detail-title').text()).toBe('CA-TEST'); expect(wrapper.get('.density-fields').findAll('dt')).toHaveLength(14); expect(wrapper.get('.density-fields').text()).toContain('在役'); expect(wrapper.text()).not.toContain('IN_SERVICE'); expect(wrapper.text()).toContain('派送记录请在海外仓的排车管理或派送管理中查看') })
|
||||
it('mounts vehicle detail and scopes both real fulfillment projections to the loaded vehicle', async () => {
|
||||
const wrapper = mountView(VehicleDetailView); await flushPromises()
|
||||
expect(wrapper.get('.density-detail-title').text()).toBe('CA-TEST')
|
||||
expect(wrapper.get('.density-fields').findAll('dt')).toHaveLength(14)
|
||||
expect(wrapper.get('.density-fields').text()).toContain('在役')
|
||||
expect(wrapper.text()).not.toContain('IN_SERVICE')
|
||||
const tables = wrapper.findAllComponents({ name: 'VehicleFulfillmentTable' })
|
||||
expect(tables.map(table => table.props())).toEqual([
|
||||
{ vehicleId: 'VH-TEST-001', kind: 'deliveries' }, { vehicleId: 'VH-TEST-001', kind: 'changes' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,8 +76,9 @@ describe('overseas master data real contract pages', () => {
|
||||
expect(detail).toContain('派送订单明细')
|
||||
expect(detail).toContain('派送更换记录')
|
||||
expect(detail).toContain('getVehicle(')
|
||||
expect(detail).toContain('派送记录请在海外仓的排车管理或派送管理中查看')
|
||||
expect(detail).toContain('车辆更换记录尚未接入')
|
||||
expect(detail).toContain(':vehicle-id="detail.id" kind="deliveries"')
|
||||
expect(detail).toContain(':vehicle-id="detail.id" kind="changes"')
|
||||
expect(detail).not.toContain('车辆更换记录尚未接入')
|
||||
expect(detail).not.toContain('AppDrawer')
|
||||
expect(detail).not.toContain('const vehicle =')
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import '@/styles/adminDensity.css'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import VehicleCareTable from '../components/VehicleCareTable.vue'
|
||||
import VehicleFulfillmentTable from '../components/VehicleFulfillmentTable.vue'
|
||||
import type { VehicleCareType } from '../vehicleCareTypes'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { vehicleStatusLabels } from '../presentation'
|
||||
@@ -17,5 +18,5 @@ function back(){void router.push({name:'overseas-master-data-vehicles'})}
|
||||
watch(() => route.params.vehicleId, () => { activeTab.value='overview';void load() }, { immediate:true })
|
||||
onBeforeUnmount(() => { request++ })
|
||||
</script>
|
||||
<template><AdminPage class="admin-density" ><DetailPageLayout class="density-detail-layout surface-panel" aria-label="车辆详情"><template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="back"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ detail?.plateNumber || '—' }}</h1><StatusBadge v-if="detail" :status="vehicleStatusLabels[detail.status]" /></div></div></template><LoadErrorState v-if="error" title="车辆详情加载失败" :message="error.message" @retry="load"/><section v-loading="loading" class="detail-surface"><el-tabs v-if="detail" v-model="activeTab"><el-tab-pane label="车辆概览" name="overview" /><el-tab-pane label="保养及加油" name="care" /><el-tab-pane label="维修记录" name="REPAIR" /><el-tab-pane label="紧急故障" name="BREAKDOWN" /><el-tab-pane label="应急救援" name="RESCUE" /><el-tab-pane label="异常管理" name="EXCEPTION" /></el-tabs><template v-if="activeTab === 'overview'"><FormSection class="density-form-section" title="车辆信息"><dl v-if="detail" class="detail-grid density-fields"><div v-for="item in [['车牌号',detail.plateNumber],['车型',detail.model],['最大标准托位',detail.maxPalletSpaces??'待确认'],['最大载重(kg)',detail.maxLoadKg??'待确认'],['可用容积(m³)',detail.maxVolumeCbm??'待确认'],['油耗(L/100km)',detail.fuelConsumptionPer100Km],['当前公里码(km)',detail.mileageKm],['司机 / 外派单位',detail.driverOrDispatchCompany||'—'],['联系电话',detail.phone||'—'],['区域',detail.region==='OVERSEAS'?'海外':'国内'],['车辆归属',detail.ownership==='OWNED'?'自有车辆':'外派车辆'],['状态',vehicleStatusLabels[detail.status]],['更新时间',detail.updatedAt],['备注',detail.remark||'—']]" :key="item[0]" :class="{ 'wide-field': ['司机 / 外派单位', '备注'].includes(String(item[0])) }"><dt>{{item[0]}}</dt><dd>{{item[1]}}</dd></div></dl></FormSection><FormSection class="density-form-section" title="派送订单明细"><p class="contract-note">派送记录请在海外仓的排车管理或派送管理中查看。</p></FormSection><FormSection class="density-form-section" title="派送更换记录" :bordered="false"><p class="contract-note">车辆更换记录尚未接入。</p></FormSection></template><div v-if="detail && activeTab === 'care'" :key="detail.id" class="care-grid"><VehicleCareTable :vehicle-id="detail.id" :plate="detail.plateNumber" record-type="MAINTENANCE" /><VehicleCareTable :vehicle-id="detail.id" :plate="detail.plateNumber" record-type="FUEL" /></div><VehicleCareTable v-if="detail && ['REPAIR','BREAKDOWN','RESCUE','EXCEPTION'].includes(activeTab)" :key="`${detail.id}:${activeTab}`" :vehicle-id="detail.id" :plate="detail.plateNumber" :record-type="activeTab as VehicleCareType" /></section></DetailPageLayout></AdminPage></template>
|
||||
<style scoped>.care-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-6)}.detail-grid .wide-field { grid-column: span 2; }.contract-note{margin:0;padding:var(--admin-space-6);color:var(--cc-muted);text-align:center}</style>
|
||||
<template><AdminPage class="admin-density" ><DetailPageLayout class="density-detail-layout surface-panel" aria-label="车辆详情"><template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="back"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ detail?.plateNumber || '—' }}</h1><StatusBadge v-if="detail" :status="vehicleStatusLabels[detail.status]" /></div></div></template><LoadErrorState v-if="error" title="车辆详情加载失败" :message="error.message" @retry="load"/><section v-loading="loading" class="detail-surface"><el-tabs v-if="detail" v-model="activeTab"><el-tab-pane label="车辆概览" name="overview" /><el-tab-pane label="保养及加油" name="care" /><el-tab-pane label="维修记录" name="REPAIR" /><el-tab-pane label="紧急故障" name="BREAKDOWN" /><el-tab-pane label="应急救援" name="RESCUE" /><el-tab-pane label="异常管理" name="EXCEPTION" /></el-tabs><template v-if="activeTab === 'overview'"><FormSection class="density-form-section" title="车辆信息"><dl v-if="detail" class="detail-grid density-fields"><div v-for="item in [['车牌号',detail.plateNumber],['车型',detail.model],['最大标准托位',detail.maxPalletSpaces??'待确认'],['最大载重(kg)',detail.maxLoadKg??'待确认'],['可用容积(m³)',detail.maxVolumeCbm??'待确认'],['油耗(L/100km)',detail.fuelConsumptionPer100Km],['当前公里码(km)',detail.mileageKm],['司机 / 外派单位',detail.driverOrDispatchCompany||'—'],['联系电话',detail.phone||'—'],['区域',detail.region==='OVERSEAS'?'海外':'国内'],['车辆归属',detail.ownership==='OWNED'?'自有车辆':'外派车辆'],['状态',vehicleStatusLabels[detail.status]],['更新时间',detail.updatedAt],['备注',detail.remark||'—']]" :key="item[0]" :class="{ 'wide-field': ['司机 / 外派单位', '备注'].includes(String(item[0])) }"><dt>{{item[0]}}</dt><dd>{{item[1]}}</dd></div></dl></FormSection><FormSection class="density-form-section" title="派送订单明细"><VehicleFulfillmentTable v-if="detail" :vehicle-id="detail.id" kind="deliveries" /></FormSection><FormSection class="density-form-section" title="派送更换记录" :bordered="false"><VehicleFulfillmentTable v-if="detail" :vehicle-id="detail.id" kind="changes" /></FormSection></template><div v-if="detail && activeTab === 'care'" :key="detail.id" class="care-grid"><VehicleCareTable :vehicle-id="detail.id" :plate="detail.plateNumber" record-type="MAINTENANCE" /><VehicleCareTable :vehicle-id="detail.id" :plate="detail.plateNumber" record-type="FUEL" /></div><VehicleCareTable v-if="detail && ['REPAIR','BREAKDOWN','RESCUE','EXCEPTION'].includes(activeTab)" :key="`${detail.id}:${activeTab}`" :vehicle-id="detail.id" :plate="detail.plateNumber" :record-type="activeTab as VehicleCareType" /></section></DetailPageLayout></AdminPage></template>
|
||||
<style scoped>.care-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-6)}.detail-grid .wide-field { grid-column: span 2; }</style>
|
||||
|
||||
Reference in New Issue
Block a user