This commit is contained in:
2026-08-04 13:32:08 +08:00
parent 6354f5ca66
commit 2192aed125
17 changed files with 711 additions and 354 deletions
@@ -50,6 +50,21 @@ describe('platform first-leg order API', () => {
})
})
it('normalizes string pagination fields returned by the gateway', async () => {
vi.mocked(http.get).mockResolvedValue({
code: '200',
message: 'success',
data: { records: [], page: '1', size: '20', total: '6', pages: '1' },
})
await expect(listPlatformFirstLegOrders({ status: 'SUBMITTED', page: 1, size: 20 })).resolves.toMatchObject({
page: 1,
size: 20,
total: 6,
pages: 1,
})
})
it('uses the platform lifecycle routes and target tenant query parameter', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { order: {} } })
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: null })
@@ -105,6 +105,13 @@ export interface PlatformFirstLegOrderPageResource {
pages: number
}
type PlatformFirstLegOrderPagePayload = Omit<PlatformFirstLegOrderPageResource, 'page' | 'size' | 'total' | 'pages'> & {
page: number | string
size: number | string
total: number | string
pages: number | string
}
export interface PlatformFirstLegOrderQuery {
tenantId?: string
keyword?: string
@@ -127,8 +134,20 @@ async function postData<T>(path: string, body?: unknown, params?: Record<string,
return unwrapApiResponse(response)
}
export function listPlatformFirstLegOrders(query: PlatformFirstLegOrderQuery) {
return getData<PlatformFirstLegOrderPageResource>('/platform/v1/orders/first-leg', { ...query })
function paginationNumber(value: number | string, fallback: number) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : fallback
}
export async function listPlatformFirstLegOrders(query: PlatformFirstLegOrderQuery) {
const result = await getData<PlatformFirstLegOrderPagePayload>('/platform/v1/orders/first-leg', { ...query })
return {
...result,
page: paginationNumber(result.page, query.page),
size: paginationNumber(result.size, query.size),
total: paginationNumber(result.total, 0),
pages: paginationNumber(result.pages, 0),
} satisfies PlatformFirstLegOrderPageResource
}
export function getPlatformFirstLegOrder(tenantId: string, orderId: string) {
@@ -25,6 +25,15 @@ describe('PlatformFirstLegOrdersPanel review queue', () => {
expect(source).toContain('await nextTick()')
})
it('keeps the fixed action column compact by grouping secondary actions', () => {
expect(source).toContain('class="table-actions order-row-actions"')
expect(source).toContain('更多订单操作')
expect(source).toContain('维护快递')
expect(source).toContain('内部备注')
expect(source).toContain('取消订单')
expect(source).toContain("minWidth: 224, fixed: 'left'")
})
it('passes the order tenant through detail and lifecycle actions', () => {
expect(source).toContain('tenantId: String(order.tenantId)')
expect(source).toContain('getPlatformFirstLegOrder(tenantId, orderId)')
@@ -2,7 +2,7 @@
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { ArrowLeft, Eye, FilePenLine, PackageCheck } from '@lucide/vue'
import { ArrowLeft, CircleX, Eye, FilePenLine, MoreHorizontal, PackageCheck } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
acceptPlatformFirstLegOrder,
@@ -90,7 +90,7 @@ const queryFields = computed<QueryField[]>(() => [
])
const orderColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', minWidth: 200, fixed: 'left', slotName: 'actions' },
{ key: 'actions', label: '操作', minWidth: 224, fixed: 'left', slotName: 'actions' },
{ key: 'orderInfo', label: '订单信息', minWidth: 210, slotName: 'orderInfo' },
{ key: 'customer', label: '客户', minWidth: 190, showOverflowTooltip: true, slotName: 'customer' },
{ key: 'transport', label: '运输方案', minWidth: 180, slotName: 'transport' },
@@ -179,6 +179,20 @@ function canCancel(order: PlatformFirstLegOrderResource) {
return can('platform:order:cancel') && ['DRAFT', 'SUBMITTED', 'APPROVED', 'ACCEPTED', 'REJECTED'].includes(order.status)
}
function canMaintainExpress(order: PlatformFirstLegOrderResource) {
return can('platform:order:fulfillment') && !['DRAFT', 'REJECTED', 'CANCELLED'].includes(order.status)
}
function hasSecondaryActions(order: PlatformFirstLegOrderResource) {
return canCancel(order) || canMaintainExpress(order) || can('platform:order:note')
}
function runSecondaryAction(command: string | number | object, order: PlatformFirstLegOrderResource) {
if (command === 'express') openExpress(order)
if (command === 'note') openNote(order)
if (command === 'cancel') void cancelOrder(order)
}
function actionLabel(order: PlatformFirstLegOrderResource) {
if (order.status === 'SUBMITTED' && can('platform:order:approve')) return '审核通过'
if (order.status === 'APPROVED' && can('platform:order:accept')) return '受理订单'
@@ -505,7 +519,22 @@ onMounted(() => void loadTenants())
empty-text="当前没有符合条件的头程订单"
row-key="id"
>
<template #actions="{ row }"><div class="table-actions"><el-button link type="primary" @click="openDetail(row as PlatformFirstLegOrderResource)"><Eye :size="15" />详情</el-button><el-button v-if="actionLabel(row as PlatformFirstLegOrderResource)" link type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)">{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button><el-button v-if="canCancel(row as PlatformFirstLegOrderResource)" link type="danger" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="cancelOrder(row as PlatformFirstLegOrderResource)">取消</el-button><el-button v-if="can('platform:order:fulfillment') && !['DRAFT', 'REJECTED', 'CANCELLED'].includes((row as PlatformFirstLegOrderResource).status)" link type="primary" @click="openExpress(row as PlatformFirstLegOrderResource)"><PackageCheck :size="15" />快递</el-button><el-button v-if="can('platform:order:note')" link type="primary" @click="openNote(row as PlatformFirstLegOrderResource)"><FilePenLine :size="15" />备注</el-button></div></template>
<template #actions="{ row }">
<div class="table-actions order-row-actions">
<el-button link type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openDetail(row as PlatformFirstLegOrderResource)"><Eye :size="15" />详情</el-button>
<el-button v-if="actionLabel(row as PlatformFirstLegOrderResource)" link type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)">{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button>
<el-dropdown v-if="hasSecondaryActions(row as PlatformFirstLegOrderResource)" trigger="click" popper-class="first-leg-action-menu" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @command="runSecondaryAction($event, row as PlatformFirstLegOrderResource)">
<el-button link type="primary" aria-label="更多订单操作"><MoreHorizontal :size="17" />更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canMaintainExpress(row as PlatformFirstLegOrderResource)" command="express"><PackageCheck :size="15" />维护快递</el-dropdown-item>
<el-dropdown-item v-if="can('platform:order:note')" command="note"><FilePenLine :size="15" />内部备注</el-dropdown-item>
<el-dropdown-item v-if="canCancel(row as PlatformFirstLegOrderResource)" command="cancel" divided><CircleX :size="15" />取消订单</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #orderInfo="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).orderNo || '-' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).referenceId || '无客户参考号' }} · {{ formattedDate((row as PlatformFirstLegOrderResource).createdAt) }}</span></div></template>
<template #customer="{ row }"><div class="primary-cell"><strong>{{ tenantName((row as PlatformFirstLegOrderResource).tenantId) }}</strong><span>{{ (row as PlatformFirstLegOrderResource).customerName }} · {{ (row as PlatformFirstLegOrderResource).tenantId }}</span></div></template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ transportLabel(row as PlatformFirstLegOrderResource) }}</strong><span>{{ (row as PlatformFirstLegOrderResource).channelName || '未填写渠道' }}</span></div></template>
@@ -538,6 +567,9 @@ onMounted(() => void loadTenants())
.detail-page-header p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.detail-table-scroll { overflow-x: auto; }
.party-grid p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
.order-row-actions { gap: 8px; white-space: nowrap; }
.order-row-actions :deep(.el-button + .el-button) { margin-left: 0; }
:global(.first-leg-action-menu .el-dropdown-menu__item) { min-height: 44px; gap: 8px; }
@media (max-width: 720px) {
.detail-page-header { align-items: flex-start; flex-direction: column; padding: 16px; }