Files
admin-web/src/modules/ocean-bookings/pages/OceanBookingsView.vue
T

77 lines
6.4 KiB
Vue
Raw Normal View History

2026-08-08 13:32:27 +08:00
<script setup lang="ts">
2026-08-14 12:50:42 +08:00
import { computed, 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'
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-14 12:50:42 +08:00
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
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-14 12:50:42 +08:00
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const query = reactive<QueryModel>({ customerId: '', keyword: '', status: '' })
const activeCustomers = computed(() => customers.value.filter((customer) => customer.status === 'ACTIVE'))
const fields = computed<QueryField[]>(() => [
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: activeCustomers.value.map((customer) => ({ label: customer.legalName?.trim() || customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
{ 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-14 12:50:42 +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-12 16:40:09 +08:00
{ prop: 'bookingNo', label: '订舱号', minWidth: 150 },
{ prop: 'customerName', label: '客户', minWidth: 160 },
{ prop: 'carrierName', label: '船公司', minWidth: 130 },
{ prop: 'bookingAgent', label: '订舱代理', minWidth: 140 },
{ prop: 'vesselName', label: '船名 / 航次', minWidth: 175, slotName: 'vessel' },
{ prop: 'pol', label: '起运港', minWidth: 125 },
{ prop: 'pod', label: '目的港', minWidth: 125 },
{ prop: 'etd', label: '预计开船', minWidth: 155, slotName: 'etd' },
{ prop: 'eta', label: '预计到港', minWidth: 155, slotName: 'eta' },
{ prop: 'status', label: '状态', minWidth: 110, slotName: 'status' },
{ prop: 'updatedAt', label: '更新时间', minWidth: 155, slotName: 'updatedAt' },
2026-08-08 15:07:34 +08:00
]
2026-08-12 16:40:09 +08:00
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
2026-08-14 12:50:42 +08:00
async function loadCustomers() { customersLoading.value = true; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((customer) => customer.status === 'ACTIVE') } catch { customers.value = [] } finally { customersLoading.value = false } }
async function load() { loading.value = true; error.value = null; try { const result = await listOceanBookings({ page: page.value, size: pageSize.value, customerId: String(query.customerId || '') || undefined, 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-14 12:50:42 +08:00
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers() })
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>
<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-12 18:16:44 +08:00
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe 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>
2026-08-13 11:57:44 +08:00
<template #actions="{ row }"><div class="table-actions"><el-button size="small" type="primary" plain @click="open(row.id)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:ocean-booking:edit')" size="small" @click="open(row.id)"><Pencil :size="15" />编辑</el-button></div></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-12 16:40:09 +08:00
<template #etd="{ row }">{{ formatDateTime(row.etd) }}</template>
<template #eta="{ row }">{{ formatDateTime(row.eta) }}</template>
2026-08-11 18:43:56 +08:00
<template #status="{ row }"><StatusBadge :status="oceanBookingStatusLabel(row.status)" /></template>
2026-08-12 16:40:09 +08:00
<template #updatedAt="{ row }">{{ formatDateTime(row.updatedAt) }}</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-12 18:16:44 +08:00
<style scoped>.ocean-bookings-page{min-width:0}.table-actions{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.table-actions :deep(.el-button){margin-left:0}@media(max-width:420px){.table-actions{flex-direction:column;align-items:stretch}.table-actions :deep(.el-button){width:100%;min-height:40px}}</style>