This commit is contained in:
2026-08-10 16:24:54 +08:00
parent e119b48662
commit 820432a381
52 changed files with 1015 additions and 705 deletions
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { Check, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
import { Check, Pencil, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -267,12 +268,9 @@ onMounted(() => { void load() })
</script>
<template>
<section class="smart-loading" aria-labelledby="smart-loading-title">
<section class="smart-loading" aria-label="智能排柜工作台">
<header class="smart-loading__heading">
<div>
<h3 id="smart-loading-title">智能排柜</h3>
<p>{{ container?.cabinetNo || container?.containerNo || `柜次 ${bookingId}` }} · {{ container?.warehouseName || '仓库待加载' }}</p>
</div>
<div class="smart-loading__context"><span>当前柜次</span><strong>{{ container?.cabinetNo || container?.containerNo || `柜次 ${bookingId}` }}</strong><small>{{ container?.warehouseName || '仓库待加载' }}</small></div>
<div class="smart-loading__actions">
<el-button circle :loading="loading" aria-label="刷新智能排柜" @click="load"><RefreshCw :size="16" /></el-button>
<el-button v-if="!plan" type="primary" :disabled="!canCreate || loading" :loading="action === 'create'" @click="createPlan"><Plus :size="15" />创建装柜草稿</el-button>
@@ -299,11 +297,11 @@ onMounted(() => { void load() })
<div><span>重量</span><strong>{{ formatNumber(usedWeight) }} / {{ formatNumber(container.maxWeightKg || 0) }} kg</strong><small>{{ weightRate == null ? '承重待配置' : `${weightRate}%` }}</small></div>
</div>
<section v-if="plan" class="smart-loading__section">
<section v-if="plan" class="smart-loading__section surface-panel table-panel">
<div class="smart-loading__section-title"><h4>当前装柜方案</h4><span>版本 {{ plan.version }} · {{ plan.planNo || '—' }}</span></div>
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading" empty-text="装柜草稿暂无明细">
<template #actions="{ row }">
<el-button link type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)">调整</el-button>
<el-button link type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)"><Pencil :size="14" />调整</el-button>
<el-button link type="danger" :disabled="!canDelete || !editable || Boolean(action)" @click="openRemove(row as SmartLoadingPlanItem)"><Trash2 :size="13" />移除</el-button>
</template>
<template #order="{ row }"><strong>{{ (row as SmartLoadingPlanItem).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingPlanItem).inboundCode || '—' }}</small></template>
@@ -315,9 +313,11 @@ onMounted(() => { void load() })
<section class="smart-loading__section">
<div class="smart-loading__section-title"><h4>候选货物</h4><span>后端按入库完成时间排序</span></div>
<QueryToolbar v-model="filters" ariaLabel="候选货物筛选" :fields="queryFields" @search="applyFilters" @reset="applyFilters" />
<ListPageLayout ariaLabel="智能排柜候选货物">
<template #toolbar><QueryToolbar v-model="filters" ariaLabel="候选货物筛选" :fields="queryFields" @search="applyFilters" @reset="applyFilters" /></template>
<section class="surface-panel table-panel">
<DataTable :data="filteredCandidates" :columns="candidateColumns" :loading="loading" empty-text="当前柜次仓库暂无可用候选货物">
<template #actions="{ row }"><el-button link type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)">加入</el-button></template>
<template #actions="{ row }"><el-button link type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)"><Plus :size="14" />加入</el-button></template>
<template #priority="{ $index }"><span class="priority">{{ Number($index) + 1 }}</span></template>
<template #order="{ row }"><strong>{{ (row as SmartLoadingCandidate).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingCandidate).inboundCode || '—' }}</small></template>
<template #destination="{ row }">{{ (row as SmartLoadingCandidate).destinationWarehouseName || (row as SmartLoadingCandidate).destinationWarehouseCode || '—' }}</template>
@@ -325,6 +325,8 @@ onMounted(() => { void load() })
<template #unitWeight="{ row }">{{ formatNumber((row as SmartLoadingCandidate).unitWeightKg) }}</template>
<template #explanation="{ row }"><el-tag size="small" :type="fittingCount(row as SmartLoadingCandidate) > 0 ? 'success' : 'danger'">{{ fittingCount(row as SmartLoadingCandidate) > 0 ? '可加入' : '冲突' }}</el-tag><span>{{ candidateExplanation(row as SmartLoadingCandidate) }}</span></template>
</DataTable>
</section>
</ListPageLayout>
</section>
</template>
@@ -356,13 +358,16 @@ onMounted(() => { void load() })
.smart-loading { display: grid; gap: 14px; }
.smart-loading__heading, .smart-loading__actions, .smart-loading__section-title { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.smart-loading__heading, .smart-loading__section-title { justify-content: space-between; }
.smart-loading__heading h3, .smart-loading__section-title h4 { margin: 0; font-size: 15px; }
.smart-loading__heading p, .smart-loading__section-title span, .smart-loading small { display: block; margin: 3px 0 0; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.smart-loading__section-title h4 { margin: 0; font-size: 15px; }
.smart-loading__context { display: grid; min-width: 0; gap: 3px; }
.smart-loading__context span { color: var(--cc-text-secondary, #64748b); font-size: 11px; }
.smart-loading__context strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.smart-loading__section-title span, .smart-loading small { display: block; margin: 3px 0 0; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.smart-loading__summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.smart-loading__summary > div { min-width: 0; padding: 10px; border: 1px solid var(--cc-border); border-radius: 5px; background: var(--el-fill-color-extra-light); }
.smart-loading__summary span { display: block; margin-bottom: 4px; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.smart-loading__summary strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
.smart-loading__section { display: grid; gap: 10px; padding-top: 4px; }
.smart-loading__section { display: grid; min-width: 0; gap: 10px; padding-top: 4px; overflow: hidden; }
.smart-loading__error p { margin: 6px 0 0; }
.smart-loading :deep(.el-table__cell small), .smart-loading :deep(.el-table__cell strong) { display: block; }
.smart-loading :deep(.el-table__cell small) { margin-top: 2px; color: var(--cc-text-secondary, #64748b); }
@@ -392,12 +392,9 @@ onMounted(() => { void refresh() })
<template>
<section class="workflow-panel" aria-label="订单后半程作业">
<div class="workflow-heading">
<div>
<h2>后半程作业</h2>
<p>查看订单的分柜、预装、拣货和装柜进度。</p>
</div>
<el-button text :loading="loading" aria-label="刷新作业数据" @click="refresh"><RefreshCw :size="15" />刷新</el-button>
<div class="workflow-heading" aria-label="订单作业上下文">
<div class="workflow-context"><span>入库作业</span><strong>#{{ props.inboundId }}</strong></div>
<el-tooltip content="刷新作业数据" placement="top"><el-button text :loading="loading" aria-label="刷新作业数据" @click="refresh"><RefreshCw :size="15" /></el-button></el-tooltip>
</div>
<el-alert v-if="error" class="workflow-alert" type="error" :closable="false" show-icon>
@@ -421,7 +418,7 @@ onMounted(() => { void refresh() })
</el-card>
<template v-if="isSplit">
<el-card shadow="never" class="workflow-card">
<el-card shadow="never" class="workflow-card surface-panel table-panel">
<template #header><div class="card-title"><span>入库预装清单</span><el-tag>{{ preloadStatusLabel(preload?.status) }}</el-tag></div></template>
<el-alert v-if="!canPreloadView" type="warning" :closable="false" show-icon>当前账号无预装清单查询权限。</el-alert>
<LoadErrorState v-else-if="preloadLoadError" title="预装清单暂时无法加载" :message="preloadLoadError.message" :loading="loading" @retry="loadPreload">
@@ -434,7 +431,6 @@ onMounted(() => { void refresh() })
<el-button link type="danger" :disabled="!canSplit || Boolean(busy)" :aria-label="`删除第 ${Number($index) + 1} 条预装明细`" @click="removeItem(Number($index))"><Trash2 :size="14" />删除</el-button>
</template>
<template #volume="{ row }">{{ formatVolume((row as PreloadItem).volumeCbm) }}</template>
<template #toolbar-start><div><strong>明细</strong><p>入库级清单由人工维护;柜次候选和智能建议在下方柜次工作台加载。</p></div></template>
<template #toolbar-actions><el-button :disabled="!canSplit || Boolean(busy)" @click="addItem"><Plus :size="15" />新增明细</el-button></template>
</DataTable>
<el-input v-model="splitRemark" :disabled="!canSplit || Boolean(busy)" class="workflow-remark" type="textarea" :rows="2" maxlength="1000" show-word-limit aria-label="分柜备注" placeholder="分柜备注(可选)" />
@@ -533,8 +529,9 @@ onMounted(() => { void refresh() })
.workflow-panel { display: grid; gap: 14px; }
.workflow-heading, .card-title, .workflow-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.workflow-heading { justify-content: space-between; }
.workflow-heading h2 { margin: 0; font-size: 17px; }
.workflow-heading p { margin: 4px 0 0; color: var(--cc-text-secondary, #64748b); font-size: 13px; }
.workflow-context { display: grid; min-width: 0; gap: 3px; }
.workflow-context span { color: var(--cc-text-secondary, #64748b); font-size: 11px; }
.workflow-context strong { overflow: hidden; color: var(--cc-text, #1f2937); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.workflow-card { border-radius: 12px; }
.card-title { justify-content: space-between; }
.preload-dialog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
import { getPlatformUnifiedOrder, type UnifiedOrderDetailResource } from '@/modules/unified-orders/api/unifiedOrders'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import UnifiedOrderWorkflowPanel from '@/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue'
@@ -37,6 +38,7 @@ onMounted(() => { void load() })
</script>
<template>
<AdminPage>
<section class="workflow-page">
<LoadErrorState v-if="error" title="后半程作业暂时无法加载" :message="error.message" :loading="loading" @retry="load">
<template #details><ApiErrorMeta :details="error" /></template>
@@ -54,6 +56,7 @@ onMounted(() => { void load() })
<el-empty v-else-if="!loading" description="该订单尚未生成入库作业,暂无后半程操作" />
<el-skeleton v-else :rows="5" animated />
</section>
</AdminPage>
</template>
<style scoped>