fix(admin): complete container loading workflows
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ArrowLeft, ListChecks, Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
@@ -38,6 +38,8 @@ const canCreate = computed(() => access.can('platform:firstleg:smart-loading:cre
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const canViewPreload = computed(() => access.can('platform:firstleg:preload:list'))
|
||||
const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list'))
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
@@ -93,6 +95,11 @@ async function load() {
|
||||
loading.value = true; loadError.value = null; noDraft.value = false
|
||||
try {
|
||||
container.value = await getContainer(containerId.value)
|
||||
if (container.value.planStatus !== 'EDITING') {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
return
|
||||
}
|
||||
try { acceptPlan(await getSmartLoadingPlanByBooking(containerId.value)) }
|
||||
catch (error) {
|
||||
const detail = getApiErrorDetails(error)
|
||||
@@ -103,11 +110,18 @@ async function load() {
|
||||
} catch (error) { loadError.value = getApiErrorDetails(error, '智能排柜工作台加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function openOperationalList() {
|
||||
if (!container.value || container.value.planStatus === 'EDITING') return
|
||||
void router.push({
|
||||
name: container.value.planStatus === 'LOADING' ? 'loading-plans' : 'loading-lists',
|
||||
query: { planId: container.value.planId || undefined, warehouseId: container.value.warehouseId },
|
||||
})
|
||||
}
|
||||
async function createPlan() {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') }
|
||||
catch (error) { handleMutationError(error, '创建排柜计划失败') }
|
||||
catch (error) { await handleMutationError(error, '创建排柜计划失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
function openAssign(source: object) { const candidate = candidateRow(source); selectedCandidate.value = candidate; assignForm.packageCount = Math.min(candidate.availablePackageCount, 1); assignOpen.value = true }
|
||||
@@ -118,7 +132,7 @@ async function assign() {
|
||||
try {
|
||||
acceptPlan(await assignSmartLoadingItem(plan.value.id, { inboundId: selectedCandidate.value.inboundId, inboundDetailId: selectedCandidate.value.inboundDetailId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version }))
|
||||
assignOpen.value = false; ElMessage.success('货物已分配')
|
||||
} catch (error) { handleMutationError(error, '分配失败') }
|
||||
} catch (error) { await handleMutationError(error, '分配失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
async function recommend() {
|
||||
@@ -126,7 +140,7 @@ async function recommend() {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)); ElMessage.success('智能建议已生成,请人工核对后再推送') }
|
||||
catch (error) { handleMutationError(error, '智能推荐失败') }
|
||||
catch (error) { await handleMutationError(error, '智能推荐失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
async function adjust(source: object) {
|
||||
@@ -137,7 +151,7 @@ async function adjust(source: object) {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') }
|
||||
catch (error) { handleMutationError(error, '调整失败') }
|
||||
catch (error) { await handleMutationError(error, '调整失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
async function remove(source: object) {
|
||||
@@ -148,7 +162,7 @@ async function remove(source: object) {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await removeSmartLoadingItem(plan.value.id, item.id, plan.value.version, item.version)); ElMessage.success('装柜明细已移除') }
|
||||
catch (error) { handleMutationError(error, '移除失败') }
|
||||
catch (error) { await handleMutationError(error, '移除失败') }
|
||||
finally { mutating.value = false }
|
||||
})
|
||||
}
|
||||
@@ -159,12 +173,19 @@ async function pushPreload() {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('已推送预装清单') }
|
||||
catch (error) { handleMutationError(error, '推送失败') }
|
||||
catch (error) { await handleMutationError(error, '推送失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
function handleMutationError(error: unknown, fallback: string) {
|
||||
async function handleMutationError(error: unknown, fallback: string) {
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
mutationError.value = { ...details, message: details.code === 'A0409' ? '计划已被其他人修改,请刷新最新计划后重试' : details.message }
|
||||
if (details.code === 'A0409') {
|
||||
const preserved = { ...quantityDrafts }
|
||||
await load()
|
||||
for (const item of plan.value?.items || []) {
|
||||
if (preserved[item.id] != null) quantityDrafts[item.id] = preserved[item.id]
|
||||
}
|
||||
}
|
||||
mutationError.value = { ...details, message: details.code === 'A0409' ? '计划已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' : details.message }
|
||||
ElMessage.error(mutationError.value.message)
|
||||
}
|
||||
|
||||
@@ -190,6 +211,13 @@ onMounted(load)
|
||||
<LoadErrorState v-if="loadError" title="智能排柜加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<template v-else>
|
||||
<LoadErrorState v-if="mutationError" title="排柜操作失败" :message="mutationError.message" compact retry-text="刷新最新计划" @retry="load"><template #details><ApiErrorMeta :details="mutationError" /></template></LoadErrorState>
|
||||
<section v-if="container?.planStatus !== 'EDITING'" class="readonly-status-card" aria-label="只读装柜状态">
|
||||
<el-tag :type="container?.planStatus === 'COMPLETED' ? 'success' : 'warning'">{{ container?.planStatus === 'COMPLETED' ? '已完成' : '已推送预装' }}</el-tag>
|
||||
<strong>{{ container?.planStatus === 'COMPLETED' ? '该柜次已完成装柜' : '该柜次正在预装流程中' }}</strong>
|
||||
<p>智能装柜方案已锁定,不能继续调整。请进入对应清单查看后续信息。</p>
|
||||
<el-button v-if="container?.planStatus === 'LOADING' ? canViewPreload : canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />{{ container?.planStatus === 'COMPLETED' ? '查看装柜清单' : '查看预装清单' }}</el-button>
|
||||
</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><div><span>目的仓</span><strong>{{ container.destinationWarehouseName || container.destinationWarehouseCode || '不限' }}</strong></div><div><span>计划状态</span><strong>{{ plan?.status === 'LOADING' ? '已推送预装' : plan ? '编辑中' : '未创建' }}</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(volumePercent,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(weightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
@@ -211,6 +239,7 @@ onMounted(load)
|
||||
<template #route="{ row }"><el-tag :type="routeMatches(row) ? 'success' : 'danger'">{{ routeMatches(row) ? '匹配' : '冲突' }}</el-tag><small>{{ explainCandidate(row) }}</small></template>
|
||||
</DataTable>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<AppDialog v-model="assignOpen" title="分配候选货物" :confirm-loading="mutating" confirm-text="确认分配" @confirm="assign"><div v-if="selectedCandidate" class="assign-dialog"><p><strong>{{ selectedCandidate.goodsName || '未命名货物' }}</strong></p><p>{{ selectedCandidate.orderNo || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱</p><el-form label-position="top"><el-form-item label="本次分配箱数" required><el-input-number v-model="assignForm.packageCount" :min="1" :max="selectedCandidate.availablePackageCount" :step="1" controls-position="right" /></el-form-item></el-form><el-alert :title="explainCandidate(selectedCandidate)" type="info" :closable="false" /></div></AppDialog>
|
||||
</section>
|
||||
@@ -232,6 +261,9 @@ onMounted(load)
|
||||
.summary-band>div { display: grid; gap: 5px; padding: 14px; background: #fff; }
|
||||
.summary-band span { color: var(--cc-muted); font-size: 12px; }
|
||||
.capacity-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.readonly-status-card { display: grid; justify-items: start; gap: 10px; padding: 24px; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
|
||||
.readonly-status-card strong { font-size: 16px; }
|
||||
.readonly-status-card p { margin: 0; color: var(--cc-muted); }
|
||||
.meter-title { justify-content: space-between; gap: 12px; margin-bottom: 10px; font-size: 13px; }
|
||||
.row-actions { min-width: 150px; }
|
||||
.workbench :deep(.el-input-number) { width: 130px; }
|
||||
|
||||
Reference in New Issue
Block a user