优化
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export { oceanBookingRoutes } from './routes'
|
||||
export { listOceanBookings } from './api/platformOceanBookings'
|
||||
export * from './status'
|
||||
export * from './types'
|
||||
|
||||
@@ -22,6 +22,11 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
expect(source).not.toContain(':disabled="customer.status !== \'ACTIVE\'"')
|
||||
})
|
||||
|
||||
it('renders the translated status once in the detail heading', () => {
|
||||
expect(source).toContain('<StatusBadge :status="statusLabel(detail.status)" />')
|
||||
expect(source).not.toContain('<StatusBadge :status="detail.status" />')
|
||||
})
|
||||
|
||||
it('provides guidance for every free-text booking field', () => {
|
||||
for (const placeholder of [
|
||||
'请输入提单号(可选)', '请输入船名', '请输入航次', '例如:CNNGB', '例如:宁波港',
|
||||
|
||||
@@ -29,11 +29,8 @@ import type {
|
||||
OceanBookingSaveRequest,
|
||||
OceanBookingStatus,
|
||||
} from '../types'
|
||||
import { oceanBookingStatusLabel } from '../status'
|
||||
|
||||
const statusLabels: Record<OceanBookingStatus, string> = {
|
||||
DRAFT: '草稿', SUBMITTED: '已提交', CONFIRMED: '已确认', RELEASED: '已放舱',
|
||||
FAILED: '订舱失败', CHANGE_PENDING: '变更中', CANCELLED: '已取消',
|
||||
}
|
||||
const partyTypes = [
|
||||
{ value: 'SHIPPER', label: '发货人' },
|
||||
{ value: 'CONSIGNEE', label: '收货人' },
|
||||
@@ -103,7 +100,7 @@ const changeDialogOpen = ref(false)
|
||||
const changeForm = reactive({ vesselName: '', voyageNo: '', estimatedSailingAt: null as string | null, estimatedArrivalAt: null as string | null, reason: '' })
|
||||
|
||||
function statusLabel(status: OceanBookingStatus | string | null | undefined) {
|
||||
return status ? (statusLabels[status as OceanBookingStatus] || status) : '未设置'
|
||||
return oceanBookingStatusLabel(status)
|
||||
}
|
||||
async function loadOptions() {
|
||||
optionsLoading.value = true
|
||||
@@ -235,7 +232,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<div class="detail-heading">
|
||||
<el-button @click="router.push('/ocean-bookings')"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<PageToolbar class="booking-page-toolbar" as="section" :title="detail?.bookingNo || '订舱资料'" :description="detail ? `${detail.customerName || '—'} · ${detail.pol || '—'} → ${detail.pod || '—'}` : '填写并保存订舱资料'">
|
||||
<template v-if="detail" #actions><div class="heading-status"><StatusBadge :status="detail.status" /><span>{{ statusLabel(detail.status) }}</span></div></template>
|
||||
<template v-if="detail" #actions><div class="heading-status"><StatusBadge :status="statusLabel(detail.status)" /></div></template>
|
||||
</PageToolbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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') }) })
|
||||
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"') }) })
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'
|
||||
import { oceanBookingStatusLabel, oceanBookingStatusOptions } from '../status'
|
||||
|
||||
const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -23,13 +24,9 @@ const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const query = reactive<QueryModel>({ keyword: '', status: '' })
|
||||
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: 'status', type: 'select', label: '状态', options: oceanBookingStatusOptions },
|
||||
]
|
||||
const columns = [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 170, slotName: 'actions', hideable: false, movable: false },
|
||||
@@ -54,7 +51,7 @@ watch([page, pageSize], () => void load()); onMounted(load)
|
||||
<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>
|
||||
<template #vessel="{ row }">{{ row.vesselName || '—' }}<span v-if="row.voyageNo"> / {{ row.voyageNo }}</span></template>
|
||||
<template #status="{ row }"><StatusBadge :status="row.status" /></template>
|
||||
<template #status="{ row }"><StatusBadge :status="oceanBookingStatusLabel(row.status)" /></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { oceanBookingStatusLabel, oceanBookingStatusOptions } from './status'
|
||||
|
||||
describe('ocean booking status labels', () => {
|
||||
it('translates every backend status for selectors and badges', () => {
|
||||
expect(oceanBookingStatusOptions).toEqual([
|
||||
{ value: 'DRAFT', label: '草稿' },
|
||||
{ value: 'SUBMITTED', label: '已提交' },
|
||||
{ value: 'CONFIRMED', label: '已确认' },
|
||||
{ value: 'RELEASED', label: '已放舱' },
|
||||
{ value: 'FAILED', label: '订舱失败' },
|
||||
{ value: 'CHANGE_PENDING', label: '变更中' },
|
||||
{ value: 'CANCELLED', label: '已取消' },
|
||||
])
|
||||
})
|
||||
|
||||
it('does not leak unknown backend enum values to users', () => {
|
||||
expect(oceanBookingStatusLabel('NEW_BACKEND_STATUS')).toBe('未知状态')
|
||||
expect(oceanBookingStatusLabel(null)).toBe('未设置')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { OceanBookingStatus } from './types'
|
||||
|
||||
export const oceanBookingStatusLabels: Record<OceanBookingStatus, string> = {
|
||||
DRAFT: '草稿',
|
||||
SUBMITTED: '已提交',
|
||||
CONFIRMED: '已确认',
|
||||
RELEASED: '已放舱',
|
||||
FAILED: '订舱失败',
|
||||
CHANGE_PENDING: '变更中',
|
||||
CANCELLED: '已取消',
|
||||
}
|
||||
|
||||
export const oceanBookingStatusOptions = Object.entries(oceanBookingStatusLabels).map(([value, label]) => ({
|
||||
value: value as OceanBookingStatus,
|
||||
label,
|
||||
}))
|
||||
|
||||
export function oceanBookingStatusLabel(status: string | null | undefined, emptyLabel = '未设置') {
|
||||
if (!status) return emptyLabel
|
||||
return oceanBookingStatusLabels[status as OceanBookingStatus] || '未知状态'
|
||||
}
|
||||
Reference in New Issue
Block a user