优化
This commit is contained in:
@@ -7,7 +7,6 @@ import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
@@ -52,18 +51,18 @@ let containerOptionRequest = 0
|
||||
const statusTab = ref<'全部' | '待分配' | '柜子'>('全部')
|
||||
const filters = ref<QueryModel>({ orderNo: '', orderType: '', warehouse: '', inboundCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] })
|
||||
const allFields: QueryField[] = [
|
||||
{ key: 'orderNo', type: 'input', label: '尾程单号', placeholder: '请输入尾程单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' },
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', placeholder: '全部类型', options: [
|
||||
{ key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', width: 170, placeholder: '全部类型', options: [
|
||||
{ label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' },
|
||||
{ label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '本土散板', value: 'LOCAL_LOOSEBOARD' },
|
||||
{ label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' },
|
||||
] },
|
||||
{ key: 'warehouse', type: 'input', label: '海外仓', placeholder: '请输入仓库名称或编码' },
|
||||
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', placeholder: '请输入来源订单号' },
|
||||
{ key: 'containerNo', type: 'input', label: '集装箱号', placeholder: '请输入集装箱号' },
|
||||
{ key: 'transferWarehouseCode', type: 'input', label: '转仓代码', placeholder: '请输入转仓代码' },
|
||||
{ key: 'status', type: 'select', label: '当前状态', placeholder: '全部状态', options: [
|
||||
{ key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
|
||||
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', width: 190, placeholder: '请输入来源订单号' },
|
||||
{ key: 'containerNo', type: 'input', label: '集装箱号', width: 180, placeholder: '请输入集装箱号' },
|
||||
{ key: 'transferWarehouseCode', type: 'input', label: '转仓代码', width: 170, placeholder: '请输入转仓代码' },
|
||||
{ key: 'status', type: 'select', label: '当前状态', width: 160, placeholder: '全部状态', options: [
|
||||
{ label: '待分配', value: 'PENDING_ASSIGNMENT' },
|
||||
{ label: '待入库', value: 'PENDING_INBOUND' },
|
||||
{ label: '入库完成', value: 'INBOUND_COMPLETED' },
|
||||
@@ -71,9 +70,9 @@ const allFields: QueryField[] = [
|
||||
{ label: '派送中', value: 'DELIVERING' },
|
||||
{ label: '派送完成', value: 'DELIVERED' },
|
||||
] },
|
||||
{ key: 'abnormal', type: 'select', label: '是否异常', placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'detained', type: 'select', label: '是否暂扣', placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'createdAt', type: 'daterange', label: '创建时间' },
|
||||
{ key: 'abnormal', type: 'select', label: '是否异常', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'detained', type: 'select', label: '是否暂扣', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'createdAt', type: 'daterange', label: '创建时间', width: 280 },
|
||||
]
|
||||
const fields = computed<QueryField[]>(() => statusTab.value === '柜子'
|
||||
? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key))
|
||||
@@ -144,15 +143,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 160 },
|
||||
{ key: 'warehouseRemark', prop: 'warehouseRemark', label: '备注(仓库)', minWidth: 160 },
|
||||
]
|
||||
const containerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '柜号', minWidth: 170 },
|
||||
{ key: 'plannedArrivalAt', prop: 'plannedArrivalAt', label: '计划到仓时间', minWidth: 170, slotName: 'plannedArrivalAt' },
|
||||
{ key: 'totalOrderCount', prop: 'totalOrderCount', label: '订单总数', width: 100, align: 'right' },
|
||||
{ key: 'inboundCodes', label: '入仓码', minWidth: 170, slotName: 'inboundCodes' },
|
||||
{ key: 'planProgress', label: '入库计划进度', minWidth: 270, slotName: 'planProgress' },
|
||||
{ key: 'readiness', label: '整柜就绪状态', minWidth: 240, slotName: 'readiness' },
|
||||
]
|
||||
const containerDetailColumns: DataTableColumn[] = [
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170 },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 150 },
|
||||
@@ -166,7 +156,19 @@ function formatDateTime(value: string | null | undefined) {
|
||||
return value ? value.replace('T', ' ').slice(0, 19) : '—'
|
||||
}
|
||||
function canGenerateContainerPlans(container: ContainerPlanProgressSummaryDto) {
|
||||
return Boolean(container.containerNo) && container.missingInboundCodeCount === 0 && container.pendingPlanCount > 0
|
||||
return Boolean(container.containerNo) && container.pendingPlanCount > 0
|
||||
}
|
||||
function pendingInboundCodeCount(container: ContainerPlanProgressSummaryDto) {
|
||||
return Math.max(0, container.inboundCodeCount - container.plannedInboundCodeCount)
|
||||
}
|
||||
function changeContainerPage(value: number) {
|
||||
page.value = value
|
||||
void load()
|
||||
}
|
||||
function changeContainerPageSize(value: number) {
|
||||
pageSize.value = value
|
||||
page.value = 1
|
||||
void load()
|
||||
}
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
@@ -182,8 +184,9 @@ async function load(model: QueryModel = filters.value) {
|
||||
createdFrom: dateRange[0] ? `${dateRange[0]}T00:00:00` : undefined,
|
||||
createdTo: dateRange[1] ? `${dateRange[1]}T23:59:59` : undefined,
|
||||
})
|
||||
containerRows.value = result.records
|
||||
containerTotal.value = result.total
|
||||
const pendingRecords = result.records.filter((container) => pendingInboundCodeCount(container) > 0)
|
||||
containerRows.value = pendingRecords
|
||||
containerTotal.value = Math.max(0, result.total - (result.records.length - pendingRecords.length))
|
||||
} catch (error) {
|
||||
containerRows.value = []
|
||||
containerTotal.value = 0
|
||||
@@ -380,7 +383,7 @@ onMounted(() => { void load() })
|
||||
<el-radio-group v-model="statusTab" aria-label="尾程订单状态" @change="changeStatusTab">
|
||||
<el-radio-button value="全部">全部</el-radio-button>
|
||||
<el-radio-button value="待分配">待分配</el-radio-button>
|
||||
<el-radio-button value="柜子">柜子({{ containerTotal }})</el-radio-button>
|
||||
<el-radio-button value="柜子">柜子工作台({{ containerTotal }})</el-radio-button>
|
||||
</el-radio-group>
|
||||
<QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" @search="search" @reset="reset" />
|
||||
</section>
|
||||
@@ -393,15 +396,28 @@ onMounted(() => { void load() })
|
||||
<LoadErrorState v-if="actionError" title="尾程订单操作失败" :message="actionError.message" compact>
|
||||
<template #details><ApiErrorMeta :details="actionError" /></template>
|
||||
</LoadErrorState>
|
||||
<section v-if="!loadError && statusTab === '柜子'" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="containerRows" :columns="containerColumns" :loading="loading" :total="containerTotal" preference-key="overseas-last-mile-container-progress" row-key="sourceContainerBookingId" empty-text="暂无柜级入库计划进度" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openContainerDetail(row as ContainerPlanProgressSummaryDto)">查看</el-button></template>
|
||||
<template #plannedArrivalAt="{ row }">{{ formatDateTime((row as ContainerPlanProgressSummaryDto).plannedArrivalAt) }}</template>
|
||||
<template #inboundCodes="{ row }">已有 {{ (row as ContainerPlanProgressSummaryDto).inboundCodeCount }} / 缺 {{ (row as ContainerPlanProgressSummaryDto).missingInboundCodeCount }}</template>
|
||||
<template #planProgress="{ row }">已生成{{ (row as ContainerPlanProgressSummaryDto).planCount }}个计划,剩余{{ (row as ContainerPlanProgressSummaryDto).pendingPlanCount }}个未操作</template>
|
||||
<template #readiness="{ row }"><el-tag :type="(row as ContainerPlanProgressSummaryDto).allPlansReady ? 'success' : 'warning'" effect="plain">{{ (row as ContainerPlanProgressSummaryDto).allPlansReady ? '整柜已就绪' : '未就绪' }}</el-tag><span v-if="!(row as ContainerPlanProgressSummaryDto).allPlansReady" class="readiness-reason">{{ (row as ContainerPlanProgressSummaryDto).blockingReason || '尚未满足全部独立计划条件,不可视为 PDA 可作业' }}</span></template>
|
||||
<template #pagination-left>共 {{ containerTotal }} 个柜</template>
|
||||
</DataTable>
|
||||
<section v-if="!loadError && statusTab === '柜子'" class="surface-panel container-workbench" :aria-busy="loading">
|
||||
<header class="container-workbench__header">
|
||||
<div><strong>待处理柜工作台</strong><span>按计划到仓时间由近到远排列,已全部生成计划的柜不再展示</span></div>
|
||||
<el-tag type="warning" effect="plain">{{ containerTotal }} 个柜待处理</el-tag>
|
||||
</header>
|
||||
<div v-if="containerRows.length" class="container-workbench__grid">
|
||||
<button v-for="container in containerRows" :key="container.sourceContainerBookingId" type="button" class="container-workbench-card" @click="openContainerDetail(container)">
|
||||
<span class="container-workbench-card__top"><strong>{{ container.containerNo || '柜号待补充' }}</strong><span>查看详情</span></span>
|
||||
<span class="container-workbench-card__arrival"><small>计划到仓</small><strong>{{ formatDateTime(container.plannedArrivalAt) }}</strong></span>
|
||||
<span class="container-workbench-card__pending">
|
||||
<strong>{{ pendingInboundCodeCount(container) }}</strong><span>个入仓码待生成计划</span>
|
||||
</span>
|
||||
<span class="container-workbench-card__facts">
|
||||
<span>共 {{ container.inboundCodeCount }} 个入仓码</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<el-empty v-else-if="!loading" description="暂无待处理柜,所有柜均已完成计划分配" />
|
||||
<div v-if="containerTotal" class="container-workbench__pagination">
|
||||
<span>共 {{ containerTotal }} 个待处理柜</span>
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :page-sizes="[10, 20, 50]" :total="containerTotal" layout="sizes, prev, pager, next" background @current-change="changeContainerPage" @size-change="changeContainerPageSize" />
|
||||
</div>
|
||||
</section>
|
||||
<section v-else-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-last-mile-orders" row-key="id" empty-text="暂无尾程订单" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" plain :disabled="selectedRows.length !== 1 || actionLoading" @click="convertInboundPlan">生成入库计划</el-button><el-button v-if="statusTab === '待分配' && access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
|
||||
@@ -438,7 +454,7 @@ onMounted(() => { void load() })
|
||||
<el-alert v-if="selectedContainer" :title="canGenerateContainerPlans(selectedContainer) ? `确认后将为该柜剩余 ${selectedContainer.pendingPlanCount} 票分别生成独立入库计划` : selectedContainer.blockingReason || '所选柜不可生成'" :type="canGenerateContainerPlans(selectedContainer) ? 'info' : 'error'" :closable="false" show-icon />
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDrawer v-model="containerDetailOpen" title="柜级入库计划详情" size="min(1040px, 96vw)" :loading="containerDetailLoading" :show-footer="false">
|
||||
<AppDialog v-model="containerDetailOpen" :title="`柜子详情 · ${containerDetail?.summary.containerNo || '加载中'}`" width="min(1120px, calc(100vw - 40px))" :loading="containerDetailLoading" :show-footer="false">
|
||||
<LoadErrorState v-if="containerDetailError" title="柜级详情加载失败" :message="containerDetailError.message" compact>
|
||||
<template #details><ApiErrorMeta :details="containerDetailError" /></template>
|
||||
</LoadErrorState>
|
||||
@@ -447,20 +463,41 @@ onMounted(() => { void load() })
|
||||
<span>柜号:{{ containerDetail.summary.containerNo || '—' }}</span>
|
||||
<span>计划到仓:{{ formatDateTime(containerDetail.summary.plannedArrivalAt) }}</span>
|
||||
<span>订单:{{ containerDetail.summary.totalOrderCount }}</span>
|
||||
<span>入仓码:{{ containerDetail.summary.inboundCodeCount }},缺 {{ containerDetail.summary.missingInboundCodeCount }}</span>
|
||||
<span>计划:{{ containerDetail.summary.planCount }},未操作 {{ containerDetail.summary.pendingPlanCount }}</span>
|
||||
<span>入仓码:{{ containerDetail.summary.inboundCodeCount }}</span>
|
||||
<span>入库计划:{{ containerDetail.summary.planCount }},待生成 {{ pendingInboundCodeCount(containerDetail.summary) }}</span>
|
||||
</div>
|
||||
<DataTable :data="containerDetail.orders" :columns="containerDetailColumns" :loading="containerDetailLoading" preference-key="overseas-last-mile-container-detail" row-key="orderId" :total="undefined" empty-text="该柜暂无尾程订单">
|
||||
<template #plannedArrivalAt="{ row }">{{ formatDateTime((row as ContainerPlanProgressDetailDto['orders'][number]).plannedArrivalAt) }}</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
</AppDrawer>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.last-mile-toolbar { padding: 14px 16px 0; }
|
||||
.last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; }
|
||||
.container-convert-hint { margin: 0 0 16px; color: var(--cc-muted); line-height: 1.6; }
|
||||
.readiness-reason { margin-left: 8px; color: var(--cc-muted); font-size: 12px; }
|
||||
.container-detail-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 16px; margin-bottom: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: var(--cc-surface); font-size: 13px; }
|
||||
.container-workbench { padding: 18px; }
|
||||
.container-workbench__header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.container-workbench__header > div { display: grid; gap: 4px; }
|
||||
.container-workbench__header strong { color: var(--cc-text); font-size: 16px; }
|
||||
.container-workbench__header span { color: var(--cc-muted); font-size: 13px; }
|
||||
.container-workbench__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
|
||||
.container-workbench-card { display: grid; min-width: 0; gap: 14px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; color: inherit; font: inherit; text-align: left; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease; }
|
||||
.container-workbench-card:hover { border-color: var(--cc-primary); box-shadow: 0 6px 18px rgb(15 23 42 / 8%); }
|
||||
.container-workbench-card:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
|
||||
.container-workbench-card__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.container-workbench-card__top strong { min-width: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 16px; }
|
||||
.container-workbench-card__top > span { flex: 0 0 auto; color: var(--cc-primary); font-size: 13px; }
|
||||
.container-workbench-card__arrival { display: grid; gap: 4px; }
|
||||
.container-workbench-card__arrival small { color: var(--cc-muted); font-size: 12px; }
|
||||
.container-workbench-card__arrival strong { color: var(--cc-text); font-size: 15px; }
|
||||
.container-workbench-card__pending { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px; border-left: 3px solid #f59e0b; background: #fffaf0; }
|
||||
.container-workbench-card__pending strong { color: #b45309; font-size: 28px; line-height: 1; }
|
||||
.container-workbench-card__pending span { color: #92400e; font-size: 13px; }
|
||||
.container-workbench-card__facts { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--cc-muted); font-size: 12px; }
|
||||
.container-workbench-card__facts .is-warning { color: #c2410c; }
|
||||
.container-workbench__pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--cc-border); color: var(--cc-muted); font-size: 13px; }
|
||||
@media (max-width: 1380px) { .container-workbench__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user