Files
EmailBill/Web/src/components/Budget/BudgetChartAnalysis.vue
SunCheng 6e95568906
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 18s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 0s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
fix
2026-02-20 13:56:29 +08:00

1132 lines
29 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="chart-analysis-container">
<!-- 仪表盘整体健康度 -->
<div class="gauges-row">
<!-- 月度仪表盘 -->
<div class="chart-card gauge-card">
<div class="chart-header">
<div class="chart-title">
<span class="chart-title-text">
{{ activeTab === BudgetCategory.Expense ? '使用情况(月度)' : '完成情况(月度)' }}
</span>
<van-icon
name="info-o"
size="16"
color="var(--van-primary-color)"
class="info-icon"
@click="handleShowDescription('month')"
/>
</div>
</div>
<div class="gauge-wrapper">
<BaseChart
type="doughnut"
:data="monthGaugeData"
:options="monthGaugeOptions"
:plugins="[chartjsGaugePlugin]"
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
/>
<div class="gauge-text-overlay">
<div class="balance-label">
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
</div>
<div
class="balance-value"
:style="{
color:
activeTab === BudgetCategory.Expense
? (overallStats.month.current > overallStats.month.limit ? 'var(--van-danger-color)' : '')
: (overallStats.month.current < overallStats.month.limit ? 'var(--van-danger-color)' : '')
}"
>
¥{{ formatMoney(Math.abs(overallStats.month.limit - overallStats.month.current)) }}
</div>
</div>
</div>
<div class="gauge-footer">
<div class="gauge-item">
<span class="label">{{ activeTab === BudgetCategory.Expense ? '已用' : '已获得' }}</span>
<span class="value">¥{{ formatMoney(overallStats.month.current) }}</span>
</div>
<div class="gauge-item">
<span class="label">{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}</span>
<span class="value">¥{{ formatMoney(overallStats.month.limit) }}</span>
</div>
</div>
</div>
<!-- 年度仪表盘 -->
<div class="chart-card gauge-card">
<div class="chart-header">
<div class="chart-title">
<span class="chart-title-text">
{{ activeTab === BudgetCategory.Expense ? '使用情况(年度)' : '完成情况(年度)' }}
</span>
<van-icon
name="info-o"
size="16"
color="var(--van-primary-color)"
class="info-icon"
@click="handleShowDescription('year')"
/>
</div>
</div>
<div class="gauge-wrapper">
<BaseChart
type="doughnut"
:data="yearGaugeData"
:options="yearGaugeOptions"
:plugins="[chartjsGaugePlugin]"
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
/>
<div class="gauge-text-overlay">
<div class="balance-label">
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
</div>
<div
class="balance-value"
:style="{
color:
activeTab === BudgetCategory.Expense
? (overallStats.year.current > overallStats.year.limit ? 'var(--van-danger-color)' : '')
: (overallStats.year.current < overallStats.year.limit ? 'var(--van-danger-color)' : '')
}"
>
¥{{ formatMoney(Math.abs(overallStats.year.limit - overallStats.year.current)) }}
</div>
</div>
</div>
<div class="gauge-footer">
<div class="gauge-item">
<span class="label">{{ activeTab === BudgetCategory.Expense ? '已用' : '已获得' }}</span>
<span class="value">¥{{ formatMoney(overallStats.year.current) }}</span>
</div>
<div class="gauge-item">
<span class="label">{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}</span>
<span class="value">¥{{ formatMoney(overallStats.year.limit) }}</span>
</div>
</div>
</div>
</div>
<!-- 月度预算进度 -->
<div
v-if="budgets.length > 0"
class="chart-card"
>
<div class="chart-header">
<div class="chart-title">
预算进度月度
</div>
<div class="chart-subtitle">
{{ activeTab === BudgetCategory.Expense ? '预算剩余消耗趋势' : '收入积累趋势' }}
</div>
</div>
<BaseChart
type="line"
:data="burndownChartData"
:options="burndownChartOptions"
class="chart-body burndown-chart"
/>
</div>
<!-- 年度预算进度 -->
<div
v-if="budgets.length > 0"
class="chart-card chart-card-spacing"
>
<div class="chart-header">
<div class="chart-title">
预算进度年度
</div>
<div class="chart-subtitle">
本年各预算执行情况
</div>
</div>
<BaseChart
type="line"
:data="yearBurndownChartData"
:options="yearBurndownChartOptions"
class="chart-body burndown-chart"
/>
</div>
<!-- 偏差分析 -->
<div
v-if="budgets.length > 0"
class="chart-card chart-card-spacing"
>
<div class="chart-header">
<div class="chart-title">
偏差分析
</div>
<div class="chart-subtitle">
预算执行偏差排行
</div>
</div>
<BaseChart
type="bar"
:data="varianceChartData"
:options="varianceChartOptions"
:plugins="varianceChartPlugins"
class="chart-body variance-chart"
:style="{ height: calculateChartHeight(budgets) + 'px' }"
/>
</div>
<!-- 空状态占位 -->
<div
v-else-if="budgets.length === 0"
class="chart-card empty-card"
>
<van-empty
description="暂无预算数据"
image="search"
/>
</div>
</div>
<!-- 详细描述弹窗 -->
<PopupContainer
v-model="showDescriptionPopup"
:title="activeDescTab === 'month' ? '预算额度/实际详情(月度)' : '预算额度/实际详情(年度)'"
height="70%"
>
<div
class="rich-html-content popup-content-padding"
v-html="
activeDescTab === 'month'
? overallStats.month?.description ||
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>'
: overallStats.year?.description ||
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>'
"
/>
</PopupContainer>
</template>
<script setup>
import { ref, computed } from 'vue'
import { BudgetCategory, BudgetPeriodType } from '@/constants/enums'
import { getCssVar } from '@/utils/theme'
import PopupContainer from '@/components/PopupContainer.vue'
import BaseChart from '@/components/Charts/BaseChart.vue'
import { useChartTheme } from '@/composables/useChartTheme'
import { chartjsGaugePlugin } from '@/plugins/chartjs-gauge-plugin'
import { Chart as ChartJS } from 'chart.js'
// 注册仪表盘插件
ChartJS.register(chartjsGaugePlugin)
const props = defineProps({
overallStats: {
type: Object,
required: true
},
budgets: {
type: Array,
default: () => []
},
activeTab: {
type: [Number, String],
default: BudgetCategory.Expense
},
selectedDate: {
type: Date,
default: () => new Date()
}
})
// 弹窗状态
const showDescriptionPopup = ref(false)
const activeDescTab = ref('month')
// 显示描述弹窗
const handleShowDescription = (tab) => {
activeDescTab.value = tab
showDescriptionPopup.value = true
}
// Chart.js 相关
const { getChartOptions } = useChartTheme()
const formatMoney = (val) => {
if (Math.abs(val) >= 10000) {
return (val / 10000).toFixed(1) + 'w'
}
return parseFloat(val || 0).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
}
// 月度仪表盘数据
const monthGaugeData = computed(() => {
// 防御性检查:如果数据未加载,返回默认结构
if (!props.overallStats || !props.overallStats.month) {
return {
datasets: [
{
data: [0, 100],
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
borderWidth: 0,
circumference: 180,
rotation: 270
}
]
}
}
const rate = parseFloat(props.overallStats.month.rate || 0)
const isExpense = props.activeTab === BudgetCategory.Expense
let displayRate
if (isExpense) {
displayRate = Math.max(0, 100 - rate)
if (rate > 100) {
displayRate = rate - 100
}
} else {
displayRate = rate
}
let color
if (isExpense) {
if (rate > 100) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 30) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 65) {
color = getCssVar('--chart-warning')
} else {
color = getCssVar('--chart-success')
}
} else {
if (displayRate <= 30) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 65) {
color = getCssVar('--chart-warning')
} else {
color = getCssVar('--chart-success')
}
}
const maxValue = isExpense && rate > 100 ? 50 : 100
const remaining = 100 - displayRate
return {
datasets: [
{
data: [displayRate, remaining],
backgroundColor: [color, getCssVar('--chart-axis')],
borderWidth: 0,
circumference: 220,
rotation: 250
}
]
}
})
const monthGaugeOptions = computed(() => {
return {
cutout: '75%',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
},
datalabels: {
display: false
},
gaugePlugin: {
centerText: false
}
},
scales: {
x: { display: false },
y: { display: false }
}
}
})
// 年度仪表盘数据
const yearGaugeData = computed(() => {
// 防御性检查:如果数据未加载,返回默认结构
if (!props.overallStats || !props.overallStats.year) {
return {
datasets: [
{
data: [0, 100],
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
borderWidth: 0,
circumference: 180,
rotation: 270
}
]
}
}
const rate = parseFloat(props.overallStats.year.rate || 0)
const isExpense = props.activeTab === BudgetCategory.Expense
let displayRate
if (isExpense) {
displayRate = Math.max(0, 100 - rate)
if (rate > 100) {
displayRate = rate - 100
}
} else {
displayRate = rate
}
let color
if (isExpense) {
if (rate > 100) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 30) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 65) {
color = getCssVar('--chart-warning')
} else {
color = getCssVar('--chart-success')
}
} else {
if (displayRate <= 30) {
color = getCssVar('--chart-danger')
} else if (displayRate <= 65) {
color = getCssVar('--chart-warning')
} else {
color = getCssVar('--chart-success')
}
}
const remaining = 100 - displayRate
return {
datasets: [
{
data: [displayRate, remaining],
backgroundColor: [color, getCssVar('--chart-axis')],
borderWidth: 0,
circumference: 220,
rotation: 250
}
]
}
})
const yearGaugeOptions = computed(() => {
return {
cutout: '75%',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
},
datalabels: {
display: false
},
gaugePlugin: {
centerText: false
}
},
scales: {
x: { display: false },
y: { display: false }
}
}
})
const calculateChartHeight = (budgets) => {
if (!budgets) {
return 100
}
const dataCount = budgets.length
const minHeight = 100
const heightPerItem = 30
const calculatedHeight = Math.max(minHeight, dataCount * heightPerItem)
return calculatedHeight
}
const varianceLabelPlugin = {
id: 'variance-label-plugin',
afterDatasetsDraw: (chart) => {
const dataset = chart.data?.datasets?.[0]
const metaData = dataset?._meta
if (!dataset || !metaData) {
return
}
const meta = chart.getDatasetMeta(0)
if (!meta?.data) {
return
}
const { ctx, chartArea } = chart
const fontFamily = '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'
ctx.save()
ctx.font = `12px ${fontFamily}`
ctx.textBaseline = 'middle'
meta.data.forEach((bar, index) => {
const item = metaData[index]
if (!item || item.value === 0) {
return
}
const label = formatVarianceLabelValue(item.value)
const textWidth = ctx.measureText(label).width
const position = bar.tooltipPosition ? bar.tooltipPosition() : { x: bar.x, y: bar.y }
const offset = 8
const isPositive = item.value > 0
ctx.fillStyle = getVarianceLabelColor(item.value)
let x = position.x + (isPositive ? offset : -offset)
const y = position.y
if (chartArea) {
const rightLimit = chartArea.right - 4
const leftLimit = chartArea.left + 4
if (isPositive && x + textWidth > rightLimit) {
x = rightLimit - textWidth
}
if (!isPositive && x - textWidth < leftLimit) {
x = leftLimit + textWidth
}
}
ctx.textAlign = isPositive ? 'left' : 'right'
ctx.fillText(label, x, y)
})
ctx.restore()
}
}
const varianceChartPlugins = computed(() => [varianceLabelPlugin])
const formatVarianceLabelValue = (value) => {
const absValue = Math.abs(Math.round(value || 0))
return absValue.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
}
const getVarianceLabelColor = (value) => {
if (props.activeTab === BudgetCategory.Expense) {
return value > 0 ? getCssVar('--van-danger-color') : getCssVar('--van-success-color')
}
return value > 0 ? getCssVar('--van-success-color') : getCssVar('--van-danger-color')
}
// 偏差分析图表数据
const varianceChartData = computed(() => {
if (!props.budgets || props.budgets.length === 0) {
return { labels: [], datasets: [] }
}
const data = props.budgets.map((b) => {
const limit = b.limit || 0
const current = b.current || 0
const diff = current - limit
return {
name: b.name + (b.type === BudgetPeriodType.Year ? ' (年)' : ''),
value: diff,
limit: limit,
current: current,
type: b.type
}
})
const monthlyData = data.filter((item) => item.type === BudgetPeriodType.Month)
const annualData = data.filter((item) => item.type === BudgetPeriodType.Year)
const sortByLimitAndRemaining = (a, b) => {
if (a.limit !== b.limit) {
return a.limit - b.limit
}
const remainingA = a.limit - a.current
const remainingB = b.limit - b.current
return remainingB - remainingA
}
monthlyData.sort(sortByLimitAndRemaining)
annualData.sort(sortByLimitAndRemaining)
const sortedData = [...monthlyData, ...annualData]
return {
labels: sortedData.map((item) => item.name),
datasets: [
{
label: '偏差',
data: sortedData.map((item) => item.value),
backgroundColor: sortedData.map((item) => {
if (props.activeTab === BudgetCategory.Expense) {
return item.value > 0
? getCssVar('--van-danger-color')
: getCssVar('--van-success-color')
} else {
return item.value > 0
? getCssVar('--van-success-color')
: getCssVar('--van-danger-color')
}
}),
borderRadius: 4,
barThickness: 20,
_meta: sortedData
}
]
}
})
const varianceChartOptions = computed(() => {
return getChartOptions({
indexAxis: 'y',
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
cornerRadius: 8,
callbacks: {
label: (context) => {
const item = context.dataset._meta[context.dataIndex]
const isExpense = props.activeTab === BudgetCategory.Expense
let diffText
if (isExpense) {
diffText = item.value > 0
? `超支: ¥${formatMoney(item.value)}`
: `结余: ¥${formatMoney(Math.abs(item.value))}`
} else {
diffText = item.value > 0
? `超额: ¥${formatMoney(item.value)}`
: `未达标: ¥${formatMoney(Math.abs(item.value))}`
}
return [
`预算: ¥${formatMoney(item.limit)}`,
`实际: ¥${formatMoney(item.current)}`,
diffText
]
}
}
}
},
scales: {
x: {
display: false
},
y: {
grid: {
display: false
},
ticks: {
autoSkip: false,
font: {
family: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif',
size: 11
},
callback: function (value, index) {
const label = this.getLabelForValue(index)
return label.length > 10 ? label.substring(0, 10) + '...' : label
}
}
}
}
})
})
// 月度燃尽图数据
const burndownChartData = computed(() => {
// 防御性检查
if (!props.overallStats || !props.overallStats.month || !props.selectedDate) {
return {
labels: [],
datasets: []
}
}
const refDate = props.selectedDate
const year = refDate.getFullYear()
const month = refDate.getMonth()
const daysInMonth = new Date(year, month + 1, 0).getDate()
const now = new Date()
const isCurrentMonth = now.getFullYear() === year && now.getMonth() === month
const isPastMonth =
now.getFullYear() > year || (now.getFullYear() === year && now.getMonth() > month)
const currentDay = isCurrentMonth ? now.getDate() : isPastMonth ? daysInMonth : 0
const isExpense = props.activeTab === BudgetCategory.Expense
const dates = []
const idealBurndown = []
const actualBurndown = []
const totalBudget = props.overallStats.month.limit || 0
const currentExpense = props.overallStats.month.current || 0
const trend = props.overallStats.month.trend || []
for (let i = 1; i <= daysInMonth; i++) {
dates.push(`${i}`)
if (isExpense) {
const idealRemaining = totalBudget * (1 - i / daysInMonth)
idealBurndown.push(Math.round(idealRemaining))
if (trend.length > 0) {
const dayValue = trend[i - 1]
if (dayValue !== undefined && dayValue !== null) {
const actualRemaining = totalBudget - dayValue
actualBurndown.push(Math.round(actualRemaining))
} else {
actualBurndown.push(null)
}
} else {
if (i <= currentDay && totalBudget > 0) {
const actualRemaining = totalBudget - (currentExpense * i) / currentDay
actualBurndown.push(Math.round(actualRemaining))
} else {
actualBurndown.push(null)
}
}
} else {
const idealAccumulated = Math.min(totalBudget, totalBudget * (i / daysInMonth))
idealBurndown.push(Math.round(idealAccumulated))
if (trend.length > 0) {
const dayValue = trend[i - 1]
if (dayValue !== undefined && dayValue !== null) {
actualBurndown.push(Math.round(dayValue))
} else {
actualBurndown.push(null)
}
} else {
if (i <= currentDay && totalBudget > 0) {
const actualAccumulated = Math.min(totalBudget, (currentExpense * i) / currentDay)
actualBurndown.push(Math.round(actualAccumulated))
} else {
actualBurndown.push(null)
}
}
}
}
const idealSeriesName = isExpense ? '理想燃尽' : '理想积累'
const actualSeriesName = isExpense ? '实际燃尽' : '实际积累'
return {
labels: dates,
datasets: [
{
label: idealSeriesName,
data: idealBurndown,
borderColor: getCssVar('--chart-warning'),
backgroundColor: getCssVar('--chart-warning'),
borderWidth: 2,
borderDash: [5, 5],
tension: 0,
pointRadius: 0
},
{
label: actualSeriesName,
data: actualBurndown,
borderColor: getCssVar('--chart-primary'),
backgroundColor: getCssVar('--chart-primary'),
borderWidth: 2,
tension: 0,
pointRadius: 0
}
]
}
})
const burndownChartOptions = computed(() => {
return getChartOptions({
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
pointStyle: 'line',
boxWidth: 20
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
cornerRadius: 8,
callbacks: {
label: (context) => {
const value = context.parsed.y
if (value !== null && value !== undefined) {
return `${context.dataset.label}: ¥${formatMoney(value)}`
}
return ''
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
maxTicksLimit: 10,
font: {
family: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif',
size: 10
}
}
},
y: {
ticks: {
callback: (value) => {
if (value >= 10000) {
return (value / 10000).toFixed(0) + 'w'
}
return value
},
font: {
size: 10
}
}
}
}
})
})
// 年度燃尽图数据
const yearBurndownChartData = computed(() => {
// 防御性检查
if (!props.overallStats || !props.overallStats.year || !props.selectedDate) {
return {
labels: [],
datasets: []
}
}
const refDate = props.selectedDate
const year = refDate.getFullYear()
const now = new Date()
const currentYear = now.getFullYear()
const currentMonth = now.getMonth()
const isExpense = props.activeTab === BudgetCategory.Expense
const months = []
const idealBurndown = []
const actualBurndown = []
const totalBudget = props.overallStats.year.limit || 0
const currentExpense = props.overallStats.year.current || 0
const trend = props.overallStats.year.trend || []
for (let i = 0; i < 12; i++) {
months.push(`${i + 1}`)
if (isExpense) {
const idealRemaining = totalBudget * (1 - (i + 1) / 12)
idealBurndown.push(Math.round(idealRemaining))
if (trend.length > 0) {
const monthValue = trend[i]
if (monthValue !== undefined && monthValue !== null) {
const actualRemaining = totalBudget - monthValue
actualBurndown.push(Math.round(actualRemaining))
} else {
actualBurndown.push(null)
}
} else {
const isFuture = year > currentYear || (year === currentYear && i > currentMonth)
if (!isFuture && totalBudget > 0) {
const progress = (i + 1) / 12
const actualRemaining = totalBudget - currentExpense * progress
actualBurndown.push(Math.round(actualRemaining))
} else {
actualBurndown.push(null)
}
}
} else {
const idealAccumulated = Math.min(totalBudget, totalBudget * ((i + 1) / 12))
idealBurndown.push(Math.round(idealAccumulated))
if (trend.length > 0) {
const monthValue = trend[i]
if (monthValue !== undefined && monthValue !== null) {
actualBurndown.push(Math.round(monthValue))
} else {
actualBurndown.push(null)
}
} else {
const isFuture = year > currentYear || (year === currentYear && i > currentMonth)
if (!isFuture && totalBudget > 0) {
const progress = (i + 1) / 12
const actualAccumulated = Math.min(totalBudget, currentExpense * progress)
actualBurndown.push(Math.round(actualAccumulated))
} else {
actualBurndown.push(null)
}
}
}
}
const idealSeriesName = isExpense ? '理想支出' : '理想收入'
const actualSeriesName = isExpense ? '实际支出' : '实际收入'
return {
labels: months,
datasets: [
{
label: idealSeriesName,
data: idealBurndown,
borderColor: getCssVar('--chart-warning'),
backgroundColor: getCssVar('--chart-warning'),
borderWidth: 2,
borderDash: [5, 5],
tension: 0,
pointRadius: 0
},
{
label: actualSeriesName,
data: actualBurndown,
borderColor: getCssVar('--chart-primary'),
backgroundColor: getCssVar('--chart-primary'),
borderWidth: 2,
tension: 0,
pointRadius: 0
}
]
}
})
const yearBurndownChartOptions = computed(() => {
return getChartOptions({
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
pointStyle: 'line',
boxWidth: 20
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
cornerRadius: 8,
callbacks: {
label: (context) => {
const value = context.parsed.y
if (value !== null && value !== undefined) {
return `${context.dataset.label}: ¥${formatMoney(value)}`
}
return ''
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
font: {
size: 10
}
}
},
y: {
ticks: {
callback: (value) => {
if (value >= 10000) {
return (value / 10000).toFixed(0) + 'w'
}
return value
},
font: {
size: 10
}
}
}
}
})
})
</script>
<style scoped>
.chart-analysis-container {
padding: var(--spacing-lg, 12px);
padding-bottom: 80px;
}
.gauges-row {
display: flex;
gap: var(--spacing-xl, 16px);
margin-bottom: var(--spacing-xl, 16px);
}
.chart-card {
background: var(--van-background-2);
border-radius: var(--radius-lg, 12px);
padding: var(--spacing-xl, 16px);
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.04));
}
.gauge-card {
flex: 1;
min-width: 0;
/* 防止 flex 子项溢出 */
padding: var(--spacing-lg, 12px);
/* 减小内边距 */
}
.chart-card-spacing {
margin-top: var(--spacing-lg, 12px);
}
.popup-content-padding {
padding: var(--spacing-xl, 16px);
}
.gauge-wrapper {
position: relative;
width: 100%;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
}
.gauge-text-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
margin-top: 8px;
}
.balance-label {
font-size: 11px;
color: var(--van-text-color-2);
letter-spacing: 1px;
}
.balance-value {
margin-top: 6px;
font-size: 24px;
font-weight: 700;
font-family:
DIN Alternate,
system-ui;
color: var(--van-text-color);
line-height: 1.1;
}
.chart-header {
margin-bottom: 12px;
}
.chart-title {
font-size: 14px;
/* 调小标题 */
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 8px;
}
.chart-title-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.info-icon {
flex-shrink: 0;
cursor: pointer;
padding: 4px;
margin: -4px;
}
.chart-subtitle {
font-size: 10px;
color: var(--van-text-color-2);
}
.chart-body {
width: 100%;
}
.gauge-chart {
height: 120px;
/* 调小高度 */
}
.variance-chart {
min-height: 200px;
}
.burndown-chart {
height: 190px;
}
.gauge-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 6px;
margin-top: -6px;
}
.gauge-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
}
.gauge-item .label {
font-size: 10px;
color: var(--van-text-color-2);
margin-bottom: 4px;
}
.gauge-item .value {
font-size: 12px;
font-weight: 600;
font-family:
DIN Alternate,
system-ui;
color: var(--van-text-color);
}
/* expand styles removed as they are no longer used */
</style>