From 0cacfeecf0410a004b34361c3b478ab113b737f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A9=E7=81=B5=E7=81=B5?= Date: Thu, 30 Apr 2026 16:01:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20loading=E6=8C=87=E7=A4=BA=E5=99=A8?= =?UTF-8?q?=E3=80=81Switch=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96=E3=80=81?= =?UTF-8?q?=E6=96=B0=E6=97=A7=E4=BA=A4=E6=9B=BF=E7=AE=97=E6=B3=95=E6=94=B9?= =?UTF-8?q?=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 174 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 66 deletions(-) diff --git a/main.py b/main.py index 716c967..8f4d9ed 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ import flet as ft import pandas as pd import re +import threading +import asyncio C_PRIMARY = "#2563eb" @@ -44,6 +46,7 @@ def _card(controls, **kwargs): bgcolor=C_CARD, border_radius=12, border=ft.Border.all(1, C_BORDER), + expand=True, ) @@ -173,20 +176,21 @@ class TeacherStudentMatcher: self._cond_refs = {} cond_controls = [] for key, label in cond_items: - cb = ft.Checkbox( + sw = ft.Switch( value=self.match_conditions[key], on_change=lambda e, k=key: self._on_condition_change(k, e), - fill_color=C_PRIMARY, - check_color="#ffffff", - shape=ft.RoundedRectangleBorder(radius=3), - height=20, - width=20, + active_color=C_PRIMARY, + inactive_track_color=C_BORDER, + active_track_color="#bfdbfe", + inactive_thumb_color="#ffffff", + height=24, + width=40, ) - self._cond_refs[key] = cb + self._cond_refs[key] = sw cond_controls.append( ft.Container( content=ft.Row( - controls=[cb, ft.Text(label, size=11, color=C_TEXT)], + controls=[sw, ft.Text(label, size=11, color=C_TEXT)], spacing=2, vertical_alignment=ft.CrossAxisAlignment.CENTER, ), @@ -221,40 +225,16 @@ class TeacherStudentMatcher: teacher_header, teacher_line = _card_header("老师信息", "Teacher Information") self.teacher_table = _table(["姓名", "类型"], []) - teacher_card = _card([ - teacher_header, - teacher_line, - ft.Container( - content=ft.ListView(controls=[self.teacher_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), - padding=ft.Padding.only(left=8, right=8, bottom=8), - expand=True, - ), - ], expand=True) + teacher_card = _card([teacher_header, teacher_line, ft.Container(content=ft.ListView(controls=[self.teacher_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), padding=ft.Padding.only(left=8, right=8, bottom=8), expand=True)], expand=True) student_header, student_line = _card_header("学生信息", "Student Information") self.student_table = _table(["姓名", "历史老师"], []) - student_card = _card([ - student_header, - student_line, - ft.Container( - content=ft.ListView(controls=[self.student_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), - padding=ft.Padding.only(left=8, right=8, bottom=8), - expand=True, - ), - ], expand=True) + student_card = _card([student_header, student_line, ft.Container(content=ft.ListView(controls=[self.student_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), padding=ft.Padding.only(left=8, right=8, bottom=8), expand=True)], expand=True) self.status_text = ft.Text("等待匹配…", size=12, color=C_TEXT_MUTED, weight=ft.FontWeight.W_400) result_header, result_line = _card_header("匹配结果", "Matching Results", trailing=self.status_text) self.result_table = _table(["学生", "老师", "日期", "时段", "状态"], []) - result_card = _card([ - result_header, - result_line, - ft.Container( - content=ft.ListView(controls=[self.result_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), - padding=ft.Padding.only(left=8, right=8, bottom=8), - expand=True, - ), - ], expand=True) + result_card = _card([result_header, result_line, ft.Container(content=ft.ListView(controls=[self.result_table], padding=ft.Padding.only(left=4, right=4, top=8, bottom=8), expand=True), padding=ft.Padding.only(left=8, right=8, bottom=8), expand=True)], expand=True) main_columns = ft.Row( controls=[ @@ -291,14 +271,37 @@ class TeacherStudentMatcher: border=ft.Border(top=ft.BorderSide(1, C_BORDER)), ) - self.page.add(top_bar) - self.page.add(toolbar) - self.page.add(main_columns) - self.page.add(self._log_bar) - self.page.add(self._log_body) + main_content = ft.Column( + controls=[top_bar, toolbar, main_columns, self._log_bar, self._log_body], + spacing=0, + expand=True, + ) + self.page.add(main_content) self.add_log("系统就绪", "info") + def _set_table_loading(self, table, col_count, text): + loading_cell = ft.DataCell( + ft.Container( + content=ft.Row( + controls=[ + ft.ProgressRing(width=16, height=16, stroke_width=2, color=C_PRIMARY), + ft.Text(text, size=11, color=C_TEXT_SEC, weight=ft.FontWeight.W_500), + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=8, + ), + padding=ft.Padding.all(24), + ) + ) + table.rows.clear() + table.rows.append(ft.DataRow(cells=[loading_cell] * col_count)) + self.page.update() + + def _clear_table_loading(self, table): + table.rows.clear() + self.page.update() + def add_log(self, message, level="info"): colors = {"info": C_TEXT_SEC, "success": C_SUCCESS, "warning": C_WARNING, "error": C_ERROR} icons = {"info": "info", "success": "check_circle", "warning": "warning", "error": "error"} @@ -355,7 +358,6 @@ class TeacherStudentMatcher: self.add_log("已清空匹配条件", "warning") def _ask_file(self, title): - import threading from tkinter import filedialog, Tk result = {} def ask(): @@ -370,7 +372,6 @@ class TeacherStudentMatcher: return result.get('path', '') def _ask_save(self): - import threading from tkinter import filedialog, Tk result = {} def ask(): @@ -389,13 +390,14 @@ class TeacherStudentMatcher: self.page.snack_bar.open = True self.page.update() - def import_teachers(self, e): + async def import_teachers(self, e): path = self._ask_file("选择老师信息文件") if not path: return + self._set_table_loading(self.teacher_table, 2, "正在导入老师信息...") try: - df = pd.read_excel(path) - self.teachers = [] + df = await asyncio.get_event_loop().run_in_executor(None, pd.read_excel, path) + teachers = [] fixed = ["老师姓名", "老师类型"] for _, row in df.iterrows(): avail = {col: str(row.get(col, "")) for col in df.columns if col not in fixed} @@ -406,33 +408,42 @@ class TeacherStudentMatcher: ttype = "旧" else: ttype = "旧" - self.teachers.append({"id": row["老师姓名"], "name": row["老师姓名"], "type": ttype, "availability": avail}) + teachers.append({"id": row["老师姓名"], "name": row["老师姓名"], "type": ttype, "availability": avail}) + self.teachers = teachers self._refresh_teacher_table() - self.add_log(f"导入 {len(self.teachers)} 位老师", "success") + self.add_log(f"导入 {len(self.teachers)} 位老师: {', '.join([t['name'] for t in self.teachers[:5]])}{'...' if len(self.teachers) > 5 else ''}", "success") self._show_snackbar(f"成功导入 {len(self.teachers)} 位老师") except Exception as ex: self.add_log(f"导入老师失败: {ex}", "error") self._show_snackbar(f"导入失败: {ex}", C_ERROR) + finally: + self._clear_table_loading(self.teacher_table) + self._refresh_teacher_table() - def import_students(self, e): + async def import_students(self, e): path = self._ask_file("选择学生信息文件") if not path: return + self._set_table_loading(self.student_table, 2, "正在导入学生信息...") try: - df = pd.read_excel(path) - self.students = [] + df = await asyncio.get_event_loop().run_in_executor(None, pd.read_excel, path) + students = [] fixed = ["学生姓名", "历史匹配老师"] for _, row in df.iterrows(): h = row.get("历史匹配老师", "") history = self._parse_history(str(h)) if pd.notna(h) and str(h).strip() else [] avail = {col: str(row.get(col, "")) for col in df.columns if col not in fixed} - self.students.append({"id": row["学生姓名"], "name": row["学生姓名"], "availability": avail, "history": history}) + students.append({"id": row["学生姓名"], "name": row["学生姓名"], "availability": avail, "history": history}) + self.students = students self._refresh_student_table() - self.add_log(f"导入 {len(self.students)} 名学生", "success") + self.add_log(f"导入 {len(self.students)} 名学生, 历史记录分布: {dict(sorted({k: sum(1 for s in self.students if len(s['history']) == k) for k in set(len(s['history']) for s in self.students)}.items()))}", "success") self._show_snackbar(f"成功导入 {len(self.students)} 名学生") except Exception as ex: self.add_log(f"导入学生失败: {ex}", "error") self._show_snackbar(f"导入失败: {ex}", C_ERROR) + finally: + self._clear_table_loading(self.student_table) + self._refresh_student_table() def _refresh_teacher_table(self): self.teacher_table.rows = [] @@ -459,9 +470,9 @@ class TeacherStudentMatcher: self.student_table.rows = [] for i, s in enumerate(self.students): bg = C_ROW_ALT if i % 2 == 0 else C_CARD - h = ", ".join(s["history"][:3]) - if len(s["history"]) > 3: - h += f" +{len(s['history']) - 3}" + h = ", ".join(s["history"][:6]) + if len(s["history"]) > 6: + h += f" +{len(s['history']) - 6}" self.student_table.rows.append(ft.DataRow( color=bg, cells=[ @@ -489,35 +500,56 @@ class TeacherStudentMatcher: slots.append((parts[0].strip(), parts[1].strip())) return slots - def start_match(self, e): + async def start_match(self, e): if not self.teachers: self._show_snackbar("请先导入老师信息", C_WARNING) return if not self.students: self._show_snackbar("请先导入学生信息", C_WARNING) return - + self._set_table_loading(self.result_table, 5, "正在匹配中...") try: - c = self.match_conditions + c = dict(self.match_conditions) active = [k for k, v in c.items() if v] self.add_log(f"开始匹配 · 启用 {len(active)} 项规则", "info") - self.matches = [] + matches = [] teacher_days = {t["id"]: set() for t in self.teachers} teacher_count = {t["id"]: 0 for t in self.teachers} student_hist = [list(s["history"]) for s in self.students] + teacher_type_map = {t["id"]: t["type"] for t in self.teachers} + for idx, student in enumerate(self.students): possible = [] hl = student_hist[idx] h_len = len(hl) - exp_type = "旧" if h_len == 0 else ("新" if h_len % 2 == 1 else "旧") + + if h_len == 0: + exp_type = "旧" + else: + last_teacher_name = hl[-1].strip().lower() + last_type = None + for name, ttype in teacher_type_map.items(): + if name.strip().lower() == last_teacher_name: + last_type = ttype + break + if last_type is None or last_type == "新": + exp_type = "旧" + else: + exp_type = "新" + + hl_normalized = {t.strip().lower() for t in hl} for teacher in self.teachers: - if c["history_avoidance"] and teacher["id"] in hl: - continue - if c["teacher_type_rule"] and teacher["type"] != exp_type: - continue + if c["history_avoidance"]: + tid_norm = teacher["id"].strip().lower() + if tid_norm in hl_normalized: + continue + + if c["teacher_type_rule"] and not (not c["history_avoidance"] and h_len > 0): + if teacher["type"] != exp_type: + continue for day, stime in student["availability"].items(): if day not in teacher["availability"]: continue @@ -545,14 +577,21 @@ class TeacherStudentMatcher: if c["balanced_assignment"]: possible.sort(key=lambda x: teacher_count[x[0]["id"]]) teacher, day, ts, te = possible[0] - self.matches.append({"student_name": student["name"], "teacher_name": teacher["name"], "match_date": day, "match_period": f"{ts}-{te}", "status": "成功"}) + + if c["history_avoidance"]: + tid_norm = teacher["id"].strip().lower() + if tid_norm in hl_normalized: + self.add_log(f"⚠ 历史规避异常: {student['name']} 历史有 {teacher['id']} 但仍被匹配", "warning") + + matches.append({"student_name": student["name"], "teacher_name": teacher["name"], "match_date": day, "match_period": f"{ts}-{te}", "status": "成功"}) student_hist[idx].append(teacher["id"]) if c["daily_unique"]: teacher_days[teacher["id"]].add(day) teacher_count[teacher["id"]] += 1 else: - self.matches.append({"student_name": student["name"], "teacher_name": "-", "match_date": "-", "match_period": "-", "status": "未匹配"}) + matches.append({"student_name": student["name"], "teacher_name": "-", "match_date": "-", "match_period": "-", "status": "未匹配"}) + self.matches = matches self._refresh_result_table() ok_count = len([m for m in self.matches if m["status"] == "成功"]) total = len(self.matches) @@ -569,6 +608,9 @@ class TeacherStudentMatcher: except Exception as ex: self.add_log(f"匹配失败: {ex}", "error") self._show_snackbar(f"匹配失败: {ex}", C_ERROR) + finally: + self._clear_table_loading(self.result_table) + self._refresh_result_table() def _refresh_result_table(self): self.result_table.rows = []