发布
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 11s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 11s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -517,19 +517,11 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
{
|
||||
statistics.TotalExpense += amount;
|
||||
statistics.ExpenseCount++;
|
||||
if (amount > statistics.MaxExpense)
|
||||
{
|
||||
statistics.MaxExpense = amount;
|
||||
}
|
||||
}
|
||||
else if (record.Type == TransactionType.Income)
|
||||
{
|
||||
statistics.TotalIncome += amount;
|
||||
statistics.IncomeCount++;
|
||||
if (amount > statistics.MaxIncome)
|
||||
{
|
||||
statistics.MaxIncome = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,8 +770,6 @@ public class MonthlyStatistics
|
||||
public int ExpenseCount { get; set; }
|
||||
public int IncomeCount { get; set; }
|
||||
public int TotalCount { get; set; }
|
||||
public decimal MaxExpense { get; set; }
|
||||
public decimal MaxIncome { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
* @returns {Object} data.expenseCount - 支出笔数
|
||||
* @returns {Object} data.incomeCount - 收入笔数
|
||||
* @returns {Object} data.totalCount - 总笔数
|
||||
* @returns {Object} data.maxExpense - 最大单笔支出
|
||||
* @returns {Object} data.maxIncome - 最大单笔收入
|
||||
*/
|
||||
export const getMonthlyStatistics = (params) => {
|
||||
return request({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
/*
|
||||
Most variables are replaced by Vant CSS variables.
|
||||
Keeping only what's necessary or mapping to Vant.
|
||||
@@ -17,6 +17,55 @@
|
||||
--color-text: var(--van-text-color);
|
||||
|
||||
--section-gap: 160px;
|
||||
|
||||
/* Chart Colors */
|
||||
--chart-color-1: #FF6B6B;
|
||||
--chart-color-2: #4ECDC4;
|
||||
--chart-color-3: #45B7D1;
|
||||
--chart-color-4: #FFA07A;
|
||||
--chart-color-5: #98D8C8;
|
||||
--chart-color-6: #F7DC6F;
|
||||
--chart-color-7: #BB8FCE;
|
||||
--chart-color-8: #85C1E2;
|
||||
--chart-color-9: #F8B88B;
|
||||
--chart-color-10: #AAB7B8;
|
||||
--chart-color-11: #FF8ED4;
|
||||
--chart-color-12: #67E6DC;
|
||||
--chart-color-13: #FFAB73;
|
||||
--chart-color-14: #C9B1FF;
|
||||
--chart-color-15: #7BDFF2;
|
||||
|
||||
/* Status Colors for Charts */
|
||||
--chart-success: #52c41a;
|
||||
--chart-warning: #faad14;
|
||||
--chart-danger: #f5222d;
|
||||
--chart-primary: #1890ff;
|
||||
--chart-shadow: rgba(0,138,255,0.45);
|
||||
--chart-axis: #E6EBF8;
|
||||
--chart-split: #eee;
|
||||
--chart-text-muted: #999;
|
||||
|
||||
/* Heatmap Colors - Light Mode */
|
||||
--heatmap-level-0: var(--van-gray-2);
|
||||
--heatmap-level-1: #9be9a8;
|
||||
--heatmap-level-2: #40c463;
|
||||
--heatmap-level-3: #30a14e;
|
||||
--heatmap-level-4: #216e39;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--chart-axis: #333;
|
||||
--chart-split: #333;
|
||||
--chart-text-muted: #666;
|
||||
|
||||
/* Heatmap Colors - Dark Mode (GitHub Style) */
|
||||
--heatmap-level-0: #161b22;
|
||||
--heatmap-level-1: #0e4429;
|
||||
--heatmap-level-2: #006d32;
|
||||
--heatmap-level-3: #26a641;
|
||||
--heatmap-level-4: #39d353;
|
||||
}
|
||||
}
|
||||
|
||||
/* Removed manual dark mode media query as Vant handles it */
|
||||
|
||||
514
Web/src/components/Budget/BudgetChartAnalysis.vue
Normal file
514
Web/src/components/Budget/BudgetChartAnalysis.vue
Normal file
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<div class="chart-analysis-container">
|
||||
<!-- 仪表盘:整体健康度 -->
|
||||
<div class="gauges-row">
|
||||
<!-- 月度仪表盘 -->
|
||||
<div class="chart-card gauge-card">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
月度健康度
|
||||
</div>
|
||||
<div class="chart-subtitle">
|
||||
本月预算
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="monthGaugeRef"
|
||||
class="chart-body gauge-chart"
|
||||
/>
|
||||
<div class="gauge-footer compact">
|
||||
<div class="gauge-item">
|
||||
<span class="label">已用</span>
|
||||
<span class="value expense">¥{{ formatMoney(overallStats.month.current) }}</span>
|
||||
</div>
|
||||
<div class="gauge-item">
|
||||
<span class="label">预算</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">
|
||||
年度健康度
|
||||
</div>
|
||||
<div class="chart-subtitle">
|
||||
本年预算
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="yearGaugeRef"
|
||||
class="chart-body gauge-chart"
|
||||
/>
|
||||
<div class="gauge-footer compact">
|
||||
<div class="gauge-item">
|
||||
<span class="label">已用</span>
|
||||
<span class="value expense">¥{{ formatMoney(overallStats.year.current) }}</span>
|
||||
</div>
|
||||
<div class="gauge-item">
|
||||
<span class="label">预算</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">
|
||||
本月各预算执行情况
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="monthBarChartRef"
|
||||
class="chart-body bar-chart"
|
||||
:style="{ height: calculateChartHeight() + 'px' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 年度预算进度 -->
|
||||
<div
|
||||
v-if="budgets.length > 0"
|
||||
class="chart-card"
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
预算进度(年度)
|
||||
</div>
|
||||
<div class="chart-subtitle">
|
||||
本年各预算执行情况
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="yearBarChartRef"
|
||||
class="chart-body bar-chart"
|
||||
:style="{ height: calculateChartHeight() + 'px' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态占位 -->
|
||||
<div
|
||||
v-else
|
||||
class="chart-card empty-card"
|
||||
>
|
||||
<van-empty
|
||||
description="暂无预算数据"
|
||||
image="search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { BudgetCategory } from '@/constants/enums'
|
||||
import { getCssVar } from '@/utils/theme'
|
||||
|
||||
const props = defineProps({
|
||||
overallStats: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
budgets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
activeTab: {
|
||||
type: [Number, String],
|
||||
default: BudgetCategory.Expense
|
||||
}
|
||||
})
|
||||
|
||||
const monthGaugeRef = ref(null)
|
||||
const yearGaugeRef = ref(null)
|
||||
const monthBarChartRef = ref(null)
|
||||
const yearBarChartRef = ref(null)
|
||||
let monthGaugeChart = null
|
||||
let yearGaugeChart = null
|
||||
let monthBarChart = null
|
||||
let yearBarChart = null
|
||||
|
||||
const formatMoney = (val) => {
|
||||
if (val >= 10000) {
|
||||
return (val / 10000).toFixed(1) + 'w'
|
||||
}
|
||||
return parseFloat(val || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
})
|
||||
}
|
||||
|
||||
const initGaugeChart = (chartInstance, dom, data, isExpense) => {
|
||||
if (!dom) { return null }
|
||||
|
||||
const chart = echarts.init(dom)
|
||||
updateSingleGauge(chart, data, isExpense)
|
||||
return chart
|
||||
}
|
||||
|
||||
const updateSingleGauge = (chart, data, isExpense) => {
|
||||
if (!chart) { return }
|
||||
|
||||
const rate = parseFloat(data.rate || 0)
|
||||
// 颜色逻辑
|
||||
let color = getCssVar('--chart-success') // 绿色
|
||||
if (isExpense) {
|
||||
if (rate >= 100) { color = getCssVar('--chart-danger') } // 红色
|
||||
else if (rate >= 80) { color = getCssVar('--chart-warning') } // 橙色
|
||||
} else {
|
||||
if (rate >= 100) { color = getCssVar('--chart-success') } // 绿色
|
||||
else if (rate >= 80) { color = getCssVar('--chart-warning') } // 橙色
|
||||
else { color = getCssVar('--chart-danger') } // 红色
|
||||
}
|
||||
|
||||
const option = {
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
min: 0,
|
||||
max: Math.max(100, rate * 1.2),
|
||||
splitNumber: 5,
|
||||
radius: '110%', // 放大一点以适应小卡片
|
||||
center: ['50%', '75%'],
|
||||
itemStyle: {
|
||||
color: color,
|
||||
shadowColor: getCssVar('--chart-shadow'),
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 2
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
roundCap: true,
|
||||
width: 12 // 变细一点
|
||||
},
|
||||
pointer: { show: false },
|
||||
axisLine: {
|
||||
roundCap: true,
|
||||
lineStyle: {
|
||||
width: 12,
|
||||
color: [[1, getCssVar('--chart-axis')]]
|
||||
}
|
||||
},
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
fontSize: 24, // 字体调小
|
||||
offsetCenter: [0, -5],
|
||||
color: 'var(--van-text-color)',
|
||||
formatter: '{value}%',
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'DIN Alternate, system-ui'
|
||||
},
|
||||
data: [{ value: rate }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
const updateCharts = () => {
|
||||
const isExpense = props.activeTab === BudgetCategory.Expense
|
||||
updateSingleGauge(monthGaugeChart, props.overallStats.month, isExpense)
|
||||
updateSingleGauge(yearGaugeChart, props.overallStats.year, isExpense)
|
||||
|
||||
// 确保 barChart 已初始化,如果还未初始化则先初始化
|
||||
if (props.budgets.length > 0) {
|
||||
if (!monthBarChart && monthBarChartRef.value) {
|
||||
monthBarChart = echarts.init(monthBarChartRef.value)
|
||||
}
|
||||
if (!yearBarChart && yearBarChartRef.value) {
|
||||
yearBarChart = echarts.init(yearBarChartRef.value)
|
||||
}
|
||||
updateBarChart()
|
||||
}
|
||||
}
|
||||
|
||||
const updateBarChart = () => {
|
||||
if (!monthBarChart || !yearBarChart) { return }
|
||||
|
||||
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 getColors = (data, limits) => {
|
||||
return data.map((current, idx) => {
|
||||
const limit = limits[idx]
|
||||
const rate = limit > 0 ? (current / limit) : 0
|
||||
if (props.activeTab === BudgetCategory.Expense) {
|
||||
if (rate >= 1) { return getCssVar('--chart-danger') }
|
||||
if (rate >= 0.8) { return getCssVar('--chart-warning') }
|
||||
return getCssVar('--chart-primary')
|
||||
} else {
|
||||
if (rate >= 1) { return getCssVar('--chart-success') }
|
||||
return getCssVar('--chart-primary')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const monthColors = getColors(monthCurrents, monthLimits)
|
||||
const yearColors = getColors(yearCurrents, yearLimits)
|
||||
|
||||
// 获取当前主题下的颜色值
|
||||
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) => {
|
||||
return {
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '8%',
|
||||
bottom: '3%',
|
||||
top: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: splitLineColor
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: axisLabelColor,
|
||||
formatter: (value) => {
|
||||
if (value >= 10000) { return (value / 10000).toFixed(0) + 'w' }
|
||||
return value
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: textColor,
|
||||
width: 50,
|
||||
overflow: 'truncate',
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
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%',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
color: (params) => colors[params.dataIndex],
|
||||
borderRadius: 5
|
||||
},
|
||||
z: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
monthBarChart.setOption(createOption(monthLimits, monthCurrents, monthColors))
|
||||
yearBarChart.setOption(createOption(yearLimits, yearCurrents, yearColors))
|
||||
}
|
||||
|
||||
const calculateChartHeight = () => {
|
||||
// 根据数据数量动态计算图表高度
|
||||
// 每个类别占用 60px,最少显示 200px,最多 400px
|
||||
const dataCount = Math.min(props.budgets.length, 10) // 最多显示10条
|
||||
const minHeight = 150
|
||||
const maxHeight = 400
|
||||
const heightPerItem = 60
|
||||
const calculatedHeight = Math.max(minHeight, dataCount * heightPerItem)
|
||||
return Math.min(calculatedHeight, maxHeight)
|
||||
}
|
||||
|
||||
watch(() => props.overallStats, () => nextTick(updateCharts), { deep: true })
|
||||
watch(() => props.budgets, () => {
|
||||
nextTick(() => {
|
||||
if (props.budgets.length > 0 && (!monthBarChart || !yearBarChart) && monthBarChartRef.value && yearBarChartRef.value) {
|
||||
// budgets 从空到有值,需要初始化图表
|
||||
monthBarChart = echarts.init(monthBarChartRef.value)
|
||||
yearBarChart = echarts.init(yearBarChartRef.value)
|
||||
updateBarChart()
|
||||
} else {
|
||||
updateCharts()
|
||||
}
|
||||
})
|
||||
}, { deep: true })
|
||||
watch(() => props.activeTab, () => nextTick(updateCharts))
|
||||
|
||||
const handleResize = () => {
|
||||
monthGaugeChart?.resize()
|
||||
yearGaugeChart?.resize()
|
||||
monthBarChart?.resize()
|
||||
yearBarChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const isExpense = props.activeTab === BudgetCategory.Expense
|
||||
monthGaugeChart = initGaugeChart(monthGaugeChart, monthGaugeRef.value, props.overallStats.month, isExpense)
|
||||
yearGaugeChart = initGaugeChart(yearGaugeChart, yearGaugeRef.value, props.overallStats.year, isExpense)
|
||||
// 只在有数据时初始化柱状图
|
||||
if (props.budgets.length > 0) {
|
||||
if (monthBarChartRef.value) {
|
||||
monthBarChart = echarts.init(monthBarChartRef.value)
|
||||
}
|
||||
if (yearBarChartRef.value) {
|
||||
yearBarChart = echarts.init(yearBarChartRef.value)
|
||||
}
|
||||
updateBarChart()
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
monthGaugeChart?.dispose()
|
||||
yearGaugeChart?.dispose()
|
||||
monthBarChart?.dispose()
|
||||
yearBarChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-analysis-container {
|
||||
padding: 12px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.gauges-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: var(--van-background-2);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.gauge-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* 防止 flex 子项溢出 */
|
||||
padding: 12px;
|
||||
/* 减小内边距 */
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
/* 调小标题 */
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: 10px;
|
||||
color: var(--van-text-color-2);
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gauge-chart {
|
||||
height: 120px;
|
||||
/* 调小高度 */
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.gauge-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
/* 分散对齐 */
|
||||
align-items: center;
|
||||
margin-top: -20px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gauge-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gauge-item .label {
|
||||
font-size: 10px;
|
||||
color: var(--van-text-color-2);
|
||||
transform: scale(0.9);
|
||||
/* 视觉上更小 */
|
||||
}
|
||||
|
||||
.gauge-item .value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, system-ui;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.gauge-item .value.expense {
|
||||
color: var(--van-primary-color);
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,22 @@
|
||||
<div class="grid-row">
|
||||
<!-- Weekday Labels (Fixed Left) -->
|
||||
<div class="weekday-col-fixed">
|
||||
<div class="weekday-label">二</div>
|
||||
<div class="weekday-label">四</div>
|
||||
<div class="weekday-label">六</div>
|
||||
<div class="weekday-label">
|
||||
二
|
||||
</div>
|
||||
<div class="weekday-label">
|
||||
四
|
||||
</div>
|
||||
<div class="weekday-label">
|
||||
六
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Heatmap Area -->
|
||||
<div ref="scrollContainer" class="heatmap-scroll-container">
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="heatmap-scroll-container"
|
||||
>
|
||||
<div class="heatmap-content">
|
||||
<!-- Month Labels -->
|
||||
<div class="month-row">
|
||||
@@ -25,7 +34,11 @@
|
||||
|
||||
<!-- Heatmap Grid -->
|
||||
<div class="heatmap-grid">
|
||||
<div v-for="(week, wIndex) in weeks" :key="wIndex" class="heatmap-week">
|
||||
<div
|
||||
v-for="(week, wIndex) in weeks"
|
||||
:key="wIndex"
|
||||
class="heatmap-week"
|
||||
>
|
||||
<div
|
||||
v-for="(day, dIndex) in week"
|
||||
:key="dIndex"
|
||||
@@ -42,7 +55,12 @@
|
||||
</div>
|
||||
|
||||
<div class="heatmap-footer">
|
||||
<div v-if="totalCount > 0" class="summary-text">过去一年共 {{ totalCount }} 笔交易</div>
|
||||
<div
|
||||
v-if="totalCount > 0"
|
||||
class="summary-text"
|
||||
>
|
||||
过去一年共 {{ totalCount }} 笔交易
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span>少</span>
|
||||
<div class="legend-item level-0" />
|
||||
@@ -110,7 +128,6 @@ const fetchData = async () => {
|
||||
}
|
||||
|
||||
const avg = last15DaysSum / 15
|
||||
console.log('avg', avg)
|
||||
// Step size calculation: ensure at least 1, roughly avg/2 to create spread
|
||||
// Level 1: 1 ~ step
|
||||
// Level 2: step+1 ~ step*2
|
||||
@@ -127,7 +144,6 @@ const fetchData = async () => {
|
||||
}
|
||||
|
||||
const generateHeatmapData = (startDate, endDate) => {
|
||||
const data = []
|
||||
const current = new Date(startDate)
|
||||
|
||||
const allDays = []
|
||||
@@ -143,7 +159,6 @@ const generateHeatmapData = (startDate, endDate) => {
|
||||
// Sunday (0) -> 6 days back
|
||||
// Tuesday (2) -> 1 day back
|
||||
|
||||
const daysToSubtract = (startDay + 6) % 7
|
||||
// We don't necessarily need to subtract from startDate for data fetching,
|
||||
// but for grid alignment we want the first column to start on Monday.
|
||||
|
||||
@@ -393,62 +408,19 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.level-0 {
|
||||
background-color: var(--van-gray-2);
|
||||
background-color: var(--heatmap-level-0);
|
||||
}
|
||||
/* Default (Light Mode) - Light to Deep Green */
|
||||
.level-1 {
|
||||
background-color: #9be9a8;
|
||||
background-color: var(--heatmap-level-1);
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #40c463;
|
||||
background-color: var(--heatmap-level-2);
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #30a14e;
|
||||
background-color: var(--heatmap-level-3);
|
||||
}
|
||||
.level-4 {
|
||||
background-color: #216e39;
|
||||
}
|
||||
|
||||
/* Dark Mode - Dark to Light/Bright Green (GitHub Dark Mode Style) */
|
||||
/* The user requested "From Light to Deep" (浅至深) which usually means standard heatmap logic (darker = more).
|
||||
HOWEVER, in dark interfaces, usually "Brighter = More".
|
||||
If the user explicitly says "colors are wrong, should be from light to deep", and they are referring to the visual gradient:
|
||||
|
||||
If they mean visual brightness:
|
||||
Light (Dim) -> Deep (Bright)
|
||||
|
||||
Let's stick to the GitHub Dark Mode palette which is scientifically designed for dark backgrounds:
|
||||
L1 (Less): Dark Green (#0e4429)
|
||||
L4 (More): Neon Green (#39d353)
|
||||
This is visually "Dim to Bright".
|
||||
|
||||
If the user meant "Light color to Dark color" literally (like white -> black green), that would look bad on dark mode.
|
||||
"浅至深" in color context usually implies saturation/intensity.
|
||||
|
||||
Let's restore the GitHub Dark Mode colors for dark mode, as my previous change might have inverted them incorrectly or caused confusion.
|
||||
|
||||
GitHub Dark Mode:
|
||||
L0: #161b22
|
||||
L1: #0e4429
|
||||
L2: #006d32
|
||||
L3: #26a641
|
||||
L4: #39d353
|
||||
|
||||
This goes from Dark Green -> Bright Green.
|
||||
*/
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.level-1 {
|
||||
background-color: #9be9a8;
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #40c463;
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #30a14e;
|
||||
}
|
||||
.level-4 {
|
||||
background-color: #216e39;
|
||||
}
|
||||
background-color: var(--heatmap-level-4);
|
||||
}
|
||||
|
||||
.heatmap-footer {
|
||||
|
||||
4
Web/src/utils/theme.js
Normal file
4
Web/src/utils/theme.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const getCssVar = (name) => {
|
||||
const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
||||
return val || '#999' // Default fallback
|
||||
}
|
||||
@@ -5,6 +5,15 @@
|
||||
title="预算管理"
|
||||
placeholder
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
class="nav-date-picker"
|
||||
@click="showDatePicker = true"
|
||||
>
|
||||
<span>{{ currentYear }}年{{ currentMonth }}月</span>
|
||||
<van-icon name="arrow-down" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<van-icon
|
||||
v-if="
|
||||
@@ -26,14 +35,7 @@
|
||||
@click="showArchiveSummary()"
|
||||
/>
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
name="plus"
|
||||
size="20"
|
||||
title="添加预算"
|
||||
@click="budgetEditRef.open({ category: activeTab })"
|
||||
/>
|
||||
<van-icon
|
||||
v-else
|
||||
v-if="activeTab === BudgetCategory.Savings"
|
||||
name="setting-o"
|
||||
size="20"
|
||||
title="储蓄分类配置"
|
||||
@@ -52,174 +54,26 @@
|
||||
title="支出"
|
||||
:name="BudgetCategory.Expense"
|
||||
>
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
v-model:date="selectedDate"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
class="scroll-content"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<template v-if="expenseBudgets?.length > 0">
|
||||
<van-swipe-cell
|
||||
v-for="budget in expenseBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{
|
||||
warning: budget.current / budget.limit > 0.8
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已支出
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
预算
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
余额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(budget.limit - budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无支出预算"
|
||||
<div class="scroll-content">
|
||||
<BudgetChartAnalysis
|
||||
:overall-stats="overallStats"
|
||||
:budgets="expenseBudgets"
|
||||
:active-tab="activeTab"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab
|
||||
title="收入"
|
||||
:name="BudgetCategory.Income"
|
||||
>
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
v-model:date="selectedDate"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
class="scroll-content"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<template v-if="incomeBudgets?.length > 0">
|
||||
<van-swipe-cell
|
||||
v-for="budget in incomeBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{
|
||||
income: budget.current / budget.limit >= 1
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已收入
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
目标
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
差额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.current >= budget.limit ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无收入预算"
|
||||
<div class="scroll-content">
|
||||
<BudgetChartAnalysis
|
||||
:overall-stats="overallStats"
|
||||
:budgets="incomeBudgets"
|
||||
:active-tab="activeTab"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab
|
||||
@@ -305,6 +159,16 @@
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<!-- 悬浮编辑按钮 -->
|
||||
<van-floating-bubble
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
v-model:offset="bubbleOffset"
|
||||
icon="edit"
|
||||
axis="xy"
|
||||
magnetic="x"
|
||||
@click="showListPopup = true"
|
||||
/>
|
||||
|
||||
<BudgetEditPopup
|
||||
ref="budgetEditRef"
|
||||
@success="fetchBudgetList"
|
||||
@@ -314,6 +178,247 @@
|
||||
@success="fetchBudgetList"
|
||||
/>
|
||||
|
||||
<!-- 预算明细列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showListPopup"
|
||||
:title="popupTitle"
|
||||
height="80%"
|
||||
>
|
||||
<template #header-actions>
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
name="plus"
|
||||
size="20"
|
||||
title="添加预算"
|
||||
@click="budgetEditRef.open({ category: activeTab })"
|
||||
/>
|
||||
<van-icon
|
||||
v-else
|
||||
name="setting-o"
|
||||
size="20"
|
||||
title="储蓄分类配置"
|
||||
@click="savingsConfigRef.open()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
style="min-height: 100%"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<!-- 支出列表 -->
|
||||
<template v-if="activeTab === BudgetCategory.Expense">
|
||||
<template v-if="expenseBudgets?.length > 0">
|
||||
<van-swipe-cell
|
||||
v-for="budget in expenseBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{
|
||||
warning: budget.current / budget.limit > 0.8
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已支出
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
预算
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
余额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(budget.limit - budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无支出预算"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 收入列表 -->
|
||||
<template v-if="activeTab === BudgetCategory.Income">
|
||||
<template v-if="incomeBudgets?.length > 0">
|
||||
<van-swipe-cell
|
||||
v-for="budget in incomeBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{
|
||||
income: budget.current / budget.limit >= 1
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已收入
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
目标
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
差额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.current >= budget.limit ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无收入预算"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 存款列表 -->
|
||||
<template v-if="activeTab === BudgetCategory.Savings">
|
||||
<template v-if="savingsBudgets?.length > 0">
|
||||
<BudgetCard
|
||||
v-for="budget in savingsBudgets"
|
||||
:key="budget.id"
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{ income: budget.current / budget.limit >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已存
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
目标
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
还差
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(Math.max(0, budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="card-footer-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow-left"
|
||||
plain
|
||||
type="primary"
|
||||
@click.stop="handleSavingsNav(budget, -1)"
|
||||
/>
|
||||
<span class="current-date-label">
|
||||
{{ getSavingsDateLabel(budget) }}
|
||||
</span>
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow"
|
||||
plain
|
||||
type="primary"
|
||||
icon-position="right"
|
||||
:disabled="disabledSavingsNextNav(budget)"
|
||||
@click.stop="handleSavingsNav(budget, 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无存款计划"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</PopupContainer>
|
||||
|
||||
<PopupContainer
|
||||
v-model="showUncoveredDetails"
|
||||
title="未覆盖预算的分类"
|
||||
@@ -373,6 +478,23 @@
|
||||
/>
|
||||
</div>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 日期选择弹窗 -->
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="pickerDate"
|
||||
title="选择年月"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:columns-type="['year', 'month']"
|
||||
@confirm="onConfirmDate"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -392,16 +514,36 @@ import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
||||
import BudgetEditPopup from '@/components/Budget/BudgetEditPopup.vue'
|
||||
import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue'
|
||||
import BudgetChartAnalysis from '@/components/Budget/BudgetChartAnalysis.vue'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
|
||||
const activeTab = ref(BudgetCategory.Expense)
|
||||
const selectedDate = ref(new Date())
|
||||
const showDatePicker = ref(false)
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date(2030, 11, 31)
|
||||
const pickerDate = ref([
|
||||
selectedDate.value.getFullYear().toString(),
|
||||
(selectedDate.value.getMonth() + 1).toString().padStart(2, '0')
|
||||
])
|
||||
|
||||
const currentYear = computed(() => selectedDate.value.getFullYear())
|
||||
const currentMonth = computed(() => selectedDate.value.getMonth() + 1)
|
||||
|
||||
const budgetEditRef = ref(null)
|
||||
const savingsConfigRef = ref(null)
|
||||
const isRefreshing = ref(false)
|
||||
const showUncoveredDetails = ref(false)
|
||||
const showListPopup = ref(false)
|
||||
const uncoveredCategories = ref([])
|
||||
|
||||
// 初始化悬浮按钮位置,避免遮挡底部导航栏
|
||||
// 默认位置:右下角,距离底部 100px (避开 Tabbar),距离右侧 24px
|
||||
const bubbleOffset = ref({
|
||||
x: window.innerWidth - 48 - 24,
|
||||
y: window.innerHeight - 48 - 100
|
||||
})
|
||||
|
||||
const showSummaryPopup = ref(false)
|
||||
const archiveSummary = ref('')
|
||||
|
||||
@@ -420,6 +562,19 @@ const activeTabTitle = computed(() => {
|
||||
return '达成'
|
||||
})
|
||||
|
||||
const popupTitle = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case BudgetCategory.Expense:
|
||||
return '支出预算明细'
|
||||
case BudgetCategory.Income:
|
||||
return '收入预算明细'
|
||||
case BudgetCategory.Savings:
|
||||
return '存款计划明细'
|
||||
default:
|
||||
return '预算明细'
|
||||
}
|
||||
})
|
||||
|
||||
const isArchive = computed(() => {
|
||||
const now = new Date()
|
||||
return (
|
||||
@@ -437,6 +592,12 @@ watch(selectedDate, async () => {
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats(), fetchUncoveredCategories()])
|
||||
})
|
||||
|
||||
const onConfirmDate = ({ selectedValues }) => {
|
||||
const [year, month] = selectedValues
|
||||
selectedDate.value = new Date(parseInt(year), parseInt(month) - 1, 1)
|
||||
showDatePicker.value = false
|
||||
}
|
||||
|
||||
const getValueClass = (rate) => {
|
||||
const numRate = parseFloat(rate)
|
||||
if (numRate === 0) {
|
||||
@@ -844,4 +1005,13 @@ const disabledSavingsNextNav = (budget) => {
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nav-date-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
<div class="page-container-flex">
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar
|
||||
title="账单统计"
|
||||
placeholder
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
class="nav-date-picker"
|
||||
@click="showMonthPicker = true"
|
||||
>
|
||||
<span>{{ currentYear }}年{{ currentMonth }}月</span>
|
||||
<van-icon name="arrow-down" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<van-icon
|
||||
name="chat-o"
|
||||
@@ -28,113 +36,16 @@
|
||||
加载统计数据中...
|
||||
</van-loading>
|
||||
|
||||
<!-- 固定概览部分(置顶不滚动) -->
|
||||
<div
|
||||
v-if="!firstLoading"
|
||||
class="overview-fixed-wrapper"
|
||||
>
|
||||
<transition
|
||||
:name="transitionName"
|
||||
mode="out-in"
|
||||
>
|
||||
<div :key="dateKey">
|
||||
<!-- 月度概览卡片 -->
|
||||
<div class="overview-card">
|
||||
<!-- 左切换按钮 -->
|
||||
<div
|
||||
class="nav-arrow left"
|
||||
@click.stop="changeMonth(-1)"
|
||||
>
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overview-item clickable"
|
||||
@click="goToTypeOverviewBills(0)"
|
||||
>
|
||||
<div class="label">
|
||||
总支出
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(monthlyData.totalExpense) }}
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
{{ monthlyData.expenseCount }}笔
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div
|
||||
class="overview-item clickable"
|
||||
@click="goToTypeOverviewBills(1)"
|
||||
>
|
||||
<div class="label">
|
||||
总收入
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(monthlyData.totalIncome) }}
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
{{ monthlyData.incomeCount }}笔
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div
|
||||
class="overview-item clickable"
|
||||
@click="goToTypeOverviewBills(null)"
|
||||
>
|
||||
<div class="label">
|
||||
结余
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="monthlyData.balance >= 0 ? 'income' : 'expense'"
|
||||
>
|
||||
{{ monthlyData.balance >= 0 ? '' : '-' }}¥{{
|
||||
formatMoney(Math.abs(monthlyData.balance))
|
||||
}}
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
{{ monthlyData.totalCount }}笔交易
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右切换按钮 -->
|
||||
<div
|
||||
class="nav-arrow right"
|
||||
:class="{ disabled: isCurrentMonth }"
|
||||
:aria-disabled="isCurrentMonth"
|
||||
@click.stop="!isCurrentMonth && changeMonth(1)"
|
||||
>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
|
||||
<!-- 月份日期标识 -->
|
||||
<div
|
||||
class="date-tag"
|
||||
@click="showMonthPicker = true"
|
||||
>
|
||||
{{ dateTagLabel }}
|
||||
<van-icon name="arrow-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 统计内容(可滚动部分) -->
|
||||
<div
|
||||
v-if="!firstLoading"
|
||||
class="statistics-content"
|
||||
>
|
||||
<transition
|
||||
:name="transitionName"
|
||||
mode="out-in"
|
||||
>
|
||||
<div :key="dateKey">
|
||||
<div>
|
||||
<!-- 趋势统计 -->
|
||||
<div
|
||||
class="common-card"
|
||||
style="padding-bottom: 5px"
|
||||
style="padding-bottom: 5px; margin-top: 12px;"
|
||||
>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
@@ -254,24 +165,16 @@
|
||||
<div
|
||||
v-for="category in incomeCategoriesView"
|
||||
:key="category.classify"
|
||||
class="category-item clickable compact"
|
||||
class="category-item clickable"
|
||||
@click="goToCategoryBills(category.classify, 1)"
|
||||
>
|
||||
<div class="compact-row top">
|
||||
<div class="category-info-compact">
|
||||
<div class="category-info">
|
||||
<div class="category-color income-color" />
|
||||
<span class="category-name text-ellipsis">{{ category.classify || '未分类' }}</span>
|
||||
</div>
|
||||
<div class="category-percent">
|
||||
{{ category.percent }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-row bottom">
|
||||
<div class="category-amount income-text">
|
||||
¥{{ formatMoney(category.amount) }}
|
||||
</div>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,67 +200,16 @@
|
||||
<div
|
||||
v-for="category in noneCategoriesView"
|
||||
:key="category.classify"
|
||||
class="category-item clickable compact"
|
||||
class="category-item clickable"
|
||||
@click="goToCategoryBills(category.classify, 2)"
|
||||
>
|
||||
<div class="compact-row top">
|
||||
<div class="category-info-compact">
|
||||
<div class="category-info">
|
||||
<div class="category-color none-color" />
|
||||
<span class="category-name text-ellipsis">{{ category.classify || '未分类' }}</span>
|
||||
</div>
|
||||
<div class="category-percent">
|
||||
{{ category.percent }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-row bottom">
|
||||
<div class="category-amount none-text">
|
||||
¥{{ formatMoney(category.amount) }}
|
||||
</div>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 其他统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
其他统计
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="other-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
日均支出
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
¥{{ formatMoney(dailyAverage.expense) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
日均收入
|
||||
</div>
|
||||
<div class="stat-value income-text">
|
||||
¥{{ formatMoney(dailyAverage.income) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
最大单笔支出
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
¥{{ formatMoney(monthlyData.maxExpense) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
最大单笔收入
|
||||
</div>
|
||||
<div class="stat-value income-text">
|
||||
¥{{ formatMoney(monthlyData.maxIncome) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,7 +217,6 @@
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
|
||||
@@ -438,6 +289,7 @@ import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
import SmartClassifyButton from '@/components/SmartClassifyButton.vue'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import { getCssVar } from '@/utils/theme'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -454,10 +306,6 @@ const selectedDate = ref([
|
||||
(new Date().getMonth() + 1).toString().padStart(2, '0')
|
||||
])
|
||||
|
||||
const transitionName = ref('slide-right')
|
||||
const dateKey = computed(() => `${currentYear.value}-${currentMonth.value}`)
|
||||
|
||||
// 账单列表相关
|
||||
const billListVisible = ref(false)
|
||||
const billListLoading = ref(false)
|
||||
const billListFinished = ref(false)
|
||||
@@ -480,9 +328,7 @@ const monthlyData = ref({
|
||||
balance: 0,
|
||||
expenseCount: 0,
|
||||
incomeCount: 0,
|
||||
totalCount: 0,
|
||||
maxExpense: 0,
|
||||
maxIncome: 0
|
||||
totalCount: 0
|
||||
})
|
||||
|
||||
// 分类数据
|
||||
@@ -550,64 +396,24 @@ const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date()
|
||||
|
||||
// 颜色配置
|
||||
const colors = [
|
||||
'#FF6B6B',
|
||||
'#4ECDC4',
|
||||
'#45B7D1',
|
||||
'#FFA07A',
|
||||
'#98D8C8',
|
||||
'#F7DC6F',
|
||||
'#BB8FCE',
|
||||
'#85C1E2',
|
||||
'#F8B88B',
|
||||
'#AAB7B8',
|
||||
'#FF8ED4',
|
||||
'#67E6DC',
|
||||
'#FFAB73',
|
||||
'#C9B1FF',
|
||||
'#7BDFF2'
|
||||
const getChartColors = () => [
|
||||
getCssVar('--chart-color-1'),
|
||||
getCssVar('--chart-color-2'),
|
||||
getCssVar('--chart-color-3'),
|
||||
getCssVar('--chart-color-4'),
|
||||
getCssVar('--chart-color-5'),
|
||||
getCssVar('--chart-color-6'),
|
||||
getCssVar('--chart-color-7'),
|
||||
getCssVar('--chart-color-8'),
|
||||
getCssVar('--chart-color-9'),
|
||||
getCssVar('--chart-color-10'),
|
||||
getCssVar('--chart-color-11'),
|
||||
getCssVar('--chart-color-12'),
|
||||
getCssVar('--chart-color-13'),
|
||||
getCssVar('--chart-color-14'),
|
||||
getCssVar('--chart-color-15')
|
||||
]
|
||||
|
||||
// 日均统计
|
||||
const dailyAverage = computed(() => {
|
||||
const daysInMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
|
||||
return {
|
||||
expense: monthlyData.value.totalExpense / daysInMonth,
|
||||
income: monthlyData.value.totalIncome / daysInMonth
|
||||
}
|
||||
})
|
||||
|
||||
// 日均统计
|
||||
const isCurrentMonth = computed(() => {
|
||||
const now = new Date()
|
||||
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
|
||||
})
|
||||
|
||||
// 日期标签展示文字
|
||||
const dateTagLabel = computed(() => {
|
||||
const now = new Date()
|
||||
const todayYear = now.getFullYear()
|
||||
const todayMonth = now.getMonth() + 1
|
||||
|
||||
if (currentYear.value === todayYear && currentMonth.value === todayMonth) {
|
||||
return '本月'
|
||||
}
|
||||
|
||||
// 计算上个月
|
||||
let lastYear = todayYear
|
||||
let lastMonth = todayMonth - 1
|
||||
if (lastMonth === 0) {
|
||||
lastMonth = 12
|
||||
lastYear--
|
||||
}
|
||||
|
||||
if (currentYear.value === lastYear && currentMonth.value === lastMonth) {
|
||||
return '上月'
|
||||
}
|
||||
|
||||
return `${currentYear.value}年${currentMonth.value}月`
|
||||
})
|
||||
|
||||
// 是否为未分类账单
|
||||
const isUnclassified = computed(() => {
|
||||
return selectedClassify.value === '未分类' || selectedClassify.value === ''
|
||||
@@ -623,34 +429,10 @@ const formatMoney = (value) => {
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// 切换月份
|
||||
const changeMonth = (offset) => {
|
||||
transitionName.value = offset > 0 ? 'slide-left' : 'slide-right'
|
||||
let newMonth = currentMonth.value + offset
|
||||
let newYear = currentYear.value
|
||||
|
||||
if (newMonth > 12) {
|
||||
newMonth = 1
|
||||
newYear++
|
||||
} else if (newMonth < 1) {
|
||||
newMonth = 12
|
||||
newYear--
|
||||
}
|
||||
|
||||
// 不能超过当前月份
|
||||
const now = new Date()
|
||||
const targetDate = new Date(newYear, newMonth - 1)
|
||||
if (targetDate > now) {
|
||||
return
|
||||
}
|
||||
|
||||
currentYear.value = newYear
|
||||
currentMonth.value = newMonth
|
||||
|
||||
// 重置展开状态
|
||||
showAllExpense.value = false
|
||||
|
||||
fetchStatistics()
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
await fetchStatistics(false)
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
// 确认月份选择
|
||||
@@ -658,16 +440,6 @@ const onMonthConfirm = ({ selectedValues }) => {
|
||||
const newYear = parseInt(selectedValues[0])
|
||||
const newMonth = parseInt(selectedValues[1])
|
||||
|
||||
// 判断方向以应用动画
|
||||
if (
|
||||
newYear > currentYear.value ||
|
||||
(newYear === currentYear.value && newMonth > currentMonth.value)
|
||||
) {
|
||||
transitionName.value = 'slide-left'
|
||||
} else {
|
||||
transitionName.value = 'slide-right'
|
||||
}
|
||||
|
||||
currentYear.value = newYear
|
||||
currentMonth.value = newMonth
|
||||
showMonthPicker.value = false
|
||||
@@ -678,12 +450,6 @@ const onMonthConfirm = ({ selectedValues }) => {
|
||||
fetchStatistics()
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
await fetchStatistics(false)
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatistics = async (showLoading = true) => {
|
||||
if (showLoading && firstLoading.value) {
|
||||
@@ -734,12 +500,13 @@ const fetchCategoryData = async () => {
|
||||
})
|
||||
|
||||
if (expenseResponse.success && expenseResponse.data) {
|
||||
const currentColors = getChartColors()
|
||||
expenseCategories.value = expenseResponse.data.map((item, index) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount,
|
||||
count: item.count,
|
||||
percent: item.percent,
|
||||
color: colors[index % colors.length]
|
||||
color: currentColors[index % currentColors.length]
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -912,7 +679,7 @@ const renderChart = (data) => {
|
||||
data: ['支出', '收入', '存款'],
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
color: '#999' // 适配深色模式
|
||||
color: getCssVar('--chart-text-muted') // 适配深色模式
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
@@ -927,14 +694,14 @@ const renderChart = (data) => {
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
color: '#999' // 适配深色模式
|
||||
color: getCssVar('--chart-text-muted') // 适配深色模式
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 5,
|
||||
axisLabel: {
|
||||
color: '#999', // 适配深色模式
|
||||
color: getCssVar('--chart-text-muted'), // 适配深色模式
|
||||
formatter: (value) => {
|
||||
return value / 1000 + 'k'
|
||||
}
|
||||
@@ -942,7 +709,7 @@ const renderChart = (data) => {
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#333' // 深色分割线
|
||||
color: getCssVar('--van-border-color') // 深色分割线
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -951,7 +718,7 @@ const renderChart = (data) => {
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: expenses,
|
||||
itemStyle: { color: '#FF6B6B' },
|
||||
itemStyle: { color: getCssVar('--chart-color-1') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
@@ -960,7 +727,7 @@ const renderChart = (data) => {
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
data: incomes,
|
||||
itemStyle: { color: '#4ECDC4' },
|
||||
itemStyle: { color: getCssVar('--chart-color-2') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
@@ -969,7 +736,7 @@ const renderChart = (data) => {
|
||||
name: '存款',
|
||||
type: 'line',
|
||||
data: balances,
|
||||
itemStyle: { color: '#FFAB73' },
|
||||
itemStyle: { color: getCssVar('--chart-color-13') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
@@ -1018,11 +785,30 @@ const renderPieChart = () => {
|
||||
// 按照金额排序
|
||||
list.sort((a, b) => b.amount - a.amount)
|
||||
|
||||
if (list.length > 0) {
|
||||
const MAX_SLICES = 8 // 最大显示扇区数,其余合并为"其他"
|
||||
const currentColors = getChartColors()
|
||||
|
||||
if (list.length > MAX_SLICES) {
|
||||
const topList = list.slice(0, MAX_SLICES - 1)
|
||||
const otherList = list.slice(MAX_SLICES - 1)
|
||||
const otherAmount = otherList.reduce((sum, item) => sum + item.amount, 0)
|
||||
|
||||
chartData = topList.map((item, index) => ({
|
||||
value: item.amount,
|
||||
name: item.classify || '未分类',
|
||||
itemStyle: { color: currentColors[index % currentColors.length] }
|
||||
}))
|
||||
|
||||
chartData.push({
|
||||
value: otherAmount,
|
||||
name: '其他',
|
||||
itemStyle: { color: getCssVar('--van-gray-6') } // 使用灰色表示其他
|
||||
})
|
||||
} else {
|
||||
chartData = list.map((item, index) => ({
|
||||
value: item.amount,
|
||||
name: item.classify || '未分类',
|
||||
itemStyle: { color: colors[index % colors.length] }
|
||||
itemStyle: { color: currentColors[index % currentColors.length] }
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1033,12 +819,12 @@ const renderPieChart = () => {
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: '#fff', // 适配深色模式
|
||||
color: getCssVar('--chart-text-muted'), // 适配深色模式
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
subtextStyle: {
|
||||
color: '#999',
|
||||
color: getCssVar('--chart-text-muted'),
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
@@ -1055,21 +841,26 @@ const renderPieChart = () => {
|
||||
minAngle: 5, // 最小扇区角度,防止扇区太小看不见
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#1a1a1a',
|
||||
borderColor: getCssVar('--van-background-2'),
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}',
|
||||
color: '#ccc', // 适配深色模式
|
||||
overflow: 'none' // 禁止文本截断
|
||||
color: getCssVar('--van-text-color-2') // 适配深色模式
|
||||
},
|
||||
labelLayout: {
|
||||
hideOverlap: true // 隐藏重叠标签
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
color: getCssVar('--van-text-color-3')
|
||||
},
|
||||
smooth: 0.2,
|
||||
length: 10,
|
||||
length2: 20
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
@@ -1101,22 +892,6 @@ const goToCategoryBills = (classify, type) => {
|
||||
loadCategoryBills()
|
||||
}
|
||||
|
||||
// 打开总支出/总收入的所有账单列表
|
||||
const goToTypeOverviewBills = (type) => {
|
||||
selectedClassify.value = null
|
||||
selectedType.value = type
|
||||
selectedCategoryTitle.value = `${type === 0 ? '总支出' : '总收入'} - 明细`
|
||||
|
||||
// 重置分页状态
|
||||
categoryBills.value = []
|
||||
billPageIndex.value = 1
|
||||
billListFinished.value = false
|
||||
|
||||
billListVisible.value = true
|
||||
// 打开弹窗后加载数据
|
||||
loadCategoryBills()
|
||||
}
|
||||
|
||||
const smartClassifyButtonRef = ref(null)
|
||||
const transactionListRef = ref(null)
|
||||
// 加载分类账单数据
|
||||
@@ -1250,7 +1025,7 @@ const onSmartClassifySave = async () => {
|
||||
}
|
||||
|
||||
const handleNotifiedTransactionId = async (transactionId) => {
|
||||
console.log('收到已处理交易ID通知:', transactionId)
|
||||
console.info('收到已处理交易ID通知:', transactionId)
|
||||
// 滚动到指定的交易项
|
||||
const index = categoryBills.value.findIndex((item) => String(item.id) === String(transactionId))
|
||||
if (index !== -1) {
|
||||
@@ -1344,12 +1119,6 @@ onBeforeUnmount(() => {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.overview-fixed-wrapper {
|
||||
background: transparent !important;
|
||||
padding: 16px 0 1px 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: 0;
|
||||
overflow-x: hidden; /* 防止滑动动画出现横向滚动条 */
|
||||
@@ -1361,163 +1130,13 @@ onBeforeUnmount(() => {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 月度概览卡片 */
|
||||
.overview-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
background: var(--van-background-2);
|
||||
margin: 0 12px 16px;
|
||||
padding: 24px 12px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--van-border-color);
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: var(--van-gray-5);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-arrow:active {
|
||||
color: var(--van-primary-color);
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.nav-arrow.left {
|
||||
left: 0;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
.nav-arrow.right {
|
||||
right: 0;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
|
||||
.nav-arrow.disabled {
|
||||
color: #c8c9cc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-arrow.disabled:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.date-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
color: var(--van-primary-color);
|
||||
background-color: var(--van-primary-color-light);
|
||||
padding: 2px 10px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
/* 导航栏日期选择器 */
|
||||
.nav-date-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.date-tag {
|
||||
background-color: rgba(var(--van-primary-color-rgb), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview-item.clickable {
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.overview-item.clickable:active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.overview-item.clickable:active {
|
||||
background-color: #2c2c2c;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-item .label {
|
||||
font-size: 13px;
|
||||
color: var(--van-text-color-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.overview-item .value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.overview-item .sub-text {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-3);
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: var(--van-border-color);
|
||||
}
|
||||
|
||||
.expense {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
.income {
|
||||
color: var(--van-success-color);
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
/* 环形图 */
|
||||
@@ -1767,39 +1386,6 @@ onBeforeUnmount(() => {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 其他统计 */
|
||||
.other-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: var(--van-background-2);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
.stat-item {
|
||||
background: var(--van-background-2);
|
||||
}
|
||||
} */
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--van-text-color-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
/* 设置页面容器背景色 */
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
@@ -1823,33 +1409,6 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 紧凑模式列表项 */
|
||||
.category-item.compact {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.compact-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.compact-row.top {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.category-info-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1857,16 +1416,4 @@ onBeforeUnmount(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 调整紧凑模式下的字体大小 */
|
||||
.compact-row .category-percent {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.compact-row .category-amount {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compact-row .category-count {
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user