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

219 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 { 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 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 chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 1200,
animationDurationUpdate: 1200,
animationEasing: 'linear',
animationEasingUpdate: 'linear',
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: resolvedItems.value.map((item) => ({
name: item.name || item.shortName,
value: item.value,
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',
valueAnimation: true,
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>