feat: 初始化收支管理系统

This commit is contained in:
wangshuo
2026-08-30 14:36:22 +08:00
commit 733ed4bb86
79 changed files with 5808 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
<script setup lang="ts">
// 统计面板组件:展示月度收支汇总、分类占比饼图、每日收支趋势柱状图
import { ref, onMounted, watch } from 'vue'
import { NCard, NGrid, NGridItem, NStatistic, NSpace, NSelect } from 'naive-ui'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, PieChart, BarChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from '../api/statistics'
// 注册 ECharts 所需组件
use([CanvasRenderer, LineChart, PieChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
const now = new Date()
const selectedYear = ref(now.getFullYear()) // 选中的年份
const selectedMonth = ref(now.getMonth() + 1) // 选中的月份
// 月度汇总数据(总收入、总支出、结余、交易笔数)
const monthly = ref({ totalIncome: 0, totalExpense: 0, balance: 0, transactionCount: 0 })
const categoryData = ref<any[]>([]) // 分类统计数据
const trendData = ref<any[]>([]) // 每日趋势数据
// 年份和月份下拉选项
const yearOptions = Array.from({ length: 10 }, (_, i) => ({ label: `${2020 + i}年`, value: 2020 + i }))
const monthOptions = Array.from({ length: 12 }, (_, i) => ({ label: `${i + 1}月`, value: i + 1 }))
const pieOption = ref({}) // 饼图配置
const trendOption = ref({}) // 趋势图配置
/** 更新支出分类饼图配置 */
function updatePieChart() {
pieOption.value = {
title: { text: '支出分类占比', left: 'center' },
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
series: [
{
type: 'pie',
radius: ['40%', '70%'], // 环形图内外半径
data: categoryData.value
.filter((c: any) => c.type === 'EXPENSE') // 只展示支出类
.map((c: any) => ({ name: c.categoryName, value: c.amount })),
},
],
}
}
/** 更新每日收支趋势柱状图配置 */
function updateTrendChart() {
trendOption.value = {
title: { text: '每日收支趋势' },
tooltip: { trigger: 'axis' },
legend: { data: ['收入', '支出'], bottom: 0 },
grid: { left: 60, right: 20, top: 40, bottom: 40 },
xAxis: {
type: 'category',
data: trendData.value.map((d: any) => d.date.substring(5)), // 只显示 MM-DD
},
yAxis: { type: 'value' },
series: [
{ name: '收入', type: 'bar', data: trendData.value.map((d: any) => d.income), color: '#18a058' },
{ name: '支出', type: 'bar', data: trendData.value.map((d: any) => d.expense), color: '#d03050' },
],
}
}
/** 并行请求所有统计数据并更新图表 */
async function fetchData() {
const params = { year: selectedYear.value, month: selectedMonth.value }
try {
const [monthlyRes, categoryRes, trendRes]: any[] = await Promise.all([
getMonthlyStatistics(params),
getCategoryStatistics(params),
getTrendStatistics(params),
])
monthly.value = monthlyRes.data
categoryData.value = categoryRes.data
trendData.value = trendRes.data
updatePieChart()
updateTrendChart()
} catch (e: any) {
console.error('加载统计数据失败', e)
}
}
// 监听年月变化,自动重新加载数据
watch([selectedYear, selectedMonth], fetchData)
onMounted(fetchData) // 组件挂载时加载数据
</script>
<template>
<div>
<!-- 顶部:标题和年月选择器 -->
<NSpace justify="space-between" align="center" style="margin-bottom: 20px;">
<h2 style="margin: 0;">统计面板</h2>
<NSpace>
<NSelect v-model:value="selectedYear" :options="yearOptions" style="width: 100px;" />
<NSelect v-model:value="selectedMonth" :options="monthOptions" style="width: 80px;" />
</NSpace>
</NSpace>
<!-- 统计卡片:总收入、总支出、结余、交易笔数 -->
<NGrid :cols="4" :x-gap="16" :y-gap="16" style="margin-bottom: 20px;">
<NGridItem>
<NCard>
<NStatistic label="总收入" :value="Number(monthly.totalIncome).toFixed(2)" style="color: #18a058;">
<template #prefix>+</template>
</NStatistic>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<NStatistic label="总支出" :value="Number(monthly.totalExpense).toFixed(2)" style="color: #d03050;">
<template #prefix>-</template>
</NStatistic>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<NStatistic label="结余" :value="Number(monthly.balance).toFixed(2)" />
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<NStatistic label="交易笔数" :value="monthly.transactionCount" />
</NCard>
</NGridItem>
</NGrid>
<!-- 图表区域:饼图 + 趋势柱状图 -->
<NGrid :cols="2" :x-gap="16" :y-gap="16">
<NGridItem>
<NCard>
<VChart :option="pieOption" style="height: 350px;" autoresize />
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<VChart :option="trendOption" style="height: 350px;" autoresize />
</NCard>
</NGridItem>
</NGrid>
</div>
</template>