优化优化
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user