This commit is contained in:
2026-08-17 16:45:46 +08:00
parent 4a9438315c
commit d95c90bd1a
48 changed files with 1217 additions and 374 deletions
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { isActiveBookingAgent } from './bookingAgents'
describe('ocean booking agent candidates', () => {
it('accepts an active booking supplier', () => {
expect(isActiveBookingAgent({ category: 'BOOKING', status: 'ACTIVE' })).toBe(true)
})
it.each([
{ category: 'BOOKING', status: 'INACTIVE' },
{ category: 'TRUCKING', status: 'ACTIVE' },
])('rejects a non-eligible supplier: %o', (supplier) => {
expect(isActiveBookingAgent(supplier)).toBe(false)
})
})
@@ -0,0 +1,9 @@
export interface BookingAgentCandidate {
category: string
status: string
}
export function isActiveBookingAgent(candidate: BookingAgentCandidate) {
return candidate.category === 'BOOKING'
&& candidate.status === 'ACTIVE'
}
@@ -29,11 +29,18 @@ describe('OceanBookingDetailView interaction contract', () => {
expect(source).not.toContain("customerId: [{ required: true")
expect(source).toContain('placeholder="请选择船公司"')
expect(source).toContain('placeholder="请选择订舱代理"')
expect(source).toContain('listSuppliers({ page: 1, size: 200, category: \'BOOKING\', status: \'ACTIVE\' })')
expect(source).toContain("listSuppliers({ page: 1, size: 200, category: 'BOOKING', status: 'ACTIVE' })")
expect(source).toContain('supplierResult.records.filter(isActiveBookingAgent)')
expect(source).toContain('bookingSuppliers.value.filter(isActiveBookingAgent)')
expect(source).toContain('selectShippingCompany')
expect(source).toContain('selectBookingAgent')
expect(source).toContain('bookingAgentLabel')
expect(source).toContain('暂无启用的订舱供应商')
expect(source).toContain('暂无启用的订舱代理')
expect(source).toContain(':label="supplier.name"')
expect(source).not.toContain('bookingAgentLabel')
expect(source).not.toContain("scope: 'INTERNATIONAL'")
expect(source).toContain('!editable && form.data.bookingAgentId')
expect(source).toContain('form.data.bookingAgentId = supplier ? String(supplier.id) : null')
expect(source).toContain('form.data.bookingAgentName = supplier?.name || null')
expect(source).not.toContain('label="客户 ID"')
expect(source).not.toContain('label="船公司 ID"')
expect(source).not.toContain('label="订舱代理 ID"')
@@ -36,6 +36,7 @@ import type {
} from '../types'
import { oceanBookingStatusLabel } from '../status'
import { bookingContainerError, createBlankBookingContainer, selectBookingContainerType } from '../containerRequirements'
import { isActiveBookingAgent } from '../bookingAgents'
const partyTypes = [
{ value: 'SHIPPER', label: '发货人' },
@@ -88,9 +89,7 @@ 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((supplier) => (
supplier.status === 'ACTIVE' || String(supplier.id) === String(form.data.bookingAgentId)
)))
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[] {
@@ -140,7 +139,7 @@ async function loadOptions() {
])
customers.value = customerResult.records.filter((customer) => customer.status === 'ACTIVE')
partners.value = partnerResult.records
bookingSuppliers.value = supplierResult.records
bookingSuppliers.value = supplierResult.records.filter(isActiveBookingAgent)
warehouses.value = warehouseResult.records.filter((warehouse) => warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'DOMESTIC')
} catch (cause) {
optionsError.value = getApiErrorDetails(cause, '客户、船公司或订舱代理选项加载失败').message
@@ -197,13 +196,10 @@ function selectShippingCompany(partnerId: string) {
form.data.shippingCompanyName = partner?.partnerName || null
}
function selectBookingAgent(supplierId: string) {
const supplier = bookingSuppliers.value.find((item) => String(item.id) === supplierId)
form.data.bookingAgentId = supplierId || null
const supplier = selectableBookingAgents.value.find((item) => String(item.id) === supplierId)
form.data.bookingAgentId = supplier ? String(supplier.id) : null
form.data.bookingAgentName = supplier?.name || null
}
function bookingAgentLabel(supplier: Supplier) {
return `${supplier.name}(${supplier.scope === 'INTERNATIONAL' ? '国外' : '国内'})`
}
function applyDetail(value: OceanBookingDetail) {
detail.value = value
form.expectedVersion = value.version
@@ -379,7 +375,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="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="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="bookingAgentLabel(supplier)" :value="String(supplier.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>
</div>
</section>
<section class="booking-group" aria-label="航线船期">
@@ -478,5 +474,5 @@ onMounted(() => { void load(); void loadOptions() })
<style scoped>
.channel-code{display:block;margin-left:8px;color:var(--cc-muted);font-size:11px}.field-error,.field-hint{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:5px;color:var(--cc-muted);font-size:12px;line-height:20px}.field-error{color:var(--el-color-danger)}
.booking-detail{min-width:0}.detail-heading{display:flex;min-width:0;align-items:flex-start;gap:16px}.booking-page-toolbar{min-width:0;flex:1}.section-title p,.history-panel p{margin:5px 0 0;color:var(--cc-muted);font-size:13px}.heading-status{display:flex;align-items:center;gap:8px}.surface-panel{padding:20px}.surface-panel h2{margin:0;font-size:16px}.section-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:18px}.actions{display:flex;flex-wrap:wrap;gap:8px}.booking-groups{display:grid;grid-template-columns:minmax(0,1.4fr) repeat(2,minmax(0,1fr));gap:16px}.booking-group{min-width:0;overflow:hidden;border:1px solid #e8edf3;border-radius:8px;background:#fff}.booking-group>h3{margin:0;padding:14px 16px;border-bottom:1px solid #eef2f6;color:#1f2937;font-size:14px}.booking-group__body{padding:16px}.booking-group__body--fields,.booking-terms{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}.booking-field--wide{grid-column:1/-1}.booking-terms :deep(.el-select){width:100%}.party-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.subsection{display:grid;gap:10px;padding:14px;border:1px solid var(--cc-border);border-radius:6px}.subsection h3{margin:0;font-size:14px}.required-mark{margin-right:4px;color:var(--el-color-danger)}.container-table-wrap{overflow-x:auto;margin-bottom:10px}.container-table{width:100%;min-width:780px;table-layout:fixed;border-collapse:collapse;font-size:12px}.container-table th,.container-table td{padding:7px 6px;border-bottom:1px solid #eef2f6;text-align:left;vertical-align:middle}.container-table th{background:#fafafa;color:var(--cc-muted);font-weight:650;white-space:nowrap}.container-table th:first-child,.container-table td:first-child{width:56px}.container-table th:nth-child(2),.container-table td:nth-child(2){width:180px}.container-table th:nth-child(3),.container-table td:nth-child(3){width:90px}.container-table th:nth-child(4),.container-table td:nth-child(4){width:110px}.container-table th:nth-child(5),.container-table td:nth-child(5),.container-table th:nth-child(6),.container-table td:nth-child(6){width:100px}.container-table td :deep(.el-select),.container-table td :deep(.el-input){width:100%;min-width:0}.container-table__action{text-align:center!important}.container-table__action :deep(.el-button){width:28px;height:28px}.container-table__specification{color:var(--cc-muted);white-space:nowrap}.container-actions{display:flex;align-items:center;gap:10px;margin-bottom:18px;color:var(--cc-muted);font-size:12px}.add-container{margin:0}.history-panel strong{font-size:14px}@media(max-width:1599px){.booking-groups{grid-template-columns:repeat(2,minmax(0,1fr))}.booking-group:last-child{grid-column:1/-1}}@media(max-width:720px){.booking-groups,.party-grid{grid-template-columns:1fr}.booking-group:last-child{grid-column:auto}.detail-heading{flex-direction:column}.booking-page-toolbar{width:100%}.section-title{flex-direction:column}.actions{width:100%}}@media(max-width:520px){.booking-group__body--fields,.booking-terms{grid-template-columns:1fr}.booking-field--wide{grid-column:auto}}@media(max-width:420px){.surface-panel{padding:12px}.booking-group__body{padding:14px}.actions :deep(.el-button){min-height:44px}}
.booking-detail{min-width:0;max-width:1880px}.detail-heading{display:flex;min-width:0;align-items:flex-start;gap:16px}.booking-page-toolbar{min-width:0;flex:1}.section-title p,.history-panel p{margin:5px 0 0;color:var(--cc-muted);font-size:13px}.heading-status{display:flex;align-items:center;gap:8px}.surface-panel{padding:16px}.surface-panel h2{margin:0;font-size:16px}.section-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px}.actions{display:flex;flex-wrap:wrap;gap:8px}.booking-groups{display:grid;grid-template-columns:minmax(0,1.5fr) repeat(2,minmax(0,1fr));gap:14px}.booking-group{min-width:0;overflow:hidden;border:1px solid #e8edf3;border-radius:10px;background:#fff}.booking-group>h3{margin:0;padding:12px 14px;border-bottom:1px solid #eef2f6;color:#1f2937;font-size:14px}.booking-group__body{padding:12px 14px}.booking-group__body--fields,.booking-terms{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:10px;row-gap:8px}.booking-field--wide{grid-column:1/-1}.booking-terms :deep(.el-select){width:100%}.party-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:14px}.subsection{display:grid;gap:10px;padding:12px;border:1px solid var(--cc-border);border-radius:6px}.subsection h3{margin:0;font-size:14px}.required-mark{margin-right:4px;color:var(--el-color-danger)}.container-table-wrap{overflow-x:auto;margin-bottom:10px}.container-table{width:100%;min-width:780px;table-layout:fixed;border-collapse:collapse;font-size:12px}.container-table th,.container-table td{padding:7px 6px;border-bottom:1px solid #eef2f6;text-align:left;vertical-align:middle}.container-table th{background:#fafafa;color:var(--cc-muted);font-weight:650;white-space:nowrap}.container-table th:first-child,.container-table td:first-child{width:56px}.container-table th:nth-child(2),.container-table td:nth-child(2){width:180px}.container-table th:nth-child(3),.container-table td:nth-child(3){width:90px}.container-table th:nth-child(4),.container-table td:nth-child(4){width:110px}.container-table th:nth-child(5),.container-table td:nth-child(5),.container-table th:nth-child(6),.container-table td:nth-child(6){width:100px}.container-table td :deep(.el-select),.container-table td :deep(.el-input){width:100%;min-width:0}.container-table__action{text-align:center!important}.container-table__action :deep(.el-button){width:28px;height:28px}.container-table__specification{color:var(--cc-muted);white-space:nowrap}.container-actions{display:flex;align-items:center;gap:10px;margin-bottom:14px;color:var(--cc-muted);font-size:12px}.add-container{margin:0}.history-panel strong{font-size:14px}@media(max-width:1599px){.booking-groups{grid-template-columns:repeat(2,minmax(0,1fr))}.booking-group:last-child{grid-column:1/-1}}@media(max-width:720px){.booking-groups,.party-grid{grid-template-columns:1fr}.booking-group:last-child{grid-column:auto}.detail-heading{flex-direction:column}.booking-page-toolbar{width:100%}.section-title{flex-direction:column}.actions{width:100%}}@media(max-width:520px){.booking-group__body--fields,.booking-terms{grid-template-columns:1fr}.booking-field--wide{grid-column:auto}}@media(max-width:420px){.surface-panel{padding:12px}.booking-group__body{padding:12px}.actions :deep(.el-button){min-height:44px}}
</style>