2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-08-21 17:20:40 +08:00
import { computed , onMounted , reactive , ref } from 'vue'
2026-08-19 16:45:51 +08:00
import { Eye } from '@lucide/vue'
2026-08-21 17:20:40 +08:00
import { ElMessage } 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-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-08-28 13:54:44 +08:00
import AppDrawer from '@/shared/components/AppDrawer.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-08-28 13:54:44 +08:00
import { convertLastMileContainerToInboundPlans , convertLastMileOrderToInboundPlan , getContainerPlanProgress , listContainerPlanProgress , listLastMileOrders , transferLastMileOrders , type ContainerPlanProgressDetailDto , type ContainerPlanProgressSummaryDto } from '../api/lastMileOrders'
2026-08-27 15:56:13 +08:00
import OrderReferenceText from '../components/OrderReferenceText.vue'
2026-08-21 17:20:40 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { overseasWarehousePermissions as permission } from '../permissions'
2026-08-20 09:10:36 +08:00
import { toLastMileOrderListItem } from '../lastMilePresentation'
2026-08-19 16:45:51 +08:00
import type { LastMileOrderListItem } from '../types'
const rows = ref < LastMileOrderListItem [] >([])
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-08-21 17:20:40 +08:00
const selectedRows = ref < LastMileOrderListItem [] >([])
const actionLoading = ref ( false )
const actionError = ref < ApiErrorDetails | null >( null )
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 ( '' )
let containerOptionRequest = 0
2026-08-28 13:54:44 +08:00
const statusTab = ref < '全部' | '待分配' | '柜子' > ( '全部' )
2026-08-21 17:20:40 +08:00
const filters = ref < QueryModel >({ orderNo : '' , orderType : '' , warehouse : '' , inboundCode : '' , sourceOrderNo : '' , containerNo : '' , status : '' , transferWarehouseCode : '' , abnormal : '' , detained : '' , createdAt : [] })
2026-08-27 17:16:38 +08:00
const allFields : QueryField [] = [
2026-08-19 16:45:51 +08:00
{ key : 'orderNo' , type : 'input' , label : '尾程单号' , placeholder : '请输入尾程单号' },
{ key : 'inboundCode' , type : 'input' , label : '入仓码' , placeholder : '请输入入仓码' },
2026-08-21 17:20:40 +08:00
{ key : 'orderType' , type : 'select' , label : '订单类型' , 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' },
] },
{ key : 'warehouse' , type : 'input' , label : '海外仓' , placeholder : '请输入仓库名称或编码' },
{ key : 'sourceOrderNo' , type : 'input' , label : '来源订单号' , placeholder : '请输入来源订单号' },
2026-08-19 16:45:51 +08:00
{ key : 'containerNo' , type : 'input' , label : '集装箱号' , placeholder : '请输入集装箱号' },
2026-08-21 17:20:40 +08:00
{ key : 'transferWarehouseCode' , type : 'input' , label : '转仓代码' , placeholder : '请输入转仓代码' },
2026-08-20 09:10:36 +08:00
{ key : 'status' , type : 'select' , label : '当前状态' , placeholder : '全部状态' , options : [
{ label : '待分配' , value : 'PENDING_ASSIGNMENT' },
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-08-21 17:20:40 +08:00
{ key : 'abnormal' , type : 'select' , label : '是否异常' , placeholder : '全部' , options : [{ label : '是' , value : 'true' }, { label : '否' , value : 'false' }] },
{ key : 'detained' , type : 'select' , label : '是否暂扣' , placeholder : '全部' , options : [{ label : '是' , value : 'true' }, { label : '否' , value : 'false' }] },
{ key : 'createdAt' , type : 'daterange' , label : '创建时间' },
2026-08-27 17:16:38 +08:00
]
2026-08-28 13:54:44 +08:00
const fields = computed < QueryField [] >(() => statusTab . value === '柜子'
? allFields . filter (( field ) => [ 'containerNo' , 'createdAt' ]. includes ( field . key ))
: allFields )
2026-08-27 17:49:05 +08:00
const selectedContainer = computed (() => containerOptions . value . find (( row ) => row . sourceContainerBookingId === selectedContainerBookingId . value ))
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-08-19 16:45:51 +08:00
const columns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 110 , 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-08-19 16:45:51 +08:00
{ key : 'warning' , prop : 'warning' , label : '预警' , width : 90 },
{ key : 'orderType' , prop : 'orderType' , label : '订单类型' , minWidth : 110 },
{ key : 'rejectedAt' , prop : 'rejectedAt' , label : '拒收时间' , minWidth : 150 },
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 140 },
{ 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 },
{ key : 'abnormal' , prop : 'abnormal' , label : '是否异常' , width : 100 },
{ key : 'held' , prop : 'held' , label : '是否暂扣' , width : 100 },
{ key : 'pallets' , prop : 'pallets' , label : '托数' , width : 80 , align : 'right' },
{ key : 'cartons' , prop : 'cartons' , label : '箱数' , width : 80 , align : 'right' },
{ key : 'volume' , prop : 'volume' , label : '体积(m³)' , width : 100 , align : 'right' },
{ key : 'quantity' , prop : 'quantity' , label : '数量' , width : 80 , align : 'right' },
{ 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 },
{ key : 'relabelOrderNo' , prop : 'relabelOrderNo' , label : '换标单号' , minWidth : 150 },
{ key : 'relabelStatus' , prop : 'relabelStatus' , label : '换标状态' , width : 110 },
{ key : 'consolidated' , prop : 'consolidated' , label : '合托标记' , width : 100 },
{ key : 'status' , prop : 'status' , label : '当前状态' , width : 110 },
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 containerColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' },
{ key : 'containerNo' , prop : 'containerNo' , label : '柜号' , minWidth : 170 },
{ key : 'plannedArrivalAt' , prop : 'plannedArrivalAt' , label : '计划到仓时间' , minWidth : 170 , slotName : 'plannedArrivalAt' },
{ key : 'totalOrderCount' , prop : 'totalOrderCount' , label : '订单总数' , width : 100 , align : 'right' },
{ key : 'inboundCodes' , label : '入仓码' , minWidth : 170 , slotName : 'inboundCodes' },
{ key : 'planProgress' , label : '入库计划进度' , minWidth : 270 , slotName : 'planProgress' },
{ key : 'readiness' , label : '整柜就绪状态' , minWidth : 240 , slotName : 'readiness' },
]
const containerDetailColumns : DataTableColumn [] = [
{ key : 'orderNo' , prop : 'orderNo' , label : '尾程单号' , minWidth : 170 },
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 150 },
{ key : 'orderStatus' , prop : 'orderStatus' , label : '订单状态' , width : 130 },
{ key : 'planNo' , prop : 'planNo' , label : '独立入库计划' , minWidth : 170 },
{ key : 'planStatus' , prop : 'planStatus' , label : '计划状态' , width : 130 },
{ key : 'plannedArrivalAt' , prop : 'plannedArrivalAt' , label : '计划到仓时间' , minWidth : 170 , slotName : 'plannedArrivalAt' },
]
function formatDateTime ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 19 ) : '—'
}
function canGenerateContainerPlans ( container : ContainerPlanProgressSummaryDto ) {
return Boolean ( container . containerNo ) && container . missingInboundCodeCount === 0 && container . pendingPlanCount > 0
}
2026-08-20 09:10:36 +08:00
async function load ( model : QueryModel = filters . value ) {
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 ,
})
containerRows . value = result . records
containerTotal . value = result . total
} 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-08-21 17:20:40 +08:00
const dateRange = Array . isArray ( filters . value . createdAt ) ? filters . value . createdAt . map ( String ) : []
const booleanValue = ( value : unknown ) => value === 'true' ? true : value === 'false' ? false : undefined
2026-08-20 09:10:36 +08:00
const result = await listLastMileOrders ({
page : page . value ,
size : pageSize . value ,
orderNo : String ( filters . value . orderNo || '' ). trim () || undefined ,
2026-08-21 17:20:40 +08:00
orderType : String ( filters . value . orderType || '' ). trim () || undefined ,
warehouse : String ( filters . value . warehouse || '' ). trim () || undefined ,
2026-08-20 09:10:36 +08:00
inboundCode : String ( filters . value . inboundCode || '' ). trim () || undefined ,
2026-08-21 17:20:40 +08:00
sourceOrderNo : String ( filters . value . sourceOrderNo || '' ). trim () || undefined ,
2026-08-20 09:10:36 +08:00
containerNo : String ( filters . value . containerNo || '' ). trim () || undefined ,
2026-08-27 17:49:05 +08:00
currentStatus : statusTab . value === '待分配' ? 'PENDING_ASSIGNMENT' : String ( filters . value . status || '' ). trim () || undefined ,
2026-08-21 17:20:40 +08:00
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
})
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
} catch ( error ) {
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 {
loading . value = false
}
}
function search ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
function reset ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
2026-08-27 17:49:05 +08:00
function changeStatusTab () {
selectedRows . value = []
page . value = 1
void load ()
}
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-08-21 17:20:40 +08:00
function onSelectionChange ( selection : object []) { selectedRows . value = selection as LastMileOrderListItem [] }
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 }
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-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-08-27 17:49:05 +08:00
async function convertInboundPlan () {
if ( selectedRows . value . length !== 1 ) { ElMessage . warning ( '生成入库计划时请选择 1 条尾程订单' ); return }
const selected = selectedRows . value [ 0 ]
const customerId = typeof selected . customerId === 'string' ? selected . customerId . trim () : ''
if ( ! customerId ) {
actionError . value = { message : '所选尾程订单缺少客户标识,无法安全生成入库计划' }
2026-08-27 16:34:32 +08:00
return
}
actionLoading . value = true
actionError . value = null
2026-08-21 17:20:40 +08:00
try {
2026-08-27 17:49:05 +08:00
await convertLastMileOrderToInboundPlan ({ orderId : String ( selected . id ), customerId })
selectedRows . value = []
ElMessage . success ( '入库计划生成成功' )
await load ()
} catch ( error ) {
actionError . value = getApiErrorDetails ( error , '生成入库计划失败,请重试' )
} finally {
actionLoading . value = false
}
}
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
actionError . value = null
containerConvertOpen . value = true
void loadContainerOptions ()
}
async function convertContainerInboundPlans () {
const container = selectedContainer . value
if ( ! container ? . sourceContainerBookingId ) { ElMessage . warning ( '请选择一个待分配柜' ); return }
2026-08-28 13:54:44 +08:00
if ( ! canGenerateContainerPlans ( container )) {
actionError . value = { message : container . blockingReason || '所选柜当前不满足生成资格' }
2026-08-27 17:49:05 +08:00
return
}
actionLoading . value = true
actionError . value = null
try {
const plans = await convertLastMileContainerToInboundPlans ({ sourceContainerBookingId : container . sourceContainerBookingId })
containerConvertOpen . value = false
selectedContainerBookingId . value = ''
ElMessage . success ( `已按柜生成 ${ plans . length } 个独立入库计划` )
2026-08-27 16:34:32 +08:00
await load ()
} catch ( error ) {
actionError . value = getApiErrorDetails ( error , '生成整柜入库计划失败,请重试' )
} finally {
actionLoading . value = false
}
2026-08-21 17:20:40 +08:00
}
2026-08-20 09:10:36 +08:00
2026-08-19 16:45:51 +08:00
function openDetail ( source : object ) {
const row = source as LastMileOrderListItem
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-08-21 11:20:09 +08:00
onMounted (() => { void load () })
2026-08-19 16:45:51 +08:00
</ script >
< template >
2026-08-21 17:20:40 +08:00
< AdminPage >< OperationErrorToast :details = "actionError" />< ListPageLayout aria -label =" 尾程订单列表 " >
2026-08-19 16:45:51 +08:00
< template # toolbar >
< section class = "surface-panel last-mile-toolbar" aria -label =" 尾程订单状态与筛选 " >
2026-08-20 09:10:36 +08:00
< el-radio-group v-model = "statusTab" aria-label="尾程订单状态" @change="changeStatusTab" >
2026-08-19 16:45:51 +08:00
< el-radio-button value = "全部" > 全部 </ el-radio-button >
< el-radio-button value = "待分配" > 待分配 </ el-radio-button >
2026-08-28 13:54:44 +08:00
< el-radio-button value = "柜子" > 柜子 ( {{ containerTotal }} ) </ el-radio-button >
2026-08-19 16:45:51 +08:00
</ el-radio-group >
2026-08-20 09:10:36 +08:00
< QueryToolbar v-model = "filters" :fields="fields" :loading="loading" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" @search="search" @reset="reset" />
2026-08-19 16:45:51 +08:00
</ section >
</ template >
2026-08-21 11:20:09 +08:00
< template v-if = "loadError" # state >
< LoadErrorState title = "尾程订单加载失败" :message = "loadError.message" :loading = "loading" @retry ="load" >
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
2026-08-20 09:10:36 +08:00
</ LoadErrorState >
</ template >
2026-08-27 15:56:13 +08:00
< LoadErrorState v-if = "actionError" title="尾程订单操作失败" :message="actionError.message" compact >
< template # details >< ApiErrorMeta :details = "actionError" /></ template >
</ LoadErrorState >
2026-08-28 13:54:44 +08:00
< section v-if = "!loadError && statusTab === '柜子'" class="surface-panel table-panel" >
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "containerRows" :columns = "containerColumns" :loading = "loading" :total = "containerTotal" preference -key =" overseas -last-mile-container-progress " row -key =" sourceContainerBookingId " empty -text =" 暂无柜级入库计划进度 " @ update :page = "load()" @ update : page -size =" page = 1 ; load ()" >
< template # actions = "{ row }" >< el-button size = "small" plain type = "primary" @click ="openContainerDetail(row as ContainerPlanProgressSummaryDto)" > 查看 </ el -button ></ template >
< template # plannedArrivalAt = "{ row }" >{{ formatDateTime (( row as ContainerPlanProgressSummaryDto ). plannedArrivalAt ) }}</ template >
< template # inboundCodes = "{ row }" > 已有 {{ ( row as ContainerPlanProgressSummaryDto ). inboundCodeCount }} / 缺 {{ ( row as ContainerPlanProgressSummaryDto ). missingInboundCodeCount }}</ template >
< template # planProgress = "{ row }" > 已生成 {{ ( row as ContainerPlanProgressSummaryDto ). planCount }} 个计划 , 剩余 {{ ( row as ContainerPlanProgressSummaryDto ). pendingPlanCount }} 个未操作 </ template >
< template # readiness = "{ row }" >< el-tag : type = "(row as ContainerPlanProgressSummaryDto).allPlansReady ? 'success' : 'warning'" effect = "plain" >{{ ( row as ContainerPlanProgressSummaryDto ). allPlansReady ? '整柜已就绪' : '未就绪' }}</ el-tag >< span v-if = "!(row as ContainerPlanProgressSummaryDto).allPlansReady" class="readiness-reason" >{{ ( row as ContainerPlanProgressSummaryDto ) .blockingReason || ' 尚未满足全部独立计划条件 , 不可视为 PDA 可作业 ' }}</ span ></ template >
< template # pagination -left > 共 {{ containerTotal }} 个柜 </ template >
</ DataTable >
</ section >
< section v-else-if = "!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="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-08-27 17:49:05 +08:00
< template # toolbar -actions >< el-button v-if = "access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" plain :disabled="selectedRows.length !== 1 || actionLoading" @click="convertInboundPlan">生成入库计划</el-button><el-button v-if="statusTab === '待分配' && access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion" > 按柜批量生成 </ el -button ></ template >
2026-08-19 16:45:51 +08:00
< template # actions = "{ row }" >< el-button size = "small" plain type = "primary" @click ="openDetail(row)" >< Eye :size = "15" /> 查看 </ el-button ></ template >
2026-08-27 15:56:13 +08:00
< template # orderNo = "{ row }" >< OrderReferenceText : value = "String(row.orderNo || '')" /></ template >
2026-08-21 17:20:40 +08:00
< template # pagination -left > 共 {{ total }} 条尾程订单 · 已选 {{ selectedRows . length }} 条 </ template >
2026-08-19 16:45:51 +08:00
</ DataTable ></ section >
</ ListPageLayout ></ AdminPage >
2026-08-28 13:54:44 +08:00
< AppDialog v-model = "transferOpen" title="转仓" confirm-text="确认转仓" :loading="transferWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!transferForm.warehouseId || Boolean(transferWarehouseError)" @confirm="submitTransfer" >
< LoadErrorState v-if = "transferWarehouseError" title="FBA 仓候选加载失败" :message="transferWarehouseError.message" compact @retry="loadTransferWarehouseOptions" >
< template # details >< ApiErrorMeta :details = "transferWarehouseError" /></ template >
</ LoadErrorState >
< el-form v-else label -position =" top " >
< 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 >
< el-alert v-if = "!transferWarehouseLoading && !transferWarehouseOptions.length" title="暂无可用 FBA 仓候选" type="info" :closable="false" show -icon />
2026-08-21 17:20:40 +08:00
< el-form-item label = "备注" >< el-input v-model = "transferForm.remark" type="textarea" maxlength="500" /></ el-form-item >
</ el-form >
</ AppDialog >
2026-08-28 13:54:44 +08:00
< AppDialog v-model = "containerConvertOpen" title="按柜批量生成入库计划" width="640px" confirm-text="确认按柜生成" :loading="containerOptionsLoading" :confirm-loading="actionLoading" :confirm-disabled="!selectedContainer || !canGenerateContainerPlans(selectedContainer)" @confirm="convertContainerInboundPlans" >
2026-08-27 17:49:05 +08:00
< p class = "container-convert-hint" > 将为所选柜内所有待分配尾程订单分别生成独立入库计划 。 </ p >
< LoadErrorState v-if = "containerLoadError" title="待分配柜加载失败" :message="containerLoadError.message" compact @retry="loadContainerOptions()" >
< template # details >< ApiErrorMeta :details = "containerLoadError" /></ template >
</ LoadErrorState >
< el-form v-else label -position =" top " >
< el-form-item label = "选择待分配柜" required >
< el-select v-model = "selectedContainerBookingId" filterable remote clearable :remote-method="loadContainerOptions" :loading="containerOptionsLoading" placeholder="输入柜号搜索并选择" >
2026-08-28 13:54:44 +08:00
< el-option v-for = "container in containerOptions" :key="container.sourceContainerBookingId" :value="container.sourceContainerBookingId" :disabled="!canGenerateContainerPlans(container)" :label="`${container.containerNo || '柜号缺失'} · ${container.totalOrderCount} 票${canGenerateContainerPlans(container) ? '' : ` · ${container.blockingReason || '不可生成'}`}`" />
2026-08-27 17:49:05 +08:00
</ el-select >
</ el-form-item >
2026-08-28 13:54:44 +08:00
< el-alert v-if = "selectedContainer" :title="canGenerateContainerPlans(selectedContainer) ? `确认后将为该柜剩余 ${selectedContainer.pendingPlanCount} 票分别生成独立入库计划` : selectedContainer.blockingReason || '所选柜不可生成'" :type="canGenerateContainerPlans(selectedContainer) ? 'info' : 'error'" :closable="false" show -icon />
2026-08-27 17:49:05 +08:00
</ el-form >
</ AppDialog >
2026-08-28 13:54:44 +08:00
< AppDrawer v-model = "containerDetailOpen" title="柜级入库计划详情" size="min(1040px, 96vw)" :loading="containerDetailLoading" :show-footer="false" >
< LoadErrorState v-if = "containerDetailError" title="柜级详情加载失败" :message="containerDetailError.message" compact >
< template # details >< ApiErrorMeta :details = "containerDetailError" /></ template >
</ LoadErrorState >
< template v-else-if = "containerDetail" >
< div class = "container-detail-summary" >
< span > 柜号 : {{ containerDetail . summary . containerNo || '—' }}</ span >
< span > 计划到仓 : {{ formatDateTime ( containerDetail . summary . plannedArrivalAt ) }}</ span >
< span > 订单 : {{ containerDetail . summary . totalOrderCount }}</ span >
< span > 入仓码 : {{ containerDetail . summary . inboundCodeCount }} , 缺 {{ containerDetail . summary . missingInboundCodeCount }}</ span >
< span > 计划 : {{ containerDetail . summary . planCount }} , 未操作 {{ containerDetail . summary . pendingPlanCount }}</ span >
</ div >
< DataTable :data = "containerDetail.orders" :columns = "containerDetailColumns" :loading = "containerDetailLoading" preference -key =" overseas -last-mile-container-detail " row -key =" orderId " :total = "undefined" empty -text =" 该柜暂无尾程订单 " >
< template # plannedArrivalAt = "{ row }" >{{ formatDateTime (( row as ContainerPlanProgressDetailDto [ 'orders' ][ number ]). plannedArrivalAt ) }}</ template >
</ DataTable >
</ template >
</ AppDrawer >
2026-08-19 16:45:51 +08:00
</ template >
< style scoped >
. last - mile - toolbar { padding : 14 px 16 px 0 ; }
. last - mile - toolbar : deep (. query - toolbar ) { margin - top : 12 px ; padding : 12 px 0 16 px ; border : 0 ; border - top : 1 px solid var ( -- cc - border ); border - radius : 0 ; }
2026-08-27 17:49:05 +08:00
. container - convert - hint { margin : 0 0 16 px ; color : var ( -- cc - muted ); line - height : 1.6 ; }
2026-08-28 13:54:44 +08:00
. readiness - reason { margin - left : 8 px ; color : var ( -- cc - muted ); font - size : 12 px ; }
. container - detail - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 8 px 16 px ; margin - bottom : 12 px ; padding : 12 px 14 px ; border : 1 px solid var ( -- cc - border ); border - radius : 6 px ; background : var ( -- cc - surface ); font - size : 13 px ; }
2026-08-19 16:45:51 +08:00
</ style >