@@ -1,8 +1,9 @@
< script setup lang = "ts" >
import '@/styles/adminDensity.css'
import { computed , onMounted , reactive , ref } from 'vue'
import { ElMessage , ElMessageBox , type FormInstance , type FormRules } from 'element-plus'
import { useRoute , useRouter } from 'vue-router'
import { ArrowLeft , Plus , Printer , Save , Trash2 } from '@lucide/vue'
import { ArrowLeft , Printer , Save , Trash2 } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useWorkspaceTabsStore } from '@/core/workspace-tabs/workspaceTabs'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
@@ -348,7 +349,7 @@ async function removeDraft() {
if ( ! detail . value || ! canDelete . value ) return
actionError . value = null
try {
await ElMessageBox . confirm ( '确认删除该订舱草稿?删除后无法恢复。' , '删除草稿' , { confirmButtonText : '确认删除' , cancelButtonText : '取消' , type : 'warning' } )
await ElMessageBox . confirm ( '确认删除该订舱草稿?删除后无法恢复。' , '删除草稿' , { customClass : 'admin-density-confirm' , confirmButtonText : '确认删除' , cancelButtonText : '取消' , type : 'warning' } )
actionSaving . value = true
await deleteOceanBookingDraft ( id . value , detail . value . version )
ElMessage . success ( '订舱草稿已删除' )
@@ -367,12 +368,12 @@ onMounted(() => { void load(); void loadOptions() })
< / script >
< template >
< AdminPage as = "div" class = "booking-detail" >
< AdminPage as = "div" class = "booking-detail admin-density " >
< OperationErrorToast :details = "actionError" / >
< DetailPageLayout aria -label = " 订舱详情 " >
< template # header >
< div class = "detail-heading" >
< el-button @click ="router.push('/ocean-bookings')" > < ArrowLeft :size = "16" / > 返回列表 < / el-button >
< el-button link class = "density-back" @click ="router.push('/ocean-bookings')" > < ArrowLeft :size = "16" / > 返回列表 < / el-button >
< PageToolbar class = "booking-page-toolbar" as = "section" : title = "detail?.bookingNo || '订舱草稿'" : description = "detail ? `${detail.customerName || '未关联客户'} · ${detail.pol || '—'} → ${detail.pod || '—'}` : '先保存订舱草稿,资料完整后再确认放舱'" >
< template v-if = "detail" #actions><div class="heading-status"><StatusBadge :status="statusLabel(detail.status)" / > < / div > < / template >
< / PageToolbar >
@@ -392,7 +393,7 @@ onMounted(() => { void load(); void loadOptions() })
< / 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 = "!formWritable" class = "booking-groups" >
< el-form ref = "formRef" :model = "form.data" :rules = "rules" label -position = " top " :disabled = "!formWritable" class = "booking-groups density-form " >
< section class = "booking-group" aria -label = " 基本信息 " >
< h3 > 基本信息 < / h3 > < div class = "booking-group__body booking-group__body--fields" >
< el-form-item label = "订舱号" > < el-input : model -value = " detail ? .bookingNo | | ' 保存后生成 ' " disabled / > < / el-form-item >
@@ -425,13 +426,13 @@ onMounted(() => { void load(); void loadOptions() })
< el-form-item label = "运费条款 Freight Term" prop = "freightTerm" > < el-input v-model = "form.data.freightTerm" clearable maxlength="32" placeholder="请输入 Freight Term" / > < / el-form-item >
< / div >
< div class = "container-table-wrap" aria -label = " 柜型需求 " >
< table class = "container-table" >
< table class = "container-table density-table " >
< thead > < tr > < th scope = "col" > 操作 < / th > < th scope = "col" > 柜型 < / th > < th scope = "col" > 箱种 < / th > < th scope = "col" > 数量 < / th > < th scope = "col" > 容量 < / th > < th scope = "col" > 限重 < / th > < th v-if = "detail?.status === 'RELEASED'" scope="col" > 剩余可关联 < / th > < / tr > < / thead >
< tbody >
< tr v-for = "(requirement, index) in form.data.containerRequirements" :key="requirement.requirementId || index" >
< td class = "container-table__action" >
< el-tooltip v-if = "formWritable" :content="form.data.containerRequirements.length === 1 ? '至少保留一项柜型需求' : '删除该柜型需求'" placement="top" >
< span > < el-button circle plain size = "small" type = "danger" : aria -label = " ` 删除柜型需求 $ { index + 1 } ` " : disabled = "form.data.containerRequirements.length === 1" @click ="removeRequirement(index)" > < Trash2 :size = "14" / > < / el-button > < / span >
< span > < el-button data -tone = " danger " class = "density-action" plain size = "small" type = "danger" : aria -label = " ` 删除柜型需求 $ { index + 1 } ` " : disabled = "form.data.containerRequirements.length === 1" @click ="removeRequirement(index)" > 删除 < / el -button > < / span >
< / el-tooltip >
< span v-else aria -hidden = " true " > — < / span >
< / td >
@@ -445,7 +446,7 @@ onMounted(() => { void load(); void loadOptions() })
< / tbody >
< / table >
< / div >
< div v-if = "formWritable" class="container-actions"><el-button class="add-container" size="small" :disabled="form.data.containerRequirements.length >= 20" @click="addRequirement"><Plus :size="15" / > 新增柜型需求 < / el-button > < span > { { form . data . containerRequirements . length } } / 20 项 < / span > < / div >
< div v-if = "formWritable" class="container-actions"><el-button class="add-container density-action " size="small" :disabled="form.data.containerRequirements.length >= 20" @click="addRequirement" > 新增柜型需求 < / el -button > < span > { { form . data . containerRequirements . length } } / 20 项 < / span > < / div >
< el-form-item label = "备注" > < el-input v-model = "form.data.remark" type="textarea" :rows="3" placeholder="请输入订舱备注(可选)" / > < / el-form-item >
< / div >
< / section >
@@ -467,18 +468,18 @@ onMounted(() => { void load(); void loadOptions() })
< OceanBookingDocumentsPanel v-if = "detail" :booking-id="detail.id" :booking-version="detail.version" :booking-status="detail.status" :documents="detail.documents" :can-manage="canManageDocuments" :manage-reason="documentManageReason" @updated="updateDocuments" / >
< div v-if = "detail" class="surface-panel"><CarrierScheduleQueryPanel :ocean-booking-id="detail.id" / > < / div >
< div v-if = "detail" class="surface-panel"><CarrierScheduleQueryPanel dense :ocean-booking-id="detail.id" / > < / div >
< section v-if = "detail" class="surface-panel history-panel" >
< div class = "section-title" > < div > < h2 > 状态历史 < / h2 > < p > 当前进度 : { { statusLabel ( detail . status ) } } < / p > < / div > < / div >
< el-timeline > < el-timeline-item v-for = "event in detail.histories" :key="event.id" :timestamp=" event.occurredAt"><strong >{{ statusLabel(event.fromStatus) }} → {{ statusLabel(event.toStatus) }}</strong><p >{{ event.operatorName || '系统' }}<span v-if="event.reason" > · {{ event.reason }} < / span > < / p > < / el -timeline -item > < / el-timeline >
< table class = "density-table booking-history-table" > < thead > < tr > < th > 时间 < / th > < th > 状态变化 < / th > < th > 操作人 < / th > < th > 备注 < / th > < / tr > < / thead > < tbody > < tr v-for = "event in detail.histories" :key="event.id"><td>{{ event.occurredAt }}</td><td >{{ statusLabel(event.fromStatus) }} → {{ statusLabel(event.toStatus) }}</td><td >{{ event.operatorName || '系统' }}</td><td>{{ event.reason || '—' }}</td></tr><tr v-if="!detail.histories.length"><td colspan="4" > 暂无状态历史 < / td > < / tr > < / tbody > < / table >
< / section >
< / template >
< / DetailPageLayout >
< OceanBookingPrintDialog v-if = "detail" v-model="printOpen" :booking-id="detail.id" :booking-no="detail.bookingNo" :allowed="canPrint" / >
< AppDialog v-model = "releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="workflowBusy" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease" >
< el -form label -position = " top " >
< AppDialog class = "admin-density-dialog" v-model = "releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="workflowBusy" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease" >
< el -form class = "density-form" label -position = " top " >
< el-alert title = "放舱后资料将同步到关联货柜,不能修改、不能删除;填写错误只能由管理员处理数据库,请确认资料无误。" type = "warning" :closable = "false" show -icon / >
< el-descriptions class = "release-summary" :column = "2" border >
< el-descriptions-item label = "提单号" > { { form . data . billOfLadingNo || '—' } } < / el-descriptions-item >
@@ -493,6 +494,6 @@ onMounted(() => { void load(); void loadOptions() })
< / template >
< style scoped >
. channel - code { display : block ; margin - left : 8 px ; color : var ( -- cc - muted ) ; font - size : 11 px } . field - error , . field - hint { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; margin - top : 5 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 20 px } . field - error { color : var ( -- el - color - danger ) }
. booking - detail { min - width : 0 ; max - width : 1880 px } . detail - heading { display : flex ; min - width : 0 ; align - items : flex - start ; gap : 16 px } . booking - page - toolbar { min - width : 0 ; flex : 1 } . section - title p , . history - panel p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 13 px } . heading - status { display : flex ; align - items : center ; gap : 8 px } . surface - panel { padding : 16 px } . surface - panel h2 { margin : 0 ; font - size : 16 px } . section - title { display : flex ; justify - content : space - between ; gap : 12 px ; align - items : flex - start ; margin - bottom : 16 px } . actions { display : flex ; flex - wrap : wrap ; gap : 8 px } . booking - groups { display : grid ; grid - template - columns : minmax ( 0 , 1.35 fr ) minmax ( 0 , 1 fr ) ; gap : 14 px } . booking - group { min - width : 0 ; overflow : hidden ; border : 1 px solid # e8edf3 ; border - radius : 10 px ; background : # fff } . booking - group : last - child { grid - column : 1 / - 1 } . booking - group > h3 { margin : 0 ; padding : 12 px 14 px ; border - bottom : 1 px solid # eef2f6 ; color : # 1 f2937 ; font - size : 14 px } . booking - group _ _body { padding : 12 px 14 px } . booking - group _ _body -- fields , . booking - terms { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : 10 px ; row - gap : 8 px } . booking - field -- wide { grid - column : 1 / - 1 } . booking - terms : deep ( . el - select ) { width : 100 % } . party - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 10 px ; margin - top : 14 px } . subsection { display : grid ; gap : 10 px ; padding : 12 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px } . subsection h3 { margin : 0 ; font - size : 14 px } . required - mark { margin - right : 4 px ; color : var ( -- el - color - danger ) } . container - table - wrap { max - width : 100 % ; overflow - x : auto ; margin - bottom : 10 px } . container - table { width : 100 % ; min - width : 780 px ; table - layout : fixed ; border - collapse : collapse ; font - size : 12 px } . container - table th , . container - table td { padding : 7 px 6 px ; border - bottom : 1 px solid # eef2f6 ; text - align : left ; vertical - align : middle } . container - table th { background : # fafafa ; color : var ( -- cc - muted ) ; font - weight : 650 ; white - space : nowrap } . container - table th : first - child , . container - table td : first - child { width : 56 px } . container - table th : nth - child ( 2 ) , . container - table td : nth - child ( 2 ) { width : 180 px } . container - table th : nth - child ( 3 ) , . container - table td : nth - child ( 3 ) { width : 90 px } . container - table th : nth - child ( 4 ) , . container - table td : nth - child ( 4 ) { width : 110 px } . container - table th : nth - child ( 5 ) , . container - table td : nth - child ( 5 ) , . container - table th : nth - child ( 6 ) , . container - table td : nth - child ( 6 ) { width : 100 px } . container - table td : deep ( . el - select ) , . container - table td : deep ( . el - input ) { width : 100 % ; min - width : 0 } . container - table _ _action { text - align : center ! important } . container - table _ _action : deep ( . el - button ) { width : 28 px ; height : 28 px } . container - table _ _specification { color : var ( -- cc - muted ) ; white - space : nowrap } . container - actions { display : flex ; align - items : center ; gap : 10 px ; margin - bottom : 14 px ; color : var ( -- cc - muted ) ; font - size : 12 px } . add - container { margin : 0 } . history - panel strong { font - size : 14 px } @ media ( max - width : 720 px ) { . booking - groups , . party - grid { grid - template - columns : 1 fr } . booking - group : last - child { grid - column : auto } . detail - heading { flex - direction : column } . booking - page - toolbar { width : 100 % } . section - title { flex - direction : column } . actions { width : 100 % } } @ media ( max - width : 520 px ) { . booking - group _ _body -- fields , . booking - terms { grid - template - columns : 1 fr } . booking - field -- wide { grid - column : auto } } @ media ( max - width : 420 px ) { . surface - panel { padding : 12 px } . booking - group _ _body { padding : 12 px } . actions : deep ( . el - button ) { min - height : 44 px } }
. channel - code { display : block ; margin - left : var ( -- admin - space - 3 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) } . field - error , . field - hint { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 3 ) ; margin - top : 5 px ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 20 px } . field - error { color : var ( -- el - color - danger ) }
. booking - detail { min - width : 0 } . detail - heading { display : flex ; min - width : 0 ; gap : var ( -- admin - space - 5 ) ; padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ) ; background : var ( -- admin - surface ) ; border : 1 px solid var ( -- cc - border ) ; align - items : center } . booking - page - toolbar { min - width : 0 ; flex : 1 } . section - title p , . history - panel p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - body ) } . heading - status { display : flex ; align - items : center ; gap : var ( -- admin - space - 3 ) } . surface - panel { padding : var ( -- admin - space - 5 ) } . surface - panel h2 { margin : 0 ; font - size : var ( -- admin - font - section ) } . section - title { display : flex ; justify - content : space - between ; gap : var ( -- admin - space - 5 ) ; align - items : flex - start ; margin - bottom : var ( -- admin - space - 5 ) } . actions { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 3 ) } . booking - groups { display : grid ; grid - template - columns : minmax ( 0 , 1.35 fr ) minmax ( 0 , 1 fr ) ; gap : var ( -- admin - space - 5 ) } . booking - group { min - width : 0 ; overflow : hidden ; background : var ( -- admin - surface ) ; border : 0 } . booking - group : last - child { grid - column : 1 / - 1 } . booking - group > h3 { margin : 0 ; color : # 1 f2937 ; font - size : var ( -- admin - font - section ) ; padding : var ( -- admin - space - 3 ) 0 ; border - bottom : 1 px solid var ( -- cc - border ) } . booking - group _ _body { padding : var ( -- admin - space - 3 ) 0 0 } . booking - group _ _body -- fields , . booking - terms { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : var ( -- admin - space - 4 ) ; row - gap : var ( -- admin - space - 3 ) } . booking - field -- wide { grid - column : 1 / - 1 } . booking - terms : deep ( . el - select ) { width : 100 % } . party - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : var ( -- admin - space - 4 ) ; margin - top : var ( -- admin - space - 5 ) } . subsection { display : grid ; gap : var ( -- admin - space - 4 ) ; border : 0 ; padding : 0 } . subsection h3 { margin : 0 ; font - size : var ( -- admin - font - section ) } . required - mark { margin - right : var ( -- admin - space - 1 ) ; color : var ( -- el - color - danger ) } . container - table - wrap { max - width : 100 % ; overflow - x : auto ; margin - bottom : var ( -- admin - space - 4 ) } . container - table { width : 100 % ; min - width : 780 px ; table - layout : fixed } . container - table th : first - child , . container - table td : first - child { width : 80 px } . container - table th : nth - child ( 2 ) , . container - table td : nth - child ( 2 ) { width : 180 px } . container - table th : nth - child ( 3 ) , . container - table td : nth - child ( 3 ) { width : 90 px } . container - table th : nth - child ( 4 ) , . container - table td : nth - child ( 4 ) { width : 110 px } . container - table th : nth - child ( 5 ) , . container - table td : nth - child ( 5 ) , . container - table th : nth - child ( 6 ) , . container - table td : nth - child ( 6 ) { width : 100 px } . container - table td : deep ( . el - select ) , . container - table td : deep ( . el - input ) { width : 100 % ; min - width : 0 } . container - table _ _action { text - align : center ! important } . container - table _ _specification { color : var ( -- cc - muted ) ; white - space : nowrap } . container - actions { display : flex ; align - items : center ; gap : var ( -- admin - space - 4 ) ; margin - bottom : var ( -- admin - space - 5 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) } . add - container { margin : 0 } @ media ( max - width : 720 px ) { . booking - groups , . party - grid { grid - template - columns : 1 fr } . booking - group : last - child { grid - column : auto } . detail - heading { flex - direction : column ; padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ) ; background : var ( -- admin - surface ) ; border : 1 px solid var ( -- cc - border ) ; align - items : center } . booking - page - toolbar { width : 100 % } . section - title { flex - direction : column } . actions { width : 100 % } } @ media ( max - width : 520 px ) { . booking - group _ _body -- fields , . booking - terms { grid - template - columns : 1 fr } . booking - field -- wide { grid - column : auto } } @ media ( max - width : 420 px ) { . surface - panel { padding : 12 px } . booking - group _ _body { padding : var ( -- admin - space - 3 ) 0 0 } . actions : deep ( . el - button ) { min - height : 44 px } } . booking - history - table th : nth - child ( 1 ) { width : 180 px } . booking - history - table th : nth - child ( 2 ) { width : 220 px } . booking - history - table th : nth - child ( 3 ) { width : 140 px } . booking - page - toolbar : deep ( h1 ) { font - size : var ( -- admin - font - title ) }
< / style >