优化优化

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
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { reviewCargoDiscard } from './lastMileOrders'
import { reviewCargoDiscard, updateLastMileOrderPalletMerge } from './lastMileOrders'
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders'
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
@@ -148,3 +148,12 @@ describe('last-mile order overseas API', () => {
)
})
})
it('sets and cancels a merge group with string IDs and explicit customer scope', async () => {
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: null })
for (const required of [true, false]) {
await updateLastMileOrderPalletMerge(['2092873463568596994'], '2092618717861048321', required)
expect(http.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/pallet-merge-required/batch',
{ orderIds: ['2092873463568596994'], palletMergeRequired: required }, { params: { customerId: '2092618717861048321' } })
}
})
@@ -427,6 +427,15 @@ export async function transferLastMileOrders(payload: TransferDestinationPayload
return unwrapApiResponse(response)
}
export async function updateLastMileOrderPalletMerge(orderIds: string[], customerId: string, palletMergeRequired: boolean) {
const response = await http.put<ApiEnvelope<null>, ApiEnvelope<null>>(
'/platform/v1/last-leg/orders/pallet-merge-required/batch',
{ orderIds, palletMergeRequired },
{ params: { customerId } },
)
return unwrapApiResponse(response)
}
export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> {
const response = await http.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>(
'/platform/v1/last-leg/orders/container-summaries',
@@ -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>
@@ -2,6 +2,7 @@ export const overseasWarehousePermissions = {
lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list',
lastMileOrderExport: 'platform:overseas-warehouse:last-mile-order:export',
lastMileOrderTransfer: 'platform:overseas-warehouse:last-mile-order:edit',
lastMileOrderPalletMerge: 'platform:overseas-warehouse:last-mile-order:edit',
warehouseTransferApprove: 'platform:overseas-warehouse:warehouse-transfer:approve',
cargoDiscardApprove: 'lastleg:cargo-discard:approve',
inboundOrderList: 'platform:overseas-warehouse:inbound-order:list',