Python + MySQL + Tkinter 桌面版图书借阅管理系统
这是一套经典的C/S架构桌面应用,适合作为课程设计或毕业设计。下面我将从系统设计、数据库建模、核心功能代码、界面布局等方面为你完整呈现。
🏛️ 系统总体设计
功能模块划分
图书借阅管理系统 ├── 登录模块 ├── 图书管理 │ ├── 图书录入/编辑/删除 │ ├── 图书检索(书名、作者、ISBN) │ └── 库存查看 ├── 读者管理 │ ├── 读者注册/编辑/注销 │ └── 读者信息查询 ├── 借书管理 │ ├── 借书操作(扫描ISBN+读者证号) │ └── 续借操作 ├── 还书管理 │ ├── 还书操作 │ └── 逾期罚款计算 └── 统计报表 ├── 热门图书排行 ├── 读者借阅排行榜 └── 每日/月流通量技术栈
层次 | 技术 |
|---|---|
客户端 | Python 3.10+, Tkinter, ttkbootstrap(美化) |
数据库 | MySQL 8.0 |
连接池 | PyMySQL 或 mysql-connector-python |
辅助库 | Pillow(图片处理), pandas(报表导出) |
🗄️ 数据库设计 (MySQL)
-- 创建数据库 CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_db; -- 管理员表 CREATE TABLE admins ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, password_hash VARCHAR(256) NOT NULL, real_name VARCHAR(50), role ENUM('admin','operator') DEFAULT 'operator' ); -- 图书类别表 CREATE TABLE categories ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT NULL, FOREIGN KEY (parent_id) REFERENCES categories(id) ); -- 图书表 CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) UNIQUE NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), publish_year YEAR, category_id INT, total_stock INT DEFAULT 1, -- 总库存 current_stock INT DEFAULT 1, -- 当前可借数量 location VARCHAR(50), -- 书架位置 cover_image BLOB, -- 封面图片(二进制) description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_title (title), INDEX idx_isbn (isbn), FOREIGN KEY (category_id) REFERENCES categories(id) ); -- 读者表 CREATE TABLE readers ( id INT AUTO_INCREMENT PRIMARY KEY, reader_no VARCHAR(18) UNIQUE NOT NULL, -- 读者证号 name VARCHAR(50) NOT NULL, gender ENUM('男','女'), phone VARCHAR(15), email VARCHAR(100), id_card VARCHAR(18), -- 身份证号 register_date DATE DEFAULT (CURRENT_DATE), max_borrow INT DEFAULT 5, -- 最大借阅数量 current_borrow INT DEFAULT 0, -- 当前已借数量 status ENUM('正常','挂失','注销') DEFAULT '正常' ); -- 借阅记录表 CREATE TABLE borrow_records ( id BIGINT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, due_date DATE NOT NULL, -- 应还日期 return_date DATETIME DEFAULT NULL, -- 实际归还日期 renew_count INT DEFAULT 0, -- 续借次数 fine_amount DECIMAL(8,2) DEFAULT 0.00, -- 罚款金额 operator_id INT, -- 办理管理员ID status ENUM('借出','已还','逾期','丢失') DEFAULT '借出', FOREIGN KEY (book_id) REFERENCES books(id), FOREIGN KEY (reader_id) REFERENCES readers(id), FOREIGN KEY (operator_id) REFERENCES admins(id), INDEX idx_reader (reader_id), INDEX idx_book (book_id), INDEX idx_status (status) ); -- 罚款记录表 CREATE TABLE fines ( id INT AUTO_INCREMENT PRIMARY KEY, record_id BIGINT NOT NULL, amount DECIMAL(8,2) NOT NULL, paid TINYINT DEFAULT 0, pay_date DATETIME DEFAULT NULL, FOREIGN KEY (record_id) REFERENCES borrow_records(id) );🖥️ Tkinter 界面设计与核心代码
项目结构
library_manager/ ├── main.py # 程序入口 ├── config.py # 数据库配置 ├── database.py # 数据库操作类 ├── models/ │ ├── book.py # 图书模型 │ ├── reader.py # 读者模型 │ └── borrow.py # 借阅模型 ├── views/ │ ├── login_window.py # 登录窗口 │ ├── main_window.py # 主界面(包含菜单栏、工具栏) │ ├── book_management.py # 图书管理页面 │ ├── reader_management.py # 读者管理页面 │ ├── borrow_return.py # 借还书页面 │ └── statistics.py # 统计报表页面 ├── utils/ │ ├── validators.py # 输入验证 │ └── helpers.py # 辅助函数(日期计算、罚款等) └── resources/ ├── icons/ # 图标文件 └── images/ # 默认封面等1. 数据库连接与操作基类 (database.py)
import pymysql from config import DB_CONFIG class DatabaseManager: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.connection = None return cls._instance def connect(self): if not self.connection or not self.connection.open: self.connection = pymysql.connect(**DB_CONFIG) return self.connection def execute_query(self, sql, params=None): conn = self.connect() with conn.cursor(pymysql.cursors.DictCursor) as cursor: cursor.execute(sql, params) result = cursor.fetchall() return result def execute_update(self, sql, params=None): conn = self.connect() with conn.cursor() as cursor: affected = cursor.execute(sql, params) conn.commit() return affected def close(self): if self.connection and self.connection.open: self.connection.close()2. 登录窗口 (views/login_window.py)
import tkinter as tk from tkinter import messagebox from database import DatabaseManager import hashlib class LoginWindow: def __init__(self): self.window = tk.Tk() self.window.title("图书借阅管理系统 - 登录") self.window.geometry("400x300") self.window.resizable(False, False) # 居中显示 screen_w = self.window.winfo_screenwidth() screen_h = self.window.winfo_screenheight() x = (screen_w - 400) // 2 y = (screen_h - 300) // 2 self.window.geometry(f"+{x}+{y}") self.setup_ui() def setup_ui(self): # 标题 tk.Label(self.window, text="图书借阅管理系统", font=("微软雅黑", 18)).pack(pady=30) # 用户名 frame_user = tk.Frame(self.window) frame_user.pack(pady=10) tk.Label(frame_user, text="用户名:", font=("微软雅黑", 12)).pack(side=tk.LEFT) self.entry_user = tk.Entry(frame_user, font=("微软雅黑", 12), width=25) self.entry_user.pack(side=tk.LEFT, padx=10) # 密码 frame_pass = tk.Frame(self.window) frame_pass.pack(pady=10) tk.Label(frame_pass, text="密 码:", font=("微软雅黑", 12)).pack(side=tk.LEFT) self.entry_pass = tk.Entry(frame_pass, show="*", font=("微软雅黑", 12), width=25) self.entry_pass.pack(side=tk.LEFT, padx=10) # 登录按钮 btn_login = tk.Button(self.window, text="登 录", font=("微软雅黑", 14), command=self.login, bg="#4CAF50", fg="white", width=15) btn_login.pack(pady=20) # 回车绑定 self.window.bind('<Return>', lambda e: self.login()) def login(self): username = self.entry_user.get().strip() password = self.entry_pass.get().strip() if not username or not password: messagebox.showwarning("提示", "请输入用户名和密码") return # 密码加密(SHA256) pwd_hash = hashlib.sha256(password.encode()).hexdigest() db = DatabaseManager() sql = "SELECT id, username, real_name, role FROM admins WHERE username=%s AND password_hash=%s" result = db.execute_query(sql, (username, pwd_hash)) if result: admin_info = result[0] self.window.destroy() from views.main_window import MainWindow MainWindow(admin_info).run() else: messagebox.showerror("错误", "用户名或密码错误")3. 主窗口 (views/main_window.py)
import tkinter as tk from tkinter import ttk, Menu from views.book_management import BookManagement from views.reader_management import ReaderManagement from views.borrow_return import BorrowReturn from views.statistics import Statistics class MainWindow: def __init__(self, admin_info): self.admin = admin_info self.root = tk.Tk() self.root.title(f"图书借阅管理系统 - 欢迎 {admin_info['real_name']}") self.root.geometry("1200x700") self.setup_menu() self.setup_notebook() self.setup_status_bar() def setup_menu(self): menubar = Menu(self.root) self.root.config(menu=menubar) # 系统菜单 sys_menu = Menu(menubar, tearoff=0) sys_menu.add_command(label="修改密码", command=self.change_password) sys_menu.add_separator() sys_menu.add_command(label="退出系统", command=self.exit_app) menubar.add_cascade(label="系统", menu=sys_menu) # 帮助菜单 help_menu = Menu(menubar, tearoff=0) help_menu.add_command(label="关于", command=self.show_about) menubar.add_cascade(label="帮助", menu=help_menu) def setup_notebook(self): self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 添加各个功能标签页 self.book_mgr = BookManagement(self.notebook, self.admin) self.reader_mgr = ReaderManagement(self.notebook, self.admin) self.borrow_mgr = BorrowReturn(self.notebook, self.admin) self.statistics = Statistics(self.notebook, self.admin) self.notebook.add(self.book_mgr, text="图书管理") self.notebook.add(self.reader_mgr, text="读者管理") self.notebook.add(self.borrow_mgr, text="借还书") self.notebook.add(self.statistics, text="统计报表") def setup_status_bar(self): status_frame = tk.Frame(self.root, relief=tk.SUNKEN, bd=1) status_frame.pack(side=tk.BOTTOM, fill=tk.X) self.lbl_admin = tk.Label(status_frame, text=f"当前管理员: {self.admin['real_name']}", anchor=tk.W) self.lbl_admin.pack(side=tk.LEFT, padx=5) self.lbl_time = tk.Label(status_frame, anchor=tk.E) self.lbl_time.pack(side=tk.RIGHT, padx=5) self.update_time() def update_time(self): from datetime import datetime now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.lbl_time.config(text=now) self.root.after(1000, self.update_time) def change_password(self): # 弹出修改密码对话框 pass def exit_app(self): if tk.messagebox.askokcancel("确认", "确定要退出系统吗?"): self.root.destroy() def show_about(self): tk.messagebox.showinfo("关于", "图书借阅管理系统 v1.0\nPython + MySQL + Tkinter") def run(self): self.root.mainloop()4. 图书管理页面 (views/book_management.py) 核心部分
import tkinter as tk from tkinter import ttk, messagebox from database import DatabaseManager class BookManagement(tk.Frame): def __init__(self, parent, admin): super().__init__(parent) self.admin = admin self.db = DatabaseManager() self.setup_ui() self.load_books() def setup_ui(self): # 顶部搜索区域 search_frame = tk.Frame(self) search_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label(search_frame, text="关键词:").pack(side=tk.LEFT) self.search_entry = tk.Entry(search_frame, width=30) self.search_entry.pack(side=tk.LEFT, padx=5) self.search_entry.bind('<KeyRelease>', self.search_books) tk.Label(search_frame, text="类别:").pack(side=tk.LEFT, padx=(20,0)) self.category_combo = ttk.Combobox(search_frame, state="readonly", width=15) self.category_combo.pack(side=tk.LEFT, padx=5) self.load_categories() self.category_combo.bind('<<ComboboxSelected>>', self.search_books) # 操作按钮 btn_frame = tk.Frame(self) btn_frame.pack(fill=tk.X, padx=10, pady=5) tk.Button(btn_frame, text="新增图书", command=self.add_book).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="编辑图书", command=self.edit_book).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="删除图书", command=self.delete_book).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="刷新", command=self.load_books).pack(side=tk.LEFT, padx=5) # 表格显示 columns = ("id", "isbn", "title", "author", "publisher", "stock", "current", "location") self.tree = ttk.Treeview(self, columns=columns, show="headings", selectmode="browse") self.tree.heading("id", text="ID") self.tree.heading("isbn", text="ISBN") self.tree.heading("title", text="书名") self.tree.heading("author", text="作者") self.tree.heading("publisher", text="出版社") self.tree.heading("stock", text="总库存") self.tree.heading("current", text="可借") self.tree.heading("location", text="位置") self.tree.column("id", width=50, anchor=tk.CENTER) self.tree.column("isbn", width=150) self.tree.column("title", width=250) self.tree.column("author", width=100) self.tree.column("publisher", width=150) self.tree.column("stock", width=70, anchor=tk.CENTER) self.tree.column("current", width=70, anchor=tk.CENTER) self.tree.column("location", width=100) scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5) self.tree.bind("<Double-1>", self.edit_book) def load_categories(self): sql = "SELECT id, name FROM categories" cats = self.db.execute_query(sql) names = ["全部"] + [c['name'] for c in cats] self.category_combo['values'] = names self.category_combo.current(0) self.cat_map = {c['name']: c['id'] for c in cats} def load_books(self, keyword="", cat_id=None): # 清空现有数据 for item in self.tree.get_children(): self.tree.delete(item) sql = "SELECT id, isbn, title, author, publisher, total_stock, current_stock, location FROM books WHERE 1=1" params = [] if keyword: sql += " AND (title LIKE %s OR author LIKE %s OR isbn LIKE %s)" kw = f"%{keyword}%" params.extend([kw, kw, kw]) if cat_id: sql += " AND category_id = %s" params.append(cat_id) sql += " ORDER BY id DESC" books = self.db.execute_query(sql, tuple(params)) for b in books: self.tree.insert("", tk.END, values=( b['id'], b['isbn'], b['title'], b['author'], b['publisher'], b['total_stock'], b['current_stock'], b['location'] )) def search_books(self, event=None): keyword = self.search_entry.get().strip() cat_name = self.category_combo.get() cat_id = self.cat_map.get(cat_name) if cat_name != "全部" else None self.load_books(keyword, cat_id) def add_book(self): # 弹出新增对话框,这里简化 from dialogs.book_dialog import BookDialog BookDialog(self, "新增图书", None, self.on_book_saved) def edit_book(self, event=None): selected = self.tree.selection() if not selected: messagebox.showwarning("提示", "请选择一本图书") return item = self.tree.item(selected[0]) book_id = item['values'][0] from dialogs.book_dialog import BookDialog BookDialog(self, "编辑图书", book_id, self.on_book_saved) def delete_book(self): selected = self.tree.selection() if not selected: messagebox.showwarning("提示", "请选择要删除的图书") return item = self.tree.item(selected[0]) book_id = item['values'][0] title = item['values'][2] if messagebox.askyesno("确认删除", f"确定要删除《{title}》吗?\n此操作不可恢复!"): try: sql = "DELETE FROM books WHERE id = %s" self.db.execute_update(sql, (book_id,)) self.load_books() messagebox.showinfo("成功", "图书已删除") except Exception as e: messagebox.showerror("错误", f"删除失败:{str(e)}") def on_book_saved(self): self.load_books()5. 借还书页面核心逻辑 (views/borrow_return.py)
import tkinter as tk from tkinter import ttk, messagebox from datetime import datetime, timedelta from database import DatabaseManager class BorrowReturn(tk.Frame): def __init__(self, parent, admin): super().__init__(parent) self.admin = admin self.db = DatabaseManager() self.setup_ui() def setup_ui(self): # 借书区域 borrow_frame = tk.LabelFrame(self, text="借书操作", padx=10, pady=10) borrow_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label(borrow_frame, text="读者证号:").grid(row=0, column=0, sticky=tk.W) self.entry_reader = tk.Entry(borrow_frame, width=25) self.entry_reader.grid(row=0, column=1, padx=5) tk.Button(borrow_frame, text="查询读者", command=self.query_reader).grid(row=0, column=2, padx=5) tk.Label(borrow_frame, text="图书ISBN:").grid(row=1, column=0, sticky=tk.W, pady=10) self.entry_isbn = tk.Entry(borrow_frame, width=25) self.entry_isbn.grid(row=1, column=1, padx=5) tk.Button(borrow_frame, text="查询图书", command=self.query_book).grid(row=1, column=2, padx=5) self.btn_borrow = tk.Button(borrow_frame, text="确认借书", command=self.do_borrow, bg="#2196F3", fg="white", width=15) self.btn_borrow.grid(row=2, column=0, columnspan=3, pady=10) # 显示读者和图书信息 self.info_text = tk.Text(borrow_frame, height=4, width=70, state=tk.DISABLED) self.info_text.grid(row=3, column=0, columnspan=3, pady=5) # 还书区域 return_frame = tk.LabelFrame(self, text="还书操作", padx=10, pady=10) return_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label(return_frame, text="借阅记录ID:").pack(side=tk.LEFT) self.entry_record = tk.Entry(return_frame, width=20) self.entry_record.pack(side=tk.LEFT, padx=5) tk.Button(return_frame, text="查询记录", command=self.query_record).pack(side=tk.LEFT, padx=5) tk.Button(return_frame, text="确认还书", command=self.do_return, bg="#FF9800", fg="white").pack(side=tk.LEFT, padx=20) # 当前借阅列表 list_frame = tk.LabelFrame(self, text="当前借阅记录", padx=10, pady=10) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) columns = ("id", "reader", "book", "borrow_date", "due_date", "status") self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=10) self.tree.heading("id", text="记录ID") self.tree.heading("reader", text="读者") self.tree.heading("book", text="图书") self.tree.heading("borrow_date", text="借书日期") self.tree.heading("due_date", text="应还日期") self.tree.heading("status", text="状态") self.tree.column("id", width=60, anchor=tk.CENTER) self.tree.column("reader", width=100) self.tree.column("book", width=200) self.tree.column("borrow_date", width=130) self.tree.column("due_date", width=110) self.tree.column("status", width=80, anchor=tk.CENTER) self.tree.pack(fill=tk.BOTH, expand=True) self.load_current_borrows() def query_reader(self): reader_no = self.entry_reader.get().strip() if not reader_no: messagebox.showwarning("提示", "请输入读者证号") return sql = "SELECT id, name, current_borrow, max_borrow, status FROM readers WHERE reader_no=%s" result = self.db.execute_query(sql, (reader_no,)) if result: self.current_reader = result[0] self.show_info(f"读者:{result[0]['name']} 已借:{result[0]['current_borrow']}/{result[0]['max_borrow']} 状态:{result[0]['status']}") else: messagebox.showerror("错误", "未找到该读者") def query_book(self): isbn = self.entry_isbn.get().strip() if not isbn: messagebox.showwarning("提示", "请输入ISBN") return sql = "SELECT id, title, author, current_stock FROM books WHERE isbn=%s" result = self.db.execute_query(sql, (isbn,)) if result: self.current_book = result[0] self.show_info(f"图书:《{result[0]['title']}》 可借:{result[0]['current_stock']}本") else: messagebox.showerror("错误", "未找到该书") def do_borrow(self): if not hasattr(self, 'current_reader') or not hasattr(self, 'current_book'): messagebox.showwarning("提示", "请先查询读者和图书") return reader = self.current_reader book = self.current_book # 检查读者状态 if reader['status'] != '正常': messagebox.showerror("错误", "该读者状态异常,无法借书") return if reader['current_borrow'] >= reader['max_borrow']: messagebox.showerror("错误", "该读者已达到最大借阅数量") return if book['current_stock'] <= 0: messagebox.showerror("错误", "该书库存不足") return # 执行借书事务 try: conn = self.db.connect() with conn.cursor() as cursor: # 插入借阅记录 due_date = (datetime.now() + timedelta(days=30)).date() # 默认借期30天 sql_insert = """INSERT INTO borrow_records (book_id, reader_id, due_date, operator_id, status) VALUES (%s, %s, %s, %s, '借出')""" cursor.execute(sql_insert, (book['id'], reader['id'], due_date, self.admin['id'])) # 更新图书库存 sql_update_book = "UPDATE books SET current_stock = current_stock - 1 WHERE id = %s" cursor.execute(sql_update_book, (book['id'],)) # 更新读者当前借阅数 sql_update_reader = "UPDATE readers SET current_borrow = current_borrow + 1 WHERE id = %s" cursor.execute(sql_update_reader, (reader['id'],)) conn.commit() messagebox.showinfo("成功", f"借书成功!应还日期:{due_date}") self.clear_borrow_form() self.load_current_borrows() except Exception as e: conn.rollback() messagebox.showerror("错误", f"借书失败:{str(e)}") def do_return(self): record_id = self.entry_record.get().strip() if not record_id: messagebox.showwarning("提示", "请输入借阅记录ID") return # 查询借阅记录 sql = """SELECT br.*, b.id as book_id, b.title, r.id as reader_id, r.name as reader_name FROM borrow_records br JOIN books b ON br.book_id = b.id JOIN readers r ON br.reader_id = r.id WHERE br.id = %s AND br.status = '借出'""" result = self.db.execute_query(sql, (record_id,)) if not result: messagebox.showerror("错误", "未找到该借阅记录或已归还") return record = result[0] # 计算逾期罚款(假设每本每天0.5元) due = record['due_date'] now = datetime.now().date() overdue_days = (now - due).days fine = max(overdue_days * 0.5, 0) if overdue_days > 0 else 0 msg = f"图书:《{record['title']}》\n读者:{record['reader_name']}\n应还日期:{due}\n逾期天数:{overdue_days if overdue_days>0 else 0}\n罚款金额:{fine:.2f}元" if fine > 0: if not messagebox.askyesno("逾期罚款", f"{msg}\n\n是否确认还书并缴纳罚款?"): return else: messagebox.showinfo("还书确认", msg) # 执行还书事务 try: conn = self.db.connect() with conn.cursor() as cursor: # 更新借阅记录 sql_update_record = """UPDATE borrow_records SET return_date=NOW(), status='已还', fine_amount=%s WHERE id=%s""" cursor.execute(sql_update_record, (fine, record_id)) # 增加图书库存 sql_update_book = "UPDATE books SET current_stock = current_stock + 1 WHERE id = %s" cursor.execute(sql_update_book, (record['book_id'],)) # 减少读者当前借阅数 sql_update_reader = "UPDATE readers SET current_borrow = current_borrow - 1 WHERE id = %s" cursor.execute(sql_update_reader, (record['reader_id'],)) # 如果有罚款,插入罚款记录 if fine > 0: sql_fine = "INSERT INTO fines (record_id, amount, paid) VALUES (%s, %s, 1)" cursor.execute(sql_fine, (record_id, fine)) conn.commit() messagebox.showinfo("成功", f"还书成功!罚款{fine:.2f}元已缴纳。") self.entry_record.delete(0, tk.END) self.load_current_borrows() except Exception as e: conn.rollback() messagebox.showerror("错误", f"还书失败:{str(e)}") def load_current_borrows(self): for item in self.tree.get_children(): self.tree.delete(item) sql = """SELECT br.id, r.name as reader_name, b.title, br.borrow_date, br.due_date, br.status FROM borrow_records br JOIN readers r ON br.reader_id = r.id JOIN books b ON br.book_id = b.id WHERE br.status IN ('借出','逾期') ORDER BY br.borrow_date DESC LIMIT 100""" records = self.db.execute_query(sql) for rec in records: self.tree.insert("", tk.END, values=( rec['id'], rec['reader_name'], rec['title'], rec['borrow_date'].strftime("%Y-%m-%d %H:%M"), rec['due_date'], rec['status'] )) def clear_borrow_form(self): self.entry_reader.delete(0, tk.END) self.entry_isbn.delete(0, tk.END) if hasattr(self, 'current_reader'): del self.current_reader if hasattr(self, 'current_book'): del self.current_book self.info_text.config(state=tk.NORMAL) self.info_text.delete(1.0, tk.END) self.info_text.config(state=tk.DISABLED) def show_info(self, text): self.info_text.config(state=tk.NORMAL) self.info_text.delete(1.0, tk.END) self.info_text.insert(tk.END, text) self.info_text.config(state=tk.DISABLED)📊 统计报表示例 (views/statistics.py)
import tkinter as tk from tkinter import ttk from database import DatabaseManager class Statistics(tk.Frame): def __init__(self, parent, admin): super().__init__(parent) self.admin = admin self.db = DatabaseManager() self.setup_ui() self.load_statistics() def setup_ui(self): notebook = ttk.Notebook(self) notebook.pack(fill=tk.BOTH, expand=True) # 热门图书 tab1 = tk.Frame(notebook) notebook.add(tab1, text="热门图书TOP10") self.popular_tree = ttk.Treeview(tab1, columns=("rank","title","author","count"), show="headings") self.popular_tree.heading("rank", text="排名") self.popular_tree.heading("title", text="书名") self.popular_tree.heading("author", text="作者") self.popular_tree.heading("count", text="借阅次数") self.popular_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 读者排行 tab2 = tk.Frame(notebook) notebook.add(tab2, text="读者借阅排行") self.reader_tree = ttk.Treeview(tab2, columns=("rank","name","count"), show="headings") self.reader_tree.heading("rank", text="排名") self.reader_tree.heading("name", text="读者姓名") self.reader_tree.heading("count", text="借阅次数") self.reader_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 日/月流通量 tab3 = tk.Frame(notebook) notebook.add(tab3, text="流通量统计") self.flow_tree = ttk.Treeview(tab3, columns=("date","borrow","return"), show="headings") self.flow_tree.heading("date", text="日期") self.flow_tree.heading("borrow", text="借书数") self.flow_tree.heading("return", text="还书数") self.flow_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) def load_statistics(self): # 热门图书 sql_popular = """SELECT b.title, b.author, COUNT(br.id) as cnt FROM borrow_records br JOIN books b ON br.book_id = b.id GROUP BY b.id ORDER BY cnt DESC LIMIT 10""" popular = self.db.execute_query(sql_popular) for i, book in enumerate(popular, 1): self.popular_tree.insert("", tk.END, values=(i, book['title'], book['author'], book['cnt'])) # 读者排行 sql_reader = """SELECT r.name, COUNT(br.id) as cnt FROM borrow_records br JOIN readers r ON br.reader_id = r.id GROUP BY r.id ORDER BY cnt DESC LIMIT 10""" readers = self.db.execute_query(sql_reader) for i, reader in enumerate(readers, 1): self.reader_tree.insert("", tk.END, values=(i, reader['name'], reader['cnt'])) # 近7天流通量 sql_flow = """SELECT DATE(borrow_date) as date, SUM(CASE WHEN status IN ('借出','逾期') THEN 1 ELSE 0 END) as borrow_cnt, SUM(CASE WHEN status = '已还' THEN 1 ELSE 0 END) as return_cnt FROM borrow_records WHERE borrow_date >= CURDATE() - INTERVAL 7 DAY GROUP BY DATE(borrow_date) ORDER BY date DESC""" flows = self.db.execute_query(sql_flow) for flow in flows: self.flow_tree.insert("", tk.END, values=(flow['date'], flow['borrow_cnt'], flow['return_cnt']))🎨 UI美化建议
使用ttkbootstrap库可以让界面更加现代化:
pip install ttkbootstrap然后在主窗口中替换ttk为ttkbootstrap:
import ttkbootstrap as tb from ttkbootstrap.constants import * class MainWindow: def __init__(self, admin_info): self.root = tb.Window(themename="flatly") # 主题可选:superhero, darkly, flatly等 ...🚀 打包发布
使用 PyInstaller 打包成 exe:
pip install pyinstaller pyinstaller --onefile --windowed --icon=resources/icon.ico main.py注意:需要将 MySQL 连接配置放在外部 config.ini 文件中,避免硬编码。