2026-08-10 15:36:53 +08:00
< script setup lang = "ts" >
2026-10-20 10:11:31 +08:00
import InlineHelp from '@/shared/components/InlineHelp.vue'
2026-09-15 18:17:17 +08:00
import SmartLoadingCandidateInbounds from '../components/SmartLoadingCandidateInbounds.vue'
2026-10-20 10:11:31 +08:00
import { containerDialogTitle } from '../containerDialogTitle'
2026-09-14 18:34:34 +08:00
import '@/styles/adminDensity.css'
2026-08-10 15:36:53 +08:00
import { computed , onMounted , reactive , ref } from 'vue'
import { useRoute , useRouter } from 'vue-router'
2026-09-14 18:34:34 +08:00
import { ArrowLeft , ListChecks , RefreshCw , Sparkles } from '@lucide/vue'
2026-08-10 15:36:53 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
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 15:36:53 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import {
2026-09-08 17:24:49 +08:00
adjustSmartLoadingItem , assignSmartLoadingGroup , assignSmartLoadingItem , assignSmartLoadingPallet , getContainer ,
2026-09-01 20:07:40 +08:00
getSmartLoadingPlanByBooking , listSmartLoadingCandidates ,
2026-08-29 10:57:01 +08:00
recommendSmartLoading , removeSmartLoadingGroup , removeSmartLoadingItem ,
2026-08-10 15:36:53 +08:00
} from '../api/platformContainerLoading'
2026-09-16 11:06:36 +08:00
import type { ContainerBookingResource , SmartLoadingCandidate , SmartLoadingCandidateGroupMember , SmartLoadingPlan } 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-10 19:25:11 +08:00
import { getSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
2026-08-10 15:36:53 +08:00
const route = useRoute ()
const router = useRouter ()
const access = useAccessPreviewStore ()
const containerId = computed (() => String ( route . params . containerId || '' ))
const container = ref < ContainerBookingResource | null >( null )
const plan = ref < SmartLoadingPlan | null >( null )
const candidates = ref < SmartLoadingCandidate [] >([])
const loading = ref ( false )
const mutating = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const mutationError = ref < ApiErrorDetails | null >( null )
const noDraft = ref ( false )
2026-09-04 14:04:57 +08:00
const editingQuantityId = ref ( '' )
2026-08-10 15:36:53 +08:00
const assignOpen = ref ( false )
2026-08-30 15:49:01 +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 ()
return code ? ` ${ action } · ${ code } ` : action
})
2026-08-10 15:36:53 +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-08-10 15:36:53 +08:00
const canAssign = computed (() => access . can ( 'platform:firstleg:smart-loading:assign' ))
const canDelete = computed (() => access . can ( 'platform:firstleg:smart-loading:delete' ))
2026-08-11 11:15:44 +08:00
const canViewLoadingList = computed (() => access . can ( 'platform:firstleg:loading-list:list' ))
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-03 15:00:58 +08:00
type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds : string []; memberInboundCodes : string []; memberOrderSummaries : string []; memberOrderCount : number ; inboundFragmentCount : number }
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-08-29 10:57:01 +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 < { 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 10:57:01 +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 ),
}
})
2026-08-29 10:57:01 +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 10:57:01 +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 10:57:01 +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 ( '、' ),
skuCode : [... new Set ( rows . map (( row ) => row . skuCode ). filter ( Boolean ))]. join ( '、' ),
productNameZh : [... new Set ( rows . map (( row ) => row . productNameZh ). 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 10:57:01 +08:00
memberInboundCodes : rows . map (( row ) => row . inboundCode || '入仓码暂缺' ),
2026-08-30 15:49:01 +08:00
memberOrderSummaries : memberOrderSummaries ( rows ),
2026-08-29 10:57:01 +08:00
})
}
return result
})
2026-08-10 15:36:53 +08:00
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-03 21:50:28 +08:00
const volumeLimit = computed (() => Number ( container . value ? . capacityCbm || 0 ) * 1.5 )
const weightLimit = computed (() => Number ( container . value ? . maxWeightKg || 0 ) * 1.5 )
2026-08-10 15:36:53 +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-08-10 15:36:53 +08:00
const itemColumns : DataTableColumn [] = [
2026-09-03 16:48:25 +08:00
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' },
2026-08-10 15:36:53 +08:00
{ key : 'sequence' , prop : 'loadSequence' , label : '顺序' , width : 75 },
2026-08-14 19:59:32 +08:00
{ key : 'order' , label : '入仓码' , minWidth : 190 , slotName : 'order' },
2026-09-08 17:24:49 +08:00
{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 150 , 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 : 150 , slotName : 'customerName' },
2026-08-10 15:36:53 +08:00
{ key : 'goods' , label : '货物' , minWidth : 190 , slotName : 'goods' },
{ key : 'packageCount' , label : '分配箱数' , width : 150 , slotName : 'packageCount' },
{ key : 'volumeCbm' , label : '体积' , width : 120 , slotName : 'volume' },
{ key : 'grossWeightKg' , label : '毛重' , width : 130 , slotName : 'weight' },
]
const candidateColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 110 , 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 : 150 , 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 : 150 , slotName : 'customerName' },
2026-09-04 15:54:36 +08:00
{ key : 'goodsSummary' , prop : 'goodsSummary' , label : '货物摘要' , minWidth : 180 , slotName : 'goodsSummary' },
2026-09-08 17:24:49 +08:00
{ key : 'availablePackageCount' , prop : 'availablePackageCount' , label : '箱数' , width : 90 },
2026-08-21 17:07:37 +08:00
{ key : 'cartonIdentityStatus' , label : '箱号归属' , width : 110 , slotName : 'cartonIdentityStatus' },
2026-09-08 17:24:49 +08:00
{ key : 'averageUnitVolumeCbm' , label : '单位体积' , width : 120 , slotName : 'averageUnitVolume' },
{ key : 'unitGrossWeightKg' , label : '单位毛重' , width : 120 , slotName : 'unitGrossWeight' },
2026-08-10 15:36:53 +08:00
]
2026-08-11 20:02:38 +08:00
function formatNumber ( value : number , digits = 3 ) { return Number . isFinite ( value ) ? value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits }) : '0' }
function percentage ( value : number ) { return Number . isFinite ( value ) ? Number ( Math . max ( value , 0 ). toFixed ( 2 )) : 0 }
2026-08-29 10:57:01 +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-09-04 14:04:57 +08:00
function adjustmentMax ( source : object ) {
const item = itemRow ( source )
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-10 15:36:53 +08:00
function explainCandidate ( source : object ) {
const candidate = candidateRow ( source )
2026-08-21 17:07:37 +08:00
const volume = Number ( candidate . averageUnitVolumeCbm || 0 )
2026-08-21 16:28:59 +08:00
const weight = Number ( candidate . unitGrossWeightKg || 0 )
2026-08-10 15:36:53 +08:00
const volumeRoom = volumeLimit . value ? Math . floor ( Math . max ( volumeLimit . value - totalVolume . value , 0 ) / Math . max ( volume , 0.000001 )) : candidate . availablePackageCount
const weightRoom = weightLimit . value ? Math . floor ( Math . max ( weightLimit . value - totalWeight . value , 0 ) / Math . max ( weight , 0.000001 )) : candidate . availablePackageCount
2026-09-03 21:50:28 +08:00
return `按 150% 上限,当前最多可装 ${ Math . max ( Math . min ( candidate . availablePackageCount , volumeRoom , weightRoom ), 0 ) } 箱`
2026-08-10 15:36:53 +08:00
}
2026-08-15 17:04:00 +08:00
function displayCabinetNo ( value : ContainerBookingResource | null | undefined ) {
if ( ! value ) return '待生成'
const cabinetNo = String ( value . cabinetNo || '' ). trim ()
return /^\d{15,}$/ . test ( cabinetNo ) ? '待生成' : cabinetNo || '待生成'
}
2026-08-14 19:09:54 +08:00
function displayContainerNo ( value : ContainerBookingResource | null | undefined ) {
2026-08-15 17:04:00 +08:00
const containerNo = String ( value ? . containerNo || '' ). trim ()
2026-09-01 15:24:37 +08:00
return containerNo && ! /^(?:MAN|SPLIT)-/i . test ( containerNo ) ? containerNo : '待补录'
2026-08-14 19:09:54 +08:00
}
2026-08-17 16:45:46 +08:00
function syncDrafts ( 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 displayedPlanItems . value ) quantityDrafts [ item . id ] = preserved [ item . id ] ?? item . packageCount
2026-08-17 16:45:46 +08:00
}
function acceptPlan ( next : SmartLoadingPlan , preserved : Record < string , number > = {}) { plan . value = next ; noDraft . value = false ; syncDrafts ( preserved ) }
2026-08-31 10:48:23 +08:00
async function acceptPlanWithGroups ( next : SmartLoadingPlan , preserved : Record < string , number > = {}) {
acceptPlan ( next , preserved )
planGroupsByInboundId . value = await loadSmartLoadingPlanCoLoadGroups ( next )
syncDrafts ( preserved )
2026-08-29 10:57:01 +08:00
}
2026-08-10 15:36:53 +08:00
async function load () {
loading . value = true ; loadError . value = null ; noDraft . value = false
2026-08-29 10:57:01 +08:00
planGroupsByInboundId . value = new Map ()
2026-08-10 15:36:53 +08:00
try {
container . value = await getContainer ( containerId . value )
2026-08-17 16:45:46 +08:00
if ( container . value . planStatus === 'COMPLETED' ) {
2026-08-11 11:15:44 +08:00
plan . value = null
candidates . value = []
return
}
2026-08-31 10:48:23 +08:00
try { await acceptPlanWithGroups ( await getSmartLoadingPlanByBooking ( containerId . value )) }
2026-08-10 15:36:53 +08:00
catch ( error ) {
const detail = getApiErrorDetails ( error )
if ( detail . code === 'A0404' ) { plan . value = null ; noDraft . value = true }
else throw error
}
2026-09-10 19:25:11 +08:00
const channelGroup = getSmartLoadingChannelGroup ( container . value . channelGroupCode )
2026-09-03 15:00:58 +08:00
candidates . value = plan . value ? . canAssign && channelGroup
? ( await listSmartLoadingCandidates ({ channelGroupCodes : [ channelGroup . code ], warehouseId : container . value . warehouseId , page : 1 , size : 200 })). records
2026-08-17 16:45:46 +08:00
: []
2026-09-04 15:54:36 +08:00
} catch ( error ) { loadError . value = getApiErrorDetails ( error , '智能装柜工作台加载失败' ) }
2026-08-10 15:36:53 +08:00
finally { loading . value = false }
}
2026-08-11 11:15:44 +08:00
function openOperationalList () {
if ( ! container . value || container . value . planStatus === 'EDITING' ) return
void router . push ({
2026-09-08 19:57:42 +08:00
name : container . value . planStatus === 'LOADING' ? 'loading-plans' : 'domestic-container-detail' ,
params : container . value . planStatus === 'COMPLETED' ? { containerId : container . value . id } : undefined ,
query : container . value . planStatus === 'COMPLETED' ? { tab : 'loading-list' } : { planId : container . value . planId || undefined , warehouseId : container . value . warehouseId },
2026-08-11 11:15:44 +08:00
})
}
2026-08-29 10:57:01 +08:00
function openAssign ( source : object ) {
const candidate = candidateRow ( source )
if ( candidateBlocked ( candidate )) return
selectedCandidate . value = candidate
assignForm . packageCount = candidate . coLoadGroupId ? Number ( candidate . groupTotalBoxes || candidate . availablePackageCount ) : Math . min ( candidate . availablePackageCount , 1 )
assignOpen . value = true
}
2026-08-10 15:36:53 +08:00
async function assign () {
2026-09-03 21:25:56 +08:00
const currentPlan = plan . value
const selected = selectedCandidate . value
const inboundId = selected ? . inboundId
2026-09-08 17:24:49 +08:00
if ( ! currentPlan || ! selected || ! inboundId ) return
if ( selected . unitType !== 'SEALED_PALLET' && assignForm . packageCount < 1 ) return
2026-09-03 21:25:56 +08:00
if ( selected . coLoadGroupId && ( selected . blockReason || selected . locked )) return
2026-08-10 15:36:53 +08:00
mutating . value = true
mutationError . value = null
try {
2026-09-08 17:24:49 +08:00
const next = selected . unitType === 'SEALED_PALLET' && selected . palletId
? await assignSmartLoadingPallet ( currentPlan . id , { palletId : selected . palletId , expectedVersion : currentPlan . version })
: selected . coLoadGroupId
2026-09-03 21:25:56 +08:00
? await assignSmartLoadingGroup ( currentPlan . id , inboundId , currentPlan . version )
: await assignSmartLoadingItem ( currentPlan . id , { inboundId , packageCount : assignForm . packageCount , expectedVersion : currentPlan . version })
2026-08-31 10:48:23 +08:00
await acceptPlanWithGroups ( next )
2026-08-29 10:57:01 +08:00
assignOpen . value = false
ElMessage . success ( selected . coLoadGroupId ? `同柜不可拆组 ${ selected . groupNo } 已整组分配` : '货物已分配' )
2026-08-11 11:15:44 +08:00
} catch ( error ) { await handleMutationError ( error , '分配失败' ) }
2026-08-10 15:36:53 +08:00
finally { mutating . value = false }
}
2026-08-29 10:57:01 +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-08-29 10:57:01 +08:00
return Boolean ( candidate . blockReason ) || candidate . locked === true
2026-09-03 21:25:56 +08:00
|| ! candidate . inboundId
2026-09-03 15:00:58 +08:00
|| candidate . memberInboundIds . some (( inboundId ) => assignedInboundIds . value . has ( inboundId ))
2026-08-29 10:57:01 +08:00
|| candidate . availablePackageCount < 1
}
2026-08-10 15:36:53 +08:00
async function recommend () {
if ( ! plan . value ) return
mutating . value = true
mutationError . value = null
2026-08-22 10:02:13 +08:00
try {
try {
2026-08-31 10:48:23 +08:00
await acceptPlanWithGroups ( await recommendSmartLoading ( plan . value . id , plan . value . version ))
2026-08-22 10:02:13 +08:00
} catch ( error ) {
const details = getApiErrorDetails ( error , '智能推荐失败' )
if ( details . code !== 'A0409' || ! /(版本|并发|已被修改|已变化)/ . test ( details . message )) throw error
await load ()
if ( ! plan . value ) throw error
2026-08-31 10:48:23 +08:00
await acceptPlanWithGroups ( await recommendSmartLoading ( plan . value . id , plan . value . version ))
2026-08-22 10:02:13 +08:00
}
2026-09-01 20:07:40 +08:00
ElMessage . success ( '智能建议已生成,请人工核对明细' )
2026-08-22 10:02:13 +08:00
}
2026-08-11 11:15:44 +08:00
catch ( error ) { await handleMutationError ( error , '智能推荐失败' ) }
2026-08-10 15:36:53 +08:00
finally { mutating . value = false }
}
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 15:36:53 +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-10 15:36:53 +08:00
mutating . value = true
mutationError . value = null
2026-09-04 14:04:57 +08:00
try { await acceptPlanWithGroups ( await adjustSmartLoadingItem ( plan . value . id , item . id , count , plan . value . version , item . version )); editingQuantityId . value = '' ; ElMessage . success ( '箱数已调整' ) }
2026-08-11 11:15:44 +08:00
catch ( error ) { await handleMutationError ( error , '调整失败' ) }
2026-08-10 15:36:53 +08:00
finally { mutating . value = false }
}
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 10:57:01 +08:00
const row = source as SmartLoadingPlanRow
const message = row . coLoadGroup
2026-08-30 15:49:01 +08:00
? `确认整组移除 ${ row . coLoadGroup . groupNo } ( ${ groupMemberLabel ( row . coLoadGroup ) } 、 ${ row . packageCount } 箱)吗?组内所有入仓片段将联动移除。`
2026-08-29 10:57:01 +08:00
: `确认从计划移除入仓码“ ${ item . inboundCode || '—' } ”的 ${ item . packageCount } 箱吗?`
2026-10-20 10:11:31 +08:00
await ElMessageBox . confirm ( message , containerDialogTitle ( row . coLoadGroup ? '移除同柜不可拆组' : '移除装柜明细' , container . value ), { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : row . coLoadGroup ? '移除整组' : '移除' , cancelButtonText : '取消' }). catch (() => false ). then ( async ( confirmed ) => {
2026-08-10 15:36:53 +08:00
if ( ! confirmed || ! plan . value ) return
mutating . value = true
mutationError . value = null
2026-08-24 18:07:38 +08:00
try {
2026-08-29 10:57:01 +08:00
if ( row . coLoadGroup ) {
2026-08-31 10:48:23 +08:00
await acceptPlanWithGroups ( await removeSmartLoadingGroup ( plan . value ! . id , row . coLoadGroup . groupId , plan . value ! . version ))
2026-08-29 10:57:01 +08:00
await load ()
ElMessage . success ( `同柜不可拆组 ${ row . coLoadGroup . groupNo } 已整组移除` )
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 )
2026-08-31 10:48:23 +08:00
await acceptPlanWithGroups ( current )
2026-08-24 18:07:38 +08:00
}
await load ()
ElMessage . success ( '该入仓码已从装柜方案移除' )
}
2026-08-11 11:59:39 +08:00
catch ( error ) { await handleMutationError ( error , '移除失败' , true ) }
2026-08-10 15:36:53 +08:00
finally { mutating . value = false }
})
}
2026-08-11 11:59:39 +08:00
async function handleMutationError ( error : unknown , fallback : string , pickingConflict = false ) {
2026-08-10 15:36:53 +08:00
const details = getApiErrorDetails ( error , fallback )
2026-08-11 11:15:44 +08:00
if ( details . code === 'A0409' ) {
const preserved = { ... quantityDrafts }
await load ()
2026-08-17 16:45:46 +08:00
syncDrafts ( preserved )
2026-08-11 11:15:44 +08:00
}
2026-08-11 11:59:39 +08:00
const pickingMessage = pickingConflict && details . code === 'A0409' && details . message . includes ( '拣货' )
2026-08-11 11:52:30 +08:00
? '该订单在拣货中不许删除'
2026-08-11 11:59:39 +08:00
: null
2026-08-22 10:02:13 +08:00
mutationError . value = { ... details , message : pickingMessage || details . message || ( details . code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details . message ) }
2026-08-10 15:36:53 +08:00
ElMessage . error ( mutationError . value . message )
}
onMounted ( load )
</ script >
< template >
2026-09-14 18:34:34 +08:00
< AdminPage class = "admin-density" >
2026-08-10 15:36:53 +08:00
< section class = "business-page workbench" >
2026-09-04 15:54:36 +08:00
< section class = "surface-panel workbench-command-bar" aria -label =" 装柜作业摘要 " >
2026-08-15 17:04:00 +08:00
< div class = "workbench-identities" >
< div class = "workbench-identity" >
< span > 柜次号 </ span >
< strong >{{ loading ? '正在加载' : displayCabinetNo ( container ) }}</ strong >
</ div >
< div class = "workbench-identity" >
< span > 集装箱号 </ span >
< strong >{{ loading ? '正在加载' : displayContainerNo ( container ) }}</ strong >
</ div >
2026-08-10 16:24:54 +08:00
</ div >
< div class = "header-actions" >
2026-09-14 18:34:34 +08:00
< el-button link class = "density-back" aria -label =" 返回货柜列表 " @click ="router.back()" >< ArrowLeft :size = "18" /> 返回 </ el-button >
2026-09-04 15:54:36 +08:00
< el-tooltip content = "刷新" placement = "top" >< el-button :loading = "loading" aria -label =" 刷新智能装柜 " @click ="load" >< 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 >
2026-08-10 16:24:54 +08:00
</ div >
</ section >
2026-09-04 15:54:36 +08:00
< LoadErrorState v-if = "loadError" title="智能装柜加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
2026-08-10 15:36:53 +08:00
< template v-else >
2026-09-04 15:54:36 +08:00
< LoadErrorState v-if = "mutationError" title="装柜操作失败" :message="mutationError.message" compact retry-text="刷新最新计划" @retry="load"><template #details><ApiErrorMeta :details="mutationError" /></ template ></ LoadErrorState >
2026-08-11 11:59:39 +08:00
< section v-if = "container?.planStatus === 'COMPLETED'" class="readonly-status-card" aria-label="装柜完成状态" >
< el-tag type = "success" > 装柜完成 </ el-tag >
< strong > 该柜次已完成装柜 </ strong >
< p > 智能装柜方案已锁定 , 不能继续调整 。 请进入装柜清单查看完整记录 。 </ p >
< el-button v-if = "canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" /> 查看装柜清单 </ el-button >
2026-08-11 11:15:44 +08:00
</ section >
< template v-else >
2026-08-14 19:09:54 +08:00
< section v-if = "container" class="summary-band" >< div >< span > 柜型 </ span >< strong >{{ container . containerType }}</ strong ></ div >< div >< span > 装柜仓库 </ span >< strong >{{ container . warehouseName || '—' }}</ strong ></ div ></ section >
2026-09-03 21:50:28 +08:00
< section v-if = "container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 150% 上限)</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>重量占用(含 150% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></ div ></ section >
2026-09-01 20:07:40 +08:00
< el-empty v-if = "noDraft" description="该柜次缺少自动生成的预装计划,请刷新后重试" />
2026-08-10 15:36:53 +08:00
< template v-else-if = "plan" >
2026-08-17 16:45:46 +08:00
< el-alert v-if = "plan.status === 'LOADING' && plan.canEdit" type="warning" :closable="false" show-icon title="已下发 PDA,仍可维护未完成的装柜明细" description="新增、增加或移除未拣明细会刷新 PDA 明细与版本;PDA 端需要刷新后继续操作。" />
< el-alert v-else-if = "!plan.canEdit" :type="plan.finalCompletionStatus === 'COMPLETED' ? 'success' : 'info'" :closable="false" show-icon :title="plan.finalCompletionStatus === 'IN_PROGRESS' ? '正在最终确认,装柜方案暂时只读' : plan.finalCompletionStatus === 'COMPLETED' ? '装柜已完成,方案永久只读' : '当前装柜方案只读'" :description="plan.blockedReason || '请刷新计划获取最新作业状态。'" />
2026-08-24 18:07:38 +08:00
< DataTable :data = "displayedPlanItems" :columns = "itemColumns" : loading = "loading || mutating" stripe preference -key =" smart -loading-items " column -settings-mode =" hide " empty -text =" 暂无已分配货物 " >
2026-10-20 10:11:31 +08:00
< template # toolbar -start >< div >< h2 >{{ displayContainerNo ( container ) }} 装柜方案 < InlineHelp title = "装柜说明" >< p > 智能建议必须经人工核对 。 </ p >< p > 系统基于装柜仓库 、 物流渠道 、 可用箱数及 150 % 容量上限校验 。 </ p ></ InlineHelp ></ h2 ></ div ></ template >
2026-09-14 18:34:34 +08:00
< template # actions = "{ row }" >< div class = "row-actions density-actions" >< el-button data -tone =" danger " class = "density-action" v-if = "canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)" > 删除 </ el -button ></ div ></ template >
2026-09-16 11:06:36 +08:00
< template # order = "{ 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} 箱`" /></details><LineClampText v-else :content="row.unitType === 'SEALED_PALLET' ? '整托加入 · 不可拆' : '按箱加入'" /></ template >
2026-09-04 15:54:36 +08:00
< template # goods = "{ row }" >< div class = "goods-cell" >< LineClampText : content = "row.productNameZh || '未命名货物'" />< LineClampText class = "goods-cell__sku" : content = "row.skuCode || '无 SKU'" /></ div ></ template >
2026-09-14 18:34:34 +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" class="density-action" :disabled="mutating" aria-label="调整箱数" @click="startQuantityEdit(row)">调整</el-button></div><small v-if="(row.remainingPackageCount ?? 0) > 0">未用:{{ row.remainingPackageCount }}</small></div><LineClampText v-if="row.detailCount > 1 && editable" :content="`含 ${row.detailCount} 个规格,需删除后重加`" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></ div ></ template >
2026-08-10 15:36:53 +08:00
< template # volume = "{ row }" >{{ formatNumber ( Number ( row . volumeCbm || 0 )) }} m³ </ template >< template # weight = "{ row }" >{{ formatNumber ( Number ( row . grossWeightKg || 0 ), 1 ) }} kg </ template >
</ DataTable >
2026-09-04 15:54:36 +08:00
< DataTable :data = "displayedCandidates" :columns = "candidateColumns" : loading = "loading || mutating" stripe preference -key =" smart -loading-candidates " column -settings-mode =" hide " empty -text =" 当前仓库暂无可装柜货物 " >
2026-10-20 10:11:31 +08:00
< template # toolbar -start >< div >< h2 > 候选货物 </ h2 ></ div ></ template >
2026-09-15 18:17:17 +08:00
< template # actions = "{ row }" >< el-button data -tone =" primary " class = "density-action" v-if = "canAssign && assignable" size="small" type="primary" plain :disabled="candidateBlocked(row as SmartLoadingCandidateRow)" @click="openAssign(row)" > 加入 </ el -button ></ template >
< 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-04 15:54:36 +08:00
< template # goodsSummary = "{ row }" >< LineClampText : content = "row.goodsSummary || '—'" /></ 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-08-21 17:07:37 +08:00
< template # averageUnitVolume = "{ row }" >{{ formatNumber ( Number ( row . averageUnitVolumeCbm || 0 )) }} m³ </ template >< template # unitGrossWeight = "{ row }" >{{ formatNumber ( Number ( row . unitGrossWeightKg || 0 ), 1 ) }} kg </ template >
< template # cartonIdentityStatus >< el-tag size = "small" type = "info" effect = "plain" > 未核验 </ el-tag ></ template >
2026-08-10 15:36:53 +08:00
</ DataTable >
</ template >
2026-08-11 11:15:44 +08:00
</ template >
2026-08-10 15:36:53 +08:00
</ template >
2026-10-20 10:11:31 +08:00
< AppDialog class = "admin-density-dialog" v-model = "assignOpen" :title="assignmentTitle" width="min(780px, calc(100vw - 28px))" :fullscreenable="Boolean(selectedCandidate?.coLoadGroupId || selectedCandidate?.unitType === 'SEALED_PALLET')" :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 =" work " >
< el-form class = "density-form" >
< table class = "density-table density-table--editable assignment-table" aria -label =" 加入当前柜次 " >< tbody >
< tr >< th > 来源编号 </ th >< td >{{ selectedCandidate . palletNo || selectedCandidate . groupNo || selectedCandidate . inboundCode || '入仓码暂缺' }}</ td >< th > 当前柜次 </ th >< td >{{ container ? . cabinetNo || displayContainerNo ( container ) }}</ td ></ tr >
< 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 colspan="3" > 整托 {{ selectedCandidate.availablePackageCount }} 箱 </ td ></ tr >
< template v-else >
< tr >< th > 货物摘要 </ th >< td colspan = "3" >< LineClampText : content = "selectedCandidate.goodsSummary || '货物摘要暂缺'" /></ td ></ tr >
< tr >< th > 可用箱数 </ th >< td >{{ selectedCandidate . availablePackageCount }} 箱 </ td >< td colspan = "2" >< el-form-item label = "本次计划装柜箱数" label -position =" left " label -width =" 140px " 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 >
< template v-if = "selectedCandidate.coLoadGroupId" >
< 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 >
< el-alert : title = "selectedCandidate.blockReason || '同柜且不可拆:全部订单及其入仓片段按完整箱数一次加入,不能部分输入或单独移除。'" : type = "selectedCandidate.blockReason ? 'error' : 'warning'" :closable = "false" />
</ template >
< template v-else-if = "selectedCandidate.unitType === 'SEALED_PALLET'" >
< section class = "density-dialog-section" data -tone =" source " >< h3 class = "density-dialog-section__title" > 托盘入仓组成 </ h3 >< SmartLoadingCandidateInbounds :candidate = "selectedCandidate" /></ section >
< el-alert title = "该封托托盘将作为一个不可拆分单元整托加入;装柜阶段复用此托盘,不会重新生成工作托盘。" type = "warning" :closable = "false" />
</ template >
< template v-else >
< el-alert :title = "explainCandidate(selectedCandidate)" type = "info" :closable = "false" />
< p class = "density-dialog-note" > 只记录数量 , 具体上柜箱号未核验 。 </ p >
</ template >
</ div >
</ AppDialog >
2026-08-10 15:36:53 +08:00
</ section >
</ AdminPage >
</ template >
< style scoped >
2026-09-14 18:34:34 +08:00
. workbench { display : grid ; gap : var ( -- admin - space - 5 ); min - width : 0 ; }
2026-08-10 16:24:54 +08:00
. workbench - command - bar ,. header - actions ,. row - actions ,. meter - title { display : flex ; align - items : center ; }
2026-09-14 18:34:34 +08:00
. workbench - command - bar { justify - content : space - between ; gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 5 ) var ( -- admin - space - 5 ); }
. workbench - identities { display : flex ; min - width : 0 ; align - items : center ; gap : var ( -- admin - space - 7 ); }
2026-08-10 16:24:54 +08:00
. workbench - identity { display : grid ; min - width : 0 ; gap : 3 px ; }
2026-09-14 18:34:34 +08:00
. workbench - identity span { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-09-14 20:03:27 +08:00
. workbench - identity strong { font - size : var ( -- admin - font - title ); overflow - wrap : anywhere ; }
2026-08-10 16:24:54 +08:00
. workbench h2 ,. workbench h2 + p { margin : 0 ; }
2026-09-14 20:03:27 +08:00
. workbench h2 { font - size : var ( -- admin - font - section ); }
2026-09-14 18:34:34 +08:00
. workbench h2 + p ,. workbench small { display : block ; margin - top : var ( -- admin - space - 1 ); color : var ( -- cc - muted ); }
. header - actions ,. row - actions { flex - wrap : wrap ; gap : var ( -- admin - space - 3 ); }
2026-08-10 15:36:53 +08:00
. header - actions : deep (. el - button + . el - button ),. row - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
2026-08-14 19:59:32 +08:00
. summary - band { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 1 px ; border : 1 px solid var ( -- cc - border ); background : var ( -- cc - border ); }
2026-09-14 18:34:34 +08:00
. summary - band > div { display : grid ; gap : 5 px ; padding : var ( -- admin - space - 5 ); background : var ( -- admin - surface ); }
. summary - band span { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
. capacity - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 5 ); border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius ); background : var ( -- admin - surface ); }
. readonly - status - card { display : grid ; justify - items : start ; gap : var ( -- admin - space - 4 ); padding : var ( -- admin - space - 6 ); border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius ); background : var ( -- admin - surface ); }
. readonly - status - card strong { font - size : var ( -- admin - font - section ); }
2026-08-11 11:15:44 +08:00
. readonly - status - card p { margin : 0 ; color : var ( -- cc - muted ); }
2026-09-14 18:34:34 +08:00
. meter - title { justify - content : space - between ; gap : var ( -- admin - space - 5 ); margin - bottom : var ( -- admin - space - 4 ); font - size : var ( -- admin - font - body ); }
2026-08-10 15:36:53 +08:00
. row - actions { min - width : 150 px ; }
2026-09-04 15:54:36 +08:00
. goods - cell { display : grid ; min - width : 0 ; gap : 2 px ; }
. goods - cell : deep (. line - clamp - text ) { width : 100 % ; font - weight : 600 ; }
2026-09-14 18:34:34 +08:00
. goods - cell : deep (. goods - cell__sku . line - clamp - text ) { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); font - weight : 400 ; }
. package - count - editor { display : grid ; min - width : 0 ; gap : var ( -- admin - space - 1 ); }
2026-09-04 14:17:36 +08:00
. package - count - summary { display : grid ; gap : 3 px ; line - height : 1.35 ; }
2026-09-14 18:34:34 +08:00
. package - count - used { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 2 ); }
. 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
. workbench : deep (. package - count - editor . el - input - number ) { width : 100 % ; min - width : 0 ; }
2026-10-20 10:11:31 +08:00
. assignment - table { table - layout : fixed ; }
. assignment - table th { width : 90 px ; }
2026-09-04 10:12:41 +08:00
. candidate - group { display : grid ; width : 100 % ; min - width : 0 ; gap : 3 px ; }
. candidate - group : deep (. el - tag ),. candidate - group : deep (. el - tag__content ),. candidate - group : deep (. line - clamp - text ) { max - width : 100 % ; min - width : 0 ; }
2026-09-14 18:34:34 +08:00
. candidate - group : deep (. line - clamp - text ) { width : 100 % ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-08-29 10:57:01 +08:00
. candidate - group . candidate - block { color : var ( -- el - color - danger ); }
2026-08-15 17:04:00 +08:00
@ media ( max - width : 760 px ) { . workbench - command - bar ,. header - actions { width : 100 % ; flex - direction : column ; align - items : stretch ; } . workbench - identities { flex - direction : column ; align - items : stretch ; gap : 10 px ; } . summary - band ,. capacity - grid { grid - template - columns : 1 fr ; } . meter - title { align - items : flex - start ; flex - direction : column ; } . header - actions . el - button { min - height : 44 px ; margin - left : 0 ; } }
2026-08-10 15:36:53 +08:00
</ style >