2026-08-10 15:36:53 +08:00
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
2026-08-31 12:40:01 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-09-08 19:57:42 +08:00
import { Boxes , Check , Eye , FileText , FileUp , Link2 , ListChecks , MoreHorizontal , Pencil , Plus , Sparkles } from '@lucide/vue'
2026-08-26 11:10:48 +08:00
import { ElMessage , type FormInstance , type FormRules } from 'element-plus'
2026-08-10 15:36:53 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-14 12:50:42 +08:00
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-08-10 15:36:53 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
2026-08-11 18:06:35 +08:00
import { listPlatformPartners , type PlatformPartnerResource } from '@/modules/master-data'
2026-08-15 11:12:22 +08:00
import { listSuppliers , type Supplier } from '@/modules/suppliers'
2026-08-25 19:56:37 +08:00
import { listOceanBookingLinkCandidates , listOceanBookings , listWarehouseLogisticsChannels , type OceanBookingResource , type OceanLogisticsChannel } from '@/modules/ocean-bookings'
2026-08-10 16:24:54 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-10 15:36:53 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-12 16:31:46 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-10 15:36:53 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 16:24:54 +08:00
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-10 15:36:53 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-08-17 12:31:27 +08:00
import { CONTAINER_SPECIFICATIONS , applyContainerSpecification , containerSpecification } from '@/shared/domain/containerSpecifications'
2026-08-10 15:36:53 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-31 12:12:51 +08:00
import { createContainerSimulation , getContainer , linkContainerOceanBooking , listContainers , updateContainer } from '../api/platformContainerLoading'
2026-09-07 23:58:18 +08:00
import {
getTerminalDrayage ,
getTerminalDrayageHistory ,
recordTerminalDrayageEmptyReturn ,
recordTerminalDrayagePickup ,
scheduleTerminalDrayagePickup ,
selectTerminalDrayageFleet ,
type TerminalDrayageAction ,
type TerminalDrayageHistory ,
type TerminalDrayageView ,
} from '../api/terminalDrayages'
2026-09-08 19:57:42 +08:00
import type { ContainerBookingResource , ContainerDocumentStatus , ContainerPlanStatus , ContainerSaveRequest , ContainerSimulationCreateRequest , ContainerWorkbenchAction , ContainerWorkbenchRow } from '../types'
2026-08-10 15:36:53 +08:00
2026-08-14 12:50:42 +08:00
type Filters = QueryModel & { customerId : string ; keyword : string ; containerType : string ; status : ContainerPlanStatus | '' ; sailingRange : string [] }
2026-08-19 09:42:12 +08:00
type ContainerForm = ContainerSaveRequest & Pick < ContainerBookingResource , ' cabinetNo ' > & { id : string ; version : number }
2026-08-10 15:36:53 +08:00
2026-09-08 19:57:42 +08:00
const props = withDefaults ( defineProps < { workbench ?: 'DOMESTIC' | 'OVERSEAS' } > (), { workbench : 'DOMESTIC' })
2026-08-10 15:36:53 +08:00
const router = useRouter ()
2026-08-31 12:40:01 +08:00
const route = useRoute ()
2026-08-10 15:36:53 +08:00
const access = useAccessPreviewStore ()
2026-09-08 19:57:42 +08:00
const rows = ref < ContainerWorkbenchRow [] >([])
2026-08-14 12:50:42 +08:00
const customers = ref < PlatformTenantResource [] >([])
const customersLoading = ref ( false )
2026-08-10 15:36:53 +08:00
const warehouses = ref < PlatformWarehouseSummaryResource [] >([])
2026-08-11 18:06:35 +08:00
const destinationWarehouses = ref < PlatformWarehouseSummaryResource [] >([])
const partners = ref < PlatformPartnerResource [] >([])
2026-08-15 11:12:22 +08:00
const truckingSuppliers = ref < Supplier [] >([])
2026-08-11 18:06:35 +08:00
const oceanBookings = ref < OceanBookingResource [] >([])
2026-08-10 15:36:53 +08:00
const loading = ref ( false )
const warehouseLoading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const warehouseError = ref < ApiErrorDetails | null >( null )
2026-08-11 18:06:35 +08:00
const businessOptionsLoading = ref ( false )
const businessOptionsError = ref ( '' )
2026-08-10 15:36:53 +08:00
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
2026-09-08 19:57:42 +08:00
const scope = ref < 'ACTIVE' | 'HISTORY' > ( 'ACTIVE' )
2026-08-14 12:50:42 +08:00
const filters = ref < Filters >({ customerId : '' , keyword : '' , containerType : '' , status : '' , sailingRange : [] })
2026-08-10 15:36:53 +08:00
const applied = ref < Filters >({ ... filters . value })
const detail = ref < ContainerBookingResource | null >( null )
const editorOpen = ref ( false )
const editorError = ref < ApiErrorDetails | null >( null )
const saving = ref ( false )
const formRef = ref < FormInstance >()
2026-08-14 20:59:19 +08:00
const simulationOpen = ref ( false )
const simulationSaving = ref ( false )
const simulationError = ref < ApiErrorDetails | null >( null )
const simulationChannels = ref < OceanLogisticsChannel [] >([])
const simulationChannelsLoading = ref ( false )
const simulationFormRef = ref < FormInstance >()
2026-08-17 12:31:27 +08:00
const simulationForm = reactive < ContainerSimulationCreateRequest >({ channelCode : '' , warehouseId : '' , containerType : '40HC' , capacityCbm : 76 , maxWeightKg : 28000 })
2026-08-14 20:59:19 +08:00
const linkOpen = ref ( false )
const linkLoading = ref ( false )
const linkSaving = ref ( false )
const linkError = ref < ApiErrorDetails | null >( null )
const linkTarget = ref < ContainerBookingResource | null >( null )
const linkCandidateKey = ref ( '' )
2026-08-18 16:09:43 +08:00
const linkContainerNo = ref ( '' )
const linkCandidates = ref < Array < { key : string ; oceanBookingId : string ; requirementId : string ; label : string ; disabled : boolean }> > ([])
2026-08-15 11:12:22 +08:00
const selectedTruckingSupplierId = ref ( '' )
2026-08-25 19:56:37 +08:00
const unresolvedTruckingSupplierName = ref < string | null >( null )
2026-09-07 23:58:18 +08:00
const drayages = ref < Record < string , TerminalDrayageView > > ({})
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 )
2026-08-10 15:36:53 +08:00
let listSequence = 0
let detailSequence = 0
2026-08-25 19:56:37 +08:00
let linkSequence = 0
2026-09-08 20:55:30 +08:00
let warehousesLoaded = false
let warehouseOptionsPromise : Promise < void > | null = null
let partnersLoaded = false
let truckingSuppliersLoaded = false
let truckingSuppliersPromise : Promise < Supplier [] > | null = null
let oceanBookingsLoaded = false
let businessOptionsPromise : Promise < void > | null = null
2026-08-10 15:36:53 +08:00
function emptyForm () : ContainerForm {
return {
id : '' , version : 0 , containerNo : null , cabinetNo : '' , warehouseId : '' , warehouseName : null ,
2026-08-17 12:31:27 +08:00
billOfLadingNo : null , vesselName : null , voyageNo : null , containerType : '40HC' , shippingCompanyId : null ,
2026-08-10 15:36:53 +08:00
shippingCompanyName : null , departurePortCode : null , departurePortName : null , destinationPortCode : null ,
destinationPortName : null , destinationWarehouseCode : null , destinationWarehouseName : null ,
2026-08-25 19:56:37 +08:00
capacityCbm : 76 , maxWeightKg : 28000 , terminalTruckingCompanyId : null , terminalTruckingCompany : null , loadingAt : null ,
2026-08-10 15:36:53 +08:00
estimatedSailingAt : null , actualSailingAt : null , estimatedPortArrivalAt : null , actualPortArrivalAt : null ,
estimatedWarehouseArrivalAt : null , actualWarehouseArrivalAt : null ,
}
}
const form = reactive < ContainerForm >( emptyForm ())
const rules : FormRules < ContainerForm > = {
warehouseId : [{ required : true , message : '请选择装柜仓库' , trigger : 'change' }],
containerType : [{ required : true , message : '请选择柜型' , trigger : 'change' }],
2026-08-25 19:56:37 +08:00
terminalTruckingCompany : [{ validator : ( _rule , value , callback ) => callback ( unresolvedTruckingSupplierName . value || ( value && ! selectedTruckingSupplierId . value ) ? new Error ( '请选择有效的合作中拖车供应商' ) : undefined ), trigger : 'change' }],
2026-08-10 15:36:53 +08:00
}
2026-09-08 19:57:42 +08:00
const isDomestic = computed (() => props . workbench === 'DOMESTIC' )
const isOverseas = computed (() => props . workbench === 'OVERSEAS' )
const canView = computed (() => isDomestic . value ? access . can ( 'platform:firstleg:container:list' ) : access . can ( 'platform:firstleg:terminal-drayage:list' ))
2026-08-10 15:36:53 +08:00
const canEdit = computed (() => access . can ( 'platform:firstleg:container:edit' ))
2026-08-14 20:59:19 +08:00
const canCreate = computed (() => access . can ( 'platform:firstleg:container:create' ))
2026-09-08 19:57:42 +08:00
const canOverseasDocumentEdit = computed (() => access . can ( 'platform:firstleg:terminal-drayage:document:edit' ))
const canFleetEdit = computed (() => access . can ( 'platform:firstleg:terminal-drayage:fleet:edit' ))
const canPickupAppointmentEdit = computed (() => access . can ( 'platform:firstleg:terminal-drayage:pickup-appointment:edit' ))
const canPickupConfirm = computed (() => access . can ( 'platform:firstleg:terminal-drayage:pickup:confirm' ))
const canEmptyReturnConfirm = computed (() => access . can ( 'platform:firstleg:terminal-drayage:empty-return:confirm' ))
2026-08-10 15:36:53 +08:00
const canSmartLoading = computed (() => access . can ( 'platform:firstleg:smart-loading:list' ))
2026-08-11 11:15:44 +08:00
const canViewPreload = computed (() => access . can ( 'platform:firstleg:preload:list' ))
const canViewLoadingList = computed (() => access . can ( 'platform:firstleg:loading-list:list' ))
2026-08-12 16:31:46 +08:00
const editorTitle = computed (() => form . id ? '编辑' : '新增' )
2026-08-11 18:06:35 +08:00
const shippingCompanies = computed (() => partners . value . filter (( partner ) => partner . status === 'ACTIVE' && partner . types . includes ( 'CARRIER' )))
2026-08-15 11:12:22 +08:00
const truckingCompanies = computed (() => truckingSuppliers . value . filter (( supplier ) => supplier . status === 'ACTIVE' && supplier . category === 'TRUCKING' ))
2026-08-14 20:59:19 +08:00
const simulationRules : FormRules < ContainerSimulationCreateRequest > = {
warehouseId : [{ required : true , message : '请选择装柜仓库' , trigger : 'change' }],
channelCode : [{ required : true , message : '请选择物流渠道' , trigger : 'change' }],
containerType : [{ required : true , message : '请选择柜型' , trigger : 'change' }],
}
2026-08-11 18:06:35 +08:00
interface PortOption { value : string ; code : string | null ; name : string | null }
function portValue ( code : string | null | undefined , name : string | null | undefined ) { return code || name ? JSON . stringify ([ code || '' , name || '' ]) : '' }
function collectPorts ( direction : 'departure' | 'destination' ) : PortOption [] {
const entries = oceanBookings . value . map (( booking ) => direction === 'departure'
? { code : booking . departurePortCode , name : booking . departurePortName }
: { code : booking . destinationPortCode , name : booking . destinationPortName })
const current = direction === 'departure'
? { code : form . departurePortCode , name : form . departurePortName }
: { code : form . destinationPortCode , name : form . destinationPortName }
entries . push ( current )
const options = new Map < string , PortOption >()
for ( const entry of entries ) {
if ( ! entry . code && ! entry . name ) continue
const value = portValue ( entry . code , entry . name )
options . set ( value , { value , code : entry . code || null , name : entry . name || null })
}
return [... options . values ()]. sort (( left , right ) => ( left . name || left . code || '' ). localeCompare ( right . name || right . code || '' , 'zh-CN' ))
}
const departurePorts = computed (() => collectPorts ( 'departure' ))
const destinationPorts = computed (() => collectPorts ( 'destination' ))
const departurePortValue = computed (() => portValue ( form . departurePortCode , form . departurePortName ))
const destinationPortValue = computed (() => portValue ( form . destinationPortCode , form . destinationPortName ))
const shippingCompanyValue = computed (() => form . shippingCompanyId || ( form . shippingCompanyName ? `name: ${ form . shippingCompanyName } ` : '' ))
const destinationWarehouseValue = computed (() => form . destinationWarehouseCode || ( form . destinationWarehouseName ? `name: ${ form . destinationWarehouseName } ` : '' ))
2026-08-10 15:36:53 +08:00
const queryFields = computed < QueryField [] >(() => [
2026-08-14 14:27:03 +08:00
{ key : 'customerId' , type : 'select' , label : '客户' , width : 220 , placeholder : '全部客户' , options : customers . value . filter (( customer ) => customer . status === 'ACTIVE' ). map (( customer ) => ({ label : customer . tenantName , value : String ( customer . id ) })), props : { filterable : true , loading : customersLoading . value } },
2026-08-15 17:04:00 +08:00
{ key : 'keyword' , type : 'input' , label : '关键词' , width : 220 , placeholder : '柜次号、集装箱号或提单号' , props : { 'aria-label' : '关键词' , maxlength : 128 } },
2026-08-25 17:43:00 +08:00
{ key : 'containerType' , type : 'select' , label : '柜型' , width : 140 , placeholder : '请选择箱型' , options : CONTAINER_SPECIFICATIONS . map (( item ) => ({ label : item . type , value : item . type })), props : { 'aria-label' : '柜型' } },
2026-09-04 15:54:36 +08:00
{ key : 'status' , type : 'select' , label : '装柜状态' , width : 150 , placeholder : '请选择货柜状态' , options : [{ label : '待装柜' , value : 'WAIT_LOADING' }, { label : '预装柜中' , value : 'EDITING' }, { label : '拣货中' , value : 'LOADING' }, { label : '装柜完成' , value : 'COMPLETED' }], props : { 'aria-label' : '装柜状态' } },
2026-08-10 15:36:53 +08:00
{ key : 'sailingRange' , type : 'daterange' , label : '预计开船' , width : 260 , props : { 'aria-label' : '预计开船日期范围' } },
])
2026-09-08 19:57:42 +08:00
const allColumns : DataTableColumn [] = [
2026-08-11 11:15:44 +08:00
{ key : 'actions' , label : '操作' , width : 310 , fixed : 'left' , slotName : 'actions' },
2026-09-07 23:58:18 +08:00
{ key : 'emptyReturnWarning' , label : '报空预警' , width : 120 , slotName : 'emptyReturnWarning' },
2026-08-10 15:36:53 +08:00
{ key : 'containerNo' , label : '集装箱号' , minWidth : 150 , slotName : 'containerNo' },
2026-08-17 19:57:32 +08:00
{ key : 'planNo' , prop : 'planNo' , label : '装柜清单号' , minWidth : 165 , formatter : ( row : ContainerBookingResource ) => row . planNo || '—' },
2026-08-10 15:36:53 +08:00
{ key : 'planStatus' , label : '状态' , width : 110 , slotName : 'status' },
2026-08-17 19:57:32 +08:00
{ key : 'exceptionActive' , label : '异常' , width : 90 , formatter : ( row : ContainerBookingResource ) => row . exceptionActive ? '异常' : '正常' },
{ key : 'cabinetNo' , label : '柜次号' , minWidth : 180 , slotName : 'cabinet' },
{ key : 'destinationClearanceDocumentStatus' , label : '清关资料' , width : 110 , formatter : ( row : ContainerBookingResource ) => documentStatusText ( row . destinationClearanceDocumentStatus ) },
{ key : 'deliveryOrderDocumentStatus' , label : 'DO资料' , width : 100 , formatter : ( row : ContainerBookingResource ) => documentStatusText ( row . deliveryOrderDocumentStatus ) },
2026-09-08 19:57:42 +08:00
{ key : 'pickupCode' , label : 'PU码' , minWidth : 140 , formatter : ( row : ContainerBookingResource ) => row . pickupCode || '—' },
2026-08-17 19:57:32 +08:00
{ key : 'oceanBooking' , label : '真实订舱' , minWidth : 170 , slotName : 'oceanBooking' },
2026-08-10 15:36:53 +08:00
{ key : 'warehouseName' , prop : 'warehouseName' , label : '装柜仓库' , minWidth : 160 },
2026-09-07 23:58:18 +08:00
{ 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' },
2026-08-13 11:57:44 +08:00
{ key : 'billOfLadingNo' , prop : 'billOfLadingNo' , label : '提单号' , minWidth : 150 },
2026-08-10 15:36:53 +08:00
{ key : 'route' , label : '航线' , minWidth : 220 , slotName : 'route' },
{ key : 'containerType' , prop : 'containerType' , label : '柜型' , width : 100 },
{ key : 'capacity' , label : '容量上限' , minWidth : 150 , slotName : 'capacity' },
{ key : 'vessel' , label : '船名/航次' , minWidth : 180 , slotName : 'vessel' },
2026-08-13 11:57:44 +08:00
{ key : 'shippingCompanyName' , prop : 'shippingCompanyName' , label : '船公司' , minWidth : 150 },
{ key : 'loadingAt' , label : '装柜时间' , minWidth : 160 , slotName : 'loadingAt' },
2026-08-10 15:36:53 +08:00
{ key : 'estimatedSailingAt' , label : '预计开船' , minWidth : 160 , slotName : 'estimatedSailingAt' },
2026-08-13 11:57:44 +08:00
{ key : 'actualSailingAt' , label : '实际开船' , minWidth : 160 , slotName : 'actualSailingAt' },
{ key : 'estimatedPortArrivalAt' , label : '预计到港' , minWidth : 160 , slotName : 'estimatedPortArrivalAt' },
{ key : 'actualPortArrivalAt' , label : '实际到港' , minWidth : 160 , slotName : 'actualPortArrivalAt' },
2026-08-10 15:36:53 +08:00
]
2026-09-08 19:57:42 +08:00
const domesticColumnKeys = new Set ([ 'actions' , 'containerNo' , 'planNo' , 'planStatus' , 'exceptionActive' , 'cabinetNo' , 'oceanBooking' , 'warehouseName' , 'billOfLadingNo' , 'route' , 'containerType' , 'capacity' , 'vessel' , 'shippingCompanyName' , 'loadingAt' , 'estimatedSailingAt' ])
const overseasColumnKeys = new Set ([ 'actions' , 'emptyReturnWarning' , 'containerNo' , 'cabinetNo' , 'destinationClearanceDocumentStatus' , 'deliveryOrderDocumentStatus' , 'pickupCode' , 'terminalTruckingCompany' , 'pickupAppointmentAt' , 'actualPickupAt' , 'estimatedEmptyReturnAt' , 'actualEmptyReturnAt' , 'billOfLadingNo' , 'route' , 'vessel' , 'actualSailingAt' , 'estimatedPortArrivalAt' , 'actualPortArrivalAt' ])
2026-09-09 13:51:14 +08:00
const columns = computed (() => allColumns
. filter (( column ) => ( isDomestic . value ? domesticColumnKeys : overseasColumnKeys ). has ( String ( column . key )))
// 国内同时展示查看、运输信息、装柜流程和更多菜单,须为四个入口预留空间。
. map (( column ) => isDomestic . value && column . key === 'actions' ? { ... column , width : 380 , minWidth : 380 } : column ))
2026-09-07 23:58:18 +08:00
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 },
]
2026-08-10 15:36:53 +08:00
function optional ( value : string | null ) { return value ? . trim () || null }
2026-08-31 12:12:51 +08:00
function normalizedContainerNo ( value : string | null | undefined ) { return String ( value || '' ). trim (). toUpperCase () }
2026-08-11 11:52:30 +08:00
function statusText ( status : ContainerPlanStatus ) { return ({ WAIT_LOADING : '待装柜' , EDITING : '预装柜中' , LOADING : '拣货中' , COMPLETED : '装柜完成' })[ status ] }
2026-08-17 19:57:32 +08:00
function documentStatusText ( status : ContainerDocumentStatus | null | undefined ) { return status ? ({ MISSING : '未上传' , UPLOADED : '已上传' , COMPLETED : '已完成' })[ status ] : '—' }
2026-08-10 15:36:53 +08:00
function formatTime ( value : string | null | undefined ) { return value ? . replace ( 'T' , ' ' ). slice ( 0 , 16 ) || '—' }
2026-09-07 23:58:18 +08:00
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 || '—' }
2026-09-09 10:26:17 +08:00
function drayageActionText ( action : TerminalDrayageAction ) { return ({ SELECT_FLEET : '选择码头车队' , SCHEDULE_PICKUP : '预约提柜' , RECORD_PICKUP : '确认实际提柜' , RECORD_EMPTY_RETURN : '确认还柜' } as Record < TerminalDrayageAction , string >)[ action ] }
2026-09-08 19:57:42 +08:00
function hasAction ( row : ContainerWorkbenchRow , action : ContainerWorkbenchAction ) { return Boolean ( row . allowedActions ? . includes ( action )) }
function canDrayage ( row : ContainerWorkbenchRow , action : TerminalDrayageAction ) {
const permission = action === 'SELECT_FLEET' ? canFleetEdit . value : action === 'SCHEDULE_PICKUP' ? canPickupAppointmentEdit . value : action === 'RECORD_PICKUP' ? canPickupConfirm . value : canEmptyReturnConfirm . value
return Boolean ( isOverseas . value && permission && hasAction ( row , action ) && drayageFor ( row . id ) ? . allowedActions . includes ( action ))
}
2026-09-07 23:58:18 +08:00
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 )
}
2026-08-10 17:42:17 +08:00
function containerViewLabel ( row : ContainerBookingResource ) {
const reference = [ row . cabinetNo , row . containerNo , row . bookingNo ]
. find (( value ) => typeof value === 'string' && value . trim ())
return reference ? `查看 ${ reference } ` : '查看货柜详情'
}
2026-08-10 15:36:53 +08:00
function replaceForm ( source : ContainerBookingResource | null ) {
Object . assign ( form , emptyForm (), source || {})
2026-08-17 12:31:27 +08:00
const specification = containerSpecification ( form . containerType )
if ( specification ) applyContainerSpecification ( form , specification . type )
2026-08-25 19:56:37 +08:00
reconcileTruckingSupplier ()
}
function reconcileTruckingSupplier () {
const supplierId = optional ( form . terminalTruckingCompanyId )
const supplierName = optional ( form . terminalTruckingCompany ) || unresolvedTruckingSupplierName . value
const supplier = supplierId
? truckingCompanies . value . find (( item ) => item . id === supplierId )
: truckingCompanies . value . find (( item ) => item . name === supplierName )
selectedTruckingSupplierId . value = supplier ? . id || ''
unresolvedTruckingSupplierName . value = supplier ? null : supplierName
if ( supplier ) {
form . terminalTruckingCompanyId = supplier . id
form . terminalTruckingCompany = supplier . name
} else if ( ! supplierName ) {
form . terminalTruckingCompanyId = null
form . terminalTruckingCompany = null
} else {
form . terminalTruckingCompanyId = null
form . terminalTruckingCompany = supplierName
}
2026-08-10 15:36:53 +08:00
}
function payload () : ContainerSaveRequest {
const selectedWarehouse = warehouses . value . find (( warehouse ) => String ( warehouse . id ) === form . warehouseId )
2026-08-25 19:56:37 +08:00
const selectedTruckingSupplier = truckingCompanies . value . find (( supplier ) => supplier . id === selectedTruckingSupplierId . value )
2026-08-10 15:36:53 +08:00
return {
2026-08-31 12:12:51 +08:00
containerNo : optional ( detail . value ? . containerNo || null ),
2026-08-10 15:36:53 +08:00
warehouseId : form . warehouseId ,
warehouseName : selectedWarehouse ? . warehouseName || optional ( form . warehouseName ),
billOfLadingNo : optional ( form . billOfLadingNo ), vesselName : optional ( form . vesselName ), voyageNo : optional ( form . voyageNo ),
containerType : form . containerType ,
shippingCompanyId : optional ( form . shippingCompanyId ), shippingCompanyName : optional ( form . shippingCompanyName ),
departurePortCode : optional ( form . departurePortCode ), departurePortName : optional ( form . departurePortName ),
destinationPortCode : optional ( form . destinationPortCode ), destinationPortName : optional ( form . destinationPortName ),
destinationWarehouseCode : optional ( form . destinationWarehouseCode ), destinationWarehouseName : optional ( form . destinationWarehouseName ),
2026-08-25 19:56:37 +08:00
capacityCbm : form . capacityCbm , maxWeightKg : form . maxWeightKg ,
terminalTruckingCompanyId : selectedTruckingSupplier ? . id || null ,
terminalTruckingCompany : selectedTruckingSupplier ? . name || null ,
2026-08-10 15:36:53 +08:00
loadingAt : form . loadingAt , estimatedSailingAt : form . estimatedSailingAt , actualSailingAt : form . actualSailingAt ,
estimatedPortArrivalAt : form . estimatedPortArrivalAt , actualPortArrivalAt : form . actualPortArrivalAt ,
estimatedWarehouseArrivalAt : form . estimatedWarehouseArrivalAt , actualWarehouseArrivalAt : form . actualWarehouseArrivalAt ,
}
}
2026-08-11 18:06:35 +08:00
function partnerLabel ( partner : PlatformPartnerResource ) {
return ` ${ partner . partnerName }${ partner . partnerCode ? `( ${ partner . partnerCode } ) ` : '' } `
}
2026-08-15 11:12:22 +08:00
function selectTruckingSupplier ( supplierId : string ) {
2026-08-25 19:56:37 +08:00
if ( ! supplierId && unresolvedTruckingSupplierName . value ) return
2026-08-15 11:12:22 +08:00
const supplier = truckingCompanies . value . find (( item ) => item . id === supplierId )
selectedTruckingSupplierId . value = supplier ? . id || ''
2026-08-25 19:56:37 +08:00
unresolvedTruckingSupplierName . value = null
form . terminalTruckingCompanyId = supplier ? . id || null
2026-08-15 11:12:22 +08:00
form . terminalTruckingCompany = supplier ? . name || null
}
2026-08-11 18:06:35 +08:00
function portLabel ( port : PortOption ) {
return port . name && port . code ? ` ${ port . name } ( ${ port . code } ) ` : port . name || port . code || '未命名港口'
}
function selectShippingCompany ( partnerId : string ) {
if ( partnerId . startsWith ( 'name:' )) {
form . shippingCompanyId = null
form . shippingCompanyName = partnerId . slice ( 5 ) || null
return
}
const partner = partners . value . find (( item ) => String ( item . id ) === partnerId )
form . shippingCompanyId = partnerId || null
form . shippingCompanyName = partner ? . partnerName || null
}
function selectPort ( direction : 'departure' | 'destination' , value : string ) {
const option = ( direction === 'departure' ? departurePorts . value : destinationPorts . value ). find (( item ) => item . value === value )
if ( direction === 'departure' ) {
form . departurePortCode = option ? . code || null
form . departurePortName = option ? . name || null
} else {
form . destinationPortCode = option ? . code || null
form . destinationPortName = option ? . name || null
}
}
function selectDestinationWarehouse ( warehouseCode : string ) {
if ( warehouseCode . startsWith ( 'name:' )) {
form . destinationWarehouseCode = null
form . destinationWarehouseName = warehouseCode . slice ( 5 ) || null
return
}
const warehouse = destinationWarehouses . value . find (( item ) => item . warehouseCode === warehouseCode )
form . destinationWarehouseCode = warehouseCode || null
form . destinationWarehouseName = warehouse ? . warehouseName || null
}
2026-08-10 15:36:53 +08:00
async function loadWarehouses () {
2026-09-08 20:55:30 +08:00
if ( warehousesLoaded ) return
if ( warehouseOptionsPromise ) return warehouseOptionsPromise
warehouseOptionsPromise = ( async () => {
warehouseLoading . value = true ; warehouseError . value = null
try {
const records = await listAllPlatformWarehouses ({ status : 'ACTIVE' })
warehouses . value = records . filter (( warehouse ) => warehouse . warehouseType === 'DOMESTIC' )
destinationWarehouses . value = records . filter (( warehouse ) => warehouse . warehouseType !== 'DOMESTIC' )
warehousesLoaded = true
}
catch ( error ) { warehouseError . value = getApiErrorDetails ( error , '装柜仓库加载失败' ) }
finally { warehouseLoading . value = false }
})()
try { await warehouseOptionsPromise }
finally { warehouseOptionsPromise = null }
2026-08-10 15:36:53 +08:00
}
2026-08-14 12:50:42 +08:00
async function loadCustomers () {
customersLoading . value = true
try {
const result = await listPlatformTenants ({ page : 1 , size : 200 , status : 'ACTIVE' })
customers . value = result . records . filter (( customer ) => customer . status === 'ACTIVE' )
} catch {
customers . value = []
} finally {
customersLoading . value = false
}
}
2026-08-11 18:06:35 +08:00
async function loadBusinessOptions () {
2026-09-08 20:55:30 +08:00
if ( partnersLoaded && truckingSuppliersLoaded && oceanBookingsLoaded ) return
if ( businessOptionsPromise ) return businessOptionsPromise
businessOptionsPromise = ( async () => {
businessOptionsLoading . value = true
businessOptionsError . value = ''
const errors : string [] = []
const [ partnerResult , supplierResult , bookingResult ] = await Promise . allSettled ([
partnersLoaded ? Promise . resolve ( partners . value ) : loadAllPartners (),
loadTruckingSuppliers (),
oceanBookingsLoaded ? Promise . resolve ( oceanBookings . value ) : loadAllOceanBookings (),
])
if ( partnerResult . status === 'fulfilled' ) { partners . value = partnerResult . value ; partnersLoaded = true }
else errors . push ( getApiErrorDetails ( partnerResult . reason , '船公司与拖车公司加载失败' ). message )
if ( supplierResult . status === 'fulfilled' ) {
if ( form . id ) reconcileTruckingSupplier ()
}
else errors . push ( getApiErrorDetails ( supplierResult . reason , '拖车供应商加载失败' ). message )
if ( bookingResult . status === 'fulfilled' ) { oceanBookings . value = bookingResult . value ; oceanBookingsLoaded = true }
else errors . push ( getApiErrorDetails ( bookingResult . reason , '港口候选加载失败' ). message )
businessOptionsError . value = errors . join ( '; ' )
businessOptionsLoading . value = false
})()
try { await businessOptionsPromise }
finally { businessOptionsPromise = null }
}
async function loadTruckingSuppliers () : Promise < Supplier [] > {
if ( truckingSuppliersLoaded ) return truckingSuppliers . value
if ( truckingSuppliersPromise ) return truckingSuppliersPromise
truckingSuppliersPromise = ( async () => {
const result = await listSuppliers ({ page : 1 , size : 200 , category : 'TRUCKING' , status : 'ACTIVE' })
const records = result . records . filter (( supplier ) => supplier . category === 'TRUCKING' && supplier . status === 'ACTIVE' )
truckingSuppliers . value = records
truckingSuppliersLoaded = true
return records
})()
try { return await truckingSuppliersPromise }
finally { truckingSuppliersPromise = null }
}
async function ensureTruckingSuppliers ( open : unknown = true ) {
if ( ! open ) return false
try { await loadTruckingSuppliers (); return true }
catch ( error ) { ElMessage . error ( getApiErrorDetails ( error , '拖车供应商加载失败' ). message ); return false }
2026-08-11 18:06:35 +08:00
}
async function loadAllPartners () {
const firstPage = await listPlatformPartners ({ page : 1 , size : 200 , status : 'ACTIVE' })
if ( firstPage . pages <= 1 ) return firstPage . records
const remaining = await Promise . all ( Array . from ({ length : firstPage . pages - 1 }, ( _ , index ) => listPlatformPartners ({ page : index + 2 , size : 200 , status : 'ACTIVE' })))
return [ firstPage , ... remaining ]. flatMap (( pageResult ) => pageResult . records )
}
async function loadAllOceanBookings () {
const firstPage = await listOceanBookings ({ page : 1 , size : 200 })
if ( firstPage . pages <= 1 ) return firstPage . records
const remaining = await Promise . all ( Array . from ({ length : firstPage . pages - 1 }, ( _ , index ) => listOceanBookings ({ page : index + 2 , size : 200 })))
return [ firstPage , ... remaining ]. flatMap (( pageResult ) => pageResult . records )
}
2026-09-08 20:55:30 +08:00
async function openSimulation () {
2026-08-17 12:31:27 +08:00
Object . assign ( simulationForm , { channelCode : '' , warehouseId : '' , containerType : '40HC' , capacityCbm : 76 , maxWeightKg : 28000 })
2026-08-14 20:59:19 +08:00
simulationChannels . value = []
simulationError . value = null
simulationOpen . value = true
2026-09-08 20:55:30 +08:00
await loadWarehouses ()
if ( warehouseError . value ) simulationError . value = warehouseError . value
2026-08-14 20:59:19 +08:00
}
2026-08-17 12:31:27 +08:00
function selectSimulationContainerType ( containerType : string ) {
applyContainerSpecification ( simulationForm , containerType )
}
2026-08-14 20:59:19 +08:00
async function selectSimulationWarehouse ( value : unknown ) {
simulationForm . warehouseId = String ( value || '' )
simulationForm . channelCode = ''
simulationChannels . value = []
simulationError . value = null
if ( ! simulationForm . warehouseId ) return
simulationChannelsLoading . value = true
try { simulationChannels . value = await listWarehouseLogisticsChannels ( simulationForm . warehouseId ) }
catch ( error ) { simulationError . value = getApiErrorDetails ( error , '物流渠道加载失败' ) }
finally { simulationChannelsLoading . value = false }
}
async function saveSimulation () {
if ( ! ( await simulationFormRef . value ? . validate (). catch (() => false ))) return
simulationSaving . value = true
simulationError . value = null
try {
await createContainerSimulation ({ ... simulationForm })
simulationOpen . value = false
ElMessage . success ( '模拟柜已创建,可先排货后关联真实订舱柜' )
await loadRows ()
} catch ( error ) { simulationError . value = getApiErrorDetails ( error , '模拟柜创建失败' ) }
finally { simulationSaving . value = false }
}
async function openLink ( source : object ) {
2026-08-25 19:56:37 +08:00
if ( linkLoading . value ) return
2026-08-14 20:59:19 +08:00
const row = source as ContainerBookingResource
2026-08-25 19:56:37 +08:00
const warehouseId = optional ( row . warehouseId )
const channelCode = optional ( row . channelCode )
2026-08-25 20:18:59 +08:00
if ( ! warehouseId || ! channelCode || ! optional ( row . containerType )) {
2026-08-25 19:56:37 +08:00
linkTarget . value = row
linkCandidates . value = []
2026-08-25 20:18:59 +08:00
linkError . value = { message : '当前模拟柜缺少仓库、渠道或柜型,无法查询可关联订舱' }
2026-08-25 19:56:37 +08:00
linkOpen . value = true
return
}
const sequence = ++ linkSequence
2026-08-14 20:59:19 +08:00
linkTarget . value = row
linkCandidateKey . value = ''
2026-08-18 16:09:43 +08:00
linkContainerNo . value = ''
2026-08-14 20:59:19 +08:00
linkCandidates . value = []
linkError . value = null
linkOpen . value = true
linkLoading . value = true
try {
2026-08-25 19:56:37 +08:00
const result = await listOceanBookingLinkCandidates ({
warehouseId , channelCode , containerType : row . containerType ,
page : 1 , size : 200 ,
})
if ( sequence !== linkSequence || linkTarget . value ? . id !== row . id ) return
linkCandidates . value = result . records . map (( candidate ) => ({
key : ` ${ candidate . oceanBookingId } : ${ candidate . requirementId } ` ,
oceanBookingId : candidate . oceanBookingId ,
requirementId : candidate . requirementId ,
disabled : false ,
label : ` ${ candidate . bookingNo } · ${ candidate . containerType } × ${ candidate . remainingQuantity } · ${ candidate . customerName } · ${ candidate . warehouseName } · ${ candidate . channelName } ` ,
}))
} catch ( error ) {
if ( sequence === linkSequence ) linkError . value = getApiErrorDetails ( error , '已放舱订舱候选加载失败' )
} finally {
if ( sequence === linkSequence ) linkLoading . value = false
}
2026-08-14 20:59:19 +08:00
}
async function saveLink () {
const target = linkTarget . value
const candidate = linkCandidates . value . find (( item ) => item . key === linkCandidateKey . value )
2026-09-01 15:24:37 +08:00
const normalized = normalizedContainerNo ( linkContainerNo . value )
const containerNo = normalized || null
linkContainerNo . value = normalized
2026-08-18 16:09:43 +08:00
if ( ! target || ! candidate || candidate . disabled ) { linkError . value = { message : '请选择规格一致且仍有剩余数量的柜型需求' }; return }
2026-08-14 20:59:19 +08:00
linkSaving . value = true
linkError . value = null
try {
2026-08-18 16:09:43 +08:00
await linkContainerOceanBooking ( target . id , { oceanBookingId : candidate . oceanBookingId , requirementId : candidate . requirementId , containerNo , expectedVersion : target . version })
2026-08-14 20:59:19 +08:00
linkOpen . value = false
2026-09-01 15:24:37 +08:00
ElMessage . success ( containerNo ? '订舱与真实集装箱号已关联,柜次号、装柜计划和明细保持不变' : '订舱已关联,真实集装箱号待司机提柜时录入' )
2026-08-14 20:59:19 +08:00
await loadRows ()
} catch ( error ) { linkError . value = getApiErrorDetails ( error , '关联真实订舱柜失败' ) }
finally { linkSaving . value = false }
}
2026-09-07 23:58:18 +08:00
function replaceDrayage ( value : TerminalDrayageView ) {
drayages . value = { ... drayages . value , [ value . containerBookingId ] : value }
}
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 )
2026-09-08 20:55:30 +08:00
if ( ! ( await ensureTruckingSuppliers ())) return
2026-09-07 23:58:18 +08:00
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'
2026-09-09 10:26:17 +08:00
? await recordTerminalDrayagePickup ( row . id , { expectedVersion : current . version , actualAt : drayageActionAt . value , remark : reason })
: await recordTerminalDrayageEmptyReturn ( row . id , { expectedVersion : current . version , actualAt : drayageActionAt . value , remark : reason })
2026-09-07 23:58:18 +08:00
replaceDrayage ( next ); drayageActionOpen . value = false
ElMessage . success ( ` ${ drayageActionText ( drayageAction . value ) } 成功` )
2026-09-09 10:53:37 +08:00
await loadRows ()
2026-09-07 23:58:18 +08:00
} 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 }
}
2026-08-10 15:36:53 +08:00
async function loadRows () {
const sequence = ++ listSequence
loading . value = true ; loadError . value = null
const [ from , to ] = applied . value . sailingRange || []
try {
const result = await listContainers ({
2026-08-14 12:50:42 +08:00
page : page . value , size : pageSize . value , customerId : applied . value . customerId || undefined , keyword : applied . value . keyword . trim () || undefined ,
2026-08-11 11:15:44 +08:00
containerType : applied . value . containerType || undefined ,
2026-08-10 15:36:53 +08:00
status : applied . value . status , estimatedSailingFrom : from ? ` ${ from } T00:00:00` : undefined ,
2026-09-08 19:57:42 +08:00
estimatedSailingTo : to ? ` ${ to } T23:59:59` : undefined , scope : scope . value ,
}, props . workbench )
2026-08-10 15:36:53 +08:00
if ( sequence !== listSequence ) return
2026-09-08 19:57:42 +08:00
rows . value = result . records as ContainerWorkbenchRow []; total . value = result . total
drayages . value = Object . fromEntries ( rows . value . flatMap (( row ) => row . terminalDrayage ? [[ row . id , row . terminalDrayage as TerminalDrayageView ]] : []))
2026-08-10 15:36:53 +08:00
} catch ( error ) {
if ( sequence !== listSequence ) return
rows . value = []; total . value = 0 ; loadError . value = getApiErrorDetails ( error , '货柜列表加载失败,请稍后重试' )
} finally { if ( sequence === listSequence ) loading . value = false }
}
2026-08-10 18:12:23 +08:00
function search () { applied . value = { ... filters . value , sailingRange : [... filters . value . sailingRange ] }; if ( page . value === 1 ) void loadRows (); else page . value = 1 }
2026-08-18 17:40:40 +08:00
function openDetail ( source : object ) {
2026-08-10 15:36:53 +08:00
const row = source as ContainerBookingResource
2026-09-08 19:57:42 +08:00
void router . push ({ name : props . workbench === 'DOMESTIC' ? 'domestic-container-detail' : 'overseas-container-detail' , params : { containerId : row . id } })
2026-08-10 15:36:53 +08:00
}
2026-08-13 11:57:44 +08:00
function handleMoreAction ( command : string , source : object ) {
const row = source as ContainerBookingResource
2026-08-14 20:59:19 +08:00
if ( command === 'link' ) { void openLink ( row ); return }
2026-08-18 17:40:40 +08:00
if ( command === 'overview' ) { openDetail ( row ); return }
2026-09-07 23:58:18 +08:00
if ( command === 'drayage-history' ) { void openDrayageHistory ( row ); return }
if ( command . startsWith ( 'drayage:' )) { openDrayageAction ( row , command . slice ( 'drayage:' . length ) as Exclude < TerminalDrayageAction , ' SELECT_FLEET ' >); return }
2026-09-08 19:57:42 +08:00
if ( command === 'documents' && isOverseas . value && hasAction ( row as ContainerWorkbenchRow , 'MANAGE_DESTINATION_DOCUMENTS' )) void router . push ({ name : 'overseas-container-detail' , params : { containerId : row . id }, query : { action : command } })
2026-08-13 11:57:44 +08:00
}
2026-08-10 15:36:53 +08:00
async function openEdit ( source : object ) {
const row = source as ContainerBookingResource
editorError . value = null
2026-08-31 12:12:51 +08:00
try {
2026-09-08 20:55:30 +08:00
const [ latest ] = await Promise . all ([ getContainer ( row . id , props . workbench ), loadWarehouses (), loadBusinessOptions ()])
2026-09-08 19:57:42 +08:00
if ( ! isDomestic . value || ! hasAction ( latest , 'EDIT_CONTAINER' )) return
2026-08-31 12:12:51 +08:00
detail . value = latest
replaceForm ( latest )
editorOpen . value = true
}
2026-08-10 15:36:53 +08:00
catch ( error ) { ElMessage . error ( getApiErrorDetails ( error , '货柜详情加载失败' ). message ) }
}
async function save () {
2026-08-25 19:56:37 +08:00
if ( unresolvedTruckingSupplierName . value ) {
editorError . value = { message : '原拖车供应商已不可用,请重新选择合作中的拖车供应商' }
return
}
2026-08-10 15:36:53 +08:00
if ( ! ( await formRef . value ? . validate (). catch (() => false ))) return
saving . value = true
editorError . value = null
try {
2026-08-31 12:12:51 +08:00
await updateContainer ( form . id , form . version , payload ())
2026-08-13 11:57:44 +08:00
ElMessage . success ( '货柜已更新' ); editorOpen . value = false ; await loadRows ()
2026-08-10 15:36:53 +08:00
} catch ( error ) {
const details = getApiErrorDetails ( error , '保存失败' )
2026-08-11 11:15:44 +08:00
if ( details . code === 'A0409' && form . id ) {
const submitted = { ... form }
try {
2026-09-08 19:57:42 +08:00
const latest = await getContainer ( form . id , props . workbench )
2026-08-11 11:15:44 +08:00
detail . value = latest
replaceForm ( latest )
Object . assign ( form , submitted , { id : latest . id , version : latest . version })
editorError . value = { ... details , message : '数据已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' }
ElMessage . warning ( editorError . value . message )
} catch ( refreshError ) {
editorError . value = getApiErrorDetails ( refreshError , '数据已被其他人修改,且最新数据刷新失败,请稍后重试' )
ElMessage . error ( editorError . value . message )
}
} else {
editorError . value = details
ElMessage . error ( details . message )
}
2026-08-10 15:36:53 +08:00
} finally { saving . value = false }
}
2026-08-11 11:15:44 +08:00
function goContainerProcess ( source : object ) {
const row = source as ContainerBookingResource
if ( row . planStatus === 'EDITING' ) {
void router . push ({ name : 'smart-loading-workbench' , params : { containerId : row . id } })
return
}
void router . push ({
2026-09-08 19:57:42 +08:00
name : row . planStatus === 'LOADING' ? 'loading-plans' : 'domestic-container-detail' ,
params : row . planStatus === 'COMPLETED' ? { containerId : row . id } : undefined ,
query : row . planStatus === 'COMPLETED' ? { tab : 'loading-list' } : { planId : row . planId || undefined , warehouseId : row . warehouseId },
2026-08-11 11:15:44 +08:00
})
}
2026-08-10 15:36:53 +08:00
2026-08-10 18:21:40 +08:00
watch ([ page , pageSize ], ([ nextPage , nextSize ], [ previousPage , previousSize ]) => {
if ( nextPage !== previousPage || nextSize !== previousSize ) void loadRows ()
})
2026-09-08 19:57:42 +08:00
watch ( scope , () => { if ( page . value === 1 ) void loadRows (); else page . value = 1 })
2026-08-25 19:56:37 +08:00
watch ( linkOpen , ( open ) => {
if ( open ) return
linkSequence += 1
linkLoading . value = false
})
2026-08-31 12:40:01 +08:00
onMounted ( async () => {
2026-09-08 20:55:30 +08:00
await Promise . all ([ loadCustomers (), loadRows ()])
2026-08-31 12:40:01 +08:00
const linkContainerId = typeof route . query . linkContainerId === 'string' ? route . query . linkContainerId : ''
2026-09-08 19:57:42 +08:00
if ( ! isDomestic . value || ! linkContainerId || ! canEdit . value ) return
2026-08-31 12:40:01 +08:00
try {
2026-09-08 19:57:42 +08:00
const target = await getContainer ( linkContainerId , 'DOMESTIC' )
if ( target . sourceType !== 'SIMULATION' || target . oceanBookingId || ! hasAction ( target , 'EDIT_CONTAINER' )) {
2026-08-31 12:40:01 +08:00
ElMessage . warning ( '当前货柜已关联订舱或所处阶段不允许关联' )
return
}
await openLink ( target )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '关联货柜加载失败' ). message )
} finally {
2026-09-08 19:57:42 +08:00
void router . replace ({ name : 'domestic-container-workbench' })
2026-08-31 12:40:01 +08:00
}
})
2026-08-10 15:36:53 +08:00
</ script >
< template >
2026-08-10 16:24:54 +08:00
< AdminPage as = "div" >
< section class = "container-page" >
2026-09-08 19:57:42 +08:00
< ListPageLayout : ariaLabel = "isDomestic ? '送柜报关工作台' : '清关提柜工作台'" >
2026-08-10 16:24:54 +08:00
< template # toolbar >
2026-09-08 20:55:30 +08:00
< div class = "container-query-panel" >
< div class = "container-scope-tabs" role = "tablist" aria -label =" 货柜完结状态 " >
< button type = "button" role = "tab" aria -controls =" container -workbench-results " : aria -selected =" scope = == ' ACTIVE '" : tabindex = "scope === 'ACTIVE' ? 0 : -1" : class = "['container-scope-tab', { 'is-active': scope === 'ACTIVE' }]" @click ="scope = 'ACTIVE'" > 未完结 </ button >
< button type = "button" role = "tab" aria -controls =" container -workbench-results " : aria -selected =" scope = == ' HISTORY '" : tabindex = "scope === 'HISTORY' ? 0 : -1" : class = "['container-scope-tab', { 'is-active': scope === 'HISTORY' }]" @click ="scope = 'HISTORY'" > 已完结 </ button >
</ div >
< QueryToolbar v-model = "filters" class="container-query-toolbar" ariaLabel="货柜筛选" :fields="queryFields" preference-key="container-loading-query" @search="search" />
</ div >
2026-08-10 16:24:54 +08:00
</ template >
2026-08-11 11:15:44 +08:00
< template v-if = "loadError" # state >
< LoadErrorState title = "货柜列表加载失败" :message = "loadError.message" :loading = "loading" @retry ="loadRows" >< template # details >< ApiErrorMeta :details = "loadError" /></ template ></ LoadErrorState >
2026-08-10 16:24:54 +08:00
</ template >
2026-09-08 20:55:30 +08:00
< section v-if = "!loadError" id="container-workbench-results" class="surface-panel table-panel" >
2026-08-12 18:16:44 +08:00
< 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 =" 暂无货柜 " >
2026-09-08 20:55:30 +08:00
< template # toolbar -actions >< el-button v-if = "isDomestic && canCreate" type="primary" @click="openSimulation"><Plus :size="16" /> 新增模拟柜 </ el-button ></ template >
2026-09-09 10:26:17 +08:00
< 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><template v-if="isDomestic"><el-button v-if="canEdit && hasAction(row as ContainerWorkbenchRow, 'EDIT_CONTAINER')" 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></template><el-button v-if="canDrayage(row as ContainerWorkbenchRow, 'RECORD_EMPTY_RETURN')" size="small" type="primary" @click="openDrayageAction(row as ContainerBookingResource, 'RECORD_EMPTY_RETURN')">确认还柜</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><template v-if="isOverseas"><el-dropdown-item command="drayage-history"><FileText :size="15" />码头拖车记录</el-dropdown-item><el-dropdown-item v-if="canDrayage(row as ContainerWorkbenchRow, 'SCHEDULE_PICKUP')" command="drayage:SCHEDULE_PICKUP">预约提柜</el-dropdown-item><el-dropdown-item v-if="canDrayage(row as ContainerWorkbenchRow, 'RECORD_PICKUP')" command="drayage:RECORD_PICKUP">确认实际提柜</el-dropdown-item><el-dropdown-item v-if="canOverseasDocumentEdit && hasAction(row as ContainerWorkbenchRow, 'MANAGE_DESTINATION_DOCUMENTS')" command="documents"><FileUp :size="15" />清关 / DO / PU</el-dropdown-item></template><el-dropdown-item v-if="isDomestic && canEdit && hasAction(row as ContainerWorkbenchRow, 'EDIT_CONTAINER') && row.sourceType === 'SIMULATION' && !row.oceanBookingId" command="link"><Link2 :size="15" /> 关联已放舱订舱 </ el-dropdown-item ></ el-dropdown-menu ></ template ></ el-dropdown ></ div ></ template >
2026-09-07 23:58:18 +08:00
< template # emptyReturnWarning = "{ row }" >< el-tag : type = "drayageWarningType(row as ContainerBookingResource)" effect = "light" : title = "drayageWarningTitle(row as ContainerBookingResource)" >{{ drayageWarningText ( row as ContainerBookingResource ) }}</ el-tag ></ template >
2026-08-15 17:04:00 +08:00
< template # cabinet = "{ row }" >< strong >{{ row . cabinetNo }}</ strong >< small > 系统生成 </ small ></ template >
2026-09-01 15:24:37 +08:00
< 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 >
2026-09-08 20:55:30 +08:00
< 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)" @ visible -change =" ensureTruckingSuppliers " @ 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 >
2026-09-07 23:58:18 +08:00
< 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 >
2026-08-10 16:24:54 +08:00
< 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 >
< template # vessel = "{ row }" >{{ [ row . vesselName , row . voyageNo ]. filter ( Boolean ). join ( ' / ' ) || '—' }}</ template >
2026-08-13 11:57:44 +08:00
< template # loadingAt = "{ row }" >{{ formatTime ( row . loadingAt ) }}</ template >
2026-08-10 16:24:54 +08:00
< template # estimatedSailingAt = "{ row }" >{{ formatTime ( row . estimatedSailingAt ) }}</ template >
2026-09-08 19:57:42 +08:00
< template # actualSailingAt = "{ row }" >{{ formatTime ( row . actualFacts ? . actualSailingAt ) }}</ template >
2026-08-13 11:57:44 +08:00
< template # estimatedPortArrivalAt = "{ row }" >{{ formatTime ( row . estimatedPortArrivalAt ) }}</ template >
2026-09-08 19:57:42 +08:00
< template # actualPortArrivalAt = "{ row }" >{{ formatTime ( row . actualFacts ? . actualPortArrivalAt ) }}</ template >
2026-08-10 16:24:54 +08:00
</ DataTable >
</ section >
</ ListPageLayout >
2026-08-10 15:36:53 +08:00
2026-08-14 20:59:19 +08:00
< AppDialog v-model = "simulationOpen" title="新增模拟柜" width="min(560px, calc(100vw - 28px))" confirm-text="创建模拟柜" cancel-text="取消" :confirm-loading="simulationSaving" @confirm="saveSimulation" >
2026-08-15 17:04:00 +08:00
< el-alert title = "模拟柜用于先排货,服务端会生成稳定的柜次号;此处不填写集装箱号和船期。" type = "info" :closable = "false" />
2026-08-14 20:59:19 +08:00
< el-form ref = "simulationFormRef" :model = "simulationForm" :rules = "simulationRules" label -position =" top " class = "simulation-form" >
< el-form-item label = "装柜仓库" prop = "warehouseId" >< el-select :model-value = "simulationForm.warehouseId" filterable placeholder = "请选择国内装柜仓库" :loading = "warehouseLoading" @ update :model-value = "selectSimulationWarehouse" >< el-option v-for = "warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" /></ el-select ></ el-form-item >
< el-form-item label = "物流渠道" prop = "channelCode" >< el-select v-model = "simulationForm.channelCode" filterable :disabled="!simulationForm.warehouseId || simulationChannelsLoading" :loading="simulationChannelsLoading" :placeholder="simulationForm.warehouseId ? '请选择物流渠道' : '请先选择装柜仓库'" no-data-text="当前仓库暂无有效渠道"><el-option v-for="channel in simulationChannels" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode" /></ el-select ></ el-form-item >
2026-08-17 12:31:27 +08:00
< el-form-item label = "柜型" prop = "containerType" >< el-select :model-value = "simulationForm.containerType" @ update : model -value =" selectSimulationContainerType ( String ($ event || ''))" >< el-option v-for = "item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></ el-select ></ el-form-item >
< el-form-item label = "固定规格" >< el-input : model -value ="`$ { containerSpecification ( simulationForm.containerType )? .capacityCbm || '—' } m ³ / $ { containerSpecification ( simulationForm.containerType )? .maxWeightKg || '—' } kg `" disabled /></ el-form-item >
2026-08-14 20:59:19 +08:00
</ el-form >
< LoadErrorState v-if = "simulationError" compact title="模拟柜创建失败" :message="simulationError.message" :show-retry="false"><template #details><ApiErrorMeta :details="simulationError" /></ template ></ LoadErrorState >
</ AppDialog >
2026-09-01 15:24:37 +08:00
< AppDialog v-model = "linkOpen" title="关联已放舱订舱" width="min(680px, calc(100vw - 28px))" confirm-text="确认关联" cancel-text="取消" :confirm-loading="linkSaving" :confirm-disabled="!linkCandidateKey" @confirm="saveLink" >
< el-alert title = "关联后保留当前柜次号、装柜计划和明细,并补充订舱快照;真实集装箱号可待司机提柜时录入。" type = "info" :closable = "false" />
< el-form label -position =" top " >< el-form-item label = "柜次号" >< el-input : model -value =" linkTarget ? .cabinetNo || ''" disabled /></ el-form-item >< el-form-item label = "已放舱订舱及柜型需求" required >< el-select v-model = "linkCandidateKey" filterable :loading="linkLoading" placeholder="请选择仍有剩余数量的柜型需求" no-data-text="暂无符合当前装柜仓库、物流渠道和柜型的已放舱需求"><el-option v-for="candidate in linkCandidates" :key="candidate.key" :label="candidate.label" :value="candidate.key" :disabled="candidate.disabled" /></el-select></el-form-item><el-form-item label="真实集装箱号(可选)"><el-input v-model="linkContainerNo" maxlength="64" placeholder="如未取得可留空,后续显示待补录" @blur="linkContainerNo = normalizedContainerNo(linkContainerNo)" /></ el-form-item ></ el-form >
2026-08-25 20:18:59 +08:00
< p class = "dialog-hint" > 仓库 、 渠道和柜型必须与当前模拟柜一致 ; 容量和限重仅展示柜型固定快照 , 不影响关联资格 。 </ p >
2026-08-14 20:59:19 +08:00
< LoadErrorState v-if = "linkError" compact title="关联失败" :message="linkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="linkError" /></ template ></ LoadErrorState >
</ AppDialog >
2026-08-31 12:12:51 +08:00
< AppDialog v-model = "editorOpen" class="container-editor-dialog" :title="editorTitle" width="min(820px, calc(100vw - 28px))" :confirm-loading="saving" :confirm-disabled="Boolean(warehouseError)" confirm-text="确定" cancel-text="关闭" @confirm="save" >
2026-08-10 15:36:53 +08:00
< LoadErrorState v-if = "editorError" title="货柜保存失败" :message="editorError.message" compact :retry-text="form.id ? '刷新最新数据' : '重试保存'" @retry="form.id ? openEdit({ id: form.id }) : save()"><template #details><ApiErrorMeta :details="editorError" /></ template ></ LoadErrorState >
2026-08-31 12:12:51 +08:00
< LoadErrorState v-if = "warehouseError" title="装柜仓库加载失败" :message="warehouseError.message" compact @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></ template ></ LoadErrorState >
< el-alert v-if = "businessOptionsError" type="warning" :closable="false" show-icon :title="businessOptionsError"><el-button link type="warning" :loading="businessOptionsLoading" @click="loadBusinessOptions" > 重新加载业务选项 </ el -button ></ el-alert >
< el-form v-if = "!warehouseError" ref="formRef" :model="form" :rules="rules" label-position="top" class="form-grid" >
2026-09-01 15:24:37 +08:00
< el-form-item label = "真实集装箱号" >< el-input : model -value =" form.containerNo || ' 待补录 '" disabled />< small > 货柜基础资料不提供柜号写入 ; 请在拖车提柜或预装详情中补录 , 后续纠错也在预装详情完成 。 </ small ></ el-form-item >
2026-08-19 09:42:12 +08:00
< el-form-item label = "柜次号" >< el-input :model-value = "form.cabinetNo" disabled placeholder = "系统生成" /></ el-form-item >
2026-08-31 12:12:51 +08:00
< el-form-item label = "装柜仓库" prop = "warehouseId" >< el-select v-model = "form.warehouseId" filterable placeholder="请选择装柜仓库" :loading="warehouseLoading" loading-text="仓库加载中" no-data-text="暂无可用国内仓库"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="`${warehouse.warehouseName}( ${warehouse.warehouseCode}) `" :value="String(warehouse.id)" /></ el-select ></ el-form-item >
< el-form-item label = "提单号" >< el-input v-model = "form.billOfLadingNo" maxlength="128" placeholder="请输入提单号(可选)" /></ el-form-item >
< el-form-item label = "船名" >< el-input v-model = "form.vesselName" maxlength="128" placeholder="请输入船名" /></ el-form-item >
2026-08-11 18:06:35 +08:00
< el-form-item label = "航次" >< el-input v-model = "form.voyageNo" maxlength="64" placeholder="请输入航次" /></ el-form-item >
2026-08-31 12:12:51 +08:00
< el-form-item label = "箱型(货柜类型)" prop = "containerType" >< el-select :model-value = "form.containerType" placeholder = "请选择箱型" @ update : model -value =" applyContainerSpecification ( form , String ($ event || ''))" >< el-option v-for = "item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></ el-select ></ el-form-item >
< el-form-item label = "船公司" >< el-select :model-value = "shippingCompanyValue" filterable clearable placeholder = "请选择船公司" :loading = "businessOptionsLoading" loading -text =" 船公司加载中 " no -data-text =" 暂无可用船公司 " @ update : model -value =" selectShippingCompany ( String ($ event || ''))" >< el-option v-if = "shippingCompanyValue && !shippingCompanies.some((partner) => String(partner.id) === shippingCompanyValue)" :label="form.shippingCompanyName || '原船公司'" :value="shippingCompanyValue" /><el-option v-for="partner in shippingCompanies" :key="String(partner.id)" :label="partnerLabel(partner)" :value="String(partner.id)" /></ el-select ></ el-form-item >
< el-form-item label = "起运港" >< el-select :model-value = "departurePortValue" filterable clearable placeholder = "请选择起运港" :loading = "businessOptionsLoading" loading -text =" 港口加载中 " no -data-text =" 暂无订舱港口数据 " @ update : model -value =" selectPort (' departure ', String ($ event || ''))" >< el-option v-for = "port in departurePorts" :key="port.value" :label="portLabel(port)" :value="port.value" /></ el-select ></ el-form-item >
< el-form-item label = "目的港" >< el-select :model-value = "destinationPortValue" filterable clearable placeholder = "请选择目的港" :loading = "businessOptionsLoading" loading -text =" 港口加载中 " no -data-text =" 暂无订舱港口数据 " @ update : model -value =" selectPort (' destination ', String ($ event || ''))" >< el-option v-for = "port in destinationPorts" :key="port.value" :label="portLabel(port)" :value="port.value" /></ el-select ></ el-form-item >
< el-form-item label = "目的仓" >< el-select :model-value = "destinationWarehouseValue" filterable clearable placeholder = "请选择目的仓" :loading = "warehouseLoading" loading -text =" 目的仓加载中 " no -data-text =" 暂无可用海外 / FBA仓库 " @ update : model -value =" selectDestinationWarehouse ( String ($ event || ''))" >< el-option v-if = "destinationWarehouseValue && !destinationWarehouses.some((warehouse) => warehouse.warehouseCode === destinationWarehouseValue)" :label="form.destinationWarehouseName || form.destinationWarehouseCode || '原目的仓'" :value="destinationWarehouseValue" /><el-option v-for="warehouse in destinationWarehouses" :key="String(warehouse.id)" :label="`${warehouse.warehouseName}( ${warehouse.warehouseCode}) `" :value="warehouse.warehouseCode" /></ el-select ></ el-form-item >
2026-08-17 12:31:27 +08:00
< el-form-item label = "货柜容量(m³)" >< el-input : model -value =" form.capacityCbm == null ? '—' : `$ { form.capacityCbm } m ³`" disabled /></ el-form-item >
< el-form-item label = "重量上限(kg) " >< el-input : model -value =" form.maxWeightKg == null ? '—' : `$ { form.maxWeightKg } kg `" disabled /></ el-form-item >
2026-08-31 12:12:51 +08:00
< el-form-item label = "拖车公司" prop = "terminalTruckingCompany" >< el-select :model-value = "selectedTruckingSupplierId" filterable clearable placeholder = "请选择拖车供应商" :loading = "businessOptionsLoading" loading -text =" 拖车供应商加载中 " no -data-text =" 暂无可用拖车供应商 " @ update : model -value =" selectTruckingSupplier ( String ($ event || ''))" >< el-option v-if = "form.terminalTruckingCompany && !selectedTruckingSupplierId" :label="`${form.terminalTruckingCompany}(需重新选择)`" value="" disabled /><el-option v-for="supplier in truckingCompanies" :key="supplier.id" :label="supplier.name" :value="supplier.id" /></ el-select ></ el-form-item >
< el-form-item label = "计划装柜时间" >< el-date-picker v-model = "form.loadingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择计划装柜时间" /></ el-form-item >
< el-form-item label = "预计开船时间" >< el-date-picker v-model = "form.estimatedSailingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计开船时间" /></ el-form-item >
< el-form-item label = "预计到港时间" >< el-date-picker v-model = "form.estimatedPortArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" /></ el-form-item >
2026-08-10 15:36:53 +08:00
</ el-form >
2026-08-12 16:31:46 +08:00
</ AppDialog >
2026-09-07 23:58:18 +08:00
< 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 >
2026-08-10 16:24:54 +08:00
</ section >
</ AdminPage >
2026-08-10 15:36:53 +08:00
</ template >
< style scoped >
2026-09-08 20:55:30 +08:00
. container - page { display : grid ; gap : 16 px ; min - width : 0 ; }. container - page small { display : block ; margin - top : 4 px ; color : var ( -- cc - muted ); }. container - query - panel { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }. container - scope - tabs { display : flex ; max - width : 100 % ; overflow - x : auto ; margin - bottom : 0 ; border - bottom : 1 px solid # e5e7eb ; background : # f9fafb ; scrollbar - width : thin ; }. container - scope - tab { position : relative ; flex : 0 0 auto ; min - height : 44 px ; padding : 0 18 px ; border : 0 ; background : transparent ; color : var ( -- cc - muted ); font : inherit ; font - size : 14 px ; cursor : pointer ; }. container - scope - tab :: after { position : absolute ; right : 12 px ; bottom : 0 ; left : 12 px ; height : 3 px ; background : transparent ; content : '' ; }. container - scope - tab : hover { color : var ( -- cc - primary ); background : # f6f9fd ; }. container - scope - tab : focus - visible { outline : 2 px solid var ( -- cc - primary ); outline - offset : - 2 px ; }. container - scope - tab . is - active { color : var ( -- cc - primary ); font - weight : 600 ; }. container - scope - tab . is - active :: after { background : var ( -- cc - primary ); }. container - query - toolbar { border : 0 ; }. 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 ; }
2026-08-12 16:31:46 +08:00
: 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 ; }
: global (. container - editor - dialog . el - dialog__body ) { min - height : 0 ; padding : 28 px ; overflow - y : auto ; }
: global (. container - editor - dialog . el - dialog__footer ) { flex : 0 0 auto ; padding : 16 px 28 px ; border - top : 1 px solid # e5eaf1 ; background : # f8fafc ; }
2026-09-08 20:55:30 +08:00
@ media ( max - width : 720 px ){. container - scope - tab { min - height : 46 px ; padding : 0 14 px } : global (. container - editor - dialog . el - dialog ){ max - height : calc ( 100 vh - 20 px ); margin - block : 10 px ! important } : global (. container - editor - dialog . el - dialog__header ), : global (. container - editor - dialog . el - dialog__body ), : global (. container - editor - dialog . el - dialog__footer ){ padding - inline : 16 px }. details : deep (. el - descriptions__body table ),. details : deep ( tbody ),. details : deep ( tr ),. details : deep ( td ){ display : block ; width : 100 %! important }. row - actions { min - width : 180 px ; }}
2026-08-10 15:36:53 +08:00
</ style >