Files
X-Financial/web/src/components/charts/BarChart.vue

224 lines
5.5 KiB
Vue
Raw Normal View History

<template>
<div class="bar-chart">
<div class="rank-labels">
<div v-for="(item, idx) in resolvedItems" :key="item.name" class="rank-label">
<span class="rank-badge" :class="medalClass(idx)">
<svg v-if="idx < 3" width="18" height="18" viewBox="0 0 18 18">
<circle cx="9" cy="9" r="8" :fill="medalFill(idx)" />
<text x="9" y="13" text-anchor="middle" fill="#fff" font-size="10" font-weight="700">{{ idx + 1 }}</text>
</svg>
<template v-else>{{ idx + 1 }}</template>
</span>
<span class="rank-name">{{ item.name || item.shortName }}</span>
</div>
</div>
<div ref="chartElement" class="chart-area" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
import { useEcharts } from '../../composables/useEcharts.js'
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
use([GridComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const progress = useAnimationProgress([() => props.items], 980)
const themeColors = useThemeColors()
const resolvedItems = computed(() => {
const fallback = themeColors.value.chartPrimary
return props.items.map((item) => ({
...item,
value: Number(item.value || item.amount || 0),
resolvedColor: resolveCssColor(item.color, fallback)
}))
})
const ariaLabel = computed(() =>
resolvedItems.value.map((item, index) => (
`${index + 1}${item.name || item.shortName}${formatValue(item.value)}`
)).join('')
)
const chartMaxValue = computed(() => Math.max(...resolvedItems.value.map((item) => item.value), 1))
const chartAxisMax = computed(() => Math.ceil((chartMaxValue.value * 1.1) / 10000) * 10000)
const animatedItems = computed(() =>
resolvedItems.value.map((item) => ({
...item,
animatedValue: progress.value >= 0.999
? item.value
: Number((item.value * progress.value).toFixed(0))
}))
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: false,
grid: {
top: 8,
right: 62,
bottom: 8,
left: 4,
containLabel: false
},
tooltip: {
trigger: 'item',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.24)',
borderWidth: 1,
padding: [9, 10],
textStyle: {
color: '#334155',
fontSize: 12,
fontWeight: 700
},
extraCssText: 'border-radius:4px;box-shadow:0 12px 28px rgba(15,23,42,.12);',
formatter: (params) => `${params.marker}${params.name}: ${formatValue(params.value)}`
},
xAxis: {
type: 'value',
min: 0,
max: chartAxisMax.value,
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: { color: 'rgba(226, 232, 240, 0.72)' }
},
axisLabel: {
color: '#94a3b8',
fontSize: 11,
fontWeight: 700,
formatter: (value) => formatValue(value)
}
},
yAxis: {
type: 'category',
data: resolvedItems.value.map((item) => item.name || item.shortName),
inverse: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
series: [
{
type: 'bar',
data: animatedItems.value.map((item) => ({
name: item.name || item.shortName,
value: item.animatedValue,
itemStyle: { color: item.resolvedColor }
})),
barWidth: 14,
showBackground: true,
backgroundStyle: {
color: 'rgba(226, 232, 240, 0.42)',
borderRadius: 6
},
itemStyle: {
borderRadius: [0, 6, 6, 0]
},
label: {
show: true,
position: 'right',
color: '#64748b',
fontSize: 11,
fontWeight: 800,
formatter: ({ value }) => formatValue(value)
}
}
]
}))
useEcharts(chartElement, chartOptions)
const medalClass = (idx) => {
if (idx === 0) return 'gold'
if (idx === 1) return 'silver'
if (idx === 2) return 'bronze'
return ''
}
const medalFill = (idx) => {
if (idx === 0) return '#f59e0b'
if (idx === 1) return '#94a3b8'
if (idx === 2) return '#cd7f32'
return '#94a3b8'
}
const formatValue = (value) => {
const number = Number(value || 0)
if (number >= 1_000_000) return `¥${(number / 1_000_000).toFixed(1)}M`
if (number >= 1_000) return `¥${(number / 1_000).toFixed(1)}K`
return `¥${number}`
}
</script>
<style scoped>
.bar-chart {
display: flex;
width: 100%;
gap: 8px;
}
.rank-labels {
flex: 0 0 auto;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 6px 0;
}
.rank-label {
display: flex;
align-items: center;
gap: 6px;
height: 34px;
white-space: nowrap;
}
.rank-badge {
width: 20px;
height: 20px;
display: grid;
place-items: center;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
color: #fff;
}
.rank-badge:not(.gold):not(.silver):not(.bronze) {
background: #cbd5e1;
}
.rank-badge svg {
display: block;
}
.rank-name {
color: #475569;
font-size: 13px;
font-weight: 500;
}
.chart-area {
flex: 1;
min-width: 0;
position: relative;
height: 240px;
}
</style>