Files
X-Financial/web/src/components/charts/BudgetTrendChart.vue
caoxiaozhu 2dcc72102d style: 全局 UI 主题皮肤重构与样式模块化
引入 Element Plus 主题定制和主题皮肤 composable,将全局
样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等),
统一色彩变量和间距规范,重构所有视图和组件样式以适配新
主题系统,优化图表和知识图谱组件视觉表现,提取审计和差
旅报销相关子组件。
2026-05-27 09:17:57 +08:00

189 lines
4.6 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="budget-trend-chart">
<Bar :data="chartData" :options="chartOptions" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Bar } from 'vue-chartjs'
import {
Chart as ChartJS,
BarElement,
CategoryScale,
Legend,
LinearScale,
Tooltip
} from 'chart.js'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
import { useThemeColors } from '../../composables/useThemeColors.js'
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend)
const props = defineProps({
labels: { type: Array, required: true },
budget: { type: Array, required: true },
used: { type: Array, required: true },
occupied: { type: Array, default: () => [] },
available: { type: Array, default: () => [] }
})
const progress = useAnimationProgress([
() => props.labels,
() => props.budget,
() => props.used,
() => props.occupied,
() => props.available
], 1000)
const themeColors = useThemeColors()
const currency = (value) =>
Number(value || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
const percent = (value, total) => {
const denominator = Number(total || 0)
if (!denominator) return 0
return Number(((Number(value || 0) / denominator) * 100).toFixed(2))
}
const percentSeries = (series) =>
props.budget.map((total, index) => percent(series[index], total))
const scaleSeries = (series) =>
series.map((value) => Number((Number(value || 0) * progress.value).toFixed(2)))
const usedPercent = computed(() => percentSeries(props.used))
const occupiedPercent = computed(() => percentSeries(props.occupied))
const availablePercent = computed(() =>
props.budget.map((total, index) => {
const usedValue = Number(props.used[index] || 0)
const occupiedValue = Number(props.occupied[index] || 0)
return percent(Math.max(Number(total || 0) - usedValue - occupiedValue, 0), total)
})
)
const yAxisMax = computed(() => {
const maxUsage = Math.max(
100,
...usedPercent.value.map((value, index) => value + Number(occupiedPercent.value[index] || 0))
)
return Math.ceil(maxUsage / 20) * 20
})
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{
label: '已使用',
data: scaleSeries(usedPercent.value),
backgroundColor: themeColors.value.chartPrimary,
borderRadius: 5,
borderSkipped: false,
stack: 'budgetUsage',
amounts: props.used
},
{
label: '已占用',
data: scaleSeries(occupiedPercent.value),
backgroundColor: themeColors.value.warning,
borderRadius: 5,
borderSkipped: false,
stack: 'budgetUsage',
amounts: props.occupied
},
{
label: '剩余可用',
data: scaleSeries(availablePercent.value),
backgroundColor: '#e5edf3',
borderRadius: 5,
borderSkipped: false,
stack: 'budgetUsage',
amounts: props.available
}
]
}))
const chartOptions = computed(() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
animation: {
duration: 760,
easing: 'easeOutQuart'
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
borderWidth: 1,
bodyColor: '#475569',
titleColor: '#0f172a',
padding: 12,
displayColors: true,
callbacks: {
label(context) {
const value = Number(context.parsed.y || 0)
const amount = Number(context.dataset.amounts?.[context.dataIndex] || 0)
return `${context.dataset.label}: ${value.toFixed(2)}%(¥${currency(amount)}`
},
afterBody(items) {
const index = items[0]?.dataIndex ?? 0
return `预算总额: ¥${currency(props.budget[index])}`
}
}
}
},
scales: {
x: {
grid: { display: false },
ticks: {
color: '#64748b',
font: { size: 12 }
},
border: { display: false }
},
y: {
beginAtZero: true,
max: yAxisMax.value,
stacked: true,
grid: {
color: '#edf2f7',
drawTicks: false
},
border: { display: false },
ticks: {
color: '#64748b',
font: { size: 12 },
stepSize: 20,
callback(value) {
return `${Number(value)}%`
}
}
}
},
datasets: {
bar: {
categoryPercentage: 0.58,
barPercentage: 0.72
}
}
}))
</script>
<style scoped>
.budget-trend-chart {
position: relative;
width: 100%;
height: 220px;
}
</style>