优化
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onMounted, reactive, ref, watch } 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 LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listOceanBookings, type OceanBookingResource } from '../api/platformOceanBookings'
|
||||
import type { OceanBookingQuery, OceanBookingStatus } from '../types'
|
||||
@@ -16,86 +14,45 @@ const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<OceanBookingResource[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
|
||||
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 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 = 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' },
|
||||
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 },
|
||||
]
|
||||
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) }
|
||||
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 open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
|
||||
onMounted(load)
|
||||
function search() { page.value = 1; void load() }
|
||||
watch([page, pageSize], () => void load()); onMounted(load)
|
||||
</script>
|
||||
|
||||
<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="暂无符合筛选条件的订舱记录">
|
||||
<div class="business-page ocean-bookings-page">
|
||||
<QueryToolbar v-model="query" ariaLabel="订舱筛选" :fields="fields" @search="search" />
|
||||
<LoadErrorState v-if="error" title="订舱列表加载失败" :message="error.message" @retry="load" />
|
||||
<section v-else class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="ocean-bookings" aria-label="订舱列表" row-key="id" 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 #toolbar-actions><el-button v-if="access.can('platform:firstleg:ocean-booking:create')" type="primary" @click="open()">新增订舱</el-button></template>
|
||||
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:ocean-booking:view')" link type="primary" @click="open(row.id)">查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" 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>
|
||||
</section>
|
||||
<div v-if="!loading && loaded && rows.length === 0 && !loadError" class="empty-hint">暂无订舱记录</div>
|
||||
</section>
|
||||
</div>
|
||||
</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>
|
||||
<style scoped>.ocean-bookings-page{display:grid;gap:16px}.table-heading{display:flex;align-items:baseline;gap:10px}.table-heading h2{margin:0;font-size:16px}.table-heading span{color:var(--cc-muted);font-size:12px}</style>
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('TruckAppointmentsView', () => {
|
||||
await flushPromises()
|
||||
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('77')
|
||||
expect(wrapper.text()).toContain('宁波运输')
|
||||
expect(wrapper.find('button[aria-label="查看 TA-9001"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user