Merge remote-tracking branch 'origin/release/test' into dev
# Conflicts: # src/modules/container-loading/pages/GlobalSmartLoadingView.vue # src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue
This commit is contained in:
@@ -4,19 +4,21 @@ import { Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
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'
|
||||
import LineClampText from '@/shared/components/LineClampText.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
adjustSmartLoadingItem,
|
||||
assignPendingReviewSmartLoadingOrder,
|
||||
assignSmartLoadingGroup,
|
||||
assignSmartLoadingItem,
|
||||
getSmartLoadingPlanByBooking,
|
||||
listContainers,
|
||||
listSmartLoadingCandidates,
|
||||
listSmartLoadingWorkbenchContainers,
|
||||
recommendSmartLoading,
|
||||
@@ -24,7 +26,6 @@ import {
|
||||
removeSmartLoadingItem,
|
||||
} from '../api/platformContainerLoading'
|
||||
import type {
|
||||
ContainerBookingResource,
|
||||
SmartLoadingCandidate,
|
||||
SmartLoadingPlan,
|
||||
SmartLoadingWorkbenchContainer,
|
||||
@@ -40,7 +41,7 @@ const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: fa
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const containers = ref<SmartLoadingWorkbenchContainer[]>([])
|
||||
const containerCatalog = ref<ContainerBookingResource[]>([])
|
||||
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const customersLoading = ref(false)
|
||||
const selectedContainerId = ref('')
|
||||
@@ -61,8 +62,8 @@ const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
const query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
const query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value, orderStatuses: [] })
|
||||
const candidatePage = ref(1)
|
||||
const candidatePageSize = ref(20)
|
||||
const candidateTotal = ref(0)
|
||||
@@ -73,8 +74,8 @@ let containerRequest = 0
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const selectedContainer = computed(() => containers.value.find((item) => item.bookingId === selectedContainerId.value) || null)
|
||||
const orchestratableContainers = computed(() => containerCatalog.value.filter((item) => ['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)))
|
||||
const completedContainerCount = computed(() => containerCatalog.value.filter((item) => item.planStatus === 'COMPLETED').length)
|
||||
const orchestratableContainers = computed(() => containers.value)
|
||||
const completedContainerCount = computed(() => 0)
|
||||
const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length)
|
||||
const unmappedChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
|
||||
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
@@ -84,17 +85,12 @@ const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.va
|
||||
)).length)
|
||||
const channelOptions = computed(() => SMART_LOADING_CHANNEL_GROUPS)
|
||||
const warehouseOptions = computed(() => {
|
||||
const options = new Map<string, string>()
|
||||
for (const item of orchestratableContainers.value) {
|
||||
if (item.warehouseId && !options.has(item.warehouseId)) options.set(item.warehouseId, item.warehouseName?.trim() || '未命名发货仓')
|
||||
}
|
||||
return [...options].map(([value, label]) => ({ value, label }))
|
||||
return warehouses.value.map((warehouse) => ({ value: String(warehouse.id), label: warehouse.warehouseName }))
|
||||
})
|
||||
const editable = computed(() => plan.value?.canEdit === true)
|
||||
const assignable = computed(() => plan.value?.canAssign === true)
|
||||
const selectedContainerResource = computed(() => containerCatalog.value.find((item) => item.id === selectedContainerId.value) || null)
|
||||
const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId)))
|
||||
type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds: string[]; memberInboundCodes: string[]; memberOrderSummaries: string[]; memberOrderCount: number; inboundFragmentCount: number; totalGrossWeightKg: number; totalVolumeCbm: number }
|
||||
type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds: string[]; memberInboundCodes: string[]; memberOrderSummaries: string[]; memberOrderCount: number; inboundFragmentCount: number; totalGrossWeightKg: number | null; totalVolumeCbm: number | null }
|
||||
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) {
|
||||
@@ -111,21 +107,21 @@ function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
: `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段`
|
||||
}
|
||||
function candidateTotalGrossWeight(candidate: SmartLoadingCandidate) {
|
||||
return Number(candidate.unitGrossWeightKg || 0) * Number(candidate.availablePackageCount || 0)
|
||||
return candidate.unitGrossWeightKg == null ? null : Number(candidate.unitGrossWeightKg) * Number(candidate.availablePackageCount || 0)
|
||||
}
|
||||
function candidateTotalVolume(candidate: SmartLoadingCandidate) {
|
||||
return candidate.availableVolumeCbm == null
|
||||
? Number(candidate.averageUnitVolumeCbm || 0) * Number(candidate.availablePackageCount || 0)
|
||||
? candidate.averageUnitVolumeCbm == null ? null : Number(candidate.averageUnitVolumeCbm) * Number(candidate.availablePackageCount || 0)
|
||||
: Number(candidate.availableVolumeCbm)
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
return candidates.value.map((candidate) => {
|
||||
const members: Array<{ accountId: string; inboundId: string; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length
|
||||
const members: Array<{ accountId: string; inboundId: string | null; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length
|
||||
? candidate.groupMembers
|
||||
: [candidate]
|
||||
return {
|
||||
...candidate,
|
||||
memberInboundIds: members.map((member) => member.inboundId),
|
||||
memberInboundIds: members.map((member) => member.inboundId).filter((id): id is string => Boolean(id)),
|
||||
memberInboundCodes: members.map((member) => member.inboundCode || '入仓码暂缺'),
|
||||
memberOrderSummaries: memberOrderSummaries(members),
|
||||
memberOrderCount: new Set(members.map((member) => member.orderId)).size,
|
||||
@@ -176,25 +172,27 @@ const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
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))
|
||||
const volumeLimit = computed(() => Number(selectedContainerResource.value?.capacityCbm || 0) * 1.1)
|
||||
const weightLimit = computed(() => Number(selectedContainerResource.value?.maxWeightKg || 0) * 1.1)
|
||||
const volumeLimit = computed(() => Number(selectedContainer.value?.capacityCbm || 0) * 1.5)
|
||||
const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.5)
|
||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
||||
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||
const candidateOrderStatusOptions = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'PENDING_REVIEW', label: '待审核' },
|
||||
{ value: 'WAIT_MEASURE', label: '待测量数据' },
|
||||
{ value: 'WAIT_SPLIT', label: '待分柜' },
|
||||
{ value: 'LOADING', label: '装柜中' },
|
||||
]
|
||||
function candidateStatusLabel(status: string | null | undefined) {
|
||||
return candidateOrderStatusOptions.find((item) => item.value === status)?.label || '—'
|
||||
}
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ 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 },
|
||||
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 260 },
|
||||
{ key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 160, options: candidateOrderStatusOptions },
|
||||
{ key: 'createdRange', type: 'daterange', label: '创建日期', placeholder: '请选择创建日期范围', width: 260 },
|
||||
{ key: 'orderStatuses', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 210, options: candidateOrderStatusOptions, props: { multiple: true, collapseTags: true, collapseTagsTooltip: true, maxCollapseTags: 2 } },
|
||||
])
|
||||
|
||||
const filteredContainers = computed(() => containers.value)
|
||||
@@ -216,7 +214,8 @@ const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true },
|
||||
{ key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70 },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' },
|
||||
{ key: 'orderStatusSnapshot', prop: 'orderStatusSnapshot', label: '订单状态', width: 100, slotName: 'orderStatusSnapshot' },
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 80 },
|
||||
// { key: 'cartonIdentityStatus', label: '箱号归属', width: 105, slotName: 'cartonIdentityStatus' },
|
||||
{ key: 'unitGrossWeightKg', label: '单箱毛重', width: 80, slotName: 'unitGrossWeight' },
|
||||
@@ -228,7 +227,7 @@ const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 80, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true },
|
||||
{ key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70 },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' },
|
||||
{ key: 'packageCount', label: '分配箱数', width: 100, slotName: 'packageCount' },
|
||||
{ key: 'grossWeightKg', label: '毛重', width: 80, slotName: 'weight' },
|
||||
{ key: 'volumeCbm', label: '体积', width: 80, slotName: 'volume' },
|
||||
@@ -237,6 +236,9 @@ const itemColumns: DataTableColumn[] = [
|
||||
function formatNumber(value: number, digits = 3) {
|
||||
return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0'
|
||||
}
|
||||
function formatMeasurement(value: number | null | undefined, unit: string, digits = 3) {
|
||||
return value == null ? '待测量' : `${formatNumber(Number(value), digits)} ${unit}`
|
||||
}
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidateRow }
|
||||
function itemRow(source: object) { return source as SmartLoadingInboundItem }
|
||||
@@ -245,17 +247,17 @@ function adjustmentMax(source: object) {
|
||||
const remaining = candidates.value.find((candidate) => candidate.inboundId === item.inboundId)?.availablePackageCount || 0
|
||||
return Math.max(item.minPackageCount, item.packageCount + remaining)
|
||||
}
|
||||
function displayCabinetNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
function displayCabinetNo(container: SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
if (!container) return '待生成'
|
||||
const cabinetReference = String(container.cabinetNo || '').trim()
|
||||
return /^\d{15,}$/.test(cabinetReference) ? '待生成' : cabinetReference || '待生成'
|
||||
}
|
||||
function hasRealContainerNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
function hasRealContainerNo(container: SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
if (!container) return false
|
||||
const value = String(container.containerNo || '').trim()
|
||||
return Boolean(value) && !/^(?:MAN|SPLIT)-/i.test(value)
|
||||
}
|
||||
function displayActualContainerNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
function displayActualContainerNo(container: SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
return hasRealContainerNo(container) ? String(container?.containerNo || '').trim() : '待补录'
|
||||
}
|
||||
function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
|
||||
@@ -264,6 +266,7 @@ function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> =
|
||||
}
|
||||
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}, syncContainerStatus = true) {
|
||||
plan.value = next
|
||||
planGroupsByInboundId.value = loadSmartLoadingPlanCoLoadGroups(next)
|
||||
noDraft.value = false
|
||||
syncDrafts(next, preserved)
|
||||
const container = selectedContainer.value
|
||||
@@ -283,8 +286,8 @@ function clearSelectionData() {
|
||||
}
|
||||
|
||||
function candidateQueryParams(): Parameters<typeof listSmartLoadingCandidates>[0] {
|
||||
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
|
||||
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
|
||||
const createdRange = Array.isArray(appliedQuery.value.createdRange)
|
||||
? appliedQuery.value.createdRange.map((value) => String(value || '').slice(0, 10))
|
||||
: []
|
||||
const params: Parameters<typeof listSmartLoadingCandidates>[0] = {
|
||||
channelGroupCodes: [...selectedChannelCodes.value],
|
||||
@@ -294,15 +297,17 @@ function candidateQueryParams(): Parameters<typeof listSmartLoadingCandidates>[0
|
||||
const warehouseId = String(appliedQuery.value.warehouseId || '')
|
||||
const customerId = String(appliedQuery.value.customerId || '')
|
||||
const inboundCode = String(appliedQuery.value.inboundCode || '').trim()
|
||||
const orderStatus = String(appliedQuery.value.orderStatus || '') as typeof params.orderStatus
|
||||
const orderStatuses = Array.isArray(appliedQuery.value.orderStatuses)
|
||||
? appliedQuery.value.orderStatuses.map((value) => String(value)).filter(Boolean) as NonNullable<typeof params.orderStatuses>
|
||||
: []
|
||||
if (warehouseId) params.warehouseId = warehouseId
|
||||
if (customerId) params.customerId = customerId
|
||||
if (inboundCode) params.inboundCode = inboundCode
|
||||
if (arrivalRange.length === 2) {
|
||||
params.expectedArrivalStart = arrivalRange[0]
|
||||
params.expectedArrivalEnd = arrivalRange[1]
|
||||
if (createdRange.length === 2) {
|
||||
params.createdStart = createdRange[0]
|
||||
params.createdEnd = createdRange[1]
|
||||
}
|
||||
if (orderStatus) params.orderStatus = orderStatus
|
||||
if (orderStatuses.length) params.orderStatuses = orderStatuses
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -350,7 +355,6 @@ async function loadSelectedPlan(preserved: Record<string, number> = {}) {
|
||||
if (request !== selectionRequest) return
|
||||
if (planResult.value) {
|
||||
acceptPlan(planResult.value, preserved, false)
|
||||
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value)
|
||||
syncDrafts(planResult.value, preserved)
|
||||
}
|
||||
else noDraft.value = planResult.missing
|
||||
@@ -391,17 +395,11 @@ async function loadAll(preserved: Record<string, number> = {}) {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const firstPage = await listContainers({ page: 1, size: 100 })
|
||||
const remainingPages = firstPage.pages > 1
|
||||
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 })))
|
||||
: []
|
||||
containerCatalog.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||||
warehouses.value = await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })
|
||||
const availableChannelCodes = new Set(channelOptions.value.map((item) => item.code))
|
||||
selectedChannelCodes.value = selectedChannelCodes.value.filter((code) => availableChannelCodes.has(code))
|
||||
if (!selectedChannelCodes.value.length) {
|
||||
selectedChannelCodes.value = [channelOptions.value.find((channel) => orchestratableContainers.value.some((item) => (
|
||||
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === channel.code
|
||||
)))?.code || channelOptions.value[0]?.code || '']
|
||||
selectedChannelCodes.value = [channelOptions.value[0]?.code || '']
|
||||
}
|
||||
await loadWorkbenchData(preserved)
|
||||
} catch (error) {
|
||||
@@ -437,7 +435,10 @@ async function selectChannels(codes: string[]) {
|
||||
await loadWorkbenchData()
|
||||
}
|
||||
async function applyFilters(model: QueryModel) {
|
||||
appliedQuery.value = { ...model }
|
||||
appliedQuery.value = {
|
||||
...model,
|
||||
orderStatuses: Array.isArray(model.orderStatuses) ? model.orderStatuses.map((value) => String(value)) : [],
|
||||
}
|
||||
candidatePage.value = 1
|
||||
await loadCandidateData()
|
||||
}
|
||||
@@ -471,20 +472,34 @@ async function assign() {
|
||||
}
|
||||
await mutate(async () => {
|
||||
const selected = selectedCandidate.value!
|
||||
acceptPlan(selected.coLoadGroupId
|
||||
acceptPlan(selected.orderStatusSnapshot === 'PENDING_REVIEW'
|
||||
? await assignPendingReviewSmartLoadingOrder(plan.value!.id, {
|
||||
customerId: selected.customerId,
|
||||
orderId: selected.orderId,
|
||||
inboundCode: selected.inboundCode || '',
|
||||
packageCount: assignForm.packageCount,
|
||||
expectedVersion: plan.value!.version,
|
||||
})
|
||||
: selected.coLoadGroupId && selected.inboundId
|
||||
? await assignSmartLoadingGroup(plan.value!.id, selected.inboundId, plan.value!.version)
|
||||
: await assignSmartLoadingItem(plan.value!.id, {
|
||||
inboundId: selected.inboundId,
|
||||
inboundId: selected.inboundId!,
|
||||
packageCount: assignForm.packageCount,
|
||||
expectedVersion: plan.value!.version,
|
||||
}))
|
||||
assignOpen.value = false
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '明细已加入当前柜次')
|
||||
}, '分配明细失败', { preserveAssign: true })
|
||||
}
|
||||
function candidateBlocked(candidate: SmartLoadingCandidateRow) {
|
||||
if (candidate.orderStatusSnapshot === 'PENDING_REVIEW') {
|
||||
return Boolean(candidate.blockReason) || candidate.locked === true
|
||||
|| !candidate.inboundCode || candidate.availablePackageCount < 1
|
||||
}
|
||||
return Boolean(candidate.blockReason) || candidate.locked === true
|
||||
|| !candidate.inboundId
|
||||
|| !candidate.inboundCode
|
||||
|| candidate.memberInboundIds.some((inboundId) => assignedInboundIds.value.has(inboundId))
|
||||
|| candidate.availablePackageCount < 1
|
||||
}
|
||||
@@ -500,7 +515,7 @@ async function recommend() {
|
||||
if (!plan.value) throw error
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
}
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success('智能推荐已生成')
|
||||
}, '智能推荐失败')
|
||||
}
|
||||
@@ -516,7 +531,7 @@ async function adjust(source: object) {
|
||||
if (count > max) { ElMessage.warning(`该明细最多可分配 ${max} 箱`); return }
|
||||
await mutate(async () => {
|
||||
acceptPlan(await adjustSmartLoadingItem(plan.value!.id, item.id, count, plan.value!.version, item.version))
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success('箱数已调整')
|
||||
}, '调整箱数失败', { preserveDrafts: { ...quantityDrafts } })
|
||||
}
|
||||
@@ -533,7 +548,7 @@ async function remove(source: object) {
|
||||
await mutate(async () => {
|
||||
if (row.coLoadGroup) {
|
||||
acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version))
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`)
|
||||
return
|
||||
}
|
||||
@@ -544,7 +559,7 @@ async function remove(source: object) {
|
||||
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
|
||||
acceptPlan(current)
|
||||
}
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success('该入仓码已从当前柜次移除')
|
||||
}, '移除装柜明细失败', { pickingConflict: true })
|
||||
}
|
||||
@@ -571,7 +586,7 @@ async function removeAll() {
|
||||
acceptPlan(current)
|
||||
}
|
||||
}
|
||||
await loadWorkbenchData()
|
||||
await loadCandidateData()
|
||||
ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`)
|
||||
}, '全部移除失败', { pickingConflict: true })
|
||||
}
|
||||
@@ -676,11 +691,13 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
@click="openAssign(row)"
|
||||
><Plus :size="15" />{{ row.coLoadGroupId ? '整组分配' : '加入' }}</el-button>
|
||||
</template>
|
||||
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroupId" class="candidate-group"><el-tag size="small" type="warning">{{ row.groupNo }} · {{ row.memberOrderCount }} 票 · {{ row.inboundFragmentCount }} 个入仓片段</el-tag><span v-for="summary in row.memberOrderSummaries" :key="summary">{{ summary }}</span><span>整组 {{ row.groupTotalBoxes }} 箱,必须全量进入同一柜</span><small v-if="row.blockReason" class="candidate-block">{{ row.blockReason }}</small><small v-else-if="row.locked">该组已锁定,不能重复分配</small></div><span v-else>普通单票</span></template>
|
||||
<template #unitGrossWeight="{ row }">{{ formatNumber(Number(row.unitGrossWeightKg || 0), 1) }} kg</template>
|
||||
<template #candidateTotalWeight="{ row }">{{ formatNumber(Number(row.totalGrossWeightKg || 0), 1) }} kg</template>
|
||||
<template #averageUnitVolume="{ row }">{{ formatNumber(Number(row.averageUnitVolumeCbm || 0)) }} m³</template>
|
||||
<template #candidateTotalVolume="{ row }">{{ formatNumber(Number(row.totalVolumeCbm || 0)) }} m³</template>
|
||||
<template #customerName="{ row }"><LineClampText :content="row.customerName" /></template>
|
||||
<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="普通单票" /></template>
|
||||
<template #orderStatusSnapshot="{ row }"><el-tag size="small" effect="plain">{{ candidateStatusLabel(row.orderStatusSnapshot) }}</el-tag></template>
|
||||
<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>
|
||||
<template #candidateTotalVolume="{ row }">{{ formatMeasurement(row.totalVolumeCbm, 'm³') }}</template>
|
||||
<template #cartonIdentityStatus><el-tag size="small" type="info" effect="plain">未核验</el-tag></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
@@ -747,8 +764,9 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</div>
|
||||
<DataTable :data="displayedPlanItems" :columns="itemColumns" :loading="selectionLoading || mutating" stripe preference-key="global-smart-loading-items" column-settings-mode="hide" empty-text="当前柜次暂无装柜明细">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canDelete && editable && row.canRemove" size="small" type="danger" plain @click="remove(row)"><Trash2 :size="15" />删除</el-button></div></template>
|
||||
<template #coLoadGroup="{ row }"><div v-if="row.coLoadGroup" class="candidate-group"><el-tag size="small" type="warning">{{ row.coLoadGroup.groupNo }} · {{ groupMemberLabel(row.coLoadGroup) }}</el-tag><span v-for="summary in row.memberOrderSummaries" :key="summary">{{ summary }}</span><span>固定 {{ row.coLoadGroup.fullPackageCount }} 箱 · {{ row.coLoadGroup.lockedPlanId ? '已锁定' : '未锁定' }}</span></div><span v-else>普通单票</span></template>
|
||||
<template #packageCount="{ row }"><strong v-if="row.coLoadGroup">{{ row.packageCount }} 箱(整组固定)</strong><div v-else class="package-count-editor"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="adjust(row)" placeholder="请输入调整箱数" /><small v-if="row.detailCount > 1 && editable">多规格,需删除后重加</small><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
|
||||
<template #customerName="{ row }"><LineClampText :content="row.customerName" /></template>
|
||||
<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="普通单票" /></template>
|
||||
<template #packageCount="{ row }"><LineClampText v-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" /><div v-else class="package-count-editor"><el-input-number v-model="quantityDrafts[row.id]" :min="row.minPackageCount" :max="adjustmentMax(row)" :step="1" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="adjust(row)" placeholder="请输入调整箱数" /><LineClampText v-if="row.detailCount > 1 && editable" content="多规格,需删除后重加" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template>
|
||||
</DataTable>
|
||||
@@ -759,13 +777,15 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<AppDialog v-model="assignOpen" :title="selectedCandidate?.coLoadGroupId ? '整组加入当前柜次' : '加入当前柜次'" :confirm-loading="mutating" :confirm-disabled="Boolean(selectedCandidate?.blockReason || selectedCandidate?.locked)" :confirm-text="selectedCandidate?.coLoadGroupId ? '确认整组分配' : '确认加入'" @confirm="assign">
|
||||
<div v-if="selectedCandidate" class="assign-dialog">
|
||||
<p><strong>{{ selectedCandidate.groupNo || selectedCandidate.customerName || '客户待补充' }}</strong></p>
|
||||
<p>目标柜次:{{ displayCabinetNo(selectedContainer) }} · 集装箱号:{{ displayActualContainerNo(selectedContainer) }}</p>
|
||||
<p>发货仓:{{ selectedContainer?.warehouseName || '—' }} · 渠道:{{ resolveSmartLoadingChannelGroup(selectedContainer?.channelCode, selectedContainer?.channelName)?.name || '—' }}</p>
|
||||
<template v-if="selectedCandidate.coLoadGroupId">
|
||||
<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" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>入仓码 {{ selectedCandidate.inboundCode || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱 · 平均单箱毛重 {{ formatNumber(Number(selectedCandidate.unitGrossWeightKg || 0), 1) }} kg · 平均单箱体积 {{ formatNumber(Number(selectedCandidate.averageUnitVolumeCbm || 0)) }} m³</p>
|
||||
<p>入仓码 {{ selectedCandidate.inboundCode || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱 · 平均单箱毛重 {{ formatMeasurement(selectedCandidate.unitGrossWeightKg, 'kg', 1) }} · 平均单箱体积 {{ formatMeasurement(selectedCandidate.averageUnitVolumeCbm, 'm³') }}</p>
|
||||
<small>本次只记录该入仓码的计划装柜箱数,具体箱号归属为未核验。</small>
|
||||
<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>
|
||||
</template>
|
||||
@@ -831,7 +851,9 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }
|
||||
.assign-dialog p { margin: 0 0 10px; }
|
||||
.assign-dialog :deep(.el-input-number) { width: 100%; }
|
||||
.candidate-group { display: grid; gap: 4px; }
|
||||
.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%; }
|
||||
.candidate-block { color: var(--el-color-danger) !important; }
|
||||
@media (max-width: 1180px) {
|
||||
: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; }
|
||||
|
||||
Reference in New Issue
Block a user