@@ -4,17 +4,18 @@ import { useRoute, useRouter } from 'vue-router'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails } from '@/shared/api/http'
import { performOceanBookingAction , createOceanBooking , getOceanBooking , updateOceanBooking , type OceanBookingAction } from '../api/platformOceanBookings'
import type { OceanBookingDetail , OceanBookingSaveRequest , OceanBookingStatus } from '../types'
import { createOceanBooking , getOceanBooking , performOceanBookingAction , updateOceanBooking , type OceanBookingAction } from '../api/platformOceanBookings'
import type { OceanBookingDetail , OceanBookingDraftData , OceanBookingSaveRequest , OceanBookingStatus } from '../types'
const route = useRoute ( ) ; const router = useRouter ( ) ; const id = computed ( ( ) => String ( route . params . bookingId || '' ) ) ; const detail = ref < OceanBookingDetail | null > ( null ) ; const loading = ref ( false ) ; const saving = ref ( false ) ; const error = ref < ReturnType < typeof getApiErrorDetails > | null > ( null ) ; const actionError = ref < ReturnType < typeof getApiErrorDetails > | null > ( null )
const form = reactive < OceanBookingSaveRequest > ( { containers : [ { containerType : '40HC' , quantity : 1 } ] , currency : 'USD' } )
const blankData = ( ) : OceanBookingDraftData = > ( { customerId : '' , customerName : '' , parties : [ ] , containerRequirements : [ { containerType : '40HC' , quantity : 1 } ] } )
const form = reactive < OceanBookingSaveRequest > ( { expectedVersion : null , data : blankData ( ) } )
const actionStatuses : OceanBookingStatus [ ] = [ 'SUBMITTED' , 'CONFIRMED' , 'RELEASED' , 'FAILED' , 'CHANGE_PENDING' , 'CANCELLED' ]
const currentStatus = computed ( ( ) => detail . value ? . status )
async function load ( ) { if ( ! id . value || id . value === 'new' ) return ; loading . value = true ; try { detail . value = await getOceanBooking ( id . value ) ; Object . assign ( form , detail . value ) } catch ( e ) { error . value = getApiErrorDetails ( e ) } finally { loading . value = false } }
async function save ( ) { saving . value = true ; actionError . value = null ; try { if ( id . value === 'new' ) { const created = await createOceanBooking ( form ) ; await router . replace ( ` /ocean-bookings/ ${ created } ` ) } else await updateOceanBooking ( id . value , form ) ; await load ( ) } catch ( e ) { actionError . value = getApiErrorDetails ( e ) } finally { saving . value = false } }
const actionByStatus : Partial < Record < OceanBookingStatus , OceanBookingAction > > = { SUBMITTED : 'submit' , CONFIRMED : 'confirm' , RELEASED : 'release' , FAILED : 'fail' , CHANGE _PENDING : 'change-request' , CANCELLED : 'cancel' }
async function action ( status : OceanBookingStatus ) { const actionName = actionByStatus [ status ] ; if ( ! actionName ) return ; actionError . value = null ; try { await performOceanBookingAction ( id . value , actionName ) ; await load ( ) } catch ( e ) { actionError . value = getApiErrorDetails ( e ) } }
async function load ( ) { if ( ! id . value || id . value === 'new' ) return ; loading . value = true ; error . value = null ; try { detail . value = await getOceanBooking ( id . value ) ; const source = detail . value . oceanBooking || detail . value ; form . expectedVersion = Number ( ( source as OceanBookingResourceWithVersion ) . expectedVersion ? ? 0 ) ; form . data = { ... blankData ( ) , ... ( source as unknown as Partial < OceanBookingDraftData > ) , customerId : String ( source . customerId || '' ) , customerName : source . customerName || '' , parties : detail . value . parties || [ ] , containerRequirements : detail . value . containerRequirements || [ ] } } catch ( e ) { error . value = getApiErrorDetails ( e ) } finally { loading . value = false } }
type OceanBookingResourceWithVersion = { expectedVersion ? : number | string | null }
async function save ( ) { saving . value = true ; actionError . value = null ; try { if ( id . value === 'new' ) { const created = await createOceanBooking ( form ) ; await router . replace ( ` /ocean-bookings/ ${ created } ` ) } else await updateOceanBooking ( id . value , form ) ; await load ( ) } catch ( e ) { actionError . value = getApiErrorDetails ( e ) } finally { saving . value = false } }
async function action ( status : OceanBookingStatus ) { const actionName = actionByStatus [ status ] ; if ( ! actionName ) return ; actionError . value = null ; try { await performOceanBookingAction ( id . value , actionName , { expectedVersion : form . expectedVersion , reason : null } ) ; await load ( ) } catch ( e ) { actionError . value = getApiErrorDetails ( e ) } }
onMounted ( load )
< / script >
< template > < div class = "business-page booking-detail" > < div class = "detail-heading" > < el-button @click ="router.push('/ocean-bookings')" > 返回列表 < / el -button > < div > < h1 > { { detail ? . bookingNo || '新增订舱' } } < / h1 > < p v-if = "detail">{{ detail.customerName || '—' }} · {{ detail.pol || '—' }} → {{ detail.pod || '—' }}</p></div><div v-if="detail" class="heading-status"><StatusBadge :status="currentStatus || 'DRAFT' " /></div></div><LoadErrorState v-if="error" title="订舱详情加载失败" :message="error.message" @retry="load" /><template v-else><el-alert v-if="actionError" type="error" :closable="false" show-icon>{{ actionError.message }}<span v-if="actionError.requestId">( requestId: {{ actionError.requestId }}) </span>< /el-alert><section class="surface-panel form-panel "><div class="section-title"><h2>订舱信息</h2><div class="actions"><template v-for="nextStatus in actionStatuses" :key="nextStatus"><el-button v-if="detail && detail.status !== nextStatus" size="small" @click="action(nextStatus)">{{ nextStatus }}</el-button></template><el-button type="primary" :loading="saving" @click="save">保存</el-button></div></div><el-form :model="form" label-position="top" class="form-grid"><el-form-item label="客户"><el-input v-model="form.customerId " /></el-form-item><el-form-item label="关联订单号 "><el-input v-model="form.orderNo " /></el-form-item><el-form-item label="船公司 "><el-input v-model="form.carrierName " /></el-form-item><el-form-item label="订舱代理 "><el-input v-model="form.bookingAgent " /></el-form-item><el-form-item label="船名 "><el-input v-model="form.vesselName " /></el-form-item><el-form-item label="航次 "><el-input v-model="form.voyageNo " /></el-form-item><el-form-item label="起运港 POL "><el-input v-model="form.pol " /></el-form-item><el-form-item label="目的港 POD "><el-input v-model="form.pod" /></el-form-item><el-form-item label="MB/L 货人"><el-input v-model="form.mblShipper " /></el-form-item><el-form-item label="MB/L 收人 "><el-input v-model="form.mblConsignee" /></el-form-item><el-form-item label="S/O号"><el-input v-model="form.soNo" /></el-form-item><el-form-item label="截关时间"><el-input v-model="form.cutoffA t" /></el-form-item><el-form-item label="预计开船"><el-input v-model="form.etd " /></el-form-item><el-form-item label="预计到港"><el-input v-model="form.eta" /></el-form-item><el-form-item label="件数"><el-input v-model.number="form.packageCoun t" /></el-form-item><el-form-item label="金额(Decimal 字符串) "><el-input v-model="form.amount" /></el-form-item><el-form-item label="币种"><el-input v-model="form.currency " /></el-form-item><el-form-item class="wide" label="货物描述"><el-input v-model="form.goodsDescription" type="textarea" /></el-form-item><el-form-item class="wide" label="备注"><el-input v-model="form.remark" type="textarea" /></el-form-item></el-form></section><section v-if="detail" class="surface-panel"><h2>状态历史</h2><el-timeline><el-timeline-item v-for="event in detail.statusH istory " :key="event.id" :timestamp="event.occurredAt" > {{ event.fromStatus | | ' — ' }} → {{ event.toStatus }} · {{ event.operatorName | | ' — ' }} < / el -timeline -item > < / el-timeline > < / section > < / template > < / div > < / template >
< style scoped > . booking - detail { display : grid ; gap : 16 px } . detail - heading { display : flex ; align - items : center ; gap : 16 px } . detail - heading h1 { margin : 0 ; font - size : 20 px } . detail - heading p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 13 px } . heading - status { margin - left : auto } . surface - panel { padding : 20 px } . section - title { display : flex ; justify - content : space - between ; gap : 12 px ; align - items : center ; margin - bottom : 18 px } . section - title h2 , . surface - panel > h2 { margin : 0 ; font - size : 16 px } . actions { display : flex ; flex - wrap : wrap ; gap : 8 px } . form - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 0 16 px } . form - grid . wide { grid - column : 1 / - 1 } @ media ( max - width : 900 px ) { . form - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) } } @ media ( max - width : 600 px ) { . detail - heading { align - items : flex - start ; flex - wrap : wrap } . heading - status { margin - left : 0 } . form - grid { grid - template - columns : 1 fr } . form - grid . wide { grid - column : auto } . section - title { align - items : flex - start ; flex - direction : column } . actions { width : 100 % } . actions : deep ( . el - button ) { min - height : 40 px } } < / style >
< template > < div class = "business-page booking-detail" > < div class = "detail-heading" > < el-button @click ="router.push('/ocean-bookings')" > 返回列表 < / el -button > < div > < h1 > { { detail ? . bookingNo || '新增订舱' } } < / h1 > < p v-if = "detail">{{ detail.customerName || '—' }} · {{ detail.pol || '—' }} → {{ detail.pod || '—' }}</p></div><div v-if="detail" class="heading-status"><StatusBadge :status="detail.status " /></div></div><LoadErrorState v-if="error" title="订舱详情加载失败" :message="error.message" @retry="load" /><template v-else><el-alert v-if="actionError" type="error" :closable="false" show-icon>{{ actionError.message }}</el-alert><section class="surface-panel"><div class="section-title"><h2>订舱信息</h2><div class="actions"><template v-for="nextStatus in actionStatuses" :key="nextStatus"><el-button v-if="detail && detail.status !== nextStatus" size="small" @click="action(nextStatus)">{{ nextStatus }}</el-button></template><el-button type="primary" :loading="saving" @click="save">保存</el-button></div></div><el-form :model="form.data " label-position="top" class="form-grid"><el-form-item label="客户名称 "><el-input v-model="form.data. customerName " /></el-form-item><el-form-item label="客户 ID "><el-input v-model="form.data.customerId " /></el-form-item><el-form-item label="关联订单号 "><el-input v-model="form.data.orderNo " /></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.shippingCompanyId " /></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.departurePortName " /></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.vesselName" /><el-input v-model="form.data.voyageNo" class="m t" /></el-form-item><el-form-item label="预计开船"><el-input v-model="form.data.estimatedSailingAt " /></el-form-item><el-form-item label="预计到港"><el-input v-model="form.data.estimatedArrivalA t" /></el-form-item><el-form-item label="S/O号 "><el-input v-model="form.data.soNo " /></el-form-item><el-form-item class="wide" label="货物描述"><el-input v-model="form.data. goodsDescription" type="textarea" /></el-form-item><el-form-item class="wide" label="备注"><el-input v-model="form.data. remark" type="textarea" /></el-form-item></el-form></section><section v-if="detail" class="surface-panel"><h2>状态历史</h2><el-timeline><el-timeline-item v-for="event in detail.h istories " :key="event.id" :timestamp="event.occurredAt" > {{ event.fromStatus | | ' — ' }} → {{ event.toStatus }} · {{ event.operatorName | | ' — ' }} < / el -timeline -item > < / el-timeline > < / section > < / template > < / div > < / template >
< style scoped > . booking - detail { display : grid ; gap : 16 px } . detail - heading { display : flex ; align - items : center ; gap : 16 px } . detail - heading h1 { margin : 0 ; font - size : 20 px } . detail - heading p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 13 px } . heading - status { margin - left : auto } . surface - panel { padding : 20 px } . section - title { display : flex ; justify - content : space - between ; gap : 12 px ; align - items : center ; margin - bottom : 18 px } . section - title h2 , . surface - panel > h2 { margin : 0 ; font - size : 16 px } . actions { display : flex ; flex - wrap : wrap ; gap : 8 px } . form - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 0 16 px } . form - grid . wide { grid - column : 1 / - 1 } . mt { margin - top : 8 px } @ media ( max - width : 900 px ) { . form - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) } } @ media ( max - width : 600 px ) { . detail - heading { align - items : flex - start ; flex - wrap : wrap } . heading - status { margin - left : 0 } . form - grid { grid - template - columns : 1 fr } . form - grid . wide { grid - column : auto } . section - title { align - items : flex - start ; flex - direction : column } . actions { width : 100 % } . actions : deep ( . el - button ) { min - height : 40 px } } < / style >