优化
This commit is contained in:
@@ -107,15 +107,13 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
|
||||
it('keeps high-frequency account actions visible and groups the remaining actions', () => {
|
||||
expect(page).toContain("{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' }")
|
||||
expect(page).toContain('@click="openAccount(row, false)">详情</el-button>')
|
||||
expect(page).toContain("openAdjustment(row as FinanceAccountSummary, 'RECHARGE')")
|
||||
expect(page).toContain("openAdjustment(row as FinanceAccountSummary, 'DEDUCTION')")
|
||||
expect(page).toContain('aria-label="更多客户账户操作"')
|
||||
expect(page).toContain('<el-dropdown-item v-if="canAdjust" command="credit">授信</el-dropdown-item>')
|
||||
expect(page).toContain('<el-dropdown-item v-if="canUpdateTerms" command="terms" :divided="canAdjust">账期</el-dropdown-item>')
|
||||
expect(page).not.toContain('<el-button link type="primary" @click="openAccount(row, false)">')
|
||||
expect(page).toContain('class="row-actions density-actions"')
|
||||
expect(page).toContain('white-space: nowrap')
|
||||
expect(page).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'")
|
||||
expect(page).toContain("{ key: 'detail', label: '详情', priority: 300 }")
|
||||
expect(page).toContain("{ key: 'recharge', label: '充值', priority: 250")
|
||||
expect(page).toContain("{ key: 'deduction', label: '扣款', priority: 200")
|
||||
expect(page).toContain("{ key: 'credit', label: '授信', priority: 100")
|
||||
expect(page).toContain("{ key: 'terms', label: '账期', visible: canUpdateTerms.value")
|
||||
expect(page).toContain('<RowActions :actions="accountActions(row as FinanceAccountSummary)"')
|
||||
})
|
||||
|
||||
it('shows authoritative CNY wallet balances and independently permitted actions', () => {
|
||||
@@ -134,7 +132,7 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels)
|
||||
expect(actions.find('.el-dropdown').exists()).toBe(overflow)
|
||||
if (overflow) {
|
||||
const trigger = actions.get('button[aria-label="更多客户账户操作"]')
|
||||
const trigger = actions.get('button[aria-label^="更多客户账户操作"]')
|
||||
expect(trigger.text()).toBe('')
|
||||
expect(trigger.find('svg').exists()).toBe(true)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, CreditCard, Download, MinusCircle, MoreHorizontal, PlusCircle, Save } from '@lucide/vue'
|
||||
import { ArrowLeft, CreditCard, Download, MinusCircle, PlusCircle, Save } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
@@ -18,6 +18,7 @@ import FormSection from '@/shared/components/FormSection.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { adjustFinanceAccount, downloadFinanceRechargeVoucher, getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest, updateFinanceCreditLimit, updateFinanceCustomerTerms } from '../api/finance'
|
||||
import { financeBusinessTypeLabel } from '../status'
|
||||
@@ -270,6 +271,21 @@ function handleAccountMore(command: string, account: FinanceAccountSummary) {
|
||||
if (command === 'credit') openCreditLimit(account)
|
||||
if (command === 'terms') void openAccount(account as unknown as Record<string, unknown>, true)
|
||||
}
|
||||
function accountActions(account: FinanceAccountSummary): RowActionItem[] {
|
||||
return [
|
||||
{ key: 'detail', label: '详情', priority: 300 },
|
||||
{ key: 'recharge', label: '充值', priority: 250, visible: canAdjust.value },
|
||||
{ key: 'deduction', label: '扣款', priority: 200, tone: 'danger', visible: canAdjust.value },
|
||||
{ key: 'credit', label: '授信', priority: 100, visible: canAdjust.value },
|
||||
{ key: 'terms', label: '账期', visible: canUpdateTerms.value, divided: canAdjust.value },
|
||||
]
|
||||
}
|
||||
function handleAccountAction(command: string, account: FinanceAccountSummary) {
|
||||
if (command === 'detail') void openAccount(account as unknown as Record<string, unknown>, false)
|
||||
else if (command === 'recharge') openAdjustment(account, 'RECHARGE')
|
||||
else if (command === 'deduction') openAdjustment(account, 'DEDUCTION')
|
||||
else handleAccountMore(command, account)
|
||||
}
|
||||
async function submitCreditLimit() {
|
||||
if (!adjustmentAccount.value) return
|
||||
const raw = creditLimitForm.amount.trim()
|
||||
@@ -425,7 +441,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
<section v-if="!stateError" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="finance-customer-accounts-table" aria-label="客户账户列表" row-key="customerId" empty-text="暂无客户账户" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="canExport" :loading="accountExportBusy" :disabled="loading || accountExportBusy" @click="exportAccounts"><Download :size="16" />导出Excel</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions"><el-button data-tone="neutral" class="density-action" size="small" @click="openAccount(row, false)">详情</el-button><el-button data-tone="neutral" class="density-action" v-if="canAdjust" size="small" @click="openAdjustment(row as FinanceAccountSummary, 'RECHARGE')">充值</el-button><el-button data-tone="danger" class="density-action" v-if="canAdjust" size="small" @click="openAdjustment(row as FinanceAccountSummary, 'DEDUCTION')">扣款</el-button><el-button class="density-action" data-tone="neutral" v-if="!canAdjust && canUpdateTerms" size="small" @click="handleAccountMore('terms', row as FinanceAccountSummary)">账期</el-button><el-dropdown v-if="canAdjust" trigger="click" @command="handleAccountMore(String($event), row as FinanceAccountSummary)"><el-button data-tone="neutral" class="density-action" size="small" aria-label="更多客户账户操作" title="更多客户账户操作"><MoreHorizontal aria-hidden="true" :size="15" /></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item v-if="canAdjust" command="credit">授信</el-dropdown-item><el-dropdown-item v-if="canUpdateTerms" command="terms" :divided="canAdjust">账期</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></template>
|
||||
<template #actions="{ row }"><RowActions :actions="accountActions(row as FinanceAccountSummary)" :more-label="`更多客户账户操作 · ${(row as FinanceAccountSummary).customerName || '当前客户'}`" @action="handleAccountAction($event, row as FinanceAccountSummary)" /></template>
|
||||
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template>
|
||||
<template #customerCode="{ row }">{{ customerField(row.customerCode) }}</template><template #contactName="{ row }">{{ customerField(row.contactName) }}</template><template #contactPhone="{ row }">{{ customerField(row.contactPhone) }}</template>
|
||||
<template #lastChangedAt="{ row }">{{ formatLastChangedAt(row.lastChangedAt) }}</template>
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('FinanceCustomerArapView G2 operations', () => {
|
||||
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels)
|
||||
expect(actions.find('.el-dropdown').exists()).toBe(overflow)
|
||||
if (overflow) {
|
||||
const trigger = actions.get('button[title="更多费用来源操作"]')
|
||||
const trigger = actions.get('button[title^="更多费用来源操作"]')
|
||||
expect(trigger.text()).toBe('')
|
||||
await trigger.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -268,18 +268,18 @@ describe('FinanceCustomerArapView G2 operations', () => {
|
||||
expect(page).not.toContain('max-width: 520px')
|
||||
})
|
||||
|
||||
it('renders permitted fee-source row operations as shared outlined text buttons', () => {
|
||||
it('renders permitted fee-source row operations through the shared row-actions component', () => {
|
||||
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8')
|
||||
expect(page).toContain('@click="openDetail(row)">详情</el-button>')
|
||||
expect(page).toContain("@click=\"openMutation(row, 'SUBMIT')\"")
|
||||
expect(page).toContain("@click=\"openMutation(row, 'ADJUST')\"")
|
||||
expect(page).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'")
|
||||
expect(page).toContain("{ key: 'detail', label: '详情', priority: 300")
|
||||
expect(page).toContain("{ key: 'submit', label: '提交审核', priority: 200")
|
||||
expect(page).toContain("{ key: 'adjust', label: '调整', priority: 150")
|
||||
expect(page).toContain("{ key: 'void', label: '作废', tone: 'danger'")
|
||||
expect(page).toContain('!isUpstreamSource(row)')
|
||||
expect(page).toContain('!isUpstreamSource(detail)')
|
||||
expect(page).toContain("['INBOUND_FEE', 'CONTAINER_FEE'].includes(type)")
|
||||
expect(page).toContain("=== 'LASTLEG'")
|
||||
const actions = page.slice(page.indexOf('<template #actions'), page.indexOf('<template #customerName'))
|
||||
expect(actions).not.toMatch(/<el-button[^>]*\blink\b/)
|
||||
expect(page).toContain('white-space: nowrap')
|
||||
expect(page).toContain('<RowActions v-if="row.feeSourceId" :actions="sourceActions(row as ArapRow)"')
|
||||
})
|
||||
|
||||
it('resolves blank detail customer names and keeps source labels business-facing', async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { MoreHorizontal, ArrowLeft, Download, FileCheck2, FilePlus2, Pencil, Send } from '@lucide/vue'
|
||||
import { ArrowLeft, Download, FileCheck2, FilePlus2, Pencil, Send } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
@@ -20,6 +20,7 @@ import FormSection from '@/shared/components/FormSection.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { adjustFinanceFeeSource, editFinanceFeeSource, getFinanceFeeSource, listFinanceFeeSources, reviewFinanceFeeSource, submitFinanceFeeSource, voidFinanceFeeSource } from '../api/finance'
|
||||
import { financeArapStatusOptions, financeDirectionLabel, financeOperationActionLabel, financeOperationResultLabel, financeSourceTypeLabel, financeStatusLabel } from '../status'
|
||||
@@ -136,18 +137,6 @@ function closeCustomerSelectOnScroll() {
|
||||
const active = document.activeElement
|
||||
if (active instanceof HTMLElement) active.blur()
|
||||
}
|
||||
function sourceActionCount(row: Record<string, unknown>) {
|
||||
if (!row.feeSourceId) return 0
|
||||
const available = row.status === 'AVAILABLE'
|
||||
const draft = ['DRAFT', 'REJECTED'].includes(String(row.reviewStatus))
|
||||
const editable = canEditSource.value && !isUpstreamSource(row)
|
||||
return Number(canViewSource.value)
|
||||
+ Number(editable && draft && available)
|
||||
+ Number(canEditSource.value && draft && available)
|
||||
+ Number(canReviewSource.value && row.reviewStatus === 'PENDING' && available)
|
||||
+ Number(editable && row.reviewStatus === 'APPROVED' && available && !row.statementGenerated)
|
||||
+ Number(editable && available && !row.statementGenerated)
|
||||
}
|
||||
function isUpstreamSource(source: { sourceSystem?: unknown; sourceType?: unknown }) {
|
||||
return isFirstlegSource(source) || String(source.sourceSystem || '').trim().toUpperCase() === 'LASTLEG'
|
||||
}
|
||||
@@ -258,14 +247,13 @@ async function loadDetail(row: ArapRow) {
|
||||
catch (error) { detailError.value = getApiErrorDetails(error, '费用来源详情加载失败,请重试'); return null }
|
||||
finally { detailLoading.value = false }
|
||||
}
|
||||
async function openDetail(row: Record<string, unknown>) {
|
||||
const target = row as unknown as ArapRow
|
||||
if (!target.feeSourceId) return
|
||||
await router.push({ name: 'finance-customer-arap-detail', params: { feeSourceId: target.feeSourceId } })
|
||||
async function openDetail(row: ArapRow) {
|
||||
if (!row.feeSourceId) return
|
||||
await router.push({ name: 'finance-customer-arap-detail', params: { feeSourceId: row.feeSourceId } })
|
||||
}
|
||||
async function openReview(row: Record<string, unknown>) {
|
||||
const target = row as unknown as ArapRow; selected.value = target
|
||||
const loaded = await loadDetail(target)
|
||||
async function openReview(row: ArapRow) {
|
||||
selected.value = row
|
||||
const loaded = await loadDetail(row)
|
||||
if (!loaded) { detailOpen.value = true; return }
|
||||
Object.assign(reviewForm, { action: 'APPROVE', reason: '' }); reviewError.value = null; reviewOpen.value = true
|
||||
}
|
||||
@@ -279,18 +267,39 @@ async function submitReview() {
|
||||
} catch (error) { reviewError.value = getApiErrorDetails(error, '费用来源审核失败,请重试') }
|
||||
finally { reviewSaving.value = false }
|
||||
}
|
||||
async function openMutation(row: Record<string, unknown>, mode: 'EDIT' | 'ADJUST' | 'SUBMIT' | 'VOID') {
|
||||
const target = row as unknown as ArapRow; selected.value = target
|
||||
if ((mode === 'EDIT' || mode === 'ADJUST' || mode === 'VOID') && isUpstreamSource(target)) {
|
||||
ElMessage.info(String(target.sourceSystem || '').toUpperCase() === 'LASTLEG'
|
||||
async function openMutation(row: ArapRow, mode: 'EDIT' | 'ADJUST' | 'SUBMIT' | 'VOID') {
|
||||
selected.value = row
|
||||
if ((mode === 'EDIT' || mode === 'ADJUST' || mode === 'VOID') && isUpstreamSource(row)) {
|
||||
ElMessage.info(String(row.sourceSystem || '').toUpperCase() === 'LASTLEG'
|
||||
? '请到对应海外业务费用中调整或作废,审核后会通过跨区事件同步到财务'
|
||||
: '请到订单入库费用或柜次费用明细中调整或作废,审核后会自动同步到财务')
|
||||
return
|
||||
}
|
||||
const loaded = await loadDetail(target)
|
||||
const loaded = await loadDetail(row)
|
||||
if (!loaded) { detailOpen.value = true; return }
|
||||
mutationMode.value = mode; Object.assign(mutationForm, { amount: loaded.amount, reason: '' }); mutationError.value = null; mutationOpen.value = true
|
||||
}
|
||||
function sourceActions(row: ArapRow): RowActionItem[] {
|
||||
const available = row.status === 'AVAILABLE'
|
||||
const draft = ['DRAFT', 'REJECTED'].includes(row.reviewStatus)
|
||||
const editable = canEditSource.value && !isUpstreamSource(row)
|
||||
return [
|
||||
{ key: 'detail', label: '详情', priority: 300, visible: canViewSource.value },
|
||||
{ key: 'edit', label: '编辑', priority: 250, visible: editable && draft && available },
|
||||
{ key: 'submit', label: '提交审核', priority: 200, tone: 'warning', visible: canEditSource.value && draft && available },
|
||||
{ key: 'review', label: '审核', priority: 200, tone: 'warning', visible: canReviewSource.value && row.reviewStatus === 'PENDING' && available },
|
||||
{ key: 'adjust', label: '调整', priority: 150, visible: editable && row.reviewStatus === 'APPROVED' && available && !row.statementGenerated },
|
||||
{ key: 'void', label: '作废', tone: 'danger', divided: true, visible: editable && available && !row.statementGenerated },
|
||||
]
|
||||
}
|
||||
function handleSourceAction(command: string, row: ArapRow) {
|
||||
if (command === 'detail') void openDetail(row)
|
||||
else if (command === 'review') void openReview(row)
|
||||
else if (command === 'edit') void openMutation(row, 'EDIT')
|
||||
else if (command === 'submit') void openMutation(row, 'SUBMIT')
|
||||
else if (command === 'adjust') void openMutation(row, 'ADJUST')
|
||||
else if (command === 'void') void openMutation(row, 'VOID')
|
||||
}
|
||||
async function submitMutation() {
|
||||
if (!detail.value) return
|
||||
if (!mutationForm.reason.trim()) { ElMessage.warning('请填写操作原因'); return }
|
||||
@@ -396,7 +405,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<section v-if="!customersError && !loadError" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" max-height="calc(100vh - 300px)" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><div class="generation-actions"><el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportFeeSources"><Download :size="16" />导出Excel</el-button><el-button v-if="canCreateStatement" type="primary" :disabled="!selectedRows.length" @click="continueSelectedGeneration"><FilePlus2 :size="16" />生成所选对账单</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions"><template v-if="row.feeSourceId"><el-button data-tone="neutral" class="density-action" v-if="canViewSource" size="small" @click="openDetail(row)">详情</el-button><el-button data-tone="neutral" class="density-action" v-if="canEditSource && !isUpstreamSource(row) && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" @click="openMutation(row, 'EDIT')">编辑</el-button><el-button data-tone="warning" class="density-action" v-if="canEditSource && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" @click="openMutation(row, 'SUBMIT')">提交审核</el-button><el-button data-tone="warning" class="density-action" v-if="canReviewSource && row.reviewStatus === 'PENDING' && row.status === 'AVAILABLE'" size="small" @click="openReview(row)">审核</el-button><el-button data-tone="neutral" class="density-action" v-if="canEditSource && !isUpstreamSource(row) && row.reviewStatus === 'APPROVED' && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" @click="openMutation(row, 'ADJUST')">调整</el-button><el-button data-tone="danger" class="density-action" v-if="canEditSource && !isUpstreamSource(row) && row.status === 'AVAILABLE' && !row.statementGenerated && sourceActionCount(row) <= 3" size="small" @click="openMutation(row, 'VOID')">作废</el-button><el-dropdown v-if="canEditSource && !isUpstreamSource(row) && row.status === 'AVAILABLE' && !row.statementGenerated && sourceActionCount(row) > 3" trigger="click"><el-button class="density-action" data-tone="neutral" size="small" title="更多费用来源操作" :aria-label="`更多费用来源操作 · ${row.documentNo || '当前费用'}`"><MoreHorizontal :size="15" aria-hidden="true" /></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item class="danger-menu-item" @click="openMutation(row, 'VOID')">作废</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template><span v-else class="muted-action">手工成本</span></div></template>
|
||||
<template #actions="{ row }"><RowActions v-if="row.feeSourceId" :actions="sourceActions(row as ArapRow)" :more-label="`更多费用来源操作 · ${(row as ArapRow).documentNo || '当前费用'}`" @action="handleSourceAction($event, row as ArapRow)" /><span v-else class="muted-action">手工成本</span></template>
|
||||
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template><template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : row.direction === 'PAYABLE' ? 'warning' : 'info'">{{ financeDirectionLabel(row.direction) }}</el-tag></template>
|
||||
<template #businessReference="{ row }">{{ businessReference(row as unknown as ArapRow) }}</template><template #sourceType="{ row }">{{ sourceTypeDisplay(row.sourceType) }}</template><template #supplierName="{ row }">{{ supplierDisplay(row as unknown as ArapRow) }}</template><template #sourceStatus="{ row }">{{ sourceStatusDisplay(row.status) }}</template>
|
||||
<template #status="{ row }"><el-tag :type="row.reviewStatus === 'APPROVED' ? 'success' : row.reviewStatus === 'REJECTED' ? 'danger' : 'warning'">{{ statusLabel(row.reviewStatus) }}</el-tag></template>
|
||||
@@ -407,7 +416,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<AdminPage v-else as="div" class="finance-arap-detail admin-density" spacing="normal">
|
||||
<section class="surface-panel finance-arap-detail__surface">
|
||||
<DetailPageLayout class="finance-arap-detail__layout finance-density-detail" aria-label="费用来源详情">
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><h1 class="density-detail-title">{{ selected?.documentNo || '费用来源详情' }}</h1><span class="finance-detail-context">{{ selected ? customerName(selected.customerId, selected.customerName) : '—' }}</span><StatusBadge v-if="detail" :status="statusLabel(detail.reviewStatus)" /></div><div class="finance-detail-actions"><el-button v-if="canEditSource && detail && !isUpstreamSource(detail) && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'EDIT')"><Pencil :size="16" />编辑</el-button><el-button v-if="canEditSource && detail && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'SUBMIT')"><Send :size="16" />提交审核</el-button><el-button v-if="canReviewSource && detail?.reviewStatus === 'PENDING' && selected" @click="openReview(selected as unknown as Record<string, unknown>)"><FileCheck2 :size="16" />审核</el-button></div></div></template>
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><h1 class="density-detail-title">{{ selected?.documentNo || '费用来源详情' }}</h1><span class="finance-detail-context">{{ selected ? customerName(selected.customerId, selected.customerName) : '—' }}</span><StatusBadge v-if="detail" :status="statusLabel(detail.reviewStatus)" /></div><div class="finance-detail-actions"><el-button v-if="canEditSource && detail && selected && !isUpstreamSource(detail) && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected, 'EDIT')"><Pencil :size="16" />编辑</el-button><el-button v-if="canEditSource && detail && selected && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected, 'SUBMIT')"><Send :size="16" />提交审核</el-button><el-button v-if="canReviewSource && detail?.reviewStatus === 'PENDING' && selected" @click="openReview(selected)"><FileCheck2 :size="16" />审核</el-button></div></div></template>
|
||||
<template v-if="detail" #summary><dl class="density-detail-overview arap-metrics" aria-label="费用来源概览"><div class="arap-metric is-amount"><dt>结算金额</dt><dd>{{ money(detail.amount) }} <small>CNY</small></dd></div><div class="arap-metric"><dt>收付方向</dt><dd>{{ financeDirectionLabel(detail.direction) }}</dd></div><div class="arap-metric"><dt>来源状态</dt><dd>{{ sourceStatusDisplay(detail.sourceStatus) }}</dd></div><div class="arap-metric"><dt>审核状态</dt><dd>{{ statusLabel(detail.reviewStatus) }}</dd></div></dl></template>
|
||||
<LoadErrorState v-if="detailError" title="费用来源详情加载失败" :message="detailError.message" :loading="detailLoading" @retry="selected && loadDetail(selected)"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information density-form-section" title="单据信息" :bordered="false"><el-descriptions border :column="2" class="arap-detail density-descriptions"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联业务">{{ detail.inboundCode || detail.containerNo || detail.sourceBusinessId || '业务关联待补充' }}</el-descriptions-item><el-descriptions-item v-if="detail.direction === 'PAYABLE'" label="供应商">{{ detail.supplierName || '供应商待补充' }}</el-descriptions-item><el-descriptions-item label="业务日期">{{ formatBusinessDate(detail.businessDate) }}</el-descriptions-item><el-descriptions-item label="原币金额">{{ detail.originalCurrency }} {{ money(detail.originalAmount) }}</el-descriptions-item><el-descriptions-item label="折算汇率">{{ detail.exchangeRate }}<template v-if="detail.exchangeRateMonth">({{ detail.exchangeRateMonth }})</template></el-descriptions-item><el-descriptions-item v-if="detail.quantity != null" label="计费依据">{{ detail.quantity }} {{ detail.quantityUnit || '' }}<template v-if="detail.unitPrice != null"> × {{ detail.originalCurrency }} {{ detail.unitPrice }}</template></el-descriptions-item><el-descriptions-item v-if="detail.periodStart" label="计费期间">{{ detail.periodStart }} 至 {{ detail.periodEnd }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement density-form-section" title="审核与结算" :bordered="false"><el-descriptions border :column="1" class="arap-detail density-descriptions"><el-descriptions-item label="费用状态">{{ sourceStatusDisplay(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '未填写' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log density-form-section" title="操作记录" :bordered="false"><DataTable :data="operationLogRows" :columns="operationLogColumns" row-key="rowKey" column-settings-mode="hide" empty-text="暂无操作记录"><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #action="{ row }">{{ financeOperationActionLabel(row.action) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template><template #result="{ row }">{{ row.remark || financeOperationResultLabel(row.result) }}</template></DataTable></FormSection></template>
|
||||
|
||||
@@ -126,20 +126,14 @@ describe('FinanceCustomerStatementsView', () => {
|
||||
expect(moduleCss).toContain('.statement-summary-item--remark { grid-column: span 2; }')
|
||||
})
|
||||
|
||||
it('uses entity buttons for row actions and only folds the fourth action into more', () => {
|
||||
it('uses the shared row-actions component and prioritizes the first three available operations', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerStatementsView.vue'), 'utf8')
|
||||
const actions = source.slice(source.indexOf('<template #actions="{ row }">'), source.indexOf('<template #status="{ row }">'))
|
||||
expect(actions).not.toContain(' link')
|
||||
for (const label of ['详情', '提交', '通过', '驳回', '出账', '调整', '重开']) {
|
||||
const actionLine = actions.split('\n').find((line) => line.includes(`${label}</el-button>`))
|
||||
expect(actionLine).toContain('class="density-action"')
|
||||
expect(actionLine).toContain('size="small"')
|
||||
}
|
||||
expect(actions).toContain('data-tone="danger"')
|
||||
expect(actions).toContain('aria-label="更多对账单操作" title="更多对账单操作"')
|
||||
expect(actions).toContain("row.status === 'PENDING_REVIEW' && canReview && canVoid")
|
||||
expect(actions.match(/<el-dropdown-item/g)).toHaveLength(1)
|
||||
expect(actions).toContain('class="danger-menu-item"')
|
||||
expect(source).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'")
|
||||
expect(source).toContain("{ key: 'detail', label: '详情', priority: 300")
|
||||
expect(source).toContain("{ key: 'approve', label: '通过', priority: 250")
|
||||
expect(source).toContain("{ key: 'reject', label: '驳回', priority: 200")
|
||||
expect(source).toContain("{ key: 'void', label: '作废', tone: 'danger', divided: true")
|
||||
expect(source).toContain('<RowActions :actions="statementActions(row as FinanceStatementReadModel)"')
|
||||
})
|
||||
|
||||
it('keeps three available row actions visible with primary and danger semantics', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import '../financeDensity.css'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Download, MoreHorizontal, Plus } from '@lucide/vue'
|
||||
import { ArrowLeft, Download, Plus } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
@@ -19,6 +19,7 @@ import FormSection from '@/shared/components/FormSection.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { actionReconciliationStatement, createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance'
|
||||
import { financeBillStatusOptions, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
|
||||
@@ -531,6 +532,29 @@ async function openAdjustment(row: FinanceStatementReadModel) {
|
||||
} catch (cause) { error.value = getApiErrorDetails(cause, '调整明细加载失败') }
|
||||
finally { adjustBusy.value = false }
|
||||
}
|
||||
function statementActions(row: FinanceStatementReadModel): RowActionItem[] {
|
||||
return [
|
||||
{ key: 'detail', label: '详情', priority: 300, disabled: loading.value },
|
||||
{ key: 'submit', label: '提交', priority: 250, visible: row.status === 'DRAFT' && canCreate.value, loading: actionBusy.value },
|
||||
{ key: 'approve', label: '通过', priority: 250, tone: 'warning', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value },
|
||||
{ key: 'reject', label: '驳回', priority: 200, tone: 'danger', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value },
|
||||
{ key: 'issue', label: '出账', priority: 200, visible: row.status === 'APPROVED' && canIssue.value, loading: actionBusy.value },
|
||||
{ key: 'adjust', label: '调整', priority: 200, visible: row.status === 'ISSUED' && canAdjust.value, loading: adjustBusy.value },
|
||||
{ key: 'reopen', label: '重开', priority: 150, visible: ['REJECTED', 'VOIDED'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value },
|
||||
{ key: 'void', label: '作废', tone: 'danger', divided: true, visible: ['DRAFT', 'PENDING_REVIEW', 'APPROVED', 'ISSUED'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value },
|
||||
]
|
||||
}
|
||||
function handleStatementAction(command: string, row: FinanceStatementReadModel) {
|
||||
if (command === 'detail') void openDetail(row)
|
||||
else if (command === 'adjust') void openAdjustment(row)
|
||||
else {
|
||||
const actions: Record<string, ReconciliationActionPayload['action']> = {
|
||||
submit: 'SUBMIT', approve: 'APPROVE', reject: 'REJECT', issue: 'ISSUE', reopen: 'REOPEN', void: 'VOID',
|
||||
}
|
||||
const action = actions[command]
|
||||
if (action) void performAction(row, action)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAdjustment() {
|
||||
const statement = adjustStatement.value
|
||||
@@ -654,21 +678,7 @@ watch(detailRouteId, (statementId) => {
|
||||
<el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportStatements"><Download :size="15" />导出</el-button>
|
||||
<el-button v-if="canCreate" type="primary" :disabled="!canGenerate" :title="canGenerate ? '生成对账单' : '缺少客户列表权限,无法选择客户'" @click="openCreate"><Plus :size="15" />生成对账单</el-button>
|
||||
</template>
|
||||
<template #actions="{ row }"><div class="density-actions">
|
||||
<el-button data-tone="neutral" class="density-action" size="small" :disabled="loading" @click.stop="openDetail(row as FinanceStatementReadModel)">详情</el-button>
|
||||
<el-button data-tone="neutral" class="density-action" v-if="row.status === 'DRAFT' && canCreate" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'SUBMIT')">提交</el-button>
|
||||
<el-button data-tone="warning" class="density-action" v-if="row.status === 'PENDING_REVIEW' && canReview" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'APPROVE')">通过</el-button>
|
||||
<el-button data-tone="danger" class="density-action" v-if="row.status === 'PENDING_REVIEW' && canReview" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'REJECT')">驳回</el-button>
|
||||
<el-dropdown v-if="row.status === 'PENDING_REVIEW' && canReview && canVoid" trigger="click" @command="(command: ReconciliationActionPayload['action']) => performAction(row as FinanceStatementReadModel, command)">
|
||||
<el-button data-tone="neutral" class="density-action" size="small" :loading="actionBusy" aria-label="更多对账单操作" title="更多对账单操作" @click.stop><MoreHorizontal aria-hidden="true" :size="16" /></el-button>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item command="VOID" class="danger-menu-item">作废</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<el-button data-tone="danger" class="density-action" v-if="row.status === 'PENDING_REVIEW' && !canReview && canVoid" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'VOID')">作废</el-button>
|
||||
<el-button data-tone="neutral" class="density-action" v-if="row.status === 'APPROVED' && canIssue" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'ISSUE')">出账</el-button>
|
||||
<el-button data-tone="neutral" class="density-action" v-if="row.status === 'ISSUED' && canAdjust" size="small" :loading="adjustBusy" @click.stop="openAdjustment(row as FinanceStatementReadModel)">调整</el-button>
|
||||
<el-button data-tone="neutral" class="density-action" v-if="['REJECTED','VOIDED'].includes(String(row.status)) && canVoid" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'REOPEN')">重开</el-button>
|
||||
<el-button data-tone="danger" class="density-action" v-if="['DRAFT','APPROVED','ISSUED'].includes(String(row.status)) && canVoid" size="small" :loading="actionBusy" @click.stop="performAction(row as FinanceStatementReadModel, 'VOID')">作废</el-button>
|
||||
</div></template>
|
||||
<template #actions="{ row }"><RowActions :actions="statementActions(row as FinanceStatementReadModel)" :more-label="`更多对账单操作 · ${(row as FinanceStatementReadModel).billNo}`" @action="handleStatementAction($event, row as FinanceStatementReadModel)" /></template>
|
||||
<template #status="{ row }"><el-tag :type="financeStatusTone(row.status)">{{ financeStatusLabel(row.status) }}</el-tag></template>
|
||||
<template #customerConfirmationStatus="{ row }"><el-tag :type="financeStatusTone(row.customerConfirmationStatus || 'NOT_REQUIRED')">{{ financeStatusLabel(row.customerConfirmationStatus || 'NOT_REQUIRED') }}</el-tag></template>
|
||||
<template #customerName="{ row }">{{ customerDisplayName(row.customerId, row.customerName) }}</template>
|
||||
|
||||
Reference in New Issue
Block a user