|
|
<!DOCTYPE html> |
|
|
<html lang="zh-CN"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>个人记账系统</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/sql-wasm.js"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<style> |
|
|
.fade-enter-active, .fade-leave-active { |
|
|
transition: opacity 0.3s; |
|
|
} |
|
|
.fade-enter, .fade-leave-to { |
|
|
opacity: 0; |
|
|
} |
|
|
.chart-container { |
|
|
height: 300px; |
|
|
} |
|
|
.scrollbar-hide::-webkit-scrollbar { |
|
|
display: none; |
|
|
} |
|
|
.scrollbar-hide { |
|
|
-ms-overflow-style: none; |
|
|
scrollbar-width: none; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 font-sans"> |
|
|
<div class="min-h-screen flex flex-col" id="app"> |
|
|
|
|
|
<header class="bg-blue-600 text-white shadow-md"> |
|
|
<div class="container mx-auto px-4 py-3 flex justify-between items-center"> |
|
|
<div class="flex items-center space-x-2"> |
|
|
<i class="fas fa-wallet text-2xl"></i> |
|
|
<h1 class="text-xl font-bold">个人记账系统</h1> |
|
|
</div> |
|
|
<div class="flex items-center space-x-4"> |
|
|
<button @click="showAddModal = true" class="bg-white text-blue-600 px-4 py-2 rounded-full font-medium hover:bg-blue-50 transition"> |
|
|
<i class="fas fa-plus mr-1"></i> 添加记录 |
|
|
</button> |
|
|
<div class="relative"> |
|
|
<button @click="userMenuOpen = !userMenuOpen" class="flex items-center space-x-2 focus:outline-none"> |
|
|
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center"> |
|
|
<i class="fas fa-user"></i> |
|
|
</div> |
|
|
</button> |
|
|
<div v-if="userMenuOpen" @click.away="userMenuOpen = false" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50"> |
|
|
<a href="#" class="block px-4 py-2 text-gray-800 hover:bg-blue-50">个人资料</a> |
|
|
<a href="#" class="block px-4 py-2 text-gray-800 hover:bg-blue-50">设置</a> |
|
|
<div class="border-t border-gray-200"></div> |
|
|
<a href="#" class="block px-4 py-2 text-gray-800 hover:bg-blue-50">退出</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main class="flex-1 container mx-auto px-4 py-6"> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> |
|
|
<div class="bg-white rounded-lg shadow p-6"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div> |
|
|
<p class="text-gray-500">总收入</p> |
|
|
<p class="text-2xl font-bold text-green-500">¥ {{ formatCurrency(totalIncome) }}</p> |
|
|
</div> |
|
|
<div class="bg-green-100 p-3 rounded-full"> |
|
|
<i class="fas fa-arrow-up text-green-600"></i> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white rounded-lg shadow p-6"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div> |
|
|
<p class="text-gray-500">总支出</p> |
|
|
<p class="text-2xl font-bold text-red-500">¥ {{ formatCurrency(totalExpense) }}</p> |
|
|
</div> |
|
|
<div class="bg-red-100 p-3 rounded-full"> |
|
|
<i class="fas fa-arrow-down text-red-600"></i> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white rounded-lg shadow p-6"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div> |
|
|
<p class="text-gray-500">结余</p> |
|
|
<p class="text-2xl font-bold text-blue-500">¥ {{ formatCurrency(totalIncome - totalExpense) }}</p> |
|
|
</div> |
|
|
<div class="bg-blue-100 p-3 rounded-full"> |
|
|
<i class="fas fa-balance-scale text-blue-600"></i> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
|
|
|
|
|
<div class="lg:col-span-2 bg-white rounded-lg shadow p-6"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h2 class="text-lg font-semibold">收支分析</h2> |
|
|
<div class="flex space-x-2"> |
|
|
<button @click="chartType = 'bar'" :class="{'bg-blue-100 text-blue-600': chartType === 'bar'}" class="px-3 py-1 rounded text-sm">柱状图</button> |
|
|
<button @click="chartType = 'pie'" :class="{'bg-blue-100 text-blue-600': chartType === 'pie'}" class="px-3 py-1 rounded text-sm">饼图</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="chart-container"> |
|
|
<canvas id="financeChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-6"> |
|
|
<h2 class="text-lg font-semibold mb-4">分类统计</h2> |
|
|
<div class="space-y-4"> |
|
|
<div v-for="category in expenseCategories" :key="category.name" class="flex items-center"> |
|
|
<div class="w-8 h-8 rounded-full flex items-center justify-center" :class="category.color"> |
|
|
<i :class="category.icon"></i> |
|
|
</div> |
|
|
<div class="ml-3 flex-1"> |
|
|
<div class="flex justify-between text-sm"> |
|
|
<span>{{ category.name }}</span> |
|
|
<span class="font-medium">¥ {{ formatCurrency(category.amount) }}</span> |
|
|
</div> |
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5 mt-1"> |
|
|
<div class="bg-blue-600 h-1.5 rounded-full" :style="`width: ${(category.amount / totalExpense) * 100}%`"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mt-8 bg-white rounded-lg shadow overflow-hidden"> |
|
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> |
|
|
<h2 class="text-lg font-semibold">交易记录</h2> |
|
|
<div class="flex space-x-2"> |
|
|
<div class="relative"> |
|
|
<select v-model="filterType" class="appearance-none bg-gray-100 border-0 py-1 pl-3 pr-8 rounded-full text-sm focus:outline-none"> |
|
|
<option value="all">所有类型</option> |
|
|
<option value="income">收入</option> |
|
|
<option value="expense">支出</option> |
|
|
</select> |
|
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> |
|
|
<i class="fas fa-chevron-down text-xs"></i> |
|
|
</div> |
|
|
</div> |
|
|
<div class="relative"> |
|
|
<select v-model="filterCategory" class="appearance-none bg-gray-100 border-0 py-1 pl-3 pr-8 rounded-full text-sm focus:outline-none"> |
|
|
<option value="all">所有分类</option> |
|
|
<option v-for="cat in allCategories" :value="cat.name">{{ cat.name }}</option> |
|
|
</select> |
|
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> |
|
|
<i class="fas fa-chevron-down text-xs"></i> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="overflow-x-auto scrollbar-hide"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr> |
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th> |
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th> |
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分类</th> |
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th> |
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">备注</th> |
|
|
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
|
<tr v-for="record in filteredRecords" :key="record.id"> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(record.date) }}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap"> |
|
|
<span :class="record.type === 'income' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"> |
|
|
{{ record.type === 'income' ? '收入' : '支出' }} |
|
|
</span> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-5 h-5 rounded-full flex items-center justify-center mr-2" :class="getCategoryColor(record.category)"> |
|
|
<i :class="getCategoryIcon(record.category)" class="text-xs"></i> |
|
|
</div> |
|
|
{{ record.category }} |
|
|
</div> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm" :class="record.type === 'income' ? 'text-green-600 font-medium' : 'text-red-600 font-medium'"> |
|
|
¥ {{ formatCurrency(record.amount) }} |
|
|
</td> |
|
|
<td class="px-6 py-4 text-sm text-gray-500">{{ record.note || '-' }}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> |
|
|
<button @click="editRecord(record)" class="text-blue-600 hover:text-blue-900 mr-3"><i class="fas fa-edit"></i></button> |
|
|
<button @click="deleteRecord(record.id)" class="text-red-600 hover:text-red-900"><i class="fas fa-trash-alt"></i></button> |
|
|
</td> |
|
|
</tr> |
|
|
<tr v-if="filteredRecords.length === 0"> |
|
|
<td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">暂无记录</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
|
|
|
<div v-if="showAddModal" class="fixed inset-0 overflow-y-auto z-50"> |
|
|
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> |
|
|
<transition name="fade"> |
|
|
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> |
|
|
<div class="absolute inset-0 bg-gray-500 opacity-75"></div> |
|
|
</div> |
|
|
</transition> |
|
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> |
|
|
|
|
|
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> |
|
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> |
|
|
<div class="sm:flex sm:items-start"> |
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> |
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> |
|
|
{{ editingRecord ? '编辑记录' : '添加新记录' }} |
|
|
</h3> |
|
|
<div class="mt-2 space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">类型</label> |
|
|
<div class="flex space-x-4"> |
|
|
<label class="inline-flex items-center"> |
|
|
<input type="radio" v-model="newRecord.type" value="income" class="form-radio h-4 w-4 text-green-600"> |
|
|
<span class="ml-2">收入</span> |
|
|
</label> |
|
|
<label class="inline-flex items-center"> |
|
|
<input type="radio" v-model="newRecord.type" value="expense" class="form-radio h-4 w-4 text-red-600"> |
|
|
<span class="ml-2">支出</span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label for="category" class="block text-sm font-medium text-gray-700 mb-1">分类</label> |
|
|
<select id="category" v-model="newRecord.category" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> |
|
|
<option v-for="cat in (newRecord.type === 'income' ? incomeCategories : expenseCategories)" :value="cat.name">{{ cat.name }}</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label for="amount" class="block text-sm font-medium text-gray-700 mb-1">金额</label> |
|
|
<div class="mt-1 relative rounded-md shadow-sm"> |
|
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
|
|
<span class="text-gray-500 sm:text-sm">¥</span> |
|
|
</div> |
|
|
<input type="number" id="amount" v-model="newRecord.amount" class="focus:ring-blue-500 focus:border-blue-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" placeholder="0.00"> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label for="date" class="block text-sm font-medium text-gray-700 mb-1">日期</label> |
|
|
<input type="date" id="date" v-model="newRecord.date" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"> |
|
|
</div> |
|
|
<div> |
|
|
<label for="note" class="block text-sm font-medium text-gray-700 mb-1">备注</label> |
|
|
<textarea id="note" v-model="newRecord.note" rows="2" class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border border-gray-300 rounded-md"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> |
|
|
<button @click="saveRecord" type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"> |
|
|
保存 |
|
|
</button> |
|
|
<button @click="showAddModal = false" type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> |
|
|
取消 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> |
|
|
<script> |
|
|
|
|
|
const config = { |
|
|
locateFile: filename => `https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/${filename}` |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
|
try { |
|
|
|
|
|
const SQL = await initSqlJs(config); |
|
|
|
|
|
|
|
|
const db = new SQL.Database(); |
|
|
|
|
|
|
|
|
db.run(` |
|
|
CREATE TABLE IF NOT EXISTS records ( |
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
type TEXT NOT NULL, |
|
|
category TEXT NOT NULL, |
|
|
amount REAL NOT NULL, |
|
|
date TEXT NOT NULL, |
|
|
note TEXT |
|
|
); |
|
|
`); |
|
|
|
|
|
|
|
|
new Vue({ |
|
|
el: '#app', |
|
|
data: { |
|
|
db: db, |
|
|
records: [], |
|
|
newRecord: { |
|
|
type: 'expense', |
|
|
category: '餐饮', |
|
|
amount: '', |
|
|
date: new Date().toISOString().split('T')[0], |
|
|
note: '' |
|
|
}, |
|
|
editingRecord: null, |
|
|
showAddModal: false, |
|
|
userMenuOpen: false, |
|
|
filterType: 'all', |
|
|
filterCategory: 'all', |
|
|
chartType: 'bar', |
|
|
chart: null, |
|
|
incomeCategories: [ |
|
|
{ name: '工资', icon: 'fas fa-money-bill-wave', color: 'bg-green-100 text-green-600' }, |
|
|
{ name: '奖金', icon: 'fas fa-gift', color: 'bg-blue-100 text-blue-600' }, |
|
|
{ name: '投资', icon: 'fas fa-chart-line', color: 'bg-purple-100 text-purple-600' }, |
|
|
{ name: '其他收入', icon: 'fas fa-coins', color: 'bg-yellow-100 text-yellow-600' } |
|
|
], |
|
|
expenseCategories: [ |
|
|
{ name: '餐饮', icon: 'fas fa-utensils', color: 'bg-red-100 text-red-600' }, |
|
|
{ name: '购物', icon: 'fas fa-shopping-bag', color: 'bg-pink-100 text-pink-600' }, |
|
|
{ name: '交通', icon: 'fas fa-bus', color: 'bg-indigo-100 text-indigo-600' }, |
|
|
{ name: '娱乐', icon: 'fas fa-gamepad', color: 'bg-purple-100 text-purple-600' }, |
|
|
{ name: '住房', icon: 'fas fa-home', color: 'bg-blue-100 text-blue-600' }, |
|
|
{ name: '医疗', icon: 'fas fa-heartbeat', color: 'bg-green-100 text-green-600' }, |
|
|
{ name: '教育', icon: 'fas fa-book', color: 'bg-yellow-100 text-yellow-600' }, |
|
|
{ name: '其他支出', icon: 'fas fa-ellipsis-h', color: 'bg-gray-100 text-gray-600' } |
|
|
] |
|
|
}, |
|
|
computed: { |
|
|
allCategories() { |
|
|
return [...this.incomeCategories, ...this.expenseCategories]; |
|
|
}, |
|
|
filteredRecords() { |
|
|
let filtered = this.records; |
|
|
|
|
|
if (this.filterType !== 'all') { |
|
|
filtered = filtered.filter(r => r.type === this.filterType); |
|
|
} |
|
|
|
|
|
if (this.filterCategory !== 'all') { |
|
|
filtered = filtered.filter(r => r.category === this.filterCategory); |
|
|
} |
|
|
|
|
|
return filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); |
|
|
}, |
|
|
totalIncome() { |
|
|
return this.records |
|
|
.filter(r => r.type === 'income') |
|
|
.reduce((sum, r) => sum + r.amount, 0); |
|
|
}, |
|
|
totalExpense() { |
|
|
return this.records |
|
|
.filter(r => r.type === 'expense') |
|
|
.reduce((sum, r) => sum + r.amount, 0); |
|
|
}, |
|
|
categorizedExpenses() { |
|
|
const result = {}; |
|
|
this.expenseCategories.forEach(cat => { |
|
|
result[cat.name] = 0; |
|
|
}); |
|
|
|
|
|
this.records |
|
|
.filter(r => r.type === 'expense') |
|
|
.forEach(r => { |
|
|
if (result[r.category] !== undefined) { |
|
|
result[r.category] += r.amount; |
|
|
} else { |
|
|
result['其他支出'] += r.amount; |
|
|
} |
|
|
}); |
|
|
|
|
|
return this.expenseCategories.map(cat => ({ |
|
|
...cat, |
|
|
amount: result[cat.name] |
|
|
})); |
|
|
} |
|
|
}, |
|
|
methods: { |
|
|
loadRecords() { |
|
|
const stmt = this.db.prepare('SELECT * FROM records ORDER BY date DESC'); |
|
|
const records = []; |
|
|
while (stmt.step()) { |
|
|
records.push(stmt.getAsObject()); |
|
|
} |
|
|
stmt.free(); |
|
|
this.records = records; |
|
|
this.updateChart(); |
|
|
}, |
|
|
saveRecord() { |
|
|
if (!this.newRecord.amount || isNaN(this.newRecord.amount)) { |
|
|
alert('请输入有效的金额'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const amount = parseFloat(this.newRecord.amount); |
|
|
if (amount <= 0) { |
|
|
alert('金额必须大于0'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!this.newRecord.date) { |
|
|
alert('请选择日期'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (this.editingRecord) { |
|
|
|
|
|
const stmt = this.db.prepare( |
|
|
'UPDATE records SET type = ?, category = ?, amount = ?, date = ?, note = ? WHERE id = ?' |
|
|
); |
|
|
stmt.bind([ |
|
|
this.newRecord.type, |
|
|
this.newRecord.category, |
|
|
amount, |
|
|
this.newRecord.date, |
|
|
this.newRecord.note || null, |
|
|
this.editingRecord.id |
|
|
]); |
|
|
stmt.step(); |
|
|
stmt.free(); |
|
|
} else { |
|
|
|
|
|
const stmt = this.db.prepare( |
|
|
'INSERT INTO records (type, category, amount, date, note) VALUES (?, ?, ?, ?, ?)' |
|
|
); |
|
|
stmt.bind([ |
|
|
this.newRecord.type, |
|
|
this.newRecord.category, |
|
|
amount, |
|
|
this.newRecord.date, |
|
|
this.newRecord.note || null |
|
|
]); |
|
|
stmt.step(); |
|
|
stmt.free(); |
|
|
} |
|
|
|
|
|
this.loadRecords(); |
|
|
this.resetForm(); |
|
|
this.showAddModal = false; |
|
|
}, |
|
|
editRecord(record) { |
|
|
this.editingRecord = record; |
|
|
this.newRecord = { |
|
|
type: record.type, |
|
|
category: record.category, |
|
|
amount: record.amount, |
|
|
date: record.date, |
|
|
note: record.note || '' |
|
|
}; |
|
|
this.showAddModal = true; |
|
|
}, |
|
|
deleteRecord(id) { |
|
|
if (confirm('确定要删除这条记录吗?')) { |
|
|
const stmt = this.db.prepare('DELETE FROM records WHERE id = ?'); |
|
|
stmt.bind([id]); |
|
|
stmt.step(); |
|
|
stmt.free(); |
|
|
this.loadRecords(); |
|
|
} |
|
|
}, |
|
|
resetForm() { |
|
|
this.newRecord = { |
|
|
type: 'expense', |
|
|
category: '餐饮', |
|
|
amount: '', |
|
|
date: new Date().toISOString().split('T')[0], |
|
|
note: '' |
|
|
}; |
|
|
this.editingRecord = null; |
|
|
}, |
|
|
formatCurrency(value) { |
|
|
return value.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); |
|
|
}, |
|
|
formatDate(dateStr) { |
|
|
const date = new Date(dateStr); |
|
|
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; |
|
|
}, |
|
|
getCategoryIcon(categoryName) { |
|
|
const category = this.allCategories.find(c => c.name === categoryName); |
|
|
return category ? category.icon : 'fas fa-question-circle'; |
|
|
}, |
|
|
getCategoryColor(categoryName) { |
|
|
const category = this.allCategories.find(c => c.name === categoryName); |
|
|
return category ? category.color : 'bg-gray-100 text-gray-600'; |
|
|
}, |
|
|
updateChart() { |
|
|
if (!window.Chart) { |
|
|
|
|
|
setTimeout(this.updateChart, 100); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (this.chart) { |
|
|
this.chart.destroy(); |
|
|
} |
|
|
|
|
|
const ctx = document.getElementById('financeChart').getContext('2d'); |
|
|
|
|
|
if (this.chartType === 'bar') { |
|
|
|
|
|
const last7Days = this.getLastNDays(7); |
|
|
const incomeData = last7Days.map(day => { |
|
|
return this.records |
|
|
.filter(r => r.type === 'income' && r.date === day) |
|
|
.reduce((sum, r) => sum + r.amount, 0); |
|
|
}); |
|
|
const expenseData = last7Days.map(day => { |
|
|
return this.records |
|
|
.filter(r => r.type === 'expense' && r.date === day) |
|
|
.reduce((sum, r) => sum + r.amount, 0); |
|
|
}); |
|
|
|
|
|
this.chart = new window.Chart(ctx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: last7Days.map(day => day.split('-').slice(1).join('-')), |
|
|
datasets: [ |
|
|
{ |
|
|
label: '收入', |
|
|
data: incomeData, |
|
|
backgroundColor: 'rgba(16, 185, 129, 0.7)', |
|
|
borderColor: 'rgba(16, 185, 129, 1)', |
|
|
borderWidth: 1 |
|
|
}, |
|
|
{ |
|
|
label: '支出', |
|
|
data: expenseData, |
|
|
backgroundColor: 'rgba(239, 68, 68, 0.7)', |
|
|
borderColor: 'rgba(239, 68, 68, 1)', |
|
|
borderWidth: 1 |
|
|
} |
|
|
] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
scales: { |
|
|
y: { |
|
|
beginAtZero: true, |
|
|
ticks: { |
|
|
callback: value => '¥' + value |
|
|
} |
|
|
} |
|
|
}, |
|
|
plugins: { |
|
|
tooltip: { |
|
|
callbacks: { |
|
|
label: context => { |
|
|
return context.dataset.label + ': ¥' + context.raw.toFixed(2); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} else { |
|
|
|
|
|
const data = this.categorizedExpenses |
|
|
.filter(cat => cat.amount > 0) |
|
|
.map(cat => ({ |
|
|
name: cat.name, |
|
|
value: cat.amount, |
|
|
color: cat.color.split(' ')[1].replace('text-', '') |
|
|
})); |
|
|
|
|
|
this.chart = new window |
|
|
</html> |