2026-08-10 17:42:17 +08:00
< script setup lang = "ts" >
2026-10-20 10:11:31 +08:00
import '@/styles/adminDensity.css'
2026-08-10 17:42:17 +08:00
import { computed , onMounted , reactive , ref } from 'vue'
2026-09-15 18:17:17 +08:00
import { Pencil , RefreshCw , Sparkles , Trash2 } from '@lucide/vue'
import SmartLoadingCandidateInbounds from '../components/SmartLoadingCandidateInbounds.vue'
2026-08-10 17:42:17 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-14 12:50:42 +08:00
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-09-04 09:57:39 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
2026-08-10 17:42:17 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-09-03 21:25:56 +08:00
import LineClampText from '@/shared/components/LineClampText.vue'
2026-08-10 17:42:17 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import {
adjustSmartLoadingItem ,
2026-09-03 21:25:56 +08:00
assignPendingReviewSmartLoadingOrder ,
2026-08-29 11:41:57 +08:00
assignSmartLoadingGroup ,
2026-08-10 17:42:17 +08:00
assignSmartLoadingItem ,
2026-09-08 17:24:49 +08:00
assignSmartLoadingPallet ,
2026-08-10 17:42:17 +08:00
getSmartLoadingPlanByBooking ,
listSmartLoadingCandidates ,
2026-09-03 15:00:58 +08:00
listSmartLoadingWorkbenchContainers ,
2026-08-10 17:42:17 +08:00
recommendSmartLoading ,
2026-09-04 10:51:31 +08:00
removeAllUnpickedSmartLoadingItems ,
2026-08-29 11:41:57 +08:00
removeSmartLoadingGroup ,
2026-08-10 17:42:17 +08:00
removeSmartLoadingItem ,
2026-09-14 14:35:30 +08:00
removeSmartLoadingPallet ,
2026-08-10 17:42:17 +08:00
} from '../api/platformContainerLoading'
import type {
SmartLoadingCandidate ,
2026-09-16 11:06:36 +08:00
SmartLoadingCandidateGroupMember ,
2026-08-10 17:42:17 +08:00
SmartLoadingPlan ,
2026-09-03 15:00:58 +08:00
SmartLoadingWorkbenchContainer ,
2026-08-10 17:42:17 +08:00
} from '../types'
2026-08-24 18:07:38 +08:00
import { groupSmartLoadingItems , type SmartLoadingInboundItem } from '../smartLoadingItems'
2026-08-31 10:48:23 +08:00
import { loadSmartLoadingPlanCoLoadGroups , type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
2026-09-03 10:27:31 +08:00
import {
SMART_LOADING_CHANNEL_GROUPS ,
resolveSmartLoadingChannelGroup ,
} from '../smartLoadingChannelGroups'
2026-08-10 17:42:17 +08:00
2026-08-14 19:02:34 +08:00
const props = withDefaults ( defineProps < { embedded ?: boolean } > (), { embedded : false })
2026-09-04 14:04:57 +08:00
const editingQuantityId = ref ( '' )
2026-08-14 19:02:34 +08:00
2026-08-10 17:42:17 +08:00
const access = useAccessPreviewStore ()
2026-09-03 15:00:58 +08:00
const containers = ref < SmartLoadingWorkbenchContainer [] >([])
2026-09-04 09:57:39 +08:00
const warehouses = ref < PlatformWarehouseSummaryResource [] >([])
2026-08-14 12:50:42 +08:00
const customers = ref < PlatformTenantResource [] >([])
const customersLoading = ref ( false )
2026-08-10 17:42:17 +08:00
const selectedContainerId = ref ( '' )
2026-09-03 11:47:32 +08:00
const selectedChannelCodes = ref < string [] >([])
2026-08-10 17:42:17 +08:00
const plan = ref < SmartLoadingPlan | null >( null )
const candidates = ref < SmartLoadingCandidate [] >([])
const loading = ref ( false )
const selectionLoading = ref ( false )
2026-09-03 15:00:58 +08:00
const candidateLoading = ref ( false )
2026-08-10 17:42:17 +08:00
const mutating = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const selectionError = ref < ApiErrorDetails | null >( null )
2026-09-03 15:00:58 +08:00
const candidateError = ref < ApiErrorDetails | null >( null )
2026-08-29 11:41:57 +08:00
const mutationError = ref < ApiErrorDetails | null >( null )
2026-08-10 17:42:17 +08:00
const noDraft = ref ( false )
const assignOpen = ref ( false )
2026-08-29 11:41:57 +08:00
const selectedCandidate = ref < SmartLoadingCandidateRow | null >( null )
2026-10-20 10:11:31 +08:00
const assignmentTitle = computed (() => {
const candidate = selectedCandidate . value
const action = candidate ? . unitType === 'SEALED_PALLET' ? '整托加入当前柜次' : candidate ? . coLoadGroupId ? '整组加入当前柜次' : '加入当前柜次'
const code = candidate ? . palletNo ? . trim () || candidate ? . groupNo ? . trim () || candidate ? . inboundCode ? . trim () || candidate ? . orderNo ? . trim ()
return code ? ` ${ action } · ${ code } ` : action
})
2026-08-10 17:42:17 +08:00
const assignForm = reactive ({ packageCount : 1 })
const quantityDrafts = reactive < Record < string , number > > ({})
2026-08-31 10:48:23 +08:00
const planGroupsByInboundId = ref ( new Map < string , SmartLoadingPlanCoLoadGroup >())
2026-09-04 18:47:43 +08:00
const query = ref < QueryModel >({ warehouseId : '' , customerId : '' , inboundCode : '' , createdRange : [], expectedArrivalRange : [], orderStatuses : [] })
2026-09-03 22:15:25 +08:00
const appliedQuery = ref < QueryModel >({ ... query . value , orderStatuses : [] })
2026-09-01 21:09:39 +08:00
const candidatePage = ref ( 1 )
const candidatePageSize = ref ( 20 )
2026-09-03 15:00:58 +08:00
const candidateTotal = ref ( 0 )
2026-08-10 17:42:17 +08:00
let selectionRequest = 0
2026-09-03 15:00:58 +08:00
let candidateRequest = 0
let containerRequest = 0
2026-08-10 17:42:17 +08:00
const canAssign = computed (() => access . can ( 'platform:firstleg:smart-loading:assign' ))
const canDelete = computed (() => access . can ( 'platform:firstleg:smart-loading:delete' ))
2026-09-03 15:00:58 +08:00
const selectedContainer = computed (() => containers . value . find (( item ) => item . bookingId === selectedContainerId . value ) || null )
2026-09-10 19:25:11 +08:00
function containerChannelGroup ( container : SmartLoadingWorkbenchContainer | null | undefined ) {
if ( ! container ) return null
return SMART_LOADING_CHANNEL_GROUPS . find (( group ) => group . code === container . channelGroupCode ) || null
}
const crossChannelAssignment = computed (() => {
const candidateGroup = resolveSmartLoadingChannelGroup ( selectedCandidate . value ? . channelCode , selectedCandidate . value ? . channelName )
const containerGroup = containerChannelGroup ( selectedContainer . value )
return Boolean ( candidateGroup && containerGroup && candidateGroup . code !== containerGroup . code )
})
2026-09-04 09:57:39 +08:00
const orchestratableContainers = computed (() => containers . value )
const completedContainerCount = computed (() => 0 )
2026-09-10 19:25:11 +08:00
const missingChannelContainerCount = computed (() => orchestratableContainers . value . filter (( item ) => ! item . channelGroupCode ). length )
2026-09-03 10:27:31 +08:00
const unmappedChannelContainerCount = computed (() => orchestratableContainers . value . filter (( item ) => (
2026-09-10 19:25:11 +08:00
item . channelGroupCode && ! containerChannelGroup ( item )
2026-09-03 10:27:31 +08:00
)). length )
const mappedWorkbenchContainerCount = computed (() => orchestratableContainers . value . filter (( item ) => (
2026-09-10 19:25:11 +08:00
containerChannelGroup ( item )
2026-09-03 10:27:31 +08:00
)). length )
2026-09-03 11:47:32 +08:00
const channelOptions = computed (() => SMART_LOADING_CHANNEL_GROUPS )
const warehouseOptions = computed (() => {
2026-09-04 09:57:39 +08:00
return warehouses . value . map (( warehouse ) => ({ value : String ( warehouse . id ), label : warehouse . warehouseName }))
2026-09-03 11:47:32 +08:00
})
2026-08-17 16:45:46 +08:00
const editable = computed (() => plan . value ? . canEdit === true )
const assignable = computed (() => plan . value ? . canAssign === true )
2026-08-21 17:07:37 +08:00
const assignedInboundIds = computed (() => new Set (( plan . value ? . items || []). map (( item ) => item . inboundId )))
2026-09-08 17:24:49 +08:00
const assignedPalletIds = computed (() => new Set (( plan . value ? . items || []). map (( item ) => item . palletId ). filter ( Boolean )))
2026-09-04 09:57:39 +08:00
type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds : string []; memberInboundCodes : string []; memberOrderSummaries : string []; memberOrderCount : number ; inboundFragmentCount : number ; totalGrossWeightKg : number | null ; totalVolumeCbm : number | null }
2026-08-30 15:49:01 +08:00
function memberOrderSummaries < T extends { orderId : string ; orderNo ? : string | null ; inboundCode ? : string | null }>( members : T []) {
const orders = new Map < string , { label : string ; inboundCodes : string [] }>()
for ( const member of members ) {
2026-08-30 17:43:03 +08:00
const current = orders . get ( member . orderId ) || { label : member . orderNo || '订单编码待补充' , inboundCodes : [] }
2026-08-30 15:49:01 +08:00
const inboundCode = member . inboundCode || '入仓码暂缺'
if ( ! current . inboundCodes . includes ( inboundCode )) current . inboundCodes . push ( inboundCode )
orders . set ( member . orderId , current )
}
return [... orders . values ()]. map (( order ) => ` ${ order . label } : ${ order . inboundCodes . join ( '、' ) } ` )
}
2026-08-31 10:48:23 +08:00
function groupMemberLabel ( group : SmartLoadingPlanCoLoadGroup ) {
return group . inboundFragmentCount === group . memberOrderCount
? ` ${ group . memberOrderCount } 票`
: ` ${ group . memberOrderCount } 票 · ${ group . inboundFragmentCount } 个入仓片段`
2026-08-30 15:49:01 +08:00
}
2026-09-03 14:12:42 +08:00
function candidateTotalGrossWeight ( candidate : SmartLoadingCandidate ) {
2026-09-04 09:57:39 +08:00
return candidate . unitGrossWeightKg == null ? null : Number ( candidate . unitGrossWeightKg ) * Number ( candidate . availablePackageCount || 0 )
2026-09-03 14:12:42 +08:00
}
function candidateTotalVolume ( candidate : SmartLoadingCandidate ) {
return candidate . availableVolumeCbm == null
2026-09-04 09:57:39 +08:00
? candidate . averageUnitVolumeCbm == null ? null : Number ( candidate . averageUnitVolumeCbm ) * Number ( candidate . availablePackageCount || 0 )
2026-09-03 14:12:42 +08:00
: Number ( candidate . availableVolumeCbm )
}
2026-08-29 11:41:57 +08:00
const displayedCandidates = computed < SmartLoadingCandidateRow [] >(() => {
2026-09-03 15:00:58 +08:00
return candidates . value . map (( candidate ) => {
2026-09-03 21:25:56 +08:00
const members : Array < { accountId : string ; inboundId : string | null ; orderId : string ; orderNo ?: string | null ; inboundCode ?: string | null } > = candidate . groupMembers . length
2026-09-03 15:00:58 +08:00
? candidate . groupMembers
: [ candidate ]
return {
... candidate ,
2026-09-03 21:25:56 +08:00
memberInboundIds : members . map (( member ) => member . inboundId ). filter (( id ) : id is string => Boolean ( id )),
2026-08-29 11:41:57 +08:00
memberInboundCodes : members . map (( member ) => member . inboundCode || '入仓码暂缺' ),
2026-08-30 15:49:01 +08:00
memberOrderSummaries : memberOrderSummaries ( members ),
memberOrderCount : new Set ( members . map (( member ) => member . orderId )). size ,
2026-09-03 15:00:58 +08:00
inboundFragmentCount : Number ( candidate . memberCount || members . length ),
totalGrossWeightKg : candidateTotalGrossWeight ( candidate ),
totalVolumeCbm : candidateTotalVolume ( candidate ),
}
})
2026-08-29 11:41:57 +08:00
})
2026-09-16 11:06:36 +08:00
const planPalletMembersByPalletId = computed (() => new Map (
( plan . value ? . palletMembers || []). map (( entry ) => [ entry . palletId , entry . members ]),
))
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup : SmartLoadingPlanCoLoadGroup | null ; palletMembers : SmartLoadingCandidateGroupMember []; memberInboundCodes : string []; memberOrderSummaries : string [] }
2026-08-29 11:41:57 +08:00
const displayedPlanItems = computed < SmartLoadingPlanRow [] >(() => {
const inboundRows = groupSmartLoadingItems ( plan . value ? . items || [])
const result : SmartLoadingPlanRow [] = []
const grouped = new Map < string , SmartLoadingInboundItem [] >()
for ( const row of inboundRows ) {
const group = planGroupsByInboundId . value . get ( row . inboundId )
if ( ! group ) {
2026-09-16 11:06:36 +08:00
result . push ({ ... row , coLoadGroup : null , palletMembers : row . palletId ? planPalletMembersByPalletId . value . get ( row . palletId ) || [] : [], memberInboundCodes : [ row . inboundCode || '入仓码暂缺' ], memberOrderSummaries : memberOrderSummaries ([ row ]) })
2026-08-29 11:41:57 +08:00
continue
}
grouped . set ( group . groupId , [...( grouped . get ( group . groupId ) || []), row ])
}
for ( const rows of grouped . values ()) {
const first = rows [ 0 ]
const group = planGroupsByInboundId . value . get ( first . inboundId ) !
result . push ({
... first ,
id : `coload: ${ group . groupId } ` ,
inboundCode : rows . map (( row ) => row . inboundCode ). filter ( Boolean ). join ( '、' ),
customerName : [... new Set ( rows . map (( row ) => row . customerName ). filter ( Boolean ))]. join ( '、' ),
packageCount : rows . reduce (( sum , row ) => sum + row . packageCount , 0 ),
productQuantity : rows . reduce (( sum , row ) => sum + Number ( row . productQuantity || 0 ), 0 ),
grossWeightKg : rows . reduce (( sum , row ) => sum + Number ( row . grossWeightKg || 0 ), 0 ),
volumeCbm : rows . reduce (( sum , row ) => sum + Number ( row . volumeCbm || 0 ), 0 ),
pickedPackageCount : rows . reduce (( sum , row ) => sum + row . pickedPackageCount , 0 ),
minPackageCount : rows . reduce (( sum , row ) => sum + row . minPackageCount , 0 ),
canAdjust : false ,
canRemove : rows . every (( row ) => row . canRemove ),
sourceItems : rows . flatMap (( row ) => row . sourceItems ),
detailCount : rows . reduce (( sum , row ) => sum + row . detailCount , 0 ),
coLoadGroup : group ,
2026-09-16 11:06:36 +08:00
palletMembers : [],
2026-08-29 11:41:57 +08:00
memberInboundCodes : rows . map (( row ) => row . inboundCode || '入仓码暂缺' ),
2026-08-30 15:49:01 +08:00
memberOrderSummaries : memberOrderSummaries ( rows ),
2026-08-29 11:41:57 +08:00
})
}
return result
})
2026-08-10 17:42:17 +08:00
const totalPackages = computed (() => ( plan . value ? . items || []). reduce (( sum , item ) => sum + Number ( item . packageCount || 0 ), 0 ))
const totalVolume = computed (() => ( plan . value ? . items || []). reduce (( sum , item ) => sum + Number ( item . volumeCbm || 0 ), 0 ))
const totalWeight = computed (() => ( plan . value ? . items || []). reduce (( sum , item ) => sum + Number ( item . grossWeightKg || 0 ), 0 ))
2026-09-04 09:57:39 +08:00
const volumeLimit = computed (() => Number ( selectedContainer . value ? . capacityCbm || 0 ) * 1.5 )
const weightLimit = computed (() => Number ( selectedContainer . value ? . maxWeightKg || 0 ) * 1.5 )
2026-08-10 17:42:17 +08:00
const volumePercent = computed (() => volumeLimit . value ? totalVolume . value / volumeLimit . value * 100 : 0 )
const weightPercent = computed (() => weightLimit . value ? totalWeight . value / weightLimit . value * 100 : 0 )
2026-08-11 20:02:38 +08:00
const displayedVolumePercent = computed (() => percentage ( volumePercent . value ))
const displayedWeightPercent = computed (() => percentage ( weightPercent . value ))
2026-09-03 11:54:20 +08:00
const candidateOrderStatusOptions = [
{ value : 'PENDING_REVIEW' , label : '待审核' },
{ value : 'WAIT_MEASURE' , label : '待测量数据' },
{ value : 'WAIT_SPLIT' , label : '待分柜' },
{ value : 'LOADING' , label : '装柜中' },
]
2026-09-03 21:25:56 +08:00
function candidateStatusLabel ( status : string | null | undefined ) {
return candidateOrderStatusOptions . find (( item ) => item . value === status ) ? . label || '—'
}
2026-08-10 17:42:17 +08:00
const queryFields = computed < QueryField [] >(() => [
2026-09-03 11:47:57 +08:00
{ key : 'warehouseId' , type : 'select' , label : '发货仓' , placeholder : '全部发货仓' , width : 160 , options : warehouseOptions . value , props : { filterable : true } },
{ key : 'customerId' , type : 'select' , label : '客户' , placeholder : '全部客户' , width : 180 , options : customers . value . filter (( customer ) => customer . status === 'ACTIVE' ). map (( customer ) => ({ label : customer . tenantName , value : String ( customer . id ) })), props : { filterable : true , loading : customersLoading . value } },
{ key : 'inboundCode' , type : 'input' , label : '入仓码' , placeholder : '请输入入仓码' , width : 210 },
2026-09-03 21:25:56 +08:00
{ key : 'createdRange' , type : 'daterange' , label : '创建日期' , placeholder : '请选择创建日期范围' , width : 260 },
2026-09-04 18:47:43 +08:00
{ key : 'expectedArrivalRange' , type : 'daterange' , label : '预计到货时间' , placeholder : '请选择预计到货时间范围' , width : 260 },
2026-09-03 22:15:25 +08:00
{ key : 'orderStatuses' , type : 'select' , label : '订单状态' , placeholder : '全部订单状态' , width : 210 , options : candidateOrderStatusOptions , props : { multiple : true , collapseTags : true , collapseTagsTooltip : true , maxCollapseTags : 2 } },
2026-08-10 17:42:17 +08:00
])
2026-09-03 15:00:58 +08:00
const filteredContainers = computed (() => containers . value )
const candidateScopeLabel = computed (() => {
const warehouseId = String ( appliedQuery . value . warehouseId || '' )
const warehouse = warehouseOptions . value . find (( item ) => item . value === warehouseId ) ? . label || '全部发货仓'
const channels = channelOptions . value . filter (( item ) => selectedChannelCodes . value . includes ( item . code )). map (( item ) => item . name )
return ` ${ warehouse } · ${ channels . join ( '、' ) || '未选择渠道' } `
2026-09-01 21:09:39 +08:00
})
2026-08-10 17:42:17 +08:00
2026-08-10 18:07:33 +08:00
function preferredContainerId () {
const visible = filteredContainers . value
2026-09-03 15:00:58 +08:00
return visible . find (( item ) => item . planStatus === 'EDITING' ) ? . bookingId
|| visible . find (( item ) => item . planStatus === 'LOADING' ) ? . bookingId
|| visible . find (( item ) => item . planStatus === 'WAIT_LOADING' ) ? . bookingId || visible [ 0 ] ? . bookingId || ''
2026-08-10 18:07:33 +08:00
}
2026-08-10 17:42:17 +08:00
const candidateColumns : DataTableColumn [] = [
2026-09-04 10:51:31 +08:00
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' },
2026-09-15 18:17:17 +08:00
{ key : 'inboundCode' , label : '入仓码' , minWidth : 220 , fixed : 'left' , slotName : 'candidateInbounds' },
2026-09-08 17:24:49 +08:00
{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 145 , showOverflowTooltip : true },
2026-09-15 19:34:55 +08:00
{ key : 'coLoadGroup' , label : '装柜规则' , minWidth : 170 , slotName : 'coLoadGroup' },
2026-09-03 21:25:56 +08:00
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 70 , slotName : 'customerName' },
{ key : 'orderStatusSnapshot' , prop : 'orderStatusSnapshot' , label : '订单状态' , width : 100 , slotName : 'orderStatusSnapshot' },
2026-09-08 17:24:49 +08:00
{ key : 'availablePackageCount' , prop : 'availablePackageCount' , label : '箱数' , width : 80 },
2026-09-03 14:12:42 +08:00
// { key: 'cartonIdentityStatus', label: '箱号归属', width: 105, slotName: 'cartonIdentityStatus' },
2026-09-08 17:24:49 +08:00
{ key : 'unitGrossWeightKg' , label : '单位毛重' , width : 90 , slotName : 'unitGrossWeight' },
2026-09-03 20:24:54 +08:00
{ key : 'totalGrossWeightKg' , label : '重量' , width : 90 , slotName : 'candidateTotalWeight' },
2026-09-08 17:24:49 +08:00
{ key : 'averageUnitVolumeCbm' , label : '单位体积' , width : 90 , slotName : 'averageUnitVolume' },
2026-09-03 20:24:54 +08:00
{ key : 'totalVolumeCbm' , label : '体积' , width : 90 , slotName : 'candidateTotalVolume' },
2026-09-04 18:47:43 +08:00
{ key : 'estimatedVolumeCbm' , label : '预估体积' , width : 100 , slotName : 'estimatedVolume' },
2026-08-10 17:42:17 +08:00
]
const itemColumns : DataTableColumn [] = [
2026-09-04 10:51:31 +08:00
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' },
2026-09-16 11:06:36 +08:00
{ key : 'inboundCode' , label : '入仓码' , minWidth : 220 , fixed : 'left' , slotName : 'planInbounds' },
2026-09-08 17:24:49 +08:00
{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 145 , showOverflowTooltip : true },
2026-09-15 19:34:55 +08:00
{ key : 'coLoadGroup' , label : '装柜规则' , minWidth : 170 , slotName : 'coLoadGroup' },
2026-09-03 21:25:56 +08:00
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 70 , slotName : 'customerName' },
2026-09-04 14:17:36 +08:00
{ key : 'packageCount' , label : '分配箱数' , width : 140 , slotName : 'packageCount' },
2026-09-04 10:51:31 +08:00
{ key : 'grossWeightKg' , label : '毛重' , width : 90 , slotName : 'weight' },
{ key : 'volumeCbm' , label : '体积' , width : 90 , slotName : 'volume' },
2026-08-10 17:42:17 +08:00
]
function formatNumber ( value : number , digits = 3 ) {
2026-08-11 20:02:38 +08:00
return Number . isFinite ( value ) ? value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits }) : '0'
2026-08-10 17:42:17 +08:00
}
2026-09-04 09:57:39 +08:00
function formatMeasurement ( value : number | null | undefined , unit : string , digits = 3 ) {
return value == null ? '待测量' : ` ${ formatNumber ( Number ( value ), digits ) } ${ unit } `
}
2026-08-11 20:02:38 +08:00
function percentage ( value : number ) { return Number . isFinite ( value ) ? Number ( Math . max ( value , 0 ). toFixed ( 2 )) : 0 }
2026-08-29 11:41:57 +08:00
function candidateRow ( source : object ) { return source as SmartLoadingCandidateRow }
2026-08-24 18:07:38 +08:00
function itemRow ( source : object ) { return source as SmartLoadingInboundItem }
2026-08-14 19:59:32 +08:00
function adjustmentMax ( source : object ) {
const item = itemRow ( source )
2026-09-04 14:04:57 +08:00
return Math . max ( item . minPackageCount , item . totalPackageCount || item . packageCount )
}
function startQuantityEdit ( source : object ) {
const item = itemRow ( source )
quantityDrafts [ item . id ] = item . packageCount
editingQuantityId . value = item . id
2026-08-14 19:59:32 +08:00
}
2026-09-04 09:57:39 +08:00
function displayCabinetNo ( container : SmartLoadingWorkbenchContainer | null | undefined ) {
2026-08-15 17:04:00 +08:00
if ( ! container ) return '待生成'
2026-08-15 12:12:38 +08:00
const cabinetReference = String ( container . cabinetNo || '' ). trim ()
2026-08-15 17:04:00 +08:00
return /^\d{15,}$/ . test ( cabinetReference ) ? '待生成' : cabinetReference || '待生成'
2026-08-15 12:12:38 +08:00
}
2026-09-04 09:57:39 +08:00
function hasRealContainerNo ( container : SmartLoadingWorkbenchContainer | null | undefined ) {
2026-08-15 12:12:38 +08:00
if ( ! container ) return false
const value = String ( container . containerNo || '' ). trim ()
return Boolean ( value ) && ! /^(?:MAN|SPLIT)-/i . test ( value )
}
2026-09-04 09:57:39 +08:00
function displayActualContainerNo ( container : SmartLoadingWorkbenchContainer | null | undefined ) {
2026-09-01 15:24:37 +08:00
return hasRealContainerNo ( container ) ? String ( container ? . containerNo || '' ). trim () : '待补录'
2026-08-10 17:42:17 +08:00
}
function syncDrafts ( next : SmartLoadingPlan , preserved : Record < string , number > = {}) {
for ( const key of Object . keys ( quantityDrafts )) delete quantityDrafts [ key ]
2026-08-24 18:07:38 +08:00
for ( const item of groupSmartLoadingItems ( next . items || [])) quantityDrafts [ item . id ] = preserved [ item . id ] ?? item . packageCount
2026-08-10 17:42:17 +08:00
}
2026-08-11 14:23:46 +08:00
function acceptPlan ( next : SmartLoadingPlan , preserved : Record < string , number > = {}, syncContainerStatus = true ) {
2026-08-10 17:42:17 +08:00
plan . value = next
2026-09-04 09:57:39 +08:00
planGroupsByInboundId . value = loadSmartLoadingPlanCoLoadGroups ( next )
2026-08-10 17:42:17 +08:00
noDraft . value = false
syncDrafts ( next , preserved )
const container = selectedContainer . value
if ( container ) {
container . planId = next . id
2026-09-03 15:00:58 +08:00
if ( syncContainerStatus && next . status !== 'COMPLETED' ) container . planStatus = next . status
2026-08-10 17:42:17 +08:00
}
}
2026-09-03 10:27:31 +08:00
2026-08-10 17:42:17 +08:00
function clearSelectionData () {
plan . value = null
noDraft . value = false
selectionError . value = null
2026-08-29 11:41:57 +08:00
mutationError . value = null
planGroupsByInboundId . value = new Map ()
2026-08-10 17:42:17 +08:00
for ( const key of Object . keys ( quantityDrafts )) delete quantityDrafts [ key ]
}
2026-09-03 15:00:58 +08:00
function candidateQueryParams () : Parameters < typeof listSmartLoadingCandidates >[ 0 ] {
2026-09-03 21:25:56 +08:00
const createdRange = Array . isArray ( appliedQuery . value . createdRange )
? appliedQuery . value . createdRange . map (( value ) => String ( value || '' ). slice ( 0 , 10 ))
2026-09-03 15:00:58 +08:00
: []
2026-09-04 18:47:43 +08:00
const expectedArrivalRange = Array . isArray ( appliedQuery . value . expectedArrivalRange )
? appliedQuery . value . expectedArrivalRange . map (( value ) => String ( value || '' ). slice ( 0 , 10 ))
: []
2026-09-03 15:00:58 +08:00
const params : Parameters < typeof listSmartLoadingCandidates >[ 0 ] = {
channelGroupCodes : [... selectedChannelCodes . value ],
page : candidatePage . value ,
size : candidatePageSize . value ,
}
const warehouseId = String ( appliedQuery . value . warehouseId || '' )
const customerId = String ( appliedQuery . value . customerId || '' )
const inboundCode = String ( appliedQuery . value . inboundCode || '' ). trim ()
2026-09-03 22:15:25 +08:00
const orderStatuses = Array . isArray ( appliedQuery . value . orderStatuses )
? appliedQuery . value . orderStatuses . map (( value ) => String ( value )). filter ( Boolean ) as NonNullable < typeof params.orderStatuses >
: []
2026-09-03 15:00:58 +08:00
if ( warehouseId ) params . warehouseId = warehouseId
if ( customerId ) params . customerId = customerId
if ( inboundCode ) params . inboundCode = inboundCode
2026-09-03 21:25:56 +08:00
if ( createdRange . length === 2 ) {
params . createdStart = createdRange [ 0 ]
params . createdEnd = createdRange [ 1 ]
2026-09-03 15:00:58 +08:00
}
2026-09-04 18:47:43 +08:00
if ( expectedArrivalRange . length === 2 ) {
params . expectedArrivalStart = expectedArrivalRange [ 0 ]
params . expectedArrivalEnd = expectedArrivalRange [ 1 ]
}
2026-09-03 22:15:25 +08:00
if ( orderStatuses . length ) params . orderStatuses = orderStatuses
2026-09-03 15:00:58 +08:00
return params
}
2026-09-03 20:24:54 +08:00
function containerQueryParams ( page = 1 ) : Parameters < typeof listSmartLoadingWorkbenchContainers >[ 0 ] {
return {
channelGroupCodes : [... selectedChannelCodes . value ],
page ,
size : 200 ,
}
}
2026-09-03 15:00:58 +08:00
async function loadCandidateData () {
if ( ! selectedChannelCodes . value . length ) return
const request = ++ candidateRequest
candidateLoading . value = true
candidateError . value = null
try {
const result = await listSmartLoadingCandidates ( candidateQueryParams ())
if ( request !== candidateRequest ) return
candidates . value = result . records
candidateTotal . value = result . total
} catch ( error ) {
if ( request === candidateRequest ) {
candidates . value = []
candidateTotal . value = 0
candidateError . value = getApiErrorDetails ( error , '候选装柜明细加载失败' )
}
} finally {
if ( request === candidateRequest ) candidateLoading . value = false
}
}
async function loadSelectedPlan ( preserved : Record < string , number > = {}) {
2026-08-10 17:42:17 +08:00
const container = selectedContainer . value
const request = ++ selectionRequest
clearSelectionData ()
if ( ! container ) return
selectionLoading . value = true
try {
2026-09-03 15:00:58 +08:00
const planResult = await getSmartLoadingPlanByBooking ( container . bookingId ). then (( value ) => ({ value , missing : false })). catch (( error ) => {
const details = getApiErrorDetails ( error )
if ( details . code === 'A0404' ) return { value : null , missing : true }
throw error
})
2026-08-10 17:42:17 +08:00
if ( request !== selectionRequest ) return
2026-08-29 11:41:57 +08:00
if ( planResult . value ) {
acceptPlan ( planResult . value , preserved , false )
2026-08-31 10:48:23 +08:00
syncDrafts ( planResult . value , preserved )
2026-08-29 11:41:57 +08:00
}
2026-08-10 17:42:17 +08:00
else noDraft . value = planResult . missing
} catch ( error ) {
if ( request === selectionRequest ) selectionError . value = getApiErrorDetails ( error , '柜次装柜数据加载失败' )
} finally {
if ( request === selectionRequest ) selectionLoading . value = false
}
}
2026-09-03 15:00:58 +08:00
async function loadContainerData ( preserved : Record < string , number > = {}) {
if ( ! selectedChannelCodes . value . length ) return
const request = ++ containerRequest
try {
2026-09-03 20:24:54 +08:00
const firstPage = await listSmartLoadingWorkbenchContainers ( containerQueryParams ())
2026-09-03 15:00:58 +08:00
const remainingPages = firstPage . pages > 1
2026-09-03 20:24:54 +08:00
? await Promise . all ( Array . from ({ length : firstPage . pages - 1 }, ( _ , index ) => listSmartLoadingWorkbenchContainers ( containerQueryParams ( index + 2 ))))
2026-09-03 15:00:58 +08:00
: []
if ( request !== containerRequest ) return
containers . value = [ firstPage , ... remainingPages ]. flatMap (( page ) => page . records )
if ( ! containers . value . some (( item ) => item . bookingId === selectedContainerId . value )) selectedContainerId . value = preferredContainerId ()
await loadSelectedPlan ( preserved )
} catch ( error ) {
if ( request === containerRequest ) {
containers . value = []
selectedContainerId . value = ''
clearSelectionData ()
selectionError . value = getApiErrorDetails ( error , '已选渠道柜次加载失败' )
}
}
}
async function loadWorkbenchData ( preserved : Record < string , number > = {}) {
await Promise . all ([ loadCandidateData (), loadContainerData ( preserved )])
2026-08-11 11:15:44 +08:00
}
async function loadAll ( preserved : Record < string , number > = {}) {
2026-08-10 17:42:17 +08:00
loading . value = true
loadError . value = null
try {
2026-09-04 09:57:39 +08:00
warehouses . value = await listAllPlatformWarehouses ({ warehouseType : 'DOMESTIC' , status : 'ACTIVE' })
2026-09-03 11:47:32 +08:00
const availableChannelCodes = new Set ( channelOptions . value . map (( item ) => item . code ))
selectedChannelCodes . value = selectedChannelCodes . value . filter (( code ) => availableChannelCodes . has ( code ))
if ( ! selectedChannelCodes . value . length ) {
2026-09-04 09:57:39 +08:00
selectedChannelCodes . value = [ channelOptions . value [ 0 ] ? . code || '' ]
2026-08-14 19:02:34 +08:00
}
2026-09-03 15:00:58 +08:00
await loadWorkbenchData ( preserved )
2026-08-10 17:42:17 +08:00
} catch ( error ) {
loadError . value = getApiErrorDetails ( error , '智能装柜工作台加载失败' )
} finally {
loading . value = false
}
}
2026-08-11 11:15:44 +08:00
async function load () { await loadAll () }
2026-08-14 12:50:42 +08:00
async function loadCustomers () {
customersLoading . value = true
try {
const result = await listPlatformTenants ({ page : 1 , size : 200 , status : 'ACTIVE' })
customers . value = result . records . filter (( customer ) => customer . status === 'ACTIVE' )
} catch {
customers . value = []
} finally {
customersLoading . value = false
}
}
2026-08-10 17:42:17 +08:00
async function selectContainer ( containerId : string ) {
if ( selectedContainerId . value === containerId ) return
selectedContainerId . value = containerId
2026-09-03 15:00:58 +08:00
await loadSelectedPlan ()
2026-08-10 17:42:17 +08:00
}
2026-09-03 11:47:32 +08:00
async function selectChannels ( codes : string []) {
if ( ! codes . length ) {
ElMessage . warning ( '至少选择一个渠道' )
return
}
selectedChannelCodes . value = codes
2026-09-03 15:00:58 +08:00
candidatePage . value = 1
await loadWorkbenchData ()
2026-08-14 19:02:34 +08:00
}
2026-09-04 14:17:36 +08:00
async function selectOnlyChannel ( code : string ) {
if ( selectedChannelCodes . value . length === 1 && selectedChannelCodes . value [ 0 ] === code ) return
selectedChannelCodes . value = [ code ]
candidatePage . value = 1
await loadWorkbenchData ()
}
2026-08-10 17:42:17 +08:00
async function applyFilters ( model : QueryModel ) {
2026-09-03 22:15:25 +08:00
appliedQuery . value = {
... model ,
orderStatuses : Array . isArray ( model . orderStatuses ) ? model . orderStatuses . map (( value ) => String ( value )) : [],
}
2026-09-01 21:09:39 +08:00
candidatePage . value = 1
2026-09-03 20:24:54 +08:00
await loadCandidateData ()
2026-09-03 15:00:58 +08:00
}
async function changeCandidatePage ( page : number ) {
candidatePage . value = page
await loadCandidateData ()
}
async function changeCandidatePageSize ( size : number ) {
candidatePageSize . value = size
candidatePage . value = 1
await loadCandidateData ()
2026-08-10 17:42:17 +08:00
}
function openAssign ( source : object ) {
const candidate = candidateRow ( source )
2026-08-29 11:41:57 +08:00
if ( candidateBlocked ( candidate )) return
2026-08-10 17:42:17 +08:00
selectedCandidate . value = candidate
2026-08-29 11:41:57 +08:00
assignForm . packageCount = candidate . coLoadGroupId
? Number ( candidate . groupTotalBoxes || candidate . availablePackageCount )
: candidate . availablePackageCount
2026-08-10 17:42:17 +08:00
assignOpen . value = true
}
async function assign () {
2026-09-08 17:24:49 +08:00
if ( ! plan . value || ! selectedCandidate . value ) return
if ( selectedCandidate . value . unitType !== 'SEALED_PALLET' && assignForm . packageCount < 1 ) return
2026-08-29 11:41:57 +08:00
if ( selectedCandidate . value . coLoadGroupId && ( selectedCandidate . value . blockReason || selectedCandidate . value . locked )) return
if ( ! selectedCandidate . value . coLoadGroupId && assignForm . packageCount > selectedCandidate . value . availablePackageCount ) {
2026-08-14 19:59:32 +08:00
ElMessage . warning ( `本次最多可分配 ${ selectedCandidate . value . availablePackageCount } 箱` )
return
}
2026-08-10 17:42:17 +08:00
await mutate ( async () => {
2026-08-29 11:41:57 +08:00
const selected = selectedCandidate . value !
2026-09-08 17:24:49 +08:00
acceptPlan ( selected . unitType === 'SEALED_PALLET' && selected . palletId
? await assignSmartLoadingPallet ( plan . value ! . id , { palletId : selected . palletId , expectedVersion : plan . value ! . version })
: selected . orderStatusSnapshot === 'PENDING_REVIEW'
2026-09-03 21:25:56 +08:00
? await assignPendingReviewSmartLoadingOrder ( plan . value ! . id , {
2026-09-04 10:51:31 +08:00
customerId : selected . accountId ,
2026-09-03 21:25:56 +08:00
orderId : selected . orderId ,
inboundCode : selected . inboundCode || '' ,
packageCount : assignForm . packageCount ,
expectedVersion : plan . value ! . version ,
})
: selected . coLoadGroupId && selected . inboundId
2026-08-29 11:41:57 +08:00
? await assignSmartLoadingGroup ( plan . value ! . id , selected . inboundId , plan . value ! . version )
: await assignSmartLoadingItem ( plan . value ! . id , {
2026-09-03 21:25:56 +08:00
inboundId : selected . inboundId ! ,
2026-08-29 11:41:57 +08:00
packageCount : assignForm . packageCount ,
expectedVersion : plan . value ! . version ,
}))
2026-08-10 17:42:17 +08:00
assignOpen . value = false
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-08-29 11:41:57 +08:00
ElMessage . success ( selected . coLoadGroupId ? `同柜不可拆组 ${ selected . groupNo } 已整组分配` : '明细已加入当前柜次' )
2026-08-18 16:09:43 +08:00
}, '分配明细失败' , { preserveAssign : true })
2026-08-10 17:42:17 +08:00
}
2026-08-29 11:41:57 +08:00
function candidateBlocked ( candidate : SmartLoadingCandidateRow ) {
2026-09-08 17:24:49 +08:00
if ( candidate . unitType === 'SEALED_PALLET' ) {
return Boolean ( candidate . blockReason ) || candidate . locked === true || ! candidate . palletId
|| ! candidate . indivisible || assignedPalletIds . value . has ( candidate . palletId )
}
2026-09-03 21:25:56 +08:00
if ( candidate . orderStatusSnapshot === 'PENDING_REVIEW' ) {
return Boolean ( candidate . blockReason ) || candidate . locked === true
2026-09-04 10:51:31 +08:00
|| ! candidate . accountId || ! candidate . inboundCode || candidate . availablePackageCount < 1
2026-09-03 21:25:56 +08:00
}
2026-08-29 11:41:57 +08:00
return Boolean ( candidate . blockReason ) || candidate . locked === true
2026-09-03 21:25:56 +08:00
|| ! candidate . inboundId
|| ! candidate . inboundCode
2026-09-03 15:00:58 +08:00
|| candidate . memberInboundIds . some (( inboundId ) => assignedInboundIds . value . has ( inboundId ))
2026-08-29 11:41:57 +08:00
|| candidate . availablePackageCount < 1
}
2026-08-10 17:42:17 +08:00
async function recommend () {
if ( ! plan . value ) return
2026-09-14 17:22:08 +08:00
const scope = {
customerId : String ( appliedQuery . value . customerId || '' ),
inboundCode : String ( appliedQuery . value . inboundCode || '' ). trim (),
}
2026-08-10 17:42:17 +08:00
await mutate ( async () => {
2026-08-22 10:02:13 +08:00
try {
2026-09-14 17:22:08 +08:00
acceptPlan ( await recommendSmartLoading ( plan . value ! . id , plan . value ! . version , scope ))
2026-08-22 10:02:13 +08:00
} catch ( error ) {
const details = getApiErrorDetails ( error , '智能推荐失败' )
if ( details . code !== 'A0409' || ! /(版本|并发|已被修改|已变化)/ . test ( details . message )) throw error
2026-09-03 15:00:58 +08:00
await loadSelectedPlan ()
2026-08-22 10:02:13 +08:00
if ( ! plan . value ) throw error
2026-09-14 17:22:08 +08:00
acceptPlan ( await recommendSmartLoading ( plan . value . id , plan . value . version , scope ))
2026-08-22 10:02:13 +08:00
}
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-08-10 17:42:17 +08:00
ElMessage . success ( '智能推荐已生成' )
}, '智能推荐失败' )
}
async function adjust ( source : object ) {
const item = itemRow ( source )
2026-09-08 17:24:49 +08:00
if ( item . unitType === 'SEALED_PALLET' ) return
2026-09-04 12:18:39 +08:00
if ( ! plan . value || mutating . value ) return
2026-08-10 17:42:17 +08:00
const count = Number ( quantityDrafts [ item . id ])
2026-09-04 12:18:39 +08:00
if ( count === item . packageCount ) return
2026-08-17 16:45:46 +08:00
if ( ! Number . isInteger ( count ) || count < item . minPackageCount ) {
ElMessage . warning ( `分配箱数不能低于已拣下限 ${ item . minPackageCount } 箱` )
return
}
2026-08-14 19:59:32 +08:00
const max = adjustmentMax ( item )
2026-08-18 16:09:43 +08:00
if ( count > max ) { ElMessage . warning ( `该明细最多可分配 ${ max } 箱` ); return }
2026-08-10 17:42:17 +08:00
await mutate ( async () => {
acceptPlan ( await adjustSmartLoadingItem ( plan . value ! . id , item . id , count , plan . value ! . version , item . version ))
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-09-04 14:04:57 +08:00
editingQuantityId . value = ''
2026-08-10 17:42:17 +08:00
ElMessage . success ( '箱数已调整' )
}, '调整箱数失败' , { preserveDrafts : { ... quantityDrafts } })
}
2026-10-20 10:11:31 +08:00
function workbenchDialogTitle ( action : string ) {
const code = selectedContainer . value ? . cabinetNo ? . trim () || ( hasRealContainerNo ( selectedContainer . value ) ? selectedContainer . value ? . containerNo ? . trim () : '' )
return code ? ` ${ action } · ${ code } ` : action
}
2026-08-10 17:42:17 +08:00
async function remove ( source : object ) {
const item = itemRow ( source )
2026-08-17 16:45:46 +08:00
if ( ! plan . value || ! item . canRemove ) return
2026-08-29 11:41:57 +08:00
const row = source as SmartLoadingPlanRow
2026-09-14 14:35:30 +08:00
const sealedPallet = item . unitType === 'SEALED_PALLET' && Boolean ( item . palletId )
2026-08-29 11:41:57 +08:00
const confirmed = await ElMessageBox . confirm ( row . coLoadGroup
2026-08-30 15:49:01 +08:00
? `确认整组移除 ${ row . coLoadGroup . groupNo } ( ${ groupMemberLabel ( row . coLoadGroup ) } 、 ${ row . packageCount } 箱)吗?组内所有入仓片段将联动移除。`
2026-09-14 14:35:30 +08:00
: sealedPallet
? `确认从当前柜次整托移除托盘“ ${ item . palletNo || '—' } ”( ${ item . packageCount } 箱)吗?`
: `确认从当前柜次移除入仓码“ ${ item . inboundCode || '—' } ”的 ${ item . packageCount } 箱吗?` ,
2026-10-20 10:11:31 +08:00
workbenchDialogTitle ( row . coLoadGroup ? '移除同柜不可拆组' : sealedPallet ? '整托移除' : '移除装柜明细' ), {
customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : row . coLoadGroup ? '移除整组' : sealedPallet ? '整托移除' : '移除' , cancelButtonText : '取消' ,
2026-08-10 17:42:17 +08:00
}). catch (() => false )
if ( ! confirmed || ! plan . value ) return
await mutate ( async () => {
2026-08-29 11:41:57 +08:00
if ( row . coLoadGroup ) {
acceptPlan ( await removeSmartLoadingGroup ( plan . value ! . id , row . coLoadGroup . groupId , plan . value ! . version ))
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-08-29 11:41:57 +08:00
ElMessage . success ( `同柜不可拆组 ${ row . coLoadGroup . groupNo } 已整组移除` )
return
}
2026-09-14 14:35:30 +08:00
if ( sealedPallet && item . palletId ) {
acceptPlan ( await removeSmartLoadingPallet ( plan . value ! . id , item . palletId , plan . value ! . version ))
await loadCandidateData ()
ElMessage . success ( `托盘 ${ item . palletNo || '—' } 已整托移除` )
return
}
2026-08-24 18:07:38 +08:00
let current = plan . value !
for ( const original of item . sourceItems ) {
const currentItem = current . items . find (( value ) => value . id === original . id )
if ( ! currentItem ) continue
current = await removeSmartLoadingItem ( current . id , currentItem . id , current . version , currentItem . version )
acceptPlan ( current )
}
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-08-24 18:07:38 +08:00
ElMessage . success ( '该入仓码已从当前柜次移除' )
2026-08-11 11:52:30 +08:00
}, '移除装柜明细失败' , { pickingConflict : true })
2026-08-10 17:42:17 +08:00
}
2026-08-14 19:59:32 +08:00
async function removeAll () {
2026-08-24 18:07:38 +08:00
const removableItems = displayedPlanItems . value . filter (( item ) => item . canRemove )
2026-08-17 16:45:46 +08:00
if ( ! removableItems . length ) return
const itemCount = removableItems . length
2026-10-20 10:11:31 +08:00
const confirmed = await ElMessageBox . confirm ( `确认移除当前方案的 ${ itemCount } 个未拣入仓码吗?已拣入仓码会保留。` , workbenchDialogTitle ( '移除未拣明细' ), {
customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认移除' , cancelButtonText : '取消' ,
2026-08-14 19:59:32 +08:00
}). catch (() => false )
if ( ! confirmed || ! plan . value ) return
await mutate ( async () => {
2026-09-04 10:51:31 +08:00
acceptPlan ( await removeAllUnpickedSmartLoadingItems ( plan . value ! . id , plan . value ! . version ))
2026-09-04 09:57:39 +08:00
await loadCandidateData ()
2026-08-24 18:07:38 +08:00
ElMessage . success ( `已移除 ${ itemCount } 个未拣入仓码` )
2026-08-14 19:59:32 +08:00
}, '全部移除失败' , { pickingConflict : true })
}
2026-08-10 17:42:17 +08:00
async function mutate (
action : () => Promise < void >,
fallback : string ,
2026-08-11 11:52:30 +08:00
recovery : { preserveDrafts ?: Record < string , number >; preserveAssign ?: boolean ; pickingConflict ?: boolean } = {},
2026-08-10 17:42:17 +08:00
) {
2026-09-04 11:35:24 +08:00
if ( mutating . value ) return
2026-08-10 17:42:17 +08:00
mutating . value = true
2026-08-29 11:41:57 +08:00
mutationError . value = null
2026-08-10 17:42:17 +08:00
try {
await action ()
} catch ( error ) {
const details = getApiErrorDetails ( error , fallback )
if ( details . code === 'A0409' ) {
2026-08-11 11:15:44 +08:00
await loadAll ( recovery . preserveDrafts )
2026-08-11 11:52:30 +08:00
const pickingMessage = recovery . pickingConflict && details . message . includes ( '拣货' )
? '该订单在拣货中不许删除'
: null
2026-08-22 10:02:13 +08:00
const message = pickingMessage || details . message || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作'
2026-08-10 17:42:17 +08:00
if ( recovery . preserveAssign ) assignOpen . value = true
2026-08-11 11:52:30 +08:00
if ( pickingMessage ) ElMessage . error ( pickingMessage )
2026-08-14 19:59:32 +08:00
else ElMessage . warning ( message )
2026-08-10 17:42:17 +08:00
} else {
ElMessage . error ( details . message )
}
2026-08-29 11:41:57 +08:00
mutationError . value = details
2026-08-10 17:42:17 +08:00
} finally {
mutating . value = false
}
}
2026-08-14 12:50:42 +08:00
onMounted (() => { void load (); void loadCustomers () })
2026-08-10 17:42:17 +08:00
</ script >
< template >
2026-10-20 10:11:31 +08:00
< component class = "admin-density" : is = "props.embedded ? 'div' : AdminPage" : class = "{ 'embedded-smart-loading': props.embedded }" >
2026-08-10 17:42:17 +08:00
< section class = "global-loading-page" >
2026-09-03 11:47:32 +08:00
< div v-if = "channelOptions.length" class="channel-summary-row" >
< el-checkbox-group :model-value = "selectedChannelCodes" class = "channel-tags" aria -label =" 渠道筛选 , 至少选择一项 " @change ="selectChannels($event as string[])" >
< span class = "channel-tags__label" > 渠道 </ span >
2026-09-04 14:17:36 +08:00
< el-checkbox-button
v-for = "channel in channelOptions"
:key = "channel.code"
:label = "channel.code"
:value = "channel.code"
title = "双击仅选择此渠道"
@dblclick.stop.prevent ="selectOnlyChannel(channel.code)"
>{{ channel . name }}</ el-checkbox-button >
2026-09-03 11:47:32 +08:00
</ el-checkbox-group >
< dl class = "compact-summary" aria -label =" 已选渠道装柜统计 " >
2026-09-03 15:00:58 +08:00
< div >< dt > 候选明细 </ dt >< dd >< strong >{{ candidateTotal }}</ strong > 条 </ dd ></ div >
2026-09-03 11:47:32 +08:00
< div >< dt > 柜子数量 </ dt >< dd >< strong >{{ filteredContainers . length }}</ strong > 个 </ dd ></ div >
2026-08-14 19:59:32 +08:00
< div >< dt > 已装 </ dt >< dd >< strong >{{ totalPackages }}</ strong > 箱 </ dd ></ div >
< div >< dt > 总重量 </ dt >< dd >< strong >{{ formatNumber ( totalWeight , 1 ) }}</ strong > kg </ dd ></ div >
< div >< dt > 总体积 </ dt >< dd >< strong >{{ formatNumber ( totalVolume ) }}</ strong > m³ </ dd ></ div >
</ dl >
</ div >
2026-09-10 19:25:11 +08:00
< el-alert v-if = "missingChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少计划渠道,已隔离;请重新创建模拟柜`" />
< el-alert v-if = "unmappedChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedChannelContainerCount} 个可编排柜次的计划渠道未纳入固定 9 类,已隔离`" />
2026-09-03 10:27:31 +08:00
< el-alert v-if = "completedContainerCount && !orchestratableContainers.length" type="success" :closable="false" show-icon title="当前柜次已完成装柜,不可继续编辑" />
2026-08-10 17:42:17 +08:00
< QueryToolbar
v-model = "query"
2026-09-03 10:27:31 +08:00
class = "smart-loading-query"
2026-08-10 17:42:17 +08:00
ariaLabel = "智能装柜筛选"
:fields = "queryFields"
2026-08-14 19:59:32 +08:00
preference -key =" global -smart-loading-query-v2 "
2026-08-10 17:42:17 +08:00
:search-on-reset = "false"
@search ="applyFilters"
2026-09-03 11:47:32 +08:00
@reset ="applyFilters"
2026-08-10 17:42:17 +08:00
/>
< LoadErrorState v-if = "loadError" title="智能装柜工作台加载失败" :message="loadError.message" :loading="loading" @retry="load" >
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
</ LoadErrorState >
< template v-else >
2026-09-03 15:00:58 +08:00
< LoadErrorState v-if = "mutationError" title="装柜操作失败" :message="mutationError.message" compact retry-text="刷新工作台" @retry="loadWorkbenchData()" >
2026-08-29 11:41:57 +08:00
< template # details >< ApiErrorMeta :details = "mutationError" /></ template >
</ LoadErrorState >
2026-08-10 17:42:17 +08:00
< section class = "dispatch-grid" aria -label =" 智能装柜调度台 " >
< section class = "dispatch-pane candidate-pane" >
2026-09-03 15:00:58 +08:00
< header class = "pane-header" >< div >< strong > 候选装柜明细 </ strong >< small >{{ candidateScopeLabel }}</ small ></ div ></ header >
< LoadErrorState v-if = "candidateError" title="候选明细加载失败" :message="candidateError.message" compact @retry="loadCandidateData()" >
< template # details >< ApiErrorMeta :details = "candidateError" /></ template >
</ LoadErrorState >
2026-08-10 17:42:17 +08:00
< DataTable
2026-09-03 15:00:58 +08:00
:page = "candidatePage"
:page-size = "candidatePageSize"
:data = "displayedCandidates"
2026-08-10 17:42:17 +08:00
:columns = "candidateColumns"
2026-09-03 15:00:58 +08:00
: loading = "loading || candidateLoading || mutating"
:total = "candidateTotal"
2026-09-01 21:09:39 +08:00
: page -sizes ="[ 20 , 50 , 100 ]"
2026-08-12 18:16:44 +08:00
stripe
2026-08-10 17:42:17 +08:00
preference -key =" global -smart-loading-candidates "
2026-08-11 20:39:34 +08:00
column -settings-mode =" hide "
2026-09-03 15:00:58 +08:00
empty -text =" 当前筛选条件下暂无候选装柜明细 "
@ update :page = "changeCandidatePage"
@ update :page-size = "changeCandidatePageSize"
2026-08-10 17:42:17 +08:00
>
2026-09-03 13:49:40 +08:00
< template # pagination -left =" { total , page , pageCount } " >< span v-if = "pageCount > 0" > 共 {{ total }} 条 · {{ page }} / {{ pageCount }} 页 </ span >< span v-else > 共 0 条 </ span ></ template >
2026-08-10 17:42:17 +08:00
< template # actions = "{ row }" >
< el-button
2026-10-20 10:11:31 +08:00
class = "density-action" data -tone =" primary "
2026-08-17 16:45:46 +08:00
v-if = "canAssign && assignable"
2026-08-12 18:00:26 +08:00
size = "small"
2026-10-20 10:11:31 +08:00
type = "primary"
2026-08-12 18:00:26 +08:00
plain
2026-09-04 11:35:24 +08:00
: disabled = "mutating || candidateBlocked(row as SmartLoadingCandidateRow)"
2026-08-10 17:42:17 +08:00
@click ="openAssign(row)"
2026-09-15 18:17:17 +08:00
> 加入 </ el-button >
2026-08-10 17:42:17 +08:00
</ template >
2026-09-15 18:17:17 +08:00
< template # candidateInbounds = "{ row }" >< SmartLoadingCandidateInbounds : candidate = "row as SmartLoadingCandidate" /></ template >
2026-09-03 21:25:56 +08:00
< template # customerName = "{ row }" >< LineClampText :content = "row.customerName" /></ template >
2026-09-15 19:34:55 +08:00
< template # coLoadGroup = "{ row }" >< details v-if = "row.coLoadGroupId" class="candidate-group"><summary>整组加入 · 不可拆</summary><el-tag size="small" type="warning"><LineClampText :content="`${row.groupNo} · ${row.memberOrderCount} 票 · ${row.inboundFragmentCount} 个入仓片段`" /></el-tag><LineClampText :content="row.memberOrderSummaries.join('; ')" /><LineClampText :content="`整组 ${row.groupTotalBoxes} 箱,必须全量进入同一柜`" /><LineClampText v-if="row.blockReason" class="candidate-block" :content="row.blockReason" /><LineClampText v-else-if="row.locked" content="该组已锁定,不能重复分配" /></details><LineClampText v-else :content="row.unitType === 'SEALED_PALLET' ? '整托加入 · 不可拆' : '按箱加入'" /></ template >
2026-09-03 21:25:56 +08:00
< template # orderStatusSnapshot = "{ row }" >< el-tag size = "small" effect = "plain" >{{ candidateStatusLabel ( row . orderStatusSnapshot ) }}</ el-tag ></ template >
2026-09-04 09:57:39 +08:00
< template # unitGrossWeight = "{ row }" >{{ formatMeasurement ( row . unitGrossWeightKg , 'kg' , 1 ) }}</ template >
< template # candidateTotalWeight = "{ row }" >{{ formatMeasurement ( row . totalGrossWeightKg , 'kg' , 1 ) }}</ template >
< template # averageUnitVolume = "{ row }" >{{ formatMeasurement ( row . averageUnitVolumeCbm , 'm³' ) }}</ template >
2026-09-04 18:47:43 +08:00
< template # candidateTotalVolume = "{ row }" >{{ formatMeasurement ( row . totalVolumeCbm , 'm³' ) }}</ template >
< template # estimatedVolume = "{ row }" >{{ row . estimatedVolumeCbm == null ? '—' : ` ${ formatNumber ( Number ( row . estimatedVolumeCbm )) } m³` }}</ template >
2026-08-21 17:07:37 +08:00
< template # cartonIdentityStatus >< el-tag size = "small" type = "info" effect = "plain" > 未核验 </ el-tag ></ template >
2026-08-10 17:42:17 +08:00
</ DataTable >
</ section >
< aside class = "dispatch-pane container-pane" aria -label =" 货柜列表 " >
2026-09-03 11:47:32 +08:00
< header class = "pane-header" >< div >< strong > 已选渠道柜次 </ strong ></ div ></ header >
2026-08-10 17:42:17 +08:00
< div v-loading = "loading" class="container-list" >
< el-empty v-if = "!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
< button
v-for = "item in filteredContainers"
2026-09-03 15:00:58 +08:00
:key = "item.bookingId"
2026-08-10 17:42:17 +08:00
type = "button"
class = "container-option"
2026-09-03 15:00:58 +08:00
: class = "{ selected: item.bookingId === selectedContainerId }"
2026-10-20 10:11:31 +08:00
: aria -pressed =" item.bookingId === selectedContainerId "
2026-09-03 15:00:58 +08:00
@click ="selectContainer(item.bookingId)"
2026-08-10 17:42:17 +08:00
>
2026-08-15 12:12:38 +08:00
< span class = "container-option__top" >
2026-08-15 17:04:00 +08:00
< strong >{{ hasRealContainerNo ( item ) ? displayActualContainerNo ( item ) : '模拟柜次' }}</ strong >
2026-08-15 12:12:38 +08:00
< el-tag size = "small" : type = "hasRealContainerNo(item) ? 'success' : 'info'" >
2026-08-15 17:04:00 +08:00
{{ hasRealContainerNo ( item ) ? '已录入集装箱号' : '空柜' }}
2026-08-15 12:12:38 +08:00
</ el-tag >
</ span >
2026-08-15 17:04:00 +08:00
< span class = "container-option__identity" > 柜次号 : {{ displayCabinetNo ( item ) }}</ span >
< span > 集装箱号 : {{ displayActualContainerNo ( item ) }}</ span >
2026-09-10 19:25:11 +08:00
< span > 渠道 : {{ containerChannelGroup ( item ) ? . name || '渠道待补充' }}</ span >
2026-08-30 17:43:03 +08:00
< span > 仓库 : {{ item . warehouseName || '仓库待补充' }} · 柜型 : {{ item . containerType || '柜型待补充' }}</ span >
2026-08-18 16:09:43 +08:00
<!-- 空柜的操作时机由状态和操作按钮表达 , 不在每张柜次卡片重复提示 。 -->
2026-08-10 17:42:17 +08:00
</ button >
</ div >
</ aside >
< section class = "dispatch-pane plan-pane" >
< header class = "pane-header plan-header" >
2026-08-18 16:09:43 +08:00
< div >< strong >{{ hasRealContainerNo ( selectedContainer ) ? displayActualContainerNo ( selectedContainer ) : displayCabinetNo ( selectedContainer ) }} 装柜方案 </ strong ></ div >
2026-08-10 17:42:17 +08:00
< div class = "plan-actions" >
2026-09-03 15:00:58 +08:00
< el-tooltip content = "刷新当前柜次" placement = "top" >< el-button aria -label =" 刷新当前柜次 " :loading = "selectionLoading" @click ="loadSelectedPlan()" >< RefreshCw :size = "16" /></ el-button ></ el-tooltip >
2026-08-17 16:45:46 +08:00
< el-button v-if = "canAssign && assignable && plan?.status === 'EDITING'" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" /> 智能推荐 </ el-button >
< el-button v-if = "canDelete && editable && plan?.items.some((item) => item.canRemove)" type="danger" plain :loading="mutating" @click="removeAll"><Trash2 :size="16" /> 移除未拣 </ el-button >
2026-08-10 17:42:17 +08:00
</ div >
</ header >
2026-09-03 15:00:58 +08:00
< LoadErrorState v-if = "selectionError" title="当前柜次加载失败" :message="selectionError.message" compact @retry="loadSelectedPlan()" >
2026-08-10 17:42:17 +08:00
< template # details >< ApiErrorMeta :details = "selectionError" /></ template >
</ LoadErrorState >
< el-empty v-else-if = "!selectedContainer" description="请选择一个货柜" />
2026-09-01 20:07:40 +08:00
< el-empty v-else-if = "noDraft" description="该柜次缺少自动生成的预装计划,请刷新后重试" />
2026-08-10 17:42:17 +08:00
< template v-else-if = "plan" >
2026-08-18 16:09:43 +08:00
<!--
已下发 PDA 的可编辑说明不再常驻展示 , 避免挤占装柜明细空间 。
操作权限仍由 plan.canEdit 、 canAdjust 和 canRemove 控制 。
-- >
2026-08-17 16:45:46 +08:00
< el-alert
2026-08-18 16:09:43 +08:00
v-if = "!plan.canEdit"
2026-08-17 16:45:46 +08:00
class = "preload-readiness-alert"
: type = "plan.finalCompletionStatus === 'COMPLETED' ? 'success' : 'info'"
:closable = "false"
show -icon
: title = "plan.finalCompletionStatus === 'IN_PROGRESS' ? '正在最终确认,装柜方案暂时只读' : plan.finalCompletionStatus === 'COMPLETED' ? '装柜已完成,方案永久只读' : '当前装柜方案只读'"
: description = "plan.blockedReason || '请刷新计划获取最新作业状态。'"
/>
2026-08-10 17:42:17 +08:00
< div class = "capacity-grid" >
2026-08-11 20:02:38 +08:00
< div >< div class = "meter-title" >< span > 体积 </ span >< strong >{{ formatNumber ( totalVolume ) }} / {{ formatNumber ( volumeLimit ) }} m³ </ strong ></ div >< el-progress : percentage = "Math.min(displayedVolumePercent, 100)" : status = "volumePercent > 100 ? 'exception' : undefined" /></ div >
< div >< div class = "meter-title" >< span > 重量 </ span >< strong >{{ formatNumber ( totalWeight , 1 ) }} / {{ formatNumber ( weightLimit , 1 ) }} kg </ strong ></ div >< el-progress : percentage = "Math.min(displayedWeightPercent, 100)" : status = "weightPercent > 100 ? 'exception' : undefined" /></ div >
2026-08-10 17:42:17 +08:00
</ div >
2026-08-24 18:07:38 +08:00
< DataTable :data = "displayedPlanItems" :columns = "itemColumns" : loading = "selectionLoading || mutating" stripe preference -key =" global -smart-loading-items " column -settings-mode =" hide " empty -text =" 当前柜次暂无装柜明细 " >
2026-10-20 10:11:31 +08:00
< template # actions = "{ row }" >< div class = "row-actions density-actions" >< el-button class = "density-action" data -tone =" danger " v-if = "canDelete && editable && row.canRemove" size="small" type="danger" plain :disabled="mutating" @click="remove(row)"><Trash2 :size="15" /> 删除 </ el-button ></ div ></ template >
2026-09-16 11:06:36 +08:00
< template # planInbounds = "{ row }" >< SmartLoadingCandidateInbounds :members = "row.palletMembers" : sealed -pallet =" row.unitType === ' SEALED_PALLET '" :fallback-inbound-code = "row.inboundCode" /></ template >
2026-09-03 21:25:56 +08:00
< template # customerName = "{ row }" >< LineClampText :content = "row.customerName" /></ template >
2026-09-15 19:34:55 +08:00
< template # coLoadGroup = "{ row }" >< details v-if = "row.coLoadGroup" class="candidate-group"><summary>整组加入 · 不可拆</summary><el-tag size="small" type="warning"><LineClampText :content="`${row.coLoadGroup.groupNo} · ${groupMemberLabel(row.coLoadGroup)}`" /></el-tag><LineClampText :content="row.memberOrderSummaries.join('; ')" /><LineClampText :content="`固定 ${row.coLoadGroup.fullPackageCount} 箱 · ${row.coLoadGroup.lockedPlanId ? '已锁定' : '未锁定'}`" /></details><LineClampText v-else :content="row.unitType === 'SEALED_PALLET' ? '整托加入 · 不可拆' : '按箱加入'" /></ template >
2026-09-08 17:24:49 +08:00
< template # packageCount = "{ row }" >< LineClampText v-if = "row.unitType === 'SEALED_PALLET'" :content="`${row.packageCount} 箱(整托固定)`" /><LineClampText v-else-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-if="editingQuantityId === row.id" v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :controls="false" :disabled="mutating" placeholder="请输入使用箱数" @blur="adjust(row)" @keyup.enter="adjust(row)" /><div v-else class="package-count-summary"><div class="package-count-used"><strong>使用:{{ row.packageCount }}</strong><el-button v-if="canAssign && editable && row.canAdjust" link circle :disabled="mutating" aria-label="调整箱数" @click="startQuantityEdit(row)"><Pencil :size="14" /></el-button></div><small v-if="(row.remainingPackageCount ?? 0) > 0">未用:{{ row.remainingPackageCount }}</small></div><LineClampText v-if="row.detailCount > 1 && editable" content="多规格,需删除后重加" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></ div ></ template >
2026-08-10 17:42:17 +08:00
< template # weight = "{ row }" >{{ formatNumber ( Number ( row . grossWeightKg || 0 ), 1 ) }} kg </ template >
2026-08-18 16:09:43 +08:00
< template # volume = "{ row }" >{{ formatNumber ( Number ( row . volumeCbm || 0 )) }} m³ </ template >
2026-08-10 17:42:17 +08:00
</ DataTable >
</ template >
</ section >
</ section >
2026-10-20 10:11:31 +08:00
< AppDialog class = "admin-density-dialog" v-model = "assignOpen" :title="assignmentTitle" width="min(920px, calc(100vw - 28px))" fullscreenable :confirm-loading="mutating" :confirm-disabled="Boolean(selectedCandidate?.blockReason || selectedCandidate?.locked)" :confirm-text="selectedCandidate?.unitType === 'SEALED_PALLET' ? '确认整托分配' : selectedCandidate?.coLoadGroupId ? '确认整组分配' : '确认加入'" @confirm="assign" >
< div v-if = "selectedCandidate" class="density-dialog-sections" >
< section class = "density-dialog-section" data -tone =" source " >
< table class = "density-table assignment-context-table" aria -label =" 全局装柜分配来源 " >< tbody >
< tr >< th > 目标柜次 </ th >< td >{{ displayCabinetNo ( selectedContainer ) }}</ td >< th > 集装箱号 </ th >< td >{{ displayActualContainerNo ( selectedContainer ) }}</ td ></ tr >
< tr >< th > 发货仓 </ th >< td >{{ selectedContainer ? . warehouseName || '—' }}</ td >< th > 柜次渠道 </ th >< td >{{ containerChannelGroup ( selectedContainer ) ? . name || '—' }}</ td ></ tr >
< tr >< th > 订单渠道 </ th >< td >{{ selectedCandidate . channelName || '—' }}</ td >< th >{{ selectedCandidate . coLoadGroupId ? '同柜组' : '客户' }}</ th >< td >{{ selectedCandidate . coLoadGroupId ? selectedCandidate . groupNo : selectedCandidate . customerName || '客户待补充' }}</ td ></ tr >
</ tbody ></ table >
</ section >
2026-09-04 15:54:36 +08:00
< el-alert v-if = "crossChannelAssignment" title="订单渠道与柜次渠道不同;这是人工装柜分配,确认后仍会加入。智能推荐不会跨渠道。" type="warning" :closable="false" />
2026-10-20 10:11:31 +08:00
< section class = "density-dialog-section" data -tone =" work " >
< el-form class = "density-form" >
< table class = "density-table density-table--editable assignment-context-table" aria -label =" 全局装柜分配填写 " >< tbody >
< tr v-if = "selectedCandidate.coLoadGroupId"><th>整组范围</th><td colspan="3" > 组内 {{ selectedCandidate.memberOrderCount }} 票 、 {{ selectedCandidate.inboundFragmentCount }} 个入仓片段 , 共 {{ selectedCandidate.groupTotalBoxes }} 箱 </ td ></ tr >
< tr v-else-if = "selectedCandidate.unitType === 'SEALED_PALLET'" >< th > 托盘号 </ th >< td >{{ selectedCandidate . palletNo || '—' }}</ td >< th > 整托箱数 </ th >< td > 整托 {{ selectedCandidate . availablePackageCount }} 箱 </ td ></ tr >
< template v-else >
< tr >< th > 入仓码 </ th >< td >{{ selectedCandidate . inboundCode || '—' }}</ td >< th > 可用箱数 </ th >< td >{{ selectedCandidate . availablePackageCount }} 箱 </ td ></ tr >
< tr >< th > 平均单箱毛重 </ th >< td >{{ formatMeasurement ( selectedCandidate . unitGrossWeightKg , 'kg' , 1 ) }}</ td >< th > 平均单箱体积 </ th >< td >{{ formatMeasurement ( selectedCandidate . averageUnitVolumeCbm , 'm³' ) }}</ td ></ tr >
< tr >< td colspan = "4" >< el-form-item label = "本次分配箱数" label -position =" left " label -width =" 128px " required >< el-input-number v-model = "assignForm.packageCount" :min="1" :max="selectedCandidate.availablePackageCount" :step="1" controls-position="right" placeholder="请输入本次分配箱数" /></ el-form-item ></ td ></ tr >
</ template >
</ tbody ></ table >
</ el-form >
</ section >
2026-08-29 11:41:57 +08:00
< template v-if = "selectedCandidate.coLoadGroupId" >
2026-10-20 10:11:31 +08:00
< section class = "density-dialog-section" data -tone =" source " >< table class = "density-table" aria -label =" 全局装柜同柜组成员 " >< thead >< tr >< th > 组内订单及入仓片段 </ th ></ tr ></ thead >< tbody >< tr v-for = "summary in selectedCandidate.memberOrderSummaries" :key="summary" >< td >{{ summary }}</ td ></ tr ></ tbody ></ table ></ section >
2026-08-30 15:49:01 +08:00
< el-alert : title = "selectedCandidate.blockReason || '同柜且不可拆:全部订单及其入仓片段按完整箱数一次加入,不能部分输入、单独分配或单独移除。'" : type = "selectedCandidate.blockReason ? 'error' : 'warning'" :closable = "false" />
2026-08-29 11:41:57 +08:00
</ template >
2026-09-08 17:24:49 +08:00
< template v-else-if = "selectedCandidate.unitType === 'SEALED_PALLET'" >
2026-10-20 10:11:31 +08:00
< section class = "density-dialog-section" data -tone =" source " >< h3 class = "density-dialog-section__title" > 托盘入仓组成 </ h3 >< SmartLoadingCandidateInbounds :candidate = "selectedCandidate" /></ section >
2026-09-08 17:24:49 +08:00
< el-alert title = "该封托托盘将作为一个不可拆分单元整托加入;装柜阶段复用此托盘,不会重新生成工作托盘。" type = "warning" :closable = "false" />
</ template >
2026-10-20 10:11:31 +08:00
< p v-else class = "density-dialog-note" > 本次只记录该入仓码的计划装柜箱数 , 具体箱号归属为未核验 。 </ p >
2026-08-10 17:42:17 +08:00
</ div >
</ AppDialog >
</ template >
</ section >
2026-08-14 19:02:34 +08:00
</ component >
2026-08-10 17:42:17 +08:00
</ template >
< style scoped >
2026-10-20 10:11:31 +08:00
. assignment - context - table { table - layout : fixed ; }
. assignment - context - table th { width : 120 px ; }
2026-09-03 13:49:40 +08:00
. global - loading - page { display : flex ; min - width : 0 ; min - height : 0 ; flex - direction : column ; gap : 12 px ; }
. embedded - smart - loading { display : flex ; width : 100 % ; min - width : 0 ; min - height : 0 ; flex : 1 ; overflow : hidden ; }
. embedded - smart - loading . global - loading - page { width : 100 % ; min - height : 0 ; flex : 1 ; overflow : hidden ; }
2026-09-03 11:47:32 +08:00
. channel - summary - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; min - width : 0 ; align - items : center ; border - bottom : 2 px solid var ( -- cc - border ); background : # fff ; }
. channel - tags { display : flex ; min - width : 0 ; align - items : center ; flex - wrap : nowrap ; gap : 6 px ; padding : 6 px 0 ; overflow - x : auto ; }
2026-10-20 10:11:31 +08:00
. channel - tags__label { flex : 0 0 auto ; margin - right : 2 px ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body ); font - weight : 600 ; }
2026-09-03 11:47:32 +08:00
. channel - tags : deep (. el - checkbox - button ) { flex : 0 0 auto ; }
2026-10-20 10:11:31 +08:00
. channel - tags : deep (. el - checkbox - button__inner ) { height : 34 px ; padding : 0 10 px ; border : 1 px solid var ( -- cc - border ) ! important ; border - radius : 6 px ! important ; box - shadow : none ! important ; font - size : var ( -- admin - font - body ); line - height : 32 px ; }
2026-09-03 11:47:32 +08:00
. compact - summary { display : flex ; min - height : 46 px ; align - items : center ; justify - content : flex - end ; gap : 12 px ; margin : 0 ; padding : 5 px 8 px ; white - space : nowrap ; }
2026-08-14 19:59:32 +08:00
. compact - summary > div { display : flex ; align - items : baseline ; gap : 4 px ; }
2026-10-20 10:11:31 +08:00
. compact - summary dt { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
. compact - summary dd { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - caption ); }
. compact - summary strong { color : var ( -- el - color - primary ); font - size : var ( -- admin - font - section ); }
2026-09-03 10:27:31 +08:00
: deep (. smart - loading - query . query - toolbar ) { padding : 0 ; border : 0 ; background : transparent ; }
2026-09-03 11:47:57 +08:00
: deep (. smart - loading - query . query - toolbar__filters ) { flex - wrap : nowrap ; gap : 8 px ; overflow - x : auto ; }
: deep (. smart - loading - query . query - toolbar__tail ) { flex - wrap : nowrap ; gap : 8 px ; }
: deep (. smart - loading - query . query - toolbar__actions ) { flex - wrap : nowrap ; gap : 6 px ; }
2026-10-20 10:11:31 +08:00
. pane - header small ,. container - option span ,. global - loading - page small { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-09-03 16:48:25 +08:00
. dispatch - grid { display : grid ; grid - template - columns : minmax ( 0 , 5 fr ) minmax ( 0 , 2 fr ) minmax ( 0 , 3 fr ); height : clamp ( 520 px , 68 vh , 720 px ); min - height : 0 ; overflow - x : auto ; border : 1 px solid var ( -- cc - border ); background : # fff ; }
2026-09-03 13:49:40 +08:00
. embedded - smart - loading . dispatch - grid { height : auto ; min - height : 0 ; flex : 1 ; }
2026-08-14 19:59:32 +08:00
. dispatch - pane { display : flex ; min - width : 0 ; min - height : 0 ; flex - direction : column ; }
2026-08-10 17:42:17 +08:00
. dispatch - pane + . dispatch - pane { border - left : 1 px solid var ( -- cc - border ); }
. pane - header { display : flex ; min - height : 58 px ; align - items : center ; justify - content : space - between ; gap : 12 px ; padding : 10 px 12 px ; border - bottom : 1 px solid var ( -- cc - border ); background : # fff ; }
. pane - header > div : first - child { display : grid ; min - width : 0 ; gap : 3 px ; }
. candidate - pane : deep (. data - table ),. plan - pane : deep (. data - table ) { min - height : 0 ; flex : 1 ; }
. candidate - pane : deep (. el - table ),. plan - pane : deep (. el - table ) { height : 100 % ; }
2026-09-03 13:49:40 +08:00
. candidate - pane : deep (. data - table__pagination ) { min - height : 46 px ; flex - wrap : nowrap ; gap : 8 px ; padding : 7 px 8 px ; }
. candidate - pane : deep (. data - table__pagination - left ) { flex : 0 0 auto ; white - space : nowrap ; }
. candidate - pane : deep (. data - table__pagination - controls ),. candidate - pane : deep (. data - table__page - size ),. candidate - pane : deep (. data - table__page - buttons ) { flex - wrap : nowrap ; gap : 4 px ; white - space : nowrap ; }
. candidate - pane : deep (. data - table__page - button [ aria - label ^= '第' ]),. candidate - pane : deep (. data - table__page - ellipsis ) { display : none ; }
. candidate - pane : deep (. data - table__page - size . el - select ) { width : 68 px ; }
2026-08-10 17:42:17 +08:00
. container - pane { background : # fafafa ; }
2026-08-14 19:59:32 +08:00
. container - list { display : grid ; min - height : 0 ; flex : 1 ; grid - auto - rows : max - content ; align - content : start ; gap : 8 px ; padding : 10 px ; overflow - y : auto ; }
2026-08-15 12:12:38 +08:00
. container - option { display : grid ; width : 100 % ; min - height : 112 px ; height : auto ; align - content : start ; gap : 6 px ; padding : 14 px ; overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 6 px ; background : # fff ; color : var ( -- cc - text ); line - height : 1.4 ; text - align : left ; cursor : pointer ; }
2026-08-10 17:42:17 +08:00
. container - option : hover ,. container - option : focus - visible { border - color : var ( -- el - color - primary ); outline : none ; }
2026-10-20 10:11:31 +08:00
. container - option . selected { border - color : var ( -- el - color - primary ); background : var ( -- el - color - primary - light - 9 ); }
2026-08-10 17:42:17 +08:00
. container - option__top { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; color : var ( -- cc - text ) ! important ; }
2026-10-20 10:11:31 +08:00
. container - option__identity { color : var ( -- cc - text ); font - size : var ( -- admin - font - body ); font - weight : 600 ; }
2026-08-15 12:12:38 +08:00
. container - option__hint { color : var ( -- el - color - warning ); font - weight : 600 ; }
2026-08-14 19:59:32 +08:00
. container - option span ,. container - option strong { min - width : 0 ; overflow - wrap : anywhere ; }
2026-08-10 17:42:17 +08:00
. plan - header { flex - wrap : wrap ; }
. plan - actions ,. row - actions ,. meter - title { display : flex ; align - items : center ; gap : 8 px ; }
. plan - actions ,. row - actions { flex - wrap : wrap ; }
. plan - actions : deep (. el - button + . el - button ),. row - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
. capacity - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px ; padding : 12 px ; border - bottom : 1 px solid var ( -- cc - border ); }
2026-08-15 17:04:00 +08:00
. preload - readiness - alert { border - radius : 0 ; border - right : 0 ; border - left : 0 ; }
2026-08-11 11:15:44 +08:00
. readonly - status - card { display : grid ; align - content : start ; justify - items : start ; gap : 10 px ; padding : 28 px 24 px ; }
2026-10-20 10:11:31 +08:00
. readonly - status - card strong { font - size : var ( -- admin - font - title ); }
2026-08-11 11:15:44 +08:00
. readonly - status - card p { max - width : 460 px ; margin : 0 ; color : var ( -- cc - muted ); line - height : 1.6 ; }
2026-10-20 10:11:31 +08:00
. meter - title { justify - content : space - between ; margin - bottom : 8 px ; font - size : var ( -- admin - font - caption ); }
2026-09-03 16:48:25 +08:00
. package - count - editor { display : grid ; min - width : 0 ; gap : 4 px ; }
2026-09-04 14:17:36 +08:00
. package - count - summary { display : grid ; gap : 3 px ; line - height : 1.35 ; }
. package - count - used { display : flex ; align - items : center ; justify - content : space - between ; gap : 6 px ; }
2026-10-20 10:11:31 +08:00
. package - count - summary strong { font - size : var ( -- admin - font - body ); font - weight : 600 ; }
. package - count - summary small { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-08-27 21:48:15 +08:00
. plan - pane : deep (. package - count - editor . el - input - number ) { width : 100 % ; min - width : 0 ; }
2026-09-04 10:12:41 +08:00
. candidate - group { display : grid ; width : 100 % ; min - width : 0 ; gap : 4 px ; }
. candidate - group : deep (. el - tag ),. candidate - group : deep (. el - tag__content ),. candidate - group : deep (. line - clamp - text ) { max - width : 100 % ; min - width : 0 ; }
. candidate - group : deep (. line - clamp - text ) { width : 100 % ; }
2026-08-29 11:41:57 +08:00
. candidate - block { color : var ( -- el - color - danger ) ! important ; }
2026-08-10 17:42:17 +08:00
@ media ( max - width : 1180 px ) {
2026-09-03 11:47:57 +08:00
: deep (. smart - loading - query . query - toolbar__filters ), : deep (. smart - loading - query . query - toolbar__tail ), : deep (. smart - loading - query . query - toolbar__actions ) { flex - wrap : wrap ; overflow : visible ; }
2026-08-14 19:59:32 +08:00
. dispatch - grid { grid - template - columns : 1 fr ; height : auto ; min - height : 0 ; overflow : visible ; }
2026-08-14 19:09:54 +08:00
. embedded - smart - loading . dispatch - grid { height : auto ; }
2026-08-10 17:42:17 +08:00
. dispatch - pane + . dispatch - pane { border - top : 1 px solid var ( -- cc - border ); border - left : 0 ; }
2026-08-14 19:59:32 +08:00
. candidate - pane ,. plan - pane { height : min ( 68 vh , 560 px ); min - height : 420 px ; }
2026-09-03 13:49:40 +08:00
. candidate - pane : deep (. data - table__pagination ) { flex - wrap : wrap ; }
2026-08-14 19:59:32 +08:00
. container - pane { max - height : min ( 58 vh , 460 px ); }
. container - list { grid - template - columns : repeat ( auto - fit , minmax ( 220 px , 1 fr )); max - height : none ; }
2026-08-10 17:42:17 +08:00
}
@ media ( max - width : 720 px ) {
2026-08-14 19:59:32 +08:00
. channel - summary - row { grid - template - columns : minmax ( 0 , 1 fr ); align - items : stretch ; }
2026-09-03 10:27:31 +08:00
. compact - summary { grid - column : 1 / - 1 ; }
2026-08-14 19:59:32 +08:00
. compact - summary { min - height : 0 ; justify - content : flex - start ; flex - wrap : wrap ; gap : 6 px 14 px ; padding : 8 px 12 px ; white - space : normal ; }
. capacity - grid { grid - template - columns : 1 fr ; }
2026-08-10 17:42:17 +08:00
. pane - header ,. plan - actions ,. meter - title { align - items : stretch ; flex - direction : column ; }
. plan - actions : deep (. el - button ) { width : 100 % ; min - height : 40 px ; margin - left : 0 ; }
. container - list { grid - template - columns : 1 fr ; }
2026-09-03 11:47:32 +08:00
. channel - tags { padding - inline : 8 px ; }
2026-08-10 17:42:17 +08:00
}
</ style >