@@ -1,7 +1,7 @@
< script setup lang = "ts" >
import { computed , onMounted , ref } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import { ArrowLeft , FileUp , Link2 } from '@lucide/vue'
import { ArrowLeft , ClipboardList , FileUp , Link2 , Package , Receipt , Wallet } from '@lucide/vue'
import { ElMessage , ElMessageBox , type UploadFile } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
@@ -46,6 +46,8 @@ const containerId = computed(() => String(route.params.containerId || ''))
const isDomestic = computed ( ( ) => props . workbench === 'DOMESTIC' )
const isOverseas = computed ( ( ) => props . workbench === 'OVERSEAS' )
const activeTab = ref ( route . query . tab === 'loading-list' ? 'loading-list' : 'overview' )
const canViewFirstLegFees = computed ( ( ) => access . can ( 'firstleg:inbound:fee-detail:view' ) )
const canViewLastLegFees = computed ( ( ) => access . can ( 'platform:finance:customer-arap:list' ) )
const firstLegFeesOpened = ref ( false )
const lastLegFeesOpened = ref ( false )
const feeCustomers = computed ( ( ) => {
@@ -65,6 +67,10 @@ const firstLegFeesEmptyReason = computed(() => {
return '当前柜次暂无可用费用归属明细。'
} )
async function rememberWorkbenchTab ( tabName : string | number ) {
if ( ( tabName === 'first-leg-fees' && ! canViewFirstLegFees . value ) || ( tabName === 'last-leg-fees' && ! canViewLastLegFees . value ) ) {
activeTab . value = 'overview'
return
}
if ( tabName === 'first-leg-fees' ) firstLegFeesOpened . value = true
if ( tabName === 'last-leg-fees' ) lastLegFeesOpened . value = true
const needsLoadingDetail = tabName === 'first-leg-fees' || ( isDomestic . value && tabName === 'loading-list' )
@@ -86,6 +92,11 @@ const canViewOutboundDispatch = computed(() => access.can('platform:firstleg:out
const canConfirmOutboundDispatch = computed ( ( ) => access . can ( 'platform:firstleg:outbound-dispatch:confirm' ) )
const canViewPreload = computed ( ( ) => access . can ( 'platform:firstleg:preload:list' ) )
const canViewLoadingList = computed ( ( ) => access . can ( 'platform:firstleg:loading-list:list' ) )
const canViewCurrentLoadingList = computed ( ( ) => {
if ( ! isDomestic . value || ! detail . value ) return false
if ( detail . value . planStatus === 'COMPLETED' ) return canViewLoadingList . value
return [ 'WAIT_LOADING' , 'EDITING' , 'LOADING' ] . includes ( detail . value . planStatus ) && canViewPreload . value
} )
const canLinkOceanBooking = computed ( ( ) => Boolean ( canEdit . value
&& detail . value ? . sourceType === 'SIMULATION'
&& ! detail . value . oceanBookingId
@@ -261,7 +272,7 @@ async function load() {
try {
const [ resource , aggregate ] = await Promise . all ( [ getContainer ( containerId . value , props . workbench ) , getContainerOverview ( containerId . value ) ] )
workbenchDetail . value = resource ; detail . value = resource ; overview . value = aggregate
if ( activeTab . value === 'loading-list' && resource . planStatus !== 'COMPLETED' ) activeTab . value = 'overview'
if ( activeTab . value === 'loading-list' && ! canViewCurrentLoadingList . value ) activeTab . value = 'overview'
if ( isDomestic . value && activeTab . value === 'loading-list' ) await loadLoadingDetail ( resource , aggregate )
if ( isDomestic . value && resource . planStatus === 'COMPLETED' && canViewOutboundDispatch . value ) await loadOutboundDispatch ( )
} catch ( cause ) { error . value = getApiErrorDetails ( cause , '货柜详情加载失败' ) }
@@ -477,7 +488,12 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
< / section >
< LoadErrorState v-if = "error" title="货柜详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" / > < / template > < / LoadErrorState >
< template v-else-if = "detail" >
< el -tabs v-model = "activeTab" class="workbench-tabs" @tab-change="rememberWorkbenchTab"><el-tab-pane :label="isDomestic ? '送柜报关' : '清关提柜'" name="overview" /><el-tab-pane v-if="isDomestic && detail.planStatus === 'COMPLETED' && canViewLoadingList" label="装柜清单" name="loading-list" /><el-tab-pane label="国内段费用(头程)" name="first-leg-fees" /><el-tab-pane label="目的港及海外段费用(尾程)" name="last-leg-fees" / > < / el-tabs >
< el -tabs v-model = "activeTab" class="workbench-tabs" @tab-change="rememberWorkbenchTab" >
< el -tab -pane : label = "isDomestic ? '送柜报关' : '清关提柜'" name = "overview" > < template # label > < span class = "workbench-tab-label" > < ClipboardList :size = "18" aria -hidden = " true " / > < span > { { isDomestic ? '送柜报关' : '清关提柜' } } < / span > < / span > < / template > < / el-tab-pane >
< el-tab-pane v-if = "canViewCurrentLoadingList" label="装柜清单" name="loading-list"><template #label><span class="workbench-tab-label"><Package :size="18" aria-hidden="true" / > < span > 装柜清单 < / span > < / span > < / template > < / el-tab-pane >
< el-tab-pane v-if = "canViewFirstLegFees" label="国内段费用(头程)" name="first-leg-fees"><template #label><span class="workbench-tab-label"><Receipt :size="18" aria-hidden="true" / > < span > 国内段费用 ( 头程 ) < / span > < / span > < / template > < / el-tab-pane >
< el-tab-pane v-if = "canViewLastLegFees" label="目的港及海外段费用(尾程)" name="last-leg-fees"><template #label><span class="workbench-tab-label"><Wallet :size="18" aria-hidden="true" / > < span > 目的港及海外段费用 ( 尾程 ) < / span > < / span > < / template > < / el-tab-pane >
< / el-tabs >
< LoadErrorState v-if = "overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" / >
< section v-show = "activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-information-heading" >
< h2 id = "container-information-heading" > 货柜信息 < / h2 >
@@ -531,14 +547,14 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
< LoadingListDetailDrawer mode = "confirmed" :loading = "loadingDetailLoading" :detail = "loadingDetail" :error = "loadingDetailError" :can-edit = "false" @back ="activeTab = 'overview'" @retry ="overview && loadLoadingDetail(detail, overview)" / >
< / section >
< section v-if = "firstLegFeesOpened" v-show="activeTab === 'first-leg-fees'" v-loading="loadingDetailLoading" class="detail-section surface-panel" aria-label="头程计费" >
< section v-if = "canViewFirstLegFees && firstLegFeesOpened" v-show="activeTab === 'first-leg-fees'" v-loading="loadingDetailLoading" class="detail-section surface-panel" aria-label="头程计费" >
< LoadErrorState v-if = "loadingDetailError" compact title="头程计费归属明细加载失败" :message="loadingDetailError.message" :loading="loadingDetailLoading" @retry="overview && loadLoadingDetail(detail, overview)"><template #details><ApiErrorMeta :details="loadingDetailError" / > < / template > < / LoadErrorState >
< ContainerFeesPanel v-else-if = "overview?.loadingPlanId && loadingDetail" title="国内段费用(头程)" :plan-id="overview.loadingPlanId" :container-booking-id="detail.id" :customers="feeCustomers" :business-references="feeBusinessReferences" :readonly="isOverseas" / >
< ContainerFeesPanel v-else-if = "overview?.loadingPlanId && loadingDetail" title="国内段费用(头程)" :plan-id="overview.loadingPlanId" :container-booking-id="detail.id" :customers="feeCustomers" :business-references="feeBusinessReferences" / >
< el-empty v-else-if = "!loadingDetailLoading" :description="firstLegFeesEmptyReason" :image-size="72" / >
< / section >
< section v-if = "lastLegFeesOpened" v-show="activeTab === 'last-leg-fees'" class="detail-section surface-panel" aria-label="尾程计费" >
< ContainerLastLegFeesPanel title = "目的港及海外段费用(尾程)" :container-booking-id = "detail.id" :container-no = "detail.containerNo" :readonly = "isDomestic" / >
< section v-if = "canViewLastLegFees && lastLegFeesOpened" v-show="activeTab === 'last-leg-fees'" class="detail-section surface-panel" aria-label="尾程计费" >
< ContainerLastLegFeesPanel title = "目的港及海外段费用(尾程)" :container-booking-id = "detail.id" :container-no = "detail.containerNo" / >
< / section >
< / template >
@@ -550,6 +566,14 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
< / template >
< style scoped >
. workbench - tabs { min - width : 0 ; overflow : hidden ; border - bottom : 1 px solid rgba ( 228 , 228 , 231 , .5 ) ; border - radius : 20 px 20 px 0 0 ; background : rgba ( 255 , 255 , 255 , .95 ) ; box - shadow : 0 - 4 px 16 px rgba ( 0 , 0 , 0 , .04 ) ; }
. workbench - tabs : deep ( . el - tabs _ _header ) { margin : 0 ; }
. workbench - tabs : deep ( . el - tabs _ _nav - wrap : : after ) , . workbench - tabs : deep ( . el - tabs _ _active - bar ) , . workbench - tabs : deep ( . el - tabs _ _content ) { display : none ; }
. workbench - tabs : deep ( . el - tabs _ _item ) { height : auto ; padding : 16 px 28 px ! important ; border - bottom : 3 px solid transparent ; color : # 71717 a ; font - size : 15 px ; font - weight : 600 ; transition : background - color .2 s , color .2 s , border - color .2 s ; }
. workbench - tabs : deep ( . el - tabs _ _item : hover ) { background : rgba ( 102 , 126 , 234 , .05 ) ; color : # 667 eea ; }
. workbench - tabs : deep ( . el - tabs _ _item . is - active ) { border - bottom - color : # 667 eea ; background : rgba ( 102 , 126 , 234 , .08 ) ; color : # 667 eea ; font - weight : 800 ; }
. workbench - tabs : deep ( . el - tabs _ _item : focus - visible ) { outline : 2 px solid # 667 eea ; outline - offset : - 4 px ; }
. workbench - tab - label { display : inline - flex ; align - items : center ; justify - content : center ; gap : 8 px ; white - space : nowrap ; }
. container - detail - page { display : grid ; min - width : 0 ; gap : 12 px ; } . container - overview { overflow : hidden ; } . container - overview _ _header { display : grid ; grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 14 px ; padding : 10 px 16 px ; } . container - overview _ _header h1 { margin : 0 ; font - size : 18 px ; } . container - overview _ _header p { margin : 2 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; } . container - overview _ _summary { display : grid ; grid - template - columns : repeat ( 5 , minmax ( 0 , 1 fr ) ) ; border - top : 1 px solid var ( -- cc - border ) ; } . container - overview _ _summary > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; padding : 8 px 14 px ; border - right : 1 px solid var ( -- cc - border ) ; } . container - overview _ _summary > div : last - child { border - right : 0 ; } . container - overview _ _summary span , dt { color : var ( -- cc - muted ) ; font - size : 12 px ; } . container - overview _ _summary strong , dd { overflow : hidden ; margin : 0 ; text - overflow : ellipsis ; white - space : nowrap ; } . detail - section { display : grid ; min - width : 0 ; gap : 12 px ; padding : 12 px 16 px } . detail - section > h2 { margin : 0 ; font - size : 15 px } . detail - fields { padding : 0 ; } . detail - fields dl { display : grid ; grid - template - columns : repeat ( 6 , minmax ( 0 , 1 fr ) ) ; gap : 9 px 16 px ; margin : 0 ; } . detail - fields dl > div { min - width : 0 ; } . detail - fields dd { margin - top : 3 px ; font - size : 13 px ; } . outbound - panel { padding : 12 px 0 0 ; border - top : 1 px solid var ( -- cc - border ) } . outbound - panel _ _header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px } . outbound - panel _ _header h2 { margin : 0 ; font - size : 15 px } . outbound - panel _ _header p , . outbound - panel _ _hint { margin : 3 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px } . outbound - panel _ _hint -- success { color : var ( -- el - color - success ) } . outbound - panel _ _actions { display : flex ; align - items : center ; gap : 8 px } . outbound - panel _ _loading { padding : 8 px 0 } . gate - summary { display : flex ; align - items : center ; gap : 14 px ; margin - top : 8 px ; color : var ( -- cc - muted ) ; font - size : 12 px } . gate - summary _ _passed strong { color : var ( -- el - color - success ) } . gate - summary _ _blocked strong { color : var ( -- el - color - danger ) } . gate - summary _ _reference { margin - left : auto ; padding - left : 10 px ; border - left : 1 px solid var ( -- cc - border ) } . gate - list { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 6 px 12 px ; margin - top : 7 px } . gate - row { display : grid ; grid - template - columns : auto minmax ( 130 px , .9 fr ) minmax ( 100 px , 1 fr ) ; align - items : center ; gap : 6 px 8 px ; min - width : 0 ; padding : 6 px 8 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : var ( -- cc - surface - muted ) } . gate - row _ _actual , . gate - row small { overflow : hidden ; color : var ( -- cc - muted ) ; font - size : 12 px ; text - overflow : ellipsis ; white - space : nowrap } . gate - row small { grid - column : 2 / - 1 } . gate - row _ _coverage { display : flex ; grid - column : 2 / - 1 ; min - width : 0 ; align - items : center ; gap : 5 px ; overflow : hidden } . gate - row _ _coverage > span { flex : 0 0 auto ; color : var ( -- cc - muted ) ; font - size : 11 px } . gate - row _ _coverage : deep ( . el - tag ) { max - width : 120 px ; overflow : hidden ; text - overflow : ellipsis } . carrier - panel { padding - top : 4 px } @ media ( max - width : 1200 px ) { . detail - fields dl { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) } . gate - list { grid - template - columns : 1 fr } } @ media ( max - width : 720 px ) { . container - overview _ _header { grid - template - columns : auto minmax ( 0 , 1 fr ) } . container - overview _ _header > : last - child { grid - column : 2 } . container - overview _ _summary , . detail - fields dl { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) } . outbound - panel _ _header { align - items : flex - start ; flex - direction : column } . outbound - panel _ _actions , . gate - summary { flex - wrap : wrap } . gate - summary _ _reference { width : 100 % ; margin - left : 0 ; padding - left : 0 ; border - left : 0 } . gate - row { grid - template - columns : auto minmax ( 0 , 1 fr ) } . gate - row _ _actual , . gate - row small , . gate - row _ _coverage { grid - column : 2 } }
. detail - section _ _heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; }
. detail - section _ _heading h2 { margin : 0 ; font - size : 15 px ; }