优化
This commit is contained in:
@@ -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">
|
||||
<!--
|
||||
|
||||
Reference in New Issue
Block a user