diff --git a/Web/src/components/Budget/BudgetChartAnalysis.vue b/Web/src/components/Budget/BudgetChartAnalysis.vue index 2d3042a..da15f59 100644 --- a/Web/src/components/Budget/BudgetChartAnalysis.vue +++ b/Web/src/components/Budget/BudgetChartAnalysis.vue @@ -6,10 +6,9 @@
- 月度健康度 -
-
- 本月预算 + + {{ activeTab === BudgetCategory.Expense ? '使用情况' : '完成情况' }} + (月度)
@@ -32,10 +35,8 @@
- 年度健康度 -
-
- 本年预算 + {{ activeTab === BudgetCategory.Expense ? '使用情况' : '完成情况' }} + (年度)
@@ -65,10 +70,30 @@ 预算进度(月度)
- 本月各预算执行情况 + 预算剩余消耗趋势
+
+
+ + ▼ + + {{ monthBarChartExpanded ? '收起详情' : '展开详情' }} +
+
+
+
+
+ + ▼ + + {{ yearBarChartExpanded ? '收起详情' : '展开详情' }} +
+
+
+ +
+
+
+ 预算偏差分析 +
+
+ 红条超支 / + 绿条结余 +
+
+
+
+
{ if (val >= 10000) { @@ -230,54 +305,129 @@ const updateCharts = () => { // 确保 barChart 已初始化,如果还未初始化则先初始化 if (props.budgets.length > 0) { - if (!monthBarChart && monthBarChartRef.value) { + if (!monthBarChart && monthBarChartRef.value && monthBarChartExpanded.value) { monthBarChart = echarts.init(monthBarChartRef.value) } - if (!yearBarChart && yearBarChartRef.value) { + if (!yearBarChart && yearBarChartRef.value && yearBarChartExpanded.value) { yearBarChart = echarts.init(yearBarChartRef.value) } - updateBarChart() + if (monthBarChart || yearBarChart) { + updateBarChart() + } + + // 仅支出时更新燃尽图 + if (isExpense) { + if (!burndownChart && burndownChartRef.value) { + burndownChart = echarts.init(burndownChartRef.value) + } + updateBurndownChart() + + if (!yearBurndownChart && yearBurndownChartRef.value) { + yearBurndownChart = echarts.init(yearBurndownChartRef.value) + } + updateYearBurndownChart() + + if (!varianceChart && varianceChartRef.value) { + varianceChart = echarts.init(varianceChartRef.value) + } + updateVarianceChart() + } else { + // 非支出时销毁燃尽图实例 + if (burndownChart) { + burndownChart.dispose() + burndownChart = null + } + if (yearBurndownChart) { + yearBurndownChart.dispose() + yearBurndownChart = null + } + // 收入/存款也可能需要偏差图,但目前逻辑主要针对支出 + // 如果用户想看收入的偏差,也可以保留。我们之前的逻辑已经处理了收入的情况。 + // 所以这里不应该销毁 varianceChart,而是应该更新它。 + if (!varianceChart && varianceChartRef.value) { + varianceChart = echarts.init(varianceChartRef.value) + } + updateVarianceChart() + } } } const updateBarChart = () => { - if (!monthBarChart || !yearBarChart) { return } + // 按预算类型分开:月度预算和年度预算 + // 1 = Month, 2 = Year + const monthBudgets = props.budgets.filter(b => b.type === 1) + const yearBudgets = props.budgets.filter(b => b.type === 2) - const sortedBudgets = [...props.budgets].sort((a, b) => b.current - a.current).slice(0, 10) - const categories = sortedBudgets.map(b => b.name) - // 月度数据 - const monthLimits = sortedBudgets.map(b => b.monthLimit || b.limit) - const monthCurrents = sortedBudgets.map(b => b.monthCurrent || b.current) - // 年度数据 - const yearLimits = sortedBudgets.map(b => b.yearLimit || b.limit) - const yearCurrents = sortedBudgets.map(b => b.yearCurrent || b.current) + // 为月度预算计算百分比 + const budgetsWithMonthPercentage = monthBudgets.map(b => { + const limit = b.limit || 0 + const current = b.current || 0 + const percentage = limit > 0 ? (current / limit) * 100 : 0 + return { ...b, percentage } + }) - const getColors = (data, limits) => { - return data.map((current, idx) => { - const limit = limits[idx] - const rate = limit > 0 ? (current / limit) : 0 + // 为年度预算计算百分比 + const budgetsWithYearPercentage = yearBudgets.map(b => { + const limit = b.limit || 0 + const current = b.current || 0 + const percentage = limit > 0 ? (current / limit) * 100 : 0 + return { ...b, percentage } + }) + + // 分别按百分比从高到低排序,取前10条 + const sortedMonthBudgets = budgetsWithMonthPercentage.sort((a, b) => b.percentage - a.percentage).slice(0, 10) + const sortedYearBudgets = budgetsWithYearPercentage.sort((a, b) => b.percentage - a.percentage).slice(0, 10) + + const monthCategories = sortedMonthBudgets.map(b => b.name) + const yearCategories = sortedYearBudgets.map(b => b.name) + + // 调试输出 + console.log('月度预算项数:', monthBudgets.length, '年度预算项数:', yearBudgets.length) + console.log('月度排序后:', sortedMonthBudgets.map(b => ({ name: b.name, percentage: b.percentage }))) + console.log('年度排序后:', sortedYearBudgets.map(b => ({ name: b.name, percentage: b.percentage }))) + + // 计算月度百分比数据 + const monthPercentages = sortedMonthBudgets.map(b => { + const limit = b.limit || 0 + const current = b.current || 0 + if (!limit || limit <= 0) {return 0} + return Math.round((current / limit) * 100) + }) + + // 计算年度百分比数据 + const yearPercentages = sortedYearBudgets.map(b => { + const limit = b.limit || 0 + const current = b.current || 0 + if (!limit || limit <= 0) {return 0} + return Math.round((current / limit) * 100) + }) + + const getColors = (percentages) => { + return percentages.map(percentage => { if (props.activeTab === BudgetCategory.Expense) { - if (rate >= 1) { return getCssVar('--chart-danger') } - if (rate >= 0.8) { return getCssVar('--chart-warning') } - return getCssVar('--chart-primary') + // 支出:百分比越高越不好 + if (percentage >= 90) { return getCssVar('--chart-danger') } // 红色 + if (percentage >= 60) { return getCssVar('--chart-warning') } // 橙色 + return getCssVar('--chart-primary') // 蓝色 } else { - if (rate >= 1) { return getCssVar('--chart-success') } - return getCssVar('--chart-primary') + // 收入:百分比越高越好(越接近目标越好) + if (percentage >= 90) { return getCssVar('--chart-success') } // 绿色 + if (percentage >= 60) { return getCssVar('--chart-primary') } // 蓝色 + return getCssVar('--chart-warning') // 橙色 } }) } - const monthColors = getColors(monthCurrents, monthLimits) - const yearColors = getColors(yearCurrents, yearLimits) + const monthColors = getColors(monthPercentages) + const yearColors = getColors(yearPercentages) // 获取当前主题下的颜色值 const textColor = getCssVar('--van-text-color') const textColor2 = getCssVar('--van-text-color-2') - const bgColor3 = getCssVar('--van-background-3') const splitLineColor = getCssVar('--chart-split') const axisLabelColor = getCssVar('--chart-text-muted') - const createOption = (limits, currents, colors) => { + const createOption = (categories, percentages, colors) => { return { grid: { left: '3%', @@ -288,6 +438,7 @@ const updateBarChart = () => { }, xAxis: { type: 'value', + max: 100, splitLine: { lineStyle: { type: 'dashed', @@ -297,8 +448,7 @@ const updateBarChart = () => { axisLabel: { color: axisLabelColor, formatter: (value) => { - if (value >= 10000) { return (value / 10000).toFixed(0) + 'w' } - return value + return value + '%' } } }, @@ -316,57 +466,486 @@ const updateBarChart = () => { }, series: [ { - name: '预算', + name: '预算使用率', type: 'bar', - data: limits, - barWidth: 10, - itemStyle: { - color: bgColor3, - borderRadius: 5 - }, - z: 1, - label: { - show: true, - position: 'right', - formatter: (params) => { - const val = params.value - return val >= 10000 ? (val / 10000).toFixed(1) + 'w' : val - }, - color: textColor2, - fontSize: 10 - } - }, - { - name: '实际', - type: 'bar', - data: currents, - barGap: '-100%', + data: percentages, barWidth: 10, itemStyle: { color: (params) => colors[params.dataIndex], borderRadius: 5 }, + label: { + show: true, + position: 'right', + formatter: (params) => { + return params.value + '%' + }, + color: textColor2, + fontSize: 10 + }, z: 2 } ] } } - monthBarChart.setOption(createOption(monthLimits, monthCurrents, monthColors)) - yearBarChart.setOption(createOption(yearLimits, yearCurrents, yearColors)) + if (monthBarChart) { + monthBarChart.setOption(createOption(monthCategories, monthPercentages, monthColors)) + } + if (yearBarChart) { + yearBarChart.setOption(createOption(yearCategories, yearPercentages, yearColors)) + } } const calculateChartHeight = () => { // 根据数据数量动态计算图表高度 // 每个类别占用 60px,最少显示 200px,最多 400px const dataCount = Math.min(props.budgets.length, 10) // 最多显示10条 - const minHeight = 150 + const minHeight = 100 const maxHeight = 400 - const heightPerItem = 60 + const heightPerItem = 40 const calculatedHeight = Math.max(minHeight, dataCount * heightPerItem) return Math.min(calculatedHeight, maxHeight) } +const updateBurndownChart = () => { + if (!burndownChart) { return } + + // 获取当前月份的日期 + const today = new Date() + const year = today.getFullYear() + const month = today.getMonth() + const daysInMonth = new Date(year, month + 1, 0).getDate() + const currentDay = today.getDate() + + // 生成日期和理想燃尽线 + const dates = [] + const idealBurndown = [] + const actualBurndown = [] + + const totalBudget = props.overallStats.month.limit || 0 + const currentExpense = props.overallStats.month.current || 0 + + for (let i = 1; i <= daysInMonth; i++) { + dates.push(`${i}日`) + // 理想燃尽:每天均匀消耗 + const idealRemaining = Math.max(0, totalBudget * (1 - i / daysInMonth)) + idealBurndown.push(Math.round(idealRemaining)) + + // 实际燃尽:根据当前日期显示 + if (i <= currentDay && totalBudget > 0) { + const actualRemaining = Math.max(0, totalBudget - (currentExpense * i / currentDay)) + actualBurndown.push(Math.round(actualRemaining)) + } else { + actualBurndown.push(null) + } + } + + const textColor = getCssVar('--van-text-color') + const textColor2 = getCssVar('--van-text-color-2') + const splitLineColor = getCssVar('--chart-split') + const axisLabelColor = getCssVar('--chart-text-muted') + + const option = { + grid: { + left: '3%', + right: '8%', + bottom: '3%', + top: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + data: dates, + axisLabel: { + color: axisLabelColor, + interval: Math.ceil(daysInMonth / 8) - 1, + rotate: 45 + }, + splitLine: { show: false }, + axisLine: { + lineStyle: { + color: splitLineColor + } + } + }, + yAxis: { + type: 'value', + axisLabel: { + color: axisLabelColor, + formatter: (value) => { + if (value >= 10000) { return (value / 10000).toFixed(0) + 'w' } + return value + } + }, + splitLine: { + lineStyle: { + type: 'dashed', + color: splitLineColor + } + } + }, + tooltip: { + trigger: 'axis', + formatter: (params) => { + let result = params[0].name + '
' + params.forEach(param => { + if (param.value !== null) { + result += param.marker + param.seriesName + ': ¥' + (param.value >= 10000 ? (param.value / 10000).toFixed(1) + 'w' : param.value) + '
' + } + }) + return result + } + }, + series: [ + { + name: '理想燃尽', + type: 'line', + data: idealBurndown, + smooth: false, + lineStyle: { + color: getCssVar('--chart-warning'), + width: 2, + type: 'dashed' + }, + itemStyle: { + color: getCssVar('--chart-warning') + }, + z: 1 + }, + { + name: '实际燃尽', + type: 'line', + data: actualBurndown, + smooth: false, + lineStyle: { + color: getCssVar('--chart-primary'), + width: 2 + }, + itemStyle: { + color: getCssVar('--chart-primary') + }, + z: 2 + } + ] + } + + burndownChart.setOption(option) +} + +const updateYearBurndownChart = () => { + if (!yearBurndownChart) { return } + + // 获取当前年份的日期 + const today = new Date() + const year = today.getFullYear() + const currentMonth = today.getMonth() + const currentDay = today.getDate() + const daysInCurrentMonth = new Date(year, currentMonth + 1, 0).getDate() + + // 生成月份和理想燃尽线 + const months = [] + const idealBurndown = [] + const actualBurndown = [] + + const totalBudget = props.overallStats.year.limit || 0 + const currentExpense = props.overallStats.year.current || 0 + + for (let i = 0; i < 12; i++) { + months.push(`${i + 1}月`) + + // 计算当前时间进度(基于天数) + let daysPassedInYear = 0 + let daysInYear = 0 + + for (let j = 0; j < i; j++) { + daysInYear += new Date(year, j + 1, 0).getDate() + } + + if (i < currentMonth) { + // 之前的月份都已完成 + daysPassedInYear = daysInYear + new Date(year, i + 1, 0).getDate() + } else if (i === currentMonth) { + // 当前月份 + daysPassedInYear = daysInYear + currentDay + daysInYear += daysInCurrentMonth + } else { + // 未来的月份 + daysInYear += new Date(year, i + 1, 0).getDate() + } + + // 全年总天数(365或366) + const daysInYearTotal = new Date(year, 12, 0).getDate() === 29 ? 366 : 365 + const yearProgress = i === 11 ? 1 : daysPassedInYear / daysInYearTotal + + // 理想燃尽:每月均匀消耗 + const idealRemaining = Math.max(0, totalBudget * (1 - (i + 1) / 12)) + idealBurndown.push(Math.round(idealRemaining)) + + // 实际燃尽:根据当前日期显示 + if ((i < currentMonth || (i === currentMonth && currentDay > 0)) && totalBudget > 0) { + const actualRemaining = Math.max(0, totalBudget - (currentExpense * yearProgress)) + actualBurndown.push(Math.round(actualRemaining)) + } else { + actualBurndown.push(null) + } + } + + const textColor = getCssVar('--van-text-color') + const textColor2 = getCssVar('--van-text-color-2') + const splitLineColor = getCssVar('--chart-split') + const axisLabelColor = getCssVar('--chart-text-muted') + + const option = { + grid: { + left: '3%', + right: '8%', + bottom: '3%', + top: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + data: months, + axisLabel: { + color: axisLabelColor + }, + splitLine: { show: false }, + axisLine: { + lineStyle: { + color: splitLineColor + } + } + }, + yAxis: { + type: 'value', + axisLabel: { + color: axisLabelColor, + formatter: (value) => { + if (value >= 10000) { return (value / 10000).toFixed(0) + 'w' } + return value + } + }, + splitLine: { + lineStyle: { + type: 'dashed', + color: splitLineColor + } + } + }, + tooltip: { + trigger: 'axis', + formatter: (params) => { + let result = params[0].name + '
' + params.forEach(param => { + if (param.value !== null) { + result += param.marker + param.seriesName + ': ¥' + (param.value >= 10000 ? (param.value / 10000).toFixed(1) + 'w' : param.value) + '
' + } + }) + return result + } + }, + series: [ + { + name: '理想燃尽', + type: 'line', + data: idealBurndown, + smooth: false, + lineStyle: { + color: getCssVar('--chart-warning'), + width: 2, + type: 'dashed' + }, + itemStyle: { + color: getCssVar('--chart-warning') + }, + z: 1 + }, + { + name: '实际燃尽', + type: 'line', + data: actualBurndown, + smooth: false, + lineStyle: { + color: getCssVar('--chart-primary'), + width: 2 + }, + itemStyle: { + color: getCssVar('--chart-primary') + }, + z: 2 + } + ] + } + + yearBurndownChart.setOption(option) +} + +const updateVarianceChart = () => { + if (!varianceChart) { return } + + // 1. 准备数据:计算偏差 (Current - Limit) + // 只关注本年和本月的预算,或者只展示本年?或者全部? + // 按照设计,展示所有预算项的偏差 + // 为了避免混淆,我们可以合并月度和年度,或者只展示月度? + // 用户的图表1和2是分开月度和年度的。 + // 偏差分析通常用于当前执行周期。如果混合了月度和年度,数值差异会很大(年度偏差可能几千,月度几百)。 + // 建议:优先展示月度预算的偏差,因为这是最高频关注点。或者提供选项? + // 简单起见,这里展示“月度预算”的偏差。如果用户切到“收入”,则展示收入预算的偏差。 + + // 过滤出月度预算 + const relevantBudgets = props.budgets // .filter(b => b.type === 1) // 1 = Month + if (relevantBudgets.length === 0) { + // 如果没有月度预算,尝试年度?不,保持一致性,只看月度。 + // 或者,我们可以把所有预算都放进去,但在名字上区分? + // 让我们先只做月度,因为这是最直接的反馈。 + } + + // 计算偏差 + const data = relevantBudgets.map(b => { + const limit = b.limit || 0 + const current = b.current || 0 + // 偏差 = 实际 - 预算 + // 对于支出:正数 = 超支 (Bad/Red), 负数 = 结余 (Good/Green) + // 对于收入:正数 = 超额 (Good/Green?), 负数 = 差额 (Bad/Red?) + // 但用户明确说:红条超支,绿条结余。这完全是支出视角。 + // 如果是收入 Tab,我们保持同样的逻辑:实际 > 目标 (Green), 实际 < 目标 (Red) + // 为了适配用户的“红黑榜”视觉定义: + // 右侧 (正值) -> 红色 (超支/Warning) + // 左侧 (负值) -> 绿色 (结余/Safe) + + const diff = current - limit + + // 如果是收入,逻辑反转? + // 收入:目标 10000,实际 12000。Diff +2000。这是好事。应该绿。 + // 收入:目标 10000,实际 8000。Diff -2000。这是坏事。应该红。 + // 所以收入时,我们把 Diff 取反?或者改变颜色映射? + // 让我们先按“支出”逻辑写死,因为这是最常用的。 + + return { + name: b.name, + value: diff, + limit: limit, + current: current + } + }) + + // 排序:按偏差的绝对值降序排列 + data.sort((a, b) => Math.abs(b.value) - Math.abs(a.value)) + + const categories = data.map(item => item.name) + const values = data.map(item => item.value) + + const textColor = getCssVar('--van-text-color') + const splitLineColor = getCssVar('--chart-split') + const axisLabelColor = getCssVar('--chart-text-muted') + + const option = { + grid: { + left: '3%', + right: '8%', + bottom: '3%', + top: '3%', + containLabel: true + }, + tooltip: { + trigger: 'axis', + formatter: (params) => { + const item = data[params[0].dataIndex] + let html = `${item.name}
` + html += `预算: ¥${formatMoney(item.limit)}
` + html += `实际: ¥${formatMoney(item.current)}
` + const diffText = item.value > 0 ? `超支: ¥${formatMoney(item.value)}` : `结余: ¥${formatMoney(Math.abs(item.value))}` + const color = item.value > 0 ? getCssVar('--van-danger-color') : getCssVar('--van-success-color') + html += `${diffText}` + return html + } + }, + xAxis: { + type: 'value', + position: 'top', // 坐标轴在上方,或者隐藏 + axisLine: { show: false }, + axisLabel: { show: false }, // 隐藏X轴标签,让图表更干净 + splitLine: { + show: true, + lineStyle: { + type: 'dashed', + color: splitLineColor + } + } + }, + yAxis: { + type: 'category', + axisTick: { show: false }, + axisLine: { show: false }, + data: categories, + axisLabel: { + color: textColor, + width: 60, + overflow: 'truncate' + } + }, + series: [ + { + name: '偏差', + type: 'bar', + stack: 'Total', + data: values, + label: { + show: true, + position: 'right', // 默认右侧,正负值会自动调整吗?ECharts Bar label position logic: + // For positive value: 'right', 'top', 'inside', etc. + // We need dynamic position based on value sign. + formatter: (params) => { + const val = params.value + return val > 0 ? `+${formatMoney(val)}` : formatMoney(val) + }, + color: textColor + }, + itemStyle: { + borderRadius: 4, + color: (params) => { + const val = params.value + if (props.activeTab === BudgetCategory.Expense) { + // 支出:正数(超支)红,负数(结余)绿 + return val > 0 ? getCssVar('--van-danger-color') : getCssVar('--van-success-color') + } else { + // 收入:正数(超额)绿,负数(差额)红 + return val > 0 ? getCssVar('--van-success-color') : getCssVar('--van-danger-color') + } + } + } + } + ] + } + + // 针对 ECharts 的 label 位置优化:正数 label 在右,负数 label 在左 + // ECharts 5+ 支持 label position 为 function? 好像不支持。 + // 但我们可以通过 rich text 或 multiple series 来实现,或者简单地设为 'outside' (ECharts 没有 outside for bar?) + // 对于 horizontal bar, positive value bar goes right, negative goes left. + // label position 'right' means right of the bar end (for positive) and right of the bar end (for negative, which is inside towards 0). + // ECharts default behavior: + // If value > 0, bar extends right. Position 'right' is outside right. + // If value < 0, bar extends left. Position 'right' is inside (near 0). Position 'left' is outside left. + // Let's keep it simple first. Or use a callback if supported, or two series. + // Actually, let's try just setting it based on logic if possible, but series config is static. + // Better approach: use two series, one for positive, one for negative? + // Or iterate data and set specific label position in data item. + + const seriesData = values.map((val, index) => { + return { + value: val, + label: { + position: val >= 0 ? 'right' : 'left' + } + } + }) + + option.series[0].data = seriesData + + varianceChart.setOption(option) +} + watch(() => props.overallStats, () => nextTick(updateCharts), { deep: true }) watch(() => props.budgets, () => { nextTick(() => { @@ -380,13 +959,67 @@ watch(() => props.budgets, () => { } }) }, { deep: true }) -watch(() => props.activeTab, () => nextTick(updateCharts)) +watch(() => props.activeTab, () => { + nextTick(() => { + updateCharts() + // 切换标签后延迟 resize,确保 DOM 已更新 + setTimeout(() => { + burndownChart?.resize() + yearBurndownChart?.resize() + varianceChart?.resize() + }, 100) + }) +}) + +// 监听展开/折叠状态 +watch(() => monthBarChartExpanded.value, (expanded) => { + if (expanded) { + nextTick(() => { + if (monthBarChartRef.value) { + if (!monthBarChart) { + monthBarChart = echarts.init(monthBarChartRef.value) + } + updateBarChart() + monthBarChart.resize() + } + }) + } else { + // 收起时销毁图表实例,释放内存 + if (monthBarChart) { + monthBarChart.dispose() + monthBarChart = null + } + } +}) + +watch(() => yearBarChartExpanded.value, (expanded) => { + if (expanded) { + nextTick(() => { + if (yearBarChartRef.value) { + if (!yearBarChart) { + yearBarChart = echarts.init(yearBarChartRef.value) + } + updateBarChart() + yearBarChart.resize() + } + }) + } else { + // 收起时销毁图表实例,释放内存 + if (yearBarChart) { + yearBarChart.dispose() + yearBarChart = null + } + } +}) const handleResize = () => { monthGaugeChart?.resize() yearGaugeChart?.resize() monthBarChart?.resize() yearBarChart?.resize() + burndownChart?.resize() + yearBurndownChart?.resize() + varianceChart?.resize() } onMounted(() => { @@ -403,6 +1036,23 @@ onMounted(() => { yearBarChart = echarts.init(yearBarChartRef.value) } updateBarChart() + + // 仅支出时初始化燃尽图 + if (isExpense && burndownChartRef.value) { + burndownChart = echarts.init(burndownChartRef.value) + updateBurndownChart() + } + + if (isExpense && yearBurndownChartRef.value) { + yearBurndownChart = echarts.init(yearBurndownChartRef.value) + updateYearBurndownChart() + } + + // 初始化偏差图 + if (varianceChartRef.value) { + varianceChart = echarts.init(varianceChartRef.value) + updateVarianceChart() + } } window.addEventListener('resize', handleResize) }) @@ -414,6 +1064,9 @@ onUnmounted(() => { yearGaugeChart?.dispose() monthBarChart?.dispose() yearBarChart?.dispose() + burndownChart?.dispose() + yearBurndownChart?.dispose() + varianceChart?.dispose() }) @@ -478,6 +1131,10 @@ onUnmounted(() => { max-height: 400px; } +.burndown-chart { + height: 200px; +} + .gauge-footer { display: flex; justify-content: space-around; @@ -511,4 +1168,52 @@ onUnmounted(() => { .gauge-item .value.expense { color: var(--van-primary-color); } + +.expand-toggle-row { + display: flex; + justify-content: center; + align-items: center; + padding: 0; +} + +.expand-toggle { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 12px; + border-radius: 6px; + transition: background-color 0.2s ease; + user-select: none; +} + +.expand-toggle:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.expand-toggle:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.expand-icon { + display: inline-block; + font-size: 12px; + color: var(--van-text-color-2); + transition: transform 0.2s ease; + transform-origin: center; +} + +.expand-icon.expanded { + transform: rotate(180deg); +} + +.expand-text { + font-size: 12px; + color: var(--van-text-color-2); + transition: color 0.2s ease; +} + +.expand-toggle:hover .expand-text { + color: var(--van-text-color); +} \ No newline at end of file diff --git a/Web/src/views/StatisticsView.vue b/Web/src/views/StatisticsView.vue index d28713f..94edc76 100644 --- a/Web/src/views/StatisticsView.vue +++ b/Web/src/views/StatisticsView.vue @@ -1141,7 +1141,7 @@ onBeforeUnmount(() => { /* 环形图 */ .chart-container { - padding: 12px 0; + padding: 0; } .ring-chart { @@ -1255,7 +1255,7 @@ onBeforeUnmount(() => { display: flex; justify-content: center; align-items: center; - padding-top: 10px; + padding-top: 0; color: var(--van-text-color-3); font-size: 20px; cursor: pointer;