diff --git a/frontend/src/locales/messages.ts b/frontend/src/locales/messages.ts index 7e73911..6f8711e 100644 --- a/frontend/src/locales/messages.ts +++ b/frontend/src/locales/messages.ts @@ -310,7 +310,9 @@ export default { avgExpense: { zh: '平均每笔支出', en: 'Avg. Expense', ja: '平均支出' }, trend: { zh: '收支趋势', en: 'Income & Expense Trend', ja: '収支の推移' }, expenseCategoryPie: { zh: '支出分类占比', en: 'Expense by Category', ja: '支出カテゴリの割合' }, - paymentPie: { zh: '支付方式分布', en: 'Payment Method Distribution', ja: '支払方法の内訳' }, + incomeCategoryPie: { zh: '收入分类占比', en: 'Income by Category', ja: '収入カテゴリの割合' }, + expensePaymentPie: { zh: '支出支付方式占比', en: 'Expense by Payment Method', ja: '支出の支払方法の割合' }, + incomePaymentPie: { zh: '收入支付方式占比', en: 'Income by Payment Method', ja: '収入の支払方法の割合' }, uncategorized: { zh: '未分类', en: 'Uncategorized', ja: '未分類' }, }, diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 4d23b8c..79e4e75 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -120,7 +120,9 @@ const categoryData = ref([]) // 分类分组数据 const paymentData = ref([]) // 支付方式分组数据 const pieOption = ref({}) // 支出分类饼图 -const paymentOption = ref({}) // 支付方式饼图 +const incomePieOption = ref({}) // 收入分类饼图 +const paymentOption = ref({}) // 支出支付方式饼图 +const incomePaymentOption = ref({}) // 收入支付方式饼图 const trendOption = ref({}) // 趋势柱状图 const pad = (n: number) => String(n).padStart(2, '0') @@ -208,16 +210,52 @@ function updatePieChart() { } } -/** 更新支付方式分布饼图 */ -function updatePaymentChart() { - paymentOption.value = { - title: { text: t('dashboard.paymentPie'), left: 'center' }, +/** 更新收入分类饼图 */ +function updateIncomePieChart() { + incomePieOption.value = { + title: { text: t('dashboard.incomeCategoryPie'), left: 'center' }, tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, series: [ { type: 'pie', radius: ['40%', '70%'], - data: paymentData.value.map((p: any) => ({ name: p.name, value: Number(p.total) })), + data: categoryData.value + .filter((c: any) => c.type === 'INCOME') + .map((c: any) => ({ name: c.name, value: Number(c.total) })), + }, + ], + } +} + +/** 更新支出支付方式占比饼图:仅统计支出,标签按当前语言本地化 */ +function updatePaymentChart() { + paymentOption.value = { + title: { text: t('dashboard.expensePaymentPie'), left: 'center' }, + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + series: [ + { + type: 'pie', + radius: ['40%', '70%'], + data: paymentData.value + .filter((p: any) => Number(p.expense) > 0) + .map((p: any) => ({ name: getPaymentLabel(p.key), value: Number(p.expense) })), + }, + ], + } +} + +/** 更新收入支付方式占比饼图:仅统计收入,标签按当前语言本地化 */ +function updateIncomePaymentChart() { + incomePaymentOption.value = { + title: { text: t('dashboard.incomePaymentPie'), left: 'center' }, + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + series: [ + { + type: 'pie', + radius: ['40%', '70%'], + data: paymentData.value + .filter((p: any) => Number(p.income) > 0) + .map((p: any) => ({ name: getPaymentLabel(p.key), value: Number(p.income) })), }, ], } @@ -263,7 +301,9 @@ async function fetchData() { categoryData.value = categoryRes.data paymentData.value = paymentRes.data updatePieChart() + updateIncomePieChart() updatePaymentChart() + updateIncomePaymentChart() updateTrendChart() } catch (e: any) { console.error('加载统计数据失败', e) @@ -362,7 +402,9 @@ watch([rangeType, customRange, granularity, filterStatsScope, filterType, filter // 语言切换时重绘图表,更新标题与图例文案 watch(locale, () => { updatePieChart() + updateIncomePieChart() updatePaymentChart() + updateIncomePaymentChart() updateTrendChart() }) @@ -527,18 +569,28 @@ onMounted(async () => { - + + + + + + + + + + + diff --git a/frontend/src/views/TransactionList.vue b/frontend/src/views/TransactionList.vue index d84c3fa..1ec89b9 100644 --- a/frontend/src/views/TransactionList.vue +++ b/frontend/src/views/TransactionList.vue @@ -508,9 +508,6 @@ function createColumns(): DataTableColumns { { title: t('transaction.column.type'), key: 'type', width: 90, render(row) { - if (!row.includeInStats) { - return h(NTag, { type: 'default', size: 'small' }, { default: () => t('transaction.notIncluded') }) - } return h(NTag, { type: row.type === 'INCOME' ? 'success' : 'error', size: 'small' }, { default: () => row.type === 'INCOME' ? t('common.types.income') : t('common.types.expense') }) }, }, @@ -524,9 +521,6 @@ function createColumns(): DataTableColumns { { title: t('transaction.column.amount'), key: 'amount', width: 120, render(row) { - if (!row.includeInStats) { - return h('span', { style: { color: '#999', fontWeight: 'bold' } }, `${row.amount.toFixed(2)}`) - } return h('span', { style: { color: row.type === 'INCOME' ? '#18a058' : '#d03050', fontWeight: 'bold' } }, `${row.type === 'INCOME' ? '+' : '-'}${row.amount.toFixed(2)}`) }, @@ -1838,6 +1832,7 @@ onMounted(() => { :loading="loading" :bordered="false" :row-key="(row: TransactionItem) => row.id" + :row-class-name="(row: TransactionItem) => row.includeInStats ? '' : 'excluded-row'" v-model:checked-row-keys="checkedTransactionIds" /> @@ -2035,4 +2030,17 @@ onMounted(() => { padding-top: 12px; border-top: 1px solid rgba(0, 0, 0, 0.06); } +/* 不计入统计的记录整行加删除线 */ +:deep(.excluded-row .n-data-table-td) { + text-decoration: line-through; +} +/* 类型、金额列保持现状,不划删除线 */ +:deep(.excluded-row .n-data-table-td[data-col-key="type"]), +:deep(.excluded-row .n-data-table-td[data-col-key="amount"]) { + text-decoration: none; +} +/* 备注单元格为块级结构,需单独划线以保持整行效果 */ +:deep(.excluded-row .remark-text) { + text-decoration: line-through; +}