feat: Add TokenRecords finance management system

- Created new finance application based on Vue Vben Admin
- Implemented transaction management, category management, and loan tracking
- Added person management for tracking financial relationships
- Integrated budget management and financial analytics
- Added data import/export functionality
- Implemented responsive design for mobile support
- Added comprehensive testing with Playwright

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
你的用户名
2025-08-06 20:09:48 +08:00
parent b93e22c45a
commit 4b4616de1e
193 changed files with 17756 additions and 16 deletions

View File

@@ -0,0 +1,170 @@
<template>
<div class="monthly-comparison-chart">
<div ref="chartRef" class="chart-container"></div>
</div>
</template>
<script setup lang="ts">
import type { EChartsOption } from '#/components/charts/useChart';
import type { Transaction } from '#/types/finance';
import { computed, onMounted, ref, watch } from 'vue';
import { useChart } from '#/components/charts/useChart';
import dayjs from 'dayjs';
interface Props {
transactions: Transaction[];
year: number;
}
const props = defineProps<Props>();
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useChart(chartRef);
const chartData = computed(() => {
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
const incomeData = new Array(12).fill(0);
const expenseData = new Array(12).fill(0);
const netData = new Array(12).fill(0);
// 统计每月数据
props.transactions.forEach(transaction => {
const date = dayjs(transaction.date);
if (date.year() === props.year) {
const monthIndex = date.month(); // 0-11
if (transaction.type === 'income') {
incomeData[monthIndex] += transaction.amount;
} else {
expenseData[monthIndex] += transaction.amount;
}
}
});
// 计算净收入
for (let i = 0; i < 12; i++) {
netData[i] = incomeData[i] - expenseData[i];
}
return {
months,
income: incomeData,
expense: expenseData,
net: netData,
};
});
const chartOptions = computed<EChartsOption>(() => ({
title: {
text: `${props.year}年月度收支对比`,
left: 'center',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
formatter: (params: any) => {
let html = `<div style="font-weight: bold">${params[0].name}</div>`;
params.forEach((item: any) => {
const value = item.value.toFixed(2);
const prefix = item.seriesName === '净收入' && item.value > 0 ? '+' : '';
html += `<div>${item.marker} ${item.seriesName}: ${prefix}¥${value}</div>`;
});
return html;
},
},
toolbox: {
feature: {
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
restore: { show: true },
saveAsImage: { show: true },
},
},
legend: {
data: ['收入', '支出', '净收入'],
top: 30,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: chartData.value.months,
axisPointer: {
type: 'shadow',
},
},
],
yAxis: [
{
type: 'value',
name: '金额',
axisLabel: {
formatter: '¥{value}',
},
},
],
series: [
{
name: '收入',
type: 'bar',
data: chartData.value.income,
itemStyle: {
color: '#52c41a',
},
},
{
name: '支出',
type: 'bar',
data: chartData.value.expense,
itemStyle: {
color: '#ff4d4f',
},
},
{
name: '净收入',
type: 'line',
data: chartData.value.net,
itemStyle: {
color: '#1890ff',
},
lineStyle: {
width: 3,
},
symbol: 'circle',
symbolSize: 8,
},
],
}));
watch(chartOptions, (options) => {
setOptions(options);
});
onMounted(() => {
setOptions(chartOptions.value);
});
</script>
<style scoped>
.monthly-comparison-chart {
width: 100%;
height: 100%;
}
.chart-container {
width: 100%;
height: 400px;
}
</style>