
我们可以通过算法来完成很多很多的功能,所以就有了一个想法,将各类工具都写出来,当然是尽可能的,毕竟未来无限可期,很多功能是我们当前还想不到的,为了最为靠谱的方法来完成,这里选择的语言为 HTML 来完成,别看只是简单的页面操作,但是里面都是各类算法来完成的,并且有很多的js库,做起来会很方便,能节约很多的时间,本系列文章会很多,有些标题命名可能不太合适,如果你用到了,感觉不好找可以在文章下面留言,我看到了后会进行对应的修改,希望本系列文章能给大家提供到各种各样的遍历。
简单记账本是一款轻量级的个人财务管理工具,旨在帮助用户轻松记录日常收支,并通过直观的数据可视化功能,提供清晰的财务状况分析。该应用采用现代化的界面设计,操作简便,适合各类用户使用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单记账本</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #6eb48c;
--secondary-color: #f5f8f6;
--accent-color: #4a9e76;
--text-color: #333;
--light-text: #666;
--border-radius: 10px;
--box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
color: var(--text-color);
line-height: 1.6;
}
.header {
background-color: white;
box-shadow: var(--box-shadow);
padding: 1.5rem 0;
margin-bottom: 2rem;
}
.header h1 {
color: var(--primary-color);
font-weight: 600;
}
.container {
max-width: 1200px;
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
transition: transform 0.3s ease;
margin-bottom: 1.5rem;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
}
.card-header {
background-color: var(--primary-color);
color: white;
font-weight: 600;
padding: 0.8rem 1.2rem;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--accent-color);
border-color: var(--accent-color);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
color: white;
}
.transaction-item {
border-left: 4px solid transparent;
background-color: white;
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.transaction-item:hover {
box-shadow: var(--box-shadow);
}
.transaction-item.income {
border-left-color: #28a745;
}
.transaction-item.expense {
border-left-color: #dc3545;
}
.amount {
font-weight: 600;
}
.income .amount {
color: #28a745;
}
.expense .amount {
color: #dc3545;
}
.category-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
background-color: var(--secondary-color);
color: var(--primary-color);
}
.footer {
background-color: white;
padding: 2rem 0;
margin-top: 3rem;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
}
.footer p {
margin-bottom: 0;
color: var(--light-text);
}
.summary-card {
text-align: center;
padding: 1.5rem;
}
.summary-value {
font-size: 1.8rem;
font-weight: bold;
display: block;
margin: 0.5rem 0;
}
.income-value {
color: #28a745;
}
.expense-value {
color: #dc3545;
}
.balance-value {
color: var(--primary-color);
}
.chart-container {
position: relative;
height: 300px;
margin-bottom: 1.5rem;
}
.date-filter {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
}
.date-filter .btn {
margin: 0 0.25rem;
}
.modal-content {
border-radius: var(--border-radius);
}
.modal-header {
background-color: var(--primary-color);
color: white;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.modal-title {
font-weight: 600;
}
.btn-close {
filter: brightness(0) invert(1);
}
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
@media (max-width: 768px) {
.header {
padding: 1rem 0;
margin-bottom: 1rem;
}
.card {
margin-bottom: 1rem;
}
.summary-value {
font-size: 1.5rem;
}
.chart-container {
height: 250px;
}
.transaction-item {
padding: 0.75rem;
}
.date-filter {
flex-wrap: wrap;
}
.date-filter .btn {
margin-bottom: 0.5rem;
}
}
</style>
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6">
<h1><i class="bi bi-wallet2 me-2"></i>简单记账本</h1>
<p class="text-muted">轻松记录每一笔收支,智能分析财务状况</p>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTransactionModal">
<i class="bi bi-plus-circle me-1"></i>记一笔
</button>
</div>
</div>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-4">
<div class="card summary-card">
<div class="card-body">
<h5>本月收入</h5>
<span class="summary-value income-value" id="totalIncome">¥0.00</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card summary-card">
<div class="card-body">
<h5>本月支出</h5>
<span class="summary-value expense-value" id="totalExpense">¥0.00</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card summary-card">
<div class="card-body">
<h5>本月结余</h5>
<span class="summary-value balance-value" id="totalBalance">¥0.00</span>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>收支明细</span>
<div class="dropdown">
<button class="btn btn-sm btn-light dropdown-toggle" type="button" id="filterDropdown" data-bs-toggle="dropdown" aria-expanded="false">
筛选
</button>
<ul class="dropdown-menu" aria-labelledby="filterDropdown">
<li><a class="dropdown-item filter-item" href="#" data-filter="all">全部</a></li>
<li><a class="dropdown-item filter-item" href="#" data-filter="income">仅收入</a></li>
<li><a class="dropdown-item filter-item" href="#" data-filter="expense">仅支出</a></li>
</ul>
</div>
</div>
<div class="card-body">
<div class="date-filter">
<button class="btn btn-sm btn-outline-primary active" data-period="month">本月</button>
<button class="btn btn-sm btn-outline-primary" data-period="week">本周</button>
<button class="btn btn-sm btn-outline-primary" data-period="day">今天</button>
</div>
<div id="transactionList">
<!-- 交易记录将通过JavaScript动态添加 -->
</div>
<div id="emptyState" class="text-center py-5">
<i class="bi bi-journal-text" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-3 text-muted">暂无交易记录</p>
<button class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#addTransactionModal">
添加第一笔记录
</button>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<span>支出分类统计</span>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="expenseChart"></canvas>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span>收入分类统计</span>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="incomeChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<span>月度收支趋势</span>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加交易模态框 -->
<div class="modal fade" id="addTransactionModal" tabindex="-1" aria-labelledby="addTransactionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTransactionModalLabel">记一笔</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="transactionForm">
<div class="mb-3">
<label class="form-label">类型</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="transactionType" id="incomeType" value="income" checked>
<label class="form-check-label" for="incomeType">收入</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="transactionType" id="expenseType" value="expense">
<label class="form-check-label" for="expenseType">支出</label>
</div>
</div>
<div class="mb-3">
<label for="amount" class="form-label">金额</label>
<div class="input-group">
<span class="input-group-text">¥</span>
<input type="number" class="form-control" id="amount" step="0.01" min="0" required>
</div>
</div>
<div class="mb-3">
<label for="category" class="form-label">分类</label>
<select class="form-select" id="category" required>
<option value="" disabled selected>请选择分类</option>
<!-- 收入分类 -->
<optgroup label="收入" id="incomeCategories">
<option value="工资">工资</option>
<option value="奖金">奖金</option>
<option value="投资">投资</option>
<option value="兼职">兼职</option>
<option value="其他收入">其他收入</option>
</optgroup>
<!-- 支出分类 -->
<optgroup label="支出" id="expenseCategories">
<option value="餐饮">餐饮</option>
<option value="购物">购物</option>
<option value="交通">交通</option>
<option value="住房">住房</option>
<option value="娱乐">娱乐</option>
<option value="医疗">医疗</option>
<option value="教育">教育</option>
<option value="其他支出">其他支出</option>
</optgroup>
</select>
</div>
<div class="mb-3">
<label for="date" class="form-label">日期</label>
<input type="date" class="form-control" id="date" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">备注</label>
<textarea class="form-control" id="description" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveTransactionBtn">保存</button>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-md-6">
<p>© 2025年8月 简单记账本</p>
</div>
<div class="col-md-6 text-md-end">
<p>科学理财,轻松生活</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 初始化日期为今天
document.getElementById('date').valueAsDate = new Date();
// 交易数据存储
let transactions = JSON.parse(localStorage.getItem('transactions')) || [];
// 更新UI
updateUI();
// 保存交易记录
document.getElementById('saveTransactionBtn').addEventListener('click', function() {
const form = document.getElementById('transactionForm');
const type = document.querySelector('input[name="transactionType"]:checked').value;
const amount = parseFloat(document.getElementById('amount').value);
const category = document.getElementById('category').value;
const date = document.getElementById('date').value;
const description = document.getElementById('description').value;
if (!amount || !category || !date) {
alert('请填写必填字段');
return;
}
const transaction = {
id: Date.now(),
type,
amount,
category,
date,
description,
timestamp: new Date().toISOString()
};
transactions.push(transaction);
localStorage.setItem('transactions', JSON.stringify(transactions));
// 重置表单
form.reset();
document.getElementById('date').valueAsDate = new Date();
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('addTransactionModal'));
modal.hide();
// 更新UI
updateUI();
});
// 类型切换时更新分类选项
document.querySelectorAll('input[name="transactionType"]').forEach(radio => {
radio.addEventListener('change', function() {
const categorySelect = document.getElementById('category');
categorySelect.value = '';
if (this.value === 'income') {
document.getElementById('incomeCategories').style.display = 'block';
document.getElementById('expenseCategories').style.display = 'none';
} else {
document.getElementById('incomeCategories').style.display = 'none';
document.getElementById('expenseCategories').style.display = 'block';
}
});
});
// 筛选功能
document.querySelectorAll('.filter-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const filter = this.getAttribute('data-filter');
filterTransactions(filter);
document.getElementById('filterDropdown').textContent = this.textContent;
});
});
// 日期筛选
document.querySelectorAll('.date-filter .btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.date-filter .btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const period = this.getAttribute('data-period');
filterByDate(period);
});
});
// 删除交易记录
document.addEventListener('click', function(e) {
if (e.target && e.target.classList.contains('delete-btn')) {
const id = parseInt(e.target.getAttribute('data-id'));
if (confirm('确定要删除这条记录吗?')) {
transactions = transactions.filter(t => t.id !== id);
localStorage.setItem('transactions', JSON.stringify(transactions));
updateUI();
}
}
});
// 更新UI函数
function updateUI() {
updateTransactionList(transactions);
updateSummary(transactions);
updateCharts(transactions);
toggleEmptyState();
}
// 更新交易列表
function updateTransactionList(data) {
const transactionList = document.getElementById('transactionList');
transactionList.innerHTML = '';
// 按日期降序排序
data.sort((a, b) => new Date(b.date) - new Date(a.date));
data.forEach(transaction => {
const item = document.createElement('div');
item.className = `transaction-item ${transaction.type}`;
const formattedDate = formatDate(transaction.date);
const formattedAmount = formatCurrency(transaction.amount);
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">${transaction.category}</div>
<div class="text-muted small">${formattedDate} · ${transaction.description || '无备注'}</div>
</div>
<div class="d-flex align-items-center">
<span class="amount me-3">${transaction.type === 'income' ? '+' : '-'}${formattedAmount}</span>
<button class="btn btn-sm btn-outline-danger delete-btn" data-id="${transaction.id}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
transactionList.appendChild(item);
});
}
// 更新汇总信息
function updateSummary(data) {
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
// 筛选本月数据
const monthData = data.filter(t => {
const date = new Date(t.date);
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
});
const income = monthData
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0);
const expense = monthData
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0);
const balance = income - expense;
document.getElementById('totalIncome').textContent = formatCurrency(income);
document.getElementById('totalExpense').textContent = formatCurrency(expense);
document.getElementById('totalBalance').textContent = formatCurrency(balance);
// 设置余额颜色
const balanceElement = document.getElementById('totalBalance');
if (balance > 0) {
balanceElement.className = 'summary-value balance-value';
} else if (balance < 0) {
balanceElement.className = 'summary-value expense-value';
} else {
balanceElement.className = 'summary-value';
}
}
// 更新图表
function updateCharts(data) {
updateCategoryChart(data, 'expense', 'expenseChart');
updateCategoryChart(data, 'income', 'incomeChart');
updateTrendChart(data);
}
// 更新分类饼图
function updateCategoryChart(data, type, chartId) {
const ctx = document.getElementById(chartId).getContext('2d');
// 筛选指定类型的交易
const filteredData = data.filter(t => t.type === type);
// 按分类汇总
const categoryData = {};
filteredData.forEach(t => {
if (categoryData[t.category]) {
categoryData[t.category] += t.amount;
} else {
categoryData[t.category] = t.amount;
}
});
// 准备图表数据
const labels = Object.keys(categoryData);
const values = Object.values(categoryData);
// 颜色配置
const colors = [
'#4CAF50', '#2196F3', '#FF9800', '#E91E63',
'#9C27B0', '#00BCD4', '#FFEB3B', '#795548'
];
// 销毁旧图表
if (window[chartId]) {
window[chartId].destroy();
}
// 创建新图表
window[chartId] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: colors.slice(0, labels.length),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12,
font: {
size: 11
}
}
}
}
}
});
// 如果没有数据,显示无数据提示
if (labels.length === 0) {
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('暂无数据', ctx.canvas.width / 2, ctx.canvas.height / 2);
}
}
// 更新趋势图
function updateTrendChart(data) {
const ctx = document.getElementById('trendChart').getContext('2d');
// 获取最近6个月的数据
const months = [];
const incomeData = [];
const expenseData = [];
const now = new Date();
for (let i = 5; i >= 0; i--) {
const month = new Date(now.getFullYear(), now.getMonth() - i, 1);
const monthName = month.toLocaleDateString('zh-CN', { month: 'short' });
months.push(monthName);
const monthYear = month.getFullYear();
const monthMonth = month.getMonth();
// 筛选当月数据
const monthlyData = data.filter(t => {
const date = new Date(t.date);
return date.getMonth() === monthMonth && date.getFullYear() === monthYear;
});
// 计算当月收入和支出
const monthlyIncome = monthlyData
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0);
const monthlyExpense = monthlyData
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0);
incomeData.push(monthlyIncome);
expenseData.push(monthlyExpense);
}
// 销毁旧图表
if (window.trendChart) {
window.trendChart.destroy();
}
// 创建新图表
window.trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: months,
datasets: [
{
label: '收入',
data: incomeData,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: '支出',
data: expenseData,
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '¥' + value;
}
}
}
}
}
});
}
// 筛选交易记录
function filterTransactions(filter) {
let filteredData;
if (filter === 'all') {
filteredData = transactions;
} else {
filteredData = transactions.filter(t => t.type === filter);
}
updateTransactionList(filteredData);
toggleEmptyState(filteredData);
}
// 按日期筛选
function filterByDate(period) {
const now = new Date();
let filteredData;
switch (period) {
case 'day':
const today = now.toISOString().split('T')[0];
filteredData = transactions.filter(t => t.date === today);
break;
case 'week':
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay());
filteredData = transactions.filter(t => {
const date = new Date(t.date);
return date >= weekStart;
});
break;
case 'month':
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
filteredData = transactions.filter(t => {
const date = new Date(t.date);
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
});
break;
default:
filteredData = transactions;
}
updateTransactionList(filteredData);
toggleEmptyState(filteredData);
}
// 切换空状态显示
function toggleEmptyState(data = transactions) {
const emptyState = document.getElementById('emptyState');
if (data.length === 0) {
emptyState.style.display = 'block';
} else {
emptyState.style.display = 'none';
}
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
}
// 格式化货币
function formatCurrency(amount) {
return amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
// 初始化分类显示
if (document.querySelector('input[name="transactionType"]:checked').value === 'income') {
document.getElementById('incomeCategories').style.display = 'block';
document.getElementById('expenseCategories').style.display = 'none';
} else {
document.getElementById('incomeCategories').style.display = 'none';
document.getElementById('expenseCategories').style.display = 'block';
}
// 添加示例数据(仅在首次加载且没有数据时)
if (transactions.length === 0) {
addSampleData();
}
// 添加示例数据
function addSampleData() {
const now = new Date();
const today = now.toISOString().split('T')[0];
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const lastWeek = new Date(now);
lastWeek.setDate(now.getDate() - 7);
const lastWeekStr = lastWeek.toISOString().split('T')[0];
const sampleData = [
{
id: Date.now() - 1000,
type: 'income',
amount: 8000,
category: '工资',
date: lastWeekStr,
description: '本月工资',
timestamp: new Date().toISOString()
},
{
id: Date.now() - 2000,
type: 'expense',
amount: 1200,
category: '住房',
date: yesterdayStr,
description: '房租',
timestamp: new Date().toISOString()
},
{
id: Date.now() - 3000,
type: 'expense',
amount: 150,
category: '餐饮',
date: today,
description: '午餐',
timestamp: new Date().toISOString()
},
{
id: Date.now() - 4000,
type: 'expense',
amount: 200,
category: '交通',
date: today,
description: '打车',
timestamp: new Date().toISOString()
},
{
id: Date.now() - 5000,
type: 'income',
amount: 500,
category: '兼职',
date: yesterday,
description: '周末兼职',
timestamp: new Date().toISOString()
}
];
transactions = sampleData;
localStorage.setItem('transactions', JSON.stringify(transactions));
}
});
</script>
</body>
</html>
