This commit is contained in:
2026-09-03 21:25:56 +08:00
parent 3ac2dc807f
commit c20ee82def
15 changed files with 242 additions and 85 deletions
@@ -9,10 +9,12 @@ 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,
@@ -61,7 +63,7 @@ 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 query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: '', createdRange: [], orderStatus: '' })
const appliedQuery = ref<QueryModel>({ ...query.value })
const candidatePage = ref(1)
const candidatePageSize = ref(20)
@@ -120,12 +122,12 @@ function candidateTotalVolume(candidate: SmartLoadingCandidate) {
}
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,
@@ -189,11 +191,14 @@ const candidateOrderStatusOptions = [
{ 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: 'createdRange', type: 'daterange', label: '创建日期', placeholder: '请选择创建日期范围', width: 260 },
{ key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 160, options: candidateOrderStatusOptions },
])
@@ -216,7 +221,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 +234,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' },
@@ -283,8 +289,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],
@@ -298,9 +304,9 @@ function candidateQueryParams(): Parameters<typeof listSmartLoadingCandidates>[0
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
return params
@@ -471,10 +477,18 @@ 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,
}))
@@ -484,7 +498,13 @@ async function assign() {
}, '分配明细失败', { 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
}
@@ -676,7 +696,9 @@ onMounted(() => { void load(); void loadCustomers() })
@click="openAssign(row)"
><Plus :size="15" />{{ row.coLoadGroupId ? '整组分配' : '加入' }}</el-button>
</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">{{ 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 #orderStatusSnapshot="{ row }"><el-tag size="small" effect="plain">{{ candidateStatusLabel(row.orderStatusSnapshot) }}</el-tag></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>
@@ -747,6 +769,7 @@ 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 #customerName="{ row }"><LineClampText :content="row.customerName" /></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)" /><small v-if="row.detailCount > 1 && editable">多规格,需删除后重加</small><small v-if="row.pickedPackageCount > 0">已拣 {{ row.pickedPackageCount }} 箱</small></div></template>
<template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0), 1) }} kg</template>