feat(admin): add simulated container booking links
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export { oceanBookingRoutes } from './routes'
|
||||
export { listOceanBookings } from './api/platformOceanBookings'
|
||||
export { getOceanBooking, listOceanBookings, listWarehouseLogisticsChannels, type OceanLogisticsChannel } from './api/platformOceanBookings'
|
||||
export * from './status'
|
||||
export * from './types'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
describe('OceanBookingsView', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingsView.vue'), 'utf8'); it('keeps the operation column first and left fixed', () => { expect(source.indexOf("key: 'actions'")).toBeLessThan(source.indexOf("prop: 'bookingNo'")); expect(source).toContain("fixed: 'left'") }); it('renders loading, empty and error through shared components', () => { expect(source).toContain('LoadErrorState'); expect(source).toContain(':loading="loading"'); expect(source).toContain(':total="total"') }); it('keeps list actions in the table toolbar without a duplicate page title', () => { expect(source).toContain('<template #toolbar-actions>'); expect(source).not.toContain('<template #toolbar-start>'); expect(source).not.toContain('PageToolbar'); expect(source).not.toContain('<template #actions><el-button') }); it('shows the two frequent row actions directly without a more menu', () => { expect(source).toContain('<Eye :size="15" />查看'); expect(source).toContain('<Pencil :size="15" />编辑'); expect(source).not.toContain('MoreHorizontal') }); it('renders backend booking statuses through the domain translation contract', () => { expect(source).toContain('options: oceanBookingStatusOptions'); expect(source).toContain(':status="oceanBookingStatusLabel(row.status)"'); expect(source).not.toContain(':status="row.status"') }) })
|
||||
describe('OceanBookingsView', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingsView.vue'), 'utf8'); it('keeps the operation column first and left fixed', () => { expect(source.indexOf("key: 'actions'")).toBeLessThan(source.indexOf("prop: 'bookingNo'")); expect(source).toContain("fixed: 'left'") }); it('renders loading, empty and error through shared components', () => { expect(source).toContain('LoadErrorState'); expect(source).toContain(':loading="loading"'); expect(source).toContain(':total="total"') }); it('does not expose internal draft creation from the formal booking list', () => { expect(source).not.toContain('<template #toolbar-actions>'); expect(source).not.toContain('新增订舱'); expect(source).not.toContain("'/ocean-bookings/new'"); expect(source).not.toContain('PageToolbar') }); it('shows only the formal booking detail action', () => { expect(source).toContain('<Eye :size="15" />查看'); expect(source).not.toContain('<Pencil :size="15" />编辑'); expect(source).not.toContain('MoreHorizontal') }); it('limits status filters to the frozen formal booking states', () => { expect(source).toContain("['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED']"); expect(source).toContain('options: formalStatusOptions'); expect(source).toContain(':status="oceanBookingStatusLabel(row.status)"'); expect(source).not.toContain(':status="row.status"') }) })
|
||||
|
||||
describe('OceanBookingsView prototype field matrix', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingsView.vue'), 'utf8')
|
||||
@@ -28,7 +28,6 @@ describe('OceanBookingsView visual action contract', () => {
|
||||
|
||||
it('uses bordered business buttons and stacks them on narrow screens', () => {
|
||||
expect(source).toContain('size="small" type="primary" plain')
|
||||
expect(source).toContain('size="small" @click="open(row.id)"')
|
||||
expect(source).not.toContain('link type="primary" @click="open(row.id)')
|
||||
expect(source).toContain('@media(max-width:420px)')
|
||||
expect(source).toContain('.table-actions')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Eye, Pencil, Plus } from '@lucide/vue'
|
||||
import { Eye } from '@lucide/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'
|
||||
@@ -10,14 +10,12 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listOceanBookings, type OceanBookingResource } from '../api/platformOceanBookings'
|
||||
import type { OceanBookingQuery, OceanBookingStatus } from '../types'
|
||||
import { oceanBookingStatusLabel, oceanBookingStatusOptions } from '../status'
|
||||
|
||||
const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<OceanBookingResource[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
|
||||
@@ -28,13 +26,14 @@ 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 formalStatusOptions = oceanBookingStatusOptions.filter((item) => ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'].includes(item.value))
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: activeCustomers.value.map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'keyword', type: 'input', label: '关键词', placeholder: '订舱号/订单号', width: 220 },
|
||||
{ key: 'status', type: 'select', label: '状态', options: oceanBookingStatusOptions },
|
||||
{ key: 'status', type: 'select', label: '状态', options: formalStatusOptions },
|
||||
])
|
||||
const columns = [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 100, slotName: 'actions', hideable: false, movable: false },
|
||||
{ prop: 'bookingNo', label: '订舱号', minWidth: 150 },
|
||||
{ prop: 'customerName', label: '客户', minWidth: 160 },
|
||||
{ prop: 'carrierName', label: '船公司', minWidth: 130 },
|
||||
@@ -50,7 +49,7 @@ const columns = [
|
||||
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
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 } }
|
||||
function open(id?: string) { void router.push(id ? `/ocean-bookings/${id}` : '/ocean-bookings/new') }
|
||||
function open(id: string) { void router.push(`/ocean-bookings/${id}`) }
|
||||
function search() { if (page.value === 1) void load(); else page.value = 1 }
|
||||
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers() })
|
||||
</script>
|
||||
@@ -61,8 +60,7 @@ watch([page, pageSize], () => void load()); onMounted(() => { void load(); void
|
||||
<template v-if="error" #state><LoadErrorState title="订舱列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template>
|
||||
<section v-if="!error" class="surface-panel table-panel">
|
||||
<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="暂无符合筛选条件的订舱">
|
||||
<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 }"><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>
|
||||
<template #actions="{ row }"><div class="table-actions"><el-button size="small" type="primary" plain @click="open(row.id)"><Eye :size="15" />查看</el-button></div></template>
|
||||
<template #vessel="{ row }">{{ row.vesselName || '—' }}<span v-if="row.voyageNo"> / {{ row.voyageNo }}</span></template>
|
||||
<template #etd="{ row }">{{ formatDateTime(row.etd) }}</template>
|
||||
<template #eta="{ row }">{{ formatDateTime(row.eta) }}</template>
|
||||
|
||||
@@ -2,7 +2,7 @@ export type OceanBookingStatus = 'DRAFT' | 'SUBMITTED' | 'CONFIRMED' | 'RELEASED
|
||||
|
||||
export interface OceanBookingParty { id?: string; partyType: string; partyName: string; contactName?: string | null; contactPhone?: string | null; address?: string | null }
|
||||
export interface OceanBookingContainer { id?: string; containerType: string; quantity: number | null; containerKind?: string | null; capacityCbm: number | null; maxWeightKg: number | null }
|
||||
export interface OceanBookingContainerAllocation extends OceanBookingContainer { allocatedQuantity?: number | null }
|
||||
export interface OceanBookingContainerAllocation { id?: string; oceanBookingId?: string; requirementId?: string; containerBookingId?: string | null; allocatedQuantity?: number | null }
|
||||
export interface OceanBookingDraftData {
|
||||
customerId: string | null; customerName: string | null; parties: OceanBookingParty[]; containerRequirements: OceanBookingContainer[]
|
||||
channelCode: string; warehouseId: string; warehouseName: string
|
||||
|
||||
Reference in New Issue
Block a user