fix(workbench): show single expense distribution chart

This commit is contained in:
caoxiaozhu
2026-06-03 15:46:51 +08:00
parent 18d716bc6b
commit e12b140508
6 changed files with 139 additions and 15 deletions

View File

@@ -51,12 +51,29 @@
</div>
<div v-if="distributionChartItems.length" class="expense-distribution-chart">
<DonutChart
class="expense-distribution-donut"
:items="distributionChartItems"
:center-value="distributionCenterValue"
center-label="费用总额"
/>
<div class="expense-distribution-chart-layout">
<DonutChart
class="expense-distribution-donut"
:items="distributionChartItems"
:center-value="distributionCenterValue"
center-label="费用总额"
:show-legend="false"
/>
<div class="expense-distribution-summary-list" aria-label="费用分布明细">
<article
v-for="(row, index) in distributionRows"
:key="`${row.label}-${index}`"
class="expense-distribution-summary-row"
>
<i :style="{ background: resolveDistributionColor(index) }"></i>
<div>
<strong>{{ row.label }}</strong>
<span>{{ row.count || 0 }} · {{ row.amountLabel || '¥0' }}</span>
</div>
<em>{{ row.percentLabel || '0%' }}</em>
</article>
</div>
</div>
</div>
<p v-else class="expense-stats-empty">暂无历史报销费用分布</p>
</section>
@@ -188,6 +205,10 @@ const distributionChartItems = computed(() => distributionRows.value.map((row, i
color: distributionChartColors[index % distributionChartColors.length]
})))
function resolveDistributionColor(index) {
return distributionChartColors[index % distributionChartColors.length]
}
function emitClose() {
emit('close')
}
@@ -454,26 +475,84 @@ function resolveTagType(tone) {
align-items: stretch;
}
.expense-distribution-donut {
.expense-distribution-chart-layout {
display: grid;
grid-template-columns: minmax(170px, 0.86fr) minmax(0, 1.14fr);
align-items: center;
gap: 12px;
min-height: 286px;
}
.expense-distribution-donut {
min-height: 0;
}
.expense-distribution-donut :deep(.donut-body) {
height: 194px;
height: 220px;
margin-top: 0;
}
.expense-distribution-donut :deep(.donut-legend) {
gap: 7px 14px;
.expense-distribution-summary-list {
min-width: 0;
display: grid;
align-content: center;
gap: 8px;
}
.expense-distribution-donut :deep(.legend-name) {
.expense-distribution-summary-row {
min-width: 0;
display: grid;
grid-template-columns: 10px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 8px 0;
border-top: 1px solid #e8eef5;
}
.expense-distribution-summary-row:first-child {
border-top: 0;
}
.expense-distribution-summary-row i {
width: 10px;
height: 10px;
border-radius: 2px;
}
.expense-distribution-summary-row div {
min-width: 0;
display: grid;
gap: 2px;
}
.expense-distribution-summary-row strong,
.expense-distribution-summary-row span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expense-distribution-summary-row strong {
color: #334155;
font-size: 12.5px;
font-weight: 850;
}
.expense-distribution-summary-row span {
color: #94a3b8;
font-size: 11px;
font-weight: 650;
}
.expense-distribution-summary-row em {
color: #0f172a;
font-size: 12px;
font-style: normal;
font-weight: 850;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.expense-processing-row {
display: grid;
grid-template-columns: minmax(124px, 0.8fr) minmax(164px, 1fr) auto 58px;
@@ -580,6 +659,14 @@ function resolveTagType(tone) {
}
@media (max-width: 620px) {
.expense-distribution-chart-layout {
grid-template-columns: 1fr;
}
.expense-distribution-donut :deep(.donut-body) {
height: 190px;
}
.expense-processing-row,
.expense-operation-row {
grid-template-columns: 1fr;

View File

@@ -1,5 +1,5 @@
<template>
<div class="donut-chart">
<div class="donut-chart" :class="{ 'donut-chart--legendless': !showLegend }">
<div class="donut-body">
<div ref="chartElement" class="donut-canvas" role="img" :aria-label="ariaLabel"></div>
<div class="donut-center">
@@ -7,7 +7,7 @@
<span>{{ centerLabel }}</span>
</div>
</div>
<div class="donut-legend">
<div v-if="showLegend" class="donut-legend">
<div v-for="item in resolvedItems" :key="item.name" class="legend-row">
<i :style="{ background: item.resolvedColor }"></i>
<span class="legend-name">{{ item.name }}</span>
@@ -32,9 +32,12 @@ use([TooltipComponent, PieChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true },
centerValue: { type: String, required: true },
centerLabel: { type: String, required: true }
centerLabel: { type: String, required: true },
showLegend: { type: Boolean, default: true }
})
const showLegend = computed(() => props.showLegend)
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const resolvedItems = computed(() => {
@@ -117,6 +120,15 @@ useEcharts(chartElement, chartOptions)
gap: 10px;
}
.donut-chart--legendless {
min-height: 0;
justify-content: center;
}
.donut-chart--legendless .donut-body {
margin-top: 0;
}
.donut-body {
position: relative;
width: 100%;

View File

@@ -0,0 +1,15 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const donutChart = readFileSync(
fileURLToPath(new URL('../src/components/charts/DonutChart.vue', import.meta.url)),
'utf8'
)
test('donut chart supports hiding its built-in legend without changing the default', () => {
assert.match(donutChart, /showLegend: \{ type: Boolean, default: true \}/)
assert.match(donutChart, /v-if="showLegend" class="donut-legend"/)
assert.match(donutChart, /donut-chart--legendless/)
})

View File

@@ -17,9 +17,12 @@ test('expense stats detail modal exposes distribution, processing time and opera
assert.match(modal, /ElTag/)
assert.match(modal, /import DonutChart from '\.\.\/charts\/DonutChart\.vue'/)
assert.match(modal, /<DonutChart/)
assert.match(modal, /:show-legend="false"/)
assert.match(modal, /distributionChartItems/)
assert.match(modal, /distributionCenterValue/)
assert.match(modal, /distributionRows/)
assert.match(modal, /expense-distribution-summary-list/)
assert.match(modal, /resolveDistributionColor/)
assert.match(modal, /processingRows/)
assert.match(modal, /operationRows/)
assert.doesNotMatch(modal, /--expense-detail-percent/)