This commit is contained in:
2026-09-03 15:00:58 +08:00
parent a25b4ed0f7
commit 764b4cc2e6
8 changed files with 452 additions and 246 deletions
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Plus, RefreshCw, Save, Sparkles, Trash2 } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -19,6 +18,7 @@ import {
getSmartLoadingPlanByBooking,
listContainers,
listSmartLoadingCandidates,
listSmartLoadingWorkbenchContainers,
recommendSmartLoading,
removeSmartLoadingGroup,
removeSmartLoadingItem,
@@ -27,6 +27,7 @@ import type {
ContainerBookingResource,
SmartLoadingCandidate,
SmartLoadingPlan,
SmartLoadingWorkbenchContainer,
} from '../types'
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
@@ -38,8 +39,8 @@ import {
const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
const access = useAccessPreviewStore()
const router = useRouter()
const containers = ref<ContainerBookingResource[]>([])
const containers = ref<SmartLoadingWorkbenchContainer[]>([])
const containerCatalog = ref<ContainerBookingResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const selectedContainerId = ref('')
@@ -48,9 +49,11 @@ const plan = ref<SmartLoadingPlan | null>(null)
const candidates = ref<SmartLoadingCandidate[]>([])
const loading = ref(false)
const selectionLoading = ref(false)
const candidateLoading = ref(false)
const mutating = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const selectionError = ref<ApiErrorDetails | null>(null)
const candidateError = ref<ApiErrorDetails | null>(null)
const mutationError = ref<ApiErrorDetails | null>(null)
const noDraft = ref(false)
const assignOpen = ref(false)
@@ -62,13 +65,16 @@ const query = ref<QueryModel>({ warehouseId: '', customerId: '', inboundCode: ''
const appliedQuery = ref<QueryModel>({ ...query.value })
const candidatePage = ref(1)
const candidatePageSize = ref(20)
const candidateTotal = ref(0)
let selectionRequest = 0
let candidateRequest = 0
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.id === selectedContainerId.value) || null)
const orchestratableContainers = computed(() => containers.value.filter((item) => ['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)))
const completedContainerCount = computed(() => containers.value.filter((item) => item.planStatus === 'COMPLETED').length)
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 missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length)
const unmappedChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
@@ -86,9 +92,9 @@ const warehouseOptions = computed(() => {
})
const editable = computed(() => plan.value?.canEdit === true)
const assignable = computed(() => plan.value?.canAssign === true)
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
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 & { sourceCandidates: SmartLoadingCandidate[]; 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; totalVolumeCbm: number }
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) {
@@ -113,34 +119,21 @@ function candidateTotalVolume(candidate: SmartLoadingCandidate) {
: Number(candidate.availableVolumeCbm)
}
const displayedCandidates = computed<SmartLoadingCandidateRow[]>(() => {
const rows: SmartLoadingCandidateRow[] = []
const groups = new Map<string, SmartLoadingCandidate[]>()
for (const candidate of candidates.value) {
if (!candidate.coLoadGroupId) {
rows.push({ ...candidate, sourceCandidates: [candidate], memberInboundCodes: [candidate.inboundCode || '入仓码暂缺'], memberOrderSummaries: memberOrderSummaries([candidate]), memberOrderCount: 1, inboundFragmentCount: 1, totalGrossWeightKg: candidateTotalGrossWeight(candidate), totalVolumeCbm: candidateTotalVolume(candidate) })
continue
}
groups.set(candidate.coLoadGroupId, [...(groups.get(candidate.coLoadGroupId) || []), candidate])
}
for (const members of groups.values()) {
const first = members[0]
rows.push({
...first,
inboundCode: members.map((member) => member.inboundCode).filter(Boolean).join('、'),
customerName: [...new Set(members.map((member) => member.customerName).filter(Boolean))].join('、'),
goodsSummary: [...new Set(members.map((member) => member.goodsSummary).filter(Boolean))].join('、'),
availablePackageCount: Number(first.groupTotalBoxes || members.reduce((sum, member) => sum + member.availablePackageCount, 0)),
availableVolumeCbm: members.reduce((sum, member) => sum + Number(member.availableVolumeCbm || 0), 0),
sourceCandidates: members,
return candidates.value.map((candidate) => {
const members: Array<{ accountId: string; inboundId: string; orderId: string; orderNo?: string | null; inboundCode?: string | null }> = candidate.groupMembers.length
? candidate.groupMembers
: [candidate]
return {
...candidate,
memberInboundIds: members.map((member) => member.inboundId),
memberInboundCodes: members.map((member) => member.inboundCode || '入仓码暂缺'),
memberOrderSummaries: memberOrderSummaries(members),
memberOrderCount: new Set(members.map((member) => member.orderId)).size,
inboundFragmentCount: Number(first.memberCount || members.length),
totalGrossWeightKg: members.reduce((sum, member) => sum + candidateTotalGrossWeight(member), 0),
totalVolumeCbm: members.reduce((sum, member) => sum + candidateTotalVolume(member), 0),
})
}
return rows
inboundFragmentCount: Number(candidate.memberCount || members.length),
totalGrossWeightKg: candidateTotalGrossWeight(candidate),
totalVolumeCbm: candidateTotalVolume(candidate),
}
})
})
type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] }
const displayedPlanItems = computed<SmartLoadingPlanRow[]>(() => {
@@ -183,8 +176,8 @@ 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(selectedContainer.value?.capacityCbm || 0) * 1.1)
const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.1)
const volumeLimit = computed(() => Number(selectedContainerResource.value?.capacityCbm || 0) * 1.1)
const weightLimit = computed(() => Number(selectedContainerResource.value?.maxWeightKg || 0) * 1.1)
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))
@@ -196,13 +189,6 @@ const candidateOrderStatusOptions = [
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
]
const candidateOrderStatusGroups: Record<string, readonly string[]> = {
PENDING_REVIEW: ['SUBMITTED', 'PENDING_REVIEW'],
WAIT_MEASURE: ['WAIT_MEASURE'],
WAIT_SPLIT: ['WAIT_SPLIT'],
LOADING: ['LOADING'],
}
const completedCandidateOrderStatuses = new Set(['LOADED', 'COMPLETED'])
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 } },
@@ -211,42 +197,19 @@ const queryFields = computed<QueryField[]>(() => [
{ key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 160, options: candidateOrderStatusOptions },
])
const filteredContainers = computed(() => containers.value.filter((item) => (
['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus)
&& (!String(appliedQuery.value.warehouseId || '') || item.warehouseId === String(appliedQuery.value.warehouseId))
&& selectedChannelCodes.value.includes(resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code || '')
)))
const filteredCandidates = computed(() => {
const customerId = String(appliedQuery.value.customerId || '')
const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase()
const orderStatus = String(appliedQuery.value.orderStatus || '').trim().toUpperCase()
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
: []
const channelCode = selectedContainer.value?.channelCode
return displayedCandidates.value.filter((item) => {
if (!channelCode || item.channelCode !== channelCode) return false
if (item.sourceCandidates.some((member) => completedCandidateOrderStatuses.has(member.orderStatusSnapshot?.trim().toUpperCase() || ''))) return false
if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false
if (inboundCode && !item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode))) return false
if (orderStatus && !item.sourceCandidates.some((member) => candidateOrderStatusGroups[orderStatus]?.includes(member.orderStatusSnapshot?.trim().toUpperCase() || ''))) return false
if (arrivalRange.length === 2 && !item.sourceCandidates.some((member) => {
const arrivalDate = member.expectedArrivalAt?.slice(0, 10) || ''
return arrivalDate && arrivalDate >= arrivalRange[0] && arrivalDate <= arrivalRange[1]
})) return false
return true
})
})
const pagedCandidates = computed(() => {
const start = (candidatePage.value - 1) * candidatePageSize.value
return filteredCandidates.value.slice(start, start + candidatePageSize.value)
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('、') || '未选择渠道'}`
})
function preferredContainerId() {
const visible = filteredContainers.value
return visible.find((item) => item.planStatus === 'EDITING')?.id
|| visible.find((item) => item.planStatus === 'LOADING')?.id
|| visible.find((item) => item.planStatus === 'WAIT_LOADING')?.id || visible[0]?.id || ''
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 || ''
}
const candidateColumns: DataTableColumn[] = [
@@ -282,17 +245,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 | null | undefined) {
function displayCabinetNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) {
if (!container) return '待生成'
const cabinetReference = String(container.cabinetNo || '').trim()
return /^\d{15,}$/.test(cabinetReference) ? '待生成' : cabinetReference || '待生成'
}
function hasRealContainerNo(container: ContainerBookingResource | null | undefined) {
function hasRealContainerNo(container: ContainerBookingResource | 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 | null | undefined) {
function displayActualContainerNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) {
return hasRealContainerNo(container) ? String(container?.containerNo || '').trim() : '待补录'
}
function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
@@ -306,13 +269,12 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> =
const container = selectedContainer.value
if (container) {
container.planId = next.id
if (syncContainerStatus) container.planStatus = next.status
if (syncContainerStatus && next.status !== 'COMPLETED') container.planStatus = next.status
}
}
function clearSelectionData() {
plan.value = null
candidates.value = []
noDraft.value = false
selectionError.value = null
mutationError.value = null
@@ -320,25 +282,64 @@ function clearSelectionData() {
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
}
async function loadSelectedData(preserved: Record<string, number> = {}) {
function candidateQueryParams(): Parameters<typeof listSmartLoadingCandidates>[0] {
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
: []
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()
const orderStatus = String(appliedQuery.value.orderStatus || '') as typeof params.orderStatus
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 (orderStatus) params.orderStatus = orderStatus
return params
}
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> = {}) {
const container = selectedContainer.value
const request = ++selectionRequest
clearSelectionData()
if (!container) return
if (container.planStatus === 'COMPLETED') return
candidatePage.value = 1
selectionLoading.value = true
try {
const [candidateResult, planResult] = await Promise.all([
listSmartLoadingCandidates(container.id, container.warehouseId),
getSmartLoadingPlanByBooking(container.id).then((value) => ({ value, missing: false })).catch((error) => {
const details = getApiErrorDetails(error)
if (details.code === 'A0404') return { value: null, missing: true }
throw error
}),
])
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
})
if (request !== selectionRequest) return
candidates.value = candidateResult
if (planResult.value) {
acceptPlan(planResult.value, preserved, false)
planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value)
@@ -352,12 +353,30 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
}
}
function openList(status: 'LOADING' | 'COMPLETED') {
const routeName = status === 'LOADING' ? 'loading-plans' : 'loading-lists'
const query = selectedContainer.value
? { planId: selectedContainer.value.planId || undefined, warehouseId: selectedContainer.value.warehouseId }
: undefined
void router.push({ name: routeName, query })
async function loadContainerData(preserved: Record<string, number> = {}) {
if (!selectedChannelCodes.value.length) return
const request = ++containerRequest
try {
const firstPage = await listSmartLoadingWorkbenchContainers({ ...candidateQueryParams(), page: 1, size: 200 })
const remainingPages = firstPage.pages > 1
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listSmartLoadingWorkbenchContainers({ ...candidateQueryParams(), page: index + 2, size: 200 })))
: []
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)])
}
async function loadAll(preserved: Record<string, number> = {}) {
@@ -368,7 +387,7 @@ async function loadAll(preserved: Record<string, number> = {}) {
const remainingPages = firstPage.pages > 1
? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 })))
: []
containers.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
containerCatalog.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
const availableChannelCodes = new Set(channelOptions.value.map((item) => item.code))
selectedChannelCodes.value = selectedChannelCodes.value.filter((code) => availableChannelCodes.has(code))
if (!selectedChannelCodes.value.length) {
@@ -376,8 +395,7 @@ async function loadAll(preserved: Record<string, number> = {}) {
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === channel.code
)))?.code || channelOptions.value[0]?.code || '']
}
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
await loadSelectedData(preserved)
await loadWorkbenchData(preserved)
} catch (error) {
loadError.value = getApiErrorDetails(error, '智能装柜工作台加载失败')
} finally {
@@ -399,7 +417,7 @@ async function loadCustomers() {
async function selectContainer(containerId: string) {
if (selectedContainerId.value === containerId) return
selectedContainerId.value = containerId
await loadSelectedData()
await loadSelectedPlan()
}
async function selectChannels(codes: string[]) {
if (!codes.length) {
@@ -407,16 +425,24 @@ async function selectChannels(codes: string[]) {
return
}
selectedChannelCodes.value = codes
selectedContainerId.value = preferredContainerId()
await loadSelectedData()
candidatePage.value = 1
await loadWorkbenchData()
}
async function applyFilters(model: QueryModel) {
appliedQuery.value = { ...model }
candidatePage.value = 1
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
selectedContainerId.value = preferredContainerId()
await loadSelectedData()
}
await loadWorkbenchData()
}
async function changeCandidatePage(page: number) {
candidatePage.value = page
await loadCandidateData()
}
async function changeCandidatePageSize(size: number) {
candidatePageSize.value = size
candidatePage.value = 1
await loadCandidateData()
}
function openAssign(source: object) {
@@ -445,13 +471,13 @@ async function assign() {
expectedVersion: plan.value!.version,
}))
assignOpen.value = false
await loadSelectedData()
await loadWorkbenchData()
ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '明细已加入当前柜次')
}, '分配明细失败', { preserveAssign: true })
}
function candidateBlocked(candidate: SmartLoadingCandidateRow) {
return Boolean(candidate.blockReason) || candidate.locked === true
|| candidate.sourceCandidates.some((member) => assignedInboundIds.value.has(member.inboundId))
|| candidate.memberInboundIds.some((inboundId) => assignedInboundIds.value.has(inboundId))
|| candidate.availablePackageCount < 1
}
async function recommend() {
@@ -462,11 +488,11 @@ async function recommend() {
} catch (error) {
const details = getApiErrorDetails(error, '智能推荐失败')
if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error
await loadSelectedData()
await loadSelectedPlan()
if (!plan.value) throw error
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
}
await loadSelectedData()
await loadWorkbenchData()
ElMessage.success('智能推荐已生成')
}, '智能推荐失败')
}
@@ -482,7 +508,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 loadSelectedData()
await loadWorkbenchData()
ElMessage.success('箱数已调整')
}, '调整箱数失败', { preserveDrafts: { ...quantityDrafts } })
}
@@ -499,7 +525,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 loadSelectedData()
await loadWorkbenchData()
ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`)
return
}
@@ -510,7 +536,7 @@ async function remove(source: object) {
current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version)
acceptPlan(current)
}
await loadSelectedData()
await loadWorkbenchData()
ElMessage.success('该入仓码已从当前柜次移除')
}, '移除装柜明细失败', { pickingConflict: true })
}
@@ -537,7 +563,7 @@ async function removeAll() {
acceptPlan(current)
}
}
await loadSelectedData()
await loadWorkbenchData()
ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`)
}, '全部移除失败', { pickingConflict: true })
}
@@ -582,7 +608,7 @@ onMounted(() => { void load(); void loadCustomers() })
<el-checkbox-button v-for="channel in channelOptions" :key="channel.code" :label="channel.code" :value="channel.code">{{ channel.name }}</el-checkbox-button>
</el-checkbox-group>
<dl class="compact-summary" aria-label="已选渠道装柜统计">
<div><dt>候选明细</dt><dd><strong>{{ filteredCandidates.length }}</strong> 条</dd></div>
<div><dt>候选明细</dt><dd><strong>{{ candidateTotal }}</strong> 条</dd></div>
<div><dt>柜子数量</dt><dd><strong>{{ filteredContainers.length }}</strong> 个</dd></div>
<div><dt>已装</dt><dd><strong>{{ totalPackages }}</strong> 箱</dd></div>
<div><dt>总重量</dt><dd><strong>{{ formatNumber(totalWeight, 1) }}</strong> kg</dd></div>
@@ -607,24 +633,29 @@ onMounted(() => { void load(); void loadCustomers() })
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
<template v-else>
<LoadErrorState v-if="mutationError" title="装柜操作失败" :message="mutationError.message" compact retry-text="刷新当前柜次" @retry="loadSelectedData()">
<LoadErrorState v-if="mutationError" title="装柜操作失败" :message="mutationError.message" compact retry-text="刷新工作台" @retry="loadWorkbenchData()">
<template #details><ApiErrorMeta :details="mutationError" /></template>
</LoadErrorState>
<section class="dispatch-grid" aria-label="智能装柜调度台">
<section class="dispatch-pane candidate-pane">
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ selectedContainer?.warehouseName || '未选择发货仓' }} · {{ resolveSmartLoadingChannelGroup(selectedContainer?.channelCode, selectedContainer?.channelName)?.name || '未选择渠道' }}</small></div></header>
<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>
<DataTable
v-model:page="candidatePage"
v-model:page-size="candidatePageSize"
:data="pagedCandidates"
:page="candidatePage"
:page-size="candidatePageSize"
:data="displayedCandidates"
:columns="candidateColumns"
:loading="loading || selectionLoading || mutating"
:total="filteredCandidates.length"
:loading="loading || candidateLoading || mutating"
:total="candidateTotal"
:page-sizes="[20, 50, 100]"
stripe
preference-key="global-smart-loading-candidates"
column-settings-mode="hide"
:empty-text="selectedContainer ? '当前仓库暂无可装明细' : '暂无可用柜次'"
empty-text="当前筛选条件下暂无候选装柜明细"
@update:page="changeCandidatePage"
@update:page-size="changeCandidatePageSize"
>
<template #pagination-left="{ total, page, pageCount }"><span v-if="pageCount > 0">共 {{ total }} 条 · {{ page }}/{{ pageCount }} 页</span><span v-else>共 0 条</span></template>
<template #actions="{ row }">
@@ -652,11 +683,11 @@ onMounted(() => { void load(); void loadCustomers() })
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
<button
v-for="item in filteredContainers"
:key="item.id"
:key="item.bookingId"
type="button"
class="container-option"
:class="{ selected: item.id === selectedContainerId }"
@click="selectContainer(item.id)"
:class="{ selected: item.bookingId === selectedContainerId }"
@click="selectContainer(item.bookingId)"
>
<span class="container-option__top">
<strong>{{ hasRealContainerNo(item) ? displayActualContainerNo(item) : '模拟柜次' }}</strong>
@@ -677,22 +708,16 @@ onMounted(() => { void load(); void loadCustomers() })
<header class="pane-header plan-header">
<div><strong>{{ hasRealContainerNo(selectedContainer) ? displayActualContainerNo(selectedContainer) : displayCabinetNo(selectedContainer) }} 装柜方案</strong></div>
<div class="plan-actions">
<el-tooltip content="刷新当前柜次" placement="top"><el-button aria-label="刷新当前柜次" :loading="selectionLoading" @click="loadSelectedData()"><RefreshCw :size="16" /></el-button></el-tooltip>
<el-tooltip content="刷新当前柜次" placement="top"><el-button aria-label="刷新当前柜次" :loading="selectionLoading" @click="loadSelectedPlan()"><RefreshCw :size="16" /></el-button></el-tooltip>
<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>
</div>
</header>
<LoadErrorState v-if="selectionError" title="当前柜次加载失败" :message="selectionError.message" compact @retry="loadSelectedData()">
<LoadErrorState v-if="selectionError" title="当前柜次加载失败" :message="selectionError.message" compact @retry="loadSelectedPlan()">
<template #details><ApiErrorMeta :details="selectionError" /></template>
</LoadErrorState>
<el-empty v-else-if="!selectedContainer" description="请选择一个货柜" />
<section v-else-if="selectedPlanStatus === 'COMPLETED'" class="readonly-status-card" aria-label="装柜完成柜次">
<el-tag type="success">装柜完成</el-tag>
<strong>该柜次已完成装柜</strong>
<p>装柜计划、明细和运输信息已永久只读,请在装柜清单查看完整记录。</p>
<el-button type="primary" @click="openList('COMPLETED')">查看装柜清单</el-button>
</section>
<el-empty v-else-if="noDraft" description="该柜次缺少自动生成的预装计划,请刷新后重试" />
<template v-else-if="plan">
<!--