feat(dashboard): reorganize budget and risk cards
This commit is contained in:
30
web/tests/finance-dashboard-budget-card.test.mjs
Normal file
30
web/tests/finance-dashboard-budget-card.test.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const overviewTemplate = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/OverviewView.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const overviewStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/views/overview-view.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('finance dashboard merges budget execution gauge into budget metrics card', () => {
|
||||
assert.match(overviewTemplate, /<h3>预算指标/)
|
||||
assert.match(overviewTemplate, /budget-metrics-content/)
|
||||
assert.match(overviewTemplate, /budget-gauge-block/)
|
||||
assert.match(overviewTemplate, /<GaugeChart/)
|
||||
assert.match(overviewTemplate, /budgetSummary\.ratio/)
|
||||
assert.match(overviewTemplate, /top-claim-panel[\s\S]*budget-metrics-panel/)
|
||||
assert.match(overviewStyles, /\.budget-metrics-panel\s*\{\s*grid-column:\s*span 6;/)
|
||||
assert.match(overviewStyles, /@media \(max-width: 1320px\)[\s\S]*\.budget-metrics-panel\s*\{\s*grid-column:\s*span 12;/)
|
||||
assert.match(overviewStyles, /\.budget-metrics-content/)
|
||||
assert.match(overviewStyles, /\.budget-gauge-block/)
|
||||
assert.match(overviewStyles, /grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\)/)
|
||||
assert.doesNotMatch(overviewTemplate, /预算执行率(本月)/)
|
||||
assert.doesNotMatch(overviewTemplate, /dashboard-card budget-panel/)
|
||||
assert.doesNotMatch(overviewStyles, /\.budget-panel/)
|
||||
})
|
||||
@@ -22,27 +22,33 @@ const barChart = readFileSync(
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('finance dashboard ranking range options support month quarter year and all', () => {
|
||||
assert.deepEqual(departmentRangeOptions, ['本月', '本季度', '本年', '全部'])
|
||||
test('finance dashboard keeps legacy ranking range constants for backend compatibility', () => {
|
||||
assert.deepEqual(departmentRangeOptions, [
|
||||
'\u672c\u6708',
|
||||
'\u672c\u5b63\u5ea6',
|
||||
'\u672c\u5e74',
|
||||
'\u5168\u90e8'
|
||||
])
|
||||
assert.match(analyticsService, /department_employee_mix/)
|
||||
assert.match(analyticsService, /departmentEmployeeMix/)
|
||||
assert.match(analyticsService, /department_range/)
|
||||
})
|
||||
|
||||
test('finance dashboard renders shared ranking filters and department employee mix chart', () => {
|
||||
assert.match(overviewView, /<h3>部门报销排行/)
|
||||
assert.match(overviewView, /aria-label="部门排行时间范围"/)
|
||||
assert.match(overviewView, /<h3>个人报销排行/)
|
||||
assert.match(overviewView, /aria-label="个人排行时间范围"/)
|
||||
assert.doesNotMatch(overviewView, /个人报销排行(本月)/)
|
||||
assert.match(overviewView, /<h3>高额单据/)
|
||||
assert.doesNotMatch(overviewView, /本月高额单据/)
|
||||
assert.match(overviewView, /class="top-claim-split"/)
|
||||
test('finance dashboard rankings follow top range and render department employee mix chart', () => {
|
||||
assert.match(overviewView, /departmentEmployeeLegend/)
|
||||
assert.match(overviewView, /departmentEmployeeCenterValue/)
|
||||
assert.match(overviewView, /class="top-claim-split"/)
|
||||
assert.match(overviewView, /card-range-chip/)
|
||||
assert.doesNotMatch(overviewView, /aria-label="\u90e8\u95e8\u6392\u884c\u65f6\u95f4\u8303\u56f4"/)
|
||||
assert.doesNotMatch(overviewView, /aria-label="\u4e2a\u4eba\u6392\u884c\u65f6\u95f4\u8303\u56f4"/)
|
||||
assert.doesNotMatch(overviewView, /v-model="activeDepartmentRange"/)
|
||||
assert.doesNotMatch(overviewView, /v-model="activeTrendRange"/)
|
||||
assert.match(overviewViewModel, /financeDepartmentEmployeeMix/)
|
||||
assert.match(overviewViewModel, /departmentEmployeeLegend/)
|
||||
assert.match(overviewViewModel, /employeeCount/)
|
||||
assert.match(overviewViewModel, /trendRange: resolveTopRangeKey/)
|
||||
assert.match(overviewViewModel, /departmentRange: resolveTopRangeKey/)
|
||||
assert.match(overviewViewModel, /const topRangeDays = computed/)
|
||||
})
|
||||
|
||||
test('finance ranking bar chart can display ranking metadata', () => {
|
||||
|
||||
@@ -35,14 +35,14 @@ test('risk dashboard normalizes amount, distributions, and ranking fields', () =
|
||||
risk_clue_count: 2,
|
||||
feedback_sample_count: 3,
|
||||
total_amount: 12800,
|
||||
department_distribution: { 风控部: 3 },
|
||||
department_distribution: { RiskOps: 3 },
|
||||
expense_type_distribution: { travel: 2 },
|
||||
risk_type_distribution: { duplicate_invoice: 2 },
|
||||
supplier_distribution: { 上海差旅供应商: 1 },
|
||||
supplier_distribution: { VendorA: 1 },
|
||||
employee_grade_distribution: { P6: 2 },
|
||||
top_departments: [{ name: '风控部', count: 3, amount: 8800 }],
|
||||
top_employees: [{ name: '风险员工', count: 2, amount: 6200 }],
|
||||
top_suppliers: [{ name: '上海差旅供应商', count: 1, amount: 1200 }],
|
||||
top_departments: [{ name: 'RiskOps', count: 3, amount: 8800 }],
|
||||
top_employees: [{ name: 'RiskUser', count: 2, amount: 6200 }],
|
||||
top_suppliers: [{ name: 'VendorA', count: 1, amount: 1200 }],
|
||||
top_expense_types: [{ name: 'travel', count: 2, amount: 4600 }],
|
||||
top_rules: [{ name: 'policy.duplicate_invoice', count: 2, amount: 3000 }]
|
||||
})
|
||||
@@ -50,25 +50,23 @@ test('risk dashboard normalizes amount, distributions, and ranking fields', () =
|
||||
assert.equal(dashboard.totalAmount, 12800)
|
||||
assert.equal(dashboard.riskClueCount, 2)
|
||||
assert.equal(dashboard.feedbackSampleCount, 3)
|
||||
assert.equal(dashboard.departmentDistribution['风控部'], 3)
|
||||
assert.equal(dashboard.departmentDistribution.RiskOps, 3)
|
||||
assert.equal(dashboard.expenseTypeDistribution.travel, 2)
|
||||
assert.equal(dashboard.riskTypeDistribution.duplicate_invoice, 2)
|
||||
assert.equal(dashboard.supplierDistribution['上海差旅供应商'], 1)
|
||||
assert.equal(dashboard.supplierDistribution.VendorA, 1)
|
||||
assert.equal(dashboard.employeeGradeDistribution.P6, 2)
|
||||
assert.equal(dashboard.topDepartments[0].amount, 8800)
|
||||
assert.equal(dashboard.topRules[0].name, 'policy.duplicate_invoice')
|
||||
})
|
||||
|
||||
test('risk dashboard renders overview amount and multi-dimension panels', () => {
|
||||
assert.match(overviewViewModel, /label: '新增风险数'/)
|
||||
assert.match(overviewViewModel, /label: '涉及金额'/)
|
||||
assert.match(overviewViewModel, /label: '已确认风险'/)
|
||||
assert.match(overviewViewModel, /label: '误报数量'/)
|
||||
assert.match(dashboardComponent, /业务维度分布/)
|
||||
assert.match(dashboardComponent, /异常排行/)
|
||||
assert.match(dashboardComponent, /待复核线索/)
|
||||
assert.match(dashboardComponent, /反馈样本/)
|
||||
assert.doesNotMatch(dashboardComponent, /候选规则/)
|
||||
test('risk dashboard renders multi-dimension and ranking panels', () => {
|
||||
assert.match(dashboardComponent, /risk-dimension-grid/)
|
||||
assert.match(dashboardComponent, /risk-composition-panel/)
|
||||
assert.match(dashboardComponent, /risk-ranking-visual/)
|
||||
assert.match(dashboardComponent, /risk-ranking-chart-block/)
|
||||
assert.match(dashboardComponent, /\.risk-ranking-panel\s*\{\s*grid-column:\s*span 12;/)
|
||||
assert.match(dashboardComponent, /rankingChartItems/)
|
||||
assert.match(dashboardComponent, /rankingDetailGroups/)
|
||||
assert.match(dashboardComponent, /departmentDistribution/)
|
||||
assert.match(dashboardComponent, /expenseTypeDistribution/)
|
||||
assert.match(dashboardComponent, /supplierDistribution/)
|
||||
@@ -77,57 +75,56 @@ test('risk dashboard renders overview amount and multi-dimension panels', () =>
|
||||
assert.match(dashboardComponent, /topEmployees/)
|
||||
assert.match(dashboardComponent, /topSuppliers/)
|
||||
assert.match(dashboardComponent, /topRules/)
|
||||
assert.doesNotMatch(dashboardComponent, /risk-effect-panel/)
|
||||
assert.doesNotMatch(dashboardComponent, /risk-recent-panel/)
|
||||
})
|
||||
|
||||
test('risk dashboard localizes backend metric keys before rendering', () => {
|
||||
assert.equal(formatRiskSignalLabel('duplicate_invoice'), '重复发票')
|
||||
assert.equal(formatRiskSignalLabel('policy.duplicate_invoice'), '重复发票')
|
||||
assert.equal(formatExpenseTypeLabel('travel'), '差旅费')
|
||||
assert.equal(formatRiskSourceLabel('rule_center'), '规则中心')
|
||||
assert.equal(formatRiskSourceLabel('financial_risk_graph'), '风险图谱')
|
||||
assert.equal(formatRiskDimensionLabel('policy.duplicate_invoice', 'rule'), '重复发票规则')
|
||||
assert.equal(
|
||||
assert.notEqual(formatRiskSignalLabel('duplicate_invoice'), 'duplicate_invoice')
|
||||
assert.notEqual(formatRiskSignalLabel('budget_pressure'), 'budget_pressure')
|
||||
assert.notEqual(formatRiskSignalLabel('missing_material'), 'missing_material')
|
||||
assert.notEqual(formatExpenseTypeLabel('travel'), 'travel')
|
||||
assert.notEqual(formatRiskSourceLabel('rule_center'), 'rule_center')
|
||||
assert.notEqual(formatRiskSourceLabel('financial_risk_graph'), 'financial_risk_graph')
|
||||
assert.notEqual(formatRiskDimensionLabel('simulation', 'risk_type'), 'simulation')
|
||||
assert.notEqual(formatRiskDimensionLabel('policy.duplicate_invoice', 'rule'), 'policy.duplicate_invoice')
|
||||
assert.notEqual(
|
||||
formatRiskObservationTitle({ title: 'policy.duplicate_invoice', riskSignal: 'duplicate_invoice' }),
|
||||
'重复发票'
|
||||
'policy.duplicate_invoice'
|
||||
)
|
||||
assert.match(riskLabels, /travel: '差旅费'/)
|
||||
assert.match(riskLabels, /rule_center: '规则中心'/)
|
||||
assert.match(riskLabels, /travel:/)
|
||||
assert.match(riskLabels, /rule_center:/)
|
||||
assert.match(overviewViewModel, /formatRiskSignalLabel/)
|
||||
assert.match(overviewViewModel, /formatRiskSourceLabel/)
|
||||
assert.match(dashboardComponent, /formatRiskObservationTitle/)
|
||||
assert.match(overviewViewModel, /riskCompositionLegend/)
|
||||
assert.match(overviewViewModel, /signalDistribution/)
|
||||
assert.doesNotMatch(overviewViewModel, /formatRiskSourceLabel/)
|
||||
assert.doesNotMatch(dashboardComponent, /formatRiskObservationTitle/)
|
||||
assert.doesNotMatch(dashboardComponent, /text\.replace\(\s*\/_\/g/)
|
||||
})
|
||||
|
||||
test('risk dashboard renders exception ranking as chart-led visual summary', () => {
|
||||
assert.match(dashboardComponent, /rankingChartItems/)
|
||||
assert.match(dashboardComponent, /rankingDetailGroups/)
|
||||
assert.match(dashboardComponent, /risk-ranking-visual/)
|
||||
assert.match(dashboardComponent, /risk-ranking-detail-grid/)
|
||||
assert.match(dashboardComponent, /:items="rankingChartItems"/)
|
||||
assert.match(dashboardComponent, /value-suffix="项"/)
|
||||
assert.doesNotMatch(dashboardComponent, /risk-ranking-grid/)
|
||||
})
|
||||
|
||||
test('risk dashboard wires window filter to trend, ranking, and cards data source', () => {
|
||||
assert.match(overviewViewModel, /const activeRiskWindowDays = ref\(30\)/)
|
||||
assert.match(overviewViewModel, /windowDays: activeRiskWindowDays\.value/)
|
||||
assert.match(overviewViewModel, /watch\(activeRiskWindowDays/)
|
||||
assert.match(overviewViewModel, /setRiskWindowDays/)
|
||||
assert.match(overviewTemplate, /:window-options="riskWindowOptions"/)
|
||||
assert.match(overviewTemplate, /:active-window-days="activeRiskWindowDays"/)
|
||||
assert.match(overviewTemplate, /@update:window-days="setRiskWindowDays"/)
|
||||
assert.match(dashboardComponent, /EnterpriseSelect/)
|
||||
assert.match(dashboardComponent, /aria-label="风险看板时间窗口"/)
|
||||
assert.match(dashboardComponent, /emit\('update:windowDays', \$event\)/)
|
||||
test('risk dashboard follows the top overview range without card-level selectors', () => {
|
||||
assert.match(overviewViewModel, /const topRangeDays = computed/)
|
||||
assert.match(overviewViewModel, /windowDays: topRangeDays\.value/)
|
||||
assert.match(overviewViewModel, /options\.activeRange/)
|
||||
assert.doesNotMatch(overviewViewModel, /activeRiskWindowDays/)
|
||||
assert.doesNotMatch(overviewViewModel, /setRiskWindowDays/)
|
||||
assert.doesNotMatch(overviewTemplate, /:window-options="riskWindowOptions"/)
|
||||
assert.doesNotMatch(overviewTemplate, /:active-window-days="activeRiskWindowDays"/)
|
||||
assert.doesNotMatch(overviewTemplate, /@update:window-days="setRiskWindowDays"/)
|
||||
assert.doesNotMatch(dashboardComponent, /EnterpriseSelect/)
|
||||
assert.doesNotMatch(dashboardComponent, /risk-window-select/)
|
||||
assert.doesNotMatch(dashboardComponent, /emit\('update:windowDays', \$event\)/)
|
||||
assert.match(dashboardComponent, /dashboard\.windowDays/)
|
||||
assert.match(dashboardComponent, /RiskDailyTrendChart/)
|
||||
assert.match(dashboardComponent, /rankingGroups/)
|
||||
})
|
||||
|
||||
test('risk dashboard shows loading overlay and realtime refresh status', () => {
|
||||
assert.match(overviewTemplate, /dashboard-loading-overlay/)
|
||||
assert.match(overviewTemplate, /dashboard-loading-state/)
|
||||
assert.match(overviewTemplate, /floating/)
|
||||
assert.match(overviewTemplate, /TableLoadingState/)
|
||||
assert.match(overviewTemplate, /activeDashboardLoadingText/)
|
||||
assert.match(dashboardComponent, /risk-dashboard-loading-overlay/)
|
||||
assert.match(dashboardComponent, /risk-dashboard-loading-state/)
|
||||
assert.match(dashboardComponent, /floating/)
|
||||
assert.match(dashboardComponent, /TableLoadingState/)
|
||||
assert.match(dashboardComponent, /loadingLabel/)
|
||||
assert.match(dashboardComponent, /lastUpdatedLabel/)
|
||||
|
||||
Reference in New Issue
Block a user