340 lines
14 KiB
Python
340 lines
14 KiB
Python
|
|
#!/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()
|