@@ -19,6 +19,18 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
import { CONTAINER _SPECIFICATIONS , applyContainerSpecification , containerSpecification } from '@/shared/domain/containerSpecifications'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { createContainerSimulation , getContainer , linkContainerOceanBooking , listContainers , updateContainer } from '../api/platformContainerLoading'
import {
getTerminalDrayage ,
getTerminalDrayageHistory ,
listTerminalDrayages ,
recordTerminalDrayageEmptyReturn ,
recordTerminalDrayagePickup ,
scheduleTerminalDrayagePickup ,
selectTerminalDrayageFleet ,
type TerminalDrayageAction ,
type TerminalDrayageHistory ,
type TerminalDrayageView ,
} from '../api/terminalDrayages'
import type { ContainerBookingResource , ContainerDocumentStatus , ContainerPlanStatus , ContainerSaveRequest , ContainerSimulationCreateRequest } from '../types'
type Filters = QueryModel & { customerId : string ; keyword : string ; containerType : string ; status : ContainerPlanStatus | '' ; sailingRange : string [ ] }
@@ -68,9 +80,25 @@ const linkContainerNo = ref('')
const linkCandidates = ref < Array < { key : string ; oceanBookingId : string ; requirementId : string ; label : string ; disabled : boolean } > > ( [ ] )
const selectedTruckingSupplierId = ref ( '' )
const unresolvedTruckingSupplierName = ref < string | null > ( null )
const drayages = ref < Record < string , TerminalDrayageView > > ( { } )
const drayageLoading = ref ( false )
const drayageLoadError = ref < ApiErrorDetails | null > ( null )
const drayageSavingIds = ref < string [ ] > ( [ ] )
const drayageActionOpen = ref ( false )
const drayageAction = ref < Exclude < TerminalDrayageAction , ' SELECT_FLEET ' > > ( 'SCHEDULE_PICKUP' )
const drayageTarget = ref < ContainerBookingResource | null > ( null )
const drayageActionAt = ref ( '' )
const drayageReason = ref ( '' )
const drayageActionSaving = ref ( false )
const drayageActionError = ref < ApiErrorDetails | null > ( null )
const drayageHistoryOpen = ref ( false )
const drayageHistory = ref < TerminalDrayageHistory [ ] > ( [ ] )
const drayageHistoryLoading = ref ( false )
const drayageHistoryError = ref < ApiErrorDetails | null > ( null )
let listSequence = 0
let detailSequence = 0
let linkSequence = 0
let drayageSequence = 0
function emptyForm ( ) : ContainerForm {
return {
@@ -140,6 +168,7 @@ const queryFields = computed<QueryField[]>(() => [
] )
const columns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 310 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'emptyReturnWarning' , label : '报空预警' , width : 120 , slotName : 'emptyReturnWarning' } ,
{ key : 'containerNo' , label : '集装箱号' , minWidth : 150 , slotName : 'containerNo' } ,
{ key : 'planNo' , prop : 'planNo' , label : '装柜清单号' , minWidth : 165 , formatter : ( row : ContainerBookingResource ) => row . planNo || '—' } ,
{ key : 'planStatus' , label : '状态' , width : 110 , slotName : 'status' } ,
@@ -150,7 +179,11 @@ const columns: DataTableColumn[] = [
{ key : 'pickupOrderDocumentStatus' , label : 'PU资料' , width : 100 , formatter : ( row : ContainerBookingResource ) => documentStatusText ( row . pickupOrderDocumentStatus ) } ,
{ key : 'oceanBooking' , label : '真实订舱' , minWidth : 170 , slotName : 'oceanBooking' } ,
{ key : 'warehouseName' , prop : 'warehouseName' , label : '装柜仓库' , minWidth : 160 } ,
{ key : 'terminalTruckingCompany' , prop : 'terminalTruckingCompany' , label : '码头车队' , minWidth : 160 } ,
{ key : 'terminalTruckingCompany' , label : '码头车队' , width : 190 , slotName : 'terminalTruckingCompany' } ,
{ key : 'pickupAppointmentAt' , label : '预约提柜' , width : 165 , slotName : 'pickupAppointmentAt' } ,
{ key : 'actualPickupAt' , label : '实际提柜' , width : 165 , slotName : 'actualPickupAt' } ,
{ key : 'estimatedEmptyReturnAt' , label : '预计还柜' , width : 165 , slotName : 'estimatedEmptyReturnAt' } ,
{ key : 'actualEmptyReturnAt' , label : '实际还柜' , width : 165 , slotName : 'actualEmptyReturnAt' } ,
{ key : 'billOfLadingNo' , prop : 'billOfLadingNo' , label : '提单号' , minWidth : 150 } ,
{ key : 'route' , label : '航线' , minWidth : 220 , slotName : 'route' } ,
{ key : 'containerType' , prop : 'containerType' , label : '柜型' , width : 100 } ,
@@ -163,12 +196,45 @@ const columns: DataTableColumn[] = [
{ key : 'estimatedPortArrivalAt' , label : '预计到港' , minWidth : 160 , slotName : 'estimatedPortArrivalAt' } ,
{ key : 'actualPortArrivalAt' , label : '实际到港' , minWidth : 160 , slotName : 'actualPortArrivalAt' } ,
]
const drayageHistoryColumns : DataTableColumn [ ] = [
{ key : 'action' , label : '动作' , width : 130 , formatter : ( row : TerminalDrayageHistory ) => drayageActionText ( row . action ) } ,
{ key : 'status' , label : '状态变化' , minWidth : 180 , formatter : ( row : TerminalDrayageHistory ) => ` ${ drayageStatusText ( row . fromStatus ) } → ${ drayageStatusText ( row . toStatus ) } ` } ,
{ key : 'version' , label : '版本' , width : 100 , formatter : ( row : TerminalDrayageHistory ) => ` ${ row . fromVersion } → ${ row . toVersion } ` } ,
{ key : 'operatorName' , label : '操作人' , width : 130 , formatter : ( row : TerminalDrayageHistory ) => row . operatorName || '—' } ,
{ key : 'createdAt' , label : '操作时间' , width : 170 , formatter : ( row : TerminalDrayageHistory ) => formatTime ( row . createdAt ) } ,
{ key : 'reason' , label : '原因' , minWidth : 200 } ,
]
function optional ( value : string | null ) { return value ? . trim ( ) || null }
function normalizedContainerNo ( value : string | null | undefined ) { return String ( value || '' ) . trim ( ) . toUpperCase ( ) }
function statusText ( status : ContainerPlanStatus ) { return ( { WAIT _LOADING : '待装柜' , EDITING : '预装柜中' , LOADING : '拣货中' , COMPLETED : '装柜完成' } ) [ status ] }
function documentStatusText ( status : ContainerDocumentStatus | null | undefined ) { return status ? ( { MISSING : '未上传' , UPLOADED : '已上传' , COMPLETED : '已完成' } ) [ status ] : '—' }
function formatTime ( value : string | null | undefined ) { return value ? . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) || '—' }
function drayageFor ( containerBookingId : string ) { return drayages . value [ containerBookingId ] }
function drayageStatusText ( status ? : string | null ) { return ( { WAITING _FOR _ARRIVAL : '待到港' , ARRIVED : '已到港' , PICKUP _SCHEDULED : '已约提柜' , PICKED _UP : '已提柜' , UNLOADED : '已卸柜' , EMPTY _RETURNED : '已还柜' } as Record < string , string > ) [ status || '' ] || status || '—' }
function drayageActionText ( action : TerminalDrayageAction ) { return ( { SELECT _FLEET : '选择码头车队' , SCHEDULE _PICKUP : '预约提柜' , RECORD _PICKUP : '确认实际提柜' , RECORD _EMPTY _RETURN : '确认实际还柜' } as Record < TerminalDrayageAction , string > ) [ action ] }
function canDrayage ( row : ContainerBookingResource , action : TerminalDrayageAction ) { return Boolean ( canEdit . value && drayageFor ( row . id ) ? . allowedActions . includes ( action ) ) }
function drayageWarningText ( row : ContainerBookingResource ) {
const warning = drayageFor ( row . id ) ? . emptyReturnWarning
if ( ! warning || warning . status === 'NOT_APPLICABLE' ) return '—'
if ( warning . status === 'RESOLVED' ) return '已还柜'
if ( warning . status === 'OVERDUE' ) return warning . overdueDays > 0 ? ` 超时 ${ warning . overdueDays } 天 ` : ` 超时 ${ warning . overdueHours } 小时 `
return '待还柜'
}
function drayageWarningType ( row : ContainerBookingResource ) {
const status = drayageFor ( row . id ) ? . emptyReturnWarning . status
return status === 'OVERDUE' ? 'danger' : status === 'RESOLVED' ? 'success' : status === 'PENDING' ? 'warning' : 'info'
}
function drayageWarningTitle ( row : ContainerBookingResource ) {
const warning = drayageFor ( row . id ) ? . emptyReturnWarning
if ( ! warning ) return ''
const deadline = warning . deadlineAt ? ` 还柜截止: ${ formatTime ( warning . deadlineAt ) } ` : ''
return [ drayageWarningText ( row ) , deadline ] . filter ( Boolean ) . join ( '; ' )
}
function localDateTimeValue ( ) {
const now = new Date ( Date . now ( ) - new Date ( ) . getTimezoneOffset ( ) * 60 _000 )
return now . toISOString ( ) . slice ( 0 , 19 )
}
function containerViewLabel ( row : ContainerBookingResource ) {
const reference = [ row . cabinetNo , row . containerNo , row . bookingNo ]
. find ( ( value ) => typeof value === 'string' && value . trim ( ) )
@@ -408,6 +474,81 @@ async function saveLink() {
} catch ( error ) { linkError . value = getApiErrorDetails ( error , '关联真实订舱柜失败' ) }
finally { linkSaving . value = false }
}
function replaceDrayage ( value : TerminalDrayageView ) {
drayages . value = { ... drayages . value , [ value . containerBookingId ] : value }
}
async function loadDrayages ( targetRows = rows . value ) {
const sequence = ++ drayageSequence
if ( ! targetRows . length ) { drayages . value = { } ; drayageLoadError . value = null ; return }
drayageLoading . value = true ; drayageLoadError . value = null
try {
const values = await listTerminalDrayages ( targetRows . map ( ( row ) => row . id ) )
if ( sequence !== drayageSequence ) return
drayages . value = Object . fromEntries ( values . map ( ( value ) => [ value . containerBookingId , value ] ) )
} catch ( error ) {
if ( sequence !== drayageSequence ) return
drayages . value = { }
drayageLoadError . value = getApiErrorDetails ( error , '码头拖车信息加载失败,请重试' )
} finally { if ( sequence === drayageSequence ) drayageLoading . value = false }
}
async function refreshDrayage ( containerBookingId : string ) {
const value = await getTerminalDrayage ( containerBookingId )
replaceDrayage ( value )
return value
}
async function selectRowFleet ( source : object , supplierIdValue : unknown ) {
const row = source as ContainerBookingResource
const current = drayageFor ( row . id )
const supplier = truckingCompanies . value . find ( ( item ) => item . id === String ( supplierIdValue || '' ) )
if ( ! current || ! supplier || ! canDrayage ( row , 'SELECT_FLEET' ) || drayageSavingIds . value . includes ( row . id ) ) return
drayageSavingIds . value = [ ... drayageSavingIds . value , row . id ]
try {
replaceDrayage ( await selectTerminalDrayageFleet ( row . id , { expectedVersion : current . version , fleetSupplierId : supplier . id , fleetSupplierName : supplier . name } ) )
ElMessage . success ( '码头车队已更新' )
} catch ( error ) {
const details = getApiErrorDetails ( error , '码头车队更新失败' )
if ( details . code === 'A0409' ) {
try { await refreshDrayage ( row . id ) } catch { /* 保留原始冲突证据 */ }
ElMessage . warning ( '运输信息已被其他人修改,已刷新当前柜最新状态' )
} else ElMessage . error ( details . message )
} finally { drayageSavingIds . value = drayageSavingIds . value . filter ( ( id ) => id !== row . id ) }
}
function openDrayageAction ( row : ContainerBookingResource , action : Exclude < TerminalDrayageAction , ' SELECT_FLEET ' > ) {
if ( ! canDrayage ( row , action ) ) return
drayageTarget . value = row ; drayageAction . value = action ; drayageActionAt . value = localDateTimeValue ( ) ; drayageReason . value = ''
drayageActionError . value = null ; drayageActionOpen . value = true
}
async function saveDrayageAction ( ) {
const row = drayageTarget . value
const current = row ? drayageFor ( row . id ) : null
if ( ! row || ! current || ! drayageActionAt . value || ! canDrayage ( row , drayageAction . value ) || drayageActionSaving . value ) return
drayageActionSaving . value = true ; drayageActionError . value = null
try {
const reason = drayageReason . value . trim ( ) || undefined
const next = drayageAction . value === 'SCHEDULE_PICKUP'
? await scheduleTerminalDrayagePickup ( row . id , { expectedVersion : current . version , pickupAppointmentAt : drayageActionAt . value , reason } )
: drayageAction . value === 'RECORD_PICKUP'
? await recordTerminalDrayagePickup ( row . id , { expectedVersion : current . version , actualAt : drayageActionAt . value , reason } )
: await recordTerminalDrayageEmptyReturn ( row . id , { expectedVersion : current . version , actualAt : drayageActionAt . value , reason } )
replaceDrayage ( next ) ; drayageActionOpen . value = false
ElMessage . success ( ` ${ drayageActionText ( drayageAction . value ) } 成功 ` )
} catch ( error ) {
const details = getApiErrorDetails ( error , ` ${ drayageActionText ( drayageAction . value ) } 失败 ` )
if ( details . code === 'A0409' ) {
try { await refreshDrayage ( row . id ) } catch { /* 保留原始冲突证据 */ }
drayageActionError . value = { ... details , message : '运输信息已被其他人修改,已刷新最新状态;请重新确认后提交' }
} else drayageActionError . value = details
} finally { drayageActionSaving . value = false }
}
async function openDrayageHistory ( row : ContainerBookingResource ) {
drayageTarget . value = row ; drayageHistory . value = [ ] ; drayageHistoryError . value = null ; drayageHistoryOpen . value = true ; drayageHistoryLoading . value = true
try {
const [ current , history ] = await Promise . all ( [ getTerminalDrayage ( row . id ) , getTerminalDrayageHistory ( row . id ) ] )
if ( drayageTarget . value ? . id !== row . id ) return
replaceDrayage ( current ) ; drayageHistory . value = history
} catch ( error ) { if ( drayageTarget . value ? . id === row . id ) drayageHistoryError . value = getApiErrorDetails ( error , '码头拖车记录加载失败' ) }
finally { if ( drayageTarget . value ? . id === row . id ) drayageHistoryLoading . value = false }
}
async function loadRows ( ) {
const sequence = ++ listSequence
loading . value = true ; loadError . value = null
@@ -421,6 +562,7 @@ async function loadRows() {
} )
if ( sequence !== listSequence ) return
rows . value = result . records ; total . value = result . total
await loadDrayages ( result . records )
} catch ( error ) {
if ( sequence !== listSequence ) return
rows . value = [ ] ; total . value = 0 ; loadError . value = getApiErrorDetails ( error , '货柜列表加载失败,请稍后重试' )
@@ -435,6 +577,8 @@ function handleMoreAction(command: string, source: object) {
const row = source as ContainerBookingResource
if ( command === 'link' ) { void openLink ( row ) ; return }
if ( command === 'overview' ) { openDetail ( row ) ; return }
if ( command === 'drayage-history' ) { void openDrayageHistory ( row ) ; return }
if ( command . startsWith ( 'drayage:' ) ) { openDrayageAction ( row , command . slice ( 'drayage:' . length ) as Exclude < TerminalDrayageAction , ' SELECT_FLEET ' > ) ; return }
if ( [ 'exception' , 'documents' ] . includes ( command ) ) void router . push ( { name : 'container-detail' , params : { containerId : row . id } , query : { action : command } } )
}
async function openEdit ( source : object ) {
@@ -534,12 +678,19 @@ onMounted(async () => {
< LoadErrorState title = "货柜列表加载失败" :message = "loadError.message" :loading = "loading" @retry ="loadRows" > < template # details > < ApiErrorMeta :details = "loadError" / > < / template > < / LoadErrorState >
< / template >
< section v-if = "!loadError" class="surface-panel table-panel" >
< el -alert v-if = "drayageLoadError" class="drayage-load-alert" type="warning" :closable="false" show-icon :title="drayageLoadError.message"><el-button link type="warning" :loading="drayageLoading" @click="loadDrayages()" > 重新加载码头拖车信息 < / el -button > < / el-alert >
< DataTable v -model :page = "page" v -model :page-size = "pageSize" :data = "rows" :columns = "columns" :total = "total" :loading = "loading" stripe preference -key = " container -loading -table " empty -text = " 暂无货柜 " >
< template # toolbar -actions > < el-button v-if = "canCreate" type="primary" @click="openSimulation"><Plus :size="16" / > 新增模拟柜 < / el-button > < / template >
< template # actions = "{ row }" > < div class = "row-actions" > < el-button v-if = "canView" size="small" type="primary" plain :aria-label="containerViewLabel(row as ContainerBookingResource)" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="canView && canEdit && ['WAIT_LOADING', 'EDITING'].includes(row.planStatus)" size="small" @click="openEdit(row)"><Pencil :size="15" />编辑运输信息</el-button><el-button v-if="row.planStatus === 'EDITING' && canSmartLoading" size="small" type="primary" @click="goContainerProcess(row)"><Sparkles :size="15" />智能装柜</el-button><el-button v-else-if="row.planStatus === 'LOADING' && canViewPreload" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />预装清单</el-button><el-button v-else-if="row.planStatus === 'COMPLETED' && canViewLoadingList" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />装柜清单</el-button><el-dropdown v-if="canView" trigger="click" @command="handleMoreAction($event, row)"><el-button size="small" plain aria-label="更多货柜操作" title="更多货柜操作"><MoreHorizontal :size="16" /></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="overview"><FileText :size="15" />轨迹与日志</el-dropdown-item><el-dropdown-item command="documents"><FileUp :size="15" />DO / PU 资料</el-dropdown-item><el-dropdown-item v-if="canEdit && row.sourceType === 'SIMULATION' && !row.oceanBookingId && ['WAIT_LOADING','EDITING'].includes(row.planStatus)" command="link"><Link2 :size="15" />关联已放舱订舱</el-dropdown-item><el-dropdown-item v-if="canEdit" command="exception" divided><TriangleAlert :size="15" / > 异常处理 < / el-dropdown-item > < / el-dropdown-menu > < / template > < / el-dropdown > < / div > < / template >
< template # actions = "{ row }" > < div class = "row-actions" > < el-button v-if = "canView" size="small" type="primary" plain :aria-label="containerViewLabel(row as ContainerBookingResource)" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="canView && canEdit && ['WAIT_LOADING', 'EDITING'].includes(row.planStatus)" size="small" @click="openEdit(row)"><Pencil :size="15" />编辑运输信息</el-button><el-button v-if="row.planStatus === 'EDITING' && canSmartLoading" size="small" type="primary" @click="goContainerProcess(row)"><Sparkles :size="15" />智能装柜</el-button><el-button v-else-if="row.planStatus === 'LOADING' && canViewPreload" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />预装清单</el-button><el-button v-else-if="row.planStatus === 'COMPLETED' && canViewLoadingList" size="small" type="primary" plain @click="goContainerProcess(row)"><ListChecks :size="15" />装柜清单</el-button><el-dropdown v-if="canView" trigger="click" @command="handleMoreAction($event, row)"><el-button size="small" plain aria-label="更多货柜操作" title="更多货柜操作"><MoreHorizontal :size="16" /></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="overview"><FileText :size="15" />轨迹与日志</el-dropdown-item><el-dropdown-item command="drayage-history"><FileText :size="15" />码头拖车记录</el-dropdown-item><el-dropdown-item v-if="canDrayage(row as ContainerBookingResource, 'SCHEDULE_PICKUP')" command="drayage:SCHEDULE_PICKUP">预约提柜</el-dropdown-item><el-dropdown-item v-if="canDrayage(row as ContainerBookingResource, 'RECORD_PICKUP')" command="drayage:RECORD_PICKUP">确认实际提柜</el-dropdown-item><el-dropdown-item v-if="canDrayage(row as ContainerBookingResource, 'RECORD_EMPTY_RETURN')" command="drayage:RECORD_EMPTY_RETURN">确认实际还柜</el-dropdown-item><el-dropdown-item command="documents"><FileUp :size="15" />DO / PU 资料</el-dropdown-item><el-dropdown-item v-if="canEdit && row.sourceType === 'SIMULATION' && !row.oceanBookingId && ['WAIT_LOADING','EDITING'].includes(row.planStatus)" command="link"><Link2 :size="15" />关联已放舱订舱</el-dropdown-item><el-dropdown-item v-if="canEdit" command="exception" divided><TriangleAlert :size="15" / > 异常处理 < / el-dropdown-item > < / el-dropdown-menu > < / template > < / el-dropdown > < / div > < / template >
< template # emptyReturnWarning = "{ row }" > < el-tag : type = "drayageWarningType(row as ContainerBookingResource)" effect = "light" : title = "drayageWarningTitle(row as ContainerBookingResource)" > { { drayageWarningText ( row as ContainerBookingResource ) } } < / el-tag > < / template >
< template # cabinet = "{ row }" > < strong > { { row . cabinetNo } } < / strong > < small > 系统生成 < / small > < / template >
< template # oceanBooking = "{ row }" > < span v-if = "row.oceanBookingId" > < strong > { { row . billOfLadingNo || '已关联订舱' } } < / strong > < small > { { row . containerNo ? ` 集装箱号: ${ row . containerNo } ` : '集装箱号:待补录' } } < / small > < / span > < span v-else > 未关联 < / span > < / template >
< template # containerNo = "{ row }" > { { row . containerNo || '待补录' } } < / template >
< template # terminalTruckingCompany = "{ row }" > < el-select class = "fleet-select" : model -value = " drayageFor ( row.id ) ? .fleetSupplierId | | ' ' " filterable placeholder = "请选择车队" :loading = "drayageSavingIds.includes(row.id)" : disabled = "!canDrayage(row as ContainerBookingResource, 'SELECT_FLEET') || drayageSavingIds.includes(row.id)" @ update : model -value = " selectRowFleet ( row , $ event ) " > < el-option v-if = "drayageFor(row.id)?.fleetSupplierName && !truckingCompanies.some((item) => item.id === drayageFor(row.id)?.fleetSupplierId)" :label="drayageFor(row.id)?.fleetSupplierName || '原码头车队'" :value="drayageFor(row.id)?.fleetSupplierId || ''" disabled /><el-option v-for="supplier in truckingCompanies" :key="supplier.id" :label="supplier.name" :value="supplier.id" / > < / el-select > < / template >
< template # pickupAppointmentAt = "{ row }" > { { formatTime ( drayageFor ( row . id ) ? . pickupAppointmentAt ) } } < / template >
< template # actualPickupAt = "{ row }" > { { formatTime ( drayageFor ( row . id ) ? . actualPickupAt ) } } < / template >
< template # estimatedEmptyReturnAt = "{ row }" > { { formatTime ( drayageFor ( row . id ) ? . estimatedEmptyReturnAt ) } } < / template >
< template # actualEmptyReturnAt = "{ row }" > { { formatTime ( drayageFor ( row . id ) ? . actualEmptyReturnAt ) } } < / template >
< template # status = "{ row }" > < el-tag : type = "row.planStatus === 'COMPLETED' ? 'success' : row.planStatus === 'LOADING' ? 'warning' : 'info'" > { { statusText ( row . planStatus ) } } < / el-tag > < / template >
< template # route = "{ row }" > { { row . departurePortName || row . departurePortCode || '—' } } → { { row . destinationPortName || row . destinationPortCode || '—' } } < / template >
< template # capacity = "{ row }" > { { row . capacityCbm ? ` ${ row . capacityCbm } m³ ` : '—' } } / { { row . maxWeightKg ? ` ${ row . maxWeightKg } kg ` : '—' } } < / template >
@@ -595,12 +746,22 @@ onMounted(async () => {
< el-form-item label = "预计到港时间" > < el-date-picker v-model = "form.estimatedPortArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" / > < / el-form-item >
< / el-form >
< / AppDialog >
< AppDialog v-model = "drayageActionOpen" :title="drayageActionText(drayageAction)" width="min(520px, calc(100vw - 28px))" :confirm-text="drayageActionText(drayageAction)" cancel-text="取消" :confirm-loading="drayageActionSaving" :confirm-disabled="!drayageActionAt" @confirm="saveDrayageAction" >
< el -form label -position = " top " > < el-form-item : label = "drayageAction === 'SCHEDULE_PICKUP' ? '预约提柜时间' : drayageAction === 'RECORD_PICKUP' ? '实际提柜时间' : '实际还柜时间'" required > < el-date-picker v-model = "drayageActionAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择时间" /></el-form-item><el-form-item label="原因/备注"><el-input v-model="drayageReason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="选填" / > < / el-form-item > < / el-form >
< LoadErrorState v-if = "drayageActionError" compact :title="`${drayageActionText(drayageAction)}失败`" :message="drayageActionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="drayageActionError" / > < / template > < / LoadErrorState >
< / AppDialog >
< AppDialog v-model = "drayageHistoryOpen" title="码头拖车记录" width="min(980px, calc(100vw - 28px))" :show-footer="false" >
< LoadErrorState v-if = "drayageHistoryError" compact title="码头拖车记录加载失败" :message="drayageHistoryError.message" :loading="drayageHistoryLoading" @retry="drayageTarget && openDrayageHistory(drayageTarget)"><template #details><ApiErrorMeta :details="drayageHistoryError" / > < / template > < / LoadErrorState >
< template v-else > < el -descriptions v-if = "drayageTarget && drayageFor(drayageTarget.id)" :column="4" border class="drayage-summary"><el-descriptions-item label="当前状态">{{ drayageStatusText(drayageFor(drayageTarget.id)?.status) }}</el-descriptions-item><el-descriptions-item label="码头车队">{{ drayageFor(drayageTarget.id)?.fleetSupplierName || '—' }}</el-descriptions-item><el-descriptions-item label="预计还柜">{{ formatTime(drayageFor(drayageTarget.id)?.estimatedEmptyReturnAt) }}</el-descriptions-item><el-descriptions-item label="实际还柜">{{ formatTime(drayageFor(drayageTarget.id)?.actualEmptyReturnAt) }}</el-descriptions-item></el-descriptions><DataTable :data="drayageHistory" :columns="drayageHistoryColumns" :loading="drayageHistoryLoading" stripe column-settings-mode="hide" empty-text="暂无码头拖车操作记录" / > < / template >
< / AppDialog >
< / section >
< / AdminPage >
< / template >
< style scoped >
. container - page { display : grid ; gap : 16 px ; min - width : 0 ; } . container - page small { display : block ; margin - top : 4 px ; color : var ( -- cc - muted ) ; } . row - actions { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 4 px ; } . row - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; } . form - grid { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) ; gap : 0 ; } . form - grid : deep ( . el - select ) , . form - grid : deep ( . el - date - editor ) , . form - grid : deep ( . el - input - number ) , . simulation - form : deep ( . el - select ) , . simulation - form : deep ( . el - input - number ) { width : 100 % ; } . details : deep ( . el - descriptions _ _label ) { width : 120 px ; } . dialog - hint { margin : 4 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; }
. container - page { display : grid ; gap : 16 px ; min - width : 0 ; } . container - page small { display : block ; margin - top : 4 px ; color : var ( -- cc - muted ) ; } . row - actions { display : flex ; align - items : center ; flex - wrap : nowrap ; gap : 4 px ; white - space : nowrap ; } . row - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; } . form - grid { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) ; gap : 0 ; } . form - grid : deep ( . el - select ) , . form - grid : deep ( . el - date - editor ) , . form - grid : deep ( . el - input - number ) , . simulation - form : deep ( . el - select ) , . simulation - form : deep ( . el - input - number ) , . fleet - select { width : 100 % ; } . details : deep ( . el - descriptions _ _label ) { width : 120 px ; } . dialog - hint { margin : 4 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; } . drayage - load - alert { margin - bottom : 12 px ; } . drayage - summary { margin - bottom : 14 px ; }
: global ( . container - editor - dialog . el - dialog ) { display : flex ; width : min ( 820 px , calc ( 100 vw - 28 px ) ) ! important ; max - height : 90 vh ; margin - block : 5 vh ! important ; overflow : hidden ; flex - direction : column ; border - radius : 12 px ; }
: global ( . container - editor - dialog . el - dialog _ _header ) { flex : 0 0 auto ; margin : 0 ; padding : 20 px 28 px ; border - bottom : 1 px solid # e5eaf1 ; }
: global ( . container - editor - dialog . el - dialog _ _title ) { color : # 1 e293b ; font - size : 18 px ; font - weight : 700 ; }