feat: 仪表盘新增收入分类饼图并拆分收支支付方式占比
- 仪表盘新增「收入分类占比」饼图,支付方式拆分为支出、收入两张饼图 - 支付方式标签按当前语言本地化,仅统计对应方向的记录 - 收支记录中不计入统计的记录整行加删除线(类型、金额列除外) - 补充中/英/日三语文案
This commit is contained in:
@@ -310,7 +310,9 @@ export default {
|
|||||||
avgExpense: { zh: '平均每笔支出', en: 'Avg. Expense', ja: '平均支出' },
|
avgExpense: { zh: '平均每笔支出', en: 'Avg. Expense', ja: '平均支出' },
|
||||||
trend: { zh: '收支趋势', en: 'Income & Expense Trend', ja: '収支の推移' },
|
trend: { zh: '收支趋势', en: 'Income & Expense Trend', ja: '収支の推移' },
|
||||||
expenseCategoryPie: { zh: '支出分类占比', en: 'Expense by Category', 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: '未分類' },
|
uncategorized: { zh: '未分类', en: 'Uncategorized', ja: '未分類' },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ const categoryData = ref<any[]>([]) // 分类分组数据
|
|||||||
const paymentData = ref<any[]>([]) // 支付方式分组数据
|
const paymentData = ref<any[]>([]) // 支付方式分组数据
|
||||||
|
|
||||||
const pieOption = ref({}) // 支出分类饼图
|
const pieOption = ref({}) // 支出分类饼图
|
||||||
const paymentOption = ref({}) // 支付方式饼图
|
const incomePieOption = ref({}) // 收入分类饼图
|
||||||
|
const paymentOption = ref({}) // 支出支付方式饼图
|
||||||
|
const incomePaymentOption = ref({}) // 收入支付方式饼图
|
||||||
const trendOption = ref({}) // 趋势柱状图
|
const trendOption = ref({}) // 趋势柱状图
|
||||||
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
@@ -208,16 +210,52 @@ function updatePieChart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新支付方式分布饼图 */
|
/** 更新收入分类饼图 */
|
||||||
function updatePaymentChart() {
|
function updateIncomePieChart() {
|
||||||
paymentOption.value = {
|
incomePieOption.value = {
|
||||||
title: { text: t('dashboard.paymentPie'), left: 'center' },
|
title: { text: t('dashboard.incomeCategoryPie'), left: 'center' },
|
||||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
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
|
categoryData.value = categoryRes.data
|
||||||
paymentData.value = paymentRes.data
|
paymentData.value = paymentRes.data
|
||||||
updatePieChart()
|
updatePieChart()
|
||||||
|
updateIncomePieChart()
|
||||||
updatePaymentChart()
|
updatePaymentChart()
|
||||||
|
updateIncomePaymentChart()
|
||||||
updateTrendChart()
|
updateTrendChart()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('加载统计数据失败', e)
|
console.error('加载统计数据失败', e)
|
||||||
@@ -362,7 +402,9 @@ watch([rangeType, customRange, granularity, filterStatsScope, filterType, filter
|
|||||||
// 语言切换时重绘图表,更新标题与图例文案
|
// 语言切换时重绘图表,更新标题与图例文案
|
||||||
watch(locale, () => {
|
watch(locale, () => {
|
||||||
updatePieChart()
|
updatePieChart()
|
||||||
|
updateIncomePieChart()
|
||||||
updatePaymentChart()
|
updatePaymentChart()
|
||||||
|
updateIncomePaymentChart()
|
||||||
updateTrendChart()
|
updateTrendChart()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -527,18 +569,28 @@ onMounted(async () => {
|
|||||||
<VChart :option="trendOption" style="height: 350px;" autoresize />
|
<VChart :option="trendOption" style="height: 350px;" autoresize />
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<!-- 饼图区域:支出分类 + 支付方式 -->
|
<!-- 饼图区域:支出/收入分类占比 + 支出/收入支付方式占比 -->
|
||||||
<NGrid :cols="2" :x-gap="16" :y-gap="16">
|
<NGrid :cols="2" :x-gap="16" :y-gap="16">
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="pieOption" style="height: 350px;" autoresize />
|
<VChart :option="pieOption" style="height: 350px;" autoresize />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<NCard>
|
||||||
|
<VChart :option="incomePieOption" style="height: 350px;" autoresize />
|
||||||
|
</NCard>
|
||||||
|
</NGridItem>
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NCard>
|
<NCard>
|
||||||
<VChart :option="paymentOption" style="height: 350px;" autoresize />
|
<VChart :option="paymentOption" style="height: 350px;" autoresize />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
|
<NGridItem>
|
||||||
|
<NCard>
|
||||||
|
<VChart :option="incomePaymentOption" style="height: 350px;" autoresize />
|
||||||
|
</NCard>
|
||||||
|
</NGridItem>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -508,9 +508,6 @@ function createColumns(): DataTableColumns<TransactionItem> {
|
|||||||
{
|
{
|
||||||
title: t('transaction.column.type'), key: 'type', width: 90,
|
title: t('transaction.column.type'), key: 'type', width: 90,
|
||||||
render(row) {
|
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') })
|
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<TransactionItem> {
|
|||||||
{
|
{
|
||||||
title: t('transaction.column.amount'), key: 'amount', width: 120,
|
title: t('transaction.column.amount'), key: 'amount', width: 120,
|
||||||
render(row) {
|
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' } },
|
return h('span', { style: { color: row.type === 'INCOME' ? '#18a058' : '#d03050', fontWeight: 'bold' } },
|
||||||
`${row.type === 'INCOME' ? '+' : '-'}${row.amount.toFixed(2)}`)
|
`${row.type === 'INCOME' ? '+' : '-'}${row.amount.toFixed(2)}`)
|
||||||
},
|
},
|
||||||
@@ -1838,6 +1832,7 @@ onMounted(() => {
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
:row-key="(row: TransactionItem) => row.id"
|
:row-key="(row: TransactionItem) => row.id"
|
||||||
|
:row-class-name="(row: TransactionItem) => row.includeInStats ? '' : 'excluded-row'"
|
||||||
v-model:checked-row-keys="checkedTransactionIds"
|
v-model:checked-row-keys="checkedTransactionIds"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -2035,4 +2030,17 @@ onMounted(() => {
|
|||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user