2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-10-20 23:34:11 +08:00
import '@/styles/adminWorkbench.css'
2026-10-08 17:08:03 +08:00
import { useExportTask } from '@/modules/downloads'
2026-09-14 20:03:27 +08:00
import '@/styles/adminDensity.css'
2026-09-09 00:44:07 +08:00
import { computed , onMounted , reactive , ref , watch } from 'vue'
2026-10-03 04:35:55 +08:00
import { ChevronDown , ChevronUp , Download , Pencil , PackagePlus , SlidersHorizontal } from '@lucide/vue'
2026-09-17 13:36:31 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-19 16:45:51 +08:00
import { useRouter } from 'vue-router'
2026-08-28 13:54:44 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
2026-10-22 14:08:06 +08:00
import { listContainerArrivalTimes , projectWarehouseArrival , type ContainerArrivalTime } from '@/modules/container-loading'
2026-08-20 09:10:36 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-19 16:45:51 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-21 17:20:40 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-10-20 10:11:31 +08:00
import InlineHelp from '@/shared/components/InlineHelp.vue'
2026-08-20 09:10:36 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-19 16:45:51 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-20 09:10:36 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-21 17:20:40 +08:00
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
2026-08-19 16:45:51 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-09-18 16:48:40 +08:00
import RowActions , { type RowActionItem } from '@/shared/components/RowActions.vue'
2026-10-10 14:58:15 +08:00
import { changeContainerReceivingWarehouse , convertLastMileContainerToInboundPlans , createManualInboundPlan , getContainerInboundPlanPreview , getContainerPlanProgress , getLastMileOrderManualNodes , listContainerPlanProgress , listLastMileOrders , reviewDirectRelabelOrder , reviewReturnOrder , transferLastMileOrders , updateLastMileOrderPalletMerge , updateLastMileOrderHold , updateLastMileOrderManualNodes , type ContainerInboundPlanPreviewDto , type ContainerInboundPlanPreviewGroupDto , type ContainerInboundPlanPreviewOrderDto , type ContainerPlanProgressDetailDto , type ContainerPlanProgressSummaryDto , type ManualNodeCode , type LastMileOrderQuery } from '../api/lastMileOrders'
2026-10-09 16:11:59 +08:00
import { businessOrderLink } from '../businessOrderLink'
2026-10-20 14:00:49 +08:00
import { getReturnOrderStatistics , type ReturnOrderStatistics } from '../api/lastMileOrders'
2026-08-27 15:56:13 +08:00
import OrderReferenceText from '../components/OrderReferenceText.vue'
2026-10-04 16:57:16 +08:00
import { reviewCargoDiscard } from '../api/lastMileOrders'
2026-09-13 21:10:05 +08:00
import WarehouseTransferReviewDialog from '../components/WarehouseTransferReviewDialog.vue'
2026-10-09 18:03:34 +08:00
import { confirmRelabelCompletion , getRelabelReviewTask , returnRelabelForRework } from '../api/relabelOrders'
2026-08-21 17:20:40 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { overseasWarehousePermissions as permission } from '../permissions'
2026-09-17 13:36:31 +08:00
import { lastMileStatusLabel , packageDifferenceText , toLastMileOrderListItem } from '../lastMilePresentation'
2026-08-19 16:45:51 +08:00
import type { LastMileOrderListItem } from '../types'
2026-10-09 17:27:47 +08:00
import { reworkRelabelApplications } from '../relabelRework'
const reworkOpen = ref ( false )
const reworkSaving = ref ( false )
const reworkOptions = ref < Array < { relabelNo : string ; status : string }> > ([])
const reworkForm = reactive ({ relabelNo : '' , remark : '' })
2026-08-19 16:45:51 +08:00
const rows = ref < LastMileOrderListItem [] >([])
2026-10-08 17:08:03 +08:00
const submitExport = useExportTask ()
2026-08-21 17:20:40 +08:00
const access = useAccessPreviewStore ()
2026-08-20 09:10:36 +08:00
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
2026-08-19 16:45:51 +08:00
const router = useRouter ()
const page = ref ( 1 )
2026-08-22 12:24:15 +08:00
const pageSize = ref ( 10 )
2026-08-20 09:10:36 +08:00
const total = ref ( 0 )
2026-08-28 13:54:44 +08:00
const containerTotal = ref ( 0 )
const containerRows = ref < ContainerPlanProgressSummaryDto [] >([])
const containerDetailOpen = ref ( false )
const containerDetailLoading = ref ( false )
const containerDetailError = ref < ApiErrorDetails | null >( null )
const containerDetail = ref < ContainerPlanProgressDetailDto | null >( null )
2026-09-13 21:10:05 +08:00
const transferReviewOpen = ref ( false )
const transferReviewTarget = ref < LastMileOrderListItem | null >( null )
2026-08-21 17:20:40 +08:00
const selectedRows = ref < LastMileOrderListItem [] >([])
2026-10-09 18:03:34 +08:00
const selectedReturnRows = computed (() => selectedRows . value . filter (( row ) => row . orderTypeCode === 'RETURN_RELABEL' && [ 'INBOUND_COMPLETED' , 'COMPLETED' , 'RECEIVED' , 'IN_WAREHOUSE' , '已完成入库' ]. includes ( String ( row . currentStatusCode || row . status || '' ))))
2026-10-20 14:00:49 +08:00
const returnStatistics = ref < ReturnOrderStatistics >()
let orderLoadRequest = 0
function returnDimension ( row : { id ?: unknown ; orderTypeCode ?: unknown }) {
return row . orderTypeCode === 'RETURN_RELABEL' ? returnStatistics . value ? . pageDimensions . find ( item => String ( item . lastlegOrderId ) === String ( row . id )) : undefined
}
2026-09-29 20:52:21 +08:00
function openReturnPackingPlan () {
2026-10-09 18:03:34 +08:00
const selected = selectedReturnRows . value
if ( ! selected . length ) { ElMessage . warning ( '请勾选已入库的退货订单' ); return }
if ( new Set ( selected . map ( row => row . customerId )). size !== 1 || new Set ( selected . map ( row => row . overseasWarehouseId )). size !== 1 ) {
ElMessage . warning ( '合箱请选择同一客户、同一海外仓的退货订单' ); return
}
void router . push ({ name : 'overseas-return-inventory' , query : {
packing : '1' , customerId : selected [ 0 ] ! . customerId , overseasWarehouseId : selected [ 0 ] ! . overseasWarehouseId ,
returnNos : selected . map ( row => row . sourceOrderNo || row . orderNo ). join ( ',' ),
} })
2026-09-29 20:52:21 +08:00
}
2026-08-30 18:50:16 +08:00
const exportLoading = ref ( false )
2026-10-09 18:03:34 +08:00
const actionLoading = ref ( false )
2026-10-08 17:08:03 +08:00
async function exportOrders () { exportLoading . value = true ; try { await submitExport ( 'LAST_MILE_ORDER_LIST' , '尾程订单' , orderQuery ()) } catch ( cause ) { ElMessage . error ( getApiErrorDetails ( cause , '尾程订单导出失败,请重试' ). message ) } finally { exportLoading . value = false } }
2026-10-10 14:58:15 +08:00
const palletMergeSelectionEditable = computed (() => selectedRows . value . length > 0
&& selectedRows . value . every ( row => row . customerId && row . currentStatusCode !== 'INBOUND_COMPLETED' && row . palletMergeFlag !== 'MERGED' )
&& new Set ( selectedRows . value . map ( row => row . customerId )). size === 1 )
const canCancelPalletMerge = computed (() => palletMergeSelectionEditable . value
&& selectedRows . value . every ( row => row . palletMergeFlag === 'REQUIRED' ))
async function submitPalletMerge ( required : boolean ) {
if ( actionLoading . value || ! access . can ( permission . lastMileOrderPalletMerge )
|| ! ( required ? palletMergeSelectionEditable . value : canCancelPalletMerge . value )) return
const selection = [... selectedRows . value ]
const label = required ? '合托' : '取消合托'
try {
await ElMessageBox . confirm ( required
? `将勾选的 ${ selection . length } 票订单标记为同一合托组?所选订单须属于同一客户、同一目的仓,PDA 将提示现场合托。`
2026-10-20 10:11:31 +08:00
: `取消勾选的 ${ selection . length } 票订单的合托标记和 PDA 提示?` , ` ${ label } · ${ selection . length } 票订单` ,
2026-10-20 23:34:11 +08:00
{ customClass : 'admin-density-confirm admin-workbench is-dialog' , confirmButtonText : `确认 ${ label } ` , cancelButtonText : '返回' , type : 'warning' })
2026-10-10 14:58:15 +08:00
} catch { return }
actionLoading . value = true
actionError . value = null
try {
await updateLastMileOrderPalletMerge ( selection . map ( row => row . id ), selection [ 0 ] ! . customerId , required )
ElMessage . success ( required ? '已标记合托' : '已取消合托标记' )
selectedRows . value = []
await load ()
} catch ( cause ) {
actionError . value = getApiErrorDetails ( cause , ` ${ label } 失败,请重试` )
} finally { actionLoading . value = false }
}
2026-08-21 17:20:40 +08:00
const actionError = ref < ApiErrorDetails | null >( null )
2026-09-07 23:58:18 +08:00
const actionTarget = ref < LastMileOrderListItem | null >( null )
const inboundPlanOpen = ref ( false )
2026-09-08 10:54:36 +08:00
const inboundPlanWarehouseLabel = computed (() => [ actionTarget . value ? . overseasWarehouseCode , actionTarget . value ? . warehouse ]
. filter (( value ) => value && value !== '—' )
. join ( ' · ' ))
2026-09-10 15:40:55 +08:00
const inboundPlanMinimumPalletCount = ref ( 1 )
2026-09-14 16:38:51 +08:00
const inboundPlanSourcePalletCount = ref < number | null >( null )
const inboundPlanPreviewLoading = ref ( false )
const inboundPlanPreviewError = ref ( '' )
const inboundPlanIsDropship = computed (() => actionTarget . value ? . orderTypeCode === 'ONE_CLICK_DROPSHIP' )
2026-10-22 14:08:06 +08:00
const inboundPlanArrival = ref < ContainerArrivalTime | null >( null )
const inboundPlanArrivalLoading = ref ( false )
const inboundPlanArrivalError = ref ( '' )
let inboundPlanArrivalRequest = 0
const inboundPlanArrivalProjection = computed (() => {
const arrival = inboundPlanArrival . value
return projectWarehouseArrival ( arrival ? { ... arrival , plannedAt : arrival . estimatedWarehouseArrivalAt } : undefined )
})
const inboundPlanSubmitDisabled = computed (() => inboundPlanArrivalLoading . value || ! inboundPlanArrivalProjection . value . date
2026-09-15 12:35:46 +08:00
|| ( inboundPlanIsDropship . value
&& ( inboundPlanPreviewLoading . value || inboundPlanSourcePalletCount . value == null
|| inboundPlanSourcePalletCount . value <= 0 )))
const inboundPlanForm = reactive ({ inboundCode : '' , palletCount : 1 , palletType : 'WOOD' as 'STANDARD' | 'WOOD' | 'PAPER' , assigned : false , mergeRequired : false , urgent : false , remark : '' })
2026-10-22 14:08:06 +08:00
const inboundPlanExpectedArrival = computed (() => {
const { date , time , isDefault } = inboundPlanArrivalProjection . value
return date ? ` ${ date }${ time ? ` ${ time } ` : '' }${ isDefault ? '(默认推算)' : '' } ` : '—'
})
2026-09-07 23:58:18 +08:00
const holdOpen = ref ( false )
const holdForm = reactive ({ held : true , chargeable : true , reason : '' })
const manualNodesOpen = ref ( false )
const manualNodesLoading = ref ( false )
const manualNodesVersion = ref ( 0 )
const manualNodeDefinitions : Array < { code : ManualNodeCode ; label : string } > = [
{ code : 'DOMESTIC_CUSTOMS_COMPLETED' , label : '国内报关完成' },
{ code : 'ESTIMATED_DEPARTURE' , label : '预计开船' },
{ code : 'ACTUAL_DEPARTURE' , label : '实际开船' },
{ code : 'ESTIMATED_ARRIVAL' , label : '预计到港' },
{ code : 'OVERSEAS_CUSTOMS_COMPLETED' , label : '国外清关完成' },
{ code : 'ACTUAL_ARRIVAL_WAITING_UNLOAD' , label : '实际到港等待卸船' },
{ code : 'PICKUP_APPOINTED' , label : '预约提柜' },
{ code : 'UNLOADED' , label : '卸柜' },
]
2026-10-26 15:51:07 +08:00
const visibleManualNodeDefinitions = manualNodeDefinitions . filter (({ code }) => [ 'DOMESTIC_CUSTOMS_COMPLETED' , 'OVERSEAS_CUSTOMS_COMPLETED' ]. includes ( code ))
2026-09-07 23:58:18 +08:00
const manualNodeValues = reactive < Record < ManualNodeCode , string | null > > ( Object . fromEntries ( manualNodeDefinitions . map (({ code }) => [ code , null ])) as Record < ManualNodeCode , string | null >)
2026-08-21 17:20:40 +08:00
const transferOpen = ref ( false )
2026-08-28 13:54:44 +08:00
const transferWarehouseLoading = ref ( false )
const transferWarehouseError = ref < ApiErrorDetails | null >( null )
const transferWarehouseOptions = ref < TransferWarehouseOption [] >([])
2026-08-21 17:20:40 +08:00
const transferForm = reactive ({ warehouseId : '' , warehouseCode : '' , warehouseName : '' , remark : '' })
2026-08-27 17:49:05 +08:00
const containerConvertOpen = ref ( false )
2026-08-28 13:54:44 +08:00
const containerOptions = ref < ContainerPlanProgressSummaryDto [] >([])
2026-08-27 17:49:05 +08:00
const containerOptionsLoading = ref ( false )
const containerLoadError = ref < ApiErrorDetails | null >( null )
const selectedContainerBookingId = ref ( '' )
2026-09-08 23:57:12 +08:00
const containerPreview = ref < ContainerInboundPlanPreviewDto | null >( null )
const containerPreviewLoading = ref ( false )
const containerPreviewError = ref ( '' )
2026-09-09 00:44:07 +08:00
type ContainerConversionPlan = Awaited < ReturnType < typeof convertLastMileContainerToInboundPlans > > [ number ]
interface ContainerConversionResult {
plans : ContainerConversionPlan []
existingPlanNos : Set < string >
newPlanCount : number
existingPlanCount : number
2026-10-05 05:40:21 +08:00
resourceRecovered : boolean
2026-09-09 00:44:07 +08:00
}
const containerConversionResult = ref < ContainerConversionResult | null >( null )
let containerPreviewRequest = 0
let containerPreviewLoadingBookingId = ''
let containerConversionIdempotencyKey = ''
2026-10-05 05:40:21 +08:00
const containerIsResourceRecovery = computed (() => Boolean ( containerPreview . value ? . resourceRetry || containerConversionResult . value ? . resourceRecovered ))
const containerConfirmText = computed (() => containerConversionResult . value
? containerConversionResult . value . resourceRecovered ? '已确认' : '已生成'
: containerPreview . value ? . resourceRetry ? '重试资源确认' : '确认生成' )
const containerConfirmDisabled = computed (() => ! access . can ( permission . inboundOrderCreate )
|| ! containerPreview . value ? . ready
|| ( ! containerPreview . value . resourceRetry && containerPreview . value . pendingOrderCount <= 0 )
|| containerOptionsLoading . value
2026-09-09 00:44:07 +08:00
|| containerPreviewLoading . value
|| actionLoading . value
|| Boolean ( containerConversionResult . value ))
2026-09-07 15:07:39 +08:00
const receivingWarehouseOpen = ref ( false )
const receivingWarehouseLoading = ref ( false )
const receivingWarehouseError = ref < ApiErrorDetails | null >( null )
const receivingWarehouseActionError = ref < ApiErrorDetails | null >( null )
const receivingWarehouseOptions = ref < TransferWarehouseOption [] >([])
const receivingWarehouseTarget = ref < ContainerPlanProgressSummaryDto | null >( null )
const receivingWarehouseForm = reactive ({ warehouseId : '' , remark : '' })
let receivingWarehouseIdempotencyKey = ''
2026-08-27 17:49:05 +08:00
let containerOptionRequest = 0
2026-10-08 15:12:49 +08:00
const statusTab = ref < '全部' | '开船前' | '开船后' | '柜子' > ( '全部' )
const statusTabs = [ '全部' , '开船前' , '开船后' ] as const
2026-10-03 04:35:55 +08:00
const showAdvancedFilters = ref ( false )
2026-10-09 16:11:59 +08:00
const filters = ref < QueryModel >({ businessType : '' , businessOrderNo : '' , externalBookingNo : '' , orderNo : '' , relabelNo : '' , orderType : '' , warehouse : '' , inboundCode : '' , lxCode : '' , sourceOrderNo : '' , containerNo : '' , status : '' , transferWarehouseCode : '' , abnormal : '' , detained : '' , createdAt : [] })
2026-08-27 17:16:38 +08:00
const allFields : QueryField [] = [
2026-10-09 16:11:59 +08:00
{ key : 'businessType' , type : 'select' , label : '业务来源' , width : 180 , placeholder : '请选择业务来源' , options : [
{ label : '本土散板(全部来源)' , value : 'LOCAL_LOOSEBOARD' }, { label : '本土卡车' , value : 'US_TRUCK' },
{ label : '仓储转运' , value : 'WAREHOUSE_TRANSFER' }, { label : '头程订单' , value : 'FIRST_LEG' },
{ label : '换标下单' , value : 'RELABEL_APPLICATION' }, { label : '退货换标' , value : 'RETURN_RELABEL' }, { label : '一件代发' , value : 'ONE_CLICK_DROPSHIP' },
] },
{ key : 'businessOrderNo' , type : 'input' , label : '关联商业单号' , width : 210 , placeholder : '请输入商业单号' },
{ key : 'externalBookingNo' , type : 'input' , label : '外接预订号' , width : 210 , placeholder : '请输入外接预订号' },
2026-09-02 14:39:34 +08:00
{ key : 'orderNo' , type : 'input' , label : '尾程单号' , width : 190 , placeholder : '请输入尾程单号' },
2026-09-13 21:10:05 +08:00
{ key : 'relabelNo' , type : 'input' , label : '换标单号' , width : 190 , placeholder : '请输入换标单号' },
2026-09-02 14:39:34 +08:00
{ key : 'inboundCode' , type : 'input' , label : '入仓码' , width : 180 , placeholder : '请输入入仓码' },
2026-09-08 13:08:54 +08:00
{ key : 'lxCode' , type : 'input' , label : 'LX代码' , width : 180 , placeholder : '请输入LX代码' },
2026-10-09 16:11:59 +08:00
{ key : 'orderType' , type : 'select' , label : '履约类型' , width : 170 , placeholder : '请选择履约类型' , options : [
2026-08-22 12:24:15 +08:00
{ label : '头程订单' , value : 'FIRST_LEG' }, { label : '换标下单' , value : 'RELABEL_APPLICATION' },
{ label : '仓储转运' , value : 'WAREHOUSE_TRANSFER' }, { label : '本土散板' , value : 'LOCAL_LOOSEBOARD' },
2026-08-21 17:20:40 +08:00
{ label : '退货换标' , value : 'RETURN_RELABEL' }, { label : '一件代发' , value : 'ONE_CLICK_DROPSHIP' },
2026-09-15 15:20:28 +08:00
{ label : '货物废弃订单' , value : 'CARGO_DISCARD' },
2026-08-21 17:20:40 +08:00
] },
2026-09-02 14:39:34 +08:00
{ key : 'warehouse' , type : 'input' , label : '海外仓' , width : 210 , placeholder : '请输入仓库名称或编码' },
{ key : 'sourceOrderNo' , type : 'input' , label : '来源订单号' , width : 190 , placeholder : '请输入来源订单号' },
{ key : 'containerNo' , type : 'input' , label : '集装箱号' , width : 180 , placeholder : '请输入集装箱号' },
{ key : 'transferWarehouseCode' , type : 'input' , label : '转仓代码' , width : 170 , placeholder : '请输入转仓代码' },
2026-09-18 11:41:27 +08:00
{ key : 'status' , type : 'select' , label : '当前状态' , width : 160 , placeholder : '请选择当前状态' , options : [
2026-08-20 09:10:36 +08:00
{ label : '待分配' , value : 'PENDING_ASSIGNMENT' },
2026-09-17 13:36:31 +08:00
{ label : '待换标' , value : 'WAIT_RELABEL' },
{ label : '待排车' , value : 'READY_FOR_DISPATCH' },
{ label : '待出库' , value : 'WAIT_OUTBOUND' },
2026-08-21 17:20:40 +08:00
{ label : '待入库' , value : 'PENDING_INBOUND' },
{ label : '入库完成' , value : 'INBOUND_COMPLETED' },
{ label : '待派送' , value : 'PENDING_DELIVERY' },
{ label : '派送中' , value : 'DELIVERING' },
{ label : '派送完成' , value : 'DELIVERED' },
2026-08-20 09:10:36 +08:00
] },
2026-09-18 11:41:27 +08:00
{ key : 'abnormal' , type : 'select' , label : '是否异常' , width : 140 , placeholder : '请选择是否异常' , options : [{ label : '是' , value : 'true' }, { label : '否' , value : 'false' }] },
{ key : 'detained' , type : 'select' , label : '是否暂扣' , width : 140 , placeholder : '请选择是否暂扣' , options : [{ label : '是' , value : 'true' }, { label : '否' , value : 'false' }] },
2026-09-02 14:39:34 +08:00
{ key : 'createdAt' , type : 'daterange' , label : '创建时间' , width : 280 },
2026-08-27 17:16:38 +08:00
]
2026-10-08 15:12:49 +08:00
const availableFields = computed (() => allFields . filter (( field ) => statusTab . value === '全部' || field . key !== 'orderType' ))
2026-08-28 13:54:44 +08:00
const fields = computed < QueryField [] >(() => statusTab . value === '柜子'
? allFields . filter (( field ) => [ 'containerNo' , 'createdAt' ]. includes ( field . key ))
2026-10-03 04:35:55 +08:00
: showAdvancedFilters . value
2026-10-08 15:12:49 +08:00
? availableFields . value
2026-10-03 04:35:55 +08:00
: allFields . filter (( field ) => [ 'orderNo' , 'inboundCode' , 'warehouse' ]. includes ( field . key )))
2026-08-27 17:49:05 +08:00
const selectedContainer = computed (() => containerOptions . value . find (( row ) => row . sourceContainerBookingId === selectedContainerBookingId . value ))
2026-09-07 20:25:14 +08:00
const selectedReceivingWarehouse = computed (() => receivingWarehouseOptions . value . find (( row ) => (
row . warehouseId === receivingWarehouseForm . warehouseId
&& row . warehouseId !== receivingWarehouseTarget . value ? . overseasWarehouseId
)))
2026-08-28 13:54:44 +08:00
interface TransferWarehouseOption {
warehouseId : string
warehouseCode : string
warehouseName : string
region : string
label : string
}
function warehouseRegion ( warehouse : PlatformWarehouseSummaryResource ) {
const addressRegion = [ warehouse . primaryAddress ? . stateProvince , warehouse . primaryAddress ? . city ]
. map (( value ) => String ( value || '' ). trim ())
. filter ( Boolean )
. join ( ' / ' )
return addressRegion || String ( warehouse . addressSummary || warehouse . countryCode || '' ). trim ()
}
function toTransferWarehouseOption ( warehouse : PlatformWarehouseSummaryResource ) : TransferWarehouseOption {
const warehouseId = String ( warehouse . id )
const warehouseCode = warehouse . warehouseCode . trim ()
const warehouseName = warehouse . warehouseName . trim ()
const region = warehouseRegion ( warehouse )
return {
warehouseId ,
warehouseCode ,
warehouseName ,
region ,
label : [ warehouseCode , warehouseName , region ]. filter ( Boolean ). join ( ' · ' ),
}
}
2026-09-07 15:07:39 +08:00
function receivingWarehouseLabel ( container : ContainerPlanProgressSummaryDto | null | undefined ) {
if ( ! container ) return '—'
const code = String ( container . overseasWarehouseCode || '' ). trim ()
const name = String ( container . overseasWarehouseName || '' ). trim ()
return [ code , name ]. filter ( Boolean ). join ( ' · ' ) || '—'
}
2026-09-08 11:45:30 +08:00
function receivingWarehouseChangeDisabledReason ( container : ContainerPlanProgressSummaryDto ) {
if ( ! access . can ( permission . lastMileOrderTransfer )) return '暂无修改接柜仓权限,请联系管理员开通尾程订单编辑权限'
if ( ! container . canChangeReceivingWarehouse ) return container . receivingWarehouseLockReason || '当前柜已锁定,不能修改接柜仓'
return ''
}
2026-09-07 15:07:39 +08:00
function newReceivingWarehouseIdempotencyKey () {
return `container-receiving-warehouse- ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } `
}
2026-08-19 16:45:51 +08:00
const columns : DataTableColumn [] = [
2026-09-14 20:21:02 +08:00
{ key : 'actions' , label : '操作' , width : 330 , fixed : 'left' , slotName : 'actions' },
2026-08-21 17:20:40 +08:00
{ key : 'selection' , type : 'selection' , width : 48 , fixed : 'left' },
2026-08-27 15:56:13 +08:00
{ key : 'orderNo' , prop : 'orderNo' , label : '尾程单号' , minWidth : 170 , slotName : 'orderNo' },
2026-10-28 10:32:33 +08:00
{ key : 'dispatchStatus' , prop : 'dispatchStatus' , label : '排车状态' , width : 110 , afterColumn : 'orderNo' },
{ key : 'relabelStatus' , slotName : 'relabelStatus' , label : '换标子状态' , minWidth : 180 , afterColumn : 'orderNo' },
{ key : 'status' , prop : 'status' , label : '当前状态' , width : 110 , afterColumn : 'orderNo' },
{ key : 'abnormal' , prop : 'abnormal' , label : '是否异常' , width : 100 , afterColumn : 'orderNo' },
{ key : 'held' , prop : 'held' , label : '是否暂扣' , width : 100 , afterColumn : 'orderNo' },
2026-10-09 16:11:59 +08:00
{ key : 'businessSource' , prop : 'businessSource' , label : '业务来源' , minWidth : 220 },
{ key : 'businessOrderNo' , prop : 'businessOrderNo' , label : '关联商业单号' , minWidth : 230 , slotName : 'businessOrderNo' },
{ key : 'externalBookingNo' , prop : 'externalBookingNo' , label : '外接预订号' , minWidth : 220 },
2026-08-31 14:42:28 +08:00
{ key : 'consolidated' , prop : 'consolidated' , label : '合托标记' , width : 100 , hideable : false },
{ key : 'palletMergeRelation' , prop : 'palletMergeRelation' , label : '合托关联入仓单' , minWidth : 220 , hideable : false },
2026-08-19 16:45:51 +08:00
{ key : 'warning' , prop : 'warning' , label : '预警' , width : 90 },
2026-10-09 16:11:59 +08:00
{ key : 'orderType' , prop : 'orderType' , label : '履约类型' , minWidth : 110 },
2026-08-19 16:45:51 +08:00
{ key : 'rejectedAt' , prop : 'rejectedAt' , label : '拒收时间' , minWidth : 150 },
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 140 },
2026-09-06 15:53:48 +08:00
{ key : 'lxCode' , prop : 'lxCode' , label : 'LX代码' , minWidth : 150 },
2026-08-19 16:45:51 +08:00
{ key : 'owner' , prop : 'owner' , label : '货主' , minWidth : 150 },
{ key : 'warehouse' , prop : 'warehouse' , label : '海外仓' , minWidth : 170 },
{ key : 'transferWarehouse' , prop : 'transferWarehouse' , label : '转仓' , minWidth : 140 },
{ key : 'fbaWarehouse' , prop : 'fbaWarehouse' , label : 'FBA仓库' , minWidth : 130 },
{ key : 'containerNo' , prop : 'containerNo' , label : '集装箱号' , minWidth : 150 },
2026-10-20 14:00:49 +08:00
{ key : 'pallets' , prop : 'pallets' , label : '托数(退货实收)' , width : 130 , align : 'right' , slotName : 'receiptPallets' },
{ key : 'cartons' , prop : 'cartons' , label : '箱数(退货实收)' , width : 130 , align : 'right' , slotName : 'receiptBoxes' },
{ key : 'returnParcels' , label : '退货实收包裹' , width : 120 , align : 'right' , slotName : 'returnParcels' },
{ key : 'returnExpected' , label : '退货预报(件)' , width : 120 , align : 'right' , slotName : 'returnExpected' },
{ key : 'returnReceived' , label : '退货实收(件)' , width : 120 , align : 'right' , slotName : 'returnReceived' },
{ key : 'returnRemaining' , label : '退货待收(件)' , width : 120 , align : 'right' , slotName : 'returnRemaining' },
2026-09-17 13:36:31 +08:00
{ key : 'cartonDifference' , prop : 'cartonDifference' , label : '差异(箱)' , width : 100 , align : 'right' , slotName : 'cartonDifference' },
2026-08-19 16:45:51 +08:00
{ key : 'volume' , prop : 'volume' , label : '体积(m³)' , width : 100 , align : 'right' },
2026-10-20 14:00:49 +08:00
{ key : 'quantity' , prop : 'quantity' , label : '数量(非退货)' , width : 110 , align : 'right' , slotName : 'nonReturnQuantity' },
{ key : 'declaredPieces' , prop : 'declaredPieces' , label : '转运预报件数' , width : 120 , align : 'right' },
{ key : 'receivedPieces' , prop : 'receivedPieces' , label : '转运实收件数' , width : 120 , align : 'right' },
2026-08-19 16:45:51 +08:00
{ key : 'volumeWeight' , prop : 'volumeWeight' , label : '体积重量' , width : 100 , align : 'right' },
{ key : 'portEta' , prop : 'portEta' , label : '预计到港时间' , minWidth : 150 },
{ key : 'portAta' , prop : 'portAta' , label : '实际到港时间' , minWidth : 150 },
{ key : 'warehouseEta' , prop : 'warehouseEta' , label : '预计到仓' , minWidth : 140 },
{ key : 'inboundOrderNo' , prop : 'inboundOrderNo' , label : '入库单号' , minWidth : 150 },
{ key : 'outboundOrderNo' , prop : 'outboundOrderNo' , label : '出库单号' , minWidth : 150 },
{ key : 'dispatchOrderNo' , prop : 'dispatchOrderNo' , label : '派送单号' , minWidth : 150 },
2026-09-22 08:47:43 +08:00
{ key : 'relabelOrderNo' , slotName : 'relabelOrderNo' , label : '换标单号' , minWidth : 220 },
2026-08-22 12:24:15 +08:00
{ key : 'createdAt' , prop : 'createdAt' , label : '创建时间' , minWidth : 170 },
2026-08-19 16:45:51 +08:00
{ key : 'internalRemark' , prop : 'internalRemark' , label : '备注(仅自己)' , minWidth : 160 },
{ key : 'customerRemark' , prop : 'customerRemark' , label : '备注(客户)' , minWidth : 160 },
{ key : 'warehouseRemark' , prop : 'warehouseRemark' , label : '备注(仓库)' , minWidth : 160 },
]
2026-08-28 13:54:44 +08:00
const containerDetailColumns : DataTableColumn [] = [
{ key : 'orderNo' , prop : 'orderNo' , label : '尾程单号' , minWidth : 170 },
2026-10-28 10:32:33 +08:00
{ key : 'orderStatus' , prop : 'orderStatus' , label : '订单状态' , width : 130 , slotName : 'orderStatus' , afterColumn : 'orderNo' },
{ key : 'planStatus' , prop : 'planStatus' , label : '计划状态' , width : 130 , slotName : 'planStatus' , afterColumn : 'orderNo' },
2026-08-28 13:54:44 +08:00
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 150 },
{ key : 'planNo' , prop : 'planNo' , label : '独立入库计划' , minWidth : 170 },
{ key : 'plannedArrivalAt' , prop : 'plannedArrivalAt' , label : '计划到仓时间' , minWidth : 170 , slotName : 'plannedArrivalAt' },
]
2026-08-19 16:45:51 +08:00
2026-08-28 13:54:44 +08:00
function formatDateTime ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 19 ) : '—'
}
2026-09-07 20:45:31 +08:00
function inboundPlanStatusLabel ( value : unknown ) {
const code = String ( value ?? '' ). trim ()
return ({
PENDING_ASSIGNMENT : '待入库' ,
ASSIGNED : '待入库' ,
PDA_PENDING : '待入库' ,
PENDING_RECEIVING : '待收货' ,
PICKING : '拣货中' ,
PARTIAL_INBOUND : '部分入库' ,
COMPLETED : '全部入库' ,
CANCELLED : '已取消' ,
} as Record < string , string >)[ code ] || ( code ? '未知状态' : '—' )
}
2026-08-28 13:54:44 +08:00
function canGenerateContainerPlans ( container : ContainerPlanProgressSummaryDto ) {
2026-09-10 16:33:43 +08:00
return Boolean ( container . containerNo
2026-09-07 16:36:21 +08:00
&& container . overseasWarehouseId
&& container . overseasWarehouseCode
&& container . overseasWarehouseName
&& container . assignmentVersion >= 0
2026-09-10 16:33:43 +08:00
&& ( container . pendingPlanCount > 0 || container . resourcePendingPlanCount > 0 ))
2026-09-07 16:36:21 +08:00
}
function containerSnapshotBlockReason ( container : ContainerPlanProgressSummaryDto ) {
if ( ! container . containerNo ) return container . blockingReason || '来源柜真实集装箱号缺失、格式无效或存在漂移'
if ( ! container . overseasWarehouseId || ! container . overseasWarehouseCode
|| ! container . overseasWarehouseName || container . assignmentVersion < 0 ) {
return '柜内接柜仓业务快照不一致'
}
return ''
2026-09-02 14:39:34 +08:00
}
function pendingInboundCodeCount ( container : ContainerPlanProgressSummaryDto ) {
return Math . max ( 0 , container . inboundCodeCount - container . plannedInboundCodeCount )
}
function changeContainerPage ( value : number ) {
page . value = value
void load ()
}
function changeContainerPageSize ( value : number ) {
pageSize . value = value
page . value = 1
void load ()
2026-08-28 13:54:44 +08:00
}
2026-10-08 15:12:49 +08:00
function orderQuery () : LastMileOrderQuery {
const dateRange = Array . isArray ( filters . value . createdAt ) ? filters . value . createdAt . map ( String ) : []
const booleanValue = ( value : unknown ) => value === 'true' ? true : value === 'false' ? false : undefined
return {
page : page . value ,
size : pageSize . value ,
2026-10-09 16:11:59 +08:00
businessType : String ( filters . value . businessType || '' ). trim () || undefined ,
businessOrderNo : String ( filters . value . businessOrderNo || '' ). trim () || undefined ,
externalBookingNo : String ( filters . value . externalBookingNo || '' ). trim () || undefined ,
2026-10-08 15:12:49 +08:00
orderNo : String ( filters . value . orderNo || '' ). trim () || undefined ,
relabelNo : String ( filters . value . relabelNo || '' ). trim () || undefined ,
orderType : statusTab . value === '全部' ? String ( filters . value . orderType || '' ). trim () || undefined : 'FIRST_LEG' ,
sailingStage : statusTab . value === '开船前' ? 'BEFORE_DEPARTURE' : statusTab . value === '开船后' ? 'AFTER_DEPARTURE' : undefined ,
warehouse : String ( filters . value . warehouse || '' ). trim () || undefined ,
inboundCode : String ( filters . value . inboundCode || '' ). trim () || undefined ,
lxCode : String ( filters . value . lxCode || '' ). trim () || undefined ,
sourceOrderNo : String ( filters . value . sourceOrderNo || '' ). trim () || undefined ,
containerNo : String ( filters . value . containerNo || '' ). trim () || undefined ,
currentStatus : String ( filters . value . status || '' ). trim () || undefined ,
transferWarehouseCode : String ( filters . value . transferWarehouseCode || '' ). trim () || undefined ,
abnormal : booleanValue ( filters . value . abnormal ),
detained : booleanValue ( filters . value . detained ),
createdFrom : dateRange [ 0 ] ? ` ${ dateRange [ 0 ] } T00:00:00` : undefined ,
createdTo : dateRange [ 1 ] ? ` ${ dateRange [ 1 ] } T23:59:59` : undefined ,
}
}
2026-08-20 09:10:36 +08:00
async function load ( model : QueryModel = filters . value ) {
2026-10-20 14:00:49 +08:00
const request = ++ orderLoadRequest
returnStatistics . value = undefined
2026-08-20 09:10:36 +08:00
Object . assign ( filters . value , model )
loading . value = true
loadError . value = null
2026-08-28 13:54:44 +08:00
if ( statusTab . value === '柜子' ) {
const dateRange = Array . isArray ( filters . value . createdAt ) ? filters . value . createdAt . map ( String ) : []
try {
const result = await listContainerPlanProgress ({
page : page . value ,
size : pageSize . value ,
containerNo : String ( filters . value . containerNo || '' ). trim () || undefined ,
createdFrom : dateRange [ 0 ] ? ` ${ dateRange [ 0 ] } T00:00:00` : undefined ,
createdTo : dateRange [ 1 ] ? ` ${ dateRange [ 1 ] } T23:59:59` : undefined ,
})
2026-09-07 20:45:31 +08:00
containerRows . value = result . records
containerTotal . value = result . total
2026-08-28 13:54:44 +08:00
} catch ( error ) {
containerRows . value = []
containerTotal . value = 0
loadError . value = getApiErrorDetails ( error , '柜级入库计划进度加载失败,请重试' )
} finally {
loading . value = false
}
return
}
2026-08-20 09:10:36 +08:00
try {
2026-10-20 14:00:49 +08:00
const query = orderQuery ()
const [ result , summary ] = await Promise . all ([ listLastMileOrders ( query ), getReturnOrderStatistics ( query )])
if ( request !== orderLoadRequest ) return
2026-08-21 11:20:09 +08:00
rows . value = result . records . map ( toLastMileOrderListItem )
2026-08-20 09:10:36 +08:00
total . value = result . total
2026-10-20 14:00:49 +08:00
returnStatistics . value = summary
2026-08-20 09:10:36 +08:00
} catch ( error ) {
2026-10-20 14:00:49 +08:00
if ( request !== orderLoadRequest ) return
2026-08-20 09:10:36 +08:00
rows . value = []
total . value = 0
2026-08-21 11:20:09 +08:00
loadError . value = error instanceof Error && error . message === '当前环境未签发海外访问令牌'
? { message : error . message }
: getApiErrorDetails ( error , '尾程订单加载失败,请重试' )
2026-08-20 09:10:36 +08:00
} finally {
2026-10-20 14:00:49 +08:00
if ( request === orderLoadRequest ) loading . value = false
2026-08-20 09:10:36 +08:00
}
}
function search ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
2026-10-03 04:35:55 +08:00
function reset () {
for ( const field of allFields ) filters . value [ field . key ] = field . type === 'daterange' ? [] : ''
page . value = 1
void load ()
}
2026-08-27 17:49:05 +08:00
function changeStatusTab () {
selectedRows . value = []
page . value = 1
void load ()
}
2026-10-03 04:35:55 +08:00
function selectStatusTab ( value : typeof statusTabs [ number ]) {
if ( statusTab . value === value ) return
statusTab . value = value
changeStatusTab ()
}
function handleStatusTabKeydown ( event : KeyboardEvent , index : number ) {
let nextIndex = index
if ( event . key === 'ArrowRight' ) nextIndex = ( index + 1 ) % statusTabs . length
else if ( event . key === 'ArrowLeft' ) nextIndex = ( index - 1 + statusTabs . length ) % statusTabs . length
else if ( event . key === 'Home' ) nextIndex = 0
else if ( event . key === 'End' ) nextIndex = statusTabs . length - 1
else return
event . preventDefault ()
const tab = statusTabs [ nextIndex ]
if ( ! tab ) return
selectStatusTab ( tab )
const buttons = ( event . currentTarget as HTMLElement ). parentElement ? . querySelectorAll < HTMLElement >( '[role="tab"]' )
buttons ? .[ nextIndex ] ? . focus ()
}
2026-08-28 13:54:44 +08:00
async function openContainerDetail ( row : ContainerPlanProgressSummaryDto ) {
containerDetailOpen . value = true
containerDetailLoading . value = true
containerDetailError . value = null
containerDetail . value = null
try {
containerDetail . value = await getContainerPlanProgress ( row . sourceContainerBookingId )
} catch ( error ) {
containerDetailError . value = getApiErrorDetails ( error , '柜级详情加载失败,请重试' )
} finally {
containerDetailLoading . value = false
}
}
2026-09-07 15:07:39 +08:00
async function loadReceivingWarehouseOptions () {
receivingWarehouseLoading . value = true
receivingWarehouseError . value = null
receivingWarehouseOptions . value = []
try {
const warehouses = await listAllPlatformWarehouses ({ warehouseType : 'OVERSEAS' , status : 'ACTIVE' })
receivingWarehouseOptions . value = warehouses . map ( toTransferWarehouseOption )
} catch ( error ) {
receivingWarehouseError . value = getApiErrorDetails ( error , '海外接柜仓候选加载失败,请重试' )
} finally {
receivingWarehouseLoading . value = false
}
}
function openReceivingWarehouseChange ( container : ContainerPlanProgressSummaryDto ) {
2026-09-08 11:45:30 +08:00
const disabledReason = receivingWarehouseChangeDisabledReason ( container )
if ( disabledReason ) {
ElMessage . warning ( disabledReason )
return
}
2026-09-07 15:07:39 +08:00
receivingWarehouseTarget . value = container
2026-09-07 20:25:14 +08:00
Object . assign ( receivingWarehouseForm , { warehouseId : '' , remark : '' })
2026-09-07 15:07:39 +08:00
receivingWarehouseIdempotencyKey = newReceivingWarehouseIdempotencyKey ()
receivingWarehouseOptions . value = []
receivingWarehouseError . value = null
receivingWarehouseActionError . value = null
actionError . value = null
receivingWarehouseOpen . value = true
void loadReceivingWarehouseOptions ()
}
async function submitReceivingWarehouseChange () {
const target = receivingWarehouseTarget . value
2026-09-07 20:25:14 +08:00
const selected = selectedReceivingWarehouse . value
2026-09-07 15:07:39 +08:00
if ( ! target || ! selected ) {
2026-09-07 20:25:14 +08:00
ElMessage . warning ( '请选择与当前仓不同的有效海外接柜仓' )
2026-09-07 15:07:39 +08:00
return
}
actionLoading . value = true
actionError . value = null
2026-09-07 20:25:14 +08:00
receivingWarehouseActionError . value = null
2026-09-07 15:07:39 +08:00
try {
await changeContainerReceivingWarehouse ( target . sourceContainerBookingId , {
overseasWarehouseId : selected . warehouseId ,
expectedVersion : target . assignmentVersion ,
remark : receivingWarehouseForm . remark . trim () || undefined ,
}, receivingWarehouseIdempotencyKey )
receivingWarehouseOpen . value = false
receivingWarehouseTarget . value = null
ElMessage . success ( '海外接柜仓修改成功' )
await load ()
} catch ( error ) {
const details = getApiErrorDetails ( error , '海外接柜仓修改失败,请重试' )
receivingWarehouseActionError . value = details . code === 'A0409'
? { ... details , message : '接柜仓信息已被其他操作更新,请刷新后重试' }
: details
actionError . value = receivingWarehouseActionError . value
if ( details . code === 'A0409' ) {
receivingWarehouseOpen . value = false
receivingWarehouseTarget . value = null
await load ()
}
} finally {
actionLoading . value = false
}
}
2026-08-21 17:20:40 +08:00
function onSelectionChange ( selection : object []) { selectedRows . value = selection as LastMileOrderListItem [] }
2026-09-20 15:23:26 +08:00
function isRejectedReturn ( source : object ) {
const row = source as LastMileOrderListItem
return row . orderTypeCode === 'RETURN_RELABEL' && row . currentStatusCode === 'REJECTED'
}
function hasSelectedRejectedReturn () { return selectedRows . value . some ( isRejectedReturn ) }
2026-09-20 21:52:12 +08:00
function hasSelectedInboundPlan () { return selectedRows . value . some ( hasInboundPlan ) }
2026-09-28 17:26:26 +08:00
const inboundPlanExcludedOrderTypes = new Set ([ 'WAREHOUSE_TRANSFER' , 'RELABEL_APPLICATION' , 'RETURN_RELABEL' ])
function canGenerateInboundPlan ( row : LastMileOrderListItem ) {
return ! inboundPlanExcludedOrderTypes . has ( String ( row . orderTypeCode || row . orderType || '' ))
}
2026-08-28 13:54:44 +08:00
async function loadTransferWarehouseOptions () {
transferWarehouseLoading . value = true
transferWarehouseError . value = null
transferWarehouseOptions . value = []
try {
const warehouses = await listAllPlatformWarehouses ({ warehouseType : 'FBA' , status : 'ACTIVE' })
transferWarehouseOptions . value = warehouses . map ( toTransferWarehouseOption )
} catch ( error ) {
transferWarehouseError . value = getApiErrorDetails ( error , 'FBA 仓候选加载失败,请重试' )
} finally {
transferWarehouseLoading . value = false
}
}
function selectTransferWarehouse ( warehouseId : string ) {
const selected = transferWarehouseOptions . value . find (( warehouse ) => warehouse . warehouseId === String ( warehouseId || '' ))
Object . assign ( transferForm , selected
? { warehouseId : selected . warehouseId , warehouseCode : selected . warehouseCode , warehouseName : selected . warehouseName }
: { warehouseId : '' , warehouseCode : '' , warehouseName : '' })
}
2026-08-21 17:20:40 +08:00
function openTransfer () {
if ( ! selectedRows . value . length ) { ElMessage . warning ( '请至少选择 1 条尾程订单' ); return }
2026-09-20 15:23:26 +08:00
if ( hasSelectedRejectedReturn ()) { ElMessage . warning ( '已驳回的退货预报只能查看详情' ); return }
2026-08-21 17:20:40 +08:00
Object . assign ( transferForm , { warehouseId : '' , warehouseCode : '' , warehouseName : '' , remark : '' })
2026-08-28 13:54:44 +08:00
transferWarehouseOptions . value = []
transferWarehouseError . value = null
2026-08-21 17:20:40 +08:00
actionError . value = null
transferOpen . value = true
2026-08-28 13:54:44 +08:00
void loadTransferWarehouseOptions ()
2026-08-31 14:42:28 +08:00
}
2026-08-21 17:20:40 +08:00
async function submitTransfer () {
2026-08-28 13:54:44 +08:00
const selectedWarehouse = transferWarehouseOptions . value . find (( warehouse ) => warehouse . warehouseId === transferForm . warehouseId )
if ( ! selectedWarehouse ) {
ElMessage . warning ( '请选择目的 FBA 仓' )
2026-08-21 17:20:40 +08:00
return
}
actionLoading . value = true ; actionError . value = null
try {
2026-08-28 13:54:44 +08:00
await transferLastMileOrders ({
orderIds : selectedRows . value . map (( row ) => String ( row . id )),
warehouseId : selectedWarehouse . warehouseId ,
warehouseCode : selectedWarehouse . warehouseCode ,
warehouseName : selectedWarehouse . warehouseName ,
remark : transferForm . remark . trim () || undefined ,
})
2026-08-21 17:20:40 +08:00
transferOpen . value = false ; selectedRows . value = []; ElMessage . success ( '转仓成功' ); await load ()
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '转仓失败,请重试' ) }
finally { actionLoading . value = false }
}
2026-09-07 23:58:18 +08:00
function idempotencyKey ( prefix : string ) {
return ` ${ prefix } - ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } `
}
function openInboundPlan ( source ?: object ) {
const target = source ? source as LastMileOrderListItem : ( selectedRows . value . length === 1 ? selectedRows . value [ 0 ] : null )
if ( ! target ) { ElMessage . warning ( '生成入库计划时请选择 1 条尾程订单' ); return }
2026-09-20 15:23:26 +08:00
if ( isRejectedReturn ( target )) { ElMessage . warning ( '已驳回的退货预报只能查看详情' ); return }
2026-09-20 21:52:12 +08:00
if ( hasInboundPlan ( target )) { ElMessage . warning ( '当前订单已提交入库计划,不能重复生成' ); return }
2026-09-20 15:23:26 +08:00
if ( target . detained ) { ElMessage . warning ( '订单处于暂扣状态,解除暂扣后才能生成入库计划' ); return }
2026-09-07 23:58:18 +08:00
actionTarget . value = target
2026-09-10 15:40:55 +08:00
const volumeMinimum = typeof target . volume === 'number' && target . volume > 0 ? Math . ceil ( target . volume / 2 ) : 1
inboundPlanMinimumPalletCount . value = Math . max ( 1 , volumeMinimum ,
typeof target . pallets === 'number' ? target . pallets : 0 )
2026-09-14 16:38:51 +08:00
inboundPlanSourcePalletCount . value = null
inboundPlanPreviewError . value = ''
2026-09-15 12:35:46 +08:00
Object . assign ( inboundPlanForm , { inboundCode : target . inboundCode === '—' ? '' : target . inboundCode , palletCount : inboundPlanMinimumPalletCount . value , palletType : 'WOOD' , assigned : false , mergeRequired : false , urgent : false , remark : '' })
2026-09-07 23:58:18 +08:00
actionError . value = null
2026-10-22 14:08:06 +08:00
inboundPlanArrival . value = null
inboundPlanArrivalError . value = ''
2026-09-07 23:58:18 +08:00
inboundPlanOpen . value = true
2026-10-22 14:08:06 +08:00
void refreshInboundPlanArrival ( target )
2026-09-10 15:40:55 +08:00
void refreshManualPalletMinimum ( target )
}
2026-10-22 14:08:06 +08:00
/** 每次打开重新读取柜级权威日期,避免使用列表旧值或在页面推算默认日期。 */
async function refreshInboundPlanArrival ( target : LastMileOrderListItem ) {
const request = ++ inboundPlanArrivalRequest
inboundPlanArrivalLoading . value = true
try {
if ( ! target . sourceContainerBookingId ) {
inboundPlanArrivalError . value = '尾程订单未绑定来源柜,无法取得预计到仓时间'
return
}
const arrivals = await listContainerArrivalTimes ([ target . sourceContainerBookingId ])
if ( request !== inboundPlanArrivalRequest || ! inboundPlanOpen . value ) return
const arrival = arrivals . find ( item => item . sourceContainerBookingId === target . sourceContainerBookingId )
if ( ! arrival || arrival . version < 0 ) {
inboundPlanArrivalError . value = '来源柜预计到仓日期缺失或不一致,请先核对柜级时间'
return
}
inboundPlanArrival . value = arrival
} catch ( error ) {
if ( request === inboundPlanArrivalRequest && inboundPlanOpen . value ) {
inboundPlanArrivalError . value = getApiErrorDetails ( error , '预计到仓时间加载失败,请重新打开入库计划重试' ). message
}
} finally {
if ( request === inboundPlanArrivalRequest ) inboundPlanArrivalLoading . value = false
}
}
2026-09-10 15:40:55 +08:00
async function refreshManualPalletMinimum ( target : LastMileOrderListItem ) {
if ( ! target . sourceContainerBookingId ) return
2026-09-14 16:38:51 +08:00
inboundPlanPreviewLoading . value = true
inboundPlanPreviewError . value = ''
2026-09-10 15:40:55 +08:00
try {
const preview = await getContainerInboundPlanPreview ( target . sourceContainerBookingId )
if ( actionTarget . value ? . id !== target . id || ! inboundPlanOpen . value ) return
const order = preview . groups . flatMap (( group ) => group . orders ). find (( item ) => item . orderId === target . id )
2026-09-14 16:38:51 +08:00
if ( target . orderTypeCode === 'ONE_CLICK_DROPSHIP' ) {
const sourcePalletCount = Number ( order ? . sourcePalletCount || 0 )
if ( sourcePalletCount <= 0 ) throw new Error ( '一件代发订单缺少头程封托托盘快照' )
inboundPlanSourcePalletCount . value = sourcePalletCount
inboundPlanMinimumPalletCount . value = sourcePalletCount
inboundPlanForm . palletCount = sourcePalletCount
return
}
2026-09-10 15:40:55 +08:00
const suggested = Number ( order ? . suggestedPalletCount || 0 )
inboundPlanMinimumPalletCount . value = Math . max ( inboundPlanMinimumPalletCount . value , suggested )
inboundPlanForm . palletCount = Math . max ( inboundPlanForm . palletCount , inboundPlanMinimumPalletCount . value )
2026-09-14 16:38:51 +08:00
} catch ( error ) {
if ( actionTarget . value ? . id === target . id && inboundPlanOpen . value ) {
inboundPlanPreviewError . value = getApiErrorDetails ( error , '柜级托盘预览加载失败,请重试' ). message
}
} finally {
if ( actionTarget . value ? . id === target . id ) inboundPlanPreviewLoading . value = false
2026-09-10 15:40:55 +08:00
}
2026-09-07 23:58:18 +08:00
}
async function submitInboundPlan () {
const row = actionTarget . value
2026-09-08 10:54:36 +08:00
const frozenInboundCode = String ( row ? . inboundCode || '' ). trim ()
2026-09-14 16:38:51 +08:00
if ( inboundPlanIsDropship . value && ( inboundPlanSourcePalletCount . value == null
|| inboundPlanForm . palletCount !== inboundPlanSourcePalletCount . value )) {
ElMessage . warning ( '一件代发必须按头程已封托托盘数整体生成入库计划' )
return
}
2026-10-22 14:08:06 +08:00
if ( ! row || ! row . overseasWarehouseId || inboundPlanArrivalLoading . value || ! inboundPlanArrivalProjection . value . date || ! frozenInboundCode || frozenInboundCode === '—' || inboundPlanForm . palletCount < inboundPlanMinimumPalletCount . value ) {
2026-09-15 12:35:46 +08:00
ElMessage . warning ( '当前柜须已分配接柜仓、具备预计到仓时间,并完整填写托数' )
2026-08-27 16:34:32 +08:00
return
}
2026-09-07 23:58:18 +08:00
actionLoading . value = true ; actionError . value = null
2026-08-21 17:20:40 +08:00
try {
2026-09-10 17:05:48 +08:00
const result = await createManualInboundPlan ({
2026-09-07 23:58:18 +08:00
customerId : row . customerId , orderId : row . id , expectedOrderVersion : row . version ,
2026-09-08 10:54:36 +08:00
palletCount : inboundPlanForm . palletCount ,
2026-09-07 23:58:18 +08:00
palletType : inboundPlanForm . palletType , assigned : inboundPlanForm . assigned ,
mergeRequired : inboundPlanForm . mergeRequired , urgent : inboundPlanForm . urgent ,
remark : inboundPlanForm . remark . trim () || undefined ,
}, idempotencyKey ( 'manual-inbound-plan' ))
2026-09-10 17:05:48 +08:00
const palletCodesReady = result . pallets ? . length === result . palletCount
&& result . pallets . every (( pallet ) => Boolean ( pallet . warehousePalletId && pallet . palletNo ))
inboundPlanOpen . value = false ; selectedRows . value = []
ElMessage . success ( palletCodesReady
? '入库计划和托盘码已自动生成'
: '入库计划已保存,整柜计划齐全后将自动生成托盘码' )
await load ()
2026-09-07 23:58:18 +08:00
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '生成入库计划失败,请重试' ) }
finally { actionLoading . value = false }
}
function openHold ( source : object ) {
const row = source as LastMileOrderListItem
2026-09-18 09:59:12 +08:00
if ( ! row . detained && row . currentStatusCode !== 'INBOUND_COMPLETED' ) {
ElMessage . warning ( '货物完成入库后才能暂扣;暂扣只限制后续排车和出库' )
return
}
2026-09-07 23:58:18 +08:00
actionTarget . value = row
Object . assign ( holdForm , { held : ! row . detained , chargeable : row . detained ? false : true , reason : '' })
actionError . value = null
holdOpen . value = true
}
async function submitHold () {
const row = actionTarget . value
if ( ! row || ! holdForm . reason . trim ()) { ElMessage . warning ( '请输入变更原因' ); return }
actionLoading . value = true ; actionError . value = null
try {
await updateLastMileOrderHold ( row . id , { held : holdForm . held , chargeable : holdForm . chargeable , reason : holdForm . reason . trim (), expectedVersion : row . version }, idempotencyKey ( 'last-mile-hold' ))
holdOpen . value = false ; ElMessage . success ( holdForm . held ? '订单已暂扣' : '订单已解除暂扣' ); await load ()
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '暂扣状态保存失败,请重试' ) }
finally { actionLoading . value = false }
}
async function openManualNodes ( source : object ) {
const row = source as LastMileOrderListItem
actionTarget . value = row
manualNodesOpen . value = true
manualNodesLoading . value = true
actionError . value = null
manualNodeDefinitions . forEach (({ code }) => { manualNodeValues [ code ] = null })
try {
const result = await getLastMileOrderManualNodes ( row . id )
manualNodesVersion . value = result . version
result . nodes . forEach (( node ) => { manualNodeValues [ node . nodeCode ] = node . nodeAt })
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '人工节点加载失败,请重试' ) }
finally { manualNodesLoading . value = false }
}
async function submitManualNodes () {
const row = actionTarget . value
if ( ! row || ! manualNodeDefinitions . some (({ code }) => Boolean ( manualNodeValues [ code ]))) { ElMessage . warning ( '请至少填写一个节点时间' ); return }
actionLoading . value = true ; actionError . value = null
try {
await updateLastMileOrderManualNodes ( row . id , manualNodesVersion . value , manualNodeDefinitions . map (({ code }) => ({ nodeCode : code , nodeAt : manualNodeValues [ code ] })), idempotencyKey ( 'last-mile-manual-nodes' ))
manualNodesOpen . value = false ; ElMessage . success ( '人工节点已保存,该订单不再继承柜级节点' ); await load ()
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '人工节点保存失败,请重试' ) }
finally { actionLoading . value = false }
2026-08-27 17:49:05 +08:00
}
async function loadContainerOptions ( containerNo = '' ) {
const request = ++ containerOptionRequest
containerOptionsLoading . value = true
containerLoadError . value = null
try {
2026-08-28 13:54:44 +08:00
const result = await listContainerPlanProgress ({
2026-08-27 17:49:05 +08:00
page : 1 ,
size : 50 ,
containerNo : containerNo . trim () || undefined ,
})
if ( request !== containerOptionRequest ) return
containerOptions . value = result . records
} catch ( error ) {
if ( request !== containerOptionRequest ) return
containerOptions . value = []
containerLoadError . value = getApiErrorDetails ( error , '待分配柜列表加载失败,请重试' )
} finally {
if ( request === containerOptionRequest ) containerOptionsLoading . value = false
}
}
function openContainerConversion () {
selectedContainerBookingId . value = ''
containerOptions . value = []
containerLoadError . value = null
2026-09-08 23:57:12 +08:00
containerPreview . value = null
containerPreviewError . value = ''
2026-09-09 00:44:07 +08:00
containerConversionResult . value = null
containerConversionIdempotencyKey = ''
2026-08-27 17:49:05 +08:00
actionError . value = null
containerConvertOpen . value = true
void loadContainerOptions ()
}
2026-09-02 15:15:47 +08:00
function openContainerConversionFor ( container : ContainerPlanProgressSummaryDto ) {
selectedContainerBookingId . value = container . sourceContainerBookingId
containerOptions . value = [ container ]
containerLoadError . value = null
2026-09-08 23:57:12 +08:00
containerPreview . value = null
containerPreviewError . value = ''
2026-09-09 00:44:07 +08:00
containerConversionResult . value = null
containerConversionIdempotencyKey = ''
2026-09-02 15:15:47 +08:00
actionError . value = null
containerConvertOpen . value = true
2026-09-08 23:57:12 +08:00
void loadContainerPreview ( container . sourceContainerBookingId )
2026-09-02 15:15:47 +08:00
}
2026-09-09 00:44:07 +08:00
async function loadContainerPreview ( sourceContainerBookingId = selectedContainerBookingId . value , preserveResult = false ) {
if ( containerPreviewLoading . value && containerPreviewLoadingBookingId === sourceContainerBookingId ) return
if ( containerPreview . value ? . sourceContainerBookingId === sourceContainerBookingId ) return
const request = ++ containerPreviewRequest
2026-09-08 23:57:12 +08:00
containerPreview . value = null
containerPreviewError . value = ''
2026-09-09 00:44:07 +08:00
if ( ! preserveResult ) {
containerConversionResult . value = null
containerConversionIdempotencyKey = ''
}
if ( ! sourceContainerBookingId ) {
containerPreviewLoading . value = false
containerPreviewLoadingBookingId = ''
return
}
2026-09-08 23:57:12 +08:00
containerPreviewLoading . value = true
2026-09-09 00:44:07 +08:00
containerPreviewLoadingBookingId = sourceContainerBookingId
2026-08-27 17:49:05 +08:00
try {
2026-09-09 00:44:07 +08:00
const preview = await getContainerInboundPlanPreview ( sourceContainerBookingId )
if ( request !== containerPreviewRequest || selectedContainerBookingId . value !== sourceContainerBookingId || ! containerConvertOpen . value ) return
containerPreview . value = preview
2026-10-05 02:13:24 +08:00
if ( ! preserveResult ) containerConversionIdempotencyKey = preview . resourceRetry
? preview . resourceRetry . idempotencyKey : idempotencyKey ( 'container-inbound-plan' )
2026-08-27 16:34:32 +08:00
} catch ( error ) {
2026-09-09 00:44:07 +08:00
if ( request !== containerPreviewRequest || selectedContainerBookingId . value !== sourceContainerBookingId || ! containerConvertOpen . value ) return
2026-09-08 23:57:12 +08:00
containerPreviewError . value = getApiErrorDetails ( error , '加载整柜入库计划预览失败,请重试' ). message
2026-08-27 16:34:32 +08:00
} finally {
2026-09-09 00:44:07 +08:00
if ( request === containerPreviewRequest ) {
containerPreviewLoading . value = false
containerPreviewLoadingBookingId = ''
}
}
}
async function confirmContainerInboundPlans () {
const preview = containerPreview . value
2026-10-05 05:40:21 +08:00
if ( ! preview || containerConfirmDisabled . value ) return
if ( preview . resourceRetry ) containerConversionIdempotencyKey = preview . resourceRetry . idempotencyKey
else if ( ! containerConversionIdempotencyKey ) containerConversionIdempotencyKey = idempotencyKey ( 'container-inbound-plan' )
const resourceRecovery = Boolean ( preview . resourceRetry )
2026-09-09 00:44:07 +08:00
const bookingId = preview . sourceContainerBookingId
2026-10-05 02:13:24 +08:00
const commandSnapshot = preview . resourceRetry ?? preview
2026-09-09 00:44:07 +08:00
const existingPlanNos = new Set ( preview . groups . flatMap (( group ) => group . orders )
2026-09-10 15:40:55 +08:00
. filter (( order ) => order . readiness !== 'READY' && order . existingPlanNo )
2026-09-09 00:44:07 +08:00
. map (( order ) => String ( order . existingPlanNo )))
actionLoading . value = true
actionError . value = null
try {
const plans = await convertLastMileContainerToInboundPlans ({
sourceContainerBookingId : bookingId ,
2026-10-05 02:13:24 +08:00
snapshotVersion : commandSnapshot . snapshotVersion ,
snapshotHash : commandSnapshot . snapshotHash ,
2026-09-09 00:44:07 +08:00
}, containerConversionIdempotencyKey )
if ( selectedContainerBookingId . value !== bookingId || ! containerConvertOpen . value ) return
const existingPlanCount = plans . filter (( plan ) => existingPlanNos . has ( plan . planNo )). length
containerConversionResult . value = {
plans ,
existingPlanNos ,
newPlanCount : plans . length - existingPlanCount ,
existingPlanCount ,
2026-10-05 05:40:21 +08:00
resourceRecovered : resourceRecovery ,
2026-09-09 00:44:07 +08:00
}
2026-10-05 05:40:21 +08:00
ElMessage . success ( resourceRecovery
? `已完成 ${ plans . length } 个已有计划的资源确认`
: `已生成 ${ plans . length - existingPlanCount } 个入库计划` )
2026-09-09 00:44:07 +08:00
containerPreview . value = null
await Promise . all ([ load (), loadContainerPreview ( bookingId , true )])
} catch ( error ) {
actionError . value = getApiErrorDetails ( error , '按柜生成入库计划失败,请使用原操作重试' )
} finally {
actionLoading . value = false
2026-08-27 16:34:32 +08:00
}
2026-08-21 17:20:40 +08:00
}
2026-08-20 09:10:36 +08:00
2026-09-08 23:57:12 +08:00
function previewGroupTitle ( group : ContainerInboundPlanPreviewGroupDto ) {
if ( group . destinationType === 'FBA' ) return ` ${ group . destinationWarehouseName || group . destinationWarehouseCode || '未配置目的仓' } FBA仓`
return group . destinationType === 'ONE_CLICK_DROPSHIP' ? '一件代发' : '私人地址'
}
function destinationText ( order : { destinationWarehouseCode ?: string | null ; destinationWarehouseName ?: string | null }) {
return [ order . destinationWarehouseCode , order . destinationWarehouseName ]. filter ( Boolean ). join ( ' / ' ) || '—'
}
function orderTypeText ( orderType : string ) {
const labels : Record < string , string > = {
FIRST_LEG : '头程订单' ,
RELABEL_APPLICATION : '换标下单' ,
WAREHOUSE_TRANSFER : '仓储转运' ,
LOCAL_LOOSEBOARD : '本土散板' ,
RETURN_RELABEL : '退货换标' ,
ONE_CLICK_DROPSHIP : '一件代发' ,
2026-09-15 15:20:28 +08:00
CARGO_DISCARD : '货物废弃订单' ,
2026-09-08 23:57:12 +08:00
}
return labels [ orderType ] || orderType
}
function readinessText ( readiness : ContainerInboundPlanPreviewOrderDto [ 'readiness' ]) {
2026-09-11 16:14:14 +08:00
return readiness === 'READY' ? '可生成' : readiness === 'RESOURCE_PENDING' ? '将自动补齐' : readiness === 'PLANNED' ? '已有计划' : '已阻断'
2026-09-08 23:57:12 +08:00
}
function readinessType ( readiness : ContainerInboundPlanPreviewOrderDto [ 'readiness' ]) {
2026-09-11 16:14:14 +08:00
return readiness === 'READY' || readiness === 'RESOURCE_PENDING' ? 'success' : readiness === 'PLANNED' ? 'info' : 'danger'
2026-09-08 23:57:12 +08:00
}
function blockerText ( blockers : string []) {
const labels : Record < string , string > = {
MISSING_INBOUND_CODE : '缺少入仓码' ,
ORDER_STATUS_NOT_READY : '订单状态未就绪' ,
CONTAINER_NO_INCONSISTENT : '货柜号不一致' ,
RECEIVING_WAREHOUSE_INCONSISTENT : '收货仓不一致' ,
DESTINATION_NOT_READY : '目的地未就绪' ,
CARGO_FACT_INVALID : '货物数据无效' ,
2026-09-10 15:40:55 +08:00
PLAN_ALREADY_STARTED : '已有计划已开始作业,不能再补生成托盘' ,
PLAN_PALLET_BINDING_INCOMPLETE : '已有计划只绑定了部分真实托盘,请联系管理员处理' ,
2026-09-08 23:57:12 +08:00
DROPSHIP_PALLET_SNAPSHOT_MISSING : '缺少一件代发托盘快照' ,
2026-09-14 16:38:51 +08:00
DROPSHIP_PALLET_COUNT_MISMATCH : '一件代发计划托数与头程已封托托盘数不一致,请重新走单' ,
2026-09-09 00:44:07 +08:00
PALLET_COUNT_NOT_READY : '计划托数暂未就绪' ,
2026-09-08 23:57:12 +08:00
}
2026-09-09 00:44:07 +08:00
return [... new Set ( blockers . map (( code ) => labels [ code ] || '计划数据暂未就绪,请刷新或联系管理员' ))]. join ( '; ' ) || '—'
2026-09-08 23:57:12 +08:00
}
2026-09-09 00:44:07 +08:00
watch ( containerConvertOpen , ( open ) => {
if ( open ) return
containerPreviewRequest += 1
containerPreviewLoadingBookingId = ''
containerPreviewLoading . value = false
containerPreview . value = null
containerPreviewError . value = ''
containerConversionResult . value = null
containerConversionIdempotencyKey = ''
})
2026-10-04 16:57:16 +08:00
const discardReviewBusy = ref ( false )
function canReviewDiscard ( row : LastMileOrderListItem ) {
return access . can ( permission . cargoDiscardApprove )
&& row . orderTypeCode === 'CARGO_DISCARD'
&& row . currentStatusCode === 'PENDING_REVIEW'
&& Boolean ( row . orderNo && row . orderNo !== '—' )
&& /^[1-9]\d*$/ . test ( row . customerId )
}
async function reviewDiscard ( row : LastMileOrderListItem ) {
if ( discardReviewBusy . value || ! canReviewDiscard ( row )) return
discardReviewBusy . value = true
try {
let decision : 'APPROVE' | 'REJECT' = 'APPROVE'
let remark : string | undefined
try {
2026-10-20 10:11:31 +08:00
await ElMessageBox . confirm ( `请核对废弃申请 ${ row . orderNo } ,确认审核结论。` , `废弃申请审核 · ${ row . orderNo || '—' } ` , {
2026-10-20 23:34:11 +08:00
customClass : 'admin-density-confirm admin-workbench is-dialog' , confirmButtonText : '审核通过' , cancelButtonText : '驳回' , distinguishCancelAndClose : true ,
2026-10-04 16:57:16 +08:00
})
} catch ( action ) {
if ( action !== 'cancel' ) throw action
2026-10-20 10:11:31 +08:00
const { value } = await ElMessageBox . prompt ( '请填写驳回原因' , `驳回废弃申请 · ${ row . orderNo || '—' } ` , {
2026-10-20 23:34:11 +08:00
customClass : 'admin-density-confirm admin-workbench is-dialog' , inputType : 'textarea' ,
2026-10-04 16:57:16 +08:00
inputValidator : ( input ) => Boolean ( input ? . trim ()) && input . trim (). length <= 1000 || '请填写 1 至 1000 字驳回原因' ,
confirmButtonText : '确认驳回' , cancelButtonText : '取消' ,
})
decision = 'REJECT'
remark = value . trim ()
}
await reviewCargoDiscard ( row . orderNo , row . customerId , decision , remark )
ElMessage . success ( decision === 'APPROVE' ? '废弃申请已审核通过' : '废弃申请已驳回' )
await load ()
} catch ( error ) {
if ( error !== 'cancel' && error !== 'close' ) ElMessage . error ( getApiErrorDetails ( error , '废弃申请审核失败' ). message )
} finally {
discardReviewBusy . value = false
}
}
2026-09-20 15:23:26 +08:00
async function reviewReturn ( source : object ) {
const row = source as LastMileOrderListItem
const returnNo = row . sourceOrderNo
if ( ! returnNo || ! row . customerId ) return
try {
2026-10-20 23:34:11 +08:00
await ElMessageBox . confirm ( '审核通过后,仓库才可以签收这张退货预报。' , `退货预报审核 · ${ returnNo } ` , { customClass : 'admin-density-confirm admin-workbench is-dialog' , confirmButtonText : '审核通过' , cancelButtonText : '驳回' , distinguishCancelAndClose : true })
2026-09-20 15:23:26 +08:00
await reviewReturnOrder ( returnNo , row . customerId , 'APPROVE' )
ElMessage . success ( '退货预报已审核通过' )
await load ()
} catch ( error ) {
if ( error === 'cancel' ) {
try {
2026-10-20 23:34:11 +08:00
const { value } = await ElMessageBox . prompt ( '请填写驳回原因' , `驳回退货预报 · ${ returnNo } ` , { customClass : 'admin-density-confirm admin-workbench is-dialog' , inputType : 'textarea' , inputValidator : ( input ) => Boolean ( input ? . trim ()) || '请填写驳回原因' , confirmButtonText : '确认驳回' , cancelButtonText : '取消' })
2026-09-20 15:23:26 +08:00
await reviewReturnOrder ( returnNo , row . customerId , 'REJECT' , value )
ElMessage . success ( '退货预报已驳回' )
await load ()
} catch ( rejectError ) { if ( rejectError !== 'cancel' && rejectError !== 'close' ) ElMessage . error ( getApiErrorDetails ( rejectError , '退货预报审核失败' ). message ) }
} else if ( error !== 'close' ) ElMessage . error ( getApiErrorDetails ( error , '退货预报审核失败' ). message )
}
}
2026-09-23 11:25:23 +08:00
async function reviewDirectRelabel ( source : object ) {
const row = source as LastMileOrderListItem
if ( ! row . id || ! row . customerId ) return
try {
2026-10-20 23:34:11 +08:00
await ElMessageBox . confirm ( '审核通过后,贴标单将进入海外仓换标作业。' , `贴标单审核 · ${ row . orderNo || '—' } ` , { customClass : 'admin-density-confirm admin-workbench is-dialog' , confirmButtonText : '审核通过' , cancelButtonText : '驳回' , distinguishCancelAndClose : true })
2026-09-23 11:25:23 +08:00
await reviewDirectRelabelOrder ( row . id , row . customerId , 'APPROVE' )
ElMessage . success ( '贴标单已审核通过' )
await load ()
} catch ( error ) {
if ( error === 'cancel' ) {
try {
2026-10-20 23:34:11 +08:00
const { value } = await ElMessageBox . prompt ( '请填写驳回原因,客户可在贴标单详情查看并修改后重新提交。' , `驳回贴标单 · ${ row . orderNo || '—' } ` , { customClass : 'admin-density-confirm admin-workbench is-dialog' , inputType : 'textarea' , inputValidator : ( input ) => Boolean ( input ? . trim ()) || '请填写驳回原因' , confirmButtonText : '确认驳回' , cancelButtonText : '取消' })
2026-09-23 11:25:23 +08:00
await reviewDirectRelabelOrder ( row . id , row . customerId , 'REJECT' , value )
ElMessage . success ( '贴标单已驳回' )
await load ()
} catch ( rejectError ) { if ( rejectError !== 'cancel' && rejectError !== 'close' ) ElMessage . error ( getApiErrorDetails ( rejectError , '贴标单审核失败' ). message ) }
} else if ( error !== 'close' ) ElMessage . error ( getApiErrorDetails ( error , '贴标单审核失败' ). message )
}
}
2026-09-17 13:36:31 +08:00
function canConfirmRelabel ( row : { relabelStatusCode ?: string }) { return row . relabelStatusCode === 'CUSTOMER_CONFIRM_PENDING' }
2026-10-09 17:27:47 +08:00
function canReworkRelabel ( row : LastMileOrderListItem ) { return reworkRelabelApplications ( row ). length > 0 }
2026-09-20 15:47:56 +08:00
function hasRelabelActions ( row : LastMileOrderListItem ) {
return access . can ( permission . relabelOrderStatus )
&& Boolean ( row . relabelOrderNo )
&& ( canConfirmRelabel ( row ) || canReworkRelabel ( row ))
}
2026-09-20 21:52:12 +08:00
const inboundPlanCreatedStatuses = new Set ([
'INBOUND_PLANNED' ,
'PENDING_RECEIVING' ,
'WAIT_PDA_RECEIVING' ,
'INBOUND_MEASURING' ,
'WAIT_CUSTOMER_CONFIRMATION' ,
'PARTIALLY_RECEIVED' ,
'INBOUND_COMPLETED' ,
])
function hasInboundPlan ( row : LastMileOrderListItem ) {
return inboundPlanCreatedStatuses . has ( row . currentStatusCode )
|| Boolean ( row . inboundOrderNo && row . inboundOrderNo !== '—' )
}
2026-10-21 21:50:14 +08:00
const departedContainerNodes = new Set ([
'ACTUAL_DEPARTURE' , 'DEPARTED_ORIGIN_PORT' , 'IN_TRANSIT' , 'ARRIVED_DESTINATION_PORT' ,
'DISCHARGED_FROM_VESSEL' , 'PICKUP_READY' , 'PICKED_UP' , 'EMPTY_RETURNED' ,
])
function isBeforeDeparture ( row : LastMileOrderListItem ) {
return row . orderTypeCode === 'FIRST_LEG' && ! departedContainerNodes . has ( row . containerNode )
}
2026-09-18 16:48:40 +08:00
function lastMileRowActions ( row : LastMileOrderListItem ) : RowActionItem [] {
2026-10-21 21:50:14 +08:00
if ( statusTab . value === '开船前' || isBeforeDeparture ( row )) return [{ key : 'detail' , label : '查看' , priority : 500 }]
2026-09-20 21:52:12 +08:00
const inboundPlanCreated = hasInboundPlan ( row )
2026-09-18 16:48:40 +08:00
return [
{ key : 'detail' , label : '查看' , priority : 500 },
2026-10-04 16:57:16 +08:00
{ key : 'review-discard' , label : '审核' , priority : 480 , tone : 'warning' , visible : canReviewDiscard ( row ), disabled : discardReviewBusy . value },
2026-09-23 11:25:23 +08:00
{ key : 'review-direct-relabel' , label : '审核' , priority : 470 , tone : 'warning' , visible : access . can ( permission . lastMileOrderTransfer ) && row . orderTypeCode === 'RELABEL_APPLICATION' && row . currentStatusCode === 'PENDING_REVIEW' },
2026-09-20 15:23:26 +08:00
{ key : 'review-return' , label : '审核' , priority : 460 , tone : 'warning' , visible : access . can ( permission . returnOrderReview ) && row . orderTypeCode === 'RETURN_RELABEL' && row . currentStatusCode === 'PENDING_REVIEW' },
2026-09-18 16:48:40 +08:00
{ key : 'review' , label : '审核' , priority : 450 , tone : 'warning' , visible : access . can ( permission . warehouseTransferApprove ) && row . orderTypeCode === 'WAREHOUSE_TRANSFER' && row . currentStatusCode === 'PENDING_REVIEW' },
2026-10-19 17:56:31 +08:00
{ key : 'inbound-plan' , label : '入库' , priority : 400 , visible : access . can ( permission . inboundOrderCreate ) && canGenerateInboundPlan ( row ) && ! inboundPlanCreated },
2026-09-20 22:04:59 +08:00
{ key : 'hold' , label : row . detained ? '解除暂扣' : '暂扣订单' , priority : 300 , tone : 'warning' , visible : access . can ( permission . lastMileOrderTransfer ) && ( row . detained || row . currentStatusCode === 'INBOUND_COMPLETED' ) },
2026-10-19 17:56:31 +08:00
{ key : 'manual-nodes' , label : '修改节点' , priority : 200 , visible : access . can ( permission . lastMileOrderTransfer ) },
2026-09-18 16:48:40 +08:00
{ key : 'confirm-relabel' , label : '换标完成确认' , priority : 150 , visible : access . can ( permission . relabelOrderStatus ) && Boolean ( row . relabelOrderNo ) && canConfirmRelabel ( row ) },
2026-10-09 17:27:47 +08:00
{ key : 'rework-relabel' , label : '换标退回重做' , priority : 100 , tone : 'danger' , divided : true , visible : access . can ( permission . relabelOrderStatus ) && canReworkRelabel ( row ) },
2026-09-18 16:48:40 +08:00
]
}
function handleLastMileRowAction ( command : string , row : LastMileOrderListItem ) {
if ( command === 'detail' ) openDetail ( row )
2026-10-04 16:57:16 +08:00
else if ( command === 'review-discard' ) void reviewDiscard ( row )
2026-09-23 11:25:23 +08:00
else if ( command === 'review-direct-relabel' ) void reviewDirectRelabel ( row )
2026-09-20 15:23:26 +08:00
else if ( command === 'review-return' ) void reviewReturn ( row )
2026-09-18 16:48:40 +08:00
else if ( command === 'review' ) openTransferReview ( row )
else if ( command === 'inbound-plan' ) openInboundPlan ( row )
else if ( command === 'hold' ) openHold ( row )
else if ( command === 'manual-nodes' ) void openManualNodes ( row )
else if ( command === 'confirm-relabel' ) void confirmRelabel ( row )
else if ( command === 'rework-relabel' ) void reworkRelabel ( row )
2026-09-17 13:36:31 +08:00
}
async function confirmRelabel ( row : { relabelOrderNo ?: string }) {
const relabelNo = row . relabelOrderNo
if ( ! relabelNo ) return
try {
2026-10-20 23:34:11 +08:00
await ElMessageBox . confirm ( '确认仓库上传的换标结果无误并继续后续流程吗?' , `换标完成确认 · ${ relabelNo } ` , { customClass : 'admin-density-confirm admin-workbench is-dialog' , confirmButtonText : '确认并继续' , cancelButtonText : '取消' })
2026-09-17 13:36:31 +08:00
await confirmRelabelCompletion ( relabelNo )
ElMessage . success ( '已确认换标完成' )
await load ()
} catch ( error ) { if ( error !== 'cancel' && error !== 'close' ) ElMessage . error ( getApiErrorDetails ( error , '换标完成确认失败' ). message ) }
}
2026-10-09 17:27:47 +08:00
function reworkRelabel ( row : LastMileOrderListItem ) {
reworkOptions . value = reworkRelabelApplications ( row )
if ( ! reworkOptions . value . length ) return
reworkForm . relabelNo = reworkOptions . value [ 0 ]. relabelNo
reworkForm . remark = ''
reworkOpen . value = true
}
async function submitReworkRelabel () {
if ( reworkSaving . value || ! reworkForm . relabelNo || ! reworkForm . remark . trim ()) return
const relabelNo = reworkForm . relabelNo
reworkSaving . value = true
2026-09-17 13:36:31 +08:00
try {
const task = await getRelabelReviewTask ( relabelNo )
2026-10-09 17:27:47 +08:00
if ( task . taskStatus !== 'WAIT_REVIEW' ) { ElMessage . warning ( '仓库尚未提交完工复核,暂不能退回重做' ); return }
await returnRelabelForRework ( relabelNo , task . version , reworkForm . remark . trim (), crypto . randomUUID ())
2026-09-17 13:36:31 +08:00
ElMessage . success ( '已退回重新换标' )
2026-10-09 17:27:47 +08:00
reworkOpen . value = false
2026-09-17 13:36:31 +08:00
await load ()
2026-10-09 17:27:47 +08:00
} catch ( error ) { ElMessage . error ( getApiErrorDetails ( error , '重新换标失败' ). message ) }
finally { reworkSaving . value = false }
2026-09-14 20:21:02 +08:00
}
2026-10-26 13:43:08 +08:00
/** 开船前统一按入仓码联查头程详情,保留返回尾程列表的来源。 */
2026-08-19 16:45:51 +08:00
function openDetail ( source : object ) {
const row = source as LastMileOrderListItem
2026-10-21 21:50:14 +08:00
if ( isBeforeDeparture ( row )) {
2026-10-26 13:43:08 +08:00
const query = { tenantId : row . customerId , from : 'last-mile-orders' }
if ( row . inboundCode && row . inboundCode !== '—' ) {
void router . push ({ name : 'unified-order-detail-by-inbound-code' , params : { inboundCode : row . inboundCode }, query })
} else if ( ! row . orderNo || row . orderNo === '—' ) {
// 未生成尾程单的聚合行 ID 才是头程订单 ID。
void router . push ({ name : 'unified-order-detail' , params : { orderId : row . id }, query })
} else {
ElMessage . warning ( '该订单缺少入仓码,暂时无法定位头程详情' )
}
2026-10-21 21:50:14 +08:00
return
}
2026-08-21 11:20:09 +08:00
void router . push ({ name : 'overseas-last-mile-order-detail' , params : { orderId : row . id } })
2026-08-19 16:45:51 +08:00
}
2026-08-20 09:10:36 +08:00
2026-09-13 21:10:05 +08:00
function openTransferReview ( source : object ) {
transferReviewTarget . value = source as LastMileOrderListItem
transferReviewOpen . value = true
}
2026-08-21 11:20:09 +08:00
onMounted (() => { void load () })
2026-08-19 16:45:51 +08:00
</ script >
< template >
2026-10-20 23:34:11 +08:00
< AdminPage class = "admin-density admin-workbench" >< OperationErrorToast :details = "actionError" />< ListPageLayout aria -label =" 尾程订单列表 " >
2026-09-30 14:01:58 +08:00
2026-10-21 13:22:42 +08:00
< div class = "workbench-table is-page-surface" >< DataTable appearance = "integrated" :results-visible = "!loadError" : column -settings-mode =" statusTab = == ' 柜子 ' ? ' hide ' : ' show '" v -model:page = "page" v -model:page-size = "pageSize" :data = "rows" :columns = "columns" :loading = "loading" : total = "statusTab === '柜子' ? undefined : total" preference -key =" overseas -last-mile-orders " row -key =" id " empty -text =" 暂无尾程订单 " @ selection -change =" onSelectionChange " @ update :page = "load()" @ update : page -size =" page = 1 ; load ()" >
2026-10-20 23:34:11 +08:00
< template # toolbar -start >
< div class = "density-status-tabs workbench-list-tabs" role = "tablist" aria -label =" 尾程订单状态 " >
2026-10-03 04:35:55 +08:00
< button v-for = "(tab, index) in statusTabs" :key="tab" type="button" role="tab"
: aria -selected =" statusTab = == tab " : tabindex = "statusTab === tab ? 0 : -1"
: class = "{ 'is-active': statusTab === tab }"
@click ="selectStatusTab(tab)" @keydown ="handleStatusTabKeydown($event, index)" >{{ tab }}</ button >
2026-09-10 15:40:55 +08:00
<!-- 柜子工作台入口暂时隐藏 , 保留现有实现便于后续恢复 。 -- >
<!-- < el-radio-button value = "柜子" > 柜子工作台 ( {{ containerTotal }} ) </ el-radio-button > -->
2026-10-03 04:35:55 +08:00
</ div >
2026-10-20 23:34:11 +08:00
</ template >
< template # filters >
< section class = "last-mile-toolbar integrated-query-group" aria -label =" 尾程订单状态与筛选 " >
2026-10-08 15:12:49 +08:00
< QueryToolbar id = "last-mile-orders-advanced-filters" v-model = "filters" :fields="fields" :available-fields="availableFields" :loading="loading" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" :search-on-reset="false" @search="search" @reset="reset" >
2026-10-03 04:35:55 +08:00
< template v-if = "statusTab !== '柜子'" # actions >
< el-button :aria-expanded = "showAdvancedFilters" aria -controls =" last -mile-orders-advanced-filters " @click ="showAdvancedFilters = !showAdvancedFilters" >
< SlidersHorizontal :size = "16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
< ChevronUp v-if = "showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
</ el-button >
</ template >
</ QueryToolbar >
2026-08-19 16:45:51 +08:00
</ section >
</ template >
2026-09-30 14:01:58 +08:00
< template # notices >< template v-if = "loadError" >
2026-08-21 11:20:09 +08:00
< LoadErrorState title = "尾程订单加载失败" :message = "loadError.message" :loading = "loading" @retry ="load" >
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
2026-08-20 09:10:36 +08:00
</ LoadErrorState >
2026-09-30 14:01:58 +08:00
</ template >< LoadErrorState v-if = "actionError" title="尾程订单操作失败" :message="actionError.message" compact >
2026-08-27 15:56:13 +08:00
< template # details >< ApiErrorMeta :details = "actionError" /></ template >
2026-09-30 14:01:58 +08:00
</ LoadErrorState ></ template >
< template v-if = "statusTab === '柜子'" #results><section class="container-workbench" :aria-busy="loading" >
2026-09-02 14:39:34 +08:00
< header class = "container-workbench__header" >
2026-10-20 15:48:23 +08:00
< div >< strong > 货柜工作台 </ strong ></ div >
2026-09-08 11:50:32 +08:00
< el-tag type = "warning" effect = "plain" >{{ containerTotal }} 个货柜 </ el-tag >
2026-09-02 14:39:34 +08:00
</ header >
< div v-if = "containerRows.length" class="container-workbench__grid" >
2026-10-20 23:34:11 +08:00
< article v-for = "container in containerRows" :key="container.sourceContainerBookingId" class="container-workbench-card workbench-panel is-compact" >
2026-09-07 20:25:14 +08:00
< span class = "container-workbench-card__top" >< strong : title = "container.containerNo || '柜号待补充'" >{{ container . containerNo || '柜号待补充' }}</ strong ></ span >
2026-09-07 15:07:39 +08:00
< span class = "container-workbench-card__warehouse" >
2026-09-07 20:25:14 +08:00
< small > 海外接柜仓 </ small >< strong :title = "receivingWarehouseLabel(container)" >{{ receivingWarehouseLabel ( container ) }}</ strong >
< small v-if = "containerSnapshotBlockReason(container)" class="is-locked" :title="containerSnapshotBlockReason(container)" >{{ containerSnapshotBlockReason ( container ) }}</ small >
< small v-else-if = "!container.canChangeReceivingWarehouse && container.receivingWarehouseLockReason" class="is-locked" :title="container.receivingWarehouseLockReason" >{{ container.receivingWarehouseLockReason }}</ small >
2026-09-07 15:07:39 +08:00
</ span >
2026-09-02 14:39:34 +08:00
< span class = "container-workbench-card__arrival" >< small > 计划到仓 </ small >< strong >{{ formatDateTime ( container . plannedArrivalAt ) }}</ strong ></ span >
< span class = "container-workbench-card__pending" >
< strong >{{ pendingInboundCodeCount ( container ) }}</ strong >< span > 个入仓码待生成计划 </ span >
</ span >
< span class = "container-workbench-card__facts" >
< span > 共 {{ container . inboundCodeCount }} 个入仓码 </ span >
2026-09-10 17:05:48 +08:00
< span v-if = "container.resourcePendingPlanCount" > 托盘码自动生成异常 , 可重试 </ span >
2026-09-02 14:39:34 +08:00
</ span >
2026-09-14 20:03:27 +08:00
< span class = "container-workbench-card__actions density-actions" >
2026-09-02 15:15:47 +08:00
< el-button class = "container-workbench-card__detail" plain @click ="openContainerDetail(container)" > 查看详情 </ el -button >
2026-09-08 11:45:30 +08:00
< el-tooltip :content = "receivingWarehouseChangeDisabledReason(container)" :disabled = "!receivingWarehouseChangeDisabledReason(container)" placement = "top" >
< span >< el-button plain : title = "receivingWarehouseChangeDisabledReason(container) || '修改接柜仓'" :disabled = "Boolean(receivingWarehouseChangeDisabledReason(container))" @click ="openReceivingWarehouseChange(container)" >< Pencil :size = "15" /> 修改接柜仓 </ el-button ></ span >
</ el-tooltip >
2026-10-19 17:56:31 +08:00
< el-button v-if = "access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading || !canGenerateContainerPlans(container)" @click="openContainerConversionFor(container)" >{{ container.resourcePendingPlanCount && ! container.pendingPlanCount ? ' 重试生成托盘码 ' : ' 入库 ' }} </ el -button >
2026-09-02 15:15:47 +08:00
</ span >
</ article >
2026-09-02 14:39:34 +08:00
</ div >
2026-09-08 11:50:32 +08:00
< el-empty v-else-if = "!loading" description="暂无货柜数据" />
2026-09-02 14:39:34 +08:00
< div v-if = "containerTotal" class="container-workbench__pagination" >
2026-09-08 11:50:32 +08:00
< span > 共 {{ containerTotal }} 个货柜 </ span >
2026-09-02 14:39:34 +08:00
< el-pagination :current-page = "page" :page-size = "pageSize" : page -sizes ="[ 10 , 20 , 50 ]" :total = "containerTotal" layout = "sizes, prev, pager, next" background @ current -change =" changeContainerPage " @ size -change =" changeContainerPageSize " />
</ div >
2026-09-30 14:01:58 +08:00
</ section ></ template >
2026-10-21 21:50:14 +08:00
< template v-if = "statusTab !== '柜子' && statusTab !== '开船前' && !selectedRows.some(isBeforeDeparture)" #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!palletMergeSelectionEditable || actionLoading" @click="submitPalletMerge(true)">合托</el-button><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!canCancelPalletMerge || actionLoading" @click="submitPalletMerge(false)">取消合托</el-button><el-button v-if="selectedReturnRows.length" type="warning" @click="openReturnPackingPlan"><PackagePlus :size="16" />创建退货合箱计划({{ selectedReturnRows.length }}) </el-button><el-button v-if="access.can(permission.lastMileOrderExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || hasSelectedRejectedReturn() || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate) && selectedRows.length === 1 && canGenerateInboundPlan(selectedRows[0])" type="primary" plain :disabled="hasSelectedInboundPlan() || actionLoading" @click="openInboundPlan()">入库</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion" > 按柜批量生成 </ el -button ></ template >
2026-09-22 08:47:43 +08:00
< template # relabelOrderNo = "{ row }" >< div v-for = "application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.relabelNo }}</div><span v-if="!row.relabelApplications?.length" >{{ row.relabelOrderNo }}</ span ></ template >
2026-10-12 21:19:07 +08:00
< template # relabelStatus = "{ row }" >< div v-for = "application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.statusLabel }}</div><span v-if="!row.relabelApplications?.length" >{{ row.relabelStatus }}</ span ></ template >
2026-09-30 14:01:58 +08:00
< template # actions = "{ row }" >< RowActions text -actions : actions = "lastMileRowActions(row as LastMileOrderListItem)" : more -label ="` 更多尾程订单操作 · $ { ( row as LastMileOrderListItem ) .orderNo || ' 当前记录 ' } `" @action ="handleLastMileRowAction($event, row as LastMileOrderListItem)" /></ template >
2026-10-09 16:11:59 +08:00
< template # businessOrderNo = "{ row }" >< el-button v-if = "businessOrderLink(row.businessType, row.businessOrderNo, access.can)" link type="primary" @click="router.push(businessOrderLink(row.businessType, row.businessOrderNo, access.can)!)" >{{ row.businessOrderNo }} </ el -button >< span v-else >{{ row.businessOrderNo }}</ span ></ template >
2026-09-01 11:03:45 +08:00
< template # orderNo = "{ row }" >< OrderReferenceText : value = "String(row.orderNo || '')" copyable /></ template >
2026-10-20 14:00:49 +08:00
< template # cartonDifference = "{ row }" >< span : class = "{ 'package-difference--nonzero': row.orderTypeCode !== 'RETURN_RELABEL' && typeof row.cartonDifference === 'number' && row.cartonDifference !== 0 }" >{{ row . orderTypeCode === 'RETURN_RELABEL' ? '—' : packageDifferenceText ( row . cartonDifference ) }}</ span ></ template >
< template # receiptPallets = "{ row }" >{{ row . orderTypeCode === 'RETURN_RELABEL' ? returnDimension ( row ) ? . receivedPalletCount ?? '—' : row . pallets }}</ template >
< template # receiptBoxes = "{ row }" >{{ row . orderTypeCode === 'RETURN_RELABEL' ? returnDimension ( row ) ? . receivedBoxCount ?? '—' : row . cartons }}</ template >
< template # returnParcels = "{ row }" >{{ returnDimension ( row ) ? . receivedParcelCount ?? '—' }}</ template >
< template # returnExpected = "{ row }" >{{ returnDimension ( row ) ? . expectedQuantity ?? '—' }}</ template >
< template # returnReceived = "{ row }" >{{ returnDimension ( row ) ? . receivedQuantity ?? '—' }}</ template >
< template # returnRemaining = "{ row }" >{{ returnDimension ( row ) ? . remainingQuantity ?? '—' }}</ template >
< template # nonReturnQuantity = "{ row }" >{{ row . orderTypeCode === 'RETURN_RELABEL' ? '—' : row . quantity }}</ template >
2026-08-21 17:20:40 +08:00
< template # pagination -left > 共 {{ total }} 条尾程订单 · 已选 {{ selectedRows . length }} 条 </ template >
2026-10-20 23:34:11 +08:00
</ DataTable ></ div >
2026-08-19 16:45:51 +08:00
</ ListPageLayout ></ AdminPage >
2026-09-13 21:10:05 +08:00
< WarehouseTransferReviewDialog v-if = "transferReviewTarget" v-model="transferReviewOpen" :customer-id="transferReviewTarget.customerId" :customer-name="transferReviewTarget.owner" :transfer-no="transferReviewTarget.orderNo" @reviewed="load()" />
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "transferOpen" :title="`转仓 · ${selectedRows.length} 票订单`" fullscreenable confirm-text="确认转仓" :loading="transferWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!transferForm.warehouseId || Boolean(transferWarehouseError)" @confirm="submitTransfer" >
2026-10-20 10:11:31 +08:00
< LoadErrorState v-if = "transferWarehouseError" title="FBA 仓候选加载失败" :message="transferWarehouseError.message" compact @retry="loadTransferWarehouseOptions"><template #details><ApiErrorMeta :details="transferWarehouseError" /></ template ></ LoadErrorState >
< section v-else class = "density-dialog-section" data -tone =" work " aria -label =" 转仓登记 " >< h3 class = "density-dialog-section__title" > 转仓登记 </ h3 >
< el-form class = "density-form" label -position =" left " label -width =" 110px " :disabled = "actionLoading" >< table class = "density-table density-table--editable" >< tbody >
< tr >< td >< el-form-item label = "目的 FBA 仓" required >< el-select v-model = "transferForm.warehouseId" filterable clearable placeholder="请选择目的 FBA 仓" @update:model-value="selectTransferWarehouse"><el-option v-for="warehouse in transferWarehouseOptions" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" /></ el-select ></ el-form-item ></ td ></ tr >
2026-10-20 14:52:06 +08:00
< tr >< td >< el-form-item label = "备注" >< el-input v-model = "transferForm.remark" type="textarea" maxlength="500" /></ el-form-item ></ td ></ tr >
2026-10-20 10:11:31 +08:00
</ tbody ></ table ></ el-form >
2026-08-28 13:54:44 +08:00
< el-alert v-if = "!transferWarehouseLoading && !transferWarehouseOptions.length" title="暂无可用 FBA 仓候选" type="info" :closable="false" show -icon />
2026-10-20 10:11:31 +08:00
</ section >
2026-08-21 17:20:40 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "containerConvertOpen" :title="`按柜生成入库计划预览${containerPreview?.containerNo ? ' · ' + containerPreview.containerNo : ''}`" width="min(1240px, calc(100vw - 80px))" fullscreenable :confirm-text="containerConfirmText" :loading="containerOptionsLoading || containerPreviewLoading" :confirm-loading="actionLoading" :confirm-disabled="containerConfirmDisabled" @confirm="confirmContainerInboundPlans" >
2026-10-20 10:11:31 +08:00
< div class = "density-dialog-sections" >
< section class = "density-dialog-section" data -tone =" work " aria -label =" 选择待分配柜 " >< h3 class = "density-dialog-section__title" > 选择待分配柜 < InlineHelp title = "按柜生成说明" > 先读取服务端全柜预览 , 确认分组 、 托数和阻断原因 ; 确认后整柜只提交一次生成命令 。 </ InlineHelp ></ h3 >
< LoadErrorState v-if = "containerLoadError" title="待分配柜加载失败" :message="containerLoadError.message" compact @retry="loadContainerOptions()"><template #details><ApiErrorMeta :details="containerLoadError" /></ template ></ LoadErrorState >
< el-form v-else class = "density-form" label -position =" left " label -width =" 110px " :disabled = "actionLoading" >< table class = "density-table density-table--editable" >< tbody >< tr >< td >< el-form-item label = "待分配柜" required >< el-select v-model = "selectedContainerBookingId" filterable remote clearable :remote-method="loadContainerOptions" :loading="containerOptionsLoading" :disabled="actionLoading" placeholder="输入柜号搜索并选择" @change="loadContainerPreview"><el-option v-for="container in containerOptions" :key="container.sourceContainerBookingId" :value="container.sourceContainerBookingId" :label="`${container.containerNo || '柜号缺失'} · ${container.totalOrderCount} 票`" /></ el-select ></ el-form-item ></ td ></ tr ></ tbody ></ table ></ el-form >
2026-09-09 00:44:07 +08:00
</ section >
2026-10-20 10:11:31 +08:00
< el-alert v-if = "containerPreviewError" type="error" :closable="false" show-icon :title="containerPreviewError" />
< div v-if = "containerPreview" class="container-preview density-dialog-sections" >
< section class = "density-dialog-section" data -tone =" summary " aria -label =" 全柜预览汇总 " >< h3 class = "density-dialog-section__title" > 全柜预览 </ h3 >
< table class = "density-table last-mile-dialog-facts" >< tbody >
< tr >< th scope = "row" > 柜号 </ th >< td >< strong class = "container-preview__identity" : title = "containerPreview.containerNo || '未录入货柜号'" >{{ containerPreview . containerNo || '未录入货柜号' }}</ strong ></ td >< th scope = "row" > 海外接柜仓 </ th >< td >{{ [ containerPreview . overseasWarehouseCode , containerPreview . overseasWarehouseName ]. filter ( Boolean ). join ( ' / ' ) || '未关联海外仓' }}</ td ></ tr >
< template v-if = "!containerIsResourceRecovery"><tr><th scope="row">头程已有托盘码</th><td>{{ containerPreview.sourcePalletCount }}</td><th scope="row">海外仓补充托盘码</th><td>{{ containerPreview.supplementPalletCount }}</td></tr><tr><th scope="row">最终生成托盘</th><td>{{ containerPreview.pendingPalletCount }}</td><th scope="row" > 本次处理订单 </ th >< td >{{ containerPreview . pendingOrderCount }}</ td ></ tr ></ template >
< tr v-else >< th scope = "row" > 已保存计划托盘 </ th >< td >{{ containerPreview . totalPlannedPalletCount }}</ td >< th scope = "row" > 资源确认 </ th >< td >{{ containerConversionResult ? . resourceRecovered ? '已确认' : '待确认' }}</ td ></ tr >
</ tbody ></ table >
</ section >
< el-alert v-if = "containerPreview.blockedOrderCount > 0" type="error" :closable="false" show-icon title="存在阻断订单,暂不可确认生成;请按下方原因处理后重新预览。" />
< el-alert v-else-if = "containerPreview.resourceRetry && !containerConversionResult" type="warning" :closable="false" show-icon title="本柜计划和托盘已保存,资源仍待确认。请重试原操作完成资源确认。" />
< el-alert v-else-if = "!containerConversionResult" type="success" :closable="false" show-icon :title="`预览已就绪:头程已有 ${containerPreview.sourcePalletCount} 个托盘码,海外仓补充 ${containerPreview.supplementPalletCount} 个,最终生成 ${containerPreview.pendingPalletCount} 个托盘。`" />
< section v-if = "containerConversionResult" class="density-dialog-section container-conversion-result" data-tone="summary" aria-label="生成结果" >
< h3 class = "density-dialog-section__title" >{{ containerConversionResult . resourceRecovered ? '资源确认完成' : '生成完成' }}</ h3 >
< p class = "density-dialog-note" >< span v-if = "containerConversionResult.resourceRecovered" > 已确认 {{ containerConversionResult.plans.length }} 个已有计划 </ span >< span v-else > 新建 {{ containerConversionResult.newPlanCount }} 个 / 已有 {{ containerConversionResult.existingPlanCount }} 个 </ span ></ p >
< table class = "density-table" >< thead >< tr >< th > 计划号 </ th >< th > 状态 </ th >< th > 托盘码 </ th ></ tr ></ thead >< tbody class = "container-conversion-result__plans" >< tr v-for = "plan in containerConversionResult.plans" :key="plan.planNo" class="container-conversion-result__plan"><td>{{ plan.planNo }}</td><td><el-tag size="small" :type="containerConversionResult.existingPlanNos.has(plan.planNo) ? 'info' : 'success'">{{ containerConversionResult.existingPlanNos.has(plan.planNo) ? '已有计划' : '本次新建' }}</el-tag></td><td><template v-if="plan.pallets.length"><span v-for="(pallet, index) in plan.pallets" :key="pallet.id" class="container-conversion-result__pallet" >{{ index ? '、' : '' }}{{ pallet.palletNo || '—' }}</ span ></ template >< span v-else > 暂无托盘码 </ span ></ td ></ tr ></ tbody ></ table >
</ section >
< section v-for = "group in containerPreview.groups" :key="`${group.destinationType}-${group.destinationWarehouseId || group.destinationWarehouseCode || 'default'}`" class="density-dialog-section" data-tone="source" aria-label="目的地分组明细" >
< h3 class = "density-dialog-section__title" >{{ previewGroupTitle ( group ) }} : {{ containerIsResourceRecovery ? `已有 ${ group . plannedPalletCount } 托` : `最终 ${ group . pendingPalletCount } 托` }}</ h3 >
< p class = "density-dialog-note" >< span v-if = "containerIsResourceRecovery" > 已保存计划托数 {{ group.plannedPalletCount }}</ span >< span v-else > 头程已有 {{ group.sourcePalletCount }} 托 / 海外仓补充 {{ group.supplementPalletCount }} 托 / 本次处理 {{ group.pendingOrderCount }} 票 </ span ></ p >
2026-09-08 23:57:12 +08:00
< el-table :data = "group.orders" size = "small" border >
< el-table-column label = "入仓码" min -width =" 148 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . inboundCode || '—' }}</ template ></ el-table-column >
< el-table-column prop = "businessOrderNo" label = "订单号" min -width =" 150 " show -overflow-tooltip />
< el-table-column label = "客户" min -width =" 140 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . customerName || '—' }}</ template ></ el-table-column >
< el-table-column label = "业务类型" min -width =" 112 " show -overflow-tooltip >< template # default = "{ row }" >{{ orderTypeText ( row . orderType ) }}</ template ></ el-table-column >
< el-table-column label = "目的地" min -width =" 190 " show -overflow-tooltip >< template # default = "{ row }" >{{ destinationText ( row ) }}</ template ></ el-table-column >
2026-09-11 15:39:47 +08:00
< el-table-column label = "头程已有" width = "94" align = "right" >< template # default = "{ row }" >{{ row . sourcePalletCount }}</ template ></ el-table-column >
< el-table-column label = "海外仓补充" width = "102" align = "right" >< template # default = "{ row }" >{{ row . supplementPalletCount }}</ template ></ el-table-column >
< el-table-column label = "最终托数" width = "94" align = "right" >< template # default = "{ row }" >{{ row . targetPalletCount }}</ template ></ el-table-column >
2026-09-08 23:57:12 +08:00
< el-table-column label = "已有计划托数" width = "118" align = "right" >< template # default = "{ row }" >{{ row . plannedPalletCount ?? '—' }}</ template ></ el-table-column >
< el-table-column label = "可生成性" width = "104" >< template # default = "{ row }" >< el-tag :type = "readinessType(row.readiness)" effect = "light" >{{ readinessText ( row . readiness ) }}</ el-tag ></ template ></ el-table-column >
< el-table-column label = "阻断原因" min -width =" 210 " show -overflow-tooltip >< template # default = "{ row }" >{{ blockerText ( row . blockers ) }}</ template ></ el-table-column >
</ el-table >
</ section >
</ div >
</ div >
2026-08-28 14:33:57 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "containerDetailOpen" :title="`柜子详情 · ${containerDetail?.summary.containerNo || '加载中'}`" width="min(1120px, calc(100vw - 40px))" :loading="containerDetailLoading" :show-footer="false" >
2026-08-28 13:54:44 +08:00
< LoadErrorState v-if = "containerDetailError" title="柜级详情加载失败" :message="containerDetailError.message" compact >
< template # details >< ApiErrorMeta :details = "containerDetailError" /></ template >
</ LoadErrorState >
< template v-else-if = "containerDetail" >
2026-10-20 23:34:11 +08:00
< div class = "container-detail-summary workbench-panel is-compact" >
2026-08-28 13:54:44 +08:00
< span > 柜号 : {{ containerDetail . summary . containerNo || '—' }}</ span >
2026-09-07 20:25:14 +08:00
< span :title = "`海外接柜仓:${receivingWarehouseLabel(containerDetail.summary)}`" > 海外接柜仓 : {{ receivingWarehouseLabel ( containerDetail . summary ) }}</ span >
2026-08-28 13:54:44 +08:00
< span > 计划到仓 : {{ formatDateTime ( containerDetail . summary . plannedArrivalAt ) }}</ span >
< span > 订单 : {{ containerDetail . summary . totalOrderCount }}</ span >
2026-09-02 14:39:34 +08:00
< span > 入仓码 : {{ containerDetail . summary . inboundCodeCount }}</ span >
< span > 入库计划 : {{ containerDetail . summary . planCount }} , 待生成 {{ pendingInboundCodeCount ( containerDetail . summary ) }}</ span >
2026-09-07 15:07:39 +08:00
< span v-if = "!containerDetail.summary.canChangeReceivingWarehouse" > 修改状态 : {{ containerDetail.summary.receivingWarehouseLockReason || ' 当前已锁定 ' }}</ span >
2026-08-28 13:54:44 +08:00
</ div >
2026-10-20 23:34:11 +08:00
< div class = "workbench-table" >< DataTable :data = "containerDetail.orders" :columns = "containerDetailColumns" :loading = "containerDetailLoading" preference -key =" overseas -last-mile-container-detail " row -key =" orderId " :total = "undefined" empty -text =" 该柜暂无尾程订单 " >
2026-09-07 20:45:31 +08:00
< template # orderStatus = "{ row }" >{{ lastMileStatusLabel (( row as ContainerPlanProgressDetailDto [ 'orders' ][ number ]). orderStatus ) }}</ template >
< template # planStatus = "{ row }" >{{ inboundPlanStatusLabel (( row as ContainerPlanProgressDetailDto [ 'orders' ][ number ]). planStatus ) }}</ template >
2026-08-28 13:54:44 +08:00
< template # plannedArrivalAt = "{ row }" >{{ formatDateTime (( row as ContainerPlanProgressDetailDto [ 'orders' ][ number ]). plannedArrivalAt ) }}</ template >
2026-10-20 23:34:11 +08:00
</ DataTable ></ div >
2026-08-28 13:54:44 +08:00
</ template >
2026-09-02 14:39:34 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "receivingWarehouseOpen" title="修改海外接柜仓" width="min(600px, calc(100vw - 28px))" confirm-text="确认修改" :loading="receivingWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!selectedReceivingWarehouse || Boolean(receivingWarehouseError)" @confirm="submitReceivingWarehouseChange" >
2026-09-07 15:07:39 +08:00
< LoadErrorState v-if = "receivingWarehouseError" title="海外接柜仓候选加载失败" :message="receivingWarehouseError.message" compact @retry="loadReceivingWarehouseOptions" >
< template # details >< ApiErrorMeta :details = "receivingWarehouseError" /></ template >
</ LoadErrorState >
2026-09-14 20:03:27 +08:00
< el-form class = "density-form" v-else label -position =" top " >
2026-09-07 15:07:39 +08:00
< el-form-item label = "当前海外接柜仓" >< el-input :model-value = "receivingWarehouseLabel(receivingWarehouseTarget)" readonly /></ el-form-item >
< el-form-item label = "新的海外接柜仓" required >
< el-select v-model = "receivingWarehouseForm.warehouseId" filterable clearable placeholder="请选择 ACTIVE 海外接柜仓" >
2026-09-07 20:25:14 +08:00
< el-option v-for = "warehouse in receivingWarehouseOptions" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" :disabled="warehouse.warehouseId === receivingWarehouseTarget?.overseasWarehouseId" />
2026-09-07 15:07:39 +08:00
</ el-select >
</ el-form-item >
2026-09-07 20:25:14 +08:00
< el-alert v-if = "receivingWarehouseForm.warehouseId === receivingWarehouseTarget?.overseasWarehouseId" title="新的海外接柜仓必须与当前仓不同" type="warning" :closable="false" show -icon />
2026-09-07 15:07:39 +08:00
< el-alert v-if = "!receivingWarehouseLoading && !receivingWarehouseOptions.length" title="暂无可用的 ACTIVE 海外接柜仓" type="info" :closable="false" show -icon />
2026-10-20 14:52:06 +08:00
< el-form-item label = "备注" >< el-input v-model = "receivingWarehouseForm.remark" type="textarea" :rows="3" maxlength="500" show -word-limit /></ el-form-item >
2026-09-07 15:07:39 +08:00
< LoadErrorState v-if = "receivingWarehouseActionError" title="海外接柜仓修改失败" :message="receivingWarehouseActionError.message" compact >
< template # details >< ApiErrorMeta :details = "receivingWarehouseActionError" /></ template >
</ LoadErrorState >
</ el-form >
</ AppDialog >
2026-10-22 23:10:09 +08:00
< AppDialog class = "inbound-plan-dialog admin-density-dialog admin-workbench is-dialog" v-model = "inboundPlanOpen" :title="`入库计划 · ${actionTarget?.orderNo || '—'}`" width="min(1160px, calc(100vw - 48px))" top="6vh" fullscreenable confirm-text="保存并生成入库单" :loading="inboundPlanPreviewLoading || inboundPlanArrivalLoading" :confirm-loading="actionLoading" :confirm-disabled="inboundPlanSubmitDisabled" @confirm="submitInboundPlan" >
< div class = "inbound-plan-content" >
2026-10-20 10:11:31 +08:00
< el-alert v-if = "inboundPlanPreviewError" :title="inboundPlanPreviewError" type="error" :closable="false" show -icon />
2026-10-22 14:08:06 +08:00
< el-alert v-if = "inboundPlanArrivalError" :title="inboundPlanArrivalError" type="error" :closable="false" show -icon />
2026-10-22 23:10:09 +08:00
< el-form class = "inbound-plan-form command-form density-form density-dialog-sections" label -position =" left " label -width =" 110px " :disabled = "actionLoading" >
< section class = "density-dialog-section" data -tone =" source " aria -label =" 入库来源 " >< h3 class = "density-dialog-section__title" > 来源摘要 < InlineHelp title = "入库来源说明" > 优先使用整柜人工维护的预计到仓时间 ; 未维护时 , 按到港当地日期加两个自然日默认推算 。 首个入库计划生成后冻结日期 , 同柜计划保持一致 。 接柜仓按柜统一 ; 如需变更 , 请在柜子工作台执行 “ 变更接柜仓 ”。 </ InlineHelp ></ h3 >
< table class = "density-table last-mile-dialog-facts inbound-plan-source" >< colgroup >< col class = "inbound-plan-source__label" />< col />< col class = "inbound-plan-source__label" />< col /></ colgroup >< tbody >
< tr >< th scope = "row" > 预计到仓时间 </ th >< td colspan = "3" >< div class = "inbound-plan-arrival" >{{ inboundPlanExpectedArrival }} < el-button v-if = "access.can('platform:firstleg:terminal-drayage:list') && actionTarget?.containerNo" link type="primary" @click="router.push({ name: 'overseas-container-workbench', query: { keyword: actionTarget.containerNo } })" > 前往货柜维护预计到仓 </ el -button ></ div ></ td ></ tr >
< tr >< th scope = "row" > 计划入库仓 </ th >< td >{{ inboundPlanWarehouseLabel || '—' }}</ td >< th scope = "row" > 入仓码 </ th >< td >{{ inboundPlanForm . inboundCode || '—' }}</ td ></ tr >
2026-10-20 10:11:31 +08:00
</ tbody ></ table >
</ section >
2026-10-22 23:10:09 +08:00
< section class = "density-dialog-section" aria -label =" 入库计划登记 " >< h3 class = "density-dialog-section__title" > 计划设置 </ h3 >
2026-10-20 10:11:31 +08:00
< table class = "density-table density-table--editable last-mile-command-table" >< tbody >
2026-10-26 15:51:07 +08:00
< tr >< td >< el-form-item label = "托数" required >< div class = "inbound-plan-pallet-field" >< span v-if = "inboundPlanIsDropship" class="frozen-pallet-count">{{ inboundPlanForm.palletCount }}</span><el-input-number v-else v-model="inboundPlanForm.palletCount" :min="inboundPlanMinimumPalletCount" :max="999" controls-position="right" /><span class="inbound-plan-pallet-note density-dialog-note" v-if="inboundPlanIsDropship">一件代发固定按头程已封托托盘数整体入库,不可增减。</span><span class="inbound-plan-pallet-note density-dialog-note" v-else>按柜级预览至少 {{ inboundPlanMinimumPalletCount }} 托,可增加,不能减少。</span></div></el-form-item></td><td><el-form-item label="托盘类型"><el-select v-model="inboundPlanForm.palletType" placeholder="请选择托盘类型"><el-option label="标准托盘" value="STANDARD" /><el-option label="木托盘" value="WOOD" /><el-option label="纸托盘" value="PAPER" /></ el-select ></ el-form-item ></ td ></ tr >
2026-10-23 18:45:47 +08:00
< tr >< td >< el-form-item label = "是否外配" >< el-switch v-model = "inboundPlanForm.assigned" inline-prompt active-text="是" inactive-text="否" /></el-form-item></td><td><el-form-item label="允许合托"><el-switch v-model="inboundPlanForm.mergeRequired" inline-prompt active-text="是" inactive-text="否" /><InlineHelp title="合托提示说明" > 仅作为现场可选提示 , 不强制工人合托 。 </ InlineHelp > </ el -form-item ></ td ></ tr >
2026-10-20 10:11:31 +08:00
< tr >< td colspan = "2" >< el-form-item label = "是否加急" >< el-switch v-model = "inboundPlanForm.urgent" inline-prompt active-text="是" inactive-text="否" /></ el-form-item ></ td ></ tr >
2026-10-20 14:52:06 +08:00
< tr >< td colspan = "2" >< el-form-item label = "备注" >< el-input v-model = "inboundPlanForm.remark" type="textarea" :rows="3" maxlength="1000" show -word-limit /></ el-form-item ></ td ></ tr >
2026-10-20 10:11:31 +08:00
</ tbody ></ table >
</ section >
2026-09-07 23:58:18 +08:00
</ el-form >
2026-10-22 23:10:09 +08:00
</ div >
2026-09-07 23:58:18 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "holdOpen" :title="`${holdForm.held ? '暂扣订单' : '解除暂扣'} · ${actionTarget?.orderNo || '—'}`" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="保存" :confirm-loading="actionLoading" @confirm="submitHold" >
2026-09-07 23:58:18 +08:00
< el-alert : title = "holdForm.held ? '暂扣后不可创建出库计划;解除暂扣后恢复出库计划操作。' : '解除暂扣后恢复该订单的出库计划操作。'" type = "warning" :closable = "false" show -icon />
2026-10-20 10:11:31 +08:00
< section class = "density-dialog-section" data -tone =" work " aria -label =" 暂扣变更登记 " >< h3 class = "density-dialog-section__title" > 变更登记 </ h3 >< el-form class = "density-form" label -position =" left " label -width =" 100px " :disabled = "actionLoading" >< table class = "density-table density-table--editable" >< tbody >
< tr >< td >< el-form-item label = "暂扣状态" required >< el-radio-group v-model = "holdForm.held"><el-radio :value="true">暂扣</el-radio><el-radio :value="false" > 解除暂扣 </ el -radio ></ el-radio-group ></ el-form-item ></ td ></ tr >
< tr >< td >< el-form-item label = "是否计费" required >< el-radio-group v-model = "holdForm.chargeable"><el-radio :value="true">是</el-radio><el-radio :value="false" > 否 </ el -radio ></ el-radio-group ></ el-form-item ></ td ></ tr >
2026-10-20 14:52:06 +08:00
< tr >< td >< el-form-item label = "变更原因" required >< el-input v-model = "holdForm.reason" type="textarea" :rows="4" maxlength="500" show -word-limit /></ el-form-item ></ td ></ tr >
2026-10-20 10:11:31 +08:00
</ tbody ></ table ></ el-form ></ section >
2026-09-07 23:58:18 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "manualNodesOpen" :title="`修改节点 · ${actionTarget?.orderNo || '—'}`" width="min(700px, calc(100vw - 28px))" fullscreenable confirm-text="保存" :loading="manualNodesLoading" :confirm-loading="actionLoading" @confirm="submitManualNodes" >
2026-10-26 15:51:07 +08:00
< section class = "density-dialog-section" aria -label =" 人工节点登记 " >< h3 class = "density-dialog-section__title" > 人工节点 </ h3 >< el-form class = "density-form" :disabled = "actionLoading" >< table class = "density-table density-table--editable manual-nodes-editor" >< thead >< tr >< th > 节点 </ th >< th > 节点时间 </ th ></ tr ></ thead >< tbody >< tr v-for = "node in visibleManualNodeDefinitions" :key="node.code"><th scope="row">{{ node.label }}</th><td><el-date-picker v-model="manualNodeValues[node.code]" :aria-label="`${node.label}时间`" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="年/月/日 --:--" /></ td ></ tr ></ tbody ></ table ></ el-form ></ section >
2026-09-07 23:58:18 +08:00
</ AppDialog >
2026-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "reworkOpen" :title="`换标退回重做 · ${reworkForm.relabelNo || '—'}`" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="确认退回重做" :confirm-loading="reworkSaving" :confirm-disabled="!reworkForm.relabelNo || !reworkForm.remark.trim()" @confirm="submitReworkRelabel" >
2026-10-20 10:11:31 +08:00
< section class = "density-dialog-section" data -tone =" work " aria -label =" 换标重做登记 " >< h3 class = "density-dialog-section__title" > 退回重做 </ h3 >< el-form class = "density-form" label -position =" left " label -width =" 100px " :disabled = "reworkSaving" >< table class = "density-table density-table--editable" >< tbody >
< tr >< td >< el-form-item label = "换标单" required >< el-select v-model = "reworkForm.relabelNo" :disabled="reworkSaving" placeholder="请选择需要退回的换标单"><el-option v-for="application in reworkOptions" :key="application.relabelNo" :label="application.relabelNo" :value="application.relabelNo" /></ el-select ></ el-form-item ></ td ></ tr >
< tr >< td >< el-form-item label = "退回原因" required >< el-input v-model = "reworkForm.remark" :disabled="reworkSaving" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="填写退回原因,仓库将重新执行换标并上传照片" /></ el-form-item ></ td ></ tr >
</ tbody ></ table ></ el-form ></ section >
2026-10-09 17:27:47 +08:00
</ AppDialog >
2026-08-19 16:45:51 +08:00
</ template >
< style scoped >
2026-10-20 10:11:31 +08:00
. last - mile - dialog - facts , . last - mile - command - table , . manual - nodes - editor { table - layout : fixed ; }
. last - mile - dialog - facts th { width : var ( -- admin - label - width ); }
. last - mile - command - table td : not ([ colspan ]) { width : 50 % ; }
2026-10-22 23:10:09 +08:00
. inbound - plan - content { display : grid ; min - width : 0 ; gap : var ( -- admin - space - 5 ); }
. inbound - plan - form { margin - top : 0 ; }
. inbound - plan - source__label { width : 112 px ; }
. inbound - plan - source td { overflow - wrap : anywhere ; }
. inbound - plan - arrival { display : flex ; align - items : center ; flex - wrap : wrap ; gap : var ( -- admin - space - 3 ); }
. inbound - plan - form : deep (. el - form - item__content ) { min - width : 0 ; }
. inbound - plan - form : deep (. el - select ) { width : 100 % ; }
2026-10-26 15:51:07 +08:00
. inbound - plan - pallet - field { display : flex ; flex - wrap : wrap ; align - items : center ; gap : var ( -- admin - space - 2 ) var ( -- admin - space - 3 ); min - width : 0 ; }
. inbound - plan - pallet - field : deep (. el - input - number ) { width : 140 px ; flex : 0 0 140 px ; }
. inbound - plan - pallet - note { margin : 0 ; line - height : 1.5 ; }
2026-10-20 10:11:31 +08:00
. manual - nodes - editor th { width : 180 px ; }
2026-09-22 08:47:43 +08:00
. relabel - suborder { min - height : 30 px ; line - height : 30 px ; white - space : nowrap ; }
2026-09-30 14:01:58 +08:00
. last - mile - toolbar { min - width : 0 ; }
2026-09-14 20:03:27 +08:00
. row - actions { white - space : nowrap ; }
2026-09-30 14:01:58 +08:00
2026-09-08 23:57:12 +08:00
. text - ellipsis { min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-10-20 23:34:11 +08:00
. container - detail - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ); margin - bottom : var ( -- admin - space - 5 ); font - size : var ( -- admin - font - body ); }
2026-09-07 20:25:14 +08:00
. container - detail - summary > span { min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-09-14 20:03:27 +08:00
. container - workbench { padding : var ( -- admin - space - 5 ); }
. container - workbench__header { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); margin - bottom : var ( -- admin - space - 5 ); }
. container - workbench__header > div { display : grid ; gap : var ( -- admin - space - 1 ); }
2026-10-20 23:34:11 +08:00
. container - workbench__header strong { color : var ( -- workbench - text ); font - size : var ( -- admin - font - section ); }
. container - workbench__header span { color : var ( -- workbench - muted ); font - size : var ( -- admin - font - body ); }
2026-09-14 20:03:27 +08:00
. container - workbench__grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : var ( -- admin - space - 5 ); }
2026-10-20 23:34:11 +08:00
. container - workbench - card { display : grid ; min - width : 0 ; gap : var ( -- admin - space - 5 ); font : inherit ; text - align : left ; }
. container - workbench - card : hover { border - color : var ( -- workbench - primary ); }
2026-09-14 20:03:27 +08:00
. container - workbench - card__top { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); }
2026-10-20 23:34:11 +08:00
. container - workbench - card__top strong { min - width : 0 ; overflow : hidden ; color : var ( -- workbench - text ); font - size : var ( -- admin - font - section ); text - overflow : ellipsis ; white - space : nowrap ; }
. container - workbench - card__top > span { flex : 0 0 auto ; color : var ( -- workbench - primary ); font - size : var ( -- admin - font - body ); }
2026-09-14 20:03:27 +08:00
. container - workbench - card__arrival { display : grid ; gap : var ( -- admin - space - 1 ); }
2026-10-20 23:34:11 +08:00
. container - workbench - card__arrival small { color : var ( -- workbench - muted ); font - size : var ( -- admin - font - caption ); }
. container - workbench - card__arrival strong { color : var ( -- workbench - text ); font - size : var ( -- admin - font - section ); }
2026-09-14 20:03:27 +08:00
. container - workbench - card__warehouse { display : grid ; gap : var ( -- admin - space - 1 ); }
2026-10-20 23:34:11 +08:00
. container - workbench - card__warehouse small { color : var ( -- workbench - muted ); font - size : var ( -- admin - font - caption ); }
. container - workbench - card__warehouse strong { min - width : 0 ; overflow : hidden ; color : var ( -- workbench - text ); font - size : var ( -- admin - font - section ); text - overflow : ellipsis ; white - space : nowrap ; }
2026-09-07 20:25:14 +08:00
. container - workbench - card__warehouse . is - locked { min - width : 0 ; overflow : hidden ; color : # c2410c ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-10-20 23:34:11 +08:00
. container - workbench - card__pending { display : flex ; align - items : baseline ; gap : var ( -- admin - space - 3 ); padding : var ( -- admin - space - 5 ) var ( -- admin - space - 5 ); border : 1 px solid var ( -- el - color - warning - light - 7 ); border - radius : var ( -- admin - dialog - section - radius ); background : var ( -- el - color - warning - light - 9 ); }
2026-09-14 20:03:27 +08:00
. container - workbench - card__pending strong { color : # b45309 ; font - size : var ( -- admin - font - title ); line - height : 1 ; }
. container - workbench - card__pending span { color : # 92400 e ; font - size : var ( -- admin - font - body ); }
2026-10-20 23:34:11 +08:00
. container - workbench - card__facts { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 2 ) var ( -- admin - space - 5 ); color : var ( -- workbench - muted ); font - size : var ( -- admin - font - caption ); }
2026-09-02 14:39:34 +08:00
. container - workbench - card__facts . is - warning { color : # c2410c ; }
2026-09-14 20:03:27 +08:00
. container - workbench - card__actions { display : flex ; justify - content : flex - end ; padding - top : 2 px ; white - space : nowrap ; }
2026-10-20 23:34:11 +08:00
. container - workbench__pagination { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); margin - top : var ( -- admin - space - 5 ); padding - top : var ( -- admin - space - 5 ); border - top : 1 px solid var ( -- workbench - border ); color : var ( -- workbench - muted ); font - size : var ( -- admin - font - body ); }
2026-09-02 14:39:34 +08:00
@ media ( max - width : 1380 px ) { . container - workbench__grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } }
2026-09-14 20:03:27 +08:00
2026-09-17 13:36:31 +08:00
. package - difference -- nonzero { color : var ( -- el - color - danger ); font - weight : 600 ; }
2026-09-14 20:03:27 +08:00
2026-08-19 16:45:51 +08:00
</ style >