This commit is contained in:
2026-09-18 16:48:40 +08:00
parent e10d0534ae
commit 24fa3d1c9d
19 changed files with 286 additions and 210 deletions
@@ -2,12 +2,13 @@
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { FileUp, MoreHorizontal, Plus, RefreshCw, X } from '@lucide/vue'
import { FileUp, Plus, RefreshCw, X } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import {
adjustInboundFeeDetail, createInboundFeeDetail, deleteInboundFeeDetail,
@@ -88,8 +89,23 @@ function canDelete(row: InboundFeeDetail) { return isManual(row) && access.can('
function canReview(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'PENDING' }
function canAdjust(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function canVoid(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function rowActionCount(row: InboundFeeDetail) {
return Number(canEdit(row)) + 2 * Number(canReview(row)) + Number(canAdjust(row)) + Number(canVoid(row)) + Number(canDelete(row))
function feeRowActions(row: InboundFeeDetail): RowActionItem[] {
return [
{ key: 'edit', label: '编辑', priority: 400, visible: canEdit(row) },
{ key: 'approve', label: '审核', priority: 350, tone: 'warning', visible: canReview(row) },
{ key: 'reject', label: '驳回', priority: 300, tone: 'warning', visible: canReview(row) },
{ key: 'adjust', label: '调整', priority: 250, visible: canAdjust(row) },
{ key: 'void', label: '作废', priority: 200, tone: 'danger', visible: canVoid(row) },
{ key: 'delete', label: '删除', tone: 'danger', divided: true, visible: canDelete(row) },
]
}
function handleFeeRowAction(command: string, row: InboundFeeDetail) {
if (command === 'edit') openEditor('edit', row)
else if (command === 'approve') void approve(row)
else if (command === 'reject') openReject(row)
else if (command === 'adjust') openEditor('adjust', row)
else if (command === 'void') void voidRow(row)
else if (command === 'delete') void remove(row)
}
function reviewLabel(value: string) { return ({ PENDING: '待审核', APPROVED: '已审核', REJECTED: '已驳回' } as Record<string, string>)[value] || value }
function publicationLabel(value: string) {
@@ -287,22 +303,7 @@ onMounted(() => void load())
<div v-loading="loading" class="fee-table-shell">
<el-table :data="rows" border stripe empty-text="暂无费用数据" row-key="id">
<el-table-column label="操作" fixed="left" width="250">
<template #default="{ row }">
<div class="density-actions">
<el-button v-if="canEdit(row as InboundFeeDetail)" class="density-action" data-tone="neutral" @click="openEditor('edit', row as InboundFeeDetail)">编辑</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" class="density-action" data-tone="warning" @click="approve(row as InboundFeeDetail)">审核</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" class="density-action" data-tone="warning" @click="openReject(row as InboundFeeDetail)">驳回</el-button>
<el-button v-if="canAdjust(row as InboundFeeDetail)" class="density-action" data-tone="neutral" @click="openEditor('adjust', row as InboundFeeDetail)">调整</el-button>
<el-button v-if="canVoid(row as InboundFeeDetail)" class="density-action" data-tone="danger" @click="voidRow(row as InboundFeeDetail)">作废</el-button>
<el-button v-if="canDelete(row as InboundFeeDetail) && rowActionCount(row as InboundFeeDetail) <= 3" class="density-action" data-tone="danger" @click="remove(row as InboundFeeDetail)">删除</el-button>
<el-dropdown v-if="canDelete(row as InboundFeeDetail) && rowActionCount(row as InboundFeeDetail) > 3" trigger="click">
<el-button class="density-action" data-tone="neutral" :aria-label="(row.feeTypeNameSnapshot || '当前费用') + '的更多操作'" title="更多操作"><MoreHorizontal aria-hidden="true" :size="15" /></el-button>
<template #dropdown><el-dropdown-menu>
<el-dropdown-item @click="remove(row as InboundFeeDetail)">删除</el-dropdown-item>
</el-dropdown-menu></template>
</el-dropdown>
</div>
</template>
<template #default="{ row }"><RowActions :actions="feeRowActions(row as InboundFeeDetail)" :more-label="`${(row as InboundFeeDetail).feeTypeNameSnapshot || '当前费用'}的更多操作`" @action="handleFeeRowAction($event, row as InboundFeeDetail)" /></template>
</el-table-column>
<el-table-column label="审核状态" width="120"><template #default="{ row }"><StatusBadge :status="reviewLabel(row.reviewStatus)" /></template></el-table-column>
<el-table-column label="来源" width="120"><template #default="{ row }">{{ row.sourceType === 'FBA_PRICING' ? '头程派生' : '人工录入' }}</template></el-table-column>
@@ -229,10 +229,9 @@ describe('UnifiedOrdersView contract', () => {
})
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('@click="openDetail(row as UnifiedOrderResource)">详情</el-button>')
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
expect(source).toContain("{ key: 'detail', label: '详情', priority: 500 }")
expect(source).toContain('...unifiedOrderSecondaryActions(row).map')
expect(source).toContain('<RowActions :actions="unifiedOrderRowActions(row as UnifiedOrderResource)"')
})
it('opens smart loading from the order table toolbar with its own permission', () => {
@@ -392,9 +391,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('请至少输入一项需要修改的外箱尺寸')
expect(source).toContain(".map((value) => value == null || value === '' ? null : Number(value))")
expect(source).toContain('相关数值已重新计算')
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
expect(source).toContain(':aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain('handleUnifiedOrderRowAction($event, row as UnifiedOrderResource)')
expect(source).toContain(':more-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain("can('platform:order:note')")
expect(source).toContain('await loadOrders()')
expect(source).toContain('orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus)')
@@ -684,9 +682,9 @@ describe('UnifiedOrdersView contract', () => {
it('uses outlined semantic buttons for row business actions while keeping detail navigation explicit', () => {
const rowActions = source.slice(source.indexOf('<template #actions'), source.indexOf('<template #inboundCode'))
expect(rowActions).toContain('density-action')
expect(rowActions).not.toContain('v-for="action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" link')
expect(rowActions).toContain('@click="openDetail(row as UnifiedOrderResource)">详情</el-button>')
expect(rowActions).toContain('<RowActions')
expect(rowActions).toContain(':actions="unifiedOrderRowActions(row as UnifiedOrderResource)"')
expect(rowActions).toContain('@action="handleUnifiedOrderRowAction($event, row as UnifiedOrderResource)"')
expect(source).toContain('<el-button link class="back-button density-back"')
})
})
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ArrowLeft, ChevronDown, ChevronUp, CircleCheck, Download, FileUp, GitBranch, Link2, MoreHorizontal, PackageOpen, Printer, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2 } from '@lucide/vue'
import { ArrowLeft, ChevronDown, ChevronUp, CircleCheck, Download, FileUp, GitBranch, Link2, PackageOpen, Printer, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2 } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder,
@@ -42,6 +42,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
import {
firstLegBusinessTypeLabel,
firstLegContainerModeLabel,
@@ -2003,12 +2004,6 @@ function unifiedOrderSecondaryActions(row: UnifiedOrderResource): UnifiedOrderRo
...(can('platform:order:note') ? ['note' 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) {
return `更多订单操作:${String(row.inboundCode || '').trim() || '当前入仓单'}`
}
@@ -2017,6 +2012,22 @@ function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
if (action === 'approve') return '通过'
return '测量'
}
function unifiedOrderRowActions(row: UnifiedOrderResource): RowActionItem[] {
return [
{ key: 'detail', label: '详情', priority: 500 },
...unifiedOrderSecondaryActions(row).map((action, index): RowActionItem => ({
key: action,
label: unifiedOrderActionLabel(action),
priority: 400 - index,
tone: action === 'approve' ? 'warning' : action === 'measurement' ? 'purple' : 'neutral',
loading: action === 'approve' && approvalLoadingId.value === String(row.orderId),
})),
]
}
function handleUnifiedOrderRowAction(command: string, row: UnifiedOrderResource) {
if (command === 'detail') openDetail(row)
else runMoreAction(command, row)
}
function runMoreAction(command: string | number | object, row: UnifiedOrderResource) {
if (command === 'note') openNoteDialog(row)
if (command === 'approve') openApproval(row)
@@ -2647,20 +2658,7 @@ onBeforeUnmount(() => {
<el-button v-if="can('platform:firstleg:inbound:view')" :loading="printingCartonLabels" :disabled="loading || printingCartonLabels || !selectedCartonLabelOrders.length" @click="printSelectedCartonLabels"><Printer :size="16" />打印箱唛<span v-if="selectedCartonLabelOrders.length">({{ selectedCartonLabelOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
</template>
<template #actions="{ row }">
<div class="row-actions density-actions">
<el-button class="density-action" size="small" @click="openDetail(row as UnifiedOrderResource)">详情</el-button>
<el-button v-for="action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" class="density-action" :data-tone="action === 'approve' ? 'warning' : action === 'measurement' ? 'purple' : 'neutral'" size="small" :loading="action === 'approve' && approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="runMoreAction(action, row as UnifiedOrderResource)">{{ unifiedOrderActionLabel(action) }}</el-button>
<el-dropdown v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length" trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)">
<el-button class="density-action" size="small" :aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" :title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"><MoreHorizontal :size="17" aria-hidden="true" /></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="action in moreUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" :command="action">{{ unifiedOrderActionLabel(action) }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #actions="{ row }"><RowActions :actions="unifiedOrderRowActions(row as UnifiedOrderResource)" :more-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" @action="handleUnifiedOrderRowAction($event, row as UnifiedOrderResource)" /></template>
<template #inboundCode="{ row }">
<div class="inbound-relation-cell order-code-cell">
<strong class="inbound-code-value" :title="(row as UnifiedOrderResource).inboundCode || '待生成入仓码'">{{ (row as UnifiedOrderResource).inboundCode || '待生成入仓码' }}</strong>