Sticky Keys, Toggle Keys and Filter Keys

本文介绍如何通过StickyKeys等特性简化键盘操作。StickyKeys允许用户按下一个修改键(如Shift或Ctrl),在按下第二个键之前保持该键激活状态。文中详细解释了如何启用和禁用此功能。
部署运行你感兴趣的模型镜像
 
Sticky Keys

在快捷方式需要通过一套组合键激活的情况下, StickyKeys特性将允许您按下一个像Shift、Ctrl、Alt或Windows Logo(Windows徽标)这样的修改键, 并在按下另一个键之前保持其处于激活状态。

如需将StickyKeys特性设定为激活状态, 请依次执行下列操作步骤:

  1. 连续按下Shift键五次。随后出现的对话框中将显示有关StickyKeys特性设置方式的操作提示。
  2. 如果您单击OK(确定), 一个图标(或一组方形对象)便会出现在提示框内。

如需将StickyKeys特性关闭, 则请连续按下Shift键五次。

Toggle Keys
典型的有:
Caps Lock:.一次开关可以控制左右字母的大小写状态
Num Lock:可以控制小键盘的数字输入,以及小键盘上方向箭头的使用
Scroll Lock:已经不常用
Filter Keys 
为了帮助按键不方便人士所需要的设置。

您可能感兴趣的与本文相关的镜像

AutoGPT

AutoGPT

AI应用

AutoGPT于2023年3月30日由游戏公司Significant Gravitas Ltd.的创始人Toran Bruce Richards发布,AutoGPT是一个AI agent(智能体),也是开源的应用程序,结合了GPT-4和GPT-3.5技术,给定自然语言的目标,它将尝试通过将其分解成子任务,并在自动循环中使用互联网和其他工具来实现这一目标

import tkinter as tk from tkinter import messagebox, simpledialog, ttk import random import json import os from datetime import datetime, timedelta from pypinyin import lazy_pinyin, Style from threading import Thread import openpyxl from openpyxl.styles import Alignment, Font, PatternFill from openpyxl.utils import get_column_letter class VirtualKeyboard: “”“虚拟键盘类”“” def __init__(self, parent, entry_widget=None): self.parent = parent self.entry = entry_widget self.window = None def show(self, entry_widget=None): if entry_widget: self.entry = entry_widget if self.window and self.window.winfo_exists(): self.window.lift() return self.window = tk.Toplevel(self.parent) self.window.title("虚拟键盘") self.window.geometry("600x250") self.window.resizable(False, False) self.window.attributes('-topmost', True) self.window.configure(bg='#f0f8ff') keys = [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"], ['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'] ] for i, row in enumerate(keys): frame = tk.Frame(self.window, bg='#f0f8ff') frame.pack(pady=2) for key in row: btn = tk.Button( frame, text=key, width=4, height=2, command=lambda k=key: self.insert_key(k), bg='#ffffff', fg='#333333', font=('Arial', 10) ) btn.pack(side=tk.LEFT, padx=1) func_frame = tk.Frame(self.window, bg='#f0f8ff') func_frame.pack(pady=2) tk.Button( func_frame, text="空格", width=8, height=2, command=lambda: self.insert_key(" "), bg='#4CAF50', fg='white' ).pack(side=tk.LEFT, padx=1) tk.Button( func_frame, text="删除", width=8, height=2, command=self.delete_key, bg='#f44336', fg='white' ).pack(side=tk.LEFT, padx=1) tk.Button( func_frame, text="关闭", width=8, height=2, command=self.window.destroy, bg='#607D8B', fg='white' ).pack(side=tk.LEFT, padx=1) def insert_key(self, key): if self.entry: self.entry.insert(tk.INSERT, key) def delete_key(self): if self.entry and self.entry.selection_present(): self.entry.delete(tk.SEL_FIRST, tk.SEL_LAST) else: pos = self.entry.index(tk.INSERT) if pos > 0: self.entry.delete(pos - 1) class ClassManager: “”“班级积分管理系统主类”“” def __init__(self, root): self.root = root self.student_frames = {} self.root.title("13班积分管理系统") self.root.geometry("1200x800") self.root.resizable(False, False) self.root.configure(bg='#f0f8ff') # DPI 感知(仅 Windows) try: import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1) except: pass # 字体设置 self.title_font = ("微软雅黑", 20, "bold") self.button_font = ("微软雅黑", 10) self.text_font = ("微软雅黑", 9) self.small_font = ("微软雅黑", 8) # 颜色常量 self.BG_NORMAL = "white" self.BG_SELECTED = "#e3f2fd" self.COLOR_POSITIVE = "#4CAF50" self.COLOR_NEGATIVE = "#f44336" self.COLOR_NEUTRAL = "#333" self.selected_students = set() # ======== 排序方式:默认按积分 ======== self.sort_mode = "score" # 可选: "score", "name" # 连续操作模式标志位 self.continuous_mode = False self.continuous_var = tk.BooleanVar(value=self.continuous_mode) # 加载用户设置 self.load_settings() # 登录验证 if not self.check_login_password(): self.root.destroy() return # 初始化数据 self.students = self.load_data() if not self.students: self.initialize_from_roster() self.admin_password = self.load_admin_password() # 构建拼音缓存 self.pinyin_cache = {} self.build_pinyin_cache() # 小组管理 self.groups = self.load_groups() # 创建界面 self.create_widgets() self.update_display() # 绑定窗口大小变化事件(防抖) self.resize_after_id = None self.root.bind("<Configure>", self.on_window_resize) # 拖动变量 self.mini_window = None self.mini_drag_data = {"x": 0, "y": 0} # 上次保存时间(节流) self.last_save_time = 0 # 协议关闭 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) def build_pinyin_cache(self): """构建姓名到拼音首字母和全拼的缓存""" for name in self.students: if not name: continue initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} def check_login_password(self): login_password = self.load_login_password() if not login_password: return self.setup_login_password() attempts = 3 while attempts > 0: password = self.ask_password("登录验证", "请输入登录密码:") if password == login_password: return True attempts -= 1 msg = f"密码错误,还剩{attempts}次机会" if attempts > 0 else "密码错误次数过多" messagebox.showerror("密码错误", msg, parent=self.root) if attempts == 0: return False return False def setup_login_password(self): password = self.ask_password("设置登录密码", "请设置登录密码:") if not password: return False confirm = self.ask_password("确认密码", "请再次输入:") if password != confirm: messagebox.showerror("错误", "两次输入不一致") return False self.save_login_password(password) messagebox.showinfo("成功", "登录密码设置成功") return True def load_login_password(self): try: if os.path.exists("login_password.json"): with open("login_password.json", "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print("加载登录密码失败:", e) return None def save_login_password(self, pwd): with open("login_password.json", "w", encoding="utf-8") as f: json.dump(pwd, f, ensure_ascii=False) def on_closing(self): if messagebox.askokcancel("退出", "确定要退出吗?"): self.save_data_async() self.save_settings() # 保存用户设置 if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.root.destroy() def load_admin_password(self): try: if os.path.exists("admin_password.json"): with open("admin_password.json", "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print("加载管理密码失败:", e) return "12" def save_admin_password(self): with open("admin_password.json", "w", encoding="utf-8") as f: json.dump(self.admin_password, f, ensure_ascii=False) def change_login_password(self): old = self.ask_password("验证原密码", "请输入原密码:") current = self.load_login_password() if old != current: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新密码", "请输入新密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码修改成功!") def change_admin_password(self): old = self.ask_password("验证原密码", "请输入管理密码:") if old != self.admin_password: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新管理密码", "请输入两位数字:") if not new or len(new) != 2 or not new.isdigit(): messagebox.showerror("错误", "必须是两位数字!") return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.admin_password = new self.save_admin_password() messagebox.showinfo("成功", "管理密码修改成功!") def forget_login_password(self): admin = self.ask_password("验证管理密码", "请输入管理密码:") if admin != self.admin_password: messagebox.showerror("错误", "管理密码错误!") return new = self.ask_password("重置登录密码", "请输入新登录密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码已重置!") def ask_password(self, title, prompt): dialog = tk.Toplevel(self.root) dialog.title(title) dialog.geometry("400x200") dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text=prompt, font=self.text_font, bg='#f0f8ff').pack(pady=20) pwd_var = tk.StringVar() entry = tk.Entry(dialog, textvariable=pwd_var, show='*', font=self.text_font, width=20) entry.pack(pady=10) # 绑定虚拟键盘 entry.bind("<Button-1>", lambda e: VirtualKeyboard(self.root, entry).show()) result = [] def on_ok(): result.append(pwd_var.get()) dialog.destroy() def on_cancel(): result.append(None) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button(btn_frame, text="确定", command=on_ok, bg='#4CAF50', fg='white', width=10).pack(side=tk.LEFT, padx=10) tk.Button(btn_frame, text="取消", command=on_cancel, bg='#f44336', fg='white', width=10).pack(side=tk.LEFT, padx=10) entry.focus_set() dialog.wait_window() return result[0] if result else None def initialize_from_roster(self): roster = [ "安琪儿", "白睦尧", "白沛妍", "鲍星焱", "边子轩", "常佳怡", "钞依冉", "崔浩然", "崔嘉诺", "高博程", "高方圆", "高海翔", "高瑞泽", "高硕", "高雅萍", "郭欣宇", "韩静舒", "贺佳萱", "贺予绘", "黄钰涵", "李佳倩", "李金格", "李俊辰", "梁家伊", "刘博宇", "刘锐宇", "刘思乔", "刘旭尧", "刘雅涵", "柳登程", "马涛涛", "孟家旭", "苗宇辰", "裴雨轩", "拓雨诺", "王嘉浩", "王婷雨", "王怡婷", "王梓", "谢孟芝", "徐茂瑞", "杨少轩", "杨雪琦", "尤宇琪", "尤子潇", "张皓茗", "张皓雅", "张家瑜", "张朴淳", "张瑞霞", "张雅琪", "赵雅彤", "赵勇杰", "郑梦茹" ] for name in roster: self.students[name] = {"score": 0, "history": []} self.save_data_throttled() def load_data(self): try: if os.path.exists("class_data.json"): with open("class_data.json", "r", encoding="utf-8") as f: data = json.load(f) # 兼容旧格式:{name: score} for k, v in data.items(): if isinstance(v, int): data[k] = {"score": v, "history": []} return data except Exception as e: print("加载数据失败:", e) return {} def save_data_throttled(self): now = datetime.now().timestamp() if hasattr(self, 'last_save_time') and now - self.last_save_time < 2: return self.save_data_async() self.last_save_time = now def save_data_async(self): def _save(): try: with open("class_data.json", "w", encoding="utf-8") as f: json.dump(self.students, f, ensure_ascii=False, indent=2) except Exception as e: print("异步保存失败:", e) Thread(target=_save, daemon=True).start() def load_groups(self): """加载学生分组信息""" try: if os.path.exists("groups.json"): with open("groups.json", "r", encoding="utf-8") as f: data = json.load(f) # 验证数据格式 for group_name, members in data.items(): if not isinstance(members, list): continue for name in members[:]: if name not in self.students: members.remove(name) return data except Exception as e: print("加载小组失败:", e) # 默认创建几个小组 return {} def save_groups(self): """异步保存小组信息""" def _save(): try: with open("groups.json", "w", encoding="utf-8") as f: json.dump(self.groups, f, ensure_ascii=False, indent=2) except Exception as e: print("保存小组失败:", e) Thread(target=_save, daemon=True).start() def add_student(self): name = simpledialog.askstring("添加学生", "请输入学生姓名:", parent=self.root) if not name: return if name in self.students: messagebox.showwarning("重复", f"学生 {name} 已存在!") return self.students[name] = {"score": 0, "history": []} initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"已添加学生:{name}") def show_add_points_menu(self): self.adjust_points_with_buttons("加分", 1) def show_subtract_points_menu(self): self.adjust_points_with_buttons("减分", -1) def adjust_points_with_buttons(self, action, multiplier): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return dialog = tk.Toplevel(self.root) dialog.title(action) dialog.geometry("440x220") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label( dialog, text=f"请选择{action}分数:", font=self.text_font, bg='#f0f8ff' ).pack(pady=(15, 5)) button_frame = tk.Frame(dialog, bg='#f0f8ff') button_frame.pack(pady=10) values = [1, 2, 3, 4, 5, 10] colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#607D8B', '#795548'] texts = [f"{v}分" for v in values] for i in range(len(values)): btn = tk.Button( button_frame, text=texts[i], font=self.button_font, width=8, height=2, bg=colors[i], fg='white', command=lambda v=values[i]: self.do_adjust_points(dialog, v * multiplier) ) btn.pack(side=tk.LEFT, padx=8) custom_frame = tk.Frame(dialog, bg='#f0f8ff') custom_frame.pack(pady=8) tk.Button( custom_frame, text="自定义", font=self.button_font, width=8, height=2, bg='#607D8B', fg='white', command=lambda: self.do_adjust_points_custom(dialog, multiplier) ).pack() tk.Button( dialog, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(pady=10) dialog.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (dialog.winfo_height() // 2) dialog.geometry(f"+{x}+{y}") dialog.focus_force() def do_adjust_points(self, dialog, change): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.selected_students: self.students[name]["score"] += change self.students[name]["history"].append({ "time": now, "change": change, "type": "add" if change > 0 else "sub" }) if not self.continuous_mode: self.selected_students.clear() dialog.destroy() self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"{change:+} 分操作完成!", parent=self.root) def do_adjust_points_custom(self, dialog, multiplier): value = simpledialog.askinteger("自定义", "请输入分数:", parent=dialog, minvalue=1, maxvalue=100) if value is not None: self.do_adjust_points(dialog, value * multiplier) def random_select_multi(self): dialog = tk.Toplevel(self.root) dialog.title("随机抽选") dialog.geometry("320x180") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text="请选择抽取人数:", font=self.text_font, bg='#f0f8ff').pack(pady=10) ctrl_frame = tk.Frame(dialog, bg='#f0f8ff') ctrl_frame.pack(pady=10) count_var = tk.IntVar(value=1) def decrease(): if count_var.get() > 1: count_var.set(count_var.get() - 1) def increase(): count_var.set(count_var.get() + 1) tk.Button(ctrl_frame, text="−", font=("Arial", 14), width=3, command=decrease, bg="#f44336", fg="white").pack(side=tk.LEFT) num_label = tk.Label( ctrl_frame, textvariable=count_var, font=("Arial", 16, "bold"), width=5, relief=tk.SUNKEN, bg="white" ) num_label.pack(side=tk.LEFT, padx=10) tk.Button(ctrl_frame, text="+", font=("Arial", 14), width=3, command=increase, bg="#4CAF50", fg="white").pack(side=tk.LEFT) def do_select(): n = count_var.get() total = len(self.students) if n > total: messagebox.showwarning("提示", f"学生总数只有 {total} 人,无法抽取 {n} 人!", parent=dialog) return candidates = list(self.students.keys()) selected = random.sample(candidates, n) self.selected_students.clear() for name in selected: self.selected_students.add(name) self.update_display() result = "\n".join([f"第{i + 1}位:{name}" for i, name in enumerate(selected)]) messagebox.showinfo(f"抽中 {n} 人", f"🎉 抽中名单:\n\n{result}", parent=dialog) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button( btn_frame, text="确定抽取", font=self.button_font, width=10, bg='#4CAF50', fg='white', command=do_select ).pack(side=tk.LEFT, padx=10) tk.Button( btn_frame, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(side=tk.LEFT, padx=10) dialog.focus_force() def show_history(self): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return selected_names = list(self.selected_students) if len(selected_names) == 1: name = selected_names[0] history = self.students[name]["history"] if not history: messagebox.showinfo("历史记录", f"{name} 暂无积分变动。", parent=self.root) return hist_text = "\n".join([f"[{r['time']}] {'+' if r['change'] > 0 else ''}{r['change']}" for r in history]) detail_win = tk.Toplevel(self.root) detail_win.title(f"{name} 的积分历史") detail_win.geometry("400x300") text = tk.Text(detail_win, font=self.text_font, wrap=tk.WORD) text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) text.insert(tk.END, hist_text) text.config(state=tk.DISABLED) return # 多个学生:使用 ttk.Notebook 标签页 self.show_multi_history_tabs(selected_names) def show_multi_history_tabs(self, names): """显示多个学生的积分历史(标签页形式)""" win = tk.Toplevel(self.root) win.title(f"多名学生积分历史(共{len(names)}人)") win.geometry("800x500") win.configure(bg='#f0f8ff') notebook = ttk.Notebook(win) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) total_changes = [] for name in names: data = self.students[name] history = data["history"] tab = tk.Frame(notebook, bg='white') notebook.add(tab, text=name[:6]) if not history: lbl = tk.Label(tab, text="暂无积分变动", font=self.text_font, fg="#999", bg="white") lbl.pack(pady=20) continue net_change = sum(r["change"] for r in history) total_changes.append((name, net_change)) text = tk.Text(tab, font=self.text_font, wrap=tk.WORD, height=15) scrollbar = tk.Scrollbar(tab, orient=tk.VERTICAL, command=text.yview) text.configure(yscrollcommand=scrollbar.set) for record in history: change = record["change"] sign = "+" if change > 0 else "" line = f"[{record['time']}] {sign}{change}\n" text.insert(tk.END, line, ("positive" if change > 0 else "negative")) text.tag_configure("positive", foreground="#4CAF50") text.tag_configure("negative", foreground="#f44336") text.config(state=tk.DISABLED, bg="white") text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) stat_frame = tk.Frame(win, bg='#f0f8ff') stat_frame.pack(fill=tk.X, padx=10, pady=5) total_net = sum(c[1] for c in total_changes) stat_label = tk.Label( stat_frame, text=f"共 {len(names)} 人 | 总净变化: {total_net:+}", font=("微软雅黑", 10, "bold"), bg='#f0f8ff', fg="#333" ) stat_label.pack() def sort_by_change(): sorted_names = [n for n, _ in sorted(total_changes, key=lambda x: x[1], reverse=True)] for i in range(len(notebook.tabs()) - 1, -1, -1): notebook.forget(i) for name in sorted_names: if name in self.selected_students: data = self.students[name] tab = tk.Frame(notebook, bg='white') text = tk.Text(tab, font=self.text_font, wrap=tk.WORD) scrollbar = tk.Scrollbar(tab, command=text.yview) text.configure(yscrollcommand=scrollbar.set) for r in data["history"]: sign = "+" if r["change"] > 0 else "" text.insert(tk.END, f"[{r['time']}] {sign}{r['change']}\n") text.config(state=tk.DISABLED, bg="white") text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) notebook.add(tab, text=name[:6]) tk.Button( stat_frame, text="按进步排序", font=self.small_font, command=sort_by_change, bg="#FF9800", fg="white" ).pack(side=tk.RIGHT) win.focus_force() def select_all(self): self.selected_students = set(self.students.keys()) self.update_display() def deselect_all(self): self.selected_students.clear() self.update_display() def reset_points(self): if messagebox.askyesno("确认", "确定要重置所有学生的积分为0吗?", parent=self.root): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.students: old_score = self.students[name]["score"] if old_score != 0: self.students[name]["history"].append({ "time": now, "change": -old_score, "type": "reset" }) self.students[name]["score"] = 0 self.save_data_throttled() self.update_display() messagebox.showinfo("成功", "所有积分已重置为0", parent=self.root) def clear_all_history(self): if messagebox.askyesno("确认", "确定要清除所有历史记录吗?", parent=self.root): for name in self.students: self.students[name]["history"] = [] self.save_data_throttled() messagebox.showinfo("成功", "所有历史记录已清除", parent=self.root) def manage_groups(self): """打开小组管理界面""" dialog = tk.Toplevel(self.root) dialog.title("管理小组") dialog.geometry("700x500") dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text="小组管理", font=self.title_font, bg='#f0f8ff').pack(pady=10) main_frame = tk.Frame(dialog, bg='#f0f8ff') main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) left = tk.Frame(main_frame, bg='#f0f8ff') left.pack(side=tk.LEFT, fill=tk.Y, padx=5) right = tk.Frame(main_frame, bg='#f0f8ff') right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5) tk.Label(left, text="小组", font=self.text_font, bg='#f0f8ff').pack(anchor=tk.W) group_listbox = tk.Listbox(left, font=self.text_font, width=15, height=15) g_scroll = tk.Scrollbar(left, command=group_listbox.yview) g_scroll.pack(side=tk.RIGHT, fill=tk.Y) group_listbox.config(yscrollcommand=g_scroll.set) for g in self.groups: group_listbox.insert(tk.END, g) tk.Label(right, text="成员", font=self.text_font, bg='#f0f8ff').pack(anchor=tk.W) member_listbox = tk.Listbox(right, font=self.text_font, selectmode=tk.MULTIPLE, height=15) m_scroll = tk.Scrollbar(right, command=member_listbox.yview) m_scroll.pack(side=tk.RIGHT, fill=tk.Y) member_listbox.config(yscrollcommand=m_scroll.set) member_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) def update_members(*args): selection = group_listbox.curselection() if not selection: return idx = selection[0] group_name = group_listbox.get(idx) member_listbox.delete(0, tk.END) for name in self.students: if name in self.groups.get(group_name, []): member_listbox.insert(tk.END, name) group_listbox.bind("<<ListboxSelect>>", update_members) btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=10) def add_group(): name = simpledialog.askstring("新小组", "请输入小组名称:", parent=dialog) if name and name not in self.groups: self.groups[name] = [] group_listbox.insert(tk.END, name) def del_group(): sel = group_listbox.curselection() if not sel: return name = group_listbox.get(sel[0]) if messagebox.askyesno("确认", f"删除小组 {name}?", parent=dialog): del self.groups[name] group_listbox.delete(sel[0]) def save_members(): sel = group_listbox.curselection() if not sel: return group_name = group_listbox.get(sel[0]) all_selected = [member_listbox.get(i) for i in member_listbox.curselection()] self.groups[group_name] = all_selected self.save_groups() messagebox.showinfo("成功", "已保存成员", parent=dialog) tk.Button(btn_frame, text="新建小组", command=add_group, bg="#4CAF50", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="删除小组", command=del_group, bg="#f44336", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="保存成员", command=save_members, bg="#2196F3", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="关闭", command=dialog.destroy, bg="#607D8B", fg="white").pack(side=tk.LEFT, padx=5) if self.groups: group_listbox.selection_set(0) update_members() def filter_by_category(self, category_type, value): """ 按分类筛选学生 :param category_type: 'all', 'surname', 'group' :param value: 对应值 """ if category_type == "all": filtered = self.students elif category_type == "surname": filtered = {name: data for name, data in self.students.items() if name.startswith(value)} elif category_type == "group": member_names = self.groups.get(value, []) filtered = {name: self.students[name] for name in member_names if name in self.students} else: filtered = self.students desc = f"【{value}】" if value else "全部" self.display_filtered(filtered, f"{desc} 学生") def create_widgets(self): top_frame = tk.Frame(self.root, bg='#3b5998', height=50) top_frame.pack(fill=tk.X) top_frame.pack_propagate(False) tk.Label(top_frame, text="13班积分管理系统", font=self.title_font, fg='white', bg='#3b5998').pack(side=tk.LEFT, padx=20, pady=8) tk.Button(top_frame, text="缩小", command=self.minimize_window, font=self.button_font, bg='#607D8B', fg='white').pack(side=tk.RIGHT, padx=10) self.manage_btn = tk.Button( top_frame, text="管理", command=self.show_management_menu, font=self.button_font, bg='#E91E63', fg='white' ) self.manage_btn.pack(side=tk.RIGHT, padx=10) button_frame = tk.Frame(self.root, bg='#f0f8ff') button_frame.pack(pady=5, fill=tk.X, padx=10) buttons = [ ("添加学生", self.add_student, '#4CAF50'), ("加分", self.show_add_points_menu, '#2196F3'), ("减分", self.show_subtract_points_menu, '#f44336'), ("随机抽选", self.random_select_multi, '#9C27B0'), ("积分历史", self.show_history, '#FF9800'), ("全选", self.select_all, '#607D8B'), ("取消全选", self.deselect_all, '#795548'), ("生成周报", self.generate_weekly_report, '#009688'), ] for i, (text, cmd, color) in enumerate(buttons): tk.Button( button_frame, text=text, command=cmd, width=10, height=1, font=self.button_font, bg=color, fg='white' ).grid(row=0, column=i, padx=4, pady=2) display_frame = tk.Frame(self.root, bg='#f0f8ff') display_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=5) category_frame = tk.Frame(display_frame, bg='#f0f8ff') category_frame.pack(fill=tk.X, pady=(0, 3)) tk.Label(category_frame, text="分类:", font=self.small_font, bg='#f0f8ff').pack(side=tk.LEFT) surnames = sorted(set(name[0] for name in self.students.keys())) btn_frame = tk.Frame(category_frame, bg='#f0f8ff') btn_frame.pack(side=tk.LEFT) tk.Button(btn_frame, text="全部", font=("微软雅黑", 8), width=3, command=lambda: self.filter_by_category("all", None), bg="#4CAF50", fg="white").pack(side=tk.LEFT, padx=1) for s in surnames: tk.Button(btn_frame, text=s, font=("微软雅黑", 8), width=2, command=lambda x=s: self.filter_by_category("surname", x), bg="#e0e0e0").pack(side=tk.LEFT, padx=1) for group_name in self.groups: tk.Button(btn_frame, text=group_name, font=("微软雅黑", 8), width=4, command=lambda x=group_name: self.filter_by_category("group", x), bg="#2196F3", fg="white").pack(side=tk.LEFT, padx=1) tk.Button(btn_frame, text="管理小组", font=("微软雅黑", 8), width=6, command=self.manage_groups, bg="#E91E63", fg="white").pack(side=tk.LEFT, padx=1) search_frame = tk.Frame(display_frame, bg='#f0f8ff') search_frame.pack(fill=tk.X, pady=(0, 5)) tk.Label(search_frame, text="搜索:", font=self.text_font, bg='#f0f8ff').pack(side=tk.LEFT) # === 排序方式选择 === sort_frame = tk.Frame(display_frame, bg='#f0f8ff') sort_frame.pack(fill=tk.X, pady=(0, 5)) tk.Label(sort_frame, text="排序:", font=self.small_font, bg='#f0f8ff').pack(side=tk.LEFT) self.sort_var = tk.StringVar(value="按积分排序" if self.sort_mode == "score" else "按姓名排序") sort_combo = ttk.Combobox( sort_frame, textvariable=self.sort_var, values=["按积分排序", "按姓名排序"], state="readonly", width=10, font=self.small_font ) sort_combo.pack(side=tk.LEFT, padx=5) sort_combo.bind("<<ComboboxSelected>>", self.on_sort_change) # 提示标签 tk.Label(sort_frame, text="💡 双击学生可选中", font=self.small_font, fg="#666", bg='#f0f8ff').pack(side=tk.RIGHT) # 搜索框 self.search_var = tk.StringVar() self.search_var.trace("w", self.on_search_change) search_entry = tk.Entry(search_frame, textvariable=self.search_var, font=self.text_font, width=30) search_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) search_entry.bind("<Button-1>", lambda e: VirtualKeyboard(self.root, search_entry).show()) container = tk.Frame(display_frame, bg='#f0f8ff') container.pack(fill=tk.BOTH, expand=True) self.canvas = tk.Canvas(container, bg='#f0f8ff', highlightthickness=0) scrollbar = tk.Scrollbar(container, orient="vertical", command=self.canvas.yview) self.scrollable_frame = tk.Frame(self.canvas, bg='#f0f8ff') self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=scrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") status_bar = tk.Label( self.root, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W, height=2 ) status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=0, pady=0) def on_window_resize(self, event): if event.widget != self.root: return if self.resize_after_id: self.root.after_cancel(self.resize_after_id) self.resize_after_id = self.root.after(50, self.check_and_update_display) def check_and_update_display(self): try: current_width = self.canvas.winfo_width() if hasattr(self, '_last_canvas_width') and self._last_canvas_width == current_width: return self._last_canvas_width = current_width self.update_display() except tk.TclError: pass def on_search_change(self, *args): query = self.search_var.get().strip().lower() if not query: self.update_display() return filtered = {} for name, data in self.students.items(): info = self.pinyin_cache.get(name, {}) if (query in name.lower() or query in info.get('full_pinyin', '') or query == info.get('initials', '') or all(c in info.get('initials', '') for c in query)): filtered[name] = data self.display_filtered(filtered, f"匹配“{query}”的结果") def display_filtered(self, students_dict, desc="结果"): for widget in self.scrollable_frame.winfo_children(): widget.destroy() if not students_dict: lbl = tk.Label(self.scrollable_frame, text="无匹配学生", font=self.text_font, fg="#999") lbl.pack(pady=20) return items = list(students_dict.items()) if self.sort_mode == "score": # 按积分降序 sorted_items = sorted(items, key=lambda x: x[1]["score"], reverse=True) else: # self.sort_mode == "name" # 按姓名拼音升序 sorted_items = sorted(items, key=lambda x: self.pinyin_cache.get(x[0], {}).get('full_pinyin', ''), reverse=False) self._create_blocks(sorted_items) def _create_blocks(self, student_list): if not student_list: return try: width = max(self.canvas.winfo_width(), 800) except tk.TclError: return blocks_per_row = 9 block_width = 105 block_height = 80 new_frames = {} for i, (name, data) in enumerate(student_list): row, col = divmod(i, blocks_per_row) is_selected = name in self.selected_students bg_color = self.BG_SELECTED if is_selected else self.BG_NORMAL highlight_color = "#1976D2" if is_selected else "gray" highlight_thickness = 2 if is_selected else 1 frame = None if name in self.student_frames: try: if self.student_frames[name].winfo_exists(): frame = self.student_frames[name] except tk.TclError: pass if frame is not None: frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.config(bg=bg_color, highlightbackground=highlight_color, highlightthickness=highlight_thickness) for child in frame.winfo_children(): child.config(bg=bg_color) else: frame = tk.Frame( self.scrollable_frame, width=block_width, height=block_height, relief=tk.RAISED, borderwidth=1, bg=bg_color, highlightbackground=highlight_color, highlightthickness=highlight_thickness ) frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.grid_propagate(False) tk.Label(frame, text=f"{i + 1}", font=self.small_font, bg=bg_color, fg='#666').place(x=2, y=1) tk.Label(frame, text=name, font=self.text_font, bg=bg_color, wraplength=90, fg='#333').place(relx=0.5, y=22, anchor=tk.CENTER) score = data["score"] score_color = self.COLOR_POSITIVE if score > 0 else self.COLOR_NEGATIVE if score < 0 else self.COLOR_NEUTRAL score_text = f"+{score}" if score > 0 else str(score) tk.Label(frame, text=score_text, font=("微软雅黑", 11, "bold"), bg=bg_color, fg=score_color).place(relx=0.5, y=52, anchor=tk.CENTER) frame.bind("<Button-1>", self.make_click_handler(name)) for child in frame.winfo_children(): child.bind("<Button-1>", lambda e, f=frame: f.event_generate("<Button-1>", x=e.x, y=e.y)) child.bind("<ButtonRelease-1>", lambda e: "break") child.configure(cursor="") new_frames[name] = frame for name, old_frame in self.student_frames.items(): if name not in new_frames: try: old_frame.destroy() except tk.TclError: pass self.student_frames = new_frames self.scrollable_frame.update_idletasks() self.canvas.config(scrollregion=self.canvas.bbox("all")) def make_click_handler(self, name): def handler(event): widget = event.widget if not widget.winfo_exists(): return is_selected = name in self.selected_students new_state = not is_selected if new_state: self.selected_students.add(name) else: self.selected_students.discard(name) frame = self.student_frames.get(name) if frame and frame.winfo_exists(): bg_color = self.BG_SELECTED if new_state else self.BG_NORMAL highlight = "#1976D2" if new_state else "gray" thickness = 2 if new_state else 1 frame.config(bg=bg_color, highlightbackground=highlight, highlightthickness=thickness) for child in frame.winfo_children(): child.config(bg=bg_color) self.save_data_throttled() return handler def update_display(self): self.display_filtered(self.students, "全部学生") def show_management_menu(self): menu = tk.Menu(self.root, tearoff=0, font=self.text_font) menu.add_command(label="修改登录密码", command=self.change_login_password) menu.add_command(label="修改管理密码", command=self.change_admin_password) menu.add_separator() menu.add_command(label="忘记登录密码", command=self.forget_login_password) menu.add_separator() menu.add_command(label="重置积分", command=self.reset_points) menu.add_command(label="清除全部历史", command=self.clear_all_history) menu.add_separator() menu.add_checkbutton( label="连续操作模式", variable=self.continuous_var, command=self.toggle_continuous_mode, font=self.text_font ) x = self.manage_btn.winfo_rootx() y = self.manage_btn.winfo_rooty() + self.manage_btn.winfo_height() menu.post(x, y) def toggle_continuous_mode(self): self.continuous_mode = self.continuous_var.get() def minimize_window(self): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.lift() return self.mini_window = tk.Toplevel(self.root) self.mini_window.title("13班") self.mini_window.geometry("100x40") self.mini_window.overrideredirect(True) self.mini_window.attributes('-topmost', True) screen_width = self.mini_window.winfo_screenwidth() self.mini_window.geometry(f"+{screen_width - 110}+10") self.mini_window.configure(bg='#3b5998') label = tk.Label( self.mini_window, text="班级管理", font=("微软雅黑", 12, "bold"), fg='white', bg='#3b5998', cursor="hand2" ) label.pack(fill=tk.BOTH, expand=True) label.bind("<ButtonPress-1>", self.start_mini_drag) label.bind("<B1-Motion>", self.on_mini_drag) label.bind("<Double-Button-1>", self.restore_window) label.bind("<Enter>", lambda e: label.config(bg='#4a69a0')) label.bind("<Leave>", lambda e: label.config(bg='#3b5998')) self.root.withdraw() def start_mini_drag(self, event): self.mini_drag_data["x"] = event.x self.mini_drag_data["y"] = event.y def on_mini_drag(self, event): x = self.mini_window.winfo_x() + (event.x - self.mini_drag_data["x"]) y = self.mini_window.winfo_y() + (event.y - self.mini_drag_data["y"]) self.mini_window.geometry(f"+{x}+{y}") def restore_window(self, event=None): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.mini_window = None self.root.deiconify() self.root.lift() def generate_weekly_report(self): week_ago = datetime.now() - timedelta(days=7) report_win = tk.Toplevel(self.root) report_win.title("积分周报") report_win.geometry("900x600") report_win.configure(bg='#f0f8ff') tk.Label(report_win, text="积分周报(本周变化总览)", font=self.title_font, bg='#f0f8ff').pack(pady=10) tree_frame = tk.Frame(report_win) tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) scroll = ttk.Scrollbar(tree_frame) scroll.pack(side=tk.RIGHT, fill=tk.Y) columns = ("name", "weekly_change", "changes") tree = ttk.Treeview(tree_frame, columns=columns, show="headings", yscrollcommand=scroll.set) tree.heading("name", text="姓名") tree.heading("weekly_change", text="净变化") tree.heading("changes", text="变动详情") tree.column("name", width=100, anchor=tk.CENTER) tree.column("weekly_change", width=80, anchor=tk.CENTER) tree.column("changes", width=400, anchor=tk.W) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.config(command=tree.yview) data = {} for name, student in self.students.items(): changes = [ r for r in student["history"] if datetime.strptime(r["time"], "%Y-%m-%d %H:%M") >= week_ago ] values = [f"{'+' if c['change'] > 0 else ''}{c['change']}" for c in changes] summary = " ".join(values) + " 分" if values else "无变动" net = sum(c["change"] for c in changes) data[name] = {"summary": summary, "net": net} for name in sorted(data, key=lambda x: data[x]["net"], reverse=True): info = data[name] tag = ("positive",) if info["net"] > 0 else ("negative",) if info["net"] < 0 else () tree.insert("", tk.END, values=(name, info["net"], info["summary"]), tags=tag) tree.tag_configure("positive", foreground=self.COLOR_POSITIVE) tree.tag_configure("negative", foreground=self.COLOR_NEGATIVE) btn_frame = tk.Frame(report_win, bg='#f0f8ff') btn_frame.pack(pady=10) tk.Button( btn_frame, text="导出 TXT", command=lambda: self.export_weekly_report_txt(data), bg='#4CAF50', fg='white', font=self.button_font, width=10 ).pack(side=tk.LEFT, padx=5) tk.Button( btn_frame, text="导出 Excel", command=lambda: self.export_weekly_report_excel(data), bg='#FF9800', fg='white', font=self.button_font, width=10 ).pack(side=tk.LEFT, padx=5) def export_weekly_report_txt(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.txt" with open(filename, "w", encoding="utf-8") as f: f.write("13班积分周报\n\n") for name, d in sorted(data.items(), key=lambda x: x[1]["net"], reverse=True): f.write(f"{name}: {d['summary']} (净变化: {d['net']})\n") messagebox.showinfo("成功", f"已导出至 {filename}") except Exception as e: messagebox.showerror("错误", str(e)) def export_weekly_report_excel(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" wb = openpyxl.Workbook() ws = wb.active ws.title = "积分周报" cell = ws.cell(1, 1, "13班积分周报") cell.font = Font(size=16, bold=True, color="1a5fb4") ws.merge_cells('A1:C1') headers = ["姓名", "净变化", "详细变动"] for col_idx, header in enumerate(headers, 1): cell = ws.cell(2, col_idx, header) cell.font = Font(bold=True) cell.fill = PatternFill(start_color="d0ebff", end_color="d0ebff", fill_type="solid") ws.column_dimensions[get_column_letter(col_idx)].width = [12, 10, 50][col_idx - 1] for idx, (name, d) in enumerate(sorted(data.items(), key=lambda x: x[1]["net"], reverse=True), start=3): ws.cell(idx, 1, name) net_cell = ws.cell(idx, 2, d["net"]) net_cell.font = Font(color="006400" if d["net"] > 0 else "DC143C" if d["net"] < 0 else "000000") ws.cell(idx, 3, d["summary"]) for row in ws.iter_rows(min_row=2, max_row=len(data) + 2, min_col=1, max_col=3): for cell in row: cell.alignment = Alignment(vertical="center", wrap_text=True) wb.save(filename) messagebox.showinfo("成功", f"Excel 已导出至:\n{filename}") except Exception as e: messagebox.showerror("导出失败", f"无法创建文件:\n{str(e)}") def save_settings(self): settings = { "continuous_mode": self.continuous_mode, "sort_mode": self.sort_mode } try: with open("settings.json", "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) except Exception as e: print("保存设置失败:", e) def load_settings(self): try: if os.path.exists("settings.json"): with open("settings.json", "r", encoding="utf-8") as f: data = json.load(f) self.continuous_mode = data.get("continuous_mode", False) self.continuous_var.set(self.continuous_mode) self.sort_mode = data.get("sort_mode", "score") except Exception as e: print("加载设置失败:", e) def on_sort_change(self, event=None): """当排序方式改变时触发""" choice = self.sort_var.get() if choice == "按积分排序": self.sort_mode = "score" elif choice == "按姓名排序": self.sort_mode = "name" self.save_settings() self.update_display() 启动入口 if name == “main”: root = tk.Tk() app = ClassManager(root) root.mainloop() 修复虚拟键盘无法使用问题
最新发布
09-23
import tkinter as tk from tkinter import messagebox, simpledialog, ttk import random import json import os from datetime import datetime, timedelta from pypinyin import lazy_pinyin, Style from threading import Thread import openpyxl from openpyxl.styles import Alignment, Font, PatternFill from openpyxl.utils import get_column_letter class VirtualKeyboard: """虚拟键盘类""" def __init__(self, parent, entry_widget=None): self.parent = parent self.entry = entry_widget self.window = None # 添加:排序方式,默认按积分 self.sort_mode = "score" # 可选: "score", "name" def show(self, entry_widget=None): if entry_widget: self.entry = entry_widget if self.window and self.window.winfo_exists(): self.window.lift() return self.window = tk.Toplevel(self.parent) self.window.title("虚拟键盘") self.window.geometry("600x250") self.window.resizable(False, False) self.window.attributes('-topmost', True) self.window.configure(bg='#f0f8ff') keys = [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"], ['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'] ] for i, row in enumerate(keys): frame = tk.Frame(self.window, bg='#f0f8ff') frame.pack(pady=2) for key in row: btn = tk.Button( frame, text=key, width=4, height=2, command=lambda k=key: self.insert_key(k), bg='#ffffff', fg='#333333', font=('Arial', 10) ) btn.pack(side=tk.LEFT, padx=1) func_frame = tk.Frame(self.window, bg='#f0f8ff') func_frame.pack(pady=2) tk.Button( func_frame, text="空格", width=8, height=2, command=lambda: self.insert_key(" "), bg='#4CAF50', fg='white' ).pack(side=tk.LEFT, padx=1) tk.Button( func_frame, text="删除", width=8, height=2, command=self.delete_key, bg='#f44336', fg='white' ).pack(side=tk.LEFT, padx=1) tk.Button( func_frame, text="关闭", width=8, height=2, command=self.window.destroy, bg='#607D8B', fg='white' ).pack(side=tk.LEFT, padx=1) def insert_key(self, key): if self.entry: self.entry.insert(tk.INSERT, key) def delete_key(self): if self.entry and self.entry.selection_present(): self.entry.delete(tk.SEL_FIRST, tk.SEL_LAST) else: pos = self.entry.index(tk.INSERT) if pos > 0: self.entry.delete(pos - 1) class ClassManager: """班级积分管理系统主类""" def __init__(self, root): self.root = root self.student_frames = {} self.root.title("13班积分管理系统") self.root.geometry("1200x800") self.root.resizable(False, False) self.root.configure(bg='#f0f8ff') # DPI 感知(仅 Windows) try: import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1) except: pass # 字体设置 self.title_font = ("微软雅黑", 20, "bold") self.button_font = ("微软雅黑", 10) self.text_font = ("微软雅黑", 9) self.small_font = ("微软雅黑", 8) # 颜色常量 self.BG_NORMAL = "white" self.BG_SELECTED = "#e3f2fd" self.COLOR_POSITIVE = "#4CAF50" self.COLOR_NEGATIVE = "#f44336" self.COLOR_NEUTRAL = "#333" self.selected_students = set() # 连续操作模式标志位 self.continuous_mode = False self.continuous_var = tk.BooleanVar(value=self.continuous_mode) # 加载用户设置 self.load_settings() # 登录验证 if not self.check_login_password(): self.root.destroy() return # 初始化数据 self.students = self.load_data() if not self.students: self.initialize_from_roster() self.admin_password = self.load_admin_password() # 构建拼音缓存 self.pinyin_cache = {} self.build_pinyin_cache() # 小组管理 self.groups = self.load_groups() # 创建界面 self.create_widgets() self.update_display() # 绑定窗口大小变化事件(防抖) self.resize_after_id = None self.root.bind("<Configure>", self.on_window_resize) # 拖动变量 self.mini_window = None self.mini_drag_data = {"x": 0, "y": 0} # 上次保存时间(节流) self.last_save_time = 0 # 协议关闭 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) def build_pinyin_cache(self): """构建姓名到拼音首字母和全拼的缓存""" for name in self.students: if not name: continue initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} def check_login_password(self): login_password = self.load_login_password() if not login_password: return self.setup_login_password() attempts = 3 while attempts > 0: password = self.ask_password("登录验证", "请输入登录密码:") if password == login_password: return True attempts -= 1 msg = f"密码错误,还剩{attempts}次机会" if attempts > 0 else "密码错误次数过多" messagebox.showerror("密码错误", msg, parent=self.root) if attempts == 0: return False return False def setup_login_password(self): password = self.ask_password("设置登录密码", "请设置登录密码:") if not password: return False confirm = self.ask_password("确认密码", "请再次输入:") if password != confirm: messagebox.showerror("错误", "两次输入不一致") return False self.save_login_password(password) messagebox.showinfo("成功", "登录密码设置成功") return True def load_login_password(self): try: if os.path.exists("login_password.json"): with open("login_password.json", "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print("加载登录密码失败:", e) return None def save_login_password(self, pwd): with open("login_password.json", "w", encoding="utf-8") as f: json.dump(pwd, f, ensure_ascii=False) def on_closing(self): if messagebox.askokcancel("退出", "确定要退出吗?"): self.save_data_async() self.save_settings() # 保存用户设置 if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.root.destroy() def load_admin_password(self): try: if os.path.exists("admin_password.json"): with open("admin_password.json", "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print("加载管理密码失败:", e) return "12" def save_admin_password(self): with open("admin_password.json", "w", encoding="utf-8") as f: json.dump(self.admin_password, f, ensure_ascii=False) def change_login_password(self): old = self.ask_password("验证原密码", "请输入原密码:") current = self.load_login_password() if old != current: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新密码", "请输入新密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码修改成功!") def change_admin_password(self): old = self.ask_password("验证原密码", "请输入管理密码:") if old != self.admin_password: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新管理密码", "请输入两位数字:") if not new or len(new) != 2 or not new.isdigit(): messagebox.showerror("错误", "必须是两位数字!") return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.admin_password = new self.save_admin_password() messagebox.showinfo("成功", "管理密码修改成功!") def forget_login_password(self): admin = self.ask_password("验证管理密码", "请输入管理密码:") if admin != self.admin_password: messagebox.showerror("错误", "管理密码错误!") return new = self.ask_password("重置登录密码", "请输入新登录密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码已重置!") def ask_password(self, title, prompt): dialog = tk.Toplevel(self.root) dialog.title(title) dialog.geometry("400x200") dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text=prompt, font=self.text_font, bg='#f0f8ff').pack(pady=20) pwd_var = tk.StringVar() entry = tk.Entry(dialog, textvariable=pwd_var, show='*', font=self.text_font, width=20) entry.pack(pady=10) # 绑定虚拟键盘 entry.bind("<Button-1>", lambda e: VirtualKeyboard(dialog, entry).show()) result = [] def on_ok(): result.append(pwd_var.get()) dialog.destroy() def on_cancel(): result.append(None) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button(btn_frame, text="确定", command=on_ok, bg='#4CAF50', fg='white', width=10).pack(side=tk.LEFT, padx=10) tk.Button(btn_frame, text="取消", command=on_cancel, bg='#f44336', fg='white', width=10).pack(side=tk.LEFT, padx=10) entry.focus_set() dialog.wait_window() return result[0] if result else None def initialize_from_roster(self): roster = [ "安琪儿", "白睦尧", "白沛妍", "鲍星焱", "边子轩", "常佳怡", "钞依冉", "崔浩然", "崔嘉诺", "高博程", "高方圆", "高海翔", "高瑞泽", "高硕", "高雅萍", "郭欣宇", "韩静舒", "贺佳萱", "贺予绘", "黄钰涵", "李佳倩", "李金格", "李俊辰", "梁家伊", "刘博宇", "刘锐宇", "刘思乔", "刘旭尧", "刘雅涵", "柳登程", "马涛涛", "孟家旭", "苗宇辰", "裴雨轩", "拓雨诺", "王嘉浩", "王婷雨", "王怡婷", "王梓", "谢孟芝", "徐茂瑞", "杨少轩", "杨雪琦", "尤宇琪", "尤子潇", "张皓茗", "张皓雅", "张家瑜", "张朴淳", "张瑞霞", "张雅琪", "赵雅彤", "赵勇杰", "郑梦茹" ] for name in roster: self.students[name] = {"score": 0, "history": []} self.save_data_throttled() def load_data(self): try: if os.path.exists("class_data.json"): with open("class_data.json", "r", encoding="utf-8") as f: data = json.load(f) # 兼容旧格式:{name: score} for k, v in data.items(): if isinstance(v, int): data[k] = {"score": v, "history": []} return data except Exception as e: print("加载数据失败:", e) return {} def save_data_throttled(self): now = datetime.now().timestamp() if hasattr(self, 'last_save_time') and now - self.last_save_time < 2: return self.save_data_async() self.last_save_time = now def save_data_async(self): def _save(): try: with open("class_data.json", "w", encoding="utf-8") as f: json.dump(self.students, f, ensure_ascii=False, indent=2) except Exception as e: print("异步保存失败:", e) Thread(target=_save, daemon=True).start() def load_groups(self): """加载学生分组信息""" try: if os.path.exists("groups.json"): with open("groups.json", "r", encoding="utf-8") as f: data = json.load(f) # 验证数据格式 for group_name, members in data.items(): if not isinstance(members, list): continue for name in members[:]: if name not in self.students: members.remove(name) return data except Exception as e: print("加载小组失败:", e) # 默认创建几个小组 return { } def save_groups(self): """异步保存小组信息""" def _save(): try: with open("groups.json", "w", encoding="utf-8") as f: json.dump(self.groups, f, ensure_ascii=False, indent=2) except Exception as e: print("保存小组失败:", e) Thread(target=_save, daemon=True).start() def add_student(self): name = simpledialog.askstring("添加学生", "请输入学生姓名:", parent=self.root) if not name: return if name in self.students: messagebox.showwarning("重复", f"学生 {name} 已存在!") return self.students[name] = {"score": 0, "history": []} initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"已添加学生:{name}") def show_add_points_menu(self): self.adjust_points_with_buttons("加分", 1) def show_subtract_points_menu(self): self.adjust_points_with_buttons("减分", -1) def adjust_points_with_buttons(self, action, multiplier): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return dialog = tk.Toplevel(self.root) dialog.title(action) dialog.geometry("440x220") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label( dialog, text=f"请选择{action}分数:", font=self.text_font, bg='#f0f8ff' ).pack(pady=(15, 5)) button_frame = tk.Frame(dialog, bg='#f0f8ff') button_frame.pack(pady=10) values = [1, 2, 3, 4, 5, 10] colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#607D8B', '#795548'] texts = [f"{v}分" for v in values] for i in range(len(values)): btn = tk.Button( button_frame, text=texts[i], font=self.button_font, width=8, height=2, bg=colors[i], fg='white', command=lambda v=values[i]: self.do_adjust_points(dialog, v * multiplier) ) btn.pack(side=tk.LEFT, padx=8) custom_frame = tk.Frame(dialog, bg='#f0f8ff') custom_frame.pack(pady=8) tk.Button( custom_frame, text="自定义", font=self.button_font, width=8, height=2, bg='#607D8B', fg='white', command=lambda: self.do_adjust_points_custom(dialog, multiplier) ).pack() tk.Button( dialog, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(pady=10) dialog.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (dialog.winfo_height() // 2) dialog.geometry(f"+{x}+{y}") dialog.focus_force() def do_adjust_points(self, dialog, change): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.selected_students: self.students[name]["score"] += change self.students[name]["history"].append({ "time": now, "change": change, "type": "add" if change > 0 else "sub" }) if not self.continuous_mode: self.selected_students.clear() dialog.destroy() self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"{change:+} 分操作完成!", parent=self.root) def do_adjust_points_custom(self, dialog, multiplier): value = simpledialog.askinteger("自定义", "请输入分数:", parent=dialog, minvalue=1, maxvalue=100) if value is not None: self.do_adjust_points(dialog, value * multiplier) def random_select_multi(self): dialog = tk.Toplevel(self.root) dialog.title("随机抽选") dialog.geometry("320x180") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text="请选择抽取人数:", font=self.text_font, bg='#f0f8ff').pack(pady=10) ctrl_frame = tk.Frame(dialog, bg='#f0f8ff') ctrl_frame.pack(pady=10) count_var = tk.IntVar(value=1) def decrease(): if count_var.get() > 1: count_var.set(count_var.get() - 1) def increase(): count_var.set(count_var.get() + 1) tk.Button(ctrl_frame, text="−", font=("Arial", 14), width=3, command=decrease, bg="#f44336", fg="white").pack(side=tk.LEFT) num_label = tk.Label( ctrl_frame, textvariable=count_var, font=("Arial", 16, "bold"), width=5, relief=tk.SUNKEN, bg="white" ) num_label.pack(side=tk.LEFT, padx=10) tk.Button(ctrl_frame, text="+", font=("Arial", 14), width=3, command=increase, bg="#4CAF50", fg="white").pack(side=tk.LEFT) def do_select(): n = count_var.get() total = len(self.students) if n > total: messagebox.showwarning("提示", f"学生总数只有 {total} 人,无法抽取 {n} 人!", parent=dialog) return candidates = list(self.students.keys()) selected = random.sample(candidates, n) self.selected_students.clear() for name in selected: self.selected_students.add(name) self.update_display() result = "\n".join([f"第{i + 1}位:{name}" for i, name in enumerate(selected)]) messagebox.showinfo(f"抽中 {n} 人", f"🎉 抽中名单:\n\n{result}", parent=dialog) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button( btn_frame, text="确定抽取", font=self.button_font, width=10, bg='#4CAF50', fg='white', command=do_select ).pack(side=tk.LEFT, padx=10) tk.Button( btn_frame, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(side=tk.LEFT, padx=10) dialog.focus_force() def show_history(self): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return selected_names = list(self.selected_students) if len(selected_names) == 1: name = selected_names[0] history = self.students[name]["history"] if not history: messagebox.showinfo("历史记录", f"{name} 暂无积分变动。", parent=self.root) return hist_text = "\n".join([f"[{r['time']}] {'+' if r['change'] > 0 else ''}{r['change']}" for r in history]) detail_win = tk.Toplevel(self.root) detail_win.title(f"{name} 的积分历史") detail_win.geometry("400x300") text = tk.Text(detail_win, font=self.text_font, wrap=tk.WORD) text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) text.insert(tk.END, hist_text) text.config(state=tk.DISABLED) return # 多个学生:使用 ttk.Notebook 标签页 self.show_multi_history_tabs(selected_names) def show_multi_history_tabs(self, names): """显示多个学生的积分历史(标签页形式)""" win = tk.Toplevel(self.root) win.title(f"多名学生积分历史(共{len(names)}人)") win.geometry("800x500") win.configure(bg='#f0f8ff') notebook = ttk.Notebook(win) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) total_changes = [] for name in names: data = self.students[name] history = data["history"] tab = tk.Frame(notebook, bg='white') notebook.add(tab, text=name[:6]) if not history: lbl = tk.Label(tab, text="暂无积分变动", font=self.text_font, fg="#999", bg="white") lbl.pack(pady=20) continue net_change = sum(r["change"] for r in history) total_changes.append((name, net_change)) text = tk.Text(tab, font=self.text_font, wrap=tk.WORD, height=15) scrollbar = tk.Scrollbar(tab, orient=tk.VERTICAL, command=text.yview) text.configure(yscrollcommand=scrollbar.set) for record in history: change = record["change"] sign = "+" if change > 0 else "" line = f"[{record['time']}] {sign}{change}\n" text.insert(tk.END, line, ("positive" if change > 0 else "negative")) text.tag_configure("positive", foreground="#4CAF50") text.tag_configure("negative", foreground="#f44336") text.config(state=tk.DISABLED, bg="white") text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) stat_frame = tk.Frame(win, bg='#f0f8ff') stat_frame.pack(fill=tk.X, padx=10, pady=5) total_net = sum(c[1] for c in total_changes) stat_label = tk.Label( stat_frame, text=f"共 {len(names)} 人 | 总净变化: {total_net:+}", font=("微软雅黑", 10, "bold"), bg='#f0f8ff', fg="#333" ) stat_label.pack() def sort_by_change(): sorted_names = [n for n, _ in sorted(total_changes, key=lambda x: x[1], reverse=True)] for i in range(len(notebook.tabs()) - 1, -1, -1): notebook.forget(i) for name in sorted_names: if name in self.selected_students: data = self.students[name] tab = tk.Frame(notebook, bg='white') text = tk.Text(tab, font=self.text_font, wrap=tk.WORD) scrollbar = tk.Scrollbar(tab, command=text.yview) text.configure(yscrollcommand=scrollbar.set) for r in data["history"]: sign = "+" if r["change"] > 0 else "" text.insert(tk.END, f"[{r['time']}] {sign}{r['change']}\n") text.config(state=tk.DISABLED, bg="white") text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) notebook.add(tab, text=name[:6]) tk.Button( stat_frame, text="按进步排序", font=self.small_font, command=sort_by_change, bg="#FF9800", fg="white" ).pack(side=tk.RIGHT) win.focus_force() def select_all(self): self.selected_students = set(self.students.keys()) self.update_display() def deselect_all(self): self.selected_students.clear() self.update_display() def reset_points(self): if messagebox.askyesno("确认", "确定要重置所有学生的积分为0吗?", parent=self.root): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.students: old_score = self.students[name]["score"] if old_score != 0: self.students[name]["history"].append({ "time": now, "change": -old_score, "type": "reset" }) self.students[name]["score"] = 0 self.save_data_throttled() self.update_display() messagebox.showinfo("成功", "所有积分已重置为0", parent=self.root) def clear_all_history(self): if messagebox.askyesno("确认", "确定要清除所有历史记录吗?", parent=self.root): for name in self.students: self.students[name]["history"] = [] self.save_data_throttled() messagebox.showinfo("成功", "所有历史记录已清除", parent=self.root) def manage_groups(self): """打开小组管理界面""" dialog = tk.Toplevel(self.root) dialog.title("管理小组") dialog.geometry("700x500") dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text="小组管理", font=self.title_font, bg='#f0f8ff').pack(pady=10) main_frame = tk.Frame(dialog, bg='#f0f8ff') main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) left = tk.Frame(main_frame, bg='#f0f8ff') left.pack(side=tk.LEFT, fill=tk.Y, padx=5) right = tk.Frame(main_frame, bg='#f0f8ff') right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5) tk.Label(left, text="小组", font=self.text_font, bg='#f0f8ff').pack(anchor=tk.W) group_listbox = tk.Listbox(left, font=self.text_font, width=15, height=15) g_scroll = tk.Scrollbar(left, command=group_listbox.yview) g_scroll.pack(side=tk.RIGHT, fill=tk.Y) group_listbox.config(yscrollcommand=g_scroll.set) for g in self.groups: group_listbox.insert(tk.END, g) tk.Label(right, text="成员", font=self.text_font, bg='#f0f8ff').pack(anchor=tk.W) member_listbox = tk.Listbox(right, font=self.text_font, selectmode=tk.MULTIPLE, height=15) m_scroll = tk.Scrollbar(right, command=member_listbox.yview) m_scroll.pack(side=tk.RIGHT, fill=tk.Y) member_listbox.config(yscrollcommand=m_scroll.set) member_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) def update_members(*args): selection = group_listbox.curselection() if not selection: return idx = selection[0] group_name = group_listbox.get(idx) member_listbox.delete(0, tk.END) for name in self.students: if name in self.groups.get(group_name, []): member_listbox.insert(tk.END, name) group_listbox.bind("<<ListboxSelect>>", update_members) btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=10) def add_group(): name = simpledialog.askstring("新小组", "请输入小组名称:", parent=dialog) if name and name not in self.groups: self.groups[name] = [] group_listbox.insert(tk.END, name) def del_group(): sel = group_listbox.curselection() if not sel: return name = group_listbox.get(sel[0]) if messagebox.askyesno("确认", f"删除小组 {name}?", parent=dialog): del self.groups[name] group_listbox.delete(sel[0]) def save_members(): sel = group_listbox.curselection() if not sel: return group_name = group_listbox.get(sel[0]) all_selected = [member_listbox.get(i) for i in member_listbox.curselection()] self.groups[group_name] = all_selected self.save_groups() messagebox.showinfo("成功", "已保存成员", parent=dialog) tk.Button(btn_frame, text="新建小组", command=add_group, bg="#4CAF50", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="删除小组", command=del_group, bg="#f44336", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="保存成员", command=save_members, bg="#2196F3", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="关闭", command=dialog.destroy, bg="#607D8B", fg="white").pack(side=tk.LEFT, padx=5) if self.groups: group_listbox.selection_set(0) update_members() def filter_by_category(self, category_type, value): """ 按分类筛选学生 :param category_type: 'all', 'surname', 'group' :param value: 对应值 """ if category_type == "all": filtered = self.students elif category_type == "surname": filtered = {name: data for name, data in self.students.items() if name.startswith(value)} elif category_type == "group": member_names = self.groups.get(value, []) filtered = {name: self.students[name] for name in member_names if name in self.students} else: filtered = self.students desc = f"【{value}】" if value else "全部" self.display_filtered(filtered, f"{desc} 学生") def create_widgets(self): top_frame = tk.Frame(self.root, bg='#3b5998', height=50) top_frame.pack(fill=tk.X) top_frame.pack_propagate(False) tk.Label(top_frame, text="13班积分管理系统", font=self.title_font, fg='white', bg='#3b5998').pack(side=tk.LEFT, padx=20, pady=8) tk.Button(top_frame, text="缩小", command=self.minimize_window, font=self.button_font, bg='#607D8B', fg='white').pack(side=tk.RIGHT, padx=10) self.manage_btn = tk.Button( top_frame, text="管理", command=self.show_management_menu, font=self.button_font, bg='#E91E63', fg='white' ) self.manage_btn.pack(side=tk.RIGHT, padx=10) button_frame = tk.Frame(self.root, bg='#f0f8ff') button_frame.pack(pady=5, fill=tk.X, padx=10) buttons = [ ("添加学生", self.add_student, '#4CAF50'), ("加分", self.show_add_points_menu, '#2196F3'), ("减分", self.show_subtract_points_menu, '#f44336'), ("随机抽选", self.random_select_multi, '#9C27B0'), ("积分历史", self.show_history, '#FF9800'), ("全选", self.select_all, '#607D8B'), ("取消全选", self.deselect_all, '#795548'), ("生成周报", self.generate_weekly_report, '#009688'), ] for i, (text, cmd, color) in enumerate(buttons): tk.Button( button_frame, text=text, command=cmd, width=10, height=1, font=self.button_font, bg=color, fg='white' ).grid(row=0, column=i, padx=4, pady=2) display_frame = tk.Frame(self.root, bg='#f0f8ff') display_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=5) category_frame = tk.Frame(display_frame, bg='#f0f8ff') category_frame.pack(fill=tk.X, pady=(0, 3)) tk.Label(category_frame, text="分类:", font=self.small_font, bg='#f0f8ff').pack(side=tk.LEFT) surnames = sorted(set(name[0] for name in self.students.keys())) btn_frame = tk.Frame(category_frame, bg='#f0f8ff') btn_frame.pack(side=tk.LEFT) tk.Button(btn_frame, text="全部", font=("微软雅黑", 8), width=3, command=lambda: self.filter_by_category("all", None), bg="#4CAF50", fg="white").pack(side=tk.LEFT, padx=1) for s in surnames: tk.Button(btn_frame, text=s, font=("微软雅黑", 8), width=2, command=lambda x=s: self.filter_by_category("surname", x), bg="#e0e0e0").pack(side=tk.LEFT, padx=1) for group_name in self.groups: tk.Button(btn_frame, text=group_name, font=("微软雅黑", 8), width=4, command=lambda x=group_name: self.filter_by_category("group", x), bg="#2196F3", fg="white").pack(side=tk.LEFT, padx=1) tk.Button(btn_frame, text="管理小组", font=("微软雅黑", 8), width=6, command=self.manage_groups, bg="#E91E63", fg="white").pack(side=tk.LEFT, padx=1) search_frame = tk.Frame(display_frame, bg='#f0f8ff') search_frame.pack(fill=tk.X, pady=(0, 5)) tk.Label(search_frame, text="搜索:", font=self.text_font, bg='#f0f8ff').pack(side=tk.LEFT) sort_frame = tk.Frame(display_frame, bg='#f0f8ff') sort_frame.pack(fill=tk.X, pady=(0, 5)) tk.Label(sort_frame, text="排序:", font=self.small_font, bg='#f0f8ff').pack(side=tk.LEFT) self.sort_var = tk.StringVar(value=self.sort_mode) sort_combo = ttk.Combobox( sort_frame, textvariable=self.sort_var, values=["按积分排序", "按姓名排序"], state="readonly", width=10, font=self.small_font ) sort_combo.pack(side=tk.LEFT, padx=5) sort_combo.bind("<<ComboboxSelected>>", lambda e: self.on_sort_change()) # 可选:加个说明标签 tk.Label(sort_frame, text="💡 双击学生可选中", font=self.small_font, fg="#666", bg='#f0f8ff').pack(side=tk.RIGHT) self.search_var = tk.StringVar() self.search_var.trace("w", self.on_search_change) search_entry = tk.Entry(search_frame, textvariable=self.search_var, font=self.text_font, width=30) search_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) search_entry.bind("<Button-1>", lambda e: VirtualKeyboard(self.root, search_entry).show()) container = tk.Frame(display_frame, bg='#f0f8ff') container.pack(fill=tk.BOTH, expand=True) self.canvas = tk.Canvas(container, bg='#f0f8ff', highlightthickness=0) scrollbar = tk.Scrollbar(container, orient="vertical", command=self.canvas.yview) self.scrollable_frame = tk.Frame(self.canvas, bg='#f0f8ff') self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=scrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") status_bar = tk.Label( self.root, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W, height=2 ) status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=0, pady=0) def on_window_resize(self, event): if event.widget != self.root: return if self.resize_after_id: self.root.after_cancel(self.resize_after_id) self.resize_after_id = self.root.after(50, self.check_and_update_display) def check_and_update_display(self): try: current_width = self.canvas.winfo_width() if hasattr(self, '_last_canvas_width') and self._last_canvas_width == current_width: return self._last_canvas_width = current_width self.update_display() except tk.TclError: pass def on_search_change(self, *args): query = self.search_var.get().strip().lower() if not query: self.update_display() return filtered = {} for name, data in self.students.items(): info = self.pinyin_cache.get(name, {}) if (query in name.lower() or query in info.get('full_pinyin', '') or query == info.get('initials', '') or all(c in info.get('initials', '') for c in query)): filtered[name] = data self.display_filtered(filtered, f"匹配“{query}”的结果") def display_filtered(self, students_dict, desc="结果"): for widget in self.scrollable_frame.winfo_children(): widget.destroy() if not students_dict: lbl = tk.Label(self.scrollable_frame, text="无匹配学生", font=self.text_font, fg="#999") lbl.pack(pady=20) return def display_filtered(self, students_dict, desc="结果"): for widget in self.scrollable_frame.winfo_children(): widget.destroy() if not students_dict: lbl = tk.Label(self.scrollable_frame, text="无匹配学生", font=self.text_font, fg="#999") lbl.pack(pady=20) return # ======== 动态排序逻辑 ======== items = list(students_dict.items()) if self.sort_mode == "score": # 按积分降序 sorted_items = sorted(items, key=lambda x: x[1]["score"], reverse=True) else: # self.sort_mode == "name" # 按姓名拼音首字母升序 sorted_items = sorted(items, key=lambda x: self.pinyin_cache.get(x[0], {}).get('full_pinyin', ''), reverse=False) self._create_blocks(sorted_items) self._create_blocks(sorted_items) def _create_blocks(self, student_list): if not student_list: return try: width = max(self.canvas.winfo_width(), 800) except tk.TclError: return blocks_per_row = 9 block_width = 105 block_height = 80 new_frames = {} for i, (name, data) in enumerate(student_list): row, col = divmod(i, blocks_per_row) is_selected = name in self.selected_students bg_color = self.BG_SELECTED if is_selected else self.BG_NORMAL highlight_color = "#1976D2" if is_selected else "gray" highlight_thickness = 2 if is_selected else 1 frame = None if name in self.student_frames: try: if self.student_frames[name].winfo_exists(): frame = self.student_frames[name] except tk.TclError: pass if frame is not None: frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.config(bg=bg_color, highlightbackground=highlight_color, highlightthickness=highlight_thickness) for child in frame.winfo_children(): child.config(bg=bg_color) else: frame = tk.Frame( self.scrollable_frame, width=block_width, height=block_height, relief=tk.RAISED, borderwidth=1, bg=bg_color, highlightbackground=highlight_color, highlightthickness=highlight_thickness ) frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.grid_propagate(False) tk.Label(frame, text=f"{i + 1}", font=self.small_font, bg=bg_color, fg='#666').place(x=2, y=1) tk.Label(frame, text=name, font=self.text_font, bg=bg_color, wraplength=90, fg='#333').place(relx=0.5, y=22, anchor=tk.CENTER) score = data["score"] score_color = self.COLOR_POSITIVE if score > 0 else self.COLOR_NEGATIVE if score < 0 else self.COLOR_NEUTRAL score_text = f"+{score}" if score > 0 else str(score) tk.Label(frame, text=score_text, font=("微软雅黑", 11, "bold"), bg=bg_color, fg=score_color).place(relx=0.5, y=52, anchor=tk.CENTER) frame.bind("<Button-1>", self.make_click_handler(name)) for child in frame.winfo_children(): child.bind("<Button-1>", lambda e, f=frame: f.event_generate("<Button-1>", x=e.x, y=e.y)) child.bind("<ButtonRelease-1>", lambda e: "break") child.configure(cursor="") new_frames[name] = frame for name, old_frame in self.student_frames.items(): if name not in new_frames: try: old_frame.destroy() except tk.TclError: pass self.student_frames = new_frames self.scrollable_frame.update_idletasks() self.canvas.config(scrollregion=self.canvas.bbox("all")) def make_click_handler(self, name): def handler(event): widget = event.widget if not widget.winfo_exists(): return is_selected = name in self.selected_students new_state = not is_selected if new_state: self.selected_students.add(name) else: self.selected_students.discard(name) frame = self.student_frames.get(name) if frame and frame.winfo_exists(): bg_color = self.BG_SELECTED if new_state else self.BG_NORMAL highlight = "#1976D2" if new_state else "gray" thickness = 2 if new_state else 1 frame.config(bg=bg_color, highlightbackground=highlight, highlightthickness=thickness) for child in frame.winfo_children(): child.config(bg=bg_color) self.save_data_throttled() return handler def update_display(self): self.display_filtered(self.students, "全部学生") def show_management_menu(self): menu = tk.Menu(self.root, tearoff=0, font=self.text_font) menu.add_command(label="修改登录密码", command=self.change_login_password) menu.add_command(label="修改管理密码", command=self.change_admin_password) menu.add_separator() menu.add_command(label="忘记登录密码", command=self.forget_login_password) menu.add_separator() menu.add_command(label="重置积分", command=self.reset_points) menu.add_command(label="清除全部历史", command=self.clear_all_history) menu.add_separator() menu.add_checkbutton( label="连续操作模式", variable=self.continuous_var, command=self.toggle_continuous_mode, font=self.text_font ) x = self.manage_btn.winfo_rootx() y = self.manage_btn.winfo_rooty() + self.manage_btn.winfo_height() menu.post(x, y) def toggle_continuous_mode(self): self.continuous_mode = self.continuous_var.get() def minimize_window(self): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.lift() return self.mini_window = tk.Toplevel(self.root) self.mini_window.title("13班") self.mini_window.geometry("100x40") self.mini_window.overrideredirect(True) self.mini_window.attributes('-topmost', True) screen_width = self.mini_window.winfo_screenwidth() self.mini_window.geometry(f"+{screen_width - 110}+10") self.mini_window.configure(bg='#3b5998') label = tk.Label( self.mini_window, text="班级管理", font=("微软雅黑", 12, "bold"), fg='white', bg='#3b5998', cursor="hand2" ) label.pack(fill=tk.BOTH, expand=True) label.bind("<ButtonPress-1>", self.start_mini_drag) label.bind("<B1-Motion>", self.on_mini_drag) label.bind("<Double-Button-1>", self.restore_window) label.bind("<Enter>", lambda e: label.config(bg='#4a69a0')) label.bind("<Leave>", lambda e: label.config(bg='#3b5998')) self.root.withdraw() def start_mini_drag(self, event): self.mini_drag_data["x"] = event.x self.mini_drag_data["y"] = event.y def on_mini_drag(self, event): x = self.mini_window.winfo_x() + (event.x - self.mini_drag_data["x"]) y = self.mini_window.winfo_y() + (event.y - self.mini_drag_data["y"]) self.mini_window.geometry(f"+{x}+{y}") def restore_window(self, event=None): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.mini_window = None self.root.deiconify() self.root.lift() def generate_weekly_report(self): week_ago = datetime.now() - timedelta(days=7) report_win = tk.Toplevel(self.root) report_win.title("积分周报") report_win.geometry("900x600") report_win.configure(bg='#f0f8ff') tk.Label(report_win, text="积分周报(本周变化总览)", font=self.title_font, bg='#f0f8ff').pack(pady=10) tree_frame = tk.Frame(report_win) tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) scroll = ttk.Scrollbar(tree_frame) scroll.pack(side=tk.RIGHT, fill=tk.Y) columns = ("name", "weekly_change", "changes") tree = ttk.Treeview(tree_frame, columns=columns, show="headings", yscrollcommand=scroll.set) tree.heading("name", text="姓名") tree.heading("weekly_change", text="净变化") tree.heading("changes", text="变动详情") tree.column("name", width=100, anchor=tk.CENTER) tree.column("weekly_change", width=80, anchor=tk.CENTER) tree.column("changes", width=400, anchor=tk.W) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.config(command=tree.yview) data = {} for name, student in self.students.items(): changes = [ r for r in student["history"] if datetime.strptime(r["time"], "%Y-%m-%d %H:%M") >= week_ago ] values = [f"{'+' if c['change'] > 0 else ''}{c['change']}" for c in changes] summary = " ".join(values) + " 分" if values else "无变动" net = sum(c["change"] for c in changes) data[name] = {"summary": summary, "net": net} for name in sorted(data, key=lambda x: data[x]["net"], reverse=True): info = data[name] tag = ("positive",) if info["net"] > 0 else ("negative",) if info["net"] < 0 else () tree.insert("", tk.END, values=(name, info["net"], info["summary"]), tags=tag) tree.tag_configure("positive", foreground=self.COLOR_POSITIVE) tree.tag_configure("negative", foreground=self.COLOR_NEGATIVE) btn_frame = tk.Frame(report_win, bg='#f0f8ff') btn_frame.pack(pady=10) tk.Button( btn_frame, text="导出 TXT", command=lambda: self.export_weekly_report_txt(data), bg='#4CAF50', fg='white', font=self.button_font, width=10 ).pack(side=tk.LEFT, padx=5) tk.Button( btn_frame, text="导出 Excel", command=lambda: self.export_weekly_report_excel(data), bg='#FF9800', fg='white', font=self.button_font, width=10 ).pack(side=tk.LEFT, padx=5) def export_weekly_report_txt(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.txt" with open(filename, "w", encoding="utf-8") as f: f.write("13班积分周报\n\n") for name, d in sorted(data.items(), key=lambda x: x[1]["net"], reverse=True): f.write(f"{name}: {d['summary']} (净变化: {d['net']})\n") messagebox.showinfo("成功", f"已导出至 {filename}") except Exception as e: messagebox.showerror("错误", str(e)) def export_weekly_report_excel(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" wb = openpyxl.Workbook() ws = wb.active ws.title = "积分周报" cell = ws.cell(1, 1, "13班积分周报") cell.font = Font(size=16, bold=True, color="1a5fb4") ws.merge_cells('A1:C1') headers = ["姓名", "净变化", "详细变动"] for col_idx, header in enumerate(headers, 1): cell = ws.cell(2, col_idx, header) cell.font = Font(bold=True) cell.fill = PatternFill(start_color="d0ebff", end_color="d0ebff", fill_type="solid") ws.column_dimensions[get_column_letter(col_idx)].width = [12, 10, 50][col_idx - 1] for idx, (name, d) in enumerate(sorted(data.items(), key=lambda x: x[1]["net"], reverse=True), start=3): ws.cell(idx, 1, name) net_cell = ws.cell(idx, 2, d["net"]) net_cell.font = Font(color="006400" if d["net"] > 0 else "DC143C" if d["net"] < 0 else "000000") ws.cell(idx, 3, d["summary"]) for row in ws.iter_rows(min_row=2, max_row=len(data) + 2, min_col=1, max_col=3): for cell in row: cell.alignment = Alignment(vertical="center", wrap_text=True) wb.save(filename) messagebox.showinfo("成功", f"Excel 已导出至:\n{filename}") except Exception as e: messagebox.showerror("导出失败", f"无法创建文件:\n{str(e)}") def save_settings(self): settings = { "continuous_mode": self.continuous_mode, "sort_mode": self.sort_mode # 新增 } try: with open("settings.json", "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) except Exception as e: print("保存设置失败:", e) def load_settings(self): try: if os.path.exists("settings.json"): with open("settings.json", "r", encoding="utf-8") as f: data = json.load(f) self.continuous_mode = data.get("continuous_mode", False) self.continuous_var.set(self.continuous_mode) self.sort_mode = data.get("sort_mode", "score") # 新增 except Exception as e: print("加载设置失败:", e) def on_sort_change(self): """当排序方式改变时触发""" choice = self.sort_var.get() if choice == "按积分排序": self.sort_mode = "score" elif choice == "按姓名排序": self.sort_mode = "name" self.save_settings() # 保存选择 self.update_display() # 启动入口 if __name__ == "__main__": root = tk.Tk() app = ClassManager(root) root.mainloop() 直接输出全部修改后代码
09-23
import tkinter as tk from tkinter import messagebox, simpledialog, ttk import random import json import os from datetime import datetime, timedelta from pypinyin import lazy_pinyin, Style from threading import Thread import openpyxl from openpyxl.styles import Alignment, Font, PatternFill from openpyxl.utils import get_column_letter class VirtualKeyboard: “”“虚拟键盘类”“” def __init__(self, parent, entry_widget=None): self.parent = parent self.entry = entry_widget self.window = None def show(self, entry_widget=None): if entry_widget: self.entry = entry_widget if self.window and self.window.winfo_exists(): self.window.lift() return self.window = tk.Toplevel(self.parent) self.window.title("虚拟键盘") self.window.geometry("600x250") self.window.resizable(False, False) self.window.attributes('-topmost', True) self.window.configure(bg='#f0f8ff') keys = [ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"], ['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'] ] for i, row in enumerate(keys): frame = tk.Frame(self.window, bg='#f0f8ff') frame.pack(pady=2) for key in row: btn = tk.Button(frame, text=key, width=4, height=2, command=lambda k=key: self.insert_key(k), bg='#ffffff', fg='#333333', font=('Arial', 10)) btn.pack(side=tk.LEFT, padx=1) func_frame = tk.Frame(self.window, bg='#f0f8ff') func_frame.pack(pady=2) tk.Button(func_frame, text="空格", width=8, height=2, command=lambda: self.insert_key(" "), bg='#4CAF50', fg='white').pack(side=tk.LEFT, padx=1) tk.Button(func_frame, text="删除", width=8, height=2, command=self.delete_key, bg='#f44336', fg='white').pack(side=tk.LEFT, padx=1) tk.Button(func_frame, text="关闭", width=8, height=2, command=self.window.destroy, bg='#607D8B', fg='white').pack(side=tk.LEFT, padx=1) def insert_key(self, key): if self.entry: self.entry.insert(tk.INSERT, key) def delete_key(self): if self.entry and self.entry.selection_present(): self.entry.delete(tk.SEL_FIRST, tk.SEL_LAST) else: pos = self.entry.index(tk.INSERT) if pos > 0: self.entry.delete(pos - 1) class ClassManager: def load_settings(self): “”“从 settings.json 加载用户设置(如连续操作模式状态)”“” try: if os.path.exists(“settings.json”): with open(“settings.json”, “r”, encoding=“utf-8”) as f: data = json.load(f) # 恢复连续操作模式状态 self.continuous_mode = data.get(“continuous_mode”, False) self.continuous_var.set(self.continuous_mode) # 同步到菜单显示 except Exception as e: print(“加载设置失败:”, e) def save_settings(self): """保存当前设置到 settings.json""" settings = { "continuous_mode": self.continuous_mode } try: with open("settings.json", "w", encoding="utf-8") as f: json.dump(settings, f, ensure_ascii=False, indent=2) except Exception as e: print("保存设置失败:", e) def toggle_continuous_mode(self): """当菜单项被勾选/取消时,同步更新 continuous_mode 状态""" self.continuous_mode = self.continuous_var.get() def __init__(self, root): self.root = root self.student_frames = {} self.root.title("13班积分管理系统") self.root.geometry("1200x800") self.root.resizable(False, False) self.root.configure(bg='#f0f8ff') # DPI 感知(仅 Windows) try: import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1) except: pass # 字体设置 self.title_font = ("微软雅黑", 20, "bold") self.button_font = ("微软雅黑", 10) self.text_font = ("微软雅黑", 9) self.small_font = ("微软雅黑", 8) # 颜色常量 self.BG_NORMAL = "white" self.BG_SELECTED = "#e3f2fd" self.COLOR_POSITIVE = "#4CAF50" self.COLOR_NEGATIVE = "#f44336" self.COLOR_NEUTRAL = "#333" self.selected_students = set() # 👇 新增:连续操作模式标志位 self.continuous_mode = False # 默认关闭 # 👇 新增:用于菜单绑定的 BooleanVar self.continuous_var = tk.BooleanVar(value=self.continuous_mode) self.load_settings() # 登录验证 if not self.check_login_password(): self.root.destroy() return # 初始化数据 self.students = self.load_data() if not self.students: self.initialize_from_roster() self.admin_password = self.load_admin_password() # 构建拼音缓存 self.pinyin_cache = {} self.build_pinyin_cache() # 创建界面 self.create_widgets() self.update_display() # 绑定窗口大小变化事件(防抖) self.resize_after_id = None self.root.bind("<Configure>", self.on_window_resize) # 拖动变量 self.mini_window = None self.mini_drag_data = {"x": 0, "y": 0} # 上次保存时间(节流) self.last_save_time = 0 # 协议关闭 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) def build_pinyin_cache(self): for name in self.students: initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} def check_login_password(self): login_password = self.load_login_password() if not login_password: return self.setup_login_password() attempts = 3 while attempts > 0: password = self.ask_password("登录验证", "请输入登录密码:") if password == login_password: return True attempts -= 1 msg = f"密码错误,还剩{attempts}次机会" if attempts > 0 else "密码错误次数过多" messagebox.showerror("密码错误", msg, parent=self.root) if attempts == 0: return False return False def setup_login_password(self): password = self.ask_password("设置登录密码", "请设置登录密码:") if not password: return False confirm = self.ask_password("确认密码", "请再次输入:") if password != confirm: messagebox.showerror("错误", "两次输入不一致") return False self.save_login_password(password) messagebox.showinfo("成功", "登录密码设置成功") return True def load_login_password(self): try: if os.path.exists("login_password.json"): with open("login_password.json", "r") as f: return json.load(f) except Exception as e: print("加载登录密码失败:", e) return None def save_login_password(self, pwd): with open("login_password.json", "w") as f: json.dump(pwd, f) def on_closing(self): if messagebox.askokcancel("退出", "确定要退出吗?"): self.save_data_async() self.save_settings() # 👈 保存设置 if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.root.destroy() def load_admin_password(self): try: if os.path.exists("admin_password.json"): with open("admin_password.json", "r") as f: return json.load(f) except Exception as e: print("加载管理密码失败:", e) return "12" def save_admin_password(self): with open("admin_password.json", "w") as f: json.dump(self.admin_password, f) def change_login_password(self): old = self.ask_password("验证原密码", "请输入原密码:") current = self.load_login_password() if old != current: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新密码", "请输入新密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码修改成功!") def change_admin_password(self): old = self.ask_password("验证原密码", "请输入管理密码:") if old != self.admin_password: messagebox.showerror("错误", "原密码错误!") return new = self.ask_password("新管理密码", "请输入两位数字:") if not new or len(new) != 2 or not new.isdigit(): messagebox.showerror("错误", "必须是两位数字!") return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.admin_password = new self.save_admin_password() messagebox.showinfo("成功", "管理密码修改成功!") def forget_login_password(self): admin = self.ask_password("验证管理密码", "请输入管理密码:") if admin != self.admin_password: messagebox.showerror("错误", "管理密码错误!") return new = self.ask_password("重置登录密码", "请输入新登录密码:") if not new: return confirm = self.ask_password("确认", "请再输一次:") if new != confirm: messagebox.showerror("错误", "两次不一致!") return self.save_login_password(new) messagebox.showinfo("成功", "登录密码已重置!") def ask_password(self, title, prompt): dialog = tk.Toplevel(self.root) dialog.title(title) dialog.geometry("400x200") dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text=prompt, font=self.text_font, bg='#f0f8ff').pack(pady=20) pwd_var = tk.StringVar() entry = tk.Entry(dialog, textvariable=pwd_var, show='*', font=self.text_font, width=20) entry.pack(pady=10) VirtualKeyboard(dialog, entry).show() result = [] def on_ok(): result.append(pwd_var.get()) dialog.destroy() def on_cancel(): result.append(None) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button(btn_frame, text="确定", command=on_ok, bg='#4CAF50', fg='white', width=10).pack(side=tk.LEFT, padx=10) tk.Button(btn_frame, text="取消", command=on_cancel, bg='#f44336', fg='white', width=10).pack(side=tk.LEFT, padx=10) entry.focus_set() dialog.wait_window() return result[0] if result else None def initialize_from_roster(self): roster = [ "安琪儿", "白睦尧", "白沛妍", "鲍星焱", "边子轩", "常佳怡", "钞依冉", "崔浩然", "崔嘉诺", "高博程", "高方圆", "高海翔", "高瑞泽", "高硕", "高雅萍", "郭欣宇", "韩静舒", "贺佳萱", "贺予绘", "黄钰涵", "李佳倩", "李金格", "李俊辰", "梁家伊", "刘博宇", "刘锐宇", "刘思乔", "刘旭尧", "刘雅涵", "柳登程", "马涛涛", "孟家旭", "苗宇辰", "裴雨轩", "拓雨诺", "王嘉浩", "王婷雨", "王怡婷", "王梓", "谢孟芝", "徐茂瑞", "杨少轩", "杨雪琦", "尤宇琪", "尤子潇", "张皓茗", "张皓雅", "张家瑜", "张朴淳", "张瑞霞", "张雅琪", "赵雅彤", "赵勇杰", "郑梦茹" ] for name in roster: self.students[name] = {"score": 0, "history": []} self.save_data_throttled() def load_data(self): try: if os.path.exists("class_data.json"): with open("class_data.json", "r", encoding="utf-8") as f: data = json.load(f) for k, v in data.items(): if isinstance(v, int): data[k] = {"score": v, "history": []} return data except Exception as e: print("加载数据失败:", e) return {} def save_data_throttled(self): now = datetime.now().timestamp() if hasattr(self, 'last_save_time') and now - self.last_save_time < 2: return self.save_data_async() self.last_save_time = now def save_data_async(self): def _save(): try: with open("class_data.json", "w", encoding="utf-8") as f: json.dump(self.students, f, ensure_ascii=False, indent=2) except Exception as e: print("异步保存失败:", e) Thread(target=_save, daemon=True).start() def create_widgets(self): top_frame = tk.Frame(self.root, bg='#3b5998', height=50) top_frame.pack(fill=tk.X) tk.Label(top_frame, text="13班积分管理系统", font=self.title_font, fg='white', bg='#3b5998').pack(side=tk.LEFT, padx=20, pady=8) tk.Button(top_frame, text="缩小", command=self.minimize_window, font=self.button_font, bg='#607D8B', fg='white').pack(side=tk.RIGHT, padx=10) self.manage_btn = tk.Button( top_frame, text="管理", command=self.show_management_menu, font=self.button_font, bg='#E91E63', fg='white' ) self.manage_btn.pack(side=tk.RIGHT, padx=10) button_frame = tk.Frame(self.root, bg='#f0f8ff') button_frame.pack(pady=5, fill=tk.X, padx=10) buttons = [ ("添加学生", self.add_student, '#4CAF50'), ("加分", self.show_add_points_menu, '#2196F3'), ("减分", self.show_subtract_points_menu, '#f44336'), ("随机抽选", self.random_select_multi, '#9C27B0'), ("积分历史", self.show_history, '#FF9800'), ("全选", self.select_all, '#607D8B'), ("取消全选", self.deselect_all, '#795548'), ("生成周报", self.generate_weekly_report, '#009688'), ] for i, (text, cmd, color) in enumerate(buttons): tk.Button(button_frame, text=text, command=cmd, width=10, height=1, font=self.button_font, bg=color, fg='white').grid(row=0, column=i, padx=4, pady=2) display_frame = tk.Frame(self.root, bg='#f0f8ff') display_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=5) surname_frame = tk.Frame(display_frame, bg='#f0f8ff') surname_frame.pack(fill=tk.X, pady=(0, 3)) tk.Label(surname_frame, text="姓氏:", font=self.small_font, bg='#f0f8ff').pack(side=tk.LEFT) for s in ["安", "白", "鲍", "边", "常", "钞", "崔", "高", "郭", "韩", "贺", "黄", "李", "梁", "刘", "柳", "马", "孟", "苗", "裴", "拓", "王", "谢", "徐", "尤", "张", "赵", "全部"]: tk.Button(surname_frame, text=s, font=("微软雅黑", 8), width=2, height=1, command=lambda x=s: self.filter_by_surname(x), bg="#e0e0e0").pack(side=tk.LEFT, padx=1) search_frame = tk.Frame(display_frame, bg='#f0f8ff') search_frame.pack(fill=tk.X, pady=(0, 5)) tk.Label(search_frame, text="搜索:", font=self.text_font, bg='#f0f8ff').pack(side=tk.LEFT) self.search_var = tk.StringVar() self.search_var.trace("w", self.on_search_change) search_entry = tk.Entry(search_frame, textvariable=self.search_var, font=self.text_font, width=30) search_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) search_entry.bind("<Button-1>", lambda e: VirtualKeyboard(self.root, search_entry).show()) container = tk.Frame(display_frame, bg='#f0f8ff') container.pack(fill=tk.BOTH, expand=True) self.canvas = tk.Canvas(container, bg='#f0f8ff', highlightthickness=0) scrollbar = tk.Scrollbar(container, orient="vertical", command=self.canvas.yview) self.scrollable_frame = tk.Frame(self.canvas, bg='#f0f8ff') self.scrollable_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=scrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") status_bar = tk.Label( self.root, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W, height=2 ) status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=0, pady=0) def on_window_resize(self, event): if event.widget != self.root: return if self.resize_after_id: self.root.after_cancel(self.resize_after_id) self.resize_after_id = self.root.after(50, self.check_and_update_display) def check_and_update_display(self): try: current_width = self.canvas.winfo_width() if hasattr(self, '_last_canvas_width') and self._last_canvas_width == current_width: return self._last_canvas_width = current_width self.update_display() except tk.TclError: pass def filter_by_surname(self, surname): if surname == "其他": filtered = { name: data for name, data in self.students.items() if len(name) > 2 or name[0] not in "安白鲍边常钞崔高郭韩贺黄李梁刘柳马孟苗裴拓王谢徐尤张赵郑" } elif surname == "全部": filtered = self.students else: filtered = {name: data for name, data in self.students.items() if name.startswith(surname)} self.display_filtered(filtered, f"【{surname}】相关学生") def on_search_change(self, *args): query = self.search_var.get().strip().lower() if not query: self.update_display() return filtered = {} for name, data in self.students.items(): info = self.pinyin_cache.get(name, {}) if (query in name.lower() or query in info.get('full_pinyin', '') or query == info.get('initials', '') or all(c in info.get('initials', '') for c in query)): filtered[name] = data self.display_filtered(filtered, f"匹配“{query}”的结果") def display_filtered(self, students_dict, desc="结果"): for widget in self.scrollable_frame.winfo_children(): widget.destroy() if not students_dict: lbl = tk.Label(self.scrollable_frame, text="无匹配学生", font=self.text_font, fg="#999") lbl.pack(pady=20) return sorted_items = sorted(students_dict.items(), key=lambda x: x[1]["score"], reverse=True) self._create_blocks(sorted_items) def _create_blocks(self, student_list): if not student_list: return try: width = max(self.canvas.winfo_width(), 800) except tk.TclError: return blocks_per_row = 9 block_width = 105 block_height = 80 new_frames = {} for i, (name, data) in enumerate(student_list): row, col = divmod(i, blocks_per_row) bg_color = self.BG_SELECTED if name in self.selected_students else self.BG_NORMAL frame = None if name in self.student_frames: try: if self.student_frames[name].winfo_exists(): frame = self.student_frames[name] except tk.TclError: pass if frame is not None: frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.config(bg=bg_color) for child in frame.winfo_children(): child.config(bg=bg_color) else: frame = tk.Frame( self.scrollable_frame, width=block_width, height=block_height, relief=tk.RAISED, borderwidth=1, bg=bg_color ) frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew") frame.grid_propagate(False) tk.Label(frame, text=f"{i + 1}", font=self.small_font, bg=bg_color, fg='#666').place(x=2, y=1) tk.Label( frame, text=name, font=self.text_font, bg=bg_color, wraplength=90, fg='#333' ).place(relx=0.5, y=22, anchor=tk.CENTER) score = data["score"] score_color = self.COLOR_POSITIVE if score > 0 else self.COLOR_NEGATIVE if score < 0 else self.COLOR_NEUTRAL score_text = f"+{score}" if score > 0 else str(score) tk.Label( frame, text=score_text, font=("微软雅黑", 11, "bold"), bg=bg_color, fg=score_color ).place(relx=0.5, y=52, anchor=tk.CENTER) frame.bind("<Button-1>", self.make_click_handler(name)) frame.bind("<Double-Button-1>", lambda e, n=name: self.show_student_detail(n)) new_frames[name] = frame for name, old_frame in self.student_frames.items(): if name not in new_frames: try: old_frame.destroy() except tk.TclError: pass self.student_frames = new_frames self.scrollable_frame.update_idletasks() self.canvas.config(scrollregion=self.canvas.bbox("all")) def make_click_handler(self, name): def handler(event): if name in self.selected_students: self.selected_students.remove(name) else: self.selected_students.add(name) frame = self.student_frames.get(name) if frame and frame.winfo_exists(): bg_color = self.BG_SELECTED if name in self.selected_students else self.BG_NORMAL frame.config(bg=bg_color) for child in frame.winfo_children(): child.config(bg=bg_color) return handler def update_display(self): self.display_filtered(self.students, "全部学生") def add_student(self): name = simpledialog.askstring("添加学生", "请输入学生姓名:", parent=self.root) if not name: return if name in self.students: messagebox.showwarning("重复", f"学生 {name} 已存在!") return self.students[name] = {"score": 0, "history": []} initials = ''.join([p[0].lower() for p in lazy_pinyin(name, style=Style.FIRST_LETTER)]) full_py = ''.join(lazy_pinyin(name, style=Style.NORMAL)).lower() self.pinyin_cache[name] = {'initials': initials, 'full_pinyin': full_py} self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"已添加学生:{name}") def show_add_points_menu(self): self.adjust_points_with_buttons("加分", 1) def show_subtract_points_menu(self): self.adjust_points_with_buttons("减分", -1) def adjust_points_with_buttons(self, action, multiplier): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return # 创建唯一的加分/减分窗口 dialog = tk.Toplevel(self.root) dialog.title(action) dialog.geometry("440x220") # 合理大小 dialog.resizable(False, False) # 禁止缩放 dialog.transient(self.root) # 模态化 dialog.grab_set() # 阻塞主窗口 dialog.configure(bg='#f0f8ff') # 标题说明 tk.Label( dialog, text=f"请选择{action}分数:", font=self.text_font, bg='#f0f8ff' ).pack(pady=(15, 5)) # 按钮区域 button_frame = tk.Frame(dialog, bg='#f0f8ff') button_frame.pack(pady=10) values = [1, 2, 3, 4, 5, 10] colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#607D8B', '#795548'] texts = [f"{v}分" for v in values] for i in range(len(values)): btn = tk.Button( button_frame, text=texts[i], font=self.button_font, width=8, height=2, bg=colors[i], fg='white', command=lambda v=values[i]: self.do_adjust_points(dialog, v * multiplier) ) btn.pack(side=tk.LEFT, padx=8) # 自定义按钮单独一行 custom_frame = tk.Frame(dialog, bg='#f0f8ff') custom_frame.pack(pady=8) tk.Button( custom_frame, text="自定义", font=self.button_font, width=8, height=2, bg='#607D8B', fg='white', command=lambda: self.do_adjust_points_custom(dialog, multiplier) ).pack() # 取消按钮 tk.Button( dialog, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(pady=10) # 居中显示 dialog.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (dialog.winfo_height() // 2) dialog.geometry(f"+{x}+{y}") dialog.focus_force() def do_adjust_points(self, dialog, change): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.selected_students: self.students[name]["score"] += change self.students[name]["history"].append({ "time": now, "change": change, "type": "add" if change > 0 else "sub" }) # ✅ 根据 continuous_mode 决定是否清除选中 if not self.continuous_mode: self.selected_students.clear() dialog.destroy() self.save_data_throttled() self.update_display() messagebox.showinfo("成功", f"{change:+} 分操作完成!", parent=self.root) def do_adjust_points_custom(self, dialog, multiplier): value = simpledialog.askinteger("自定义", "请输入分数:", parent=dialog, minvalue=1, maxvalue=100) if value is not None: self.do_adjust_points(dialog, value * multiplier) def random_select_multi(self): """随机抽取 N 名学生,并自动选中""" dialog = tk.Toplevel(self.root) dialog.title("随机抽选") dialog.geometry("320x180") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() dialog.configure(bg='#f0f8ff') tk.Label(dialog, text="请选择抽取人数:", font=self.text_font, bg='#f0f8ff').pack(pady=10) ctrl_frame = tk.Frame(dialog, bg='#f0f8ff') ctrl_frame.pack(pady=10) count_var = tk.IntVar(value=1) def decrease(): if count_var.get() > 1: count_var.set(count_var.get() - 1) def increase(): count_var.set(count_var.get() + 1) tk.Button(ctrl_frame, text="−", font=("Arial", 14), width=3, command=decrease, bg="#f44336", fg="white").pack( side=tk.LEFT) num_label = tk.Label( ctrl_frame, textvariable=count_var, font=("Arial", 16, "bold"), width=5, relief=tk.SUNKEN, bg="white" ) num_label.pack(side=tk.LEFT, padx=10) tk.Button(ctrl_frame, text="+", font=("Arial", 14), width=3, command=increase, bg="#4CAF50", fg="white").pack( side=tk.LEFT) def do_select(): n = count_var.get() total = len(self.students) if n > total: messagebox.showwarning("提示", f"学生总数只有 {total} 人,无法抽取 {n} 人!", parent=dialog) return candidates = list(self.students.keys()) selected = random.sample(candidates, n) # 清除旧选择并加入新抽中者 self.selected_students.clear() for name in selected: self.selected_students.add(name) # 更新 UI 高亮 self.update_display() result = "\n".join([f"第{i + 1}位:{name}" for i, name in enumerate(selected)]) messagebox.showinfo(f"抽中 {n} 人", f"🎉 抽中名单:\n\n{result}", parent=dialog) dialog.destroy() btn_frame = tk.Frame(dialog, bg='#f0f8ff') btn_frame.pack(pady=20) tk.Button( btn_frame, text="确定抽取", font=self.button_font, width=10, bg='#4CAF50', fg='white', command=do_select ).pack(side=tk.LEFT, padx=10) tk.Button( btn_frame, text="取消", font=self.button_font, width=10, bg='#f44336', fg='white', command=dialog.destroy ).pack(side=tk.LEFT, padx=10) dialog.focus_force() def show_history(self): if not self.selected_students: messagebox.showwarning("提示", "请先选择学生!", parent=self.root) return if len(self.selected_students) > 1: messagebox.showwarning("提示", "只能查看单个学生的历史!", parent=self.root) return name = list(self.selected_students)[0] history = self.students[name]["history"] if not history: messagebox.showinfo("历史记录", f"{name} 暂无积分变动。", parent=self.root) return hist_text = "\n".join([f"[{r['time']}] {'+' if r['change'] > 0 else ''}{r['change']}" for r in history]) detail_win = tk.Toplevel(self.root) detail_win.title(f"{name} 的积分历史") detail_win.geometry("400x300") text = tk.Text(detail_win, font=self.text_font, wrap=tk.WORD) text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) text.insert(tk.END, hist_text) text.config(state=tk.DISABLED) def select_all(self): self.selected_students = set(self.students.keys()) self.update_display() def deselect_all(self): self.selected_students.clear() self.update_display() def reset_points(self): if messagebox.askyesno("确认", "确定要重置所有学生的积分为0吗?", parent=self.root): now = datetime.now().strftime("%Y-%m-%d %H:%M") for name in self.students: old_score = self.students[name]["score"] if old_score != 0: self.students[name]["history"].append({ "time": now, "change": -old_score, "type": "reset" }) self.students[name]["score"] = 0 self.save_data_throttled() self.update_display() messagebox.showinfo("成功", "所有积分已重置为0", parent=self.root) def clear_all_history(self): if messagebox.askyesno("确认", "确定要清除所有历史记录吗?", parent=self.root): for name in self.students: self.students[name]["history"] = [] self.save_data_throttled() messagebox.showinfo("成功", "所有历史记录已清除", parent=self.root) def show_student_detail(self, name): data = self.students[name] history = "\n".join( [f"[{r['time']}] {'+' if r['change'] > 0 else ''}{r['change']}" for r in data["history"][-10:]]) msg = f"姓名:{name}\n当前积分:{data['score']}\n\n最近10条记录:\n{history if history else '无'}" messagebox.showinfo("学生详情", msg, parent=self.root) def show_management_menu(self): menu = tk.Menu(self.root, tearoff=0, font=self.text_font) menu.add_command(label="修改登录密码", command=self.change_login_password) menu.add_command(label="修改管理密码", command=self.change_admin_password) menu.add_separator() menu.add_command(label="忘记登录密码", command=self.forget_login_password) menu.add_separator() menu.add_command(label="重置积分", command=self.reset_points) menu.add_command(label="清除全部历史", command=self.clear_all_history) # ✅ 添加连续操作模式开关 menu.add_separator() menu.add_checkbutton( label="连续操作模式", variable=self.continuous_var, command=self.toggle_continuous_mode, font=self.text_font ) x = self.manage_btn.winfo_rootx() y = self.manage_btn.winfo_rooty() + self.manage_btn.winfo_height() menu.post(x, y) def minimize_window(self): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.lift() return self.mini_window = tk.Toplevel(self.root) self.mini_window.title("13班") self.mini_window.geometry("100x40") self.mini_window.overrideredirect(True) self.mini_window.attributes('-topmost', True) screen_width = self.mini_window.winfo_screenwidth() self.mini_window.geometry(f"+{screen_width - 110}+10") self.mini_window.configure(bg='#3b5998') label = tk.Label(self.mini_window, text="班级管理", font=("微软雅黑", 12, "bold"), fg='white', bg='#3b5998', cursor="hand2") label.pack(fill=tk.BOTH, expand=True) label.bind("<ButtonPress-1>", self.start_mini_drag) label.bind("<B1-Motion>", self.on_mini_drag) label.bind("<Double-Button-1>", self.restore_window) label.bind("<Enter>", lambda e: label.config(bg='#4a69a0')) label.bind("<Leave>", lambda e: label.config(bg='#3b5998')) self.root.withdraw() def start_mini_drag(self, event): self.mini_drag_data["x"] = event.x self.mini_drag_data["y"] = event.y def on_mini_drag(self, event): x = self.mini_window.winfo_x() + (event.x - self.mini_drag_data["x"]) y = self.mini_window.winfo_y() + (event.y - self.mini_drag_data["y"]) self.mini_window.geometry(f"+{x}+{y}") def restore_window(self, event=None): if self.mini_window and self.mini_window.winfo_exists(): self.mini_window.destroy() self.mini_window = None self.root.deiconify() self.root.lift() def generate_weekly_report(self): week_ago = datetime.now() - timedelta(days=7) report_win = tk.Toplevel(self.root) report_win.title("积分周报") report_win.geometry("900x600") report_win.configure(bg='#f0f8ff') tk.Label(report_win, text="积分周报(本周变化总览)", font=self.title_font, bg='#f0f8ff').pack(pady=10) tree_frame = tk.Frame(report_win) tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) scroll = ttk.Scrollbar(tree_frame) scroll.pack(side=tk.RIGHT, fill=tk.Y) columns = ("name", "weekly_change", "changes") tree = ttk.Treeview(tree_frame, columns=columns, show="headings", yscrollcommand=scroll.set) tree.heading("name", text="姓名") tree.heading("weekly_change", text="净变化") tree.heading("changes", text="变动详情") tree.column("name", width=100, anchor=tk.CENTER) tree.column("weekly_change", width=80, anchor=tk.CENTER) tree.column("changes", width=400, anchor=tk.W) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.config(command=tree.yview) data = {} for name, student in self.students.items(): changes = [r for r in student["history"] if datetime.strptime(r["time"], "%Y-%m-%d %H:%M") >= week_ago] values = [f"{'+' if c['change'] > 0 else ''}{c['change']}" for c in changes] summary = " ".join(values) + " 分" if values else "无变动" net = sum(c["change"] for c in changes) data[name] = {"summary": summary, "net": net} for name in sorted(data, key=lambda x: data[x]["net"], reverse=True): info = data[name] tag = ("positive",) if info["net"] > 0 else ("negative",) if info["net"] < 0 else () tree.insert("", tk.END, values=(name, info["net"], info["summary"]), tags=tag) tree.tag_configure("positive", foreground=self.COLOR_POSITIVE) tree.tag_configure("negative", foreground=self.COLOR_NEGATIVE) btn_frame = tk.Frame(report_win, bg='#f0f8ff') btn_frame.pack(pady=10) tk.Button(btn_frame, text="导出 TXT", command=lambda: self.export_weekly_report_txt(data), bg='#4CAF50', fg='white', font=self.button_font, width=10).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="导出 Excel", command=lambda: self.export_weekly_report_excel(data), bg='#FF9800', fg='white', font=self.button_font, width=10).pack(side=tk.LEFT, padx=5) def export_weekly_report_txt(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.txt" with open(filename, "w", encoding="utf-8") as f: f.write("13班积分周报\n\n") for name, d in sorted(data.items(), key=lambda x: x[1]["net"], reverse=True): f.write(f"{name}: {d['summary']} (净变化: {d['net']})\n") messagebox.showinfo("成功", f"已导出至 {filename}") except Exception as e: messagebox.showerror("错误", str(e)) def export_weekly_report_excel(self, data): try: filename = f"周报_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" wb = openpyxl.Workbook() ws = wb.active ws.title = "积分周报" cell = ws.cell(1, 1, "13班积分周报") cell.font = Font(size=16, bold=True, color="1a5fb4") ws.merge_cells('A1:C1') headers = ["姓名", "净变化", "详细变动"] for col_idx, header in enumerate(headers, 1): cell = ws.cell(2, col_idx, header) cell.font = Font(bold=True) cell.fill = PatternFill(start_color="d0ebff", end_color="d0ebff", fill_type="solid") ws.column_dimensions[get_column_letter(col_idx)].width = [12, 10, 50][col_idx - 1] for idx, (name, d) in enumerate(sorted(data.items(), key=lambda x: x[1]["net"], reverse=True), start=3): ws.cell(idx, 1, name) net_cell = ws.cell(idx, 2, d["net"]) net_cell.font = Font(color="006400" if d["net"] > 0 else "DC143C" if d["net"] < 0 else "000000") ws.cell(idx, 3, d["summary"]) for row in ws.iter_rows(min_row=2, max_row=len(data) + 2, min_col=1, max_col=3): for cell in row: cell.alignment = Alignment(vertical="center", wrap_text=True) wb.save(filename) messagebox.showinfo("成功", f"Excel 已导出至:\n{filename}") except Exception as e: messagebox.showerror("导出失败", f"无法创建文件:\n{str(e)}") def save_settings(self): settings = { “continuous_mode”: self.continuous_mode } try: with open(“settings.json”, “w”, encoding=“utf-8”) as f: json.dump(settings, f, ensure_ascii=False, indent=2) except Exception as e: print(“保存设置失败:”, e) def load_settings(self): try: if os.path.exists(“settings.json”): with open(“settings.json”, “r”, encoding=“utf-8”) as f: data = json.load(f) self.continuous_mode = data.get(“continuous_mode”, False) self.continuous_var.set(self.continuous_mode) except Exception as e: print(“加载设置失败:”, e) if name == “main”: root = tk.Tk() app = ClassManager(root) root.mainloop() 修复该代码所有问题,并直接给出完整代码
09-22
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值