2026-08-08 13:32:27 +08:00
|
|
|
<script setup lang="ts">
|
2026-08-08 15:07:34 +08:00
|
|
|
import { onMounted, reactive, ref, watch } from 'vue'
|
2026-08-08 13:32:27 +08:00
|
|
|
import { useRouter } from 'vue-router'
|
2026-08-10 16:24:54 +08:00
|
|
|
import { Eye, Pencil, Plus } from '@lucide/vue'
|
2026-08-08 13:32:27 +08:00
|
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
2026-08-08 15:07:34 +08:00
|
|
|
import DataTable from '@/shared/components/DataTable.vue'
|
2026-08-08 15:05:37 +08:00
|
|
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
2026-08-08 15:07:34 +08:00
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
2026-08-11 11:26:31 +08:00
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
2026-08-10 16:24:54 +08:00
|
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
|
|
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
2026-08-08 15:07:34 +08:00
|
|
|
import { getApiErrorDetails } from '@/shared/api/http'
|
2026-08-08 15:05:37 +08:00
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
2026-08-08 13:32:27 +08:00
|
|
|
import { listOceanBookings, type OceanBookingResource } from '../api/platformOceanBookings'
|
|
|
|
|
import type { OceanBookingQuery, OceanBookingStatus } from '../types'
|
2026-08-11 18:43:56 +08:00
|
|
|
import { oceanBookingStatusLabel, oceanBookingStatusOptions } from '../status'
|
2026-08-08 13:32:27 +08:00
|
|
|
|
2026-08-08 15:05:37 +08:00
|
|
|
const router = useRouter()
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
const rows = ref<OceanBookingResource[]>([])
|
|
|
|
|
const loading = ref(false)
|
2026-08-08 15:07:34 +08:00
|
|
|
const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
|
2026-08-08 15:05:37 +08:00
|
|
|
const page = ref(1)
|
|
|
|
|
const pageSize = ref(20)
|
|
|
|
|
const total = ref(0)
|
2026-08-10 09:12:06 +08:00
|
|
|
const query = reactive<QueryModel>({ keyword: '', status: '' })
|
2026-08-08 15:07:34 +08:00
|
|
|
const fields: QueryField[] = [
|
|
|
|
|
{ key: 'keyword', type: 'input', label: '关键词', placeholder: '订舱号/订单号/客户', width: 220 },
|
2026-08-11 18:43:56 +08:00
|
|
|
{ key: 'status', type: 'select', label: '状态', options: oceanBookingStatusOptions },
|
2026-08-08 15:05:37 +08:00
|
|
|
]
|
2026-08-08 15:07:34 +08:00
|
|
|
const columns = [
|
|
|
|
|
{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false },
|
2026-08-10 09:12:06 +08:00
|
|
|
{ prop: 'bookingNo', label: '订舱编号', minWidth: 150 }, { prop: 'customerName', label: '客户', minWidth: 160 },
|
|
|
|
|
{ prop: 'billOfLadingNo', label: '提单号', minWidth: 140 }, { prop: 'pol', label: '起运港', minWidth: 125 }, { prop: 'pod', label: '目的港', minWidth: 125 },
|
|
|
|
|
{ prop: 'carrierName', label: '船公司', minWidth: 130 }, { prop: 'bookingAgent', label: '订舱代理', minWidth: 140 },
|
|
|
|
|
{ prop: 'vesselName', label: '船名/航次', minWidth: 150, slotName: 'vessel' }, { prop: 'etd', label: '预计开船', minWidth: 140 },
|
|
|
|
|
{ prop: 'eta', label: '预计到港', minWidth: 140 }, { prop: 'status', label: '状态', minWidth: 110, slotName: 'status' },
|
2026-08-08 15:07:34 +08:00
|
|
|
]
|
2026-08-10 09:12:06 +08:00
|
|
|
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 } satisfies OceanBookingQuery); rows.value = result.records; total.value = result.total } catch (e) { error.value = getApiErrorDetails(e) } finally { loading.value = false } }
|
2026-08-08 15:05:37 +08:00
|
|
|
function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
|
2026-08-10 18:12:23 +08:00
|
|
|
function search() { if (page.value === 1) void load(); else page.value = 1 }
|
2026-08-08 15:07:34 +08:00
|
|
|
watch([page, pageSize], () => void load()); onMounted(load)
|
2026-08-08 13:32:27 +08:00
|
|
|
</script>
|
2026-08-08 15:05:37 +08:00
|
|
|
<template>
|
2026-08-10 16:24:54 +08:00
|
|
|
<AdminPage as="div" class="ocean-bookings-page">
|
|
|
|
|
<ListPageLayout aria-label="订舱列表">
|
|
|
|
|
<template #toolbar><QueryToolbar v-model="query" class="surface-panel" ariaLabel="订舱筛选" :fields="fields" @search="search" /></template>
|
2026-08-11 11:26:31 +08:00
|
|
|
<template v-if="error" #state><LoadErrorState title="订舱列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template>
|
2026-08-10 16:24:54 +08:00
|
|
|
<section v-if="!error" class="surface-panel table-panel">
|
2026-08-08 15:07:34 +08:00
|
|
|
<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="暂无符合筛选条件的订舱">
|
2026-08-10 16:24:54 +08:00
|
|
|
<template #toolbar-actions><el-button v-if="access.can('platform:firstleg:ocean-booking:create')" type="primary" @click="open()"><Plus :size="16" />新增订舱</el-button></template>
|
|
|
|
|
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:ocean-booking:view')" link type="primary" @click="open(row.id)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" link @click="open(row.id)"><Pencil :size="15" />编辑</el-button></template>
|
2026-08-08 15:07:34 +08:00
|
|
|
<template #vessel="{ row }">{{ row.vesselName || '—' }}<span v-if="row.voyageNo"> / {{ row.voyageNo }}</span></template>
|
2026-08-11 18:43:56 +08:00
|
|
|
<template #status="{ row }"><StatusBadge :status="oceanBookingStatusLabel(row.status)" /></template>
|
2026-08-08 15:05:37 +08:00
|
|
|
</DataTable>
|
2026-08-10 16:24:54 +08:00
|
|
|
</section>
|
|
|
|
|
</ListPageLayout>
|
|
|
|
|
</AdminPage>
|
2026-08-08 15:05:37 +08:00
|
|
|
</template>
|
2026-08-10 16:24:54 +08:00
|
|
|
<style scoped>.ocean-bookings-page{min-width:0}</style>
|