#!/usr/bin/env python3 """ 莞工教务系统 GUI - 图形化选课工具 =================================== 自适应探测选课类型标签页,支持选修/必修/公选/跨专业等。 """ import os import sys import threading import time # 引入同目录下的核心模块 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from dgut_core import ( ORIGIN, discover_tabs, drop_course, ensure_in_course_selection, ensure_session, get_courses, get_cycles, select_course, ) import customtkinter as ctk ctk.set_appearance_mode("System") ctk.set_default_color_theme("green") # ─── GUI 组件 ──────────────────────────────────────────────────────────── class CourseRow(ctk.CTkFrame): """单行课程。""" def __init__(self, parent, course: dict, on_action, **kwargs): super().__init__(parent, height=40, corner_radius=6, **kwargs) self.course = course self.on_action = on_action self.pack(fill="x", padx=4, pady=2) is_sel = course.get("xkzt") == "9" kcmc = course.get("kcmc", "-") kch = course.get("kch", "-") xf = course.get("xf", "-") syrs = int(course.get("syrs", 0) or 0) pkrs = int(course.get("pkrs", 0) or 0) xqmc = course.get("xqmc", "-") ctk.CTkLabel(self, text="✅" if is_sel else "⬜", width=30, anchor="center", font=("", 14)).pack(side="left", padx=(8, 2)) txt = kcmc if len(kcmc) <= 18 else kcmc[:16] + ".." ctk.CTkLabel(self, text=txt, width=160, anchor="w", font=ctk.CTkFont(size=13, weight="bold")).pack(side="left", padx=4) ctk.CTkLabel(self, text=kch, width=80, anchor="center", font=("Consolas", 12)).pack(side="left", padx=4) ctk.CTkLabel(self, text=str(xf), width=40, anchor="center", font=("", 13)).pack(side="left", padx=2) cap_c = "#e74c3c" if syrs <= 5 else "#2ecc71" if syrs > 20 else "#f39c12" ctk.CTkLabel(self, text=f"{syrs}/{pkrs}", width=60, anchor="center", text_color=cap_c, font=("Consolas", 12)).pack(side="left", padx=4) short = xqmc[:4] if len(xqmc) > 4 else xqmc ctk.CTkLabel(self, text=short, width=60, anchor="center", font=("", 12)).pack(side="left", padx=2) if is_sel: btn = ctk.CTkButton(self, text="退课", width=60, height=28, fg_color="#e74c3c", hover_color="#c0392b", font=ctk.CTkFont(size=12), command=self._drop) else: btn = ctk.CTkButton(self, text="选课", width=60, height=28, fg_color="#2ecc71", hover_color="#27ae60", font=ctk.CTkFont(size=12), command=self._select) btn.pack(side="right", padx=(4, 8)) def _select(self): self.on_action("select", self.course) def _drop(self): self.on_action("drop", self.course) # ─── 主窗口 ────────────────────────────────────────────────────────────── class DgutGUI(ctk.CTk): def __init__(self): super().__init__() self.title("🎓 莞工教务选课系统") self.geometry("920x720") self.minsize(800, 600) self.is_loading = False self.courses: list[dict] = [] self.filtered: list[dict] = [] self.tabs: list[dict] = [] self.current_tab: dict = {} self.cycle_id: str = "" self._build_ui() self.after(500, self._init_load) # ─── 界面 ────────────────────────────────────────────────────────── def _build_ui(self): self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(2, weight=1) # 标题栏 tf = ctk.CTkFrame(self, height=48, corner_radius=0) tf.grid(row=0, column=0, sticky="ew") tf.grid_columnconfigure(1, weight=1) ctk.CTkLabel(tf, text="🎓 莞工教务选课系统", font=ctk.CTkFont(size=20, weight="bold")).grid(row=0, column=0, padx=20, pady=8) self.cycle_lbl = ctk.CTkLabel(tf, text="选课轮次: 加载中...", font=ctk.CTkFont(size=13)) self.cycle_lbl.grid(row=0, column=1, padx=10, sticky="w") self.stat_lbl = ctk.CTkLabel(tf, text="📊 -", font=ctk.CTkFont(size=13)) self.stat_lbl.grid(row=0, column=2, padx=20, sticky="e") # 工具栏 tb = ctk.CTkFrame(self, height=40, corner_radius=0) tb.grid(row=1, column=0, sticky="ew", pady=2) tb.grid_columnconfigure(3, weight=1) self.refresh_btn = ctk.CTkButton(tb, text="🔄 刷新", font=ctk.CTkFont(size=13), command=self._refresh) self.refresh_btn.grid(row=0, column=0, padx=(10, 4), pady=4) ctk.CTkLabel(tb, text="课程类型:", font=ctk.CTkFont(size=13)).grid(row=0, column=1, padx=(10, 2)) self.tab_var = ctk.StringVar(value="加载中...") self.tab_menu = ctk.CTkOptionMenu(tb, variable=self.tab_var, values=["加载中..."], command=self._on_tab_switch, width=210, font=ctk.CTkFont(size=13)) self.tab_menu.grid(row=0, column=2, padx=4) self.sv = ctk.StringVar() self.sv.trace_add("write", lambda *_: self._filter()) ctk.CTkEntry(tb, textvariable=self.sv, placeholder_text="🔍 搜索...", width=200).grid(row=0, column=3, padx=(20, 4), pady=4, sticky="e") ctk.CTkButton(tb, text="✕", width=36, font=ctk.CTkFont(size=12), command=lambda: self.sv.set("")).grid(row=0, column=4) # 课程列表 lc = ctk.CTkFrame(self) lc.grid(row=2, column=0, sticky="nsew", padx=10, pady=(5, 0)) lc.grid_columnconfigure(0, weight=1) lc.grid_rowconfigure(1, weight=1) hdr = ctk.CTkFrame(lc, height=32, corner_radius=6, fg_color=("gray85", "gray20")) hdr.grid(row=0, column=0, sticky="ew", padx=4, pady=(4, 2)) for ci, (t, w) in enumerate([("状态", 50), ("课程名称", 1), ("编号", 85), ("学分", 48), ("容量", 68), ("校区", 68), ("操作", 80)]): kw = {"text": t, "font": ctk.CTkFont(size=12, weight="bold")} if w == 1: kw["anchor"] = "w" lbl = ctk.CTkLabel(hdr, **kw) lbl.grid(row=0, column=ci, padx=2, sticky="ew") hdr.grid_columnconfigure(ci, weight=1) else: kw["width"] = w kw["anchor"] = "center" ctk.CTkLabel(hdr, **kw).grid(row=0, column=ci, padx=2) self.sf = ctk.CTkScrollableFrame(lc, corner_radius=6) self.sf.grid(row=1, column=0, sticky="nsew", padx=4, pady=(0, 4)) self.sf.grid_columnconfigure(0, weight=1) # 日志 lf = ctk.CTkFrame(self, height=110, corner_radius=0) lf.grid(row=3, column=0, sticky="ew", pady=(5, 0)) lf.grid_columnconfigure(0, weight=1) ctk.CTkLabel(lf, text="📝 操作日志", font=ctk.CTkFont(size=12, weight="bold")).grid(row=0, column=0, padx=15, pady=(5, 0), sticky="w") self.log = ctk.CTkTextbox(lf, height=60, corner_radius=6, font=ctk.CTkFont(family="Consolas", size=11)) self.log.grid(row=1, column=0, sticky="ew", padx=10, pady=(2, 8)) self.log.configure(state="disabled") def _log(self, msg): def _do(): self.log.configure(state="normal") self.log.insert("end", f"[{time.strftime('%H:%M:%S')}] {msg}\n") self.log.see("end") self.log.configure(state="disabled") self.after(0, _do) def _loading(self, on: bool): self.is_loading = on st = "disabled" if on else "normal" txt = "⏳ ..." if on else "🔄 刷新" self.after(0, lambda: self.refresh_btn.configure(state=st, text=txt)) # ─── 数据 ────────────────────────────────────────────────────────── def _init_load(self): self._log("🔄 连接教务系统...") threading.Thread(target=self._load_cycles, daemon=True).start() def _refresh(self): if not self.is_loading: self._log("🔄 刷新...") threading.Thread(target=self._load_cycles, daemon=True).start() def _load_cycles(self): self._loading(True) try: cycles = get_cycles() if not cycles: self._log("⚠️ 无可用选课轮次") self.after(0, lambda: self.cycle_lbl.configure(text="选课轮次: 暂无")) return cyc = next((c for c in cycles if c.get("xkzt") == "1"), cycles[0]) self.cycle_id = cyc.get("jx0502zbid", "") info = f"{cyc.get('xnxq01id', '')} {cyc.get('xklc_mc', '')}" self.after(0, lambda: self.cycle_lbl.configure(text=f"选课轮次: {info}")) self._log(f"✅ {info}") # 探测标签 if self.cycle_id: self._log("🔍 探测选课类型标签页...") self.tabs = discover_tabs(self.cycle_id) if self.tabs: labels = [f"{t['label']} {'✅' if t.get('api') else '📄'}" for t in self.tabs] has_api = [t for t in self.tabs if t.get("api")] def _update(): self.tab_menu.configure(values=labels) if has_api: idx = self.tabs.index(has_api[0]) self.tab_var.set(labels[idx]) self._on_tab_switch(labels[idx]) else: self.tab_var.set(labels[0]) self.after(0, _update) self._log(f"📑 {len(self.tabs)} 个标签页: {', '.join(t['label'] for t in self.tabs)}") else: self._log("⚠️ 未探测到标签页") except RuntimeError as e: self._log(f"❌ {e}") finally: self._loading(False) def _on_tab_switch(self, selected: str): label = selected.split(" ✅")[0].split(" 📄")[0] tab = next((t for t in self.tabs if t["label"] == label), None) if not tab: return self.current_tab = tab if not tab.get("api"): self._log(f"📄 {label} 为信息页") self.courses = [] self.after(0, self._render) return self._log(f"📋 加载 {label}...") threading.Thread(target=self._load_courses, args=(tab,), daemon=True).start() def _load_courses(self, tab: dict): self._loading(True) try: ref = f"{ORIGIN}{tab['iframe']}" courses = get_courses(tab["api"], ref) self.courses = courses self.sv.set("") sel = [c for c in courses if c.get("xkzt") == "9"] sc = sum(float(c.get("xf", 0)) for c in sel) self.after(0, lambda: self.stat_lbl.configure( text=f"📊 已选 {sc:.1f}学分/{len(sel)}门 | 共{len(courses)}门")) self._log(f"📊 {len(courses)}门,已选{len(sel)}门({sc:.1f}学分)") self.after(0, self._render) except RuntimeError as e: self._log(f"❌ {e}") finally: self._loading(False) def _filter(self): kw = self.sv.get().strip().lower() if not kw: self.filtered = self.courses else: self.filtered = [c for c in self.courses if kw in c.get("kcmc", "").lower() or kw in c.get("kch", "").lower()] self._render() def _render(self): for w in self.sf.winfo_children(): w.destroy() disp = self.filtered if self.sv.get().strip() else self.courses if not self.courses: ctk.CTkLabel(self.sf, text="📭 暂无课程数据", font=ctk.CTkFont(size=14)).pack(pady=60) elif not disp: ctk.CTkLabel(self.sf, text="📭 无匹配课程", font=ctk.CTkFont(size=14)).pack(pady=60) else: for c in disp: CourseRow(self.sf, c, self._on_action) # ─── 操作 ────────────────────────────────────────────────────────── def _on_action(self, action: str, course: dict): if self.is_loading or not self.current_tab: return ref = f"{ORIGIN}{self.current_tab['iframe']}" kcid, kcmc, jxid = course["kch"], course["kcmc"], course["jx0404id"] self._log(f"📤 {'选课' if action == 'select' else '退课'}: {kcmc}") t = threading.Thread(target=self._do, args=(action, kcid, jxid, kcmc, ref), daemon=True) t.start() def _do(self, action: str, kcid: str, jxid: str, kcmc: str, ref: str): self._loading(True) try: fn = select_course if action == "select" else drop_course args = (kcid, jxid, ref) if action == "select" else (jxid, ref) result = fn(*args) if result.get("success"): self._log(f"✅ {kcmc}: {'选课成功' if action == 'select' else '退课成功'}") time.sleep(0.5) if self.current_tab: self.after(0, lambda: self._on_tab_switch(self.tab_var.get())) else: self._log(f"❌ {kcmc}: {result.get('message', '失败')}") except RuntimeError as e: self._log(f"❌ 出错: {e}") finally: self._loading(False) if __name__ == "__main__": DgutGUI().mainloop()