This commit is contained in:
2026-08-17 17:49:10 +08:00
parent 911a3620e1
commit c7334b2cf8
30 changed files with 1043 additions and 249 deletions
@@ -5,6 +5,7 @@ import {
acceptPlatformFirstLegOrder,
addPlatformFirstLegOrderNote,
approvePlatformFirstLegOrder,
batchApprovePlatformFirstLegOrders,
cancelPlatformFirstLegOrder,
completePlatformFirstLegOrder,
getPlatformFirstLegOrder,
@@ -96,6 +97,22 @@ describe('platform first-leg order API', () => {
expect(http.post).toHaveBeenNthCalledWith(6, '/platform/v1/orders/first-leg/90001/internal-notes', { content: '已联系客户确认' }, { params: { tenantId: '10001' } })
})
it('uses the formal tenant-scoped batch approval contract and preserves long order ids', async () => {
vi.mocked(http.post).mockResolvedValue({
code: '200', message: 'success',
data: { total: 2, successCount: 1, failureCount: 1, successOrderIds: ['2089223263733518337'], failedOrders: [{ orderId: '2089223263733518338', code: 'A0409', message: '订单状态已变化' }] },
})
await expect(batchApprovePlatformFirstLegOrders('2089176869176389634', [
'2089223263733518337', '2089223263733518338',
])).resolves.toMatchObject({ total: 2, successCount: 1, failureCount: 1 })
expect(http.post).toHaveBeenCalledWith(
'/platform/v1/orders/first-leg/batch-approve',
{ orderIds: ['2089223263733518337', '2089223263733518338'] },
{ params: { tenantId: '2089176869176389634' } },
)
})
it('writes the backend express contract as names, not a guessed master-data ID', async () => {
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: null })
@@ -115,6 +115,20 @@ export interface PlatformTenantOrderStatistics {
asOf: string
}
export interface PlatformOrderBatchReviewFailure {
orderId: number | string
code: string
message: string
}
export interface PlatformOrderBatchReviewResult {
total: number
successCount: number
failureCount: number
successOrderIds: Array<number | string>
failedOrders: PlatformOrderBatchReviewFailure[]
}
type PlatformFirstLegOrderPagePayload = Omit<PlatformFirstLegOrderPageResource, 'page' | 'size' | 'total' | 'pages'> & {
page: number | string
size: number | string
@@ -176,6 +190,14 @@ export function approvePlatformFirstLegOrder(tenantId: string, orderId: string)
return postData<void>(`/platform/v1/orders/first-leg/${orderId}/approve`, undefined, { tenantId })
}
export function batchApprovePlatformFirstLegOrders(tenantId: string, orderIds: string[]) {
return postData<PlatformOrderBatchReviewResult>(
'/platform/v1/orders/first-leg/batch-approve',
{ orderIds },
{ tenantId },
)
}
export function acceptPlatformFirstLegOrder(tenantId: string, orderId: string) {
return postData<void>(`/platform/v1/orders/first-leg/${orderId}/accept`, undefined, { tenantId })
}
@@ -34,6 +34,27 @@ describe('PlatformFirstLegOrdersPanel review queue', () => {
expect(source).toContain('await nextTick()')
})
it('supports tenant-aware batch approval with partial failure feedback', () => {
expect(source).toContain("key: 'selection', type: 'selection'")
expect(source).toContain("row.status === 'SUBMITTED' && !batchReviewLoading.value")
expect(source).toContain('@selection-change="handleReviewSelectionChange"')
expect(source).toContain('<template #toolbar-actions>')
expect(source).toContain('批量审核通过')
expect(source).toContain('groupOrdersByTenant(selected)')
expect(source.indexOf('batchReviewLoading.value = true')).toBeLessThan(source.indexOf('await ElMessageBox.confirm('))
expect(source).toContain('batchApprovePlatformFirstLegOrders(')
expect(source).toContain('successCount += result.successCount')
expect(source).toContain('failureCount += result.failureCount')
expect(source).toContain('成功 ${successCount} 个,失败 ${failureCount} 个')
})
it('keeps the operation column first before the batch selection column', () => {
const actionColumn = source.indexOf("{ key: 'actions', label: '操作'")
const selectionColumn = source.indexOf("key: 'selection', type: 'selection'")
expect(actionColumn).toBeGreaterThan(0)
expect(selectionColumn).toBeGreaterThan(actionColumn)
})
it('shows up to three row actions directly and overflows only the fourth action onward', () => {
expect(source).toContain('class="table-actions order-row-actions"')
expect(source).toContain('directSecondaryActionCount')
@@ -8,6 +8,7 @@ import {
acceptPlatformFirstLegOrder,
addPlatformFirstLegOrderNote,
approvePlatformFirstLegOrder,
batchApprovePlatformFirstLegOrders,
cancelPlatformFirstLegOrder,
completePlatformFirstLegOrder,
getPlatformFirstLegOrder,
@@ -40,6 +41,8 @@ const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const actionLoadingId = ref('')
const batchReviewLoading = ref(false)
const selectedReviewOrders = ref<PlatformFirstLegOrderResource[]>([])
const loadError = ref<ApiErrorDetails | null>(null)
const detailLoading = ref(false)
const detail = ref<PlatformFirstLegOrderDetailResource | null>(null)
@@ -96,8 +99,12 @@ const queryFields = computed<QueryField[]>(() => [
{ key: 'dateRange', type: 'daterange', label: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
])
const orderColumns: DataTableColumn[] = [
const orderColumns = computed<DataTableColumn[]>(() => [
{ key: 'actions', label: '操作', minWidth: 288, fixed: 'left', slotName: 'actions' },
...(can('platform:order:approve') ? [{
key: 'selection', type: 'selection', width: 48, fixed: 'left',
selectable: (row: PlatformFirstLegOrderResource) => row.status === 'SUBMITTED' && !batchReviewLoading.value,
} satisfies DataTableColumn] : []),
{ key: 'status', label: '状态', width: 108, slotName: 'status' },
{ key: 'orderInfo', label: '订单信息', minWidth: 210, slotName: 'orderInfo' },
{ key: 'customer', label: '客户', minWidth: 190, showOverflowTooltip: true, slotName: 'customer' },
@@ -105,7 +112,7 @@ const orderColumns: DataTableColumn[] = [
{ key: 'destination', label: '目的地', minWidth: 150, slotName: 'destination' },
{ key: 'receivingWarehouse', label: '平台收货仓', minWidth: 180, slotName: 'receivingWarehouse' },
{ key: 'express', label: '履约快递', minWidth: 160, slotName: 'express' },
]
])
const expressRules: FormRules = {
expressCompany: [{ required: true, message: '请填写快递公司', trigger: 'blur' }],
@@ -270,6 +277,7 @@ function actionLabel(order: PlatformFirstLegOrderResource) {
async function loadOrders() {
loadError.value = null
selectedReviewOrders.value = []
loading.value = true
try {
const result = await listPlatformFirstLegOrders({
@@ -292,6 +300,64 @@ async function loadOrders() {
}
}
function handleReviewSelectionChange(rows: PlatformFirstLegOrderResource[]) {
selectedReviewOrders.value = rows.filter((row) => row.status === 'SUBMITTED')
}
function groupOrdersByTenant(selected: PlatformFirstLegOrderResource[]) {
const groups = new Map<string, PlatformFirstLegOrderResource[]>()
for (const order of selected) {
const tenantId = String(order.tenantId)
groups.set(tenantId, [...(groups.get(tenantId) || []), order])
}
return groups
}
async function batchApproveSelectedOrders() {
const selected = [...selectedReviewOrders.value]
if (!selected.length || batchReviewLoading.value) return
const customerCount = new Set(selected.map((order) => String(order.tenantId))).size
batchReviewLoading.value = true
try {
await ElMessageBox.confirm(
`确认批量审核通过已勾选的 ${selected.length} 个订单吗?涉及 ${customerCount} 个客户,审核成功后订单将进入待测量。`,
'批量审核通过',
{ confirmButtonText: '确认批量审核', cancelButtonText: '取消', type: 'warning' },
)
let successCount = 0
let failureCount = 0
const failureMessages: string[] = []
for (const [tenantId, tenantOrders] of groupOrdersByTenant(selected)) {
try {
const result = await batchApprovePlatformFirstLegOrders(
tenantId,
tenantOrders.map((order) => String(order.id)),
)
successCount += result.successCount
failureCount += result.failureCount
failureMessages.push(...result.failedOrders.map((failure) => failure.message))
} catch (error) {
failureCount += tenantOrders.length
failureMessages.push(getApiErrorDetails(error, '该客户订单批量审核失败').message)
}
}
await loadOrders()
const reasons = [...new Set(failureMessages.filter(Boolean))].slice(0, 3).join(';')
if (failureCount === 0) {
ElMessage.success(`已批量审核通过 ${successCount} 个订单`)
} else if (successCount > 0) {
ElMessage.warning(`批量审核完成:成功 ${successCount} 个,失败 ${failureCount} 个${reasons ? `。${reasons}` : ''}`)
} else {
ElMessage.error(`批量审核失败:${failureCount} 个订单未通过${reasons ? `。${reasons}` : ''}`)
}
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(getApiErrorDetails(error, '批量审核失败,请稍后重试').message)
} finally {
batchReviewLoading.value = false
}
}
function searchOrders() {
if (page.value !== 1) {
page.value = 1
@@ -597,7 +663,17 @@ onMounted(() => void loadTenants())
:page-sizes="[20, 50, 100]"
empty-text="当前没有符合条件的头程订单"
row-key="id"
@selection-change="handleReviewSelectionChange"
>
<template #toolbar-actions>
<el-button
v-if="can('platform:order:approve')"
type="success"
:loading="batchReviewLoading"
:disabled="loading || batchReviewLoading || selectedReviewOrders.length === 0"
@click="batchApproveSelectedOrders"
><CircleCheckBig :size="16" />批量审核通过<span v-if="selectedReviewOrders.length">({{ selectedReviewOrders.length }})</span></el-button>
</template>
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 条订单</span></template>
<template #actions="{ row }">
<div class="table-actions order-row-actions">