大体实现的功能参照以下链接:

个人财务管理系统设计与实现_个人账务管理系统python课设mysql-CSDN博客

使用AI写代码,迅速实现了全部功能。

"""
个人财务管理系统
基于 Tkinter + SQLite + Matplotlib 实现
功能:注册登录、个人资料、账户管理、收支管理、借入借出、统计图表
"""

import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import datetime
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import sys

# ==================== 数据库管理 ====================
class DatabaseManager:
    """SQLite数据库操作封装"""
    
    def __init__(self, db_path="finance.db"):
        self.db_path = db_path
        self._init_db()
    
    def _init_db(self):
        """初始化数据库表结构"""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        # 用户表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password TEXT NOT NULL,
                name TEXT,
                school TEXT,
                signature TEXT,
                avatar TEXT
            )
        ''')
        
        # 账户表(银行卡)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                card_number TEXT,
                holder_name TEXT,
                bank_name TEXT,
                balance REAL DEFAULT 0,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        ''')
        
        # 收入表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS incomes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                amount REAL NOT NULL,
                type TEXT NOT NULL,
                time TEXT NOT NULL,
                description TEXT,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        ''')
        
        # 支出表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS expenses (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                amount REAL NOT NULL,
                type TEXT NOT NULL,
                time TEXT NOT NULL,
                description TEXT,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        ''')
        
        # 借入表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS borrow_in (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                amount REAL NOT NULL,
                lender TEXT NOT NULL,
                borrow_date TEXT NOT NULL,
                due_date TEXT,
                description TEXT,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        ''')
        
        # 借出表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS borrow_out (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                amount REAL NOT NULL,
                borrower TEXT NOT NULL,
                lend_date TEXT NOT NULL,
                due_date TEXT,
                description TEXT,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        ''')
        
        # 收支类型预设
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS income_types (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL
            )
        ''')
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS expense_types (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL
            )
        ''')
        
        # 插入默认类型
        default_income = ['兼职收入', '奖学金', '生活费', '红包', '投资收益', '其他']
        default_expense = ['餐饮', '购物', '交通', '娱乐', '学习', '医疗', '通讯', '其他']
        
        for t in default_income:
            cursor.execute('INSERT OR IGNORE INTO income_types (name) VALUES (?)', (t,))
        for t in default_expense:
            cursor.execute('INSERT OR IGNORE INTO expense_types (name) VALUES (?)', (t,))
        
        conn.commit()
        conn.close()
    
    def get_connection(self):
        return sqlite3.connect(self.db_path)
    
    # ---------- 用户操作 ----------
    def register_user(self, username, password):
        """注册新用户"""
        conn = self.get_connection()
        cursor = conn.cursor()
        try:
            cursor.execute(
                'INSERT INTO users (username, password, name) VALUES (?, ?, ?)',
                (username, password, username)
            )
            conn.commit()
            user_id = cursor.lastrowid
            conn.close()
            return True, user_id
        except sqlite3.IntegrityError:
            conn.close()
            return False, "用户名已存在"
    
    def login_user(self, username, password):
        """用户登录验证"""
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'SELECT id, username, name, school, signature FROM users WHERE username=? AND password=?',
            (username, password)
        )
        user = cursor.fetchone()
        conn.close()
        return user
    
    def get_user_info(self, user_id):
        """获取用户详细信息"""
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'SELECT id, username, name, school, signature FROM users WHERE id=?',
            (user_id,)
        )
        user = cursor.fetchone()
        conn.close()
        return user
    
    def update_user_info(self, user_id, name, school, signature):
        """更新用户资料"""
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'UPDATE users SET name=?, school=?, signature=? WHERE id=?',
            (name, school, signature, user_id)
        )
        conn.commit()
        conn.close()
    
    # ---------- 账户操作 ----------
    def add_account(self, user_id, card_number, holder_name, bank_name, balance):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO accounts (user_id, card_number, holder_name, bank_name, balance) VALUES (?, ?, ?, ?, ?)',
            (user_id, card_number, holder_name, bank_name, balance)
        )
        conn.commit()
        conn.close()
    
    def get_accounts(self, user_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'SELECT id, card_number, holder_name, bank_name, balance FROM accounts WHERE user_id=?',
            (user_id,)
        )
        data = cursor.fetchall()
        conn.close()
        return data
    
    def delete_account(self, account_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('DELETE FROM accounts WHERE id=?', (account_id,))
        conn.commit()
        conn.close()
    
    # ---------- 收入操作 ----------
    def add_income(self, user_id, amount, type_name, time_str, description):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO incomes (user_id, amount, type, time, description) VALUES (?, ?, ?, ?, ?)',
            (user_id, amount, type_name, time_str, description)
        )
        conn.commit()
        conn.close()
    
    def get_incomes(self, user_id, start_date=None, end_date=None):
        conn = self.get_connection()
        cursor = conn.cursor()
        if start_date and end_date:
            cursor.execute(
                'SELECT id, amount, type, time, description FROM incomes WHERE user_id=? AND time BETWEEN ? AND ? ORDER BY time DESC',
                (user_id, start_date, end_date)
            )
        else:
            cursor.execute(
                'SELECT id, amount, type, time, description FROM incomes WHERE user_id=? ORDER BY time DESC',
                (user_id,)
            )
        data = cursor.fetchall()
        conn.close()
        return data
    
    def delete_income(self, income_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('DELETE FROM incomes WHERE id=?', (income_id,))
        conn.commit()
        conn.close()
    
    # ---------- 支出操作 ----------
    def add_expense(self, user_id, amount, type_name, time_str, description):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO expenses (user_id, amount, type, time, description) VALUES (?, ?, ?, ?, ?)',
            (user_id, amount, type_name, time_str, description)
        )
        conn.commit()
        conn.close()
    
    def get_expenses(self, user_id, start_date=None, end_date=None):
        conn = self.get_connection()
        cursor = conn.cursor()
        if start_date and end_date:
            cursor.execute(
                'SELECT id, amount, type, time, description FROM expenses WHERE user_id=? AND time BETWEEN ? AND ? ORDER BY time DESC',
                (user_id, start_date, end_date)
            )
        else:
            cursor.execute(
                'SELECT id, amount, type, time, description FROM expenses WHERE user_id=? ORDER BY time DESC',
                (user_id,)
            )
        data = cursor.fetchall()
        conn.close()
        return data
    
    def delete_expense(self, expense_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('DELETE FROM expenses WHERE id=?', (expense_id,))
        conn.commit()
        conn.close()
    
    # ---------- 借入操作 ----------
    def add_borrow_in(self, user_id, amount, lender, borrow_date, due_date, description):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO borrow_in (user_id, amount, lender, borrow_date, due_date, description) VALUES (?, ?, ?, ?, ?, ?)',
            (user_id, amount, lender, borrow_date, due_date, description)
        )
        conn.commit()
        conn.close()
    
    def get_borrow_in(self, user_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'SELECT id, amount, lender, borrow_date, due_date, description FROM borrow_in WHERE user_id=? ORDER BY borrow_date DESC',
            (user_id,)
        )
        data = cursor.fetchall()
        conn.close()
        return data
    
    # ---------- 借出操作 ----------
    def add_borrow_out(self, user_id, amount, borrower, lend_date, due_date, description):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO borrow_out (user_id, amount, borrower, lend_date, due_date, description) VALUES (?, ?, ?, ?, ?, ?)',
            (user_id, amount, borrower, lend_date, due_date, description)
        )
        conn.commit()
        conn.close()
    
    def get_borrow_out(self, user_id):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute(
            'SELECT id, amount, borrower, lend_date, due_date, description FROM borrow_out WHERE user_id=? ORDER BY lend_date DESC',
            (user_id,)
        )
        data = cursor.fetchall()
        conn.close()
        return data
    
    # ---------- 统计查询 ----------
    def get_income_by_type(self, user_id, start_date=None, end_date=None):
        conn = self.get_connection()
        cursor = conn.cursor()
        if start_date and end_date:
            cursor.execute(
                'SELECT type, SUM(amount) FROM incomes WHERE user_id=? AND time BETWEEN ? AND ? GROUP BY type',
                (user_id, start_date, end_date)
            )
        else:
            cursor.execute(
                'SELECT type, SUM(amount) FROM incomes WHERE user_id=? GROUP BY type',
                (user_id,)
            )
        data = cursor.fetchall()
        conn.close()
        return data
    
    def get_expense_by_type(self, user_id, start_date=None, end_date=None):
        conn = self.get_connection()
        cursor = conn.cursor()
        if start_date and end_date:
            cursor.execute(
                'SELECT type, SUM(amount) FROM expenses WHERE user_id=? AND time BETWEEN ? AND ? GROUP BY type',
                (user_id, start_date, end_date)
            )
        else:
            cursor.execute(
                'SELECT type, SUM(amount) FROM expenses WHERE user_id=? GROUP BY type',
                (user_id,)
            )
        data = cursor.fetchall()
        conn.close()
        return data
    
    def get_monthly_summary(self, user_id, year):
        """获取月度收支汇总"""
        conn = self.get_connection()
        cursor = conn.cursor()
        monthly = {}
        for month in range(1, 13):
            month_str = f"{year:04d}-{month:02d}"
            # 收入
            cursor.execute(
                'SELECT SUM(amount) FROM incomes WHERE user_id=? AND time LIKE ?',
                (user_id, f"{month_str}%")
            )
            income = cursor.fetchone()[0] or 0
            # 支出
            cursor.execute(
                'SELECT SUM(amount) FROM expenses WHERE user_id=? AND time LIKE ?',
                (user_id, f"{month_str}%")
            )
            expense = cursor.fetchone()[0] or 0
            monthly[month] = (income, expense)
        conn.close()
        return monthly
    
    def get_income_types(self):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('SELECT name FROM income_types')
        data = [row[0] for row in cursor.fetchall()]
        conn.close()
        return data
    
    def get_expense_types(self):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('SELECT name FROM expense_types')
        data = [row[0] for row in cursor.fetchall()]
        conn.close()
        return data


# ==================== 主应用程序 ====================
class FinanceApp:
    """个人财务管理系统主界面"""
    
    def __init__(self, root):
        self.root = root
        self.root.title("个人财务管理系统")
        self.root.geometry("1100x700")
        self.root.minsize(1000, 650)
        
        # 设置背景色
        self.bg_color = "#f0f4f8"
        self.root.configure(bg=self.bg_color)
        
        # 统一字体(中文字体)
        self.font_family = "Microsoft YaHei"  # Windows
        # 若为 macOS 可改为 "PingFang SC",Linux 可改为 "WenQuanYi Micro Hei"
        self.default_font = (self.font_family, 10)
        self.title_font = (self.font_family, 14, "bold")
        self.head_font = (self.font_family, 18, "bold")
        
        self.db = DatabaseManager()
        self.current_user = None
        self.user_id = None
        
        # 显示登录界面
        self.show_login()
    
    # ==================== 界面辅助 ====================
    def clear_window(self):
        for widget in self.root.winfo_children():
            widget.destroy()
    
    def create_title(self, parent, text):
        """创建标题标签"""
        label = tk.Label(
            parent, text=text, font=self.head_font,
            bg=self.bg_color, fg="#2c3e50"
        )
        return label
    
    def create_label(self, parent, text, **kwargs):
        return tk.Label(
            parent, text=text, font=self.default_font,
            bg=self.bg_color, fg="#34495e", **kwargs
        )
    
    def create_entry(self, parent, **kwargs):
        return tk.Entry(
            parent, font=self.default_font,
            relief="solid", bd=1, **kwargs
        )
    
    def create_button(self, parent, text, command, **kwargs):
        return tk.Button(
            parent, text=text, font=self.default_font,
            command=command, relief="raised", bd=2,
            bg="#3498db", fg="white", activebackground="#2980b9",
            activeforeground="white", **kwargs
        )
    
    def create_treeview(self, parent, columns, headings, widths=None):
        """创建表格视图"""
        tree = ttk.Treeview(parent, columns=columns, show="headings", height=12)
        if widths is None:
            widths = [100] * len(columns)
        for col, head, w in zip(columns, headings, widths):
            tree.heading(col, text=head)
            tree.column(col, width=w, anchor="center")
        
        # 设置滚动条
        scrollbar = ttk.Scrollbar(parent, orient="vertical", command=tree.yview)
        tree.configure(yscrollcommand=scrollbar.set)
        
        return tree, scrollbar
    
    # ==================== 登录/注册界面 ====================
    def show_login(self):
        self.clear_window()
        self.current_user = None
        self.user_id = None
        
        # 主容器 - 居中
        main_frame = tk.Frame(self.root, bg=self.bg_color)
        main_frame.place(relx=0.5, rely=0.5, anchor="center")
        
        # 标题
        title = self.create_title(main_frame, "个人财务管理系统")
        title.pack(pady=(0, 30))
        
        # 登录框
        login_frame = tk.Frame(main_frame, bg="white", relief="solid", bd=1, padx=40, pady=30)
        login_frame.pack()
        
        tk.Label(login_frame, text="用户登录", font=self.title_font,
                 bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
        
        tk.Label(login_frame, text="用户名:", font=self.default_font,
                 bg="white").grid(row=1, column=0, sticky="e", padx=5, pady=8)
        self.entry_login_user = self.create_entry(login_frame, width=20)
        self.entry_login_user.grid(row=1, column=1, padx=5, pady=8)
        self.entry_login_user.focus()
        
        tk.Label(login_frame, text="密码:", font=self.default_font,
                 bg="white").grid(row=2, column=0, sticky="e", padx=5, pady=8)
        self.entry_login_pass = self.create_entry(login_frame, width=20, show="*")
        self.entry_login_pass.grid(row=2, column=1, padx=5, pady=8)
        
        btn_login = self.create_button(login_frame, "登 录", self.do_login, width=10)
        btn_login.grid(row=3, column=0, padx=10, pady=20)
        
        btn_register = self.create_button(login_frame, "注 册", self.show_register, width=10)
        btn_register.grid(row=3, column=1, padx=10, pady=20)
        
        # 绑定回车键
        self.root.bind('<Return>', lambda e: self.do_login())
        
        # 底部提示
        tk.Label(main_frame, text="个人财务管理系统 v1.0",
                 font=("Microsoft YaHei", 8), bg=self.bg_color, fg="#95a5a6").pack(pady=(20, 0))
    
    def show_register(self):
        """注册窗口"""
        reg_window = tk.Toplevel(self.root)
        reg_window.title("注册新用户")
        reg_window.geometry("400x350")
        reg_window.configure(bg="white")
        reg_window.resizable(False, False)
        reg_window.grab_set()
        
        main_frame = tk.Frame(reg_window, bg="white", padx=40, pady=30)
        main_frame.pack(fill="both", expand=True)
        
        tk.Label(main_frame, text="用户注册", font=self.title_font,
                 bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
        
        tk.Label(main_frame, text="用户名:", font=self.default_font,
                 bg="white").grid(row=1, column=0, sticky="e", padx=5, pady=8)
        entry_user = self.create_entry(main_frame, width=20)
        entry_user.grid(row=1, column=1, padx=5, pady=8)
        entry_user.focus()
        
        tk.Label(main_frame, text="密码:", font=self.default_font,
                 bg="white").grid(row=2, column=0, sticky="e", padx=5, pady=8)
        entry_pass = self.create_entry(main_frame, width=20, show="*")
        entry_pass.grid(row=2, column=1, padx=5, pady=8)
        
        tk.Label(main_frame, text="确认密码:", font=self.default_font,
                 bg="white").grid(row=3, column=0, sticky="e", padx=5, pady=8)
        entry_pass2 = self.create_entry(main_frame, width=20, show="*")
        entry_pass2.grid(row=3, column=1, padx=5, pady=8)
        
        def do_register():
            username = entry_user.get().strip()
            password = entry_pass.get()
            password2 = entry_pass2.get()
            
            if not username or not password:
                messagebox.showerror("错误", "用户名和密码不能为空")
                return
            if password != password2:
                messagebox.showerror("错误", "两次输入的密码不一致")
                return
            if len(password) < 4:
                messagebox.showerror("错误", "密码长度至少4位")
                return
            
            success, result = self.db.register_user(username, password)
            if success:
                messagebox.showinfo("成功", f"注册成功!用户ID: {result}")
                reg_window.destroy()
            else:
                messagebox.showerror("错误", result)
        
        btn_reg = self.create_button(main_frame, "注 册", do_register, width=10)
        btn_reg.grid(row=4, column=0, columnspan=2, pady=20)
        
        btn_cancel = tk.Button(main_frame, text="取消", command=reg_window.destroy,
                               font=self.default_font, bg="#95a5a6", fg="white")
        btn_cancel.grid(row=5, column=0, columnspan=2)
    
    def do_login(self):
        username = self.entry_login_user.get().strip()
        password = self.entry_login_pass.get()
        
        if not username or not password:
            messagebox.showerror("错误", "请输入用户名和密码")
            return
        
        user = self.db.login_user(username, password)
        if user:
            self.user_id = user[0]
            self.current_user = {
                'id': user[0], 'username': user[1],
                'name': user[2], 'school': user[3], 'signature': user[4]
            }
            self.root.unbind('<Return>')
            self.show_main()
        else:
            messagebox.showerror("错误", "用户名或密码错误")
    
    # ==================== 主界面 ====================
    def show_main(self):
        self.clear_window()
        
        # 主容器
        main_frame = tk.Frame(self.root, bg=self.bg_color)
        main_frame.pack(fill="both", expand=True)
        
        # 顶部标题栏
        header = tk.Frame(main_frame, bg="#2c3e50", height=60)
        header.pack(fill="x")
        header.pack_propagate(False)
        
        tk.Label(header, text="个人财务管理系统",
                 font=self.head_font, bg="#2c3e50", fg="white").pack(side="left", padx=20)
        
        tk.Label(header, text=f"欢迎,{self.current_user['name']}",
                 font=("Microsoft YaHei", 11), bg="#2c3e50", fg="#ecf0f1").pack(side="right", padx=20)
        
        btn_logout = tk.Button(header, text="退出登录", command=self.logout,
                               font=self.default_font, bg="#e74c3c", fg="white",
                               activebackground="#c0392b", activeforeground="white")
        btn_logout.pack(side="right", padx=10)
        
        # 主体:左侧菜单 + 右侧内容
        body = tk.Frame(main_frame, bg=self.bg_color)
        body.pack(fill="both", expand=True, padx=10, pady=10)
        
        # 左侧菜单
        menu_frame = tk.Frame(body, bg="white", relief="solid", bd=1, width=180)
        menu_frame.pack(side="left", fill="y", padx=(0, 10))
        menu_frame.pack_propagate(False)
        
        menu_items = [
            ("个人资料", self.show_profile),
            ("账户管理", self.show_accounts),
            ("收入管理", self.show_incomes),
            ("支出管理", self.show_expenses),
            ("借入管理", self.show_borrow_in),
            ("借出管理", self.show_borrow_out),
            ("统计查询", self.show_statistics),
        ]
        
        for text, cmd in menu_items:
            btn = tk.Button(menu_frame, text=text, command=cmd,
                            font=("Microsoft YaHei", 11), bg="white", fg="#2c3e50",
                            relief="flat", anchor="w", padx=15, pady=12,
                            activebackground="#3498db", activeforeground="white")
            btn.pack(fill="x")
        
        # 右侧内容区域
        self.content_frame = tk.Frame(body, bg="white", relief="solid", bd=1)
        self.content_frame.pack(side="left", fill="both", expand=True)
        
        # 默认显示个人资料
        self.show_profile()
    
    def logout(self):
        if messagebox.askyesno("确认", "确定要退出登录吗?"):
            self.current_user = None
            self.user_id = None
            self.show_login()
    
    # ==================== 个人资料 ====================
    def show_profile(self):
        self.clear_content()
        
        info = self.db.get_user_info(self.user_id)
        if not info:
            return
        
        frame = tk.Frame(self.content_frame, bg="white", padx=40, pady=30)
        frame.pack(fill="both", expand=True)
        
        self.create_title(frame, "个人资料").grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 20))
        
        labels = [("用户名:", info[1]), ("姓名:", info[2] or ""), ("学校:", info[3] or ""), ("个性签名:", info[4] or "")]
        
        for i, (label, value) in enumerate(labels, start=1):
            tk.Label(frame, text=label, font=self.default_font,
                     bg="white", fg="#34495e").grid(row=i, column=0, sticky="e", padx=10, pady=8)
            tk.Label(frame, text=value, font=self.default_font,
                     bg="white", fg="#2c3e50").grid(row=i, column=1, sticky="w", padx=10, pady=8)
        
        # 编辑按钮
        def edit_profile():
            edit_win = tk.Toplevel(self.root)
            edit_win.title("编辑个人资料")
            edit_win.geometry("400x350")
            edit_win.configure(bg="white")
            edit_win.resizable(False, False)
            edit_win.grab_set()
            
            ef = tk.Frame(edit_win, bg="white", padx=30, pady=20)
            ef.pack(fill="both", expand=True)
            
            tk.Label(ef, text="编辑个人资料", font=self.title_font,
                     bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 20))
            
            tk.Label(ef, text="姓名:", bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=8)
            e_name = self.create_entry(ef, width=25)
            e_name.insert(0, info[2] or "")
            e_name.grid(row=1, column=1, padx=5, pady=8)
            
            tk.Label(ef, text="学校:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=8)
            e_school = self.create_entry(ef, width=25)
            e_school.insert(0, info[3] or "")
            e_school.grid(row=2, column=1, padx=5, pady=8)
            
            tk.Label(ef, text="个性签名:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="ne", padx=5, pady=8)
            e_sig = tk.Text(ef, width=25, height=4, font=self.default_font, relief="solid", bd=1)
            e_sig.insert("1.0", info[4] or "")
            e_sig.grid(row=3, column=1, padx=5, pady=8)
            
            def save_profile():
                name = e_name.get().strip()
                school = e_school.get().strip()
                sig = e_sig.get("1.0", "end").strip()
                self.db.update_user_info(self.user_id, name, school, sig)
                self.current_user['name'] = name
                self.current_user['school'] = school
                self.current_user['signature'] = sig
                edit_win.destroy()
                self.show_profile()
                messagebox.showinfo("成功", "资料已更新")
            
            btn_save = self.create_button(ef, "保存修改", save_profile, width=10)
            btn_save.grid(row=4, column=0, columnspan=2, pady=20)
            
            btn_cancel = tk.Button(ef, text="取消", command=edit_win.destroy,
                                   font=self.default_font, bg="#95a5a6", fg="white")
            btn_cancel.grid(row=5, column=0, columnspan=2)
        
        btn_edit = self.create_button(frame, "编辑资料", edit_profile, width=12)
        btn_edit.grid(row=len(labels)+1, column=0, columnspan=2, pady=20)
    
    # ==================== 账户管理 ====================
    def show_accounts(self):
        self.clear_content()
        
        frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
        frame.pack(fill="both", expand=True)
        
        self.create_title(frame, "我的账户").pack(anchor="w", pady=(0, 10))
        
        # 表格
        columns = ("id", "card_number", "holder_name", "bank_name", "balance")
        headings = ("ID", "卡号", "持卡人", "开户行", "余额(¥)")
        widths = (50, 160, 100, 120, 100)
        tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
        
        tree_frame = tk.Frame(frame)
        tree_frame.pack(fill="both", expand=True)
        tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # 加载数据
        def load_accounts():
            for item in tree.get_children():
                tree.delete(item)
            data = self.db.get_accounts(self.user_id)
            for row in data:
                tree.insert("", "end", values=row)
        
        load_accounts()
        
        # 操作按钮
        btn_frame = tk.Frame(frame, bg="white")
        btn_frame.pack(fill="x", pady=10)
        
        def add_account():
            win = tk.Toplevel(self.root)
            win.title("添加账户")
            win.geometry("400x320")
            win.configure(bg="white")
            win.resizable(False, False)
            win.grab_set()
            
            af = tk.Frame(win, bg="white", padx=30, pady=20)
            af.pack(fill="both", expand=True)
            
            tk.Label(af, text="添加银行卡", font=self.title_font,
                     bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
            
            fields = [("卡号:", "card"), ("持卡人:", "holder"), ("开户行:", "bank"), ("余额:", "balance")]
            entries = {}
            for i, (label, key) in enumerate(fields, start=1):
                tk.Label(af, text=label, bg="white", font=self.default_font).grid(row=i, column=0, sticky="e", padx=5, pady=6)
                e = self.create_entry(af, width=25)
                e.grid(row=i, column=1, padx=5, pady=6)
                entries[key] = e
            
            def do_add():
                card = entries['card'].get().strip()
                holder = entries['holder'].get().strip()
                bank = entries['bank'].get().strip()
                try:
                    balance = float(entries['balance'].get() or 0)
                except ValueError:
                    messagebox.showerror("错误", "请输入有效的余额数字")
                    return
                if not card or not holder or not bank:
                    messagebox.showerror("错误", "请填写完整信息")
                    return
                self.db.add_account(self.user_id, card, holder, bank, balance)
                win.destroy()
                load_accounts()
                messagebox.showinfo("成功", "账户添加成功")
            
            btn_add = self.create_button(af, "添 加", do_add, width=10)
            btn_add.grid(row=5, column=0, columnspan=2, pady=15)
            
            btn_cancel = tk.Button(af, text="取消", command=win.destroy,
                                   font=self.default_font, bg="#95a5a6", fg="white")
            btn_cancel.grid(row=6, column=0, columnspan=2)
        
        def delete_account():
            selected = tree.selection()
            if not selected:
                messagebox.showerror("错误", "请先选择要删除的账户")
                return
            item = tree.item(selected[0])
            acc_id = item['values'][0]
            if messagebox.askyesno("确认", "确定要删除该账户吗?"):
                self.db.delete_account(acc_id)
                load_accounts()
                messagebox.showinfo("成功", "账户已删除")
        
        self.create_button(btn_frame, "+ 添加账户", add_account, width=12).pack(side="left", padx=5)
        self.create_button(btn_frame, "删除账户", delete_account, width=12).pack(side="left", padx=5)
    
    # ==================== 收入管理 ====================
    def show_incomes(self):
        self._show_transaction("incomes", "收入管理")
    
    def show_expenses(self):
        self._show_transaction("expenses", "支出管理")
    
    def _show_transaction(self, tx_type, title):
        """通用收支管理界面"""
        self.clear_content()
        
        frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
        frame.pack(fill="both", expand=True)
        
        self.create_title(frame, title).pack(anchor="w", pady=(0, 10))
        
        # 筛选条件
        filter_frame = tk.Frame(frame, bg="white")
        filter_frame.pack(fill="x", pady=(0, 10))
        
        tk.Label(filter_frame, text="开始日期:", bg="white", font=self.default_font).pack(side="left", padx=5)
        entry_start = self.create_entry(filter_frame, width=12)
        entry_start.insert(0, (datetime.datetime.now() - datetime.timedelta(days=30)).strftime("%Y-%m-%d"))
        entry_start.pack(side="left", padx=5)
        
        tk.Label(filter_frame, text="结束日期:", bg="white", font=self.default_font).pack(side="left", padx=5)
        entry_end = self.create_entry(filter_frame, width=12)
        entry_end.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
        entry_end.pack(side="left", padx=5)
        
        # 表格
        columns = ("id", "amount", "type", "time", "description")
        headings = ("ID", "金额(¥)", "类型", "时间", "备注")
        widths = (40, 80, 100, 120, 150)
        tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
        
        tree_frame = tk.Frame(frame)
        tree_frame.pack(fill="both", expand=True)
        tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        def load_data():
            for item in tree.get_children():
                tree.delete(item)
            start = entry_start.get().strip()
            end = entry_end.get().strip()
            if tx_type == "incomes":
                data = self.db.get_incomes(self.user_id, start, end)
            else:
                data = self.db.get_expenses(self.user_id, start, end)
            for row in data:
                tree.insert("", "end", values=row)
        
        load_data()
        
        # 操作按钮
        btn_frame = tk.Frame(frame, bg="white")
        btn_frame.pack(fill="x", pady=10)
        
        def add_transaction():
            win = tk.Toplevel(self.root)
            win.title(f"添加{title}")
            win.geometry("420x350")
            win.configure(bg="white")
            win.resizable(False, False)
            win.grab_set()
            
            af = tk.Frame(win, bg="white", padx=30, pady=20)
            af.pack(fill="both", expand=True)
            
            tk.Label(af, text=f"添加{title}", font=self.title_font,
                     bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
            
            # 类型下拉
            tk.Label(af, text="类型:", bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=6)
            type_var = tk.StringVar()
            type_combo = ttk.Combobox(af, textvariable=type_var, width=22, font=self.default_font)
            if tx_type == "incomes":
                type_combo['values'] = self.db.get_income_types()
            else:
                type_combo['values'] = self.db.get_expense_types()
            type_combo.grid(row=1, column=1, padx=5, pady=6)
            type_combo.current(0)
            
            tk.Label(af, text="金额:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=6)
            e_amount = self.create_entry(af, width=25)
            e_amount.grid(row=2, column=1, padx=5, pady=6)
            
            tk.Label(af, text="日期:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="e", padx=5, pady=6)
            e_time = self.create_entry(af, width=25)
            e_time.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
            e_time.grid(row=3, column=1, padx=5, pady=6)
            
            tk.Label(af, text="备注:", bg="white", font=self.default_font).grid(row=4, column=0, sticky="ne", padx=5, pady=6)
            e_desc = tk.Text(af, width=25, height=3, font=self.default_font, relief="solid", bd=1)
            e_desc.grid(row=4, column=1, padx=5, pady=6)
            
            def do_add():
                try:
                    amount = float(e_amount.get().strip())
                except ValueError:
                    messagebox.showerror("错误", "请输入有效的金额数字")
                    return
                if amount <= 0:
                    messagebox.showerror("错误", "金额必须大于0")
                    return
                tx_type_name = type_var.get()
                time_str = e_time.get().strip()
                desc = e_desc.get("1.0", "end").strip()
                if not time_str:
                    time_str = datetime.datetime.now().strftime("%Y-%m-%d")
                if tx_type == "incomes":
                    self.db.add_income(self.user_id, amount, tx_type_name, time_str, desc)
                else:
                    self.db.add_expense(self.user_id, amount, tx_type_name, time_str, desc)
                win.destroy()
                load_data()
                messagebox.showinfo("成功", f"{title}添加成功")
            
            btn_add = self.create_button(af, "添 加", do_add, width=10)
            btn_add.grid(row=5, column=0, columnspan=2, pady=15)
            
            btn_cancel = tk.Button(af, text="取消", command=win.destroy,
                                   font=self.default_font, bg="#95a5a6", fg="white")
            btn_cancel.grid(row=6, column=0, columnspan=2)
        
        def delete_transaction():
            selected = tree.selection()
            if not selected:
                messagebox.showerror("错误", "请先选择要删除的记录")
                return
            item = tree.item(selected[0])
            tx_id = item['values'][0]
            if messagebox.askyesno("确认", "确定要删除该记录吗?"):
                if tx_type == "incomes":
                    self.db.delete_income(tx_id)
                else:
                    self.db.delete_expense(tx_id)
                load_data()
                messagebox.showinfo("成功", "记录已删除")
        
        def refresh_data():
            load_data()
        
        self.create_button(btn_frame, "+ 添加记录", add_transaction, width=12).pack(side="left", padx=5)
        self.create_button(btn_frame, "删除记录", delete_transaction, width=12).pack(side="left", padx=5)
        self.create_button(btn_frame, "刷新", refresh_data, width=12).pack(side="left", padx=5)
        
        # 绑定筛选事件
        def on_filter_changed(event=None):
            load_data()
        
        entry_start.bind('<KeyRelease>', on_filter_changed)
        entry_end.bind('<KeyRelease>', on_filter_changed)
    
    # ==================== 借入管理 ====================
    def show_borrow_in(self):
        self._show_borrow("borrow_in", "借入管理")
    
    def show_borrow_out(self):
        self._show_borrow("borrow_out", "借出管理")
    
    def _show_borrow(self, borrow_type, title):
        """通用借入借出管理界面"""
        self.clear_content()
        
        frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
        frame.pack(fill="both", expand=True)
        
        self.create_title(frame, title).pack(anchor="w", pady=(0, 10))
        
        if borrow_type == "borrow_in":
            columns = ("id", "amount", "lender", "borrow_date", "due_date", "description")
            headings = ("ID", "金额(¥)", "借款人", "借款日期", "还款日期", "备注")
        else:
            columns = ("id", "amount", "borrower", "lend_date", "due_date", "description")
            headings = ("ID", "金额(¥)", "出借人", "出借日期", "还款日期", "备注")
        widths = (40, 80, 100, 100, 100, 120)
        tree, scrollbar = self.create_treeview(frame, columns, headings, widths)
        
        tree_frame = tk.Frame(frame)
        tree_frame.pack(fill="both", expand=True)
        tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        def load_data():
            for item in tree.get_children():
                tree.delete(item)
            if borrow_type == "borrow_in":
                data = self.db.get_borrow_in(self.user_id)
            else:
                data = self.db.get_borrow_out(self.user_id)
            for row in data:
                tree.insert("", "end", values=row)
        
        load_data()
        
        btn_frame = tk.Frame(frame, bg="white")
        btn_frame.pack(fill="x", pady=10)
        
        def add_borrow():
            win = tk.Toplevel(self.root)
            win.title(f"添加{title}")
            win.geometry("420x380")
            win.configure(bg="white")
            win.resizable(False, False)
            win.grab_set()
            
            af = tk.Frame(win, bg="white", padx=30, pady=20)
            af.pack(fill="both", expand=True)
            
            tk.Label(af, text=f"添加{title}", font=self.title_font,
                     bg="white", fg="#2c3e50").grid(row=0, column=0, columnspan=2, pady=(0, 15))
            
            if borrow_type == "borrow_in":
                label_person = "借款人:"
            else:
                label_person = "出借人:"
            
            tk.Label(af, text=label_person, bg="white", font=self.default_font).grid(row=1, column=0, sticky="e", padx=5, pady=6)
            e_person = self.create_entry(af, width=25)
            e_person.grid(row=1, column=1, padx=5, pady=6)
            
            tk.Label(af, text="金额:", bg="white", font=self.default_font).grid(row=2, column=0, sticky="e", padx=5, pady=6)
            e_amount = self.create_entry(af, width=25)
            e_amount.grid(row=2, column=1, padx=5, pady=6)
            
            tk.Label(af, text="日期:", bg="white", font=self.default_font).grid(row=3, column=0, sticky="e", padx=5, pady=6)
            e_date = self.create_entry(af, width=25)
            e_date.insert(0, datetime.datetime.now().strftime("%Y-%m-%d"))
            e_date.grid(row=3, column=1, padx=5, pady=6)
            
            tk.Label(af, text="还款日期:", bg="white", font=self.default_font).grid(row=4, column=0, sticky="e", padx=5, pady=6)
            e_due = self.create_entry(af, width=25)
            e_due.insert(0, (datetime.datetime.now() + datetime.timedelta(days=30)).strftime("%Y-%m-%d"))
            e_due.grid(row=4, column=1, padx=5, pady=6)
            
            tk.Label(af, text="备注:", bg="white", font=self.default_font).grid(row=5, column=0, sticky="ne", padx=5, pady=6)
            e_desc = tk.Text(af, width=25, height=3, font=self.default_font, relief="solid", bd=1)
            e_desc.grid(row=5, column=1, padx=5, pady=6)
            
            def do_add():
                person = e_person.get().strip()
                try:
                    amount = float(e_amount.get().strip())
                except ValueError:
                    messagebox.showerror("错误", "请输入有效的金额数字")
                    return
                if not person:
                    messagebox.showerror("错误", "请输入姓名")
                    return
                if amount <= 0:
                    messagebox.showerror("错误", "金额必须大于0")
                    return
                date_str = e_date.get().strip() or datetime.datetime.now().strftime("%Y-%m-%d")
                due_str = e_due.get().strip() or ""
                desc = e_desc.get("1.0", "end").strip()
                if borrow_type == "borrow_in":
                    self.db.add_borrow_in(self.user_id, amount, person, date_str, due_str, desc)
                else:
                    self.db.add_borrow_out(self.user_id, amount, person, date_str, due_str, desc)
                win.destroy()
                load_data()
                messagebox.showinfo("成功", f"{title}添加成功")
            
            btn_add = self.create_button(af, "添 加", do_add, width=10)
            btn_add.grid(row=6, column=0, columnspan=2, pady=15)
            
            btn_cancel = tk.Button(af, text="取消", command=win.destroy,
                                   font=self.default_font, bg="#95a5a6", fg="white")
            btn_cancel.grid(row=7, column=0, columnspan=2)
        
        self.create_button(btn_frame, "+ 添加记录", add_borrow, width=12).pack(side="left", padx=5)
        self.create_button(btn_frame, "刷新", load_data, width=12).pack(side="left", padx=5)
    
    # ==================== 统计查询 ====================
    def show_statistics(self):
        self.clear_content()
        
        frame = tk.Frame(self.content_frame, bg="white", padx=20, pady=15)
        frame.pack(fill="both", expand=True)
        
        self.create_title(frame, "统计查询").pack(anchor="w", pady=(0, 10))
        
        # 筛选
        filter_frame = tk.Frame(frame, bg="white")
        filter_frame.pack(fill="x", pady=(0, 10))
        
        tk.Label(filter_frame, text="年份:", bg="white", font=self.default_font).pack(side="left", padx=5)
        year_var = tk.StringVar()
        year_combo = ttk.Combobox(filter_frame, textvariable=year_var, width=10, font=self.default_font)
        year_combo['values'] = [str(y) for y in range(2020, 2031)]
        year_combo.set(str(datetime.datetime.now().year))
        year_combo.pack(side="left", padx=5)
        
        def update_charts():
            year = int(year_var.get())
            self._render_charts(frame, year)
        
        btn_update = self.create_button(filter_frame, "更新图表", update_charts, width=10)
        btn_update.pack(side="left", padx=10)
        
        # 图表容器
        chart_frame = tk.Frame(frame, bg="white")
        chart_frame.pack(fill="both", expand=True)
        
        self._render_charts(chart_frame, int(year_var.get()))
    
    def _render_charts(self, parent, year):
        """渲染统计图表"""
        # 清除旧图表
        for widget in parent.winfo_children():
            widget.destroy()
        
        # 获取数据
        income_by_type = self.db.get_income_by_type(self.user_id)
        expense_by_type = self.db.get_expense_by_type(self.user_id)
        monthly = self.db.get_monthly_summary(self.user_id, year)
        
        # 创建图表
        fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
        fig.patch.set_facecolor('white')
        
        # 饼图:收入分类
        ax1 = axes[0]
        if income_by_type:
            labels = [row[0] for row in income_by_type if row[1] and row[1] > 0]
            sizes = [row[1] for row in income_by_type if row[1] and row[1] > 0]
            if sizes:
                ax1.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
                        colors=plt.cm.Paired(range(len(sizes))))
                ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
            else:
                ax1.text(0.5, 0.5, '暂无收入数据', ha='center', va='center', fontsize=12)
                ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
        else:
            ax1.text(0.5, 0.5, '暂无收入数据', ha='center', va='center', fontsize=12)
            ax1.set_title('收入分类占比', fontsize=12, fontweight='bold')
        
        # 饼图:支出分类
        ax2 = axes[1]
        if expense_by_type:
            labels = [row[0] for row in expense_by_type if row[1] and row[1] > 0]
            sizes = [row[1] for row in expense_by_type if row[1] and row[1] > 0]
            if sizes:
                ax2.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
                        colors=plt.cm.Set3(range(len(sizes))))
                ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
            else:
                ax2.text(0.5, 0.5, '暂无支出数据', ha='center', va='center', fontsize=12)
                ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
        else:
            ax2.text(0.5, 0.5, '暂无支出数据', ha='center', va='center', fontsize=12)
            ax2.set_title('支出分类占比', fontsize=12, fontweight='bold')
        
        plt.tight_layout()
        
        # 嵌入到Tkinter
        canvas = FigureCanvasTkAgg(fig, master=parent)
        canvas.draw()
        canvas.get_tk_widget().pack(side="top", fill="both", expand=True, pady=5)
        
        # 月度趋势图(柱状图)
        fig2, ax3 = plt.subplots(figsize=(10, 3))
        fig2.patch.set_facecolor('white')
        
        months = list(range(1, 13))
        incomes = [monthly[m][0] for m in months]
        expenses = [monthly[m][1] for m in months]
        
        x = range(len(months))
        width = 0.35
        ax3.bar([i - width/2 for i in x], incomes, width, label='收入', color='#2ecc71')
        ax3.bar([i + width/2 for i in x], expenses, width, label='支出', color='#e74c3c')
        ax3.set_xlabel('月份', fontsize=10)
        ax3.set_ylabel('金额 (¥)', fontsize=10)
        ax3.set_title(f'{year}年 月度收支趋势', fontsize=12, fontweight='bold')
        ax3.set_xticks(x)
        ax3.set_xticklabels([f'{m}月' for m in months])
        ax3.legend()
        ax3.grid(axis='y', linestyle='--', alpha=0.7)
        
        plt.tight_layout()
        
        canvas2 = FigureCanvasTkAgg(fig2, master=parent)
        canvas2.draw()
        canvas2.get_tk_widget().pack(side="top", fill="both", expand=True, pady=5)
    
    # ==================== 辅助 ====================
    def clear_content(self):
        for widget in self.content_frame.winfo_children():
            widget.destroy()


# ==================== 主程序入口 ====================
if __name__ == "__main__":
    # 检查必要的库
    try:
        import matplotlib
        import PIL
    except ImportError:
        print("请安装依赖库: pip install matplotlib Pillow")
        sys.exit(1)
    
    root = tk.Tk()
    app = FinanceApp(root)
    root.mainloop()

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐