deepsite / index.html
shengzi's picture
Add 1 files
ab73d6e verified
<!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">&#8203;</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>
// 配置SQL.js
const config = {
locateFile: filename => `https://cdn.jsdelivr.net/npm/sql.js@1.8.0/dist/${filename}`
};
// 初始化应用
document.addEventListener('DOMContentLoaded', async () => {
try {
// 加载SQL.js
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
);
`);
// 初始化Vue应用
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) {
// 如果Chart.js还未加载,延迟执行
setTimeout(this.updateChart, 100);
return;
}
if (this.chart) {
this.chart.destroy();
}
const ctx = document.getElementById('financeChart').getContext('2d');
if (this.chartType === 'bar') {
// 柱状图 - 显示最近7天的收支情况
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>