优化
This commit is contained in:
@@ -6,7 +6,15 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/p
|
||||
|
||||
describe('OceanBookingDetailView interaction contract', () => {
|
||||
it('selects business objects and automatically keeps their ids and names together', () => {
|
||||
expect(source).toContain('placeholder="请选择客户"')
|
||||
expect(source).toContain('placeholder="请选择客户(可选)"')
|
||||
expect(source).toContain('filterable clearable')
|
||||
expect(source).toContain('@update:model-value="selectCustomer"')
|
||||
expect(source).toContain('no-data-text="暂无可选客户"')
|
||||
expect(source).toContain('customerId: null, customerName: null')
|
||||
expect(source).toContain("value == null || value === '' ? null : String(value)")
|
||||
expect(source).toContain('form.data.customerName = form.data.customerId ? form.data.customerName : null')
|
||||
expect(source).toContain('normalizeDraftData()')
|
||||
expect(source).not.toContain("customerId: [{ required: true")
|
||||
expect(source).toContain('placeholder="请选择船公司"')
|
||||
expect(source).toContain('placeholder="请选择订舱代理"')
|
||||
expect(source).toContain("selectPartner('shippingCompany'")
|
||||
@@ -38,22 +46,37 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
|
||||
it('provides guidance for every free-text booking field', () => {
|
||||
for (const placeholder of [
|
||||
'请输入提单号(可选)', '请输入船名', '请输入航次', '例如:CNNGB', '例如:宁波港',
|
||||
'例如:USLAX', '例如:洛杉矶港', '请输入最终交货地点', '请输入订舱备注(可选)',
|
||||
'例如:CNNGB', '例如:宁波港', '例如:USLAX', '例如:洛杉矶港',
|
||||
'请输入最终交货地点', '请输入订舱备注(可选)',
|
||||
]) expect(source).toContain(`placeholder="${placeholder}"`)
|
||||
})
|
||||
|
||||
it('uses compact choices and keeps capacity snapshots behind one specification control', () => {
|
||||
expect(source).toContain("const serviceTermOptions = ['CY-CY', 'CY-DOOR', 'DOOR-CY', 'DOOR-DOOR']")
|
||||
expect(source).toContain("const freightTermOptions = ['PREPAID', 'COLLECT']")
|
||||
it('keeps release-only transport facts out of draft editing and saving', () => {
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
||||
expect(source).not.toContain(`v-model="form.data.${field}"`)
|
||||
}
|
||||
expect(source).toContain(':model-value="detail?.billOfLadingNo || \'\'" disabled placeholder="确认放舱时录入"')
|
||||
expect(source).toContain(':model-value="detail?.vesselName || \'\'" disabled placeholder="确认放舱时录入"')
|
||||
expect(source).toContain('v-model="releaseForm.billOfLadingNo"')
|
||||
expect(source).toContain('v-model="releaseForm.vesselName"')
|
||||
expect(source).toContain('v-model="releaseForm.voyageNo"')
|
||||
expect(source).toContain('v-model="releaseForm.estimatedSailingAt"')
|
||||
expect(source).toContain('v-model="releaseForm.estimatedArrivalAt"')
|
||||
})
|
||||
|
||||
it('uses source-backed free text terms and keeps capacity snapshots behind one specification control', () => {
|
||||
expect(source).not.toContain('serviceTermOptions')
|
||||
expect(source).not.toContain('freightTermOptions')
|
||||
expect(source).toContain("const containerTypeOptions = ['20GP', '40GP', '40HC', '40NR', '45HC']")
|
||||
expect(source).toContain('<el-select v-model="form.data.serviceTerm"')
|
||||
expect(source).toContain('<el-select v-model="form.data.freightTerm"')
|
||||
expect(source).toContain('label="主要运输条款 Service Term" prop="serviceTerm"')
|
||||
expect(source).toContain('<el-input v-model="form.data.serviceTerm" clearable maxlength="32" placeholder="请输入 Service Term"')
|
||||
expect(source).toContain('label="运费条款 Freight Term" prop="freightTerm"')
|
||||
expect(source).toContain('<el-input v-model="form.data.freightTerm" clearable maxlength="32" placeholder="请输入 Freight Term"')
|
||||
expect(source).toContain('<el-select v-model="container.containerType"')
|
||||
expect(source).toContain('<el-select v-model="container.containerKind"')
|
||||
expect(source).toContain(':aria-label="`设置箱型 ${index + 1} 的容量与限重`"')
|
||||
expect(source).toContain('function containerSpecificationLabel(')
|
||||
expect(source).not.toContain('<el-input v-model="form.data.serviceTerm"')
|
||||
expect(source).not.toContain("['PREPAID', 'COLLECT']")
|
||||
expect(source).not.toContain('<el-input v-model="container.containerType"')
|
||||
})
|
||||
|
||||
@@ -124,7 +147,7 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
|
||||
it('shows client-side validation failures instead of rendering an empty error alert', () => {
|
||||
expect(source).toContain(':title="actionError.message"')
|
||||
expect(source).toContain("actionError.value = { message: '请完善客户、装柜仓库和物流渠道等必填信息' }")
|
||||
expect(source).toContain("actionError.value = { message: '请完善装柜仓库、物流渠道和运输条款等必填信息' }")
|
||||
expect(source).toContain("actionError.value = { message: '发货人、收货人和通知人名称均为必填项' }")
|
||||
expect(source).toContain('规格(必填)')
|
||||
expect(source).toContain('placeholder="单位名称(必填)"')
|
||||
|
||||
@@ -41,8 +41,6 @@ const partyTypes = [
|
||||
{ value: 'CONSIGNEE', label: '收货人' },
|
||||
{ value: 'NOTIFY_PARTY', label: '通知人' },
|
||||
] as const
|
||||
const serviceTermOptions = ['CY-CY', 'CY-DOOR', 'DOOR-CY', 'DOOR-DOOR']
|
||||
const freightTermOptions = ['PREPAID', 'COLLECT']
|
||||
const containerTypeOptions = ['20GP', '40GP', '40HC', '40NR', '45HC']
|
||||
const containerKindOptions = [
|
||||
{ value: 'FCL', label: '整箱 FCL' },
|
||||
@@ -100,15 +98,16 @@ function blankParties(): OceanBookingParty[] {
|
||||
}
|
||||
function blankData(): OceanBookingDraftData {
|
||||
return {
|
||||
customerId: '', customerName: '', channelCode: '', warehouseId: '', warehouseName: '', parties: blankParties(),
|
||||
customerId: null, customerName: null, channelCode: '', warehouseId: '', warehouseName: '', serviceTerm: null, freightTerm: null, parties: blankParties(),
|
||||
containerRequirements: [createBlankContainerRequirement()],
|
||||
}
|
||||
}
|
||||
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
|
||||
const rules: FormRules = {
|
||||
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
warehouseId: [{ required: true, message: '请选择装柜仓库', trigger: 'change' }],
|
||||
channelCode: [{ required: true, message: '请选择物流渠道', trigger: 'change' }],
|
||||
serviceTerm: [{ required: true, message: '请输入主要运输条款 Service Term', trigger: 'blur' }],
|
||||
freightTerm: [{ required: true, message: '请输入运费条款 Freight Term', trigger: 'blur' }],
|
||||
}
|
||||
const editable = computed(() => isNew.value || detail.value?.status === 'DRAFT')
|
||||
const canSave = computed(() => isNew.value
|
||||
@@ -143,10 +142,17 @@ async function loadOptions() {
|
||||
optionsLoading.value = false
|
||||
}
|
||||
}
|
||||
function selectCustomer(customerId: string) {
|
||||
function selectCustomer(value: unknown) {
|
||||
const customerId = value == null || value === '' ? null : String(value)
|
||||
form.data.customerId = customerId
|
||||
const customer = customers.value.find((item) => String(item.id) === customerId)
|
||||
form.data.customerName = customer?.tenantName || ''
|
||||
const customer = customerId == null ? null : customers.value.find((item) => String(item.id) === customerId)
|
||||
form.data.customerName = customer?.tenantName || null
|
||||
}
|
||||
function normalizeDraftData() {
|
||||
form.data.customerId = form.data.customerId || null
|
||||
form.data.customerName = form.data.customerId ? form.data.customerName : null
|
||||
form.data.serviceTerm = form.data.serviceTerm?.trim() || null
|
||||
form.data.freightTerm = form.data.freightTerm?.trim() || null
|
||||
}
|
||||
function selectWarehouse(value: unknown) {
|
||||
const warehouseId = String(value || '')
|
||||
@@ -190,14 +196,12 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
detail.value = value
|
||||
form.expectedVersion = value.version
|
||||
form.data = {
|
||||
customerId: value.customerId || '', customerName: value.customerName || '', channelCode: value.channelCode || '', warehouseId: value.warehouseId || '', warehouseName: value.warehouseName || '', billOfLadingNo: value.billOfLadingNo,
|
||||
customerId: value.customerId ?? null, customerName: value.customerName ?? null, channelCode: value.channelCode || '', warehouseId: value.warehouseId || '', warehouseName: value.warehouseName || '',
|
||||
shippingCompanyId: value.shippingCompanyId, shippingCompanyName: value.shippingCompanyName,
|
||||
bookingAgentId: value.bookingAgentId, bookingAgentName: value.bookingAgentName,
|
||||
vesselName: value.vesselName, voyageNo: value.voyageNo,
|
||||
departurePortCode: value.departurePortCode, departurePortName: value.departurePortName,
|
||||
destinationPortCode: value.destinationPortCode, destinationPortName: value.destinationPortName,
|
||||
placeOfDelivery: value.placeOfDelivery, serviceTerm: value.serviceTerm, freightTerm: value.freightTerm,
|
||||
estimatedSailingAt: value.estimatedSailingAt, estimatedArrivalAt: value.estimatedArrivalAt,
|
||||
remark: value.remark,
|
||||
parties: value.parties.length ? value.parties.map(({ partyType, partyName, contactName, contactPhone, address }) => ({ partyType, partyName, contactName, contactPhone, address })) : blankParties(),
|
||||
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ containerType, containerKind, capacityCbm, maxWeightKg, quantity }) => ({ containerType, containerKind, capacityCbm: capacityCbm ?? null, maxWeightKg: maxWeightKg ?? null, quantity: quantity ?? null })) : [createBlankContainerRequirement()],
|
||||
@@ -217,8 +221,9 @@ async function load() {
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
actionError.value = null
|
||||
normalizeDraftData()
|
||||
if (!await formRef.value?.validate().catch(() => false)) {
|
||||
actionError.value = { message: '请完善客户、装柜仓库和物流渠道等必填信息' }
|
||||
actionError.value = { message: '请完善装柜仓库、物流渠道和运输条款等必填信息' }
|
||||
return
|
||||
}
|
||||
if (form.data.parties.some((party) => !party.partyName.trim())) {
|
||||
@@ -339,7 +344,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<template #header>
|
||||
<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 || '—'}` : '填写并保存订舱资料'">
|
||||
<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="statusLabel(detail.status)" /></div></template>
|
||||
</PageToolbar>
|
||||
</div>
|
||||
@@ -361,33 +366,33 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<section class="booking-group" aria-label="基本信息">
|
||||
<h3>基本信息</h3><div class="booking-group__body booking-group__body--fields">
|
||||
<el-form-item label="订舱号"><el-input :model-value="detail?.bookingNo || '保存后生成'" disabled /></el-form-item>
|
||||
<el-form-item label="客户" prop="customerId"><el-select :model-value="form.data.customerId" filterable placeholder="请选择客户" :loading="optionsLoading" :disabled="!isNew" style="width:100%" @update:model-value="selectCustomer(String($event || ''))"><el-option v-if="form.data.customerId && !selectableCustomers.some((customer) => String(customer.id) === String(form.data.customerId))" :label="form.data.customerName || '客户名称缺失'" :value="String(form.data.customerId)" /><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="客户(可选)"><el-select :model-value="form.data.customerId" filterable clearable placeholder="请选择客户(可选)" no-data-text="暂无可选客户" :loading="optionsLoading" style="width:100%" @update:model-value="selectCustomer"><el-option v-if="form.data.customerId && !selectableCustomers.some((customer) => String(customer.id) === String(form.data.customerId))" :label="form.data.customerName || '客户名称缺失'" :value="String(form.data.customerId)" /><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item>
|
||||
<el-form-item label="状态"><el-input :model-value="detail ? statusLabel(detail.status) : '待创建'" disabled /></el-form-item>
|
||||
<el-form-item label="装柜仓库" prop="warehouseId"><el-select :model-value="form.data.warehouseId" filterable placeholder="请选择装柜仓库" :loading="optionsLoading" style="width:100%" @update:model-value="selectWarehouse"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" /></el-select></el-form-item>
|
||||
<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="提单号"><el-input v-model="form.data.billOfLadingNo" placeholder="请输入提单号(可选)" /></el-form-item>
|
||||
<el-form-item label="提单号"><el-input :model-value="detail?.billOfLadingNo || ''" disabled 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="selectPartner('shippingCompany', 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" style="width:100%" @update:model-value="selectPartner('bookingAgent', String($event || ''))"><el-option v-if="form.data.bookingAgentId && !bookingAgents.some((partner) => String(partner.id) === String(form.data.bookingAgentId))" :label="form.data.bookingAgentName || '订舱代理名称缺失'" :value="String(form.data.bookingAgentId)" /><el-option v-for="partner in bookingAgents" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="booking-group" aria-label="航线船期">
|
||||
<h3>航线船期</h3><div class="booking-group__body booking-group__body--fields">
|
||||
<el-form-item label="船名"><el-input v-model="form.data.vesselName" placeholder="请输入船名" /></el-form-item>
|
||||
<el-form-item label="航次"><el-input v-model="form.data.voyageNo" placeholder="请输入航次" /></el-form-item>
|
||||
<el-form-item label="船名"><el-input :model-value="detail?.vesselName || ''" disabled placeholder="确认放舱时录入" /></el-form-item>
|
||||
<el-form-item label="航次"><el-input :model-value="detail?.voyageNo || ''" disabled placeholder="确认放舱时录入" /></el-form-item>
|
||||
<el-form-item label="起运港代码"><el-input v-model="form.data.departurePortCode" placeholder="例如:CNNGB" /></el-form-item>
|
||||
<el-form-item label="起运港"><el-input v-model="form.data.departurePortName" placeholder="例如:宁波港" /></el-form-item>
|
||||
<el-form-item label="目的港代码"><el-input v-model="form.data.destinationPortCode" placeholder="例如:USLAX" /></el-form-item>
|
||||
<el-form-item label="目的港"><el-input v-model="form.data.destinationPortName" placeholder="例如:洛杉矶港" /></el-form-item>
|
||||
<el-form-item class="booking-field--wide" label="交货地点"><el-input v-model="form.data.placeOfDelivery" placeholder="请输入最终交货地点" /></el-form-item>
|
||||
<el-form-item label="预计开船"><el-date-picker v-model="form.data.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计开船时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="预计到港"><el-date-picker v-model="form.data.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="预计开船"><el-date-picker :model-value="detail?.estimatedSailingAt || null" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" disabled placeholder="确认放舱时录入" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="预计到港"><el-date-picker :model-value="detail?.estimatedArrivalAt || null" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" disabled placeholder="确认放舱时录入" style="width:100%" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="booking-group" aria-label="条款与柜量">
|
||||
<h3>条款与柜量</h3><div class="booking-group__body">
|
||||
<div class="booking-terms">
|
||||
<el-form-item label="服务条款"><el-select v-model="form.data.serviceTerm" clearable placeholder="请选择服务条款"><el-option v-for="value in serviceTermOptions" :key="value" :label="value" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item label="运费条款"><el-select v-model="form.data.freightTerm" clearable placeholder="请选择运费条款"><el-option v-for="value in freightTermOptions" :key="value" :label="value" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item label="主要运输条款 Service Term" prop="serviceTerm"><el-input v-model="form.data.serviceTerm" clearable maxlength="32" placeholder="请输入 Service Term" /></el-form-item>
|
||||
<el-form-item label="运费条款 Freight Term" prop="freightTerm"><el-input v-model="form.data.freightTerm" clearable maxlength="32" placeholder="请输入 Freight Term" /></el-form-item>
|
||||
</div>
|
||||
<div class="container-table-wrap" aria-label="箱型箱量">
|
||||
<table class="container-table">
|
||||
|
||||
Reference in New Issue
Block a user