feat(admin): support indivisible coload groups

This commit is contained in:
2026-08-29 10:57:01 +08:00
parent aee562d998
commit 1aa2dfcdff
13 changed files with 621 additions and 38 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, Unlink, WalletCards } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder,
@@ -15,6 +15,12 @@ import {
type UnifiedOrderMainStatus,
type UnifiedOrderResource,
} from '@/modules/unified-orders/api/unifiedOrders'
import {
batchQueryPlatformCoLoadGroups,
cancelPlatformCoLoadGroup,
createPlatformCoLoadGroup,
type CoLoadGroup,
} from '@/modules/unified-orders/api/coLoadGroups'
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -121,6 +127,11 @@ const approvalLoadingId = ref('')
const approvalSaving = ref(false)
const batchApprovalSaving = ref(false)
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
const selectedOrders = ref<UnifiedOrderResource[]>([])
const coLoadGroupsByOrderId = ref(new Map<string, CoLoadGroup>())
const coLoadCreateOpen = ref(false)
const coLoadSaving = ref(false)
const coLoadError = ref<ApiErrorDetails | null>(null)
const approvalOrder = ref<UnifiedOrderResource | null>(null)
const noteOpen = ref(false)
const noteSaving = ref(false)
@@ -241,12 +252,13 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns = computed<DataTableColumn[]>(() => [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
...(can('platform:order:approve') ? [{
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') ? [{
key: 'selection', type: 'selection', width: 48, fixed: 'left',
selectable: (row: UnifiedOrderResource) => canApprove(row) && !batchApprovalSaving.value,
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value,
} satisfies DataTableColumn] : []),
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 180, slotName: 'field' },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
{ key: 'coLoadGroup', label: '同柜组', minWidth: 190, slotName: 'coLoadGroup' },
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
@@ -694,8 +706,11 @@ async function loadWarehouses() {
async function loadOrders() {
selectedApprovalOrders.value = []
selectedOrders.value = []
coLoadGroupsByOrderId.value = new Map()
loading.value = true
loadError.value = null
coLoadError.value = null
try {
const activeFilters = appliedFilters.value
const range = activeFilters.createdRange
@@ -722,6 +737,14 @@ async function loadOrders() {
})
records.value = result.records
total.value = result.total
if (can('platform:firstleg:smart-loading:assign') && result.records.length) {
try {
const groups = await batchQueryPlatformCoLoadGroups(result.records.map((row) => String(row.orderId)))
coLoadGroupsByOrderId.value = new Map(groups.map((item) => [item.orderId, item.group]))
} catch (error) {
coLoadError.value = getApiErrorDetails(error, '同柜不可拆组信息加载失败')
}
}
} catch (error) {
records.value = []
total.value = 0
@@ -759,10 +782,62 @@ function openMeasurement(row: UnifiedOrderResource) {
measurementDialogOpen.value = true
}
function handleApprovalSelectionChange(rows: UnifiedOrderResource[]) {
function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
selectedOrders.value = rows
selectedApprovalOrders.value = rows.filter(canApprove)
}
function coLoadGroup(row: UnifiedOrderResource) {
return coLoadGroupsByOrderId.value.get(String(row.orderId)) || null
}
function openCoLoadCreate() {
if (selectedOrders.value.length < 2) return
coLoadError.value = null
coLoadCreateOpen.value = true
}
async function createCoLoadGroup() {
if (selectedOrders.value.length < 2 || coLoadSaving.value) return
coLoadSaving.value = true
coLoadError.value = null
try {
const group = await createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))
coLoadCreateOpen.value = false
ElMessage.success(`已创建同柜不可拆组 ${group.groupNo},共 ${group.members.length} 票`)
await loadOrders()
} catch (error) {
coLoadError.value = getApiErrorDetails(error, '创建同柜不可拆组失败')
} finally {
coLoadSaving.value = false
}
}
async function cancelCoLoadGroup(row: UnifiedOrderResource) {
const group = coLoadGroup(row)
if (!group || group.status !== 'ACTIVE' || coLoadSaving.value) return
let reason = ''
try {
const result = await ElMessageBox.prompt(
`将整组取消 ${group.groupNo}(${group.members.length} 票)。不支持单独移除成员;如需调整,请取消后重新建组。`,
'取消同柜不可拆组',
{ confirmButtonText: '确认整组取消', cancelButtonText: '返回', inputPlaceholder: '取消原因(选填)', inputType: 'textarea' },
)
reason = result.value
} catch { return }
coLoadSaving.value = true
coLoadError.value = null
try {
await cancelPlatformCoLoadGroup(group.groupId, group.version, reason)
ElMessage.success(`同柜不可拆组 ${group.groupNo} 已整组取消`)
await loadOrders()
} catch (error) {
coLoadError.value = getApiErrorDetails(error, '取消同柜不可拆组失败')
} finally {
coLoadSaving.value = false
}
}
async function batchApproveOrders() {
const selected = [...selectedApprovalOrders.value]
if (!selected.length || batchApprovalSaving.value) return
@@ -816,17 +891,19 @@ function clearMeasurementOrder() {
measurementOrder.value = null
}
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement' | 'cancelCoLoad'
const unifiedOrderRowActionIcons: Record<UnifiedOrderRowAction, Component> = {
note: StickyNote,
approve: CircleCheck,
measurement: Ruler,
cancelCoLoad: Unlink,
}
function unifiedOrderSecondaryActions(row: UnifiedOrderResource): UnifiedOrderRowAction[] {
return [
...(can('platform:order:note') ? ['note' as const] : []),
...(canApprove(row) ? ['approve' as const] : []),
...(row.mainStatus === 'WAIT_MEASURE' && row.inboundId && can('platform:firstleg:inbound:measure:view') ? ['measurement' as const] : []),
...(can('platform:firstleg:smart-loading:assign') && coLoadGroup(row)?.status === 'ACTIVE' ? ['cancelCoLoad' as const] : []),
]
}
function directUnifiedOrderActions(row: UnifiedOrderResource) {
@@ -841,15 +918,17 @@ function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
if (action === 'note') return '添加备注'
if (action === 'approve') return '审核通过'
if (action === 'cancelCoLoad') return '取消同柜组'
return '测量作业'
}
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
return action === 'approve' ? 'success' : 'primary'
return action === 'approve' ? 'success' : action === 'cancelCoLoad' ? 'danger' : 'primary'
}
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
if (command === 'note') openNoteDialog(row)
if (command === 'approve') openApproval(row)
if (command === 'measurement') openMeasurement(row)
if (command === 'cancelCoLoad') void cancelCoLoadGroup(row)
}
function closeDetail() {
@@ -1223,9 +1302,11 @@ onBeforeUnmount(() => {
<template #default>
<div v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleApprovalSelectionChange">
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
<template #toolbar-actions>
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:assign')" type="primary" plain :loading="coLoadSaving" :disabled="loading || selectedOrders.length < 2" @click="openCoLoadCreate"><Link2 :size="16" />设为同柜不可拆组<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
</template>
<template #actions="{ row }">
@@ -1255,6 +1336,7 @@ onBeforeUnmount(() => {
><GitBranch :size="14" />{{ orderRelationLabel(row as UnifiedOrderResource) }}</el-button>
</div>
</template>
<template #coLoadGroup="{ row }"><div v-if="coLoadGroup(row as UnifiedOrderResource)" class="coload-cell"><el-tag size="small" :type="coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroup(row as UnifiedOrderResource)?.groupNo }}</el-tag><span>{{ coLoadGroup(row as UnifiedOrderResource)?.members.length }} 票 · {{ coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? '未锁定' : coLoadGroup(row as UnifiedOrderResource)?.status === 'LOCKED' ? '已锁定' : '已消费' }}</span></div><span v-else>—</span></template>
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
<template #field="{ row, column }"><LineClampText v-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" /><template v-else>{{ displayField(row as UnifiedOrderResource, column.property) }}</template></template>
@@ -1272,6 +1354,11 @@ onBeforeUnmount(() => {
</template>
</ListPageLayout>
<SmartLoadingDialog v-model="smartLoadingOpen" />
<AppDialog v-model="coLoadCreateOpen" title="设为同柜不可拆组" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="selectedOrders.length < 2" @confirm="createCoLoadGroup">
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票必须全量进入同一柜,不能部分装柜、拆子单或单独移除成员。`" />
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.orderNo || row.inboundCode || row.orderId }}</span></div>
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
</AppDialog>
</template>
<AppDrawer v-model="relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy-on-close>
@@ -1337,6 +1424,12 @@ onBeforeUnmount(() => {
.status-tab.is-active::after { background: var(--cc-primary); }
.orders-query-toolbar { border: 0; }
.table-card { overflow: hidden; }
.coload-error { margin: 8px 12px; padding: 8px 10px; border: 1px solid var(--el-color-danger-light-5); border-radius: 4px; background: var(--el-color-danger-light-9); color: var(--el-color-danger); font-size: 12px; }
.coload-error :deep(.api-error-meta) { margin-top: 5px; }
.coload-cell { display: grid; width: fit-content; gap: 3px; }
.coload-cell span { color: var(--cc-muted); font-size: 11px; }
.coload-selection { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.coload-selection > span { padding: 4px 7px; border: 1px solid var(--cc-border); border-radius: 4px; background: var(--cc-surface-muted); font-size: 12px; }
.table-card :deep(.el-table) { --el-table-border-color: #e5e7eb; --el-table-header-bg-color: #f9fafb; --el-table-row-hover-bg-color: #f3f7fb; --el-table-tr-bg-color: #fff; color: #374151; font-size: 12px; }
.table-card :deep(.el-table th.el-table__cell) { height: 42px; padding: 0; background: #f9fafb; color: #374151; font-size: 12px; font-weight: 600; }
.table-card :deep(.el-table td.el-table__cell) { min-height: 42px; padding: 9px 0; }