优化优化

This commit is contained in:
2026-08-11 18:06:35 +08:00
parent 8a8c5a3faa
commit 03e1cd0c80
40 changed files with 2092 additions and 354 deletions
+1
View File
@@ -1,2 +1,3 @@
export { oceanBookingRoutes } from './routes'
export { listOceanBookings } from './api/platformOceanBookings'
export * from './types'
@@ -4,30 +4,29 @@ import { describe, expect, it } from 'vitest'
const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingDetailView.vue'), 'utf8')
describe('OceanBookingDetailView contract', () => {
it('uses the backend draft fields and all required parties', () => {
for (const field of ['SHIPPER', 'CONSIGNEE', 'NOTIFY_PARTY', 'partyName', 'departurePortName', 'destinationPortName', 'vesselName', 'voyageNo', 'containerRequirements']) {
expect(source).toContain(field)
}
expect(source).not.toContain('goodsDescription')
expect(source).not.toContain('form.data.soNo')
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('placeholder="请选择订舱代理"')
expect(source).toContain("selectPartner('shippingCompany'")
expect(source).toContain("selectPartner('bookingAgent'")
expect(source).not.toContain('label="客户 ID"')
expect(source).not.toContain('label="船公司 ID"')
expect(source).not.toContain('label="订舱代理 ID"')
})
it('reads the real version and protects create/edit permissions', () => {
expect(source).toContain('form.expectedVersion = value.version')
expect(source).toContain('expectedVersion: 0')
expect(source).toContain("platform:firstleg:ocean-booking:create")
expect(source).toContain("platform:firstleg:ocean-booking:edit")
expect(source).toContain(':disabled="!isNew"')
expect(source).toContain(':disabled="!editable"')
it('hides unavailable customers instead of filling the selector with disabled options', () => {
expect(source).toContain("customer.status === 'ACTIVE' || String(customer.id) === String(form.data.customerId)")
expect(source).toContain('v-for="customer in selectableCustomers"')
expect(source).not.toContain(':disabled="customer.status !== \'ACTIVE\'"')
})
it('maps each status action to its server permission and uses a change-specific request', () => {
for (const permission of ['submit', 'confirm', 'release', 'cancel', 'change']) {
expect(source).toContain(`platform:firstleg:ocean-booking:${permission}`)
}
expect(source).toContain('requestOceanBookingChange')
expect(source).toContain("changeForm.vesselName = detail.value?.vesselName")
expect(source).toContain('expectedVersion: detail.value.version')
it('provides guidance for every free-text booking field', () => {
for (const placeholder of [
'请输入提单号(可选)', '请输入船名', '请输入航次', '例如:CNNGB', '例如:宁波港',
'例如:USLAX', '例如:洛杉矶港', '请输入最终交货地点', '例如:CY-CY',
'例如:PREPAID', '请输入订舱备注(可选)',
]) expect(source).toContain(`placeholder="${placeholder}"`)
})
})
@@ -12,6 +12,8 @@ import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import PageToolbar from '@/shared/components/PageToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data/counterparties'
import {
createOceanBooking,
getOceanBooking,
@@ -67,6 +69,15 @@ const actionSaving = ref(false)
const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
const actionError = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
const formRef = ref<FormInstance>()
const customers = ref<PlatformTenantResource[]>([])
const partners = ref<PlatformPartnerResource[]>([])
const optionsLoading = ref(false)
const optionsError = ref('')
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 bookingAgents = computed(() => partners.value.filter((partner) => partner.types.some((type) => ['SUPPLIER', 'CUSTOMS_BROKER'].includes(type))))
function blankParties(): OceanBookingParty[] {
return partyTypes.map(({ value }) => ({ partyType: value, partyName: '', contactName: '', contactPhone: '', address: '' }))
@@ -79,8 +90,7 @@ function blankData(): OceanBookingDraftData {
}
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
const rules: FormRules = {
customerId: [{ required: true, message: '请输入客户 ID', trigger: 'blur' }],
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
}
const editable = computed(() => isNew.value || detail.value?.status === 'DRAFT')
const canSave = computed(() => isNew.value
@@ -95,6 +105,36 @@ const changeForm = reactive({ vesselName: '', voyageNo: '', estimatedSailingAt:
function statusLabel(status: OceanBookingStatus | string | null | undefined) {
return status ? (statusLabels[status as OceanBookingStatus] || status) : '未设置'
}
async function loadOptions() {
optionsLoading.value = true
optionsError.value = ''
try {
const [customerResult, partnerResult] = await Promise.all([
listPlatformTenants({ page: 1, size: 200 }),
listPlatformPartners({ page: 1, size: 200, status: 'ACTIVE' }),
])
customers.value = customerResult.records
partners.value = partnerResult.records
} catch (cause) {
optionsError.value = getApiErrorDetails(cause, '客户及合作单位选项加载失败').message
} finally {
optionsLoading.value = false
}
}
function selectCustomer(customerId: string) {
form.data.customerId = customerId
form.data.customerName = customers.value.find((customer) => String(customer.id) === customerId)?.tenantName || ''
}
function selectPartner(kind: 'shippingCompany' | 'bookingAgent', partnerId: string) {
const partner = partners.value.find((item) => String(item.id) === partnerId)
if (kind === 'shippingCompany') {
form.data.shippingCompanyId = partnerId || null
form.data.shippingCompanyName = partner?.partnerName || null
} else {
form.data.bookingAgentId = partnerId || null
form.data.bookingAgentName = partner?.partnerName || null
}
}
function applyDetail(value: OceanBookingDetail) {
detail.value = value
form.expectedVersion = value.version
@@ -185,7 +225,7 @@ async function runAction(action: OceanBookingAction) {
function addContainer() { form.data.containerRequirements.push({ containerType: '40HC', containerKind: 'FCL', quantity: 1 }) }
function removeContainer(index: number) { if (form.data.containerRequirements.length > 1) form.data.containerRequirements.splice(index, 1) }
onMounted(load)
onMounted(() => { void load(); void loadOptions() })
</script>
<template>
@@ -211,26 +251,24 @@ onMounted(load)
<el-button v-if="canSave" type="primary" :loading="saving" @click="save"><Save :size="16" />保存</el-button>
</div>
</div>
<el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions">重新加载选项</el-button></el-alert>
<el-form ref="formRef" :model="form.data" :rules="rules" label-position="top" :disabled="!editable" class="form-grid">
<el-form-item label="客户 ID" prop="customerId"><el-input v-model="form.data.customerId" :disabled="!isNew" /></el-form-item>
<el-form-item label="客户名称" prop="customerName"><el-input v-model="form.data.customerName" /></el-form-item>
<el-form-item label="提单号"><el-input v-model="form.data.billOfLadingNo" /></el-form-item>
<el-form-item label="船公司 ID"><el-input v-model="form.data.shippingCompanyId" /></el-form-item>
<el-form-item label="船公司名称"><el-input v-model="form.data.shippingCompanyName" /></el-form-item>
<el-form-item label="订舱代理 ID"><el-input v-model="form.data.bookingAgentId" /></el-form-item>
<el-form-item label="订舱代理名称"><el-input v-model="form.data.bookingAgentName" /></el-form-item>
<el-form-item label="船名"><el-input v-model="form.data.vesselName" /></el-form-item>
<el-form-item label="航次"><el-input v-model="form.data.voyageNo" /></el-form-item>
<el-form-item label="起运港代码"><el-input v-model="form.data.departurePortCode" /></el-form-item>
<el-form-item label="起运港"><el-input v-model="form.data.departurePortName" /></el-form-item>
<el-form-item label="目的港代码"><el-input v-model="form.data.destinationPortCode" /></el-form-item>
<el-form-item label="目的港"><el-input v-model="form.data.destinationPortName" /></el-form-item>
<el-form-item label="交货地点"><el-input v-model="form.data.placeOfDelivery" /></el-form-item>
<el-form-item label="服务条款"><el-input v-model="form.data.serviceTerm" /></el-form-item>
<el-form-item label="运费条款"><el-input v-model="form.data.freightTerm" /></el-form-item>
<el-form-item label="预计开船"><el-date-picker v-model="form.data.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
<el-form-item label="预计到港"><el-date-picker v-model="form.data.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
<el-form-item class="wide" label="备注"><el-input v-model="form.data.remark" type="textarea" /></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-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 v-model="form.data.billOfLadingNo" 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-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-for="partner in bookingAgents" :key="String(partner.id)" :label="partner.partnerName" :value="String(partner.id)" /></el-select></el-form-item>
<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 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 label="交货地点"><el-input v-model="form.data.placeOfDelivery" placeholder="请输入最终交货地点" /></el-form-item>
<el-form-item label="服务条款"><el-input v-model="form.data.serviceTerm" placeholder="例如:CY-CY" /></el-form-item>
<el-form-item label="运费条款"><el-input v-model="form.data.freightTerm" placeholder="例如:PREPAID" /></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 class="wide" label="备注"><el-input v-model="form.data.remark" type="textarea" placeholder="请输入订舱备注(可选)" /></el-form-item>
</el-form>
</section>
@@ -266,11 +304,11 @@ onMounted(load)
<AppDialog v-model="changeDialogOpen" title="申请改船" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="!changeForm.vesselName.trim() || !changeForm.voyageNo.trim() || !changeForm.reason.trim()" confirm-text="提交申请" @confirm="submitChange">
<el-form label-position="top">
<el-form-item label="新船名" required><el-input v-model="changeForm.vesselName" /></el-form-item>
<el-form-item label="新航次" required><el-input v-model="changeForm.voyageNo" /></el-form-item>
<el-form-item label="预计开船"><el-date-picker v-model="changeForm.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
<el-form-item label="预计到港"><el-date-picker v-model="changeForm.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
<el-form-item label="变更原因" required><el-input v-model="changeForm.reason" type="textarea" /></el-form-item>
<el-form-item label="新船名" required><el-input v-model="changeForm.vesselName" placeholder="请输入新船名" /></el-form-item>
<el-form-item label="新航次" required><el-input v-model="changeForm.voyageNo" placeholder="请输入新航次" /></el-form-item>
<el-form-item label="预计开船"><el-date-picker v-model="changeForm.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="changeForm.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item>
<el-form-item label="变更原因" required><el-input v-model="changeForm.reason" type="textarea" placeholder="请说明改船原因" /></el-form-item>
</el-form>
</AppDialog>
</AdminPage>