This commit is contained in:
2026-08-14 12:50:42 +08:00
parent 87c6d2c4ca
commit 590a457dcd
36 changed files with 242 additions and 95 deletions
@@ -26,7 +26,7 @@ describe('PortSchedulesView', () => {
it('selects real customers and customer-scoped ocean bookings', () => {
expect(source).toContain("from '@/modules/iam'")
expect(source).toContain("from '@/modules/ocean-bookings'")
expect(source).toContain('listPlatformTenants({ page: 1, size: 200 })')
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
expect(source).toContain('listOceanBookings({ page: 1, size: 200, customerId })')
expect(source).toContain("form.oceanBookingId = ''")
expect(source).not.toContain('label="客户 ID"')
@@ -19,8 +19,8 @@ import type { PortScheduleData, PortScheduleView } from '../types'
const access = useAccessPreviewStore()
const rows = ref<PortScheduleView[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null)
const page = ref(1); const pageSize = ref(20); const total = ref(0)
const query = reactive<QueryModel>({ keyword: '', expired: '' })
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '船期号/船名/航次/港口', width: 230 }, { key: 'expired', type: 'select', label: '有效性', options: [{ label: '有效', value: 'false' }, { label: '已过期', value: 'true' }] }]
const query = reactive<QueryModel>({ customerId: '', keyword: '', expired: '' })
const fields = computed<QueryField[]>(() => [{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customerLabel(customer), value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } }, { key: 'keyword', type: 'input', label: '关键词', placeholder: '船期号/船名/航次/港口', width: 230 }, { key: 'expired', type: 'select', label: '有效性', options: [{ label: '有效', value: 'false' }, { label: '已过期', value: 'true' }] }])
const columns = [
{ key: 'actions', label: '操作', fixed: 'left', width: 165, slotName: 'actions', hideable: false, movable: false },
{ prop: 'schedule.scheduleNo', label: '船期编号', minWidth: 145 }, { prop: 'schedule.sourceName', label: '信息来源', minWidth: 130 },
@@ -41,17 +41,17 @@ const selectableCustomers = computed(() => customers.value.filter((customer) =>
const canCreate = computed(() => access.can('platform:firstleg:port-schedule:create'))
const dialogTitle = computed(() => dialogMode.value === 'create' ? '新增船期' : dialogMode.value === 'edit' ? '编辑船期' : '查看船期')
const dialogReadonly = computed(() => dialogMode.value === 'view')
function customerLabel(customer: PlatformTenantResource) { return `${customer.tenantName}(${customer.tenantCode})` }
function customerLabel(customer: PlatformTenantResource) { return customer.legalName?.trim() || customer.tenantName }
function bookingLabel(booking: OceanBookingResource) { return [booking.bookingNo || '待生成订舱号', [booking.vesselName, booking.voyageNo].filter(Boolean).join(' / '), [booking.departurePortCode, booking.destinationPortCode].filter(Boolean).join(' → ')].filter(Boolean).join(' · ') }
async function loadCustomers() { const serial = ++customerRequestSerial; customersLoading.value = true; customersError.value = null; try { const result = await listPlatformTenants({ page: 1, size: 200 }); if (serial === customerRequestSerial) customers.value = result.records } catch (cause) { if (serial === customerRequestSerial) customersError.value = getApiErrorDetails(cause, '客户候选加载失败') } finally { if (serial === customerRequestSerial) customersLoading.value = false } }
async function loadCustomers() { const serial = ++customerRequestSerial; customersLoading.value = true; customersError.value = null; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); if (serial === customerRequestSerial) customers.value = result.records } catch (cause) { if (serial === customerRequestSerial) customersError.value = getApiErrorDetails(cause, '客户候选加载失败') } finally { if (serial === customerRequestSerial) customersLoading.value = false } }
async function loadBookings(customerId = String(form.customerId || '')) { const serial = ++bookingRequestSerial; bookings.value = []; bookingsError.value = null; if (!customerId) { bookingsLoading.value = false; return } bookingsLoading.value = true; try { const result = await listOceanBookings({ page: 1, size: 200, customerId }); if (serial === bookingRequestSerial && customerId === String(form.customerId || '')) bookings.value = result.records } catch (cause) { if (serial === bookingRequestSerial && customerId === String(form.customerId || '')) bookingsError.value = getApiErrorDetails(cause, '订舱候选加载失败') } finally { if (serial === bookingRequestSerial) bookingsLoading.value = false } }
function handleCustomerChange(value: string) { form.customerId = String(value || ''); form.oceanBookingId = ''; void formRef.value?.clearValidate('oceanBookingId'); void loadBookings(form.customerId) }
async function load() { loading.value = true; error.value = null; try { const result = await listPortSchedules({ page: page.value, size: pageSize.value, keyword: String(query.keyword || '') || undefined, expired: query.expired === '' ? undefined : query.expired === 'true' }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
async function load() { loading.value = true; error.value = null; try { const result = await listPortSchedules({ page: page.value, size: pageSize.value, customerId: String(query.customerId || '') || undefined, keyword: String(query.keyword || '') || undefined, expired: query.expired === '' ? undefined : query.expired === 'true' }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
function search() { if (page.value === 1) void load(); else page.value = 1 }
function openCreate() { dialogMode.value = 'create'; editingId.value = ''; expectedVersion.value = 0; Object.assign(form, blank()); bookings.value = []; bookingsError.value = null; actionError.value = null; dialogOpen.value = true; if (!customers.value.length) void loadCustomers() }
async function openSchedule(id: string, mode: Extract<DialogMode, 'view' | 'edit'>) { actionError.value = null; try { const result = await getPortSchedule(id); dialogMode.value = mode; editingId.value = id; expectedVersion.value = result.portSchedule.schedule.version; Object.assign(form, result.portSchedule.schedule); dialogOpen.value = true; if (!customers.value.length) void loadCustomers(); void loadBookings(String(form.customerId || '')) } catch (cause) { error.value = getApiErrorDetails(cause) } }
async function save() { if (dialogReadonly.value || !await formRef.value?.validate().catch(() => false)) return; saving.value = true; actionError.value = null; try { if (editingId.value) await updatePortSchedule(editingId.value, { expectedVersion: expectedVersion.value, data: { ...form }, reason: '后台维护船期' }); else await createPortSchedule({ ...form }); ElMessage.success(editingId.value ? '船期已更新' : '船期已新增'); dialogOpen.value = false; await load() } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409' && editingId.value) await openSchedule(editingId.value, 'edit') } finally { saving.value = false } }
watch([page, pageSize], () => void load()); onMounted(load)
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers() })
</script>
<template>
<AdminPage as="div"><ListPageLayout aria-label="船期列表">
+1 -1
View File
@@ -29,6 +29,6 @@ export interface PortScheduleView { schedule: PortScheduleResource; expired: boo
export interface PortScheduleHistory { id: string; action: string; fromVersion?: number | null; toVersion?: number | null; reason?: string | null; operatorName?: string | null; createdAt: string }
export interface PortScheduleDetail { portSchedule: PortScheduleView; histories: PortScheduleHistory[] }
export interface PortSchedulePage { records: PortScheduleView[]; page: number; size: number; total: number; pages: number }
export interface PortScheduleQuery { page: number; size: number; keyword?: string; expired?: boolean }
export interface PortScheduleQuery { page: number; size: number; customerId?: string; keyword?: string; expired?: boolean }
export type PortScheduleData = Omit<PortScheduleResource, 'id' | 'scheduleNo' | 'version' | 'updatedAt'>
export interface PortScheduleSaveRequest { expectedVersion: number; data: PortScheduleData; reason?: string | null }