优化优化

This commit is contained in:
2026-10-10 14:58:15 +08:00
parent 1abc831df1
commit 40cccee266
5 changed files with 88 additions and 4 deletions
@@ -21,6 +21,7 @@ import {
listLastMileOrders,
reviewCargoDiscard,
transferLastMileOrders,
updateLastMileOrderPalletMerge,
getLastMileOrderManualNodes,
updateLastMileOrderHold,
updateLastMileOrderManualNodes,
@@ -38,7 +39,7 @@ vi.mock('../api/lastMileOrders', () => ({
changeContainerReceivingWarehouse: vi.fn(),
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
reviewCargoDiscard: vi.fn(),
reviewCargoDiscard: vi.fn(), updateLastMileOrderPalletMerge: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
@@ -1189,3 +1190,40 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(routerPush).toHaveBeenCalledWith({ name: 'overseas-return-inventory', query: expect.objectContaining({ packing: '1', returnNos: 'RETURN-1' }) })
})
})
describe('tail-leg pallet merge toolbar', () => {
beforeEach(() => {
canAccess.mockImplementation(() => true)
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
vi.mocked(updateLastMileOrderPalletMerge).mockReset().mockResolvedValue(null)
})
it('submits the selected group and cancellation with the customer scope', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const wrapper = await mountView()
const row = { id: '2092873463568596994', customerId, currentStatusCode: 'PENDING_ASSIGNMENT', palletMergeFlag: '' }
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
await selectRows(wrapper, [row])
await wrapper.findAll('button').find(b => b.text() === '合托')!.trigger('click')
await flushPromises()
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, true)
await selectRows(wrapper, [{ ...row, palletMergeFlag: 'REQUIRED' }])
await wrapper.findAll('button').find(b => b.text() === '取消合托')!.trigger('click')
await flushPromises()
expect(updateLastMileOrderPalletMerge).toHaveBeenLastCalledWith([row.id], customerId, false)
})
it('blocks completed and mixed-customer selections and hides unauthorized actions', async () => {
const wrapper = await mountView()
for (const rows of [
[{ id: '1', customerId, currentStatusCode: 'INBOUND_COMPLETED', palletMergeFlag: 'REQUIRED' }],
[{ id: '1', customerId, palletMergeFlag: 'MERGED' }],
[{ id: '1', customerId }, { id: '2', customerId: 'other' }],
]) {
await selectRows(wrapper, rows)
expect(wrapper.findAll('button').find(b => b.text() === '合托')!.attributes('disabled')).toBeDefined()
expect(wrapper.findAll('button').find(b => b.text() === '取消合托')!.attributes('disabled')).toBeDefined()
}
canAccess.mockImplementation(() => false)
const denied = await mountView()
expect(denied.findAll('button').some(b => b.text() === '合托' || b.text() === '取消合托')).toBe(false)
})
})
@@ -16,7 +16,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders'
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerInboundPlanPreview, getContainerPlanProgress, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, reviewDirectRelabelOrder, reviewReturnOrder, transferLastMileOrders, updateLastMileOrderPalletMerge, updateLastMileOrderHold, updateLastMileOrderManualNodes, type ContainerInboundPlanPreviewDto, type ContainerInboundPlanPreviewGroupDto, type ContainerInboundPlanPreviewOrderDto, type ContainerPlanProgressDetailDto, type ContainerPlanProgressSummaryDto, type ManualNodeCode, type LastMileOrderQuery } from '../api/lastMileOrders'
import { businessOrderLink } from '../businessOrderLink'
import OrderReferenceText from '../components/OrderReferenceText.vue'
import { reviewCargoDiscard } from '../api/lastMileOrders'
@@ -64,6 +64,33 @@ function openReturnPackingPlan() {
const exportLoading = ref(false)
const actionLoading = ref(false)
async function exportOrders() { exportLoading.value = true; try { await submitExport('LAST_MILE_ORDER_LIST', '尾程订单', orderQuery()) } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '尾程订单导出失败,请重试').message) } finally { exportLoading.value = false } }
const palletMergeSelectionEditable = computed(() => selectedRows.value.length > 0
&& selectedRows.value.every(row => row.customerId && row.currentStatusCode !== 'INBOUND_COMPLETED' && row.palletMergeFlag !== 'MERGED')
&& new Set(selectedRows.value.map(row => row.customerId)).size === 1)
const canCancelPalletMerge = computed(() => palletMergeSelectionEditable.value
&& selectedRows.value.every(row => row.palletMergeFlag === 'REQUIRED'))
async function submitPalletMerge(required: boolean) {
if (actionLoading.value || !access.can(permission.lastMileOrderPalletMerge)
|| !(required ? palletMergeSelectionEditable.value : canCancelPalletMerge.value)) return
const selection = [...selectedRows.value]
const label = required ? '合托' : '取消合托'
try {
await ElMessageBox.confirm(required
? `将勾选的 ${selection.length} 票订单标记为同一合托组?所选订单须属于同一客户、同一目的仓,PDA 将提示现场合托。`
: `取消勾选的 ${selection.length} 票订单的合托标记和 PDA 提示?`, label,
{ customClass: 'admin-density-confirm', confirmButtonText: `确认${label}`, cancelButtonText: '返回', type: 'warning' })
} catch { return }
actionLoading.value = true
actionError.value = null
try {
await updateLastMileOrderPalletMerge(selection.map(row => row.id), selection[0]!.customerId, required)
ElMessage.success(required ? '已标记合托' : '已取消合托标记')
selectedRows.value = []
await load()
} catch (cause) {
actionError.value = getApiErrorDetails(cause, `${label}失败,请重试`)
} finally { actionLoading.value = false }
}
const actionError = ref<ApiErrorDetails | null>(null)
const actionTarget = ref<LastMileOrderListItem | null>(null)
const inboundPlanOpen = ref(false)
@@ -1108,7 +1135,7 @@ onMounted(() => { void load() })
</div>
</section></template>
<template v-if="statusTab !== '柜子'" #toolbar-actions><el-button v-if="selectedReturnRows.length" type="warning" @click="openReturnPackingPlan"><PackagePlus :size="16" />创建退货合箱计划({{ selectedReturnRows.length }})</el-button><el-button v-if="access.can(permission.lastMileOrderExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || hasSelectedRejectedReturn() || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate) && selectedRows.length === 1 && canGenerateInboundPlan(selectedRows[0])" type="primary" plain :disabled="hasSelectedInboundPlan() || actionLoading" @click="openInboundPlan()">生成入库计划</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
<template v-if="statusTab !== '柜子'" #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!palletMergeSelectionEditable || actionLoading" @click="submitPalletMerge(true)">合托</el-button><el-button v-if="access.can(permission.lastMileOrderPalletMerge)" plain :disabled="!canCancelPalletMerge || actionLoading" @click="submitPalletMerge(false)">取消合托</el-button><el-button v-if="selectedReturnRows.length" type="warning" @click="openReturnPackingPlan"><PackagePlus :size="16" />创建退货合箱计划({{ selectedReturnRows.length }})</el-button><el-button v-if="access.can(permission.lastMileOrderExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || hasSelectedRejectedReturn() || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate) && selectedRows.length === 1 && canGenerateInboundPlan(selectedRows[0])" type="primary" plain :disabled="hasSelectedInboundPlan() || actionLoading" @click="openInboundPlan()">生成入库计划</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
<template #relabelOrderNo="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.relabelNo }}</div><span v-if="!row.relabelApplications?.length">{{ row.relabelOrderNo }}</span></template>
<template #relabelStatus="{ row }"><div v-for="application in row.relabelApplications" :key="application.relabelNo" class="relabel-suborder">{{ application.statusLabel }} · {{ application.quantity }} 件</div><span v-if="!row.relabelApplications?.length">{{ row.relabelStatus }}</span></template>
<template #actions="{ row }"><RowActions text-actions :actions="lastMileRowActions(row as LastMileOrderListItem)" :more-label="`更多尾程订单操作 · ${(row as LastMileOrderListItem).orderNo || '当前记录'}`" @action="handleLastMileRowAction($event, row as LastMileOrderListItem)" /></template>