This commit is contained in:
2026-08-08 15:05:37 +08:00
parent 315c370ec9
commit 9ca7237741
6 changed files with 118 additions and 32 deletions
@@ -1,22 +1,101 @@
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Eye, Pencil, Plus } from '@lucide/vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listOceanBookings, type OceanBookingResource } from '../api/platformOceanBookings'
import type { OceanBookingQuery, OceanBookingStatus } from '../types'
const router = useRouter(); const rows = ref<OceanBookingResource[]>([]); const loading = ref(false); const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
const query = reactive<QueryModel>({ keyword: '', status: '', pol: '', pod: '', soNo: '' })
const statuses: Array<{ label: string; value: string }> = [{ label: '草稿', value: 'DRAFT' }, { label: '已提交', value: 'SUBMITTED' }, { label: '已确认', value: 'CONFIRMED' }, { label: '已放舱', value: 'RELEASED' }, { label: '失败', value: 'FAILED' }, { label: '变更中', value: 'CHANGE_PENDING' }, { label: '已取消', value: 'CANCELLED' }]
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '订舱号/订单号/客户', width: 220 }, { key: 'status', type: 'select', label: '状态', options: statuses }, { key: 'pol', type: 'input', label: '起运港', width: 150 }, { key: 'pod', type: 'input', label: '目的港', width: 150 }, { key: 'soNo', type: 'input', label: 'S/O号', width: 150 }]
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false }, { prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'orderNo', label: '关联订单', minWidth: 140 }, { prop: 'customerName', label: '委托单位', minWidth: 160 }, { prop: 'mblShipper', label: 'MB/L货人', minWidth: 150 }, { prop: 'mblConsignee', label: 'MB/L收人', minWidth: 150 }, { prop: 'pol', label: '起运港(POL)', minWidth: 125 }, { prop: 'pod', label: '目的港(POD)', minWidth: 125 }, { prop: 'containerSummary', label: '箱型/箱量', minWidth: 120 }, { prop: 'carrierName', label: '船公司', minWidth: 130 }, { prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'soNo', label: 'S/O号', minWidth: 130 }, { prop: 'cutoffAt', label: '预计截关', minWidth: 150 }, { prop: 'etd', label: 'ETD', minWidth: 140 }, { prop: 'atd', label: 'ATD', minWidth: 140 }, { prop: 'eta', label: 'ETA', minWidth: 140 }, { prop: 'ata', label: 'ATA', minWidth: 140 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' }]
async function load() { loading.value = true; error.value = null; try { const result = await listOceanBookings({ page: page.value, size: pageSize.value, keyword: String(query.keyword || '') || undefined, status: (query.status || undefined) as OceanBookingStatus | undefined, pol: String(query.pol || '') || undefined, pod: String(query.pod || '') || undefined, soNo: String(query.soNo || '') || undefined } satisfies OceanBookingQuery); rows.value = result.records; total.value = result.total } catch (e) { error.value = getApiErrorDetails(e) } finally { loading.value = false } }
function search() { page.value = 1; void load() }; function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
watch([page, pageSize], () => void load()); onMounted(load)
const router = useRouter()
const access = useAccessPreviewStore()
const rows = ref<OceanBookingResource[]>([])
const loading = ref(false)
const loaded = ref(false)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const loadError = ref<ApiErrorDetails | null>(null)
const filters = ref<QueryModel>({ keyword: '', status: '', pol: '', pod: '', soNo: '' })
const applied = ref({ ...filters.value })
const statuses: Array<{ label: string; value: OceanBookingStatus }> = [
{ label: '草稿', value: 'DRAFT' }, { label: '待确认', value: 'SUBMITTED' },
{ label: '已订舱', value: 'CONFIRMED' }, { label: '已放舱', value: 'RELEASED' },
{ label: '订舱失败', value: 'FAILED' }, { label: '变更中', value: 'CHANGE_PENDING' }, { label: '已取消', value: 'CANCELLED' },
]
const fields = computed<QueryField[]>(() => [
{ key: 'keyword', type: 'input', label: '业务关键词', placeholder: '业务编号、委托单位、订单号', width: 250 },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: statuses },
{ key: 'pol', type: 'input', label: '起运港 POL', placeholder: '全部起运港', width: 150 },
{ key: 'pod', type: 'input', label: '目的港 POD', placeholder: '全部目的港', width: 150 },
{ key: 'soNo', type: 'input', label: 'S/O号', placeholder: '请输入 S/O 号', width: 160 },
])
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 160, fixed: 'left', slotName: 'actions', hideable: false, movable: false },
{ key: 'customerName', prop: 'customerName', label: '委托单位', minWidth: 180 },
{ key: 'bookingNo', prop: 'bookingNo', label: '主票号', minWidth: 165 },
{ key: 'release', label: '是否放舱', width: 100, slotName: 'release' },
{ key: 'mblShipper', prop: 'mblShipper', label: 'MB/L货人', minWidth: 160 },
{ key: 'mblConsignee', prop: 'mblConsignee', label: 'MB/L收人', minWidth: 160 },
{ key: 'pol', prop: 'pol', label: '起运港(POL)', minWidth: 135 },
{ key: 'pod', prop: 'pod', label: '目的港(POD)', minWidth: 135 },
{ key: 'containerSummary', prop: 'containerSummary', label: '箱/箱量', minWidth: 120 },
{ key: 'carrierName', prop: 'carrierName', label: '船公司', minWidth: 140 },
{ key: 'cutoffAt', prop: 'cutoffAt', label: '预计截关日', minWidth: 155, formatter: (row: OceanBookingResource) => formatDate(row.cutoffAt) },
{ key: 'etd', prop: 'etd', label: '开航日期(ETD)', minWidth: 155, formatter: (row: OceanBookingResource) => formatDate(row.etd) },
{ key: 'atd', prop: 'atd', label: '实际开船日期(ATD)', minWidth: 165, formatter: (row: OceanBookingResource) => formatDate(row.atd) },
{ key: 'ata', prop: 'ata', label: '到港日期(ATA)', minWidth: 155, formatter: (row: OceanBookingResource) => formatDate(row.ata) },
{ key: 'vessel', label: '船名/航次', minWidth: 170, slotName: 'vessel' },
{ key: 'bookingAgent', prop: 'bookingAgent', label: '订舱代理', minWidth: 150 },
{ key: 'soNo', prop: 'soNo', label: 'S/O号', minWidth: 135 },
{ key: 'status', prop: 'status', label: '状态', width: 115, slotName: 'status' },
]
function formatDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
async function load() {
loading.value = true; loadError.value = null
try {
const result = await listOceanBookings({ page: page.value, size: pageSize.value, keyword: String(applied.value.keyword || '') || undefined, status: (applied.value.status || undefined) as OceanBookingStatus | undefined, pol: String(applied.value.pol || '') || undefined, pod: String(applied.value.pod || '') || undefined, soNo: String(applied.value.soNo || '') || undefined } satisfies OceanBookingQuery)
rows.value = result.records; total.value = result.total; loaded.value = true
} catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '订舱列表加载失败,请稍后重试') }
finally { loading.value = false }
}
function search(model: QueryModel) { applied.value = { ...model }; page.value = 1; void load() }
function reset() { filters.value = { keyword: '', status: '', pol: '', pod: '', soNo: '' }; search(filters.value) }
function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
onMounted(load)
</script>
<template><div class="business-page ocean-bookings-page"><div class="page-heading"><div><h1>海运订舱</h1><p>维护订舱申请、船期及放舱信息</p></div><el-button type="primary" @click="open()">新增订舱</el-button></div><QueryToolbar v-model="query" ariaLabel="订舱查询" :fields="fields" @search="search" /><LoadErrorState v-if="error" title="订舱列表加载失败" :message="error.message" @retry="load" /><DataTable v-else v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="ocean-bookings"><template #actions="{ row }"><el-button link type="primary" @click="open(row.id)">查看</el-button><el-button link @click="open(row.id)">编辑</el-button></template><template #vessel="{ row }">{{ row.vesselName || '—' }}<span v-if="row.voyageNo"> / {{ row.voyageNo }}</span></template><template #status="{ row }"><StatusBadge :status="row.status" /></template></DataTable></div></template>
<style scoped>.ocean-bookings-page{display:grid;gap:16px}.page-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}.page-heading h1{margin:0;font-size:20px}.page-heading p{margin:6px 0 0;color:var(--cc-muted);font-size:13px}@media(max-width:600px){.page-heading{align-items:flex-start;flex-direction:column}.page-heading :deep(.el-button){width:100%;min-height:40px}}</style>
<template>
<section class="business-page ocean-bookings-page">
<div class="page-heading"><div><h1>订舱列表</h1><p>维护海运订舱申请、船期及放舱信息</p></div></div>
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="订舱查询" :fields="fields" preference-key="ocean-bookings-query" @search="search" @reset="reset" />
<LoadErrorState v-if="loadError" class="surface-panel" title="订舱列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<section class="surface-panel table-panel" :aria-busy="loading">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="ocean-bookings" aria-label="订舱列表" empty-text="暂无符合筛选条件的订舱记录">
<template #toolbar-start><div class="table-heading"><h2>订舱列表</h2><span>共 {{ total }} 条</span></div></template>
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:ocean-booking:create')" type="primary" :disabled="loading" @click="open()"><Plus :size="16" />新增订舱</el-button></template>
<template #actions="{ row }"><el-button text type="primary" @click="open(row.id)"><Eye :size="16" />查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" text @click="open(row.id)"><Pencil :size="16" />编辑</el-button></template>
<template #release="{ row }"><el-tag size="small" :type="row.status === 'RELEASED' ? 'success' : 'info'">{{ row.status === 'RELEASED' ? '是' : '否' }}</el-tag></template>
<template #vessel="{ row }">{{ row.vesselName || '—' }}<small v-if="row.voyageNo">{{ row.voyageNo }}</small></template>
<template #status="{ row }"><StatusBadge :status="row.status" /></template>
</DataTable>
</section>
<div v-if="!loading && loaded && rows.length === 0 && !loadError" class="empty-hint">暂无订舱记录</div>
</section>
</template>
<style scoped>
.ocean-bookings-page { min-width: 0; padding: 24px; }
.page-heading { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:18px; flex-wrap:wrap; }
h1 { margin:0; font-size:24px; }.page-heading p { margin:6px 0 0; color:var(--el-text-color-secondary); }
.surface-panel { margin-bottom:16px; }.table-panel { overflow:hidden; }.table-heading { display:flex; align-items:baseline; gap:10px; }.table-heading h2 { margin:0; font-size:16px; }.table-heading span { color:var(--el-text-color-secondary); font-size:13px; }
small { display:block; color:var(--el-text-color-secondary); margin-top:3px; }
.empty-hint { display:flex; justify-content:center; color:var(--el-text-color-secondary); padding:24px; }
@media (max-width:600px) { .ocean-bookings-page { padding:16px 12px; } h1 { font-size:20px; } }
</style>
@@ -41,6 +41,7 @@ export async function listTruckAppointments(query: TruckAppointmentQuery = {}) {
keyword: query.keyword?.trim() || undefined,
status: query.status || undefined,
containerBookingId: query.containerBookingId || undefined,
containerType: query.containerType || undefined,
},
})
return normalizePage(unwrapApiResponse(response))
@@ -22,12 +22,15 @@ defineExpose({ validate })
<el-form ref="formRef" :model="draft" :rules="rules" label-position="top" :disabled="readonly || submitting" class="truck-form">
<el-alert v-if="resource" type="info" :closable="false" class="relation-alert">关联资源:{{ relationText }};柜型由关联柜体返回(只读)</el-alert>
<el-row :gutter="16">
<el-col :xs="24"><el-divider content-position="left">关联资源</el-divider></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="柜体关联 ID"><el-input v-model="draft.containerBookingId" placeholder="请输入柜体预约 ID" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="出仓单关联 ID"><el-input v-model="draft.outboundOrderId" placeholder="请输入出仓单 ID" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="海运订舱关联 ID"><el-input v-model="draft.oceanBookingId" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="装柜计划关联 ID"><el-input v-model="draft.loadingPlanId" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="仓库关联 ID"><el-input v-model="draft.warehouseId" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="客户关联 ID"><el-input v-model="draft.customerId" /></el-form-item></el-col>
<el-col v-if="resource" :xs="24" :sm="12"><el-form-item label="派车单号"><el-input :model-value="resource.appointmentNo || ''" disabled /></el-form-item></el-col>
<el-col :xs="24"><el-divider content-position="left">运输与时间计划</el-divider></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="承运商 ID" prop="carrierId"><el-input v-model="draft.carrierId" placeholder="请输入后端资源 ID" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="承运商名称"><el-input v-model="draft.carrierName" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="车队联系人"><el-input v-model="draft.fleetContact" /></el-form-item></el-col>
@@ -43,12 +46,14 @@ defineExpose({ validate })
<el-col :xs="24" :sm="8"><el-form-item label="体积(m³)"><el-input v-model="draft.volumeCbm" /></el-form-item></el-col>
<el-col :xs="24" :sm="8"><el-form-item label="托盘数量"><el-input v-model="draft.palletCount" inputmode="numeric" /></el-form-item></el-col>
<el-col :xs="24" :sm="8"><el-form-item label="在途分钟"><el-input v-model="draft.transitMinutes" inputmode="numeric" /></el-form-item></el-col>
<el-col :xs="24"><el-divider content-position="left">费用信息</el-divider></el-col>
<el-col :xs="24" :sm="8"><el-form-item label="应付拖车费"><el-input v-model="draft.payableAmount"><template #append>RMB</template></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="8"><el-form-item label="应收拖车费"><el-input v-model="draft.receivableAmount"><template #append>RMB</template></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="8"><el-form-item label="币种"><el-input v-model="draft.currency" maxlength="8" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="门店名称"><el-input v-model="draft.storeName" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="门店地址"><el-input v-model="draft.storeAddress" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="门店联系人"><el-input v-model="draft.storeContact" /></el-form-item></el-col>
<el-col :xs="24"><el-divider content-position="left">门店信息与备注</el-divider></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="门店电话"><el-input v-model="draft.storePhone" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="城市"><el-input v-model="draft.storeCity" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="邮编"><el-input v-model="draft.storeZipcode" /></el-form-item></el-col>
@@ -25,7 +25,7 @@ describe('TruckAppointmentsView', () => {
it('loads the real list and renders relationship snapshots', async () => {
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerBookingId: '' })
expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerBookingId: '', containerType: '' })
expect(wrapper.text()).toContain('TA-9001')
expect(wrapper.text()).toContain('柜体 77')
expect(wrapper.text()).toContain('宁波运输')
@@ -14,22 +14,25 @@ import { TRUCK_APPOINTMENT_STATUSES, truckAppointmentStatusLabels, type TruckApp
const router = useRouter(); const rows = ref<TruckAppointmentResource[]>([]); const loading = ref(false); const loaded = ref(false); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loadError = ref<ApiErrorDetails | null>(null)
const access = useAccessPreviewStore()
const filters = ref({ keyword: '', status: '' as TruckAppointmentStatus | '', containerBookingId: '' }); const applied = ref({ ...filters.value })
const filters = ref({ keyword: '', status: '' as TruckAppointmentStatus | '', containerBookingId: '', containerType: '' }); const applied = ref({ ...filters.value })
const fields = computed<QueryField[]>(() => [
{ key: 'keyword', type: 'input', label: '关键词', placeholder: '委托单位、预约单号、车牌' },
{ key: 'keyword', type: 'input', label: '关键词', placeholder: '委托单位、派车单号、车牌号' },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: TRUCK_APPOINTMENT_STATUSES.map((value) => ({ value, label: truckAppointmentStatusLabels[value] })) },
{ key: 'containerBookingId', type: 'input', label: '柜体 ID', placeholder: '按关联柜体筛选' },
{ key: 'containerType', type: 'select', label: '柜型', placeholder: '全部柜型', options: [
{ value: '20GP', label: '20GP' }, { value: '40GP', label: '40GP' }, { value: '40HC', label: '40HC' },
] },
])
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
{ key: 'appointmentNo', prop: 'appointmentNo', label: '预约单号', minWidth: 170 },
{ key: 'relation', label: '关联资源', minWidth: 220, slotName: 'relation' },
{ key: 'company', label: '承运商', minWidth: 150, slotName: 'company' },
{ key: 'driver', label: '司机/车头', minWidth: 150, slotName: 'driver' },
{ key: 'store', label: '门店', minWidth: 130, slotName: 'store' },
{ key: 'plannedLoadingAt', label: '预计装箱时间', minWidth: 170, formatter: (row: TruckAppointmentResource) => formatDateTime(row.plannedLoadingAt) },
{ key: 'appointmentNo', prop: 'appointmentNo', label: '派车单号', minWidth: 170 },
{ key: 'containerBookingId', prop: 'containerBookingId', label: '关联柜体', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.containerBookingId || '—' },
{ key: 'company', label: '运输公司', minWidth: 160, slotName: 'company' },
{ key: 'driver', label: '司机', minWidth: 130, slotName: 'driver' },
{ key: 'vehicleNo', prop: 'vehicleNo', label: '车头号', minWidth: 120, formatter: (row: TruckAppointmentResource) => row.vehicleNo || '—' },
{ key: 'store', label: '门店', minWidth: 140, slotName: 'store' },
{ key: 'payableAmount', label: '应付拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.payableAmount ? `¥${row.payableAmount}` : '—' },
{ key: 'receivableAmount', label: '应收拖车费', minWidth: 130, formatter: (row: TruckAppointmentResource) => row.receivableAmount ? `¥${row.receivableAmount}` : '—' },
{ key: 'plannedLoadingAt', label: '预计装箱日期', minWidth: 170, formatter: (row: TruckAppointmentResource) => formatDateTime(row.plannedLoadingAt) },
{ key: 'status', label: '状态', width: 130, slotName: 'status' },
{ key: 'attachments', label: '附件', width: 90, formatter: (row: TruckAppointmentResource) => String(row.attachments?.length || 0) },
]
@@ -40,22 +43,20 @@ function statusLabel(value: unknown) {
: String(value || '—')
}
async function load() { loading.value = true; loadError.value = null; try { const result = await listTruckAppointments({ page: page.value, size: pageSize.value, ...applied.value } as TruckAppointmentQuery); rows.value = result.records; total.value = result.total; loaded.value = true } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '拖车预约加载失败,请稍后重试') } finally { loading.value = false } }
function search(model: QueryModel) { applied.value = { keyword: String(model.keyword || ''), status: (model.status || '') as TruckAppointmentStatus | '', containerBookingId: String(model.containerBookingId || '') }; page.value = 1; void load() }
function reset() { filters.value = { keyword: '', status: '', containerBookingId: '' }; search(filters.value) }
function search(model: QueryModel) { applied.value = { keyword: String(model.keyword || ''), status: (model.status || '') as TruckAppointmentStatus | '', containerBookingId: '', containerType: String(model.containerType || '') }; page.value = 1; void load() }
function reset() { filters.value = { keyword: '', status: '', containerBookingId: '', containerType: '' }; search(filters.value) }
onMounted(load)
</script>
<template>
<section class="business-page trucking-page">
<div class="page-heading"><div><h1>拖车预约</h1><p>维护拖车预约、关联资源与派车状态</p></div></div>
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="拖车预约查询" :fields="fields" preference-key="trucking-query" @search="search" @reset="reset" />
<LoadErrorState v-if="loadError" class="surface-panel" title="拖车预约加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<section class="surface-panel table-panel" :aria-busy="loading">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="trucking-appointments" aria-label="拖车预约列表" empty-text="暂无符合筛选条件的拖车预约">
<template #toolbar-start><div class="table-heading"><h2>拖车预约列表</h2><span>共 {{ total }} 条</span></div></template>
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:truck-appointment:create')" type="primary" :disabled="loading" @click="router.push({ name: 'trucking-new' })"><Plus :size="16" />新增拖车</el-button></template>
<template #relation="{ row }"><div>{{ row.customerName || '—' }}</div><small>{{ row.entrustingPartyName || '委托单位待关联' }} · 出仓单 {{ row.outboundOrderId || '—' }} · 柜体 {{ row.containerBookingId || '—' }}</small><small>柜型 {{ row.relatedContainerType || '由关联资源返回' }}</small></template>
<template #company="{ row }">{{ row.carrierName || row.carrierId || '—' }}</template>
<template #driver="{ row }">{{ row.driverName || '—' }}<small>{{ row.vehicleNo || '—' }}</small></template>
<template #driver="{ row }">{{ row.driverName || '—' }}<small>{{ row.driverPhone || '—' }}</small></template>
<template #store="{ row }">{{ row.storeName || '—' }}</template>
<template #status="{ row }"><StatusBadge :status="statusLabel(row.status)" /></template>
<template #actions="{ row }"><el-button text :aria-label="`查看 ${row.appointmentNo || row.id}`" @click="router.push({ name: 'trucking-detail', params: { id: String(row.id) } })"><Eye :size="16" />查看</el-button><el-button v-if="access.can('platform:firstleg:truck-appointment:edit')" text :aria-label="`编辑 ${row.appointmentNo || row.id}`" @click="router.push({ name: 'trucking-detail', params: { id: String(row.id) }, query: { edit: '1' } })"><Pencil :size="16" />编辑</el-button></template>
@@ -66,8 +67,7 @@ onMounted(load)
</template>
<style scoped>
.trucking-page { min-width: 0; padding: 24px; }
.page-heading { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:18px; flex-wrap:wrap; }
h1 { margin:0; font-size:24px; }.page-heading p { margin:6px 0 0; color:var(--el-text-color-secondary); }.surface-panel { margin-bottom:16px; }.table-panel { overflow:hidden; }.table-heading { display:flex; align-items:baseline; gap:10px; }.table-heading h2 { margin:0; font-size:16px; }.table-heading span { color:var(--el-text-color-secondary); font-size:13px; }.empty-hint { display:flex; justify-content:center; gap:8px; color:var(--el-text-color-secondary); padding:24px; }
.surface-panel { margin-bottom:16px; }.table-panel { overflow:hidden; }.table-heading { display:flex; align-items:baseline; gap:10px; }.table-heading h2 { margin:0; font-size:16px; }.table-heading span { color:var(--el-text-color-secondary); font-size:13px; }.empty-hint { display:flex; justify-content:center; gap:8px; color:var(--el-text-color-secondary); padding:24px; }
small { display:block; color:var(--el-text-color-secondary); margin-top:3px; }
@media (max-width:600px) { .trucking-page { padding:16px 12px; } h1 { font-size:20px; } }
@media (max-width:600px) { .trucking-page { padding:16px 12px; } }
</style>
+1
View File
@@ -125,6 +125,7 @@ export interface TruckAppointmentQuery {
keyword?: string
status?: TruckAppointmentStatus | ''
containerBookingId?: string | ''
containerType?: string | ''
}
export interface TruckAppointmentPage {