1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 32s
Docker Build & Deploy / Deploy to Production (push) Successful in 13s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 32s
Docker Build & Deploy / Deploy to Production (push) Successful in 13s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -57,16 +57,16 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
||||
/// </summary>
|
||||
/// <param name="year">年份</param>
|
||||
/// <param name="month">月份</param>
|
||||
/// <returns>每天的消费笔数和金额</returns>
|
||||
Task<Dictionary<string, (int count, decimal amount)>> GetDailyStatisticsAsync(int year, int month);
|
||||
/// <returns>每天的消费笔数和金额详情</returns>
|
||||
Task<Dictionary<string, (int count, decimal expense, decimal income)>> GetDailyStatisticsAsync(int year, int month);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定日期范围内的每日统计
|
||||
/// </summary>
|
||||
/// <param name="startDate">开始日期</param>
|
||||
/// <param name="endDate">结束日期</param>
|
||||
/// <returns>每天的消费笔数和金额</returns>
|
||||
Task<Dictionary<string, (int count, decimal amount)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate);
|
||||
/// <returns>每天的消费笔数和金额详情</returns>
|
||||
Task<Dictionary<string, (int count, decimal expense, decimal income)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定日期范围内的交易记录
|
||||
@@ -345,7 +345,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync(t => t.Classify);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, (int count, decimal amount)>> GetDailyStatisticsAsync(int year, int month)
|
||||
public async Task<Dictionary<string, (int count, decimal expense, decimal income)>> GetDailyStatisticsAsync(int year, int month)
|
||||
{
|
||||
var startDate = new DateTime(year, month, 1);
|
||||
var endDate = startDate.AddMonths(1);
|
||||
@@ -353,7 +353,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
return await GetDailyStatisticsByRangeAsync(startDate, endDate);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, (int count, decimal amount)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate)
|
||||
public async Task<Dictionary<string, (int count, decimal expense, decimal income)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var records = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate)
|
||||
@@ -366,11 +366,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
g =>
|
||||
{
|
||||
// 分别统计收入和支出
|
||||
var income = g.Where(t => t.Type == TransactionType.Income).Sum(t => t.Amount);
|
||||
var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => t.Amount);
|
||||
// 净额 = 收入 - 支出(消费大于收入时为负数)
|
||||
var netAmount = income - expense;
|
||||
return (count: g.Count(), amount: netAmount);
|
||||
var income = g.Where(t => t.Type == TransactionType.Income).Sum(t => Math.Abs(t.Amount));
|
||||
var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount));
|
||||
return (count: g.Count(), expense: expense, income: income);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vant": "^4.9.22",
|
||||
"vue": "^3.5.25",
|
||||
|
||||
23
Web/pnpm-lock.yaml
generated
23
Web/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
echarts:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(vue@3.5.26)
|
||||
@@ -787,6 +790,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
echarts@6.0.0:
|
||||
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
|
||||
|
||||
electron-to-chromium@1.5.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
@@ -1296,6 +1302,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1435,6 +1444,9 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zrender@6.0.0:
|
||||
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
@@ -2131,6 +2143,11 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
echarts@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.0.0
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
|
||||
entities@7.0.0: {}
|
||||
@@ -2611,6 +2628,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -2744,3 +2763,7 @@ snapshots:
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zrender@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
@@ -69,36 +69,28 @@
|
||||
<div v-if="!firstLoading" class="statistics-content">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<div :key="dateKey">
|
||||
<!-- 趋势统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">每日收支趋势</h3>
|
||||
</div>
|
||||
|
||||
<div class="trend-chart" style="height: 240px; padding: 10px 0;">
|
||||
<div ref="chartRef" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">支出分类统计</h3>
|
||||
<van-tag type="primary" size="medium">{{ expenseCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">支出分类统计</h3>
|
||||
<van-tag type="primary" size="medium">{{ expenseCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
|
||||
<!-- 环形图区域 -->
|
||||
<div v-if="expenseCategoriesView.length > 0" class="chart-container">
|
||||
<div class="ring-chart">
|
||||
<svg viewBox="0 0 200 200" class="ring-svg">
|
||||
<circle
|
||||
v-for="(segment, index) in chartSegments"
|
||||
:key="index"
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="70"
|
||||
fill="none"
|
||||
:stroke="segment.color"
|
||||
:stroke-width="35"
|
||||
:stroke-dasharray="`${segment.length} ${circumference - segment.length}`"
|
||||
:stroke-dashoffset="-segment.offset"
|
||||
transform="rotate(-90 100 100)"
|
||||
class="ring-segment"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ring-center">
|
||||
<div class="center-value">¥{{ formatMoney(monthlyData.totalExpense) }}</div>
|
||||
<div class="center-label">总支出</div>
|
||||
</div>
|
||||
<div ref="pieChartRef" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,144 +122,96 @@
|
||||
description="本月暂无支出记录"
|
||||
image="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 收入分类统计 -->
|
||||
<div v-if="incomeCategoriesView.length > 0" class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">收入分类统计</h3>
|
||||
<van-tag type="success" size="medium">{{ incomeCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in incomeCategoriesView"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="category.isOther ? (showAllIncome = true) : goToCategoryBills(category.classify, 1)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color income-color"></div>
|
||||
<div class="category-name-with-count">
|
||||
<span class="category-name">{{ category.classify || '未分类' }}</span>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-stats">
|
||||
<div class="category-amount income-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不计收支分类统计 -->
|
||||
<div v-if="noneCategoriesView.length > 0" class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">不计收支分类统计</h3>
|
||||
<van-tag type="info" size="medium">{{ noneCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in noneCategoriesView"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="category.isOther ? (showAllNone = true) : goToCategoryBills(category.classify, 2)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color none-color"></div>
|
||||
<div class="category-name-with-count">
|
||||
<span class="category-name">{{ category.classify || '未分类' }}</span>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
<!-- 收入分类统计 -->
|
||||
<div v-if="incomeCategoriesView.length > 0" class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">收入分类统计</h3>
|
||||
<van-tag type="success" size="medium">{{ incomeCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
<div class="category-stats">
|
||||
<div class="category-amount none-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 趋势统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">近6个月趋势</h3>
|
||||
</div>
|
||||
|
||||
<div class="trend-chart">
|
||||
<div class="trend-bars">
|
||||
<div
|
||||
v-for="item in trendData"
|
||||
:key="item.month"
|
||||
class="trend-bar-group"
|
||||
>
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="bar expense-bar"
|
||||
:style="{ height: getBarHeight(item.expense, maxTrendValue) }"
|
||||
>
|
||||
<div v-if="item.expense > 0" class="bar-value">
|
||||
{{ formatShortMoney(item.expense) }}
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in incomeCategoriesView"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="category.isOther ? (showAllIncome = true) : goToCategoryBills(category.classify, 1)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color income-color"></div>
|
||||
<div class="category-name-with-count">
|
||||
<span class="category-name">{{ category.classify || '未分类' }}</span>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bar income-bar"
|
||||
:style="{ height: getBarHeight(item.income, maxTrendValue) }"
|
||||
>
|
||||
<div v-if="item.income > 0" class="bar-value">
|
||||
{{ formatShortMoney(item.income) }}
|
||||
<div class="category-stats">
|
||||
<div class="category-amount income-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不计收支分类统计 -->
|
||||
<div v-if="noneCategoriesView.length > 0" class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">不计收支分类统计</h3>
|
||||
<van-tag type="info" size="medium">{{ noneCategoriesView.length }}类</van-tag>
|
||||
</div>
|
||||
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in noneCategoriesView"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="category.isOther ? (showAllNone = true) : goToCategoryBills(category.classify, 2)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color none-color"></div>
|
||||
<div class="category-name-with-count">
|
||||
<span class="category-name">{{ category.classify || '未分类' }}</span>
|
||||
<span class="category-count">{{ category.count }}笔</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-stats">
|
||||
<div class="category-amount none-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
<div class="bar-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color expense-color"></div>
|
||||
<span>支出</span>
|
||||
<!-- 其他统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">其他统计</h3>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color income-color"></div>
|
||||
<span>收入</span>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -330,7 +274,8 @@ import { ref, computed, onMounted, onActivated, nextTick } from 'vue'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from '@/api/statistics'
|
||||
import { getMonthlyStatistics, getCategoryStatistics, getDailyStatistics } from '@/api/statistics'
|
||||
import * as echarts from 'echarts'
|
||||
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
@@ -462,6 +407,11 @@ const noneCategoriesView = computed(() => {
|
||||
|
||||
// 趋势数据
|
||||
const trendData = ref([])
|
||||
const dailyData = ref([])
|
||||
const chartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
let chartInstance = null
|
||||
let pieChartInstance = null
|
||||
|
||||
// 日期范围
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
@@ -500,13 +450,7 @@ const dailyAverage = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 趋势图最大值
|
||||
const maxTrendValue = computed(() => {
|
||||
const allValues = trendData.value.flatMap(item => [item.expense, item.income])
|
||||
return Math.max(...allValues, 1)
|
||||
})
|
||||
|
||||
// 是否是当前月
|
||||
// 日均统计
|
||||
const isCurrentMonth = computed(() => {
|
||||
const now = new Date()
|
||||
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
|
||||
@@ -548,25 +492,6 @@ const formatMoney = (value) => {
|
||||
return Number(value).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// 格式化短金额(k为单位)
|
||||
const formatShortMoney = (value) => {
|
||||
if (!value) return '0'
|
||||
if (value >= 10000) {
|
||||
return (value / 10000).toFixed(1) + 'w'
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return value.toFixed(0)
|
||||
}
|
||||
|
||||
// 获取柱状图高度
|
||||
const getBarHeight = (value, maxValue) => {
|
||||
if (!value || !maxValue) return '0%'
|
||||
const percent = (value / maxValue) * 100
|
||||
return Math.max(percent, 5) + '%' // 最小5%以便显示
|
||||
}
|
||||
|
||||
// 切换月份
|
||||
const changeMonth = (offset) => {
|
||||
transitionName.value = offset > 0 ? 'slide-left' : 'slide-right'
|
||||
@@ -639,7 +564,7 @@ const fetchStatistics = async (showLoading = true) => {
|
||||
await Promise.all([
|
||||
fetchMonthlyData(),
|
||||
fetchCategoryData(),
|
||||
fetchTrendData()
|
||||
fetchDailyData()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
@@ -647,6 +572,11 @@ const fetchStatistics = async (showLoading = true) => {
|
||||
} finally {
|
||||
loading.value = false
|
||||
firstLoading.value = false
|
||||
// DOM 更新后渲染图表
|
||||
nextTick(() => {
|
||||
renderChart(dailyData.value)
|
||||
renderPieChart()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,39 +654,273 @@ const fetchCategoryData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取趋势数据
|
||||
const fetchTrendData = async () => {
|
||||
// 获取每日统计数据并渲染图表
|
||||
const fetchDailyData = async () => {
|
||||
try {
|
||||
// 计算开始年月(当前月往前推5个月)
|
||||
let startYear = currentYear.value
|
||||
let startMonth = currentMonth.value - 5
|
||||
|
||||
if (startMonth <= 0) {
|
||||
startMonth += 12
|
||||
startYear--
|
||||
}
|
||||
|
||||
const response = await getTrendStatistics({
|
||||
startYear,
|
||||
startMonth,
|
||||
monthCount: 6
|
||||
const response = await getDailyStatistics({
|
||||
year: currentYear.value,
|
||||
month: currentMonth.value
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
trendData.value = response.data.map(item => ({
|
||||
year: item.year,
|
||||
month: item.month,
|
||||
label: `${item.month}月`,
|
||||
expense: item.expense,
|
||||
income: item.income
|
||||
}))
|
||||
dailyData.value = response.data
|
||||
// 如果不是首次加载(即DOM已存在),直接渲染
|
||||
if (!firstLoading.value) {
|
||||
nextTick(() => {
|
||||
renderChart(response.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取趋势数据失败:', error)
|
||||
showToast('获取趋势数据失败')
|
||||
console.error('获取每日统计数据失败:', error)
|
||||
showToast('获取每日统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = (data) => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
if (!chartInstance) {
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
}
|
||||
|
||||
// 补全当月所有日期
|
||||
const now = new Date()
|
||||
let daysInMonth
|
||||
|
||||
if (currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1) {
|
||||
// 如果是当前月,只显示到今天
|
||||
daysInMonth = now.getDate()
|
||||
} else {
|
||||
// 如果是过去月,显示整月
|
||||
daysInMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
|
||||
}
|
||||
|
||||
const fullData = []
|
||||
|
||||
// 创建日期映射
|
||||
const dataMap = new Map()
|
||||
data.forEach(item => {
|
||||
const day = new Date(item.date).getDate()
|
||||
dataMap.set(day, item)
|
||||
})
|
||||
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const item = dataMap.get(i)
|
||||
if (item) {
|
||||
fullData.push(item)
|
||||
} else {
|
||||
fullData.push({
|
||||
date: `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}-${String(i).padStart(2, '0')}`,
|
||||
count: 0,
|
||||
expense: 0,
|
||||
income: 0,
|
||||
balance: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dates = fullData.map(item => {
|
||||
const date = new Date(item.date)
|
||||
return `${date.getDate()}日`
|
||||
})
|
||||
|
||||
// Calculate cumulative values
|
||||
let accumulatedExpense = 0
|
||||
let accumulatedIncome = 0
|
||||
let accumulatedBalance = 0
|
||||
|
||||
const expenses = fullData.map(item => {
|
||||
accumulatedExpense += item.expense
|
||||
return accumulatedExpense
|
||||
})
|
||||
|
||||
const incomes = fullData.map(item => {
|
||||
accumulatedIncome += item.income
|
||||
return accumulatedIncome
|
||||
})
|
||||
|
||||
const balances = fullData.map(item => {
|
||||
accumulatedBalance += item.balance
|
||||
return accumulatedBalance
|
||||
})
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
let result = params[0].name + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + formatMoney(param.value) + '<br/>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['支出', '收入', '存款'],
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
color: '#999' // 适配深色模式
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
color: '#999' // 适配深色模式
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
interval: 1000, // 固定间隔1k
|
||||
axisLabel: {
|
||||
color: '#999', // 适配深色模式
|
||||
formatter: (value) => {
|
||||
return (value / 1000) + 'k'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#333' // 深色分割线
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: expenses,
|
||||
itemStyle: { color: '#FF6B6B' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
data: incomes,
|
||||
itemStyle: { color: '#4ECDC4' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '存款',
|
||||
type: 'line',
|
||||
data: balances,
|
||||
itemStyle: { color: '#FFAB73' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
const renderPieChart = () => {
|
||||
if (!pieChartRef.value) return
|
||||
if (expenseCategoriesView.value.length === 0) return
|
||||
|
||||
if (!pieChartInstance) {
|
||||
pieChartInstance = echarts.init(pieChartRef.value)
|
||||
}
|
||||
|
||||
// 使用 Top N + Other 的数据逻辑,确保图表不会太拥挤
|
||||
const list = [...expenseCategoriesView.value]
|
||||
let chartData = []
|
||||
|
||||
// 按照金额排序
|
||||
list.sort((a, b) => b.amount - a.amount)
|
||||
|
||||
if (list.length <= 8) {
|
||||
chartData = list.map((item, index) => ({
|
||||
value: item.amount,
|
||||
name: item.classify || '未分类',
|
||||
itemStyle: { color: colors[index % colors.length] }
|
||||
}))
|
||||
} else {
|
||||
const top = list.slice(0, 7)
|
||||
const rest = list.slice(7)
|
||||
chartData = top.map((item, index) => ({
|
||||
value: item.amount,
|
||||
name: item.classify || '未分类',
|
||||
itemStyle: { color: colors[index % colors.length] }
|
||||
}))
|
||||
|
||||
const otherAmount = rest.reduce((s, c) => s + c.amount, 0)
|
||||
if (otherAmount > 0) {
|
||||
chartData.push({
|
||||
value: otherAmount,
|
||||
name: '其他',
|
||||
itemStyle: { color: '#AAB7B8' }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '¥' + formatMoney(monthlyData.value.totalExpense),
|
||||
subtext: '总支出',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: '#fff', // 适配深色模式
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
subtextStyle: {
|
||||
color: '#999',
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出分类',
|
||||
type: 'pie',
|
||||
radius: ['50%', '80%'],
|
||||
avoidLabelOverlap: true,
|
||||
minAngle: 5, // 最小扇区角度,防止扇区太小看不见
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#1a1a1a',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}',
|
||||
color: '#ccc', // 适配深色模式
|
||||
overflow: 'none' // 禁止文本截断
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
pieChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 跳转到智能分析页面
|
||||
const goToAnalysis = () => {
|
||||
router.push('/bill-analysis')
|
||||
@@ -944,8 +1108,14 @@ const handleNotifiedTransactionId = async (transactionId) => {
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchStatistics()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
chartInstance && chartInstance.resize()
|
||||
pieChartInstance && pieChartInstance.resize()
|
||||
}
|
||||
|
||||
// 页面激活时刷新数据(从其他页面返回时)
|
||||
onActivated(() => {
|
||||
fetchStatistics()
|
||||
@@ -961,6 +1131,9 @@ window.addEventListener && window.addEventListener('transaction-deleted', onGlob
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance && chartInstance.dispose()
|
||||
pieChartInstance && pieChartInstance.dispose()
|
||||
})
|
||||
|
||||
const onGlobalTransactionsChanged = () => {
|
||||
@@ -1157,12 +1330,12 @@ onBeforeUnmount(() => {
|
||||
|
||||
/* 环形图 */
|
||||
.chart-container {
|
||||
padding: 20px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.ring-chart {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,13 @@ public class TransactionRecordController(
|
||||
try
|
||||
{
|
||||
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month);
|
||||
var result = statistics.Select(s => new DailyStatisticsDto(s.Key, s.Value.count, s.Value.amount)).ToList();
|
||||
var result = statistics.Select(s => new DailyStatisticsDto(
|
||||
s.Key,
|
||||
s.Value.count,
|
||||
s.Value.expense,
|
||||
s.Value.income,
|
||||
s.Value.income - s.Value.expense // Balance = Income - Expense
|
||||
)).ToList();
|
||||
|
||||
return result.Ok();
|
||||
}
|
||||
@@ -300,7 +306,13 @@ public class TransactionRecordController(
|
||||
var effectiveStartDate = startDate.Date;
|
||||
|
||||
var statistics = await transactionRepository.GetDailyStatisticsByRangeAsync(effectiveStartDate, effectiveEndDate);
|
||||
var result = statistics.Select(s => new DailyStatisticsDto(s.Key, s.Value.count, s.Value.amount)).ToList();
|
||||
var result = statistics.Select(s => new DailyStatisticsDto(
|
||||
s.Key,
|
||||
s.Value.count,
|
||||
s.Value.expense,
|
||||
s.Value.income,
|
||||
s.Value.income - s.Value.expense
|
||||
)).ToList();
|
||||
|
||||
return result.Ok();
|
||||
}
|
||||
@@ -755,7 +767,9 @@ public record UpdateTransactionDto(
|
||||
public record DailyStatisticsDto(
|
||||
string Date,
|
||||
int Count,
|
||||
decimal Amount
|
||||
decimal Expense,
|
||||
decimal Income,
|
||||
decimal Balance
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user