feat(admin): filter booking agents by carrier
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createOceanBooking, deleteOceanBookingDraft, listOceanBookingLinkCandidates, listOceanBookings, listWarehouseLogisticsChannels, releaseOceanBooking, updateOceanBookingDraft } from './platformOceanBookings'
|
||||
import { createOceanBooking, deleteOceanBookingDraft, listOceanBookingAgents, listOceanBookingLinkCandidates, listOceanBookings, listWarehouseLogisticsChannels, releaseOceanBooking, updateOceanBookingDraft } from './platformOceanBookings'
|
||||
import { http } from '@/shared/api/http'
|
||||
|
||||
const detailResponse = (version = 0, status = 'DRAFT') => ({
|
||||
@@ -68,4 +68,15 @@ describe('ocean bookings api', () => {
|
||||
await expect(listWarehouseLogisticsChannels('8')).resolves.toEqual([{ channelCode: 'SEA_FCL', channelName: '海运整箱' }])
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/ocean-bookings/channels', { params: { originWarehouseId: '8' } })
|
||||
})
|
||||
|
||||
it('loads only server-filtered booking agents for the selected carrier and preserves long ids', async () => {
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ supplierId: 9007199254740993n, name: '代理A', carrierCodes: ['MATSON', 'COSCO'] }] })
|
||||
await expect(listOceanBookingAgents('MATSON')).resolves.toEqual([{ supplierId: '9007199254740993', name: '代理A', carrierCodes: ['MATSON', 'COSCO'] }])
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/ocean-bookings/booking-agents', { params: { carrierCode: 'MATSON' } })
|
||||
})
|
||||
|
||||
it('treats an empty server candidate response as a successful empty list', async () => {
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [] })
|
||||
await expect(listOceanBookingAgents('HEDE')).resolves.toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OceanBookingDetail, OceanBookingLinkCandidatePage, OceanBookingLinkCandidateQuery, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
|
||||
import type { OceanBookingAgentOption, OceanBookingCarrierCode, OceanBookingDetail, OceanBookingLinkCandidatePage, OceanBookingLinkCandidateQuery, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
|
||||
|
||||
const basePath = '/platform/v1/ocean-bookings'
|
||||
|
||||
@@ -16,6 +16,7 @@ function booking(raw: OceanBookingResource): OceanBookingResource {
|
||||
version: Number(item.version ?? 0),
|
||||
customerId: raw.customerId == null ? null : String(raw.customerId),
|
||||
shippingCompanyId: raw.shippingCompanyId == null ? null : String(raw.shippingCompanyId),
|
||||
carrierCode: raw.carrierCode ?? null,
|
||||
bookingAgentId: raw.bookingAgentId == null ? null : String(raw.bookingAgentId),
|
||||
containerCount: Number(raw.containerCount ?? 0),
|
||||
pol: raw.pol ?? raw.departurePortName ?? null,
|
||||
@@ -91,4 +92,8 @@ export async function listWarehouseLogisticsChannels(warehouseId: string): Promi
|
||||
const response = await http.get<ApiEnvelope<OceanLogisticsChannel[]>, ApiEnvelope<OceanLogisticsChannel[]>>(`${basePath}/channels`, { params: { originWarehouseId: warehouseId } })
|
||||
return (unwrapApiResponse(response) || []).map((item) => ({ channelCode: String(item.channelCode), channelName: String(item.channelName) }))
|
||||
}
|
||||
export async function listOceanBookingAgents(carrierCode: OceanBookingCarrierCode): Promise<OceanBookingAgentOption[]> {
|
||||
const response = await http.get<ApiEnvelope<OceanBookingAgentOption[]>, ApiEnvelope<OceanBookingAgentOption[]>>(`${basePath}/booking-agents`, { params: { carrierCode } })
|
||||
return (unwrapApiResponse(response) || []).map((item) => ({ ...item, supplierId: String(item.supplierId), name: String(item.name), carrierCodes: [...(item.carrierCodes || [])] }))
|
||||
}
|
||||
export type { OceanBookingResource, OceanBookingStatus }
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isActiveBookingAgent } from './bookingAgents'
|
||||
import { shouldClearBookingAgent } from './bookingAgents'
|
||||
|
||||
describe('ocean booking agent candidates', () => {
|
||||
it('accepts an active booking supplier', () => {
|
||||
expect(isActiveBookingAgent({ category: 'BOOKING', status: 'ACTIVE' })).toBe(true)
|
||||
const options = [{ supplierId: '11', name: '代理A', carrierCodes: ['MATSON', 'COSCO'] as const }]
|
||||
|
||||
it('keeps an agent returned by the server-filtered carrier candidates', () => {
|
||||
expect(shouldClearBookingAgent('11', options.map((item) => ({ ...item, carrierCodes: [...item.carrierCodes] })))).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ category: 'BOOKING', status: 'INACTIVE' },
|
||||
{ category: 'TRUCKING', status: 'ACTIVE' },
|
||||
])('rejects a non-eligible supplier: %o', (supplier) => {
|
||||
expect(isActiveBookingAgent(supplier)).toBe(false)
|
||||
it('clears an agent missing from the newly selected carrier candidates', () => {
|
||||
expect(shouldClearBookingAgent('12', options.map((item) => ({ ...item, carrierCodes: [...item.carrierCodes] })))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not clear an empty selection', () => {
|
||||
expect(shouldClearBookingAgent(null, [])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
export interface BookingAgentCandidate {
|
||||
category: string
|
||||
status: string
|
||||
}
|
||||
import type { OceanBookingAgentOption } from './types'
|
||||
|
||||
export function isActiveBookingAgent(candidate: BookingAgentCandidate) {
|
||||
return candidate.category === 'BOOKING'
|
||||
&& candidate.status === 'ACTIVE'
|
||||
export function shouldClearBookingAgent(currentSupplierId: string | null | undefined, options: OceanBookingAgentOption[]) {
|
||||
return Boolean(currentSupplierId) && !options.some((option) => option.supplierId === String(currentSupplierId))
|
||||
}
|
||||
|
||||
@@ -49,21 +49,40 @@ describe('OceanBookingDetailView draft and release contract', () => {
|
||||
expect(source).toContain('function releaseValidationError()')
|
||||
expect(source).toContain('预计到港时间不能早于预计开船时间')
|
||||
expect(source).toContain("[form.data.customerId, '客户']")
|
||||
expect(source).toContain("if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'")
|
||||
expect(source).toContain("return '船公司标准编码尚未保存,请先保存草稿后再放舱'")
|
||||
expect(source).toContain('放舱前请填写${missing[1]}')
|
||||
expect(source).toContain('放舱后资料将同步到关联货柜,不能修改、不能删除;填写错误只能由管理员处理数据库,请确认资料无误。')
|
||||
expect(source).toContain('releaseOceanBooking(id.value, { expectedVersion: detail.value.version })')
|
||||
expect(source).not.toContain('releaseReason')
|
||||
})
|
||||
|
||||
it('keeps warehouse-scoped channel and customer/supplier selectors accessible', () => {
|
||||
it('freezes the selected carrier standard code into the booking draft', () => {
|
||||
expect(source).toContain('form.data.carrierCode = partner?.carrierCode ?? null')
|
||||
expect(source).toContain('carrierCode: value.carrierCode ?? selectedCarrier?.carrierCode ?? null')
|
||||
expect(source).toContain('form.data.shippingCompanyId && !form.data.carrierCode')
|
||||
expect(source).toContain('form.data.carrierCode = selected?.carrierCode ?? null')
|
||||
})
|
||||
|
||||
it('keeps warehouse-scoped channel and server-filtered booking-agent selectors accessible', () => {
|
||||
expect(source).toContain("warehouseType: 'DOMESTIC', status: 'ACTIVE'")
|
||||
expect(source).toContain('listWarehouseLogisticsChannels')
|
||||
expect(source).toContain('placeholder="草稿可暂不选择"')
|
||||
expect(source).toContain('暂无启用的订舱代理')
|
||||
expect(source).toContain('当前船公司暂无可用订舱代理')
|
||||
expect(source).toContain(':label="supplier.name"')
|
||||
expect(source).toContain('filterable clearable')
|
||||
})
|
||||
|
||||
it('loads booking agents from the carrier-filtered backend contract and clears an invalid selection', () => {
|
||||
expect(source).toContain('listOceanBookingAgents(carrierCode)')
|
||||
expect(source).toContain('shouldClearBookingAgent(form.data.bookingAgentId, options)')
|
||||
expect(source).toContain('clearBookingAgent()')
|
||||
expect(source).toContain("form.data.carrierCode ? '请选择订舱代理' : '请先选择船公司'")
|
||||
expect(source).toContain('<ApiErrorMeta :details="bookingAgentsError" />')
|
||||
expect(source).not.toContain('listSuppliers(')
|
||||
expect(source).not.toContain('isActiveBookingAgent')
|
||||
})
|
||||
|
||||
it('keeps dense layout, operation feedback and narrow-screen behavior', () => {
|
||||
expect(source).toContain('<OperationErrorToast :details="actionError" />')
|
||||
expect(source).toContain('LoadErrorState')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Plus, Save, Trash2 } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useWorkspaceTabsStore } from '@/core/workspace-tabs/workspaceTabs'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
@@ -17,13 +17,13 @@ import PageToolbar from '@/shared/components/PageToolbar.vue'
|
||||
import { CONTAINER_SPECIFICATIONS } from '@/shared/domain/containerSpecifications'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
|
||||
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses'
|
||||
import { CarrierScheduleQueryPanel } from '@/modules/port-schedules'
|
||||
import {
|
||||
createOceanBooking,
|
||||
deleteOceanBookingDraft,
|
||||
getOceanBooking,
|
||||
listOceanBookingAgents,
|
||||
releaseOceanBooking,
|
||||
listWarehouseLogisticsChannels,
|
||||
updateOceanBookingDraft,
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
} from '../api/platformOceanBookings'
|
||||
import type {
|
||||
OceanBookingDetail,
|
||||
OceanBookingAgentOption,
|
||||
OceanBookingCarrierCode,
|
||||
OceanBookingDraftData,
|
||||
OceanBookingParty,
|
||||
OceanBookingSaveRequest,
|
||||
@@ -38,7 +40,7 @@ import type {
|
||||
} from '../types'
|
||||
import { oceanBookingStatusLabel } from '../status'
|
||||
import { containerRequirementError, createBlankContainerRequirement, selectContainerRequirementType } from '../containerRequirements'
|
||||
import { isActiveBookingAgent } from '../bookingAgents'
|
||||
import { shouldClearBookingAgent } from '../bookingAgents'
|
||||
|
||||
const partyTypes = [
|
||||
{ value: 'SHIPPER', label: '发货人' },
|
||||
@@ -65,7 +67,9 @@ const actionError = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const partners = ref<PlatformPartnerResource[]>([])
|
||||
const bookingSuppliers = ref<Supplier[]>([])
|
||||
const bookingAgents = ref<OceanBookingAgentOption[]>([])
|
||||
const bookingAgentsLoading = ref(false)
|
||||
const bookingAgentsError = ref<ApiErrorDetails | null>(null)
|
||||
const warehouses = ref<PlatformWarehouseResource[]>([])
|
||||
const optionsLoading = ref(false)
|
||||
const optionsError = ref('')
|
||||
@@ -74,11 +78,11 @@ const channelsLoading = ref(false)
|
||||
const channelsError = ref('')
|
||||
const selectedChannelName = ref('')
|
||||
let channelRequestSerial = 0
|
||||
let bookingAgentRequestSerial = 0
|
||||
const selectableCustomers = computed(() => customers.value.filter((customer) => (
|
||||
customer.status === 'ACTIVE' || String(customer.id) === String(form.data.customerId)
|
||||
)))
|
||||
const shippingCompanies = computed(() => partners.value.filter((partner) => partner.types.includes('CARRIER')))
|
||||
const selectableBookingAgents = computed(() => bookingSuppliers.value.filter(isActiveBookingAgent))
|
||||
const channelUnavailable = computed(() => Boolean(form.data.warehouseId) && !channelsLoading.value && !channelsError.value && channels.value.length === 0)
|
||||
|
||||
function blankParties(): OceanBookingParty[] {
|
||||
@@ -88,6 +92,7 @@ function blankData(): OceanBookingDraftData {
|
||||
return {
|
||||
customerId: null, customerName: null, channelCode: '', warehouseId: '', serviceTerm: null, freightTerm: null, parties: blankParties(),
|
||||
billOfLadingNo: '', vesselName: '', voyageNo: '', estimatedSailingAt: null, estimatedArrivalAt: null,
|
||||
shippingCompanyId: null, shippingCompanyName: null, carrierCode: null,
|
||||
containerRequirements: [createBlankContainerRequirement()],
|
||||
}
|
||||
}
|
||||
@@ -106,18 +111,21 @@ async function loadOptions() {
|
||||
optionsLoading.value = true
|
||||
optionsError.value = ''
|
||||
try {
|
||||
const [customerResult, partnerResult, supplierResult, warehouseResult] = await Promise.all([
|
||||
const [customerResult, partnerResult, warehouseResult] = await Promise.all([
|
||||
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||
listPlatformPartners({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||
listSuppliers({ page: 1, size: 200, category: 'BOOKING', status: 'ACTIVE' }),
|
||||
listPlatformWarehouses({ page: 1, size: 200, warehouseType: 'DOMESTIC', status: 'ACTIVE' }),
|
||||
])
|
||||
customers.value = customerResult.records.filter((customer) => customer.status === 'ACTIVE')
|
||||
partners.value = partnerResult.records
|
||||
bookingSuppliers.value = supplierResult.records.filter(isActiveBookingAgent)
|
||||
if (form.data.shippingCompanyId && !form.data.carrierCode) {
|
||||
const selected = partners.value.find((partner) => String(partner.id) === String(form.data.shippingCompanyId))
|
||||
form.data.carrierCode = selected?.carrierCode ?? null
|
||||
}
|
||||
warehouses.value = warehouseResult.records.filter((warehouse) => warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'DOMESTIC')
|
||||
if (form.data.carrierCode) void loadBookingAgents(form.data.carrierCode)
|
||||
} catch (cause) {
|
||||
optionsError.value = getApiErrorDetails(cause, '客户、船公司或订舱代理选项加载失败').message
|
||||
optionsError.value = getApiErrorDetails(cause, '客户、船公司或仓库选项加载失败').message
|
||||
} finally {
|
||||
optionsLoading.value = false
|
||||
}
|
||||
@@ -165,24 +173,53 @@ function selectChannel(channelCode: string) {
|
||||
form.data.channelCode = channelCode
|
||||
selectedChannelName.value = channel?.channelName || ''
|
||||
}
|
||||
function clearBookingAgent() {
|
||||
form.data.bookingAgentId = null
|
||||
form.data.bookingAgentName = null
|
||||
}
|
||||
async function loadBookingAgents(carrierCode: OceanBookingCarrierCode) {
|
||||
const serial = ++bookingAgentRequestSerial
|
||||
bookingAgentsLoading.value = true
|
||||
bookingAgentsError.value = null
|
||||
try {
|
||||
const options = await listOceanBookingAgents(carrierCode)
|
||||
if (serial !== bookingAgentRequestSerial || carrierCode !== form.data.carrierCode) return
|
||||
bookingAgents.value = options
|
||||
if (editable.value && shouldClearBookingAgent(form.data.bookingAgentId, options)) clearBookingAgent()
|
||||
} catch (cause) {
|
||||
if (serial !== bookingAgentRequestSerial) return
|
||||
bookingAgents.value = []
|
||||
bookingAgentsError.value = getApiErrorDetails(cause, '订舱代理候选加载失败')
|
||||
} finally {
|
||||
if (serial === bookingAgentRequestSerial) bookingAgentsLoading.value = false
|
||||
}
|
||||
}
|
||||
function selectShippingCompany(partnerId: string) {
|
||||
const partner = partners.value.find((item) => String(item.id) === partnerId)
|
||||
form.data.shippingCompanyId = partnerId || null
|
||||
form.data.shippingCompanyName = partner?.partnerName || null
|
||||
form.data.carrierCode = partner?.carrierCode ?? null
|
||||
bookingAgents.value = []
|
||||
bookingAgentsError.value = null
|
||||
bookingAgentRequestSerial += 1
|
||||
if (form.data.carrierCode) void loadBookingAgents(form.data.carrierCode)
|
||||
else clearBookingAgent()
|
||||
}
|
||||
function selectBookingAgent(supplierId: string) {
|
||||
const supplier = selectableBookingAgents.value.find((item) => String(item.id) === supplierId)
|
||||
form.data.bookingAgentId = supplier ? String(supplier.id) : null
|
||||
form.data.bookingAgentName = supplier?.name || null
|
||||
const supplier = bookingAgents.value.find((item) => item.supplierId === supplierId)
|
||||
form.data.bookingAgentId = supplier?.supplierId ?? null
|
||||
form.data.bookingAgentName = supplier?.name ?? null
|
||||
}
|
||||
function applyDetail(value: OceanBookingDetail) {
|
||||
detail.value = value
|
||||
form.expectedVersion = value.version
|
||||
const selectedCarrier = partners.value.find((partner) => String(partner.id) === String(value.shippingCompanyId || ''))
|
||||
form.data = {
|
||||
customerId: value.customerId ?? null, customerName: value.customerName ?? null, channelCode: value.channelCode || '', warehouseId: value.warehouseId || '',
|
||||
billOfLadingNo: value.billOfLadingNo || '', vesselName: value.vesselName || '', voyageNo: value.voyageNo || '',
|
||||
estimatedSailingAt: value.estimatedSailingAt || null, estimatedArrivalAt: value.estimatedArrivalAt || null,
|
||||
shippingCompanyId: value.shippingCompanyId, shippingCompanyName: value.shippingCompanyName,
|
||||
carrierCode: value.carrierCode ?? selectedCarrier?.carrierCode ?? null,
|
||||
bookingAgentId: value.bookingAgentId, bookingAgentName: value.bookingAgentName,
|
||||
departurePortCode: value.departurePortCode, departurePortName: value.departurePortName,
|
||||
destinationPortCode: value.destinationPortCode, destinationPortName: value.destinationPortName,
|
||||
@@ -194,6 +231,7 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
selectedChannelName.value = value.channelName || ''
|
||||
channels.value = []
|
||||
if (form.data.warehouseId) void loadChannels(form.data.warehouseId)
|
||||
if (form.data.carrierCode) void loadBookingAgents(form.data.carrierCode)
|
||||
}
|
||||
async function load() {
|
||||
if (!id.value || isNew.value) return
|
||||
@@ -239,6 +277,11 @@ function openReleaseDialog() {
|
||||
releaseDialogOpen.value = true
|
||||
}
|
||||
function releaseValidationError() {
|
||||
if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'
|
||||
if (detail.value && (detail.value.carrierCode !== form.data.carrierCode
|
||||
|| String(detail.value.shippingCompanyId || '') !== String(form.data.shippingCompanyId || ''))) {
|
||||
return '船公司标准编码尚未保存,请先保存草稿后再放舱'
|
||||
}
|
||||
const requiredFields: Array<[unknown, string]> = [
|
||||
[form.data.customerId, '客户'], [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'],
|
||||
[form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'],
|
||||
@@ -321,7 +364,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<el-form-item label="物流渠道" prop="channelCode"><el-select :model-value="form.data.channelCode" filterable :disabled="!form.data.warehouseId || channelsLoading || !!channelsError || channelUnavailable" :loading="channelsLoading" :placeholder="form.data.warehouseId ? '请选择物流渠道' : '请先选择装柜仓库'" no-data-text="该仓库暂无有效物流渠道" style="width:100%" @update:model-value="selectChannel(String($event || ''))"><el-option v-if="form.data.channelCode && !channels.some((channel) => channel.channelCode === form.data.channelCode)" :label="selectedChannelName || form.data.channelCode" :value="form.data.channelCode" /><el-option v-for="channel in channels" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode"><span>{{ channel.channelName }}</span><small class="channel-code">{{ channel.channelCode }}</small></el-option></el-select><div v-if="channelsError" class="field-error"><span>{{ channelsError }}</span><el-button link type="primary" :loading="channelsLoading" @click="form.data.warehouseId && loadChannels(form.data.warehouseId)">重新加载</el-button></div><div v-else-if="channelUnavailable" class="field-hint">该仓库暂无有效物流渠道,请更换仓库或联系管理员。</div></el-form-item>
|
||||
<el-form-item label="提单号" prop="billOfLadingNo"><el-input v-model="form.data.billOfLadingNo" maxlength="128" placeholder="请输入提单号" /></el-form-item>
|
||||
<el-form-item label="船公司"><el-select :model-value="form.data.shippingCompanyId" filterable clearable placeholder="请选择船公司" :loading="optionsLoading" style="width:100%" @update:model-value="selectShippingCompany(String($event || ''))"><el-option v-if="form.data.shippingCompanyId && !shippingCompanies.some((partner) => String(partner.id) === String(form.data.shippingCompanyId))" :label="form.data.shippingCompanyName || '船公司名称缺失'" :value="String(form.data.shippingCompanyId)" /><el-option v-for="partner in shippingCompanies" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="订舱代理"><el-select :model-value="form.data.bookingAgentId" filterable clearable placeholder="请选择订舱代理" :loading="optionsLoading" no-data-text="暂无启用的订舱代理" style="width:100%" @update:model-value="selectBookingAgent(String($event || ''))"><el-option v-if="!editable && form.data.bookingAgentId && !selectableBookingAgents.some((supplier) => String(supplier.id) === String(form.data.bookingAgentId))" :label="form.data.bookingAgentName || '订舱代理名称缺失'" :value="String(form.data.bookingAgentId)" /><el-option v-for="supplier in selectableBookingAgents" :key="String(supplier.id)" :label="supplier.name" :value="String(supplier.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="订舱代理"><el-select :model-value="form.data.bookingAgentId" filterable clearable :disabled="!form.data.carrierCode || bookingAgentsLoading || !!bookingAgentsError" :placeholder="form.data.carrierCode ? '请选择订舱代理' : '请先选择船公司'" :loading="bookingAgentsLoading" no-data-text="当前船公司暂无可用订舱代理" style="width:100%" @update:model-value="selectBookingAgent(String($event || ''))"><el-option v-if="!editable && form.data.bookingAgentId && !bookingAgents.some((supplier) => supplier.supplierId === String(form.data.bookingAgentId))" :label="form.data.bookingAgentName || '订舱代理名称缺失'" :value="String(form.data.bookingAgentId)" /><el-option v-for="supplier in bookingAgents" :key="supplier.supplierId" :label="supplier.name" :value="supplier.supplierId" /></el-select><div v-if="bookingAgentsError" class="field-error"><ApiErrorMeta :details="bookingAgentsError" /><el-button link type="primary" :loading="bookingAgentsLoading" @click="form.data.carrierCode && loadBookingAgents(form.data.carrierCode)">重新加载</el-button></div></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="booking-group" aria-label="航线船期">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
export type OceanBookingStatus = 'DRAFT' | 'RELEASED'
|
||||
export type OceanBookingContainerType = '20GP' | '40GP' | '40HC' | '40NR' | '45HC'
|
||||
export type OceanBookingCarrierCode = 'MATSON' | 'SML' | 'ZIM' | 'COSCO' | 'EMC' | 'HEDE' | 'OTHER'
|
||||
|
||||
export interface OceanBookingAgentOption {
|
||||
supplierId: string
|
||||
name: string
|
||||
carrierCodes: OceanBookingCarrierCode[]
|
||||
}
|
||||
|
||||
export interface OceanBookingParty { id?: string; partyType: string; partyName: string; contactName?: string | null; contactPhone?: string | null; address?: string | null }
|
||||
export interface OceanBookingContainerRequirement {
|
||||
@@ -27,7 +34,7 @@ export interface OceanBookingDraftData {
|
||||
channelCode: string | null; warehouseId: string | null
|
||||
billOfLadingNo: string; vesselName: string; voyageNo: string
|
||||
estimatedSailingAt: string | null; estimatedArrivalAt: string | null
|
||||
shippingCompanyId?: string | null; shippingCompanyName?: string | null; bookingAgentId?: string | null; bookingAgentName?: string | null
|
||||
shippingCompanyId?: string | null; shippingCompanyName?: string | null; carrierCode?: OceanBookingCarrierCode | null; bookingAgentId?: string | null; bookingAgentName?: string | null
|
||||
departurePortCode?: string | null; departurePortName?: string | null; destinationPortCode?: string | null; destinationPortName?: string | null
|
||||
placeOfDelivery?: string | null; serviceTerm: string | null; freightTerm: string | null
|
||||
remark?: string | null
|
||||
@@ -38,7 +45,7 @@ export interface OceanBookingResource {
|
||||
/** 订舱柜型需求 quantity 的总和,不是已关联真实柜数。 */
|
||||
shipperName: string | null; consigneeName: string | null; containerCount: number
|
||||
channelCode: string | null; channelName: string | null; warehouseId: string | null; warehouseName: string | null
|
||||
shippingCompanyId: string | null; shippingCompanyName: string | null; bookingAgentId: string | null; bookingAgentName: string | null
|
||||
shippingCompanyId: string | null; shippingCompanyName: string | null; carrierCode: OceanBookingCarrierCode | null; bookingAgentId: string | null; bookingAgentName: string | null
|
||||
vesselName: string | null; voyageNo: string | null; departurePortCode: string | null; departurePortName: string | null
|
||||
destinationPortCode: string | null; destinationPortName: string | null; placeOfDelivery: string | null
|
||||
serviceTerm: string | null; freightTerm: string | null; estimatedSailingAt: string | null; estimatedArrivalAt: string | null
|
||||
|
||||
Reference in New Issue
Block a user