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:
@@ -9,6 +9,7 @@ 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 { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
adjustSmartLoadingItem, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer,
|
||||
@@ -62,12 +63,12 @@ function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) {
|
||||
}
|
||||
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
|
||||
return candidates.value.map((candidate) => {
|
||||
const members: Array<{ inboundId: string; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length
|
||||
const members: Array<{ 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,
|
||||
@@ -117,8 +118,8 @@ const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
|
||||
})
|
||||
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(container.value?.capacityCbm || 0) * 1.1)
|
||||
const weightLimit = computed(() => Number(container.value?.maxWeightKg || 0) * 1.1)
|
||||
const volumeLimit = computed(() => Number(container.value?.capacityCbm || 0) * 1.5)
|
||||
const weightLimit = computed(() => Number(container.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))
|
||||
@@ -129,7 +130,7 @@ const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'sequence', prop: 'loadSequence', label: '顺序', width: 75 },
|
||||
{ key: 'order', label: '入仓码', minWidth: 190, slotName: 'order' },
|
||||
{ key: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150 },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'customerName' },
|
||||
{ key: 'goods', label: '货物', minWidth: 190, slotName: 'goods' },
|
||||
{ key: 'packageCount', label: '分配箱数', width: 150, slotName: 'packageCount' },
|
||||
{ key: 'volumeCbm', label: '体积', width: 120, slotName: 'volume' },
|
||||
@@ -139,7 +140,7 @@ const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, fixed: 'left', showOverflowTooltip: true },
|
||||
{ key: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150 },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'customerName' },
|
||||
{ key: 'goodsSummary', prop: 'goodsSummary', label: '货物摘要', minWidth: 180, showOverflowTooltip: true },
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 110 },
|
||||
{ key: 'cartonIdentityStatus', label: '箱号归属', width: 110, slotName: 'cartonIdentityStatus' },
|
||||
@@ -157,7 +158,7 @@ function explainCandidate(source: object) {
|
||||
const weight = Number(candidate.unitGrossWeightKg || 0)
|
||||
const volumeRoom = volumeLimit.value ? Math.floor(Math.max(volumeLimit.value - totalVolume.value, 0) / Math.max(volume, 0.000001)) : candidate.availablePackageCount
|
||||
const weightRoom = weightLimit.value ? Math.floor(Math.max(weightLimit.value - totalWeight.value, 0) / Math.max(weight, 0.000001)) : candidate.availablePackageCount
|
||||
return `按 110% 上限,当前最多可装 ${Math.max(Math.min(candidate.availablePackageCount, volumeRoom, weightRoom), 0)} 箱`
|
||||
return `按 150% 上限,当前最多可装 ${Math.max(Math.min(candidate.availablePackageCount, volumeRoom, weightRoom), 0)} 箱`
|
||||
}
|
||||
function displayCabinetNo(value: ContainerBookingResource | null | undefined) {
|
||||
if (!value) return '待生成'
|
||||
@@ -217,15 +218,17 @@ function openAssign(source: object) {
|
||||
assignOpen.value = true
|
||||
}
|
||||
async function assign() {
|
||||
if (!plan.value || !selectedCandidate.value || assignForm.packageCount < 1) return
|
||||
if (selectedCandidate.value.coLoadGroupId && (selectedCandidate.value.blockReason || selectedCandidate.value.locked)) return
|
||||
const currentPlan = plan.value
|
||||
const selected = selectedCandidate.value
|
||||
const inboundId = selected?.inboundId
|
||||
if (!currentPlan || !selected || !inboundId || assignForm.packageCount < 1) return
|
||||
if (selected.coLoadGroupId && (selected.blockReason || selected.locked)) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try {
|
||||
const selected = selectedCandidate.value
|
||||
const next = selected.coLoadGroupId
|
||||
? await assignSmartLoadingGroup(plan.value.id, selected.inboundId, plan.value.version)
|
||||
: await assignSmartLoadingItem(plan.value.id, { inboundId: selected.inboundId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version })
|
||||
? await assignSmartLoadingGroup(currentPlan.id, inboundId, currentPlan.version)
|
||||
: await assignSmartLoadingItem(currentPlan.id, { inboundId, packageCount: assignForm.packageCount, expectedVersion: currentPlan.version })
|
||||
await acceptPlanWithGroups(next)
|
||||
assignOpen.value = false
|
||||
ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '货物已分配')
|
||||
@@ -234,6 +237,7 @@ async function assign() {
|
||||
}
|
||||
function candidateBlocked(candidate: SmartLoadingCandidateRow) {
|
||||
return Boolean(candidate.blockReason) || candidate.locked === true
|
||||
|| !candidate.inboundId
|
||||
|| candidate.memberInboundIds.some((inboundId) => assignedInboundIds.value.has(inboundId))
|
||||
|| candidate.availablePackageCount < 1
|
||||
}
|
||||
@@ -347,7 +351,7 @@ onMounted(load)
|
||||
</section>
|
||||
<template v-else>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</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>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 150% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 150% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该柜次缺少自动生成的预装计划,请刷新后重试" />
|
||||
<template v-else-if="plan">
|
||||
<el-alert v-if="plan.status === 'LOADING' && plan.canEdit" type="warning" :closable="false" show-icon title="已下发 PDA,仍可维护未完成的装柜明细" description="新增、增加或移除未拣明细会刷新 PDA 明细与版本;PDA 端需要刷新后继续操作。" />
|
||||
@@ -356,15 +360,17 @@ onMounted(load)
|
||||
<template #toolbar-start><div><h2>{{ displayContainerNo(container) }} 装柜方案</h2><p>智能建议必须经人工核对。</p></div></template>
|
||||
<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 #order="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></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 }} 箱</span></div><span v-else>普通单票</span></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} 箱`" /></div><LineClampText v-else content="普通单票" /></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></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" :step="1" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="adjust(row)" placeholder="请输入调整箱数" /><small v-if="row.detailCount > 1 && editable">含 {{ row.detailCount }} 个规格,需删除后重加</small><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></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" :step="1" :disabled="mutating || !canAssign || !editable || !row.canAdjust" controls-position="right" @change="adjust(row)" placeholder="请输入调整箱数" /><LineClampText v-if="row.detailCount > 1 && editable" :content="`含 ${row.detailCount} 个规格,需删除后重加`" /><LineClampText v-if="row.pickedPackageCount > 0" :content="`已拣 ${row.pickedPackageCount} 箱`" /></div></template>
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
</DataTable>
|
||||
<DataTable :data="displayedCandidates" :columns="candidateColumns" :loading="loading || mutating" stripe preference-key="smart-loading-candidates" column-settings-mode="hide" empty-text="当前仓库暂无可排柜货物">
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于装柜仓库、物流渠道、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于装柜仓库、物流渠道、可用箱数及 150% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && assignable" size="small" type="primary" plain :disabled="candidateBlocked(row as SmartLoadingCandidateRow)" @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 #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 #averageUnitVolume="{ row }">{{ formatNumber(Number(row.averageUnitVolumeCbm || 0)) }} m³</template><template #unitGrossWeight="{ row }">{{ formatNumber(Number(row.unitGrossWeightKg || 0),1) }} kg</template>
|
||||
<template #cartonIdentityStatus><el-tag size="small" type="info" effect="plain">未核验</el-tag></template>
|
||||
</DataTable>
|
||||
@@ -401,8 +407,9 @@ onMounted(load)
|
||||
.workbench :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; width: fit-content; gap: 3px; }
|
||||
.candidate-group > span,.candidate-group > small { color: var(--cc-muted); font-size: 11px; }
|
||||
.candidate-group { display: grid; width: 100%; min-width: 0; gap: 3px; }
|
||||
.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%; color: var(--cc-muted); font-size: 11px; }
|
||||
.candidate-group .candidate-block { color: var(--el-color-danger); }
|
||||
@media (max-width: 760px) { .workbench-command-bar,.header-actions { width: 100%; flex-direction: column; align-items: stretch; } .workbench-identities { flex-direction: column; align-items: stretch; gap: 10px; } .summary-band,.capacity-grid { grid-template-columns: 1fr; } .meter-title { align-items: flex-start; flex-direction: column; } .header-actions .el-button { min-height: 44px; margin-left: 0; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user