This commit is contained in:
2026-08-08 13:32:27 +08:00
parent 24af934198
commit f17a6a60a4
27 changed files with 867 additions and 5 deletions
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
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'
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 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)} }
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.cutoffAt" /></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.packageCount" /></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.statusHistory" :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:16px}.detail-heading{display:flex;align-items:center;gap:16px}.detail-heading h1{margin:0;font-size:20px}.detail-heading p{margin:5px 0 0;color:var(--cc-muted);font-size:13px}.heading-status{margin-left:auto}.surface-panel{padding:20px}.section-title{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:18px}.section-title h2,.surface-panel>h2{margin:0;font-size:16px}.actions{display:flex;flex-wrap:wrap;gap:8px}.form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 16px}.form-grid .wide{grid-column:1/-1}@media(max-width:900px){.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:600px){.detail-heading{align-items:flex-start;flex-wrap:wrap}.heading-status{margin-left:0}.form-grid{grid-template-columns:1fr}.form-grid .wide{grid-column:auto}.section-title{align-items:flex-start;flex-direction:column}.actions{width:100%}.actions :deep(.el-button){min-height:40px}}</style>