From ec460376c656c8c15afb91999f910f05ad5cc316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E8=AF=9A?= Date: Sun, 11 Jan 2026 16:33:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E6=A0=B7=E5=BC=8F=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Entity/BudgetArchive.cs | 5 + Service/BudgetService.cs | 8 +- Web/src/components/Budget/BudgetCard.vue | 2 +- Web/src/views/StatisticsView.vue | 365 ++++++++++++++++++----- 4 files changed, 300 insertions(+), 80 deletions(-) diff --git a/Entity/BudgetArchive.cs b/Entity/BudgetArchive.cs index a64a73c..22bca5f 100644 --- a/Entity/BudgetArchive.cs +++ b/Entity/BudgetArchive.cs @@ -22,6 +22,11 @@ public class BudgetArchive : BaseEntity /// public decimal RealizedAmount { get; set; } + /// + /// 详细描述 + /// + public string? Description { get; set; } + /// /// 归档目标年份 /// diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs index 6a162f0..921db86 100644 --- a/Service/BudgetService.cs +++ b/Service/BudgetService.cs @@ -96,7 +96,11 @@ public class BudgetService( if (archive != null) // 存在归档 直接读取归档数据 { budget.Limit = archive.BudgetedAmount; - return BudgetResult.FromEntity(budget, archive.RealizedAmount, referenceDate); + return BudgetResult.FromEntity( + budget, + archive.RealizedAmount, + referenceDate, + archive.Description ?? string.Empty); } } @@ -271,6 +275,7 @@ public class BudgetService( { archive.RealizedAmount = budget.Current; archive.ArchiveDate = DateTime.Now; + archive.Description = budget.Description; updateArchives.Add(archive); } else @@ -283,6 +288,7 @@ public class BudgetService( Month = month, BudgetedAmount = budget.Limit, RealizedAmount = budget.Current, + Description = budget.Description, ArchiveDate = DateTime.Now }; diff --git a/Web/src/components/Budget/BudgetCard.vue b/Web/src/components/Budget/BudgetCard.vue index bec281f..6c2c397 100644 --- a/Web/src/components/Budget/BudgetCard.vue +++ b/Web/src/components/Budget/BudgetCard.vue @@ -291,7 +291,7 @@ const timePercentage = computed(() => { margin-left: 0; margin-right: 0; margin-bottom: 0; - padding-bottom: 8px; + padding: 8px 12px; overflow: hidden; position: relative; cursor: pointer; diff --git a/Web/src/views/StatisticsView.vue b/Web/src/views/StatisticsView.vue index 5eedb65..e413912 100644 --- a/Web/src/views/StatisticsView.vue +++ b/Web/src/views/StatisticsView.vue @@ -7,62 +7,69 @@ - - - - - + + 加载统计数据中... - -
+ +
+ +
+ +
+ + - -
-
-
总支出
-
¥{{ formatMoney(monthlyData.totalExpense) }}
-
{{ monthlyData.expenseCount }}笔
-
-
-
-
总收入
-
¥{{ formatMoney(monthlyData.totalIncome) }}
-
{{ monthlyData.incomeCount }}笔
-
-
-
-
结余
-
- {{ monthlyData.balance >= 0 ? '' : '-' }}¥{{ formatMoney(Math.abs(monthlyData.balance)) }} +
+
总支出
+
¥{{ formatMoney(monthlyData.totalExpense) }}
+
{{ monthlyData.expenseCount }}笔
+
+
+
+
总收入
+
¥{{ formatMoney(monthlyData.totalIncome) }}
+
{{ monthlyData.incomeCount }}笔
+
+
+
+
结余
+
+ {{ monthlyData.balance >= 0 ? '' : '-' }}¥{{ formatMoney(Math.abs(monthlyData.balance)) }} +
+
{{ monthlyData.totalCount }}笔交易
+
+ + + + + +
+ {{ dateTagLabel }} + +
-
{{ monthlyData.totalCount }}笔交易
-
+ +
- -
+ +
+ +
+ +

支出分类统计

{{ expenseCategoriesView.length }}类 @@ -98,9 +105,9 @@
@@ -113,7 +120,7 @@
¥{{ formatMoney(category.amount) }}
{{ category.percent }}%
- +
@@ -134,9 +141,9 @@
@@ -149,7 +156,7 @@
¥{{ formatMoney(category.amount) }}
{{ category.percent }}%
- +
@@ -164,9 +171,9 @@
@@ -179,7 +186,7 @@
¥{{ formatMoney(category.amount) }}
{{ category.percent }}%
- +
@@ -260,6 +267,8 @@
+
+
@@ -331,12 +340,19 @@ const router = useRouter() // 响应式数据 const loading = ref(true) +const firstLoading = ref(true) const refreshing = ref(false) const showMonthPicker = ref(false) +const showAllExpense = ref(false) +const showAllIncome = ref(false) +const showAllNone = ref(false) const currentYear = ref(new Date().getFullYear()) const currentMonth = ref(new Date().getMonth() + 1) const selectedDate = ref([new Date().getFullYear().toString(), (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) @@ -371,33 +387,70 @@ const incomeCategories = ref([]) const noneCategories = ref([]) const expenseCategoriesView = computed(() => { - const sorted = [...expenseCategories.value] - const unclassifiedIndex = sorted.findIndex(c => !c.classify) + const list = [...expenseCategories.value] + const unclassifiedIndex = list.findIndex(c => !c.classify) if (unclassifiedIndex !== -1) { - const [unclassified] = sorted.splice(unclassifiedIndex, 1) - sorted.unshift(unclassified) + const [unclassified] = list.splice(unclassifiedIndex, 1) + list.unshift(unclassified) } - return sorted + + if (showAllExpense.value || list.length <= 7) return list + + const top = list.slice(0, 6) + const rest = list.slice(6) + top.push({ + classify: '其他', + amount: rest.reduce((s, c) => s + c.amount, 0), + count: rest.reduce((s, c) => s + c.count, 0), + percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)), + color: '#AAB7B8', + isOther: true + }) + return top }) const incomeCategoriesView = computed(() => { - const sorted = [...incomeCategories.value] - const unclassifiedIndex = sorted.findIndex(c => !c.classify) + const list = [...incomeCategories.value] + const unclassifiedIndex = list.findIndex(c => !c.classify) if (unclassifiedIndex !== -1) { - const [unclassified] = sorted.splice(unclassifiedIndex, 1) - sorted.unshift(unclassified) + const [unclassified] = list.splice(unclassifiedIndex, 1) + list.unshift(unclassified) } - return sorted + + if (showAllIncome.value || list.length <= 7) return list + + const top = list.slice(0, 6) + const rest = list.slice(6) + top.push({ + classify: '其他', + amount: rest.reduce((s, c) => s + c.amount, 0), + count: rest.reduce((s, c) => s + c.count, 0), + percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)), + isOther: true + }) + return top }) const noneCategoriesView = computed(() => { - const sorted = [...noneCategories.value] - const unclassifiedIndex = sorted.findIndex(c => !c.classify) + const list = [...noneCategories.value] + const unclassifiedIndex = list.findIndex(c => !c.classify) if (unclassifiedIndex !== -1) { - const [unclassified] = sorted.splice(unclassifiedIndex, 1) - sorted.unshift(unclassified) + const [unclassified] = list.splice(unclassifiedIndex, 1) + list.unshift(unclassified) } - return sorted + + if (showAllNone.value || list.length <= 7) return list + + const top = list.slice(0, 6) + const rest = list.slice(6) + top.push({ + classify: '其他', + amount: rest.reduce((s, c) => s + c.amount, 0), + count: rest.reduce((s, c) => s + c.count, 0), + percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)), + isOther: true + }) + return top }) // 趋势数据 @@ -418,7 +471,7 @@ const colors = [ const circumference = computed(() => 2 * Math.PI * 70) const chartSegments = computed(() => { let offset = 0 - return expenseCategories.value.map((category) => { + return expenseCategoriesView.value.map((category) => { const percent = category.percent / 100 const length = circumference.value * percent const segment = { @@ -452,6 +505,31 @@ const isCurrentMonth = computed(() => { 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 === '' @@ -484,6 +562,7 @@ const getBarHeight = (value, maxValue) => { // 切换月份 const changeMonth = (offset) => { + transitionName.value = offset > 0 ? 'slide-left' : 'slide-right' let newMonth = currentMonth.value + offset let newYear = currentYear.value @@ -504,26 +583,51 @@ const changeMonth = (offset) => { currentYear.value = newYear currentMonth.value = newMonth + + // 重置展开状态 + showAllExpense.value = false + showAllIncome.value = false + showAllNone.value = false + fetchStatistics() } // 确认月份选择 const onMonthConfirm = ({ selectedValues }) => { - currentYear.value = parseInt(selectedValues[0]) - currentMonth.value = parseInt(selectedValues[1]) + 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 + + // 重置展开状态 + showAllExpense.value = false + showAllIncome.value = false + showAllNone.value = false + fetchStatistics() } // 下拉刷新 const onRefresh = async () => { - await fetchStatistics() + await fetchStatistics(false) refreshing.value = false } // 获取统计数据 -const fetchStatistics = async () => { - loading.value = true +const fetchStatistics = async (showLoading = true) => { + if (showLoading && firstLoading.value) { + loading.value = true + } + try { await Promise.all([ fetchMonthlyData(), @@ -535,6 +639,7 @@ const fetchStatistics = async () => { showToast('获取统计数据失败') } finally { loading.value = false + firstLoading.value = false } } @@ -863,8 +968,24 @@ onBeforeUnmount(() => {