This commit is contained in:
2026-08-17 19:13:25 +08:00
parent 28f2cd4cc5
commit d618683467
22 changed files with 263 additions and 50 deletions
@@ -71,6 +71,16 @@ describe('InboundFeeDetailManager', () => {
expect(source).toMatch(/size="small" plain type="danger"[^>]+[^>]*>\s*<X/)
})
it('keeps FBA_PRICING rows derived and read-only regardless of write permissions', async () => {
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...approved, sourceType: 'FBA_PRICING' }])
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:inbound:fee-detail:delete', 'firstleg:inbound:fee-detail:review'])
await flushPromises()
const row = wrapper.find('.el-table__row')
expect(row.text()).toContain('头程派生')
const buttons = row.findAll('button').map((button) => button.text())
for (const action of ['编辑', '审核', '驳回', '调整', '作废', '删除']) expect(buttons).not.toContain(action)
})
it('does not request or expose fee rows without view permission', async () => {
mountManager([]); await flushPromises()
expect(listInboundFeeDetails).not.toHaveBeenCalled()
@@ -19,7 +19,8 @@ import type {
} from '@/modules/unified-orders/api/inboundFeeDetails'
const props = defineProps<{ accountId: string; inboundId: string }>()
const emit = defineEmits<{ changed: [] }>()
interface OtherFeeSummary { currency: string; approvedReceivable: number; approvedPayable: number }
const emit = defineEmits<{ changed: []; summary: [value: OtherFeeSummary] }>()
const access = useAccessPreviewStore()
const rows = ref<InboundFeeDetail[]>([])
const feeItems = ref<InboundFeeItemOption[]>([])
@@ -62,18 +63,27 @@ const directionOptions = computed(() => [
])
const applicableItems = computed(() => feeItems.value.filter((item) => form.direction === 'RECEIVABLE' ? item.warehouseReceivable : item.warehousePayable))
function canEdit(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus !== 'APPROVED' }
function canDelete(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:delete') && row.reviewStatus !== 'APPROVED' }
function canReview(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'PENDING' }
function canAdjust(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function canVoid(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function isManual(row: InboundFeeDetail) { return row.sourceType === 'MANUAL' }
function canEdit(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'PENDING' }
function canDelete(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:delete') && row.reviewStatus === 'PENDING' }
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 reviewLabel(value: string) { return ({ PENDING: '待审核', APPROVED: '已审核', REJECTED: '已驳回' } as Record<string, string>)[value] || value }
function directionLabel(value: string) { return value === 'RECEIVABLE' ? '应收' : '应付' }
async function load() {
if (!canView.value) return
loading.value = true; loadError.value = null
try { rows.value = await listInboundFeeDetails(props.accountId, props.inboundId) }
try {
rows.value = await listInboundFeeDetails(props.accountId, props.inboundId)
const approved = rows.value.filter((row) => isManual(row) && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' && row.settlementIncluded)
emit('summary', {
currency: approved.find((row) => row.currency)?.currency || 'CNY',
approvedReceivable: approved.filter((row) => row.direction === 'RECEIVABLE').reduce((sum, row) => sum + Number(row.amount || 0), 0),
approvedPayable: approved.filter((row) => row.direction === 'PAYABLE').reduce((sum, row) => sum + Number(row.amount || 0), 0),
})
}
catch (error) { loadError.value = getApiErrorDetails(error, '费用明细加载失败') }
finally { loading.value = false }
}
@@ -208,6 +218,7 @@ onMounted(() => void load())
</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>
<el-table-column label="应收/应付" width="110"><template #default="{ row }">{{ directionLabel(row.direction) }}</template></el-table-column>
<el-table-column prop="feeTypeNameSnapshot" label="费用类型" min-width="150" />
<el-table-column label="往来单位/客户" min-width="170"><template #default="{ row }">{{ row.direction === 'PAYABLE' ? row.partnerName : row.customerName || '—' }}</template></el-table-column>