帅哥520 发表于 2026-2-26 20:52:14

python答疑送福利

本帖最后由 帅哥520 于 2026-2-27 11:25 编辑

第一期
本期奖励:30鱼币
本期的题目是:个人资金管理系统
要求
收支记录:记录每笔资金的金额、类型(收入 / 支出)、分类、备注、时间;
分类统计:按日 / 周 / 月 / 分类统计收支情况,计算结余;
预算管理:设置月度分类预算,预警超支情况;
数据可视化:生成收支趋势图、分类占比图;
数据持久化:收支数据保存到本地 JSON 文件,支持导入 / 导出;
使用Tkinter制作可视化界面
代码行数200行以上
加分点:
1.达到上面所有要求
2.额外多出1个功能(如有请标识功能名称及其功能)                                    {:5_106:}
3.界面简洁美观(此点很容易得奖哦)
已开启抢楼,第6、16、36、56层有奖励


补充内容 (2026-2-26 20:59):
欢迎各位参与
本期难度:中等
因为是第一次做这个系列,下一期奖励会提升
我改了下奖励,多了20鱼币,一定要给代码

小甲鱼 发表于 2026-2-27 06:03:22

做出来的程序效果精美,我追加 30RMB 作为奖励~

fan8276 发表于 2026-2-28 17:29:06

{:13_453:}

苦难求生者 发表于 2026-3-1 16:26:33

需要学完你的零基础课程才能完成这个任务吗?

苦难求生者 发表于 2026-3-1 19:29:25

如何上传照片文件?

知行合一@WHG 发表于 2026-3-2 18:49:47

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import json
import datetime
import os
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import calendar
from collections import defaultdict
import shutil

# 设置matplotlib中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams['axes.unicode_minus'] = False

class FinanceManager:
    def __init__(self, root):
      self.root = root
      self.root.title("个人资金管理系统")
      self.root.geometry("1000x700")
      self.root.resizable(True, True)
      
      # 数据文件路径
      self.data_file = "finance_data.json"
      self.backup_dir = "finance_backups"
      
      # 初始化数据结构
      self.finance_data = {
            "records": [],# 收支记录
            "budgets": {}   # 月度分类预算
      }
      
      # 默认分类
      self.income_categories = ["工资", "奖金", "理财", "兼职", "其他收入"]
      self.expense_categories = ["餐饮", "交通", "购物", "娱乐", "住房", "医疗", "其他支出"]
      
      # 加载数据
      self.load_data()
      
      # 创建界面
      self.create_widgets()
      
    def load_data(self):
      """加载本地JSON数据"""
      if os.path.exists(self.data_file):
            try:
                with open(self.data_file, "r", encoding="utf-8") as f:
                  self.finance_data = json.load(f)
            except Exception as e:
                messagebox.showerror("错误", f"加载数据失败:{str(e)}")
      else:
            # 初始化空数据文件
            self.save_data()
   
    def save_data(self):
      """保存数据到JSON文件"""
      try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(self.finance_data, f, ensure_ascii=False, indent=4)
      except Exception as e:
            messagebox.showerror("错误", f"保存数据失败:{str(e)}")
   
    def create_widgets(self):
      """创建主界面组件"""
      # 创建菜单栏
      self.create_menu()
      
      # 创建主面板
      main_notebook = ttk.Notebook(self.root)
      main_notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
      
      # 1. 收支记录标签页
      self.create_record_tab(main_notebook)
      
      # 2. 统计分析标签页
      self.create_analysis_tab(main_notebook)
      
      # 3. 预算管理标签页
      self.create_budget_tab(main_notebook)
      
      # 4. 数据可视化标签页
      self.create_visual_tab(main_notebook)
   
    def create_menu(self):
      """创建菜单栏"""
      menubar = tk.Menu(self.root)
      
      # 文件菜单
      file_menu = tk.Menu(menubar, tearoff=0)
      file_menu.add_command(label="导入数据", command=self.import_data)
      file_menu.add_command(label="导出数据", command=self.export_data)
      # 新增:导出Excel表格
      file_menu.add_command(label="导出Excel表格", command=self.export_to_excel)
      file_menu.add_separator()
      file_menu.add_command(label="备份数据", command=self.backup_data)
      file_menu.add_command(label="恢复数据", command=self.restore_data)
      file_menu.add_separator()
      file_menu.add_command(label="退出", command=self.root.quit)
      menubar.add_cascade(label="文件", menu=file_menu)
   
      # 帮助菜单
      help_menu = tk.Menu(menubar, tearoff=0)
      help_menu.add_command(label="关于", command=self.show_about)
      menubar.add_cascade(label="帮助", menu=help_menu)
   
      self.root.config(menu=menubar)
   
    def create_record_tab(self, parent):
      """创建收支记录标签页"""
      record_frame = ttk.Frame(parent)
      parent.add(record_frame, text="收支记录")
      
      # 录入区域
      input_frame = ttk.LabelFrame(record_frame, text="收支录入")
      input_frame.pack(fill=tk.X, padx=10, pady=10)
      
      # 金额
      ttk.Label(input_frame, text="金额:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
      self.amount_var = tk.StringVar()
      ttk.Entry(input_frame, textvariable=self.amount_var).grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
      
      # 类型(收入/支出)
      ttk.Label(input_frame, text="类型:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
      self.type_var = tk.StringVar(value="支出")
      type_combo = ttk.Combobox(input_frame, textvariable=self.type_var, values=["收入", "支出"], state="readonly")
      type_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
      
      # 分类
      ttk.Label(input_frame, text="分类:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
      self.category_var = tk.StringVar()
      self.category_combo = ttk.Combobox(input_frame, textvariable=self.category_var, state="readonly")
      self.category_combo.grid(row=0, column=5, padx=5, pady=5, sticky=tk.W)
      
      # 绑定类型变更事件,更新分类列表
      self.type_var.trace('w', self.update_categories)
      self.update_categories()
      
      # 备注
      ttk.Label(input_frame, text="备注:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
      self.note_var = tk.StringVar()
      ttk.Entry(input_frame, textvariable=self.note_var, width=50).grid(row=1, column=1, columnspan=5, padx=5, pady=5, sticky=tk.W)

      # 新增:收支日期
      ttk.Label(input_frame, text="日期:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
      self.date_var = tk.StringVar(value=datetime.datetime.now().strftime("%Y-%m-%d"))
      date_entry = ttk.Entry(input_frame, textvariable=self.date_var)
      date_entry.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
      ttk.Label(input_frame, text="格式:YYYY-MM-DD").grid(row=2, column=2, padx=5, pady=5, sticky=tk.W)

      # 按钮区域
      btn_frame = ttk.Frame(input_frame)
      btn_frame.grid(row=3, column=0, columnspan=6, pady=10)
      
      ttk.Button(btn_frame, text="添加记录", command=self.add_record).pack(side=tk.LEFT, padx=5)
      ttk.Button(btn_frame, text="清空输入", command=self.clear_input).pack(side=tk.LEFT, padx=5)
      ttk.Button(btn_frame, text="删除选中记录", command=self.delete_record).pack(side=tk.LEFT, padx=5)
      
      # 记录列表区域
      list_frame = ttk.LabelFrame(record_frame, text="收支记录列表")
      list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
      
      # 创建表格
      columns = ("时间", "类型", "分类", "金额", "备注")
      self.record_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=15)
      
      # 设置列标题
      for col in columns:
            self.record_tree.heading(col, text=col)
            self.record_tree.column(col, width=150 if col == "备注" else 100)
      
      # 滚动条
      scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.record_tree.yview)
      self.record_tree.configure(yscrollcommand=scrollbar.set)
      
      self.record_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
      scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
      
      # 刷新记录列表
      self.refresh_record_list()
   
    def create_analysis_tab(self, parent):
      """创建统计分析标签页"""
      analysis_frame = ttk.Frame(parent)
      parent.add(analysis_frame, text="统计分析")
      
      # 统计选项区域
      option_frame = ttk.LabelFrame(analysis_frame, text="统计选项")
      option_frame.pack(fill=tk.X, padx=10, pady=10)
      
      # 统计维度
      ttk.Label(option_frame, text="统计维度:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
      self.analysis_dim_var = tk.StringVar(value="月度")
      dim_combo = ttk.Combobox(option_frame, textvariable=self.analysis_dim_var,
                              values=["按日", "按周", "按月", "按分类"], state="readonly")
      dim_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
      
      # 时间范围
      ttk.Label(option_frame, text="时间范围:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
      self.year_var = tk.StringVar(value=str(datetime.datetime.now().year))
      year_combo = ttk.Combobox(option_frame, textvariable=self.year_var,
                                 values=, state="readonly")
      year_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
      
      self.month_var = tk.StringVar(value=str(datetime.datetime.now().month))
      month_combo = ttk.Combobox(option_frame, textvariable=self.month_var,
                                  values=, state="readonly")
      month_combo.grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
      
      # 统计按钮
      ttk.Button(option_frame, text="开始统计", command=self.run_analysis).grid(row=0, column=5, padx=5, pady=5)
      
      # 统计结果显示区域
      result_frame = ttk.LabelFrame(analysis_frame, text="统计结果")
      result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
      
      # 创建结果文本框
      self.analysis_text = tk.Text(result_frame, wrap=tk.WORD, height=20)
      scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.analysis_text.yview)
      self.analysis_text.configure(yscrollcommand=scrollbar.set)
      
      self.analysis_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
      scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
   
    def create_budget_tab(self, parent):
      """创建预算管理标签页"""
      budget_frame = ttk.Frame(parent)
      parent.add(budget_frame, text="预算管理")
      
      # 预算设置区域
      setup_frame = ttk.LabelFrame(budget_frame, text="月度预算设置")
      setup_frame.pack(fill=tk.X, padx=10, pady=10)
      
      # 年份月份
      ttk.Label(setup_frame, text="年份:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
      self.budget_year_var = tk.StringVar(value=str(datetime.datetime.now().year))
      budget_year_combo = ttk.Combobox(setup_frame, textvariable=self.budget_year_var,
                                        values=, state="readonly")
      budget_year_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
      
      ttk.Label(setup_frame, text="月份:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
      self.budget_month_var = tk.StringVar(value=str(datetime.datetime.now().month))
      budget_month_combo = ttk.Combobox(setup_frame, textvariable=self.budget_month_var,
                                       values=, state="readonly")
      budget_month_combo.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
      
      # 分类和金额
      ttk.Label(setup_frame, text="支出分类:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
      self.budget_category_var = tk.StringVar()
      budget_category_combo = ttk.Combobox(setup_frame, textvariable=self.budget_category_var,
                                          values=self.expense_categories, state="readonly")
      budget_category_combo.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
      
      ttk.Label(setup_frame, text="预算金额:").grid(row=1, column=2, padx=5, pady=5, sticky=tk.W)
      self.budget_amount_var = tk.StringVar()
      ttk.Entry(setup_frame, textvariable=self.budget_amount_var).grid(row=1, column=3, padx=5, pady=5, sticky=tk.W)
      
      # 按钮
      btn_frame = ttk.Frame(setup_frame)
      btn_frame.grid(row=1, column=4, padx=5, pady=5)
      ttk.Button(btn_frame, text="设置预算", command=self.set_budget).pack(side=tk.LEFT, padx=5)
      ttk.Button(btn_frame, text="查看预算", command=self.view_budget).pack(side=tk.LEFT, padx=5)
      
      # 预算预警区域
      alert_frame = ttk.LabelFrame(budget_frame, text="预算预警")
      alert_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
      
      self.alert_text = tk.Text(alert_frame, wrap=tk.WORD, height=15)
      scrollbar = ttk.Scrollbar(alert_frame, orient=tk.VERTICAL, command=self.alert_text.yview)
      self.alert_text.configure(yscrollcommand=scrollbar.set)
      
      self.alert_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10,0), pady=10)
      scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,10), pady=10)
      
      # 加载预算预警
      self.check_budget_alert()
   
    def create_visual_tab(self, parent):
      """创建数据可视化标签页"""
      visual_frame = ttk.Frame(parent)
      parent.add(visual_frame, text="数据可视化")
      
      # 可视化选项
      option_frame = ttk.LabelFrame(visual_frame, text="图表类型")
      option_frame.pack(fill=tk.X, padx=10, pady=10)
      
      self.chart_type_var = tk.StringVar(value="收支趋势图")
      chart_types = ["收支趋势图", "分类占比图", "月度收支对比图"]
      for i, chart_type in enumerate(chart_types):
            ttk.Radiobutton(option_frame, text=chart_type, variable=self.chart_type_var,
                           value=chart_type).grid(row=0, column=i, padx=10, pady=5)
      
      ttk.Button(option_frame, text="生成图表", command=self.generate_chart).grid(row=0, column=3, padx=10, pady=5)
      
      # 图表显示区域
      self.chart_frame = ttk.Frame(visual_frame)
      self.chart_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
   
    # ------------------------ 核心功能实现 ------------------------
    def update_categories(self, *args):
      """根据收支类型更新分类列表"""
      if self.type_var.get() == "收入":
            self.category_combo['values'] = self.income_categories
            if self.category_var.get() not in self.income_categories:
                self.category_var.set(self.income_categories)
      else:
            self.category_combo['values'] = self.expense_categories
            if self.category_var.get() not in self.expense_categories:
                self.category_var.set(self.expense_categories)
   
    def add_record(self):
      """添加收支记录"""
      try:
            # 验证输入
            amount = float(self.amount_var.get())
            if amount <= 0:
                messagebox.showwarning("警告", "金额必须大于0!")
                return
      
            record_type = self.type_var.get()
            category = self.category_var.get()
            note = self.note_var.get().strip()
      
            # 新增:处理用户输入的日期
            date_str = self.date_var.get()
            try:
                # 解析日期,时间部分默认 00:00:00
                record_time = datetime.datetime.strptime(date_str, "%Y-%m-%d")
            except ValueError:
                messagebox.showerror("错误", "日期格式不正确,请使用 YYYY-MM-DD 格式!")
                return
      
            # 创建记录
            record = {
                "id": len(self.finance_data["records"]) + 1,
                "amount": amount,
                "type": record_type,
                "category": category,
                "note": note,
                "time": record_time.strftime("%Y-%m-%d %H:%M:%S")# 使用用户选择的日期
            }
      
            # 添加到数据并保存
            self.finance_data["records"].append(record)
            self.save_data()
      
            # 刷新列表并清空输入
            self.refresh_record_list()
            self.clear_input()
      
            messagebox.showinfo("成功", "记录添加成功!")
      
            # 检查预算预警
            self.check_budget_alert()
      
      except ValueError:
            messagebox.showerror("错误", "请输入有效的金额数字!")
      except Exception as e:
            messagebox.showerror("错误", f"添加记录失败:{str(e)}")
   
    def clear_input(self):
      """清空输入框"""
      self.amount_var.set("")
      self.note_var.set("")
      self.type_var.set("支出")
      self.date_var.set(datetime.datetime.now().strftime("%Y-%m-%d"))# 重置为今天
   
    def refresh_record_list(self):
      """刷新收支记录列表"""
      # 清空现有数据
      for item in self.record_tree.get_children():
            self.record_tree.delete(item)
      
      # 添加新数据
      for record in self.finance_data["records"]:
            self.record_tree.insert("", tk.END, values=(
                record["time"],
                record["type"],
                record["category"],
                record["amount"],
                record["note"]
            ))
   
    def delete_record(self):
      """删除选中的记录"""
      selected = self.record_tree.selection()
      if not selected:
            messagebox.showwarning("警告", "请先选中要删除的记录!")
            return
      
      if messagebox.askyesno("确认", "确定要删除选中的记录吗?"):
            # 获取选中行的索引
            index = self.record_tree.index(selected)
            
            # 删除记录
            del self.finance_data["records"]
            
            # 重新编号
            for i, record in enumerate(self.finance_data["records"]):
                record["id"] = i + 1
            
            # 保存并刷新
            self.save_data()
            self.refresh_record_list()
            
            messagebox.showinfo("成功", "记录删除成功!")
   
    def run_analysis(self):
      """执行统计分析"""
      self.analysis_text.delete(1.0, tk.END)
      
      try:
            year = int(self.year_var.get())
            month = int(self.month_var.get())
            dim = self.analysis_dim_var.get()
            
            # 筛选指定年月的记录
            filtered_records = []
            for record in self.finance_data["records"]:
                rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
                if rec_time.year == year and rec_time.month == month:
                  filtered_records.append(record)
            
            if not filtered_records:
                self.analysis_text.insert(tk.END, f"{year}年{month}月暂无收支记录!")
                return
            
            # 按不同维度统计
            if dim == "按日":
                self.analyze_by_day(filtered_records, year, month)
            elif dim == "按周":
                self.analyze_by_week(filtered_records, year, month)
            elif dim == "按月":
                self.analyze_by_month(filtered_records, year, month)
            elif dim == "按分类":
                self.analyze_by_category(filtered_records, year, month)
               
      except Exception as e:
            self.analysis_text.insert(tk.END, f"统计出错:{str(e)}")
   
    def analyze_by_day(self, records, year, month):
      """按日统计"""
      daily_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
      
      for record in records:
            day = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S").day
            if record["type"] == "收入":
                daily_stats["收入"] += record["amount"]
            else:
                daily_stats["支出"] += record["amount"]
            daily_stats["结余"] = daily_stats["收入"] - daily_stats["支出"]
      
      # 生成统计结果
      result = f"{year}年{month}月 按日收支统计\n"
      result += "-" * 80 + "\n"
      result += f"{'日期':<6}{'收入':<10}{'支出':<10}{'结余':<10}\n"
      result += "-" * 80 + "\n"
      
      total_income = 0
      total_expense = 0
      
      for day in sorted(daily_stats.keys()):
            stats = daily_stats
            result += f"{day:<6}日{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
            total_income += stats["收入"]
            total_expense += stats["支出"]
      
      total_balance = total_income - total_expense
      result += "-" * 80 + "\n"
      result += f"总计:<6{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
      
      self.analysis_text.insert(tk.END, result)
   
    def analyze_by_week(self, records, year, month):
      """按周统计"""
      weekly_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
      
      for record in records:
            rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
            week = rec_time.isocalendar()# 获取周数
            if record["type"] == "收入":
                weekly_stats["收入"] += record["amount"]
            else:
                weekly_stats["支出"] += record["amount"]
            weekly_stats["结余"] = weekly_stats["收入"] - weekly_stats["支出"]
      
      # 生成统计结果
      result = f"{year}年{month}月 按周收支统计\n"
      result += "-" * 80 + "\n"
      result += f"{'周数':<6}{'收入':<10}{'支出':<10}{'结余':<10}\n"
      result += "-" * 80 + "\n"
      
      total_income = 0
      total_expense = 0
      
      for week in sorted(weekly_stats.keys()):
            stats = weekly_stats
            result += f"第{week}周:<6{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
            total_income += stats["收入"]
            total_expense += stats["支出"]
      
      total_balance = total_income - total_expense
      result += "-" * 80 + "\n"
      result += f"总计:<6{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
      
      self.analysis_text.insert(tk.END, result)
   
    def analyze_by_month(self, records, year, month):
      """按月统计"""
      # 汇总当月所有记录
      income = sum(r["amount"] for r in records if r["type"] == "收入")
      expense = sum(r["amount"] for r in records if r["type"] == "支出")
      balance = income - expense
      
      result = f"{year}年{month}月 收支汇总\n"
      result += "-" * 50 + "\n"
      result += f"总收入:{income:.2f} 元\n"
      result += f"总支出:{expense:.2f} 元\n"
      result += f"月结余:{balance:.2f} 元\n"
      result += "-" * 50 + "\n"
      
      # 显示各项支出占比
      result += "\n支出分类明细:\n"
      expense_categories = defaultdict(float)
      for r in records:
            if r["type"] == "支出":
                expense_categories] += r["amount"]
      
      for cat, amt in expense_categories.items():
            ratio = (amt / expense) * 100 if expense > 0 else 0
            result += f"{cat}:{amt:.2f} 元 ({ratio:.1f}%)\n"
      
      self.analysis_text.insert(tk.END, result)
   
    def analyze_by_category(self, records, year, month):
      """按分类统计"""
      # 按分类汇总
      category_stats = defaultdict(lambda: {"收入": 0, "支出": 0, "结余": 0})
      
      for record in records:
            cat = record["category"]
            if record["type"] == "收入":
                category_stats["收入"] += record["amount"]
            else:
                category_stats["支出"] += record["amount"]
            category_stats["结余"] = category_stats["收入"] - category_stats["支出"]
      
      # 生成统计结果
      result = f"{year}年{month}月 按分类收支统计\n"
      result += "-" * 80 + "\n"
      result += f"{'分类':<10}{'收入':<10}{'支出':<10}{'结余':<10}\n"
      result += "-" * 80 + "\n"
      
      total_income = 0
      total_expense = 0
      
      for cat in sorted(category_stats.keys()):
            stats = category_stats
            result += f"{cat:<10}{stats['收入']:<10.2f}{stats['支出']:<10.2f}{stats['结余']:<10.2f}\n"
            total_income += stats["收入"]
            total_expense += stats["支出"]
      
      total_balance = total_income - total_expense
      result += "-" * 80 + "\n"
      result += f"总计:<10{total_income:<10.2f}{total_expense:<10.2f}{total_balance:<10.2f}\n"
      
      self.analysis_text.insert(tk.END, result)
   
    def set_budget(self):
      """设置月度分类预算"""
      try:
            year = int(self.budget_year_var.get())
            month = int(self.budget_month_var.get())
            category = self.budget_category_var.get()
            amount = float(self.budget_amount_var.get())
            
            if amount < 0:
                messagebox.showwarning("警告", "预算金额不能为负数!")
                return
            
            # 构建预算键
            budget_key = f"{year}-{month:02d}"
            if budget_key not in self.finance_data["budgets"]:
                self.finance_data["budgets"] = {}
            
            # 设置预算
            self.finance_data["budgets"] = amount
            self.save_data()
            
            messagebox.showinfo("成功", f"{year}年{month}月 {category} 预算设置为:{amount:.2f} 元")
            
            # 清空输入
            self.budget_amount_var.set("")
            
            # 刷新预算预警
            self.check_budget_alert()
            
      except ValueError:
            messagebox.showerror("错误", "请输入有效的预算金额!")
      except Exception as e:
            messagebox.showerror("错误", f"设置预算失败:{str(e)}")
   
    def view_budget(self):
      """查看月度预算"""
      self.alert_text.delete(1.0, tk.END)
      
      try:
            year = int(self.budget_year_var.get())
            month = int(self.budget_month_var.get())
            budget_key = f"{year}-{month:02d}"
            
            if budget_key not in self.finance_data["budgets"] or not self.finance_data["budgets"]:
                self.alert_text.insert(tk.END, f"{year}年{month}月暂无预算设置!")
                return
            
            # 显示预算信息
            result = f"{year}年{month}月 预算设置\n"
            result += "-" * 50 + "\n"
            
            budgets = self.finance_data["budgets"]
            for cat, amt in budgets.items():
                result += f"{cat}:{amt:.2f} 元\n"
            
            self.alert_text.insert(tk.END, result)
            
      except Exception as e:
            self.alert_text.insert(tk.END, f"查看预算失败:{str(e)}")
   
    def check_budget_alert(self):
      """检查预算超支预警"""
      self.alert_text.delete(1.0, tk.END)
      
      # 获取当前年月
      now = datetime.datetime.now()
      year = now.year
      month = now.month
      budget_key = f"{year}-{month:02d}"
      
      # 筛选当月支出记录
      monthly_expenses = defaultdict(float)
      for record in self.finance_data["records"]:
            rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
            if rec_time.year == year and rec_time.month == month and record["type"] == "支出":
                monthly_expenses] += record["amount"]
      
      # 检查预算
      alert_info = f"{year}年{month}月 预算预警\n"
      alert_info += "-" * 60 + "\n"
      
      if budget_key in self.finance_data["budgets"]:
            budgets = self.finance_data["budgets"]
            has_alert = False
            
            for cat, budget_amt in budgets.items():
                actual_amt = monthly_expenses.get(cat, 0)
                usage_ratio = (actual_amt / budget_amt) * 100 if budget_amt > 0 else 0
               
                alert_info += f"{cat}:\n"
                alert_info += f"预算:{budget_amt:.2f} 元 | 已支出:{actual_amt:.2f} 元 | 使用率:{usage_ratio:.1f}%\n"
               
                if actual_amt > budget_amt:
                  alert_info += f"⚠️ 超支警告:超出预算 {actual_amt - budget_amt:.2f} 元\n"
                  has_alert = True
                elif usage_ratio >= 90:
                  alert_info += f"⚠️ 预警:预算即将用尽(剩余 {budget_amt - actual_amt:.2f} 元)\n"
                  has_alert = True
               
                alert_info += "\n"
            
            if not has_alert:
                alert_info += "✅ 所有分类预算使用正常,暂无超支风险\n"
      else:
            alert_info += "本月暂无预算设置,无法进行超支预警\n"
      
      self.alert_text.insert(tk.END, alert_info)
   
    def generate_chart(self):
      """生成可视化图表"""
      # 清空现有图表
      for widget in self.chart_frame.winfo_children():
            widget.destroy()
      
      chart_type = self.chart_type_var.get()
      try:
            if chart_type == "收支趋势图":
                self.generate_trend_chart()
            elif chart_type == "分类占比图":
                self.generate_pie_chart()
            elif chart_type == "月度收支对比图":
                self.generate_monthly_compare_chart()
               
      except Exception as e:
            messagebox.showerror("错误", f"生成图表失败:{str(e)}")
   
    def generate_trend_chart(self):
      """生成收支趋势图"""
      # 按月份汇总数据
      monthly_data = defaultdict(lambda: {"收入": 0, "支出": 0})
      
      for record in self.finance_data["records"]:
            rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
            month_key = rec_time.strftime("%Y-%m")
            
            if record["type"] == "收入":
                monthly_data["收入"] += record["amount"]
            else:
                monthly_data["支出"] += record["amount"]
      
      if not monthly_data:
            messagebox.showwarning("警告", "暂无收支数据,无法生成趋势图!")
            return
      
      # 排序月份
      sorted_months = sorted(monthly_data.keys())
      incomes = ["收入"] for m in sorted_months]
      expenses = ["支出"] for m in sorted_months]
      
      # 创建图表
      fig, ax = plt.subplots(figsize=(10, 6))
      ax.plot(sorted_months, incomes, marker='o', label='收入', color='green')
      ax.plot(sorted_months, expenses, marker='s', label='支出', color='red')
      
      ax.set_title('收支趋势图', fontsize=14)
      ax.set_xlabel('月份', fontsize=12)
      ax.set_ylabel('金额 (元)', fontsize=12)
      ax.legend()
      ax.grid(True, alpha=0.3)
      
      # 旋转x轴标签
      plt.xticks(rotation=45)
      plt.tight_layout()
      
      # 显示在Tkinter界面
      canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
      canvas.draw()
      canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
   
    def generate_pie_chart(self):
      """生成分类占比图"""
      # 汇总支出分类
      expense_categories = defaultdict(float)
      for record in self.finance_data["records"]:
            if record["type"] == "支出":
                expense_categories] += record["amount"]
      
      if not expense_categories:
            messagebox.showwarning("警告", "暂无支出数据,无法生成分类占比图!")
            return
      
      # 准备数据
      labels = list(expense_categories.keys())
      sizes = list(expense_categories.values())
      colors = ['#ff9999','#66b3ff','#99ff99','#ffcc99','#c2c2f0','#ffb3e6', '#c4e17f']
      
      # 创建图表
      fig, ax = plt.subplots(figsize=(8, 8))
      ax.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
      ax.axis('equal')# 保证饼图是正圆形
      
      ax.set_title('支出分类占比图', fontsize=14)
      
      # 显示在Tkinter界面
      canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
      canvas.draw()
      canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
   
    def generate_monthly_compare_chart(self):
      """生成月度收支对比图"""
      # 按月份汇总数据
      monthly_data = defaultdict(lambda: {"收入": 0, "支出": 0})
      
      for record in self.finance_data["records"]:
            rec_time = datetime.datetime.strptime(record["time"], "%Y-%m-%d %H:%M:%S")
            month_key = rec_time.strftime("%Y-%m")
            
            if record["type"] == "收入":
                monthly_data["收入"] += record["amount"]
            else:
                monthly_data["支出"] += record["amount"]
      
      if not monthly_data:
            messagebox.showwarning("警告", "暂无收支数据,无法生成对比图!")
            return
      
      # 排序月份
      sorted_months = sorted(monthly_data.keys())
      incomes = ["收入"] for m in sorted_months]
      expenses = ["支出"] for m in sorted_months]
      
      # 创建图表
      fig, ax = plt.subplots(figsize=(10, 6))
      x = range(len(sorted_months))
      width = 0.35
      
      ax.bar(, incomes, width, label='收入', color='green', alpha=0.7)
      ax.bar(, expenses, width, label='支出', color='red', alpha=0.7)
      
      ax.set_title('月度收支对比图', fontsize=14)
      ax.set_xlabel('月份', fontsize=12)
      ax.set_ylabel('金额 (元)', fontsize=12)
      ax.set_xticks(x)
      ax.set_xticklabels(sorted_months, rotation=45)
      ax.legend()
      ax.grid(True, alpha=0.3, axis='y')
      
      plt.tight_layout()
      
      # 显示在Tkinter界面
      canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
      canvas.draw()
      canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
   
    # ------------------------ 数据导入导出/备份恢复 ------------------------
    def import_data(self):
      """导入JSON数据"""
      file_path = filedialog.askopenfilename(
            title="选择数据文件",
            filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
      )
      
      if file_path:
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                  imported_data = json.load(f)
               
                # 验证数据结构
                if "records" not in imported_data or "budgets" not in imported_data:
                  messagebox.showerror("错误", "导入的文件格式不正确!")
                  return
               
                # 合并数据
                self.finance_data = imported_data
                self.save_data()
               
                # 刷新界面
                self.refresh_record_list()
                self.check_budget_alert()
               
                messagebox.showinfo("成功", "数据导入成功!")
               
            except Exception as e:
                messagebox.showerror("错误", f"导入数据失败:{str(e)}")
   
    def export_data(self):
      """导出JSON数据"""
      file_path = filedialog.asksaveasfilename(
            title="保存数据文件",
            defaultextension=".json",
            filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
      )
      
      if file_path:
            try:
                with open(file_path, "w", encoding="utf-8") as f:
                  json.dump(self.finance_data, f, ensure_ascii=False, indent=4)
               
                messagebox.showinfo("成功", f"数据已导出到:{file_path}")
               
            except Exception as e:
                messagebox.showerror("错误", f"导出数据失败:{str(e)}")

    def export_to_excel(self):
      """导出收支数据到Excel表格(.xlsx格式)"""
      if not self.finance_data["records"]:
            messagebox.showwarning("警告", "暂无收支记录,无法导出Excel!")
            return
   
      # 让用户选择保存路径和文件名
      file_path = filedialog.asksaveasfilename(
            title="导出Excel表格",
            defaultextension=".xlsx",
            filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
      )
   
      if not file_path:
            return# 用户取消选择
   
      try:
            from openpyxl import Workbook
            from openpyxl.styles import Font, Alignment, Border, Side
            from openpyxl.utils import get_column_letter
      
            # 创建工作簿
            wb = Workbook()
            ws = wb.active
            ws.title = "收支记录"
      
            # 1. 设置表头
            headers = ["序号", "时间", "类型", "分类", "金额(元)", "备注", "结余(元)"]
            for col, header in enumerate(headers, 1):
                cell = ws.cell(row=1, column=col, value=header)
                # 设置表头样式(加粗、居中、边框)
                cell.font = Font(bold=True, size=11)
                cell.alignment = Alignment(horizontal="center", vertical="center")
                cell.border = Border(
                  left=Side(style="thin"), right=Side(style="thin"),
                  top=Side(style="thin"), bottom=Side(style="thin")
                )
      
            # 2. 填充数据行
            total_income = 0# 累计收入
            total_expense = 0 # 累计支出
            for row, record in enumerate(self.finance_data["records"], 2):
                # 计算累计结余
                if record["type"] == "收入":
                  total_income += record["amount"]
                else:
                  total_expense += record["amount"]
                current_balance = total_income - total_expense
            
                # 填充数据
                data = [
                  record["id"],
                  record["time"],
                  record["type"],
                  record["category"],
                  round(record["amount"], 2),
                  record["note"] if record["note"] else "-",
                  round(current_balance, 2)
                ]
            
                # 写入单元格并设置样式
                for col, value in enumerate(data, 1):
                  cell = ws.cell(row=row, column=col, value=value)
                  cell.alignment = Alignment(horizontal="center", vertical="center")
                  cell.border = Border(
                        left=Side(style="thin"), right=Side(style="thin"),
                        top=Side(style="thin"), bottom=Side(style="thin")
                  )
      
            # 3. 自动调整列宽
            column_widths =
            for col, width in enumerate(column_widths, 1):
                ws.column_dimensions.width = width
      
            # 4. 添加汇总行
            summary_row = len(self.finance_data["records"]) + 3
            ws.cell(row=summary_row, column=1, value="汇总").font = Font(bold=True)
            ws.cell(row=summary_row, column=4, value="总收入:").font = Font(bold=True)
            ws.cell(row=summary_row, column=5, value=round(total_income, 2))
            ws.cell(row=summary_row+1, column=4, value="总支出:").font = Font(bold=True)
            ws.cell(row=summary_row+1, column=5, value=round(total_expense, 2))
            ws.cell(row=summary_row+2, column=4, value="总结余:").font = Font(bold=True)
            ws.cell(row=summary_row+2, column=5, value=round(total_income - total_expense, 2))
      
            # 5. 保存文件
            wb.save(file_path)
      
            messagebox.showinfo("成功", f"Excel表格已导出到:\n{file_path}")
      
      except ImportError:
            messagebox.showerror("错误", "未安装openpyxl库,请先执行:pip install openpyxl")
      except Exception as e:
            messagebox.showerror("错误", f"导出Excel失败:{str(e)}")
   
    def backup_data(self):
      """备份数据(加分项功能)"""
      # 创建备份目录
      if not os.path.exists(self.backup_dir):
            os.makedirs(self.backup_dir)
      
      # 生成备份文件名
      backup_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
      backup_file = os.path.join(self.backup_dir, f"finance_backup_{backup_time}.json")
      
      try:
            # 复制数据文件到备份文件
            shutil.copy2(self.data_file, backup_file)
            messagebox.showinfo("成功", f"数据备份成功!\n备份文件:{backup_file}")
            
      except Exception as e:
            messagebox.showerror("错误", f"备份数据失败:{str(e)}")
   
    def restore_data(self):
      """恢复数据(加分项功能)"""
      if not os.path.exists(self.backup_dir):
            messagebox.showwarning("警告", "暂无备份数据!")
            return
      
      # 选择备份文件
      backup_file = filedialog.askopenfilename(
            title="选择备份文件",
            initialdir=self.backup_dir,
            filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
      )
      
      if backup_file:
            if messagebox.askyesno("确认", "恢复数据将覆盖当前所有数据,确定继续吗?"):
                try:
                  # 复制备份文件到数据文件
                  shutil.copy2(backup_file, self.data_file)
                  
                  # 重新加载数据
                  self.load_data()
                  
                  # 刷新界面
                  self.refresh_record_list()
                  self.check_budget_alert()
                  
                  messagebox.showinfo("成功", "数据恢复成功!")
                  
                except Exception as e:
                  messagebox.showerror("错误", f"恢复数据失败:{str(e)}")
   
    def show_about(self):
      """显示关于信息"""
      about_text = """个人资金管理系统 v1.0
功能说明:
1. 收支记录:记录每笔资金的金额、类型、分类、备注、时间
2. 分类统计:按日/周/月/分类统计收支情况,计算结余
3. 预算管理:设置月度分类预算,预警超支情况
4. 数据可视化:生成收支趋势图、分类占比图、月度收支对比图
5. 数据持久化:收支数据保存到本地JSON文件,支持导入/导出
6. 数据备份与恢复:额外功能,保障数据安全
使用Tkinter开发,界面简洁美观"""
      
      messagebox.showinfo("关于", about_text)

# ------------------------ 程序入口 ------------------------
if __name__ == "__main__":
    root = tk.Tk()
    app = FinanceManager(root)
    root.mainloop()

iHobe 发表于 5 天前

我记得大学的时候 有同学接过这个类似的单子,因为自己一个人开发,到后期就放弃了
页: [1]
查看完整版本: python答疑送福利