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

This commit is contained in:
孙诚
2026-01-15 22:10:57 +08:00
parent 71a8707241
commit 9069e3dbcf
5 changed files with 426 additions and 217 deletions

View File

@@ -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);
}
);

View File

@@ -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
View File

@@ -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

View File

@@ -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;
}

View File

@@ -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>