2026-08-10 17:42:17 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
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,
|
|
|
|
|
|
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-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-08-31 10:48:23 +08:00
|
|
|
|
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; 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-08-30 15:49:01 +08:00
|
|
|
|
result.push({ ...row, coLoadGroup: null, 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,
|
|
|
|
|
|
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 },
|
|
|
|
|
|
{ key: 'unitType', label: '分配单位', width: 105, slotName: 'unitType' },
|
2026-09-03 14:12:42 +08:00
|
|
|
|
{ key: 'coLoadGroup', label: '同柜', minWidth: 80, 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-03 20:24:54 +08:00
|
|
|
|
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true },
|
2026-09-08 17:24:49 +08:00
|
|
|
|
{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 145, showOverflowTooltip: true },
|
2026-09-03 20:24:54 +08:00
|
|
|
|
{ key: 'coLoadGroup', label: '同柜', minWidth: 80, 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 } })
|
|
|
|
|
|
}
|
|
|
|
|
|
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} 箱吗?`,
|
|
|
|
|
|
row.coLoadGroup ? '移除同柜不可拆组' : sealedPallet ? '整托移除' : '移除装柜明细', {
|
|
|
|
|
|
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-08-24 18:07:38 +08:00
|
|
|
|
const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 个未拣入仓码吗?已拣入仓码会保留。`, '移除未拣明细', {
|
2026-08-17 16:45:46 +08:00
|
|
|
|
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-08-14 19:02:34 +08:00
|
|
|
|
<component :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-08-17 16:45:46 +08:00
|
|
|
|
v-if="canAssign && assignable"
|
2026-08-12 18:00:26 +08:00
|
|
|
|
size="small"
|
2026-09-04 10:51:31 +08:00
|
|
|
|
type="text"
|
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-08 17:24:49 +08:00
|
|
|
|
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroupId" class="candidate-group"><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="该组已锁定,不能重复分配" /></div><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 }"
|
|
|
|
|
|
@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-09-04 11:35:24 +08:00
|
|
|
|
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" text :disabled="mutating" @click="remove(row)"><Trash2 :size="15" />删除</el-button></div></template>
|
2026-09-03 21:25:56 +08:00
|
|
|
|
<template #customerName="{ row }"><LineClampText :content="row.customerName" /></template>
|
2026-09-08 17:24:49 +08:00
|
|
|
|
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroup" class="candidate-group"><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 ? '已锁定' : '未锁定'}`" /></div><LineClampText v-else :content="row.unitType === 'SEALED_PALLET' ? '封托不可拆' : '普通单票'" /></template>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<template #unitType="{ row }"><el-tag size="small" :type="row.unitType === 'SEALED_PALLET' ? 'success' : 'info'">{{ row.unitType === 'SEALED_PALLET' ? '封托托盘' : '箱件' }}</el-tag></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-09-08 17:24:49 +08:00
|
|
|
|
<AppDialog v-model="assignOpen" :title="selectedCandidate?.unitType === 'SEALED_PALLET' ? '整托加入当前柜次' : selectedCandidate?.coLoadGroupId ? '整组加入当前柜次' : '加入当前柜次'" :confirm-loading="mutating" :confirm-disabled="Boolean(selectedCandidate?.blockReason || selectedCandidate?.locked)" :confirm-text="selectedCandidate?.unitType === 'SEALED_PALLET' ? '确认整托分配' : selectedCandidate?.coLoadGroupId ? '确认整组分配' : '确认加入'" @confirm="assign">
|
2026-08-10 17:42:17 +08:00
|
|
|
|
<div v-if="selectedCandidate" class="assign-dialog">
|
2026-08-29 11:41:57 +08:00
|
|
|
|
<p><strong>{{ selectedCandidate.groupNo || selectedCandidate.customerName || '客户待补充' }}</strong></p>
|
2026-09-04 09:57:39 +08:00
|
|
|
|
<p>目标柜次:{{ displayCabinetNo(selectedContainer) }} · 集装箱号:{{ displayActualContainerNo(selectedContainer) }}</p>
|
2026-09-10 19:25:11 +08:00
|
|
|
|
<p>发货仓:{{ selectedContainer?.warehouseName || '—' }} · 柜次渠道:{{ containerChannelGroup(selectedContainer)?.name || '—' }}</p>
|
2026-09-04 10:51:31 +08:00
|
|
|
|
<p>订单渠道:{{ selectedCandidate.channelName || '—' }}</p>
|
2026-09-04 15:54:36 +08:00
|
|
|
|
<el-alert v-if="crossChannelAssignment" title="订单渠道与柜次渠道不同;这是人工装柜分配,确认后仍会加入。智能推荐不会跨渠道。" type="warning" :closable="false" />
|
2026-08-29 11:41:57 +08:00
|
|
|
|
<template v-if="selectedCandidate.coLoadGroupId">
|
2026-08-30 15:49:01 +08:00
|
|
|
|
<p>组内 {{ selectedCandidate.memberOrderCount }} 票、{{ selectedCandidate.inboundFragmentCount }} 个入仓片段,共 {{ selectedCandidate.groupTotalBoxes }} 箱</p>
|
|
|
|
|
|
<p v-for="summary in selectedCandidate.memberOrderSummaries" :key="summary">{{ summary }}</p>
|
|
|
|
|
|
<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-09-15 18:17:17 +08:00
|
|
|
|
<p>托盘号 {{ selectedCandidate.palletNo || '—' }} · 整托 {{ selectedCandidate.availablePackageCount }} 箱</p>
|
|
|
|
|
|
<SmartLoadingCandidateInbounds :candidate="selectedCandidate" />
|
2026-09-08 17:24:49 +08:00
|
|
|
|
<el-alert title="该封托托盘将作为一个不可拆分单元整托加入;装柜阶段复用此托盘,不会重新生成工作托盘。" type="warning" :closable="false" />
|
|
|
|
|
|
</template>
|
2026-08-29 11:41:57 +08:00
|
|
|
|
<template v-else>
|
2026-09-04 09:57:39 +08:00
|
|
|
|
<p>入仓码 {{ selectedCandidate.inboundCode || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱 · 平均单箱毛重 {{ formatMeasurement(selectedCandidate.unitGrossWeightKg, 'kg', 1) }} · 平均单箱体积 {{ formatMeasurement(selectedCandidate.averageUnitVolumeCbm, 'm³') }}</p>
|
2026-08-29 11:41:57 +08:00
|
|
|
|
<small>本次只记录该入仓码的计划装柜箱数,具体箱号归属为未核验。</small>
|
2026-09-03 19:29:38 +08:00
|
|
|
|
<el-form label-position="top"><el-form-item label="本次分配箱数" required><el-input-number v-model="assignForm.packageCount" :min="1" :max="selectedCandidate.availablePackageCount" :step="1" controls-position="right" placeholder="请输入本次分配箱数" /></el-form-item></el-form>
|
2026-08-29 11:41:57 +08:00
|
|
|
|
</template>
|
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-09-03 13:49:40 +08:00
|
|
|
|
.global-loading-page { display: flex; min-width: 0; min-height: 0; flex-direction: column; gap: 12px; }
|
|
|
|
|
|
.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, 1fr) auto; min-width: 0; align-items: center; border-bottom: 2px solid var(--cc-border); background: #fff; }
|
|
|
|
|
|
.channel-tags { display: flex; min-width: 0; align-items: center; flex-wrap: nowrap; gap: 6px; padding: 6px 0; overflow-x: auto; }
|
|
|
|
|
|
.channel-tags__label { flex: 0 0 auto; margin-right: 2px; color: var(--cc-text); font-size: 13px; font-weight: 600; }
|
|
|
|
|
|
.channel-tags :deep(.el-checkbox-button) { flex: 0 0 auto; }
|
|
|
|
|
|
.channel-tags :deep(.el-checkbox-button__inner) { height: 34px; padding: 0 10px; border: 1px solid var(--cc-border) !important; border-radius: 6px !important; box-shadow: none !important; font-size: 13px; line-height: 32px; }
|
|
|
|
|
|
.compact-summary { display: flex; min-height: 46px; align-items: center; justify-content: flex-end; gap: 12px; margin: 0; padding: 5px 8px; white-space: nowrap; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.compact-summary > div { display: flex; align-items: baseline; gap: 4px; }
|
|
|
|
|
|
.compact-summary dt { color: var(--cc-muted); font-size: 12px; }
|
|
|
|
|
|
.compact-summary dd { margin: 0; color: var(--cc-text); font-size: 12px; }
|
|
|
|
|
|
.compact-summary strong { color: var(--el-color-primary); font-size: 14px; }
|
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: 8px; overflow-x: auto; }
|
|
|
|
|
|
:deep(.smart-loading-query .query-toolbar__tail) { flex-wrap: nowrap; gap: 8px; }
|
|
|
|
|
|
:deep(.smart-loading-query .query-toolbar__actions) { flex-wrap: nowrap; gap: 6px; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.pane-header small,.container-option span,.global-loading-page small { color: var(--cc-muted); font-size: 12px; }
|
2026-09-03 16:48:25 +08:00
|
|
|
|
.dispatch-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 2fr) minmax(0, 3fr); height: clamp(520px, 68vh, 720px); min-height: 0; overflow-x: auto; border: 1px 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: 1px solid var(--cc-border); }
|
|
|
|
|
|
.pane-header { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--cc-border); background: #fff; }
|
|
|
|
|
|
.pane-header > div:first-child { display: grid; min-width: 0; gap: 3px; }
|
|
|
|
|
|
.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: 46px; flex-wrap: nowrap; gap: 8px; padding: 7px 8px; }
|
|
|
|
|
|
.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: 4px; 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: 68px; }
|
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: 8px; padding: 10px; overflow-y: auto; }
|
2026-08-15 12:12:38 +08:00
|
|
|
|
.container-option { display: grid; width: 100%; min-height: 112px; height: auto; align-content: start; gap: 6px; padding: 14px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; 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; }
|
|
|
|
|
|
.container-option.selected { border-color: var(--el-color-primary); box-shadow: inset 3px 0 0 var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
|
|
|
|
|
.container-option__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--cc-text) !important; }
|
2026-08-15 12:12:38 +08:00
|
|
|
|
.container-option__identity { color: var(--cc-text); font-size: 13px; font-weight: 600; }
|
|
|
|
|
|
.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: 8px; }
|
|
|
|
|
|
.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, 1fr)); gap: 12px; padding: 12px; border-bottom: 1px 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: 10px; padding: 28px 24px; }
|
|
|
|
|
|
.readonly-status-card strong { font-size: 16px; }
|
|
|
|
|
|
.readonly-status-card p { max-width: 460px; margin: 0; color: var(--cc-muted); line-height: 1.6; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.meter-title { justify-content: space-between; margin-bottom: 8px; font-size: 12px; }
|
2026-09-03 16:48:25 +08:00
|
|
|
|
.package-count-editor { display: grid; min-width: 0; gap: 4px; }
|
2026-09-04 14:17:36 +08:00
|
|
|
|
.package-count-summary { display: grid; gap: 3px; line-height: 1.35; }
|
|
|
|
|
|
.package-count-used { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
|
|
|
|
.package-count-summary strong { font-size: 13px; font-weight: 600; }
|
|
|
|
|
|
.package-count-summary small { color: var(--cc-muted); font-size: 12px; }
|
2026-08-27 21:48:15 +08:00
|
|
|
|
.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.assign-dialog p { margin: 0 0 10px; }
|
|
|
|
|
|
.assign-dialog :deep(.el-input-number) { width: 100%; }
|
2026-09-04 10:12:41 +08:00
|
|
|
|
.candidate-group { display: grid; width: 100%; min-width: 0; gap: 4px; }
|
|
|
|
|
|
.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: 1180px) {
|
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: 1fr; 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: 1px solid var(--cc-border); border-left: 0; }
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }
|
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(58vh, 460px); }
|
|
|
|
|
|
.container-list { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); max-height: none; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
2026-08-14 19:59:32 +08:00
|
|
|
|
.channel-summary-row { grid-template-columns: minmax(0, 1fr); 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: 6px 14px; padding: 8px 12px; white-space: normal; }
|
|
|
|
|
|
.capacity-grid { grid-template-columns: 1fr; }
|
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: 40px; margin-left: 0; }
|
|
|
|
|
|
.container-list { grid-template-columns: 1fr; }
|
2026-09-03 11:47:32 +08:00
|
|
|
|
.channel-tags { padding-inline: 8px; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|