feat(phase3): complete loading workbenches
This commit is contained in:
@@ -28,7 +28,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('provides labeled prototype-aligned advanced filters backed by real query fields', () => {
|
||||
for (const key of [
|
||||
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'internalOrderStatus', 'expectedArrivalDate',
|
||||
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'expectedArrivalDate',
|
||||
'feeReviewStatus', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded', 'fullContainer',
|
||||
'measurementReviewStatus', 'fbaCode', 'createdRange',
|
||||
]) expect(source).toContain(`key: '${key}'`)
|
||||
@@ -45,9 +45,11 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
|
||||
expect(source).not.toContain("key: 'orderNo'")
|
||||
expect(source.match(/key: 'inboundCode', type: 'input'/g)).toHaveLength(1)
|
||||
expect(source.match(/key: 'internalOrderStatus'/g)).toHaveLength(1)
|
||||
expect(source).not.toContain('internalOrderStatus: filters.value.internalOrderStatus')
|
||||
expect(source).not.toContain("key: 'keyword'")
|
||||
const prototypeFieldOrder = [
|
||||
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'internalOrderStatus', 'expectedArrivalDate',
|
||||
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'expectedArrivalDate',
|
||||
'feeReviewStatus', 'tenantId', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded',
|
||||
'fullContainer', 'measurementReviewStatus', 'fbaCode', 'createdRange',
|
||||
]
|
||||
@@ -56,8 +58,17 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('preference-key="unified-orders-query-v2"')
|
||||
})
|
||||
|
||||
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
|
||||
expect(source).toContain('filters.value.mainStatus || undefined')
|
||||
expect(source).toContain('<template #actions>')
|
||||
expect(source).toContain(':aria-expanded="showAdvancedFilters"')
|
||||
expect(source).not.toContain('class="filter-mode-actions"')
|
||||
expect(source).not.toContain('.filter-mode-actions')
|
||||
expect(source).not.toContain('请选择订单状态')
|
||||
})
|
||||
|
||||
it('covers real list states and keeps operation column first and left-fixed', () => {
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 180, fixed: 'left'")
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
|
||||
expect(source).toContain("{ key: 'selection', type: 'selection', width: 52, fixed: 'left'")
|
||||
expect(source).toContain('批量审核接口待接入')
|
||||
expect(source).toContain('@selection-change="handleSelectionChange"')
|
||||
@@ -74,6 +85,23 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('uses ListPageLayout regions for one 16px query-to-table gap', () => {
|
||||
expect(source).toContain('<ListPageLayout ariaLabel="统一订单列表">')
|
||||
expect(source).toContain('<template #toolbar>')
|
||||
expect(source).toContain('<template v-if="loadError" #state>')
|
||||
expect(source).toContain('<template #default>')
|
||||
expect(source).toContain('v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card"')
|
||||
expect(source).not.toContain('class="orders-load-error"')
|
||||
expect(source).not.toContain('.orders-load-error { margin-top: 0; }')
|
||||
})
|
||||
|
||||
it('counts detail in the three directly visible row actions', () => {
|
||||
expect(source).toContain('unifiedOrderSecondaryActions(row).slice(0, 2)')
|
||||
expect(source).toContain('unifiedOrderSecondaryActions(row).slice(2)')
|
||||
expect(source).toContain('<Eye :size="15" />详情')
|
||||
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
|
||||
})
|
||||
|
||||
it('keeps only confirmed business actions as explicit boundaries', () => {
|
||||
expect(source).toContain('nextActionBoundary')
|
||||
expect(source).not.toContain('class="action-boundary"')
|
||||
@@ -116,11 +144,20 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('<ChevronDown v-else :size="18" />')
|
||||
expect(source).toContain("{ key: 'girthCm', label: '箱规周长(cm)' }")
|
||||
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
|
||||
expect(source).toContain('aria-label="更多订单操作"')
|
||||
expect(source).toContain(':aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
|
||||
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
|
||||
expect(source).toContain("can('platform:order:note')")
|
||||
expect(source).toContain('await loadOrders()')
|
||||
})
|
||||
|
||||
it('distinguishes overflow action buttons by row and keeps a stable missing-field fallback', () => {
|
||||
expect(source).toContain('function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource)')
|
||||
expect(source).toContain('[compatibleOrder.inboundCode, compatibleOrder.orderNo, compatibleOrder.orderId, compatibleOrder.id]')
|
||||
expect(source).toContain("identifier || '当前订单'")
|
||||
expect(source).not.toContain('aria-label="更多订单操作"')
|
||||
expect(source).not.toContain('title="更多操作"')
|
||||
})
|
||||
|
||||
it('renders order detail as a routed full page instead of a drawer', () => {
|
||||
expect(source).toContain('<template v-if="orderId">')
|
||||
expect(source).toContain('class="order-detail-header"')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -42,7 +42,6 @@ type FilterModel = QueryModel & {
|
||||
receivingWarehouseId: string
|
||||
destinationWarehouseId: string
|
||||
inboundCode: string
|
||||
internalOrderStatus: string
|
||||
expectedArrivalDate: string
|
||||
feeReviewStatus: string
|
||||
channelName: string
|
||||
@@ -140,10 +139,6 @@ const primaryQueryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'receivingWarehouseId', type: 'select', width: 220, label: '定位仓库', placeholder: warehouseLoadError.value ? '仓库加载失败' : '请选择定位仓库', options: receivingWarehouseOptions.value, props: { 'aria-label': '定位仓库', filterable: true, loading: warehouseLoading.value, disabled: Boolean(warehouseLoadError.value) } },
|
||||
{ key: 'destinationWarehouseId', type: 'select', width: 220, label: '交货仓', placeholder: warehouseLoadError.value ? '仓库加载失败' : '请选择交货仓', options: warehouseOptions.value, props: { 'aria-label': '交货仓', filterable: true, loading: warehouseLoading.value, disabled: Boolean(warehouseLoadError.value) } },
|
||||
{ key: 'inboundCode', type: 'input', width: 220, label: '入仓码', placeholder: '请输入入仓码', props: { 'aria-label': '入仓码' } },
|
||||
{ key: 'internalOrderStatus', type: 'select', width: 190, label: '订单状态', placeholder: '请选择订单状态', options: [
|
||||
{ value: 'SUBMITTED', label: '待审核' }, { value: 'APPROVED', label: '已审核' },
|
||||
{ value: 'ACCEPTED', label: '已受理' }, { value: 'PROCESSING', label: '处理中' }, { value: 'COMPLETED', label: '已完成' },
|
||||
], props: { 'aria-label': '订单状态' } },
|
||||
])
|
||||
|
||||
const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
@@ -184,7 +179,7 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
: primaryQueryFields.value)
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 52, fixed: 'left', align: 'center', selectable: canBulkReview },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'orderStatus' },
|
||||
@@ -243,7 +238,7 @@ const columns: DataTableColumn[] = [
|
||||
function emptyFilters(): FilterModel {
|
||||
return {
|
||||
tenantId: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
|
||||
inboundCode: '', internalOrderStatus: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
|
||||
inboundCode: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
|
||||
containerNo: '', cabinetNo: '', sailingDate: '', loaded: '', fullContainer: '', measurementReviewStatus: '',
|
||||
fbaCode: '', createdRange: [],
|
||||
}
|
||||
@@ -634,7 +629,6 @@ async function loadOrders() {
|
||||
receivingWarehouseId: filters.value.receivingWarehouseId || undefined,
|
||||
destinationWarehouseId: filters.value.destinationWarehouseId || undefined,
|
||||
inboundCode: trimmed(filters.value.inboundCode),
|
||||
internalOrderStatus: filters.value.internalOrderStatus || undefined,
|
||||
expectedArrivalDate: filters.value.expectedArrivalDate || undefined,
|
||||
feeReviewStatus: filters.value.feeReviewStatus || undefined,
|
||||
channelName: trimmed(filters.value.channelName),
|
||||
@@ -697,6 +691,40 @@ function openInboundFromDetail() {
|
||||
})
|
||||
}
|
||||
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
|
||||
const unifiedOrderRowActionIcons: Record<UnifiedOrderRowAction, Component> = {
|
||||
note: StickyNote,
|
||||
approve: CircleCheck,
|
||||
measurement: Ruler,
|
||||
}
|
||||
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:view') ? ['measurement' as const] : []),
|
||||
]
|
||||
}
|
||||
function directUnifiedOrderActions(row: UnifiedOrderResource) {
|
||||
return unifiedOrderSecondaryActions(row).slice(0, 2)
|
||||
}
|
||||
function moreUnifiedOrderActions(row: UnifiedOrderResource) {
|
||||
return unifiedOrderSecondaryActions(row).slice(2)
|
||||
}
|
||||
function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
|
||||
const compatibleOrder = row as UnifiedOrderResource & { id?: string | number | null }
|
||||
const identifier = [compatibleOrder.inboundCode, compatibleOrder.orderNo, compatibleOrder.orderId, compatibleOrder.id]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find(Boolean)
|
||||
return `更多订单操作:${identifier || '当前订单'}`
|
||||
}
|
||||
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
||||
if (action === 'note') return '添加备注'
|
||||
if (action === 'approve') return '审核通过'
|
||||
return '测量作业'
|
||||
}
|
||||
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
|
||||
return action === 'approve' ? 'success' : 'primary'
|
||||
}
|
||||
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
|
||||
if (command === 'note') openNoteDialog(row)
|
||||
if (command === 'approve') openApproval(row)
|
||||
@@ -982,6 +1010,7 @@ onMounted(() => {
|
||||
|
||||
<template v-else>
|
||||
<ListPageLayout ariaLabel="统一订单列表">
|
||||
<template #toolbar>
|
||||
<div class="orders-query-panel">
|
||||
<div class="status-tabs" role="tablist" aria-label="订单主状态">
|
||||
<button
|
||||
@@ -998,20 +1027,25 @@ onMounted(() => {
|
||||
>{{ tab.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-mode-actions">
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset" />
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<template #actions>
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
|
||||
</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
</div>
|
||||
<LoadErrorState v-if="loadError" class="orders-load-error" title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders">
|
||||
</template>
|
||||
|
||||
<template v-if="loadError" #state>
|
||||
<LoadErrorState title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders">
|
||||
<template #details><ApiErrorMeta :details="loadError" /></template>
|
||||
</LoadErrorState>
|
||||
</template>
|
||||
|
||||
<div v-else id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
|
||||
<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" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleSelectionChange">
|
||||
<template #toolbar-actions>
|
||||
<el-button type="primary" plain :disabled="selectedOrders.length === 0" title="批量审核接口待接入" @click="openBulkReviewPlaceholder"><ListChecks :size="16" />批量审核<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||
@@ -1019,14 +1053,12 @@ onMounted(() => {
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as UnifiedOrderResource)"><Eye :size="15" />详情</el-button>
|
||||
<el-dropdown trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)">
|
||||
<el-button link type="primary" aria-label="更多订单操作" title="更多操作"><MoreHorizontal :size="17" /></el-button>
|
||||
<el-button v-for="action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" link :type="unifiedOrderActionType(action)" :loading="action === 'approve' && approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="runMoreAction(action, row as UnifiedOrderResource)"><component :is="unifiedOrderRowActionIcons[action]" :size="15" />{{ unifiedOrderActionLabel(action) }}</el-button>
|
||||
<el-dropdown v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length" trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)">
|
||||
<el-button link type="primary" :aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" :title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"><MoreHorizontal :size="17" /></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="can('platform:order:note')" command="note"><StickyNote :size="15" />添加备注</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canApprove(row as UnifiedOrderResource)" command="approve"><CircleCheck :size="15" />审核通过</el-dropdown-item>
|
||||
<el-dropdown-item v-if="(row as UnifiedOrderResource).mainStatus === 'WAIT_MEASURE' && (row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view')" command="measurement">测量作业</el-dropdown-item>
|
||||
<el-dropdown-item v-if="!can('platform:order:note') && !canApprove(row as UnifiedOrderResource) && !((row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view'))" disabled>暂无可用操作</el-dropdown-item>
|
||||
<el-dropdown-item v-for="action in moreUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" :command="action"><component :is="unifiedOrderRowActionIcons[action]" :size="15" />{{ unifiedOrderActionLabel(action) }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -1047,6 +1079,7 @@ onMounted(() => {
|
||||
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 条</span></template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
</ListPageLayout>
|
||||
</template>
|
||||
|
||||
@@ -1070,7 +1103,6 @@ onMounted(() => {
|
||||
.page-intro :deep(.el-button) { flex: 0 0 auto; }
|
||||
.orders-query-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.status-tabs { display: flex; max-width: 100%; overflow-x: auto; margin-bottom: 0; border-bottom: 1px solid var(--cc-border); scrollbar-width: thin; }
|
||||
.filter-mode-actions { display: flex; justify-content: flex-end; padding: 12px 16px 0; }
|
||||
.status-tab { position: relative; flex: 0 0 auto; min-height: 44px; padding: 0 18px; border: 0; background: transparent; color: var(--cc-muted); font: inherit; font-size: 14px; cursor: pointer; }
|
||||
.status-tab::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 3px; background: transparent; content: ''; }
|
||||
.status-tab:hover { color: var(--cc-primary); background: #f6f9fd; }
|
||||
@@ -1078,7 +1110,6 @@ onMounted(() => {
|
||||
.status-tab.is-active { color: var(--cc-primary); font-weight: 600; }
|
||||
.status-tab.is-active::after { background: var(--cc-primary); }
|
||||
.orders-query-toolbar { border: 0; }
|
||||
.orders-load-error { margin-top: 0; }
|
||||
.table-card { overflow: hidden; }
|
||||
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.row-actions :deep(.el-button) { margin-left: 0; }
|
||||
|
||||
Reference in New Issue
Block a user