@@ -2,9 +2,10 @@
import { computed , onMounted , reactive , ref , watch } from 'vue'
import type { FormInstance , FormRules } from 'element-plus'
import { Eye , Save } from '@lucide/vue'
import { TRUCK _APPOINTMENT _CURRENCIES , normalizeTruckAppointmentCurrency , type SaveTruckAppointmentRequest , type TruckAppointmentResource } from '../types'
import { TRUCK _APPOINTMENT _CURRENCIES , normalizeTruckAppointmentCurrency , type SaveTruckAppointmentRequest , type TruckAppointmentCandidate , type TruckAppointmentResource } from '../types'
import { listTruckAppointmentCandidates } from '../api/platformTrucking'
import { listSuppliers , type Supplier } from '@/modules/suppliers'
import { getOceanBooking , listOceanBookings , type OceanBookingAllocatedContainer , type OceanBookingDocument , type OceanBookingResource } from '@/modules/ocean-bookings'
import { getOceanBooking , type OceanBookingDocument } from '@/modules/ocean-bookings'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
@@ -15,6 +16,7 @@ const relationWarning = ref('')
const bookingContainersLoading = ref ( false )
const bookingContainersError = ref < ApiErrorDetails | null > ( null )
const bookingDocuments = ref < OceanBookingDocument [ ] > ( [ ] )
const relationLocked = computed ( ( ) => Boolean ( props . resource ) )
const defaults : Partial < SaveTruckAppointmentRequest > = { expectedVersion : 0 , currency : 'RMB' , payableAmount : '0.00' , packageCount : 0 , palletCount : 0 , transitMinutes : 0 }
function normalizeDraft ( value : SaveTruckAppointmentRequest ) : SaveTruckAppointmentRequest { return { ... defaults , ... value , currency : normalizeTruckAppointmentCurrency ( value . currency ) } }
const draft = reactive < SaveTruckAppointmentRequest > ( normalizeDraft ( props . modelValue ) )
@@ -31,12 +33,12 @@ const phoneRule = (label: string) => ({ validator: (_rule: unknown, value: unkno
} , trigger : 'blur' } )
const rules : FormRules = {
oceanBookingId : [ { required : true , message : '请选择已放舱订舱' , trigger : 'change' } , { validator : ( _rule , value , callback ) => {
if ( ! ( bookings . value . some ( ( item ) => item . id === String ( value ) && item . status === 'RELEASED' ) || selectedBookingFallback . value ? . id === String ( value ) ) ) return callback ( new Error ( '请选择有效 的已放舱订舱' ) )
if ( ! ( bookings . value . some ( ( item ) => item . id === String ( value ) ) || ( relationLocked . value && selectedBookingFallback . value ? . id === String ( value ) ) ) ) return callback ( new Error ( '请选择仍有可预约柜次 的已放舱订舱' ) )
if ( bookingContainersError . value ) return callback ( new Error ( '订舱资料状态加载失败,请重试' ) )
if ( ! draft . billOfLadingNo ? . trim ( ) ) return callback ( new Error ( '所选订舱缺少有效提单号' ) )
return callback ( bookingDocumentReady . value ? undefined : new Error ( '所选订舱缺少有效的订舱确认文件或提单文件' ) )
} , trigger : 'change' } ] ,
containerBookingId : [ { required : true , message : '请选择该订舱下已分配 的柜次' , trigger : 'change' } , { validator : ( _rule , value , callback ) => callback ( selectableContainers . value . some ( ( item ) => item . containerBookingId === String ( value ) ) || ( props . readonly && selectedContainerFallback . value ? . id === String ( value ) ) ? undefined : new Error ( '请选择该订舱下有效的已分配 柜次' ) ) , trigger : 'change' } ] ,
containerBookingId : [ { required : true , message : '请选择该订舱下可预约 的柜次' , trigger : 'change' } , { validator : ( _rule , value , callback ) => callback ( selectableContainers . value . some ( ( item ) => item . containerBookingId === String ( value ) ) || ( relationLocked . value && selectedContainerFallback . value ? . id === String ( value ) ) ? undefined : new Error ( '请选择该订舱下仍可预约的 柜次' ) ) , trigger : 'change' } ] ,
carrierId : [ { required : true , message : '请选择承运商' , trigger : 'change' } , { validator : ( _rule , value , callback ) => callback ( selectableCarriers . value . some ( ( item ) => String ( item . id ) === String ( value ) ) ? undefined : new Error ( '请选择有效的合作中拖车供应商' ) ) , trigger : 'change' } ] ,
plannedLoadingAt : [ { required : true , message : '请选择预计装箱时间' , trigger : 'change' } ] ,
fleetPhone : [ phoneRule ( '车队电话' ) ] , driverPhone : [ phoneRule ( '司机电话' ) ] , storePhone : [ phoneRule ( '门店电话' ) ] ,
@@ -44,33 +46,47 @@ const rules: FormRules = {
grossWeightKg : [ nonNegative ( '毛重' ) ] , volumeCbm : [ nonNegative ( '体积' ) ] , payableAmount : [ nonNegative ( '应付拖车费' ) ] ,
}
const carriers = ref < Supplier [ ] > ( [ ] )
const containers = ref < OceanBookingAllocatedContainer [ ] > ( [ ] )
const bookings = ref < OceanBookingResource [ ] > ( [ ] )
const candidates = ref < TruckAppointmentCandidate [ ] > ( [ ] )
const optionsLoading = ref ( false )
const optionsError = ref ( '' )
let bookingLoadSequence = 0
const selectableCarriers = computed ( ( ) => carriers . value . filter ( ( item ) => (
item . status === 'ACTIVE'
) ) )
const selectedReleasedBooking = computed ( ( ) => bookings . value . find ( ( item ) => item . id === draft . oceanBookingId && item . status === 'RELEASED' ) || null )
const bookingDocumentReady = computed ( ( ) => bookingDocuments . value . some ( ( document ) => document . valid && [ 'BOOKING_CONFIRMATION' , 'BILL_OF_LADING' ] . includes ( document . documentType ) ) )
const selectableContainers = computed ( ( ) => containers . value )
function containerOptionLabel ( item : OceanBookingAllocatedContainer ) {
const bookings = computed ( ( ) => {
const result = new Map < string , { id : string ; bookingNo : string ; billOfLadingNo : string ; warehouseId : string ; warehouseName : string | null ; bookingDocumentReady : boolean } > ( )
for ( const item of candidates . value ) if ( ! result . has ( item . oceanBookingId ) ) result . set ( item . oceanBookingId , {
id : item . oceanBookingId ,
bookingNo : item . oceanBookingNo ,
billOfLadingNo : item . billOfLadingNo ,
warehouseId : item . warehouseId ,
warehouseName : item . warehouseName ,
bookingDocumentReady : item . bookingDocumentReady ,
} )
return [ ... result . values ( ) ]
} )
const selectedReleasedBooking = computed ( ( ) => bookings . value . find ( ( item ) => item . id === draft . oceanBookingId ) || null )
const bookingDocumentReady = computed ( ( ) => relationLocked . value
? bookingDocuments . value . some ( ( document ) => document . valid && [ 'BOOKING_CONFIRMATION' , 'BILL_OF_LADING' ] . includes ( document . documentType ) )
: Boolean ( selectedReleasedBooking . value ? . bookingDocumentReady ) )
const selectableContainers = computed ( ( ) => candidates . value . filter ( ( item ) => item . oceanBookingId === draft . oceanBookingId ) )
function containerOptionLabel ( item : TruckAppointmentCandidate ) {
return ` 柜次号: ${ item . cabinetNo || '待生成' } / 柜型: ${ item . containerType || '—' } / 集装箱号: ${ item . containerNo || '待补录' } `
}
const selectedContainerFallback = computed ( ( ) => {
const id = draft . containerBookingId
const snapshot = props . resource
if ( ! id || selectableContainers . value . some ( ( item ) => String ( item . containerBookingId ) === String ( id ) ) || String ( snapshot ? . containerBookingId ) !== String ( id ) ) return null
if ( ! id || ! relationLocked . value || String ( snapshot ? . containerBookingId ) !== String ( id ) ) return null
return {
id : String ( id ) ,
label : ` 柜次号: ${ snapshot ? . cabinetNo || '待生成' } / 柜型: ${ snapshot ? . relatedContainerType || '—' } / 集装箱号: ${ snapshot ? . containerNo || '待补录' } ` ,
label : ` 柜次号: ${ snapshot ? . cabinetNo || '待生成' } / 柜型: ${ snapshot ? . relatedContainerType || '—' } / 集装箱号: ${ snapshot ? . containerNo || '待补录' } (当前绑定,不可重选) ` ,
}
} )
const selectedBookingFallback = computed ( ( ) => {
const id = draft . oceanBookingId
const snapshot = props . resource
if ( ! id || bookings . value . some ( ( item ) => String ( item . id ) === String ( id ) ) || String ( snapshot ? . oceanBookingId ) !== String ( id ) ) return null
return { id : String ( id ) , label : snapshot ? . oceanBookingNo || '订舱号缺失' }
if ( ! id || ! relationLocked . value || String ( snapshot ? . oceanBookingId ) !== String ( id ) ) return null
return { id : String ( id ) , label : ` ${ snapshot ? . oceanBookingNo || '订舱号缺失' } (当前绑定,不可重选) ` }
} )
function selectCarrier ( carrierId : string ) { const carrier = selectableCarriers . value . find ( ( item ) => String ( item . id ) === carrierId ) ; draft . carrierId = carrier ? . id || null ; draft . carrierName = carrier ? . name || null }
function selectContainer ( containerId : string ) {
@@ -81,48 +97,37 @@ function selectContainer(containerId: string) {
draft . loadingPlanId = null
return
}
draft . loadingPlanId = null
draft . loadingPlanId = item . loadingPlanId
}
async function loadBookingContainers ( bookingId : string ) {
async function loadCurrentBookingContext ( bookingId : string ) {
const sequence = ++ bookingLoadSequence
bookingContainersLoading . value = true
bookingContainersError . value = null
try {
const booking = await getOceanBooking ( bookingId )
if ( sequence !== bookingLoadSequence || String ( draft . oceanBookingId || '' ) !== bookingId ) return
if ( booking . status !== 'RELEASED' ) {
containers . value = [ ]
bookingDocuments . value = [ ]
draft . containerBookingId = null
draft . loadingPlanId = null
bookingContainersError . value = { message : '所选订舱尚未放舱,不能创建拖车预约' }
bookingContainersError . value = { message : '当前绑定订舱已不是放舱状态,请刷新后处理' }
return
}
const bookingIndex = bookings . value . findIndex ( ( item ) => item . id === booking . id )
if ( bookingIndex >= 0 ) bookings . value [ bookingIndex ] = booking
else bookings . value . push ( booking )
draft . warehouseId = booking . warehouseId
draft . billOfLadingNo = booking . billOfLadingNo
containers . value = booking . containers || [ ]
bookingDocuments . value = booking . documents || [ ]
if ( ! selectableContainers . value . some ( ( item ) => item . containerBookingId === draft . containerBookingId ) ) {
draft . containerBookingId = null
draft . loadingPlanId = null
}
} catch ( cause ) {
containers . value = [ ]
if ( sequence !== bookingLoadSequence || String ( draft . oceanBookingId || '' ) !== bookingId ) return
bookingDocuments . value = [ ]
draft . containerBookingId = null
draft . loadingPlanId = null
bookingContainersError . value = getApiErrorDetails ( cause , '该订舱柜次加载失败,请重试' )
bookingContainersError . value = getApiErrorDetails ( cause , '当前绑定订舱资料加载失败,请重试' )
} finally {
bookingContainersLoading . value = false
if ( sequence === bookingLoadSequence ) bookingContainersLoading . value = false
}
}
async function selectBooking ( bookingId : string ) {
bookingLoadSequence += 1
const item = bookings . value . find ( ( entry ) => entry . id === bookingId )
draft . oceanBookingId = item ? . id || null
draft . containerBookingId = null
draft . loadingPlanId = null
containers . value = [ ]
bookingDocuments . value = [ ]
relationWarning . value = ''
if ( ! item ) {
@@ -132,26 +137,39 @@ async function selectBooking(bookingId: string) {
}
draft . warehouseId = item . warehouseId
draft . billOfLadingNo = item . billOfLadingNo
await loadBookingContainers ( item . id )
}
async function loadOptions ( ) {
optionsLoading . value = true ; optionsError . value = ''
if ( relationLocked . value ) candidates . value = [ ]
const results = await Promise . allSettled ( [
listSuppliers ( { page : 1 , size : 200 , category : 'TRUCKING' , status : 'ACTIVE' } ) ,
listOceanBookings ( { page : 1 , size : 200 , status : 'RELEASED' } ) ,
relationLocked . value ? Promise . resolve ( { records : [ ] as TruckAppointmentCandidate [ ] } ) : listTruckAppointmentCandidates ( { page : 1 , size : 200 } ) ,
] )
if ( results [ 0 ] . status === 'fulfilled' ) carriers . value = results [ 0 ] . value . records . filter ( ( item ) => item . category === 'TRUCKING' && item . status === 'ACTIVE' )
if ( results [ 1 ] . status === 'fulfilled' ) bookings . value = results [ 1 ] . value . records . filter ( ( item ) => item . status === 'RELEASED' )
const failed = [ '承运商' , '已放舱订舱 ' ] . filter ( ( _ , index ) => results [ index ] ? . status === 'rejected' )
if ( results [ 1 ] . status === 'fulfilled' ) candidates . value = results [ 1 ] . value . records
const failed = [ '承运商' , '可预约订舱柜次 ' ] . filter ( ( _ , index ) => results [ index ] ? . status === 'rejected' )
if ( failed . length ) optionsError . value = ` ${ failed . join ( '、' ) } 选项加载失败 `
optionsLoading . value = false
if ( draft . oceanBookingId ) await loadBookingContainers ( String ( draft . oceanBookingId ) )
if ( relationLocked . value && draft . oceanBookingId ) await loadCurrentBookingContext ( String ( draft . oceanBookingId ) )
else if ( draft . oceanBookingId ) {
const candidate = candidates . value . find ( ( item ) => item . oceanBookingId === draft . oceanBookingId && item . containerBookingId === draft . containerBookingId )
if ( candidate ) {
draft . warehouseId = candidate . warehouseId
draft . billOfLadingNo = candidate . billOfLadingNo
draft . loadingPlanId = candidate . loadingPlanId
} else {
draft . oceanBookingId = null
draft . containerBookingId = null
draft . loadingPlanId = null
}
}
}
async function validate ( ) { return formRef . value ? . validate ( ) }
async function submitForm ( ) { if ( props . submitting ) return ; if ( await formRef . value ? . validate ( ) ) emit ( 'submit' ) }
defineExpose ( { validate } )
onMounted ( ( ) => { void loadOptions ( ) } )
watch ( ( ) => props . readonly , ( readonly ) => { if ( ! readonly && ! bookings . value . length ) void loadOptions ( ) } )
watch ( ( ) => props . readonly , ( readonly ) => { if ( ! readonly && ! carriers . value . length ) void loadOptions ( ) } )
watch ( ( ) => props . resource ? . id , ( id , previous ) => { if ( id && id !== previous ) void loadOptions ( ) } )
< / script >
< template >
@@ -161,8 +179,8 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !bookings.value.len
< el-alert v-if = "optionsError" type="warning" :closable="false" show-icon :title="optionsError" class="relation-alert"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions" > 重新加载选项 < / el -button > < / el-alert >
< el-row :gutter = "16" >
< el-col :xs = "24" > < el-divider content -position = " left " > 订舱与柜次 ( 必填 ) < / el-divider > < / el-col >
< el-col class = "relation-column" :xs = "24" :sm = "12" > < el-form-item label = "订舱" prop = "oceanBookingId" > < el-select :model-value = "draft.oceanBookingId" filterable placeholder = "请先 选择已放舱订舱" :loading = "optionsLoading" no -data -text = " 暂无可用 的已放舱订舱 " style = "width:100%" @ update : model -value = " selectBooking ( String ( $ event | | ' ' ) ) " > < el-option v-if = "selectedBookingFallback" :key="selectedBookingFallback.id" :label="selectedBookingFallback.label" :value="selectedBookingFallback.id" /><el-option v-for="item in bookings" :key="item.id" :label="`${item.bookingNo || '订舱号缺失'}${item.billOfLadingNo ? ` / 提单号:${item.billOfLadingNo}` : ''}` " :value="item.id" / > < / el-select > < / el-form-item > < / el-col >
< el-col class = "relation-column" :xs = "24" :sm = "12" > < el-form-item label = "柜次" prop = "containerBookingId" > < el-select :model-value = "draft.containerBookingId" filterable : disabled = "!draft.oceanBookingId || bookingContainersLoading" placeholder = "请先选择订舱,再选择已分配 柜次" :loading = " bookingContainersLoading" : no -data -text = " draft.oceanBookingId ? ' 该订舱暂无已分配 柜次 ' : ' 请先选择订舱 ' " style = "width:100%" @ update : model -value = " selectContainer ( String ( $ event | | ' ' ) ) " > < el-option v-if = "selectedContainerFallback" :key="selectedContainerFallback.id" :label="selectedContainerFallback.label" :value="selectedContainerFallback.id" : disabled="!readonly" /><el-option v-for="item in selectableContainers" :key="item.containerBookingId" :label="containerOptionLabel(item)" :value="item.containerBookingId" /></el-select><div v-if="bookingContainersError" class="field-error"><span>{{ bookingContainersError.message }}</span><ApiErrorMeta :details="bookingContainersError" /><el-button link type="primary" :loading="bookingContainersLoading" @click="draft.oceanBookingId && loadBookingContainers (String(draft.oceanBookingId))">重新加载</el-button></div><div v-else-if="draft.oceanBookingId && !bookingContainersLoading && !selectableContainers.length" class="field-hint" > 只有已分配至该订舱的内部柜次可以预约拖车 。 < / div > < / el -form -item > < / el-col >
< el-col class = "relation-column" :xs = "24" :sm = "12" > < el-form-item label = "订舱" prop = "oceanBookingId" > < el-select :model-value = "draft.oceanBookingId" filterable :disabled = "relationLocked" placeholder = "请选择仍有可预约柜次的 已放舱订舱" :loading = "optionsLoading" no -data -text = " 暂无仍有可预约柜次 的已放舱订舱 " style = "width:100%" @ update : model -value = " selectBooking ( String ( $ event | | ' ' ) ) " > < el-option v-if = "selectedBookingFallback" :key="selectedBookingFallback.id" :label="selectedBookingFallback.label" :value="selectedBookingFallback.id" disabled /><el-option v-for="item in bookings" :key="item.id" :label="`${item.bookingNo || '订舱号缺失'} / 提单号:${item.billOfLadingNo}`" :value="item.id" / > < / el-select > < / el-form-item > < / el-col >
< el-col class = "relation-column" :xs = "24" :sm = "12" > < el-form-item label = "柜次" prop = "containerBookingId" > < el-select :model-value = "draft.containerBookingId" filterable : disabled = "relationLocked || !draft.oceanBookingId || optionsLoading || bookingContainersLoading" placeholder = "请先选择订舱,再选择可预约 柜次" : loading = "optionsLoading || bookingContainersLoading" : no -data -text = " draft.oceanBookingId ? ' 该订舱暂无可预约 柜次 ' : ' 请先选择订舱 ' " style = "width:100%" @ update : model -value = " selectContainer ( String ( $ event | | ' ' ) ) " > < el-option v-if = "selectedContainerFallback" :key="selectedContainerFallback.id" :label="selectedContainerFallback.label" :value="selectedContainerFallback.id" disabled /><el-option v-for="item in selectableContainers" :key="item.containerBookingId" :label="containerOptionLabel(item)" :value="item.containerBookingId" /></el-select><div v-if="bookingContainersError" class="field-error"><span>{{ bookingContainersError.message }}</span><ApiErrorMeta :details="bookingContainersError" /><el-button link type="primary" :loading="bookingContainersLoading" @click="draft.oceanBookingId && loadCurrent BookingContext (String(draft.oceanBookingId))">重新加载</el-button></div><div v-else-if="draft.oceanBookingId && !optionsLoading && !bookingContainersLoading && !selectableContainers.length && !relationLocked " class="field-hint" > 该订舱当前没有可创建拖车预约的柜次 。 < / div > < / el -form -item > < / el-col >
< el-col :xs = "24" > < el-divider content -position = " left " > 基本信息 < / el-divider > < / el-col >
< el-col v-if = "draft.oceanBookingId && !bookingContainersLoading && !bookingContainersError && !draft.billOfLadingNo?.trim()" :xs="24"><el-alert title="所选订舱缺少有效提单号,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / > < / el-col >
< el-col v-if = "draft.oceanBookingId && !bookingContainersLoading && !bookingContainersError && !bookingDocumentReady" :xs="24"><el-alert title="所选订舱缺少有效的订舱确认文件或提单文件,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / > < / el-col >