Merge pull request '统一司机、车次及出车记录列表样式' (#13) from release/test into main

This commit was merged in pull request #13.
This commit is contained in:
2026-10-07 12:25:14 +08:00
9 changed files with 73 additions and 44 deletions
@@ -1,5 +1,17 @@
# 海外通用资料原型核对矩阵
## 2026-10-07 三个车辆列表样式统一
依据:本轮用户提供的司机管理、车次管理、出车记录三张截图;对齐已确认的 `VehiclesView.vue` 连续列表及 `docs/Admin高密度界面规范.md`。仅改列表容器与排版,不改变下列操作承载、权限或请求。
| 页面 | 操作与当前承载 | 本轮结论 |
|---|---|---|
| 司机管理 | 查询/到期统计筛选;详情独立路由;新增/编辑1080px居中;证照预览620px居中;启停原因确认;审核在详情 | 列表接入integrated标题、筛选、统计、表格、分页;三项统计保留点击筛选,紧凑排列;各操作承载保持 |
| 车次管理 | 查询;新建1080px计划弹框;详情独立路由;编辑、审核、确认在详情;列表无文件预览/删除 | 接入相同integrated列表,详情操作保留在第一列;弹框与详情保持 |
| 出车记录 | 日期/货源筛选、图表和汇总;导出;车次详情独立路由;实测记录820px居中及其中凭证预览;列表无删除/审核 | 图表、说明与汇总放入连续列表内,统一间距,空图表不重复显示空系列标题;保留未知金额、成本及统计说明 |
改后视觉结论待用户在1920×1080、100%缩放提供截图确认;自动检查单独记录,不以测试代替视觉通过。
事实来源:`docs/悦/货代SAAS平台后台/prototype-src/modules/10-general.html`、`prototype-src/overlays.html`、`prototype-src/app.js`。
| 页面 | 操作 | 原型证据 | 原型承载方式 | 原型尺寸/布局 | 当前实现 | 结论 |
@@ -13,14 +13,15 @@ function width(value: number, series: TripOperationsSeries) { const max = Math.m
<template><section class="operations-charts" aria-label="出车数据统计"><article v-for="section in sections" :key="section.title" class="chart">
<strong>{{ section.title }}</strong><p>{{ section.note }}</p>
<p v-if="!section.series.some(s => s.points.length)">暂无对应统计</p>
<div class="chart-values"><div v-for="(series, si) in section.series" :key="`${series.label}-${si}`"><b>{{ series.label }}({{ series.unit }})</b>
<div class="chart-values"><template v-for="(series, si) in section.series" :key="`${series.label}-${si}`"><div v-if="series.points.length"><b>{{ series.label }}({{ series.unit }})</b>
<div v-for="(point, pi) in series.points" :key="pi" class="point"><span>{{ point.label }}</span><div class="bar-track" aria-hidden="true"><div class="bar" :style="{ width: width(point.value, series) + '%' }" /></div><span>{{ point.value }}</span></div>
</div></div>
</div></template></div>
</article></section></template>
<style scoped>
.operations-charts { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:var(--admin-space-3); }
.chart { padding:var(--admin-space-3); border:1px solid var(--el-border-color); border-radius:var(--admin-radius); background:var(--admin-surface); }
.chart p { color:var(--el-text-color-secondary); font-size:var(--admin-font-caption); }
.chart { display:grid; align-content:start; gap:var(--admin-space-2); min-width:0; padding:var(--admin-space-3); border:1px solid var(--cc-border); border-radius:var(--admin-radius); background:var(--admin-surface); }
.chart > strong { font-size:var(--admin-font-section); }
.chart p { margin:0; line-height:1.6; color:var(--cc-muted); font-size:var(--admin-font-caption); }
.chart-values { max-height:220px; overflow:auto; }
.point { display:grid; grid-template-columns:minmax(95px,1fr) minmax(45px,1fr) minmax(40px,auto); gap:var(--admin-space-2); align-items:center; margin:var(--admin-space-2) 0; }
.bar-track { height:10px; background:var(--el-fill-color-light); }.bar { height:100%; background:var(--el-color-primary); }
@@ -29,7 +29,7 @@ const layout = { template: '<div><slot name="header"/><slot/></div>' }
const button = { props: ['disabled', 'loading'], template: '<button :disabled="disabled || loading"><slot/></button>' }
const dialog = { name: 'AppDialog', props: ['modelValue'], emits: ['confirm', 'update:modelValue'], template: '<div v-if="modelValue" data-test="dialog"><slot/><button data-test="confirm" @click="$emit(\'confirm\')">保存</button></div>' }
const input = { name: 'ElInput', props: ['modelValue'], emits: ['update:modelValue'], template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />' }
const table = { name: 'DataTable', props: ['data', 'page', 'pageSize', 'total', 'loading'], emits: ['update:page', 'update:pageSize'], template: '<div><slot name="toolbar-actions"/><div v-for="row in data" :key="row.id || row.recordVersion"><span>{{row.driverNo}}</span><slot name="actions" :row="row"/><slot name="file" :row="row"/></div></div>' }
const table = { name: 'DataTable', props: ['data', 'page', 'pageSize', 'total', 'loading'], emits: ['update:page', 'update:pageSize'], template: '<div><slot name="toolbar-start"/><slot name="toolbar-actions"/><slot name="filters"/><slot name="notices"/><div v-for="row in data" :key="row.id || row.recordVersion"><span>{{row.driverNo}}</span><slot name="actions" :row="row"/><slot name="file" :row="row"/></div></div>' }
const stubs = { AdminPage: layout, ListPageLayout: layout, DetailPageLayout: layout, FormSection: layout, DataTable: table, QueryToolbar: true, RowActions: true, StatusBadge: true, LoadErrorState: true, AppDialog: dialog, DriverEditorDialog: true, DriverLicensePreview: true, ElButton: button, ElForm: layout, ElFormItem: layout, ElInput: input, ElSelect: { ...input, name: 'ElSelect' }, ElOption: true, ElDatePicker: { ...input, name: 'ElDatePicker' } }
const wrappers: VueWrapper[] = []
function mount(component: object, props: Record<string, unknown> = {}): VueWrapper {
@@ -31,11 +31,11 @@ const editorOpen = ref(false), editing = ref<DriverDetail | null>(null), actionI
const previewOpen = ref(false), preview = ref<DriverDetail | null>(null)
const fields: QueryField[] = [
{ key: 'keyword', type: 'input', label: '司机关键词', placeholder: '姓名 / 手机 / 司机编号', width: 220 },
{ key: 'region', type: 'select', label: '区域', width: 120, options: [{ label: '国内', value: 'DOMESTIC' }, { label: '海外', value: 'OVERSEAS' }] },
{ key: 'ownership', type: 'select', label: '归属', width: 140, options: [{ label: '自有司机', value: 'OWNED' }, { label: '外派司机', value: 'DISPATCHED' }] },
{ key: 'active', type: 'select', label: '状态', width: 120, options: [{ label: '启用', value: 'true' }, { label: '停用', value: 'false' }] },
{ key: 'reviewStatus', type: 'select', label: '审核状态', width: 140, options: Object.entries(driverReviewLabels).map(([value, label]) => ({ value, label })) },
{ key: 'licenseState', type: 'select', label: '驾照有效期', width: 150, options: Object.entries(driverLicenseLabels).map(([value, label]) => ({ value, label })) },
{ key: 'region', type: 'select', label: '区域', placeholder: '全部区域', width: 120, options: [{ label: '国内', value: 'DOMESTIC' }, { label: '海外', value: 'OVERSEAS' }] },
{ key: 'ownership', type: 'select', label: '归属', placeholder: '全部归属', width: 140, options: [{ label: '自有司机', value: 'OWNED' }, { label: '外派司机', value: 'DISPATCHED' }] },
{ key: 'active', type: 'select', label: '状态', placeholder: '全部状态', width: 120, options: [{ label: '启用', value: 'true' }, { label: '停用', value: 'false' }] },
{ key: 'reviewStatus', type: 'select', label: '审核状态', placeholder: '全部审核状态', width: 140, options: Object.entries(driverReviewLabels).map(([value, label]) => ({ value, label })) },
{ key: 'licenseState', type: 'select', label: '驾照有效期', placeholder: '全部驾照有效期', width: 150, options: Object.entries(driverLicenseLabels).map(([value, label]) => ({ value, label })) },
]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 205, fixed: 'left', slotName: 'actions' },
@@ -111,15 +111,18 @@ onBeforeUnmount(() => { request++ })
<template>
<AdminPage class="admin-density">
<ListPageLayout>
<div class="driver-summary" aria-label="当前筛选范围司机统计">
<section class="surface-panel"><span>司机总数</span><strong>{{ summary?.total ?? '—' }}</strong><small>自有 {{ summary?.owned ?? '—' }} · 外派 {{ summary?.dispatched ?? '—' }}</small></section>
<button class="surface-panel" type="button" @click="filterExpiry('EXPIRING')"><span>即将到期</span><strong>{{ summary?.expiringSoon ?? '—' }}</strong><small>30 天内到期 · 点击筛选</small></button>
<button class="surface-panel" type="button" @click="filterExpiry('EXPIRED')"><span>驾照已过期</span><strong>{{ summary?.expired ?? '—' }}</strong><small>不可派车 · 点击筛选</small></button>
</div>
<section class="surface-panel"><QueryToolbar v-model="filters" :fields="fields" ariaLabel="司机查询" preference-key="overseas-drivers-query" :loading="loading" @search="search" /></section>
<LoadErrorState v-if="error" title="司机列表加载失败" :message="error.message" @retry="load" ><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="id" empty-text="暂无符合条件的司机">
<ListPageLayout aria-label="司机管理">
<DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="true" preference-key="overseas-drivers-table" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="id" empty-text="暂无符合条件的司机">
<template #toolbar-start><h1>司机管理</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="司机查询" preference-key="overseas-drivers-query" :loading="loading" @search="search" /></template>
<template #notices>
<LoadErrorState v-if="error" title="司机列表加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<div class="driver-summary" aria-label="当前筛选范围司机统计">
<section class="driver-summary__item"><span>司机总数</span><strong>{{ summary?.total ?? '—' }}</strong><small>自有 {{ summary?.owned ?? '—' }} · 外派 {{ summary?.dispatched ?? '—' }}</small></section>
<button class="driver-summary__item" type="button" @click="filterExpiry('EXPIRING')"><span>即将到期</span><strong>{{ summary?.expiringSoon ?? '—' }}</strong><small>30 天内到期 · 点击筛选</small></button>
<button class="driver-summary__item" type="button" @click="filterExpiry('EXPIRED')"><span>驾照已过期</span><strong>{{ summary?.expired ?? '—' }}</strong><small>不可派车 · 点击筛选</small></button>
</div>
</template>
<template #toolbar-actions><el-button v-if="access.can(permission.driverCreate)" type="primary" @click="create"><Plus :size="16" />新增司机</el-button></template>
<template #actions="{ row }"><RowActions :actions="actions(row as DriverDetail)" text-actions @action="action($event, row as DriverDetail)" /></template>
<template #ownership="{ row }">{{ row.details.ownership === 'OWNED' ? '自有司机' : '外派司机' }}</template>
@@ -140,11 +143,13 @@ onBeforeUnmount(() => { request++ })
</template>
<style scoped>
.driver-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-6); }
.driver-summary > * { display: grid; gap: var(--admin-space-3); padding: var(--admin-space-6); text-align: left; color: inherit; font: inherit; }
.driver-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-3); }
.driver-summary__item { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: var(--admin-space-2) var(--admin-space-3); padding: var(--admin-space-3); border: 1px solid var(--cc-border); border-radius: var(--admin-radius); background: var(--admin-surface); text-align: left; color: inherit; font: inherit; }
.driver-summary button { cursor: pointer; }
.driver-summary strong { font-size: 24px; }
.driver-summary small { color: var(--cc-muted); }
.driver-summary button:hover { background: var(--el-fill-color-light); }
.driver-summary button:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: 2px; }
.driver-summary strong { font-size: var(--admin-font-title); font-variant-numeric: tabular-nums; }
.driver-summary small { grid-column: 1 / -1; font-size: var(--admin-font-caption); color: var(--cc-muted); }
.expiry-warning { color: var(--el-color-warning); }
.expiry-danger { color: var(--el-color-danger); }
</style>
@@ -11,7 +11,7 @@ vi.mock('../api/tripOperations', () => ({ listTripOperations: vi.fn(), exportTri
const wrappers: VueWrapper[] = []
const layout = { template: '<div><slot/></div>' }
function mount(component: object) { const wrapper = shallowMount(component, { global: { stubs: { AdminPage: layout, ListPageLayout: layout, QueryToolbar: true,
DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '<div><slot name="toolbar-actions"/></div>' }, ElButton: { template: '<button><slot/></button>' },
DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '<div><slot name="toolbar-start"/><slot name="toolbar-actions"/><slot name="filters"/><slot name="notices"/></div>' }, ElButton: { template: '<button><slot/></button>' },
LoadErrorState: true, FleetQuoteEditor: true, FleetQuoteImport: true, ApiErrorMeta: true } } }); wrappers.push(wrapper); return wrapper }
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()); vi.clearAllMocks() })
it('quotes read-only view fetches a page once and hides all write/export tools', async () => {
@@ -45,19 +45,24 @@ async function download() { exporting.value = true; try { saveFleetCsv(await exp
const money = (rows: TripOperationsMoney[]) => rows.length ? rows.map(r => `${r.currency} ${r.amount.toFixed(2)}`).join(' / ') : '—'
watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ })
</script>
<template><AdminPage class="admin-density"><ListPageLayout>
<LoadErrorState v-if="error" title="查询或导出失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<QueryToolbar v-model="filters" :fields="fields" ariaLabel="运营记录查询" @search="search" @reset="search" />
<TripOperationsCharts v-if="result" :charts="result.charts" />
<div v-if="result" class="operations-summary" aria-live="polite">
<span>实际出车 {{ result.summary.trips }} 次</span><span>在途 {{ result.summary.inTransit }} 次</span><span>货物结案 {{ result.summary.completed }} 次</span>
<span>实物托盘 {{ result.summary.physicalPallets }} 托</span><span>外接约定 {{ money(result.summary.externalAgreedAmounts) }}</span>
</div>
<p v-if="result?.summary.costs.visible">已登记直接成本:{{ result.summary.costs.totals.length ? result.summary.costs.totals.map(c => `${c.currency} ${c.directCost.toFixed(2)}`).join(' / ') : '—' }};未登记 {{ result.summary.costs.unrecordedTrips }} 趟,已登记但未能分摊 {{ result.summary.costs.unallocatedTrips }} 趟。</p>
<p>按实际发车时间查询,每趟车只计一次;货源筛选展示含该类货物的整趟车。约定金额不代表应收或实收,结案不代表车辆与司机已释放。</p>
<DataTable v-model:page="page" v-model:page-size="size" :data="result?.page.records || []" :columns="columns" :total="result?.page.total || 0" :loading="loading" row-key="tripNo" empty-text="该范围暂无实际出车记录">
<template><AdminPage class="admin-density"><ListPageLayout aria-label="出车记录">
<DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="true" preference-key="fleet-operations-table" v-model:page="page" v-model:page-size="size" :data="result?.page.records || []" :columns="columns" :total="result?.page.total || 0" :loading="loading" row-key="tripNo" empty-text="该范围暂无实际出车记录">
<template #toolbar-start><h1>出车记录</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="运营记录查询" preference-key="fleet-operations-query" :loading="loading" @search="search" @reset="search" /></template>
<template #notices>
<div class="operations-overview">
<LoadErrorState v-if="error" title="查询或导出失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<TripOperationsCharts v-if="result" :charts="result.charts" />
<div v-if="result" class="operations-summary" aria-live="polite">
<span>实际出车 <strong>{{ result.summary.trips }}</strong> 次</span><span>在途 <strong>{{ result.summary.inTransit }}</strong> 次</span><span>货物结案 <strong>{{ result.summary.completed }}</strong> 次</span>
<span>实物托盘 <strong>{{ result.summary.physicalPallets }}</strong> 托</span><span>外接约定 {{ money(result.summary.externalAgreedAmounts) }}</span>
</div>
<p v-if="result?.summary.costs.visible" class="operations-note">已登记直接成本:{{ result.summary.costs.totals.length ? result.summary.costs.totals.map(c => `${c.currency} ${c.directCost.toFixed(2)}`).join(' / ') : '—' }};未登记 {{ result.summary.costs.unrecordedTrips }} 趟,已登记但未能分摊 {{ result.summary.costs.unallocatedTrips }} 趟。</p>
<p class="operations-note">按实际发车时间查询,每趟车只计一次;货源筛选展示含该类货物的整趟车。约定金额不代表应收或实收,结案不代表车辆与司机已释放。</p>
</div>
</template>
<template #toolbar-actions><el-button v-if="access.can('platform:fleet:operations:export')" :loading="exporting" @click="download">导出CSV</el-button></template>
<template #actions="{ row }"><el-button v-if="access.can('platform:fleet:trip:list')" class="density-action" @click="router.push({ name: 'overseas-master-data-trip-detail', params: { tripNo: row.tripNo } })">详情</el-button><el-button class="density-action" @click="openRecord(row.tripNo)">实测记录</el-button></template>
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button v-if="access.can('platform:fleet:trip:list')" class="density-action" @click="router.push({ name: 'overseas-master-data-trip-detail', params: { tripNo: row.tripNo } })">详情</el-button><el-button class="density-action" @click="openRecord(row.tripNo)">实测记录</el-button></div></template>
<template #status="{ row }">{{ row.status === 'COMPLETED' ? '货物已结案' : '在途' }}</template><template #destinations="{ row }">{{ row.destinations.map((d: TripOperationsRow['destinations'][number]) => d.label).join(' / ') }}</template>
<template #distance="{ row }">{{ row.measured?.actualDistanceMiles ?? '—' }}</template><template #fuel="{ row }">{{ row.measured?.actualFuelLitres ?? '—' }}</template>
<template #agreed="{ row }">{{ money(row.externalAgreedAmounts) }}</template><template #finance="{ row }"><TripOperationsFinanceCell :trip-no="row.tripNo" /></template>
@@ -66,4 +71,8 @@ watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ }
</DataTable>
<TripOperationsRecordDialog v-model="recordOpen" :row="selected" :readonly="!access.can('platform:fleet:operations:edit')" @saved="load" />
</ListPageLayout></AdminPage></template>
<style scoped>.operations-summary { display:flex; flex-wrap:wrap; gap:var(--admin-space-4); padding:var(--admin-space-3); background:var(--admin-surface); }</style>
<style scoped>
.operations-overview { display: grid; gap: var(--admin-space-3); min-width: 0; }
.operations-summary { display: flex; flex-wrap: wrap; gap: var(--admin-space-2) var(--admin-space-6); font-variant-numeric: tabular-nums; }
.operations-note { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption); line-height: 1.6; }
</style>
@@ -25,7 +25,7 @@ const detail: TripDetail = { trip, driverQualification: null, dispatches: [], lo
const layout = { template: '<div><slot name="header"/><slot/></div>' }
const input = { name: 'ElInput', props: ['modelValue'], emits: ['update:modelValue'], template: '<input :value="modelValue" @input="$emit(\'update:modelValue\',$event.target.value)" />' }
const dialog = { name: 'AppDialog', props: ['modelValue'], emits: ['confirm'], template: '<div v-if="modelValue"><slot/><button @click="$emit(\'confirm\')">保存计划</button></div>' }
const table = { name: 'DataTable', props: ['data', 'columns', 'page', 'pageSize'], emits: ['update:page'], template: '<div><slot name="toolbar-actions"/><div v-for="(row,index) in data" :key="index"><slot name="actions" :row="row"/><slot name="status" :row="row"/><slot name="weight" :row="row"/><slot name="volume" :row="row"/></div></div>' }
const table = { name: 'DataTable', props: ['data', 'columns', 'page', 'pageSize'], emits: ['update:page'], template: '<div><slot name="toolbar-start"/><slot name="toolbar-actions"/><slot name="filters"/><slot name="notices"/><div v-for="(row,index) in data" :key="index"><slot name="actions" :row="row"/><slot name="status" :row="row"/><slot name="weight" :row="row"/><slot name="volume" :row="row"/></div></div>' }
const wrappers: VueWrapper[] = []
function mount(component: object, props: Record<string, unknown> = {}): VueWrapper {
const wrapper = shallowMount(component, { props, global: { directives: { loading: () => undefined }, stubs: {
@@ -20,7 +20,7 @@ const rows = ref<FleetTrip[]>([]), total = ref(0), page = ref(1), size = ref(20)
const error = ref<ApiErrorDetails | null>(null), filters = ref<QueryModel>({ keyword: '', status: '' })
let applied: Omit<TripQuery, 'page' | 'size'> = {}, sequence = 0
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '车次号 / 车牌 / 司机', width: 250 },
{ key: 'status', type: 'select', label: '状态', width: 150, options: Object.entries(tripStatusLabels).map(([value, label]) => ({ value, label })) }]
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 150, options: Object.entries(tripStatusLabels).map(([value, label]) => ({ value, label })) }]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 85, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 215 },
{ key: 'status', label: '状态', slotName: 'status', width: 120 }, { key: 'warehouseName', label: '起运仓', minWidth: 160 },
@@ -44,12 +44,13 @@ function created(detail: TripDetail) { view(detail.trip.tripNo) }
watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ })
</script>
<template>
<AdminPage class="admin-density"><ListPageLayout>
<LoadErrorState v-if="error" title="车次加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<QueryToolbar v-model="filters" :fields="fields" ariaLabel="车次查询" preference-key="fleet-trips-query" @search="search" @reset="search" />
<DataTable v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="tripNo" empty-text="暂无车次,先建立计划再关联排车单">
<AdminPage class="admin-density"><ListPageLayout aria-label="车次管理">
<DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="true" preference-key="fleet-trips-table" v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" row-key="tripNo" empty-text="暂无车次,先建立计划再关联排车单">
<template #toolbar-start><h1>车次管理</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="车次查询" preference-key="fleet-trips-query" :loading="loading" @search="search" @reset="search" /></template>
<template #notices><LoadErrorState v-if="error" title="车次加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template>
<template #toolbar-actions><el-button v-if="access.can(permission.tripCreate)" type="primary" @click="editorOpen = true">新建车次</el-button></template>
<template #actions="{ row }"><el-button class="density-action" @click="view(row.tripNo)">详情</el-button></template>
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" @click="view(row.tripNo)">详情</el-button></div></template>
<template #status="{ row }">{{ tripStatusLabels[row.status as keyof typeof tripStatusLabels] }}</template>
<template #departure="{ row }">{{ time(row.plannedDepartureAt) }}</template><template #end="{ row }">{{ time(row.expectedDriverEndAt) }}</template>
<template #route="{ row }">{{ row.stops.map((stop: FleetTrip['stops'][number]) => stop.name).join(' → ') }}</template>
+1
View File
@@ -64,6 +64,7 @@ const generalCatalogConsumers = [
'overseas-master-data/pages/VehicleDetailView.vue',
'overseas-master-data/pages/DriversView.vue',
'overseas-master-data/pages/TripsView.vue',
'overseas-master-data/pages/TripOperationsView.vue',
'overseas-master-data/pages/TripDetailView.vue',
'overseas-master-data/components/TripPlanDialog.vue',
'overseas-master-data/pages/DriverDetailView.vue',