fix(admin): complete container loading workflows
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -30,6 +31,7 @@ import type {
|
||||
} from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const containers = ref<ContainerBookingResource[]>([])
|
||||
const selectedContainerId = ref('')
|
||||
const plan = ref<SmartLoadingPlan | null>(null)
|
||||
@@ -45,7 +47,7 @@ const assignOpen = ref(false)
|
||||
const selectedCandidate = ref<SmartLoadingCandidate | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const query = ref<QueryModel>({ orderKeyword: '', containerKeyword: '', warehouseId: '', status: '' })
|
||||
const query = ref<QueryModel>({ orderKeyword: '', containerKeyword: '', status: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
let selectionRequest = 0
|
||||
|
||||
@@ -54,7 +56,8 @@ const canAssign = computed(() => access.can('platform:firstleg:smart-loading:ass
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const selectedContainer = computed(() => containers.value.find((item) => item.id === selectedContainerId.value) || null)
|
||||
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
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))
|
||||
@@ -64,28 +67,20 @@ const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg |
|
||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||
|
||||
const warehouseOptions = computed(() => {
|
||||
const values = new Map<string, string>()
|
||||
for (const item of containers.value) values.set(item.warehouseId, item.warehouseName || `仓库 ${item.warehouseId}`)
|
||||
return [...values].map(([value, label]) => ({ value, label }))
|
||||
})
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、客户、货物', width: 250 },
|
||||
{ key: 'containerKeyword', type: 'input', label: '柜次', placeholder: '柜次号、集装箱号、目的仓', width: 230 },
|
||||
{ key: 'warehouseId', type: 'select', label: '装柜仓库', options: warehouseOptions.value, width: 190 },
|
||||
{ key: 'status', type: 'select', label: '柜次状态', width: 160, options: [
|
||||
{ label: '可编辑', value: 'EDITING' },
|
||||
{ label: '装柜中', value: 'LOADING' },
|
||||
{ label: '已推送预装', value: 'LOADING' },
|
||||
{ label: '已完成', value: 'COMPLETED' },
|
||||
] },
|
||||
])
|
||||
|
||||
const filteredContainers = computed(() => {
|
||||
const keyword = String(appliedQuery.value.containerKeyword || '').trim().toLowerCase()
|
||||
const warehouseId = String(appliedQuery.value.warehouseId || '')
|
||||
const status = String(appliedQuery.value.status || '')
|
||||
return containers.value.filter((item) => {
|
||||
if (item.planStatus === 'COMPLETED') return false
|
||||
if (warehouseId && item.warehouseId !== warehouseId) return false
|
||||
if (status && item.planStatus !== status) return false
|
||||
if (!keyword) return true
|
||||
return [item.cabinetNo, item.containerNo, item.bookingNo, item.destinationWarehouseCode, item.destinationWarehouseName]
|
||||
@@ -131,7 +126,7 @@ function formatNumber(value: number, digits = 3) {
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function statusLabel(status: ContainerPlanStatus) {
|
||||
return ({ EDITING: '可编辑', LOADING: '装柜中', COMPLETED: '已完成' } as const)[status]
|
||||
return ({ EDITING: '可编辑', LOADING: '已推送预装', COMPLETED: '已完成' } as const)[status]
|
||||
}
|
||||
function statusType(status: ContainerPlanStatus) {
|
||||
return status === 'EDITING' ? 'primary' : status === 'LOADING' ? 'warning' : 'success'
|
||||
@@ -168,7 +163,9 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
const request = ++selectionRequest
|
||||
clearSelectionData()
|
||||
if (!container) return
|
||||
if (container.planStatus === 'COMPLETED') return
|
||||
// Draft recovery is an EDITING-only contract. Later states remain visible and
|
||||
// continue through their dedicated read-only operational lists.
|
||||
if (container.planStatus !== 'EDITING') return
|
||||
selectionLoading.value = true
|
||||
try {
|
||||
const [candidateResult, planResult] = await Promise.all([
|
||||
@@ -190,7 +187,15 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
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 loadAll(preserved: Record<string, number> = {}) {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
@@ -198,17 +203,16 @@ async function load() {
|
||||
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)
|
||||
.filter((item) => item.planStatus !== 'COMPLETED')
|
||||
containers.value = [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||||
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) selectedContainerId.value = preferredContainerId()
|
||||
await loadSelectedData()
|
||||
await loadSelectedData(preserved)
|
||||
} catch (error) {
|
||||
loadError.value = getApiErrorDetails(error, '智能装柜工作台加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
async function load() { await loadAll() }
|
||||
async function selectContainer(containerId: string) {
|
||||
if (selectedContainerId.value === containerId) return
|
||||
selectedContainerId.value = containerId
|
||||
@@ -300,7 +304,7 @@ async function mutate(
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
if (details.code === 'A0409') {
|
||||
await loadSelectedData(recovery.preserveDrafts)
|
||||
await loadAll(recovery.preserveDrafts)
|
||||
mutationError.value = { ...details, message: '方案已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' }
|
||||
if (recovery.preserveAssign) assignOpen.value = true
|
||||
ElMessage.warning(mutationError.value.message)
|
||||
@@ -374,7 +378,7 @@ onMounted(load)
|
||||
</section>
|
||||
|
||||
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
|
||||
<header class="pane-header"><div><strong>货柜</strong><small>可用与装柜中</small></div></header>
|
||||
<header class="pane-header"><div><strong>货柜</strong><small>可用、已推送与已完成</small></div></header>
|
||||
<div v-loading="loading" class="container-list">
|
||||
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
|
||||
<button
|
||||
@@ -409,6 +413,18 @@ onMounted(load)
|
||||
<template #details><ApiErrorMeta :details="selectionError" /></template>
|
||||
</LoadErrorState>
|
||||
<el-empty v-else-if="!selectedContainer" description="请选择一个货柜" />
|
||||
<section v-else-if="selectedPlanStatus === 'LOADING'" class="readonly-status-card" aria-label="预装中柜次">
|
||||
<el-tag type="warning">已推送预装</el-tag>
|
||||
<strong>该柜次正在预装流程中</strong>
|
||||
<p>智能装柜方案已锁定,不能继续调整。预装运输信息和未拣货明细请在预装清单维护。</p>
|
||||
<el-button type="primary" @click="openList('LOADING')">查看预装清单</el-button>
|
||||
</section>
|
||||
<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="当前货柜尚未创建装柜方案">
|
||||
<el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建装柜方案</el-button>
|
||||
</el-empty>
|
||||
@@ -466,6 +482,9 @@ onMounted(load)
|
||||
.plan-actions,.row-actions { flex-wrap: wrap; }
|
||||
.plan-actions :deep(.el-button + .el-button),.row-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.capacity-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; padding: 12px; border-bottom: 1px solid var(--cc-border); }
|
||||
.readonly-status-card { display: grid; align-content: start; justify-items: start; gap: 10px; padding: 28px 24px; }
|
||||
.readonly-status-card strong { font-size: 16px; }
|
||||
.readonly-status-card p { max-width: 460px; margin: 0; color: var(--cc-muted); line-height: 1.6; }
|
||||
.meter-title { justify-content: space-between; margin-bottom: 8px; font-size: 12px; }
|
||||
.plan-pane :deep(.el-input-number) { width: 120px; }
|
||||
.assign-dialog p { margin: 0 0 10px; }
|
||||
|
||||
Reference in New Issue
Block a user