跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonPay大前端

基于 Flask 的 Python 个人记账本 Web 应用实现

介绍使用 Python Flask 框架开发个人记账本 Web 应用的完整过程。项目包含支出记录增删改查、分类管理、数据可视化(Matplotlib 生成饼图和柱状图)、Excel 导出及响应式界面设计(Bootstrap)。后端采用 JSON 存储,支持通过 PyInstaller 打包为单文件 EXE。适合 Python Web 初学者学习前后端交互与数据持久化。

芝士奶盖发布于 2026/3/26更新于 2026/5/2529 浏览
基于 Flask 的 Python 个人记账本 Web 应用实现

前言

在现代生活中,合理的财务管理和支出记录对每个人都非常重要。本文介绍一个基于 Python Flask 框架开发的个人记账本 Web 应用,支持支出记录、数据统计、可视化图表和数据导出等功能。

项目特色

  • 纯 Python 实现,使用 Flask 轻量级 Web 框架
  • 完整的记账功能,支持分类管理
  • 数据可视化,自动生成饼图和柱状图
  • 响应式设计,支持移动端访问
  • 数据导出,支持 Excel 格式导出
  • 美观界面,采用 Bootstrap 现代化设计

技术栈

  • 后端: Python + Flask
  • 前端: HTML5 + CSS3 + JavaScript + Bootstrap
  • 数据可视化: Matplotlib
  • 数据处理: Pandas
  • 数据存储: JSON 文件

项目结构

personal-account-book/
├── app.py              # 主程序文件
├── account_data.json   # 数据存储文件
└── build_single.txt    # 打包脚本(后缀改为 bat)

核心代码实现

1. 数据模型设计

class Expense:
    """支出记录类"""
    def __init__(self, amount, category, date, note=''):
        self.amount = float(amount)
        self.category = category
        self.date = date
        self.note = note

    def to_dict(self):
        return {
            'amount': self.amount,
            'category': self.category,
            'date': self.date.strftime('%Y-%m-%d'),
            'note': self.note
        }

2. 账本管理类

class AccountBook:
    """账本管理类"""
    def __init__(self):
        self.expenses = []
        self.filename = "account_data.json"
        self.categories = ["餐饮", "交通", "购物", "娱乐", "学习", "医疗", "住房", "其他"]
        self.load_data()

    def add_expense(self, amount, category, date, note=''):
        """添加支出"""
        try:
            expense = Expense(amount, category, date, note)
            self.expenses.append(expense)
            success = self.save_data()
            return success
        except Exception as e:
            print(f"添加失败:{e}")
            return False

3. Flask API 路由

@app.route('/api/expenses', methods=['POST'])
def add_expense():
    """添加支出记录"""
    data = request.json
    amount = data.get('amount')
    category = data.get('category')
    date_str = data.get('date')
    note = data.get('note', '')
    if not all([amount, category, date_str]):
        return jsonify({'success': False, 'message': '请填写完整信息'})
    try:
        amount_val = float(amount)
        if amount_val <= 0:
            return jsonify({'success': False, 'message': '金额必须大于 0'})
        date = datetime.strptime(date_str, '%Y-%m-%d')
    except (ValueError, TypeError) as e:
        return jsonify({'success': False, 'message': f'数据格式错误:{e}'})
    if account_book.add_expense(amount_val, category, date, note):
        return jsonify({'success': True, 'message': '添加成功'})
    return jsonify({'success': False, 'message': '添加失败'})

4. 数据可视化

@app.route('/api/chart')
def get_chart():
    """生成图表"""
    stats = account_book.get_monthly_statistics()
    if not stats['category_totals']:
        return jsonify({'success': False, 'message': '没有数据'})
    try:
        plt.figure(figsize=(10, 8))
        categories = list(stats['category_totals'].keys())
        amounts = list(stats['category_totals'].values())
        # 饼图
        plt.subplot(1, 2, 1)
        plt.pie(amounts, labels=categories, autopct='%1.1f%%', startangle=90)
        plt.title('本月支出分类比例')
        # 柱状图
        plt.subplot(1, 2, 2)
        colors = ['#e74c3c', '#3498db', '#9b59b6', '#f39c12', '#2ecc71', '#e67e22', '#34495e', '#95a5a6']
        plt.bar(categories, amounts, color=colors[:len(categories)])
        plt.title('各分类支出金额')
        plt.xticks(rotation=45)
        plt.tight_layout()
        img = io.BytesIO()
        plt.savefig(img, format='png', bbox_inches='tight', dpi=100)
        img.seek(0)
        chart_url = base64.b64encode(img.getvalue()).decode()
        plt.close()
        return jsonify({'success': True, 'chart': chart_url})
    except Exception as e:
        return jsonify({'success': False, 'message': f'生成图表失败:{e}'})

前端界面设计

响应式布局

使用 Bootstrap 5 实现响应式设计,确保在手机、平板、电脑上都有良好的显示效果。

动态交互

通过 JavaScript 实现前后端交互,无需刷新页面即可完成数据操作:

// 添加支出记录
async function addExpense() {
    const amount = document.getElementById('amount').value;
    const category = document.getElementById('category').value;
    const date = document.getElementById('date').value;
    const note = document.getElementById('note').value;
    if (!amount || !category || !date) {
        showAlert('请填写完整信息', 'warning');
        return;
    }
    try {
        const response = await fetch('/api/expenses', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({ amount, category, date, note })
        });
        const result = await response.json();
        if (result.success) {
            showAlert(result.message, 'success');
            document.getElementById('amount').value = '';
            document.getElementById('note').value = '';
            loadData();
        }
    } catch (error) {
        showAlert('添加失败:' + error, 'danger');
    }
}

部署运行

环境要求

  • Python 3.6+
  • 所需依赖包:Flask, Pandas, Matplotlib

安装步骤

  1. 安装依赖:
pip install flask pandas matplotlib
  1. 运行应用:
python app.py
  1. 访问应用: 在浏览器中打开 http://127.0.0.1:5000

功能演示

主要功能

  1. 支出记录:添加、查看、删除支出记录
  2. 分类管理:8 个预设支出分类
  3. 数据统计:实时统计本月总支出、记录数量
  4. 可视化图表:饼图和柱状图展示支出分布
  5. 数据导出:一键导出 Excel 文件

界面展示

  • 统计卡片:显示本月总支出、记录数、最大支出分类
  • 输入表单:简洁的支出记录表单
  • 数据表格:清晰的支出记录列表
  • 图表展示:直观的数据可视化

技术亮点

  1. 中文字体兼容:自动检测并配置中文字体
  2. 错误处理:完善的异常处理机制
  3. 数据持久化:使用 JSON 文件存储,简单可靠
  4. RESTful API:规范的 API 设计
  5. 响应式通知:美观的弹窗提示

扩展建议

这个项目还有很大的扩展空间:

  1. 用户系统:支持多用户登录
  2. 收入记录:增加收入管理功能
  3. 预算设置:设置月度预算和预警
  4. 数据备份:支持云存储备份
  5. 移动端 APP:开发移动端应用

总结

通过这个项目,我们不仅实现了一个实用的个人记账工具,还展示了如何使用 Flask 框架快速开发 Web 应用。项目涵盖了前后端开发、数据可视化、文件操作等多个重要知识点,非常适合 Python Web 开发的初学者学习和参考。

完整代码

import os
import json
import base64
import io
import sys
from datetime import datetime, timedelta
from flask import Flask, render_template_string, request, jsonify, send_file
import pandas as pd
import matplotlib
# 设置系统编码
if sys.stdout.encoding != 'UTF-8':
    try:
        sys.stdout.reconfigure(encoding='utf-8')
    except AttributeError:
        pass
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib import font_manager

# 设置中文字体支持
def setup_chinese_font():
    """设置中文字体支持"""
    try:
        # 尝试使用系统中常见的中文字体
        font_names = [
            'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'STKaiti', 'STSong',
            'LiHei Pro', 'WenQuanYi Micro Hei', 'DejaVu Sans'
        ]
        for font_name in font_names:
            if font_manager.findfont(font_name):
                plt.rcParams['font.family'] = font_name
                plt.rcParams['axes.unicode_minus'] = False
                print(f"使用字体:{font_name}")
                return True
        plt.rcParams['font.family'] = ['DejaVu Sans', 'Arial']
        plt.rcParams['axes.unicode_minus'] = False
        print("使用默认字体")
        return True
    except Exception as e:
        print(f"字体设置失败:{e}")
        return False

setup_chinese_font()

HTML_TEMPLATE = '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>个人记账本 - Web 版</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <style>
        body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; font-family: 'Microsoft YaHei', sans-serif; }
        .header-bg { background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: 0 0 20px 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
        .stat-card { border: none; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
        .stat-card:hover { transform: translateY(-5px); }
        .card { border: none; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px; }
        .btn { border-radius: 10px; padding: 10px 20px; font-weight: 500; transition: all 0.3s ease; }
        .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; }
        .table th { border: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 500; }
        .table td { border: none; padding: 15px 10px; }
        .table tbody tr:hover { background-color: rgba(102, 126, 234, 0.1); transform: translateX(5px); }
        .category-badge { padding: 5px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; color: white; }
        .amount-cell { font-weight: 600; color: #e74c3c; }
        .delete-btn { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border: none; border-radius: 8px; padding: 5px 12px; }
        .chinese-font { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }
    </style>
</head>
<body>
    <div class="container-fluid">
        <div class="header-bg p-4 mb-4 text-white">
            <h1><i class="fas fa-wallet"></i> 个人记账本</h1>
            <p id="current-date"></p>
        </div>
        <!-- 统计卡片 -->
        <div class="row mb-4">
            <div class="col-md-4">
                <div class="card stat-card p-3">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h4 id="total-amount">¥0.00</h4>
                            <p class="mb-0">本月总支出</p>
                        </div>
                        <i class="fas fa-coins fa-2x text-warning"></i>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card stat-card p-3">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h4 id="total-count">0</h4>
                            <p class="mb-0">本月记录数</p>
                        </div>
                        <i class="fas fa-list fa-2x text-info"></i>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card stat-card p-3">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h4 id="top-category">-</h4>
                            <p class="mb-0">最大支出分类</p>
                        </div>
                        <i class="fas fa-chart-pie fa-2x text-success"></i>
                    </div>
                </div>
            </div>
        </div>
        <!-- 输入表单 -->
        <div class="card p-4 mb-4">
            <h5><i class="fas fa-plus-circle"></i> 记录新支出</h5>
            <form onsubmit="event.preventDefault(); addExpense();">
                <div class="row g-3">
                    <div class="col-md-3">
                        <label>金额</label>
                        <input type="number" id="amount" placeholder="0.00" step="0.01" class="form-control">
                    </div>
                    <div class="col-md-3">
                        <label>分类</label>
                        <select id="category" class="form-select">
                            <option value="餐饮">餐饮</option>
                            <option value="交通">交通</option>
                            <option value="购物">购物</option>
                            <option value="娱乐">娱乐</option>
                            <option value="学习">学习</option>
                            <option value="医疗">医疗</option>
                            <option value="住房">住房</option>
                            <option value="其他">其他</option>
                        </select>
                    </div>
                    <div class="col-md-3">
                        <label>日期</label>
                        <input type="date" id="date" class="form-control">
                    </div>
                    <div class="col-md-3">
                        <label>备注</label>
                        <input type="text" id="note" placeholder="可选备注" class="form-control">
                    </div>
                </div>
                <button type="submit" class="btn btn-primary mt-3"><i class="fas fa-save"></i> 添加记录</button>
            </form>
        </div>
        <!-- 功能按钮 -->
        <div class="card p-3 mb-4">
            <button onclick="refreshData()" class="btn btn-outline-secondary me-2"><i class="fas fa-sync"></i> 刷新数据</button>
            <button onclick="showChart()" class="btn btn-outline-secondary me-2"><i class="fas fa-chart-bar"></i> 查看图表</button>
            <button onclick="exportData()" class="btn btn-outline-secondary"><i class="fas fa-file-excel"></i> 导出 Excel</button>
        </div>
        <!-- 支出列表 -->
        <div class="card p-4">
            <h5><i class="fas fa-table"></i> 支出记录</h5>
            <div class="table-responsive">
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>日期</th>
                            <th>分类</th>
                            <th>金额</th>
                            <th>备注</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody id="expenses-table">
                        <!-- 数据通过 JavaScript 动态加载 -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <!-- 图表模态框 -->
    <div class="modal fade" id="chartModal" tabindex="-1">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">支出统计图表</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body text-center">
                    <img id="chart-image" style="max-width: 100%;">
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        document.getElementById('date').valueAsDate = new Date();
        document.getElementById('current-date').textContent = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });
        function getCategoryColor(category) {
            const colors = { '餐饮': '#e74c3c', '交通': '#3498db', '购物': '#9b59b6', '娱乐': '#f39c12', '学习': '#2ecc71', '医疗': '#e67e22', '住房': '#34495e', '其他': '#95a5a6' };
            return colors[category] || '#95a5a6';
        }
        async function loadData() {
            try {
                const response = await fetch('/api/expenses');
                const expenses = await response.json();
                renderExpenses(expenses);
                updateStatistics();
            } catch (error) { showAlert('加载数据失败:' + error, 'danger'); }
        }
        function renderExpenses(expenses) {
            const tbody = document.getElementById('expenses-table');
            tbody.innerHTML = '';
            if (expenses.length === 0) {
                tbody.innerHTML = '<tr><td colspan="5" class="text-center"><i class="fas fa-inbox"></i><br>还没有支出记录</td></tr>';
                return;
            }
            expenses.forEach((expense, index) => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td>${expense.date}</td>
                    <td><span class="badge" style="background-color: ${getCategoryColor(expense.category)}">${expense.category}</span></td>
                    <td class="amount-cell">¥${parseFloat(expense.amount).toFixed(2)}</td>
                    <td>${expense.note || '-'}</td>
                    <td><button onclick="deleteExpense(${index})" class="btn btn-sm delete-btn"><i class="fas fa-trash"></i> 删除</button></td>
                `;
                tbody.appendChild(row);
            });
        }
        async function addExpense() {
            const amount = document.getElementById('amount').value;
            const category = document.getElementById('category').value;
            const date = document.getElementById('date').value;
            const note = document.getElementById('note').value;
            if (!amount || !category || !date) { showAlert('请填写完整信息', 'warning'); return; }
            try {
                const response = await fetch('/api/expenses', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ amount, category, date, note })
                });
                const result = await response.json();
                if (result.success) {
                    showAlert(result.message, 'success');
                    document.getElementById('amount').value = '';
                    document.getElementById('note').value = '';
                    loadData();
                } else { showAlert(result.message, 'danger'); }
            } catch (error) { showAlert('添加失败:' + error, 'danger'); }
        }
        async function deleteExpense(index) {
            if (!confirm('确定要删除这条记录吗?')) return;
            try {
                const response = await fetch(`/api/expenses/${index}`, {method: 'DELETE'});
                const result = await response.json();
                if (result.success) { showAlert(result.message, 'success'); loadData(); }
                else { showAlert(result.message, 'danger'); }
            } catch (error) { showAlert('删除失败:' + error, 'danger'); }
        }
        async function updateStatistics() {
            try {
                const response = await fetch('/api/statistics');
                const stats = await response.json();
                document.getElementById('total-amount').textContent = '¥' + stats.total.toFixed(2);
                document.getElementById('total-count').textContent = stats.count;
                if (stats.category_totals && Object.keys(stats.category_totals).length > 0) {
                    const topCategory = Object.entries(stats.category_totals).reduce((a, b) => a[1] > b[1] ? a : b);
                    document.getElementById('top-category').textContent = topCategory[0];
                }
            } catch (error) { console.error('更新统计失败:', error); }
        }
        async function showChart() {
            try {
                const response = await fetch('/api/chart');
                const result = await response.json();
                if (result.success) {
                    document.getElementById('chart-image').src = 'data:image/png;base64,' + result.chart;
                    new bootstrap.Modal(document.getElementById('chartModal')).show();
                } else { showAlert(result.message, 'warning'); }
            } catch (error) { showAlert('生成图表失败:' + error, 'danger'); }
        }
        async function exportData() {
            try {
                const response = await fetch('/api/export');
                if (response.ok) {
                    const blob = await response.blob();
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `记账数据_${new Date().toISOString().slice(0,19).replace(/:/g,'')}.xlsx`;
                    a.click();
                    window.URL.revokeObjectURL(url);
                    showAlert('导出成功', 'success');
                } else { showAlert('导出失败', 'danger'); }
            } catch (error) { showAlert('导出失败:' + error, 'danger'); }
        }
        function refreshData() { loadData(); showAlert('数据已刷新', 'info'); }
        function showAlert(message, type) {
            const alertDiv = document.createElement('div');
            alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
            alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
            alertDiv.innerHTML = `${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
            document.body.appendChild(alertDiv);
            setTimeout(() => { if (alertDiv.parentNode) alertDiv.parentNode.removeChild(alertDiv); }, 3000);
        }
        document.addEventListener('DOMContentLoaded', loadData);
    </script>
</body>
</html>'''

class Expense:
    def __init__(self, amount, category, date, note=''):
        self.amount = float(amount)
        self.category = category
        self.date = date
        self.note = note

    @classmethod
    def from_dict(cls, data):
        return cls(
            data['amount'],
            data['category'],
            datetime.strptime(data['date'], '%Y-%m-%d'),
            data.get('note', '')
        )

    def to_dict(self):
        return {
            'amount': self.amount,
            'category': self.category,
            'date': self.date.strftime('%Y-%m-%d'),
            'note': self.note
        }

class AccountBook:
    def __init__(self):
        self.expenses = []
        self.filename = "account_data.json"
        self.categories = ["餐饮", "交通", "购物", "娱乐", "学习", "医疗", "住房", "其他"]
        self.load_data()

    def load_data(self):
        if os.path.exists(self.filename):
            try:
                with open(self.filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self.expenses = [Expense.from_dict(item) for item in data]
                    print(f"成功加载 {len(self.expenses)} 条记录")
            except Exception as e:
                print(f"加载数据失败:{e}")

    def save_data(self):
        try:
            data = [expense.to_dict() for expense in self.expenses]
            with open(self.filename, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            print(f"成功保存 {len(data)} 条记录")
            return True
        except Exception as e:
            print(f"保存数据失败:{e}")
            return False

    def add_expense(self, amount, category, date, note=''):
        try:
            expense = Expense(amount, category, date, note)
            self.expenses.append(expense)
            success = self.save_data()
            return success
        except Exception as e:
            print(f"添加失败:{e}")
            return False

    def delete_expense(self, index):
        if 0 <= index < len(self.expenses):
            self.expenses.pop(index)
            success = self.save_data()
            return success
        return False

    def get_monthly_statistics(self, year=None, month=None):
        if not year or not month:
            now = datetime.now()
            year, month = now.year, now.month
        monthly_expenses = [
            exp for exp in self.expenses
            if exp.date.year == year and exp.date.month == month
        ]
        category_totals = {}
        for exp in monthly_expenses:
            category_totals[exp.category] = category_totals.get(exp.category, 0) + exp.amount
        return {
            'total': sum(exp.amount for exp in monthly_expenses),
            'category_totals': category_totals,
            'count': len(monthly_expenses)
        }

app = Flask(__name__)
account_book = AccountBook()

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/api/expenses', methods=['GET'])
def get_expenses():
    sorted_expenses = sorted(account_book.expenses, key=lambda x: x.date, reverse=True)
    return jsonify([expense.to_dict() for expense in sorted_expenses])

@app.route('/api/expenses', methods=['POST'])
def add_expense_route():
    data = request.json
    amount = data.get('amount')
    category = data.get('category')
    date_str = data.get('date')
    note = data.get('note', '')
    if not all([amount, category, date_str]):
        return jsonify({'success': False, 'message': '请填写完整信息'})
    try:
        amount_val = float(amount)
        if amount_val <= 0:
            return jsonify({'success': False, 'message': '金额必须大于 0'})
        date = datetime.strptime(date_str, '%Y-%m-%d')
    except (ValueError, TypeError) as e:
        return jsonify({'success': False, 'message': f'数据格式错误:{e}'})
    if account_book.add_expense(amount_val, category, date, note):
        return jsonify({'success': True, 'message': '添加成功'})
    return jsonify({'success': False, 'message': '添加失败'})

@app.route('/api/expenses/<int:index>', methods=['DELETE'])
def delete_expense_route(index):
    if account_book.delete_expense(index):
        return jsonify({'success': True, 'message': '删除成功'})
    return jsonify({'success': False, 'message': '删除失败'})

@app.route('/api/statistics')
def get_statistics():
    return jsonify(account_book.get_monthly_statistics())

@app.route('/api/chart')
def get_chart():
    stats = account_book.get_monthly_statistics()
    if not stats['category_totals']:
        return jsonify({'success': False, 'message': '没有数据'})
    try:
        plt.figure(figsize=(10, 8))
        categories = list(stats['category_totals'].keys())
        amounts = list(stats['category_totals'].values())
        plt.subplot(1, 2, 1)
        plt.pie(amounts, labels=categories, autopct='%1.1f%%', startangle=90)
        plt.title('本月支出分类比例')
        plt.subplot(1, 2, 2)
        colors = ['#e74c3c', '#3498db', '#9b59b6', '#f39c12', '#2ecc71', '#e67e22', '#34495e', '#95a5a6']
        plt.bar(categories, amounts, color=colors[:len(categories)])
        plt.title('各分类支出金额')
        plt.xticks(rotation=45)
        plt.tight_layout()
        img = io.BytesIO()
        plt.savefig(img, format='png', bbox_inches='tight', dpi=100)
        img.seek(0)
        chart_url = base64.b64encode(img.getvalue()).decode()
        plt.close()
        return jsonify({'success': True, 'chart': chart_url})
    except Exception as e:
        print(f"生成图表失败:{e}")
        return jsonify({'success': False, 'message': f'生成图表失败:{e}'})

@app.route('/api/export')
def export_data():
    if not account_book.expenses:
        return jsonify({'success': False, 'message': '没有数据可以导出'})
    try:
        data = [expense.to_dict() for expense in account_book.expenses]
        df = pd.DataFrame(data)
        filename = f"记账数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        df.to_excel(filename, index=False, encoding='utf-8')
        return send_file(filename, as_attachment=True)
    except Exception as e:
        return jsonify({'success': False, 'message': f'导出失败:{e}'})

def main():
    print("=" * 50)
    print("个人记账本 Web 版")
    print("=" * 50)
    print("正在启动服务器...")
    print("请在浏览器中访问:http://127.0.0.1:5000")
    print("按 Ctrl+C 停止服务器")
    print("=" * 50)
    try:
        import webbrowser
        webbrowser.open('http://127.0.0.1:5000')
    except Exception as e:
        print(f"自动打开浏览器失败:{e}")
    app.run(debug=False, host='127.0.0.1', port=5000)

if __name__ == '__main__':
    main()

打包说明

@echo off
chcp 65001
echo 正在打包单文件 Web 记账本...
pyinstaller --onefile --noconsole --name="Web 记账本" ^
 --add-data "account_data.json;." ^
 --hidden-import=matplotlib.backends.backend_agg ^
 --hidden-import=flask ^
 --hidden-import=pandas ^
 web_account_app.py
echo.
if %errorlevel% == 0 (
 echo 打包成功!
 echo 生成的 exe 文件在 dist 文件夹中
 echo 使用方法:双击 "Web 记账本.exe",自动打开浏览器
) else (
 echo 打包失败!
)
pause

注意修改文件后缀为 .bat,双击运行即可。

目录

  1. 前言
  2. 项目特色
  3. 技术栈
  4. 项目结构
  5. 核心代码实现
  6. 1. 数据模型设计
  7. 2. 账本管理类
  8. 3. Flask API 路由
  9. 4. 数据可视化
  10. 前端界面设计
  11. 响应式布局
  12. 动态交互
  13. 部署运行
  14. 环境要求
  15. 安装步骤
  16. 功能演示
  17. 主要功能
  18. 界面展示
  19. 技术亮点
  20. 扩展建议
  21. 总结
  22. 完整代码
  23. 设置系统编码
  24. 设置中文字体支持
  25. 打包说明
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Java 连接电科金仓数据库(KingbaseES)实战指南
  • Ubuntu 安装 Codex CLI 及 IDE 插件报 403 Forbidden 错误排查指南
  • ms-Mamba: 多尺度 Mamba 时间序列预测论文解读
  • C++高性能服务器开发:CPU核心绑定与性能优化
  • Vue3+Python 气象数据共享平台设计与实现
  • 多模态大模型 API 调用与本地部署成本深度对比
  • Python 中文分词利器:jieba 库快速入门与实战指南
  • 多模态 Agent 图像识别 Skills 开发实战:JavaScript+Python 全栈方案
  • FPGA Aurora 64B/66B 高速串行通信开发指南
  • OpenClaw 智能体实战:从零搭建 AI 员工(原理、算法与代码)
  • Stable Diffusion WebUI 部署与核心功能实战解析
  • Mac 系统下 Anaconda 与 Python 环境安装完整指南
  • Git-AI:追踪 AI 生成代码的 Git 扩展工具
  • 华为交换机首次开局配置:Console 连接与 Web 管理设置
  • LeetCode 两两交换链表中的节点:Java 递归与迭代解法
  • 云开发 Copilot:AI 赋能的低代码开发实践
  • Ubuntu 22.04 安装英伟达显卡驱动及 CUDA 环境
  • 2025 年 3 月 CCF-GESP C++ 三级真题解析
  • ClawdBot 实战:语音会议录音转写与重点内容摘要翻译
  • SQL 防火墙体系化实践:构建数据库内生安全防线

相关免费在线工具

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online