feat: loading指示器、Switch样式优化、新旧交替算法改进

This commit is contained in:
2026-04-30 16:01:03 +08:00
parent 304856429c
commit 0cacfeecf0

174
main.py
View File

@@ -1,6 +1,8 @@
import flet as ft import flet as ft
import pandas as pd import pandas as pd
import re import re
import threading
import asyncio
C_PRIMARY = "#2563eb" C_PRIMARY = "#2563eb"
@@ -44,6 +46,7 @@ def _card(controls, **kwargs):
bgcolor=C_CARD, bgcolor=C_CARD,
border_radius=12, border_radius=12,
border=ft.Border.all(1, C_BORDER), border=ft.Border.all(1, C_BORDER),
expand=True,
) )
@@ -173,20 +176,21 @@ class TeacherStudentMatcher:
self._cond_refs = {} self._cond_refs = {}
cond_controls = [] cond_controls = []
for key, label in cond_items: for key, label in cond_items:
cb = ft.Checkbox( sw = ft.Switch(
value=self.match_conditions[key], value=self.match_conditions[key],
on_change=lambda e, k=key: self._on_condition_change(k, e), on_change=lambda e, k=key: self._on_condition_change(k, e),
fill_color=C_PRIMARY, active_color=C_PRIMARY,
check_color="#ffffff", inactive_track_color=C_BORDER,
shape=ft.RoundedRectangleBorder(radius=3), active_track_color="#bfdbfe",
height=20, inactive_thumb_color="#ffffff",
width=20, height=24,
width=40,
) )
self._cond_refs[key] = cb self._cond_refs[key] = sw
cond_controls.append( cond_controls.append(
ft.Container( ft.Container(
content=ft.Row( 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, spacing=2,
vertical_alignment=ft.CrossAxisAlignment.CENTER, vertical_alignment=ft.CrossAxisAlignment.CENTER,
), ),
@@ -221,40 +225,16 @@ class TeacherStudentMatcher:
teacher_header, teacher_line = _card_header("老师信息", "Teacher Information") teacher_header, teacher_line = _card_header("老师信息", "Teacher Information")
self.teacher_table = _table(["姓名", "类型"], []) self.teacher_table = _table(["姓名", "类型"], [])
teacher_card = _card([ 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_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") student_header, student_line = _card_header("学生信息", "Student Information")
self.student_table = _table(["姓名", "历史老师"], []) self.student_table = _table(["姓名", "历史老师"], [])
student_card = _card([ 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_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) 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) result_header, result_line = _card_header("匹配结果", "Matching Results", trailing=self.status_text)
self.result_table = _table(["学生", "老师", "日期", "时段", "状态"], []) self.result_table = _table(["学生", "老师", "日期", "时段", "状态"], [])
result_card = _card([ 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_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( main_columns = ft.Row(
controls=[ controls=[
@@ -291,14 +271,37 @@ class TeacherStudentMatcher:
border=ft.Border(top=ft.BorderSide(1, C_BORDER)), border=ft.Border(top=ft.BorderSide(1, C_BORDER)),
) )
self.page.add(top_bar) main_content = ft.Column(
self.page.add(toolbar) controls=[top_bar, toolbar, main_columns, self._log_bar, self._log_body],
self.page.add(main_columns) spacing=0,
self.page.add(self._log_bar) expand=True,
self.page.add(self._log_body) )
self.page.add(main_content)
self.add_log("系统就绪", "info") 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"): def add_log(self, message, level="info"):
colors = {"info": C_TEXT_SEC, "success": C_SUCCESS, "warning": C_WARNING, "error": C_ERROR} colors = {"info": C_TEXT_SEC, "success": C_SUCCESS, "warning": C_WARNING, "error": C_ERROR}
icons = {"info": "info", "success": "check_circle", "warning": "warning", "error": "error"} icons = {"info": "info", "success": "check_circle", "warning": "warning", "error": "error"}
@@ -355,7 +358,6 @@ class TeacherStudentMatcher:
self.add_log("已清空匹配条件", "warning") self.add_log("已清空匹配条件", "warning")
def _ask_file(self, title): def _ask_file(self, title):
import threading
from tkinter import filedialog, Tk from tkinter import filedialog, Tk
result = {} result = {}
def ask(): def ask():
@@ -370,7 +372,6 @@ class TeacherStudentMatcher:
return result.get('path', '') return result.get('path', '')
def _ask_save(self): def _ask_save(self):
import threading
from tkinter import filedialog, Tk from tkinter import filedialog, Tk
result = {} result = {}
def ask(): def ask():
@@ -389,13 +390,14 @@ class TeacherStudentMatcher:
self.page.snack_bar.open = True self.page.snack_bar.open = True
self.page.update() self.page.update()
def import_teachers(self, e): async def import_teachers(self, e):
path = self._ask_file("选择老师信息文件") path = self._ask_file("选择老师信息文件")
if not path: if not path:
return return
self._set_table_loading(self.teacher_table, 2, "正在导入老师信息...")
try: try:
df = pd.read_excel(path) df = await asyncio.get_event_loop().run_in_executor(None, pd.read_excel, path)
self.teachers = [] teachers = []
fixed = ["老师姓名", "老师类型"] fixed = ["老师姓名", "老师类型"]
for _, row in df.iterrows(): for _, row in df.iterrows():
avail = {col: str(row.get(col, "")) for col in df.columns if col not in fixed} avail = {col: str(row.get(col, "")) for col in df.columns if col not in fixed}
@@ -406,33 +408,42 @@ class TeacherStudentMatcher:
ttype = "" ttype = ""
else: else:
ttype = "" 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._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)} 位老师") self._show_snackbar(f"成功导入 {len(self.teachers)} 位老师")
except Exception as ex: except Exception as ex:
self.add_log(f"导入老师失败: {ex}", "error") self.add_log(f"导入老师失败: {ex}", "error")
self._show_snackbar(f"导入失败: {ex}", C_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("选择学生信息文件") path = self._ask_file("选择学生信息文件")
if not path: if not path:
return return
self._set_table_loading(self.student_table, 2, "正在导入学生信息...")
try: try:
df = pd.read_excel(path) df = await asyncio.get_event_loop().run_in_executor(None, pd.read_excel, path)
self.students = [] students = []
fixed = ["学生姓名", "历史匹配老师"] fixed = ["学生姓名", "历史匹配老师"]
for _, row in df.iterrows(): for _, row in df.iterrows():
h = row.get("历史匹配老师", "") h = row.get("历史匹配老师", "")
history = self._parse_history(str(h)) if pd.notna(h) and str(h).strip() else [] 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} 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._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)} 名学生") self._show_snackbar(f"成功导入 {len(self.students)} 名学生")
except Exception as ex: except Exception as ex:
self.add_log(f"导入学生失败: {ex}", "error") self.add_log(f"导入学生失败: {ex}", "error")
self._show_snackbar(f"导入失败: {ex}", C_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): def _refresh_teacher_table(self):
self.teacher_table.rows = [] self.teacher_table.rows = []
@@ -459,9 +470,9 @@ class TeacherStudentMatcher:
self.student_table.rows = [] self.student_table.rows = []
for i, s in enumerate(self.students): for i, s in enumerate(self.students):
bg = C_ROW_ALT if i % 2 == 0 else C_CARD bg = C_ROW_ALT if i % 2 == 0 else C_CARD
h = ", ".join(s["history"][:3]) h = ", ".join(s["history"][:6])
if len(s["history"]) > 3: if len(s["history"]) > 6:
h += f" +{len(s['history']) - 3}" h += f" +{len(s['history']) - 6}"
self.student_table.rows.append(ft.DataRow( self.student_table.rows.append(ft.DataRow(
color=bg, color=bg,
cells=[ cells=[
@@ -489,35 +500,56 @@ class TeacherStudentMatcher:
slots.append((parts[0].strip(), parts[1].strip())) slots.append((parts[0].strip(), parts[1].strip()))
return slots return slots
def start_match(self, e): async def start_match(self, e):
if not self.teachers: if not self.teachers:
self._show_snackbar("请先导入老师信息", C_WARNING) self._show_snackbar("请先导入老师信息", C_WARNING)
return return
if not self.students: if not self.students:
self._show_snackbar("请先导入学生信息", C_WARNING) self._show_snackbar("请先导入学生信息", C_WARNING)
return return
self._set_table_loading(self.result_table, 5, "正在匹配中...")
try: try:
c = self.match_conditions c = dict(self.match_conditions)
active = [k for k, v in c.items() if v] active = [k for k, v in c.items() if v]
self.add_log(f"开始匹配 · 启用 {len(active)} 项规则", "info") self.add_log(f"开始匹配 · 启用 {len(active)} 项规则", "info")
self.matches = [] matches = []
teacher_days = {t["id"]: set() for t in self.teachers} teacher_days = {t["id"]: set() for t in self.teachers}
teacher_count = {t["id"]: 0 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] 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): for idx, student in enumerate(self.students):
possible = [] possible = []
hl = student_hist[idx] hl = student_hist[idx]
h_len = len(hl) 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: for teacher in self.teachers:
if c["history_avoidance"] and teacher["id"] in hl: if c["history_avoidance"]:
continue tid_norm = teacher["id"].strip().lower()
if c["teacher_type_rule"] and teacher["type"] != exp_type: if tid_norm in hl_normalized:
continue 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(): for day, stime in student["availability"].items():
if day not in teacher["availability"]: if day not in teacher["availability"]:
continue continue
@@ -545,14 +577,21 @@ class TeacherStudentMatcher:
if c["balanced_assignment"]: if c["balanced_assignment"]:
possible.sort(key=lambda x: teacher_count[x[0]["id"]]) possible.sort(key=lambda x: teacher_count[x[0]["id"]])
teacher, day, ts, te = possible[0] 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"]) student_hist[idx].append(teacher["id"])
if c["daily_unique"]: if c["daily_unique"]:
teacher_days[teacher["id"]].add(day) teacher_days[teacher["id"]].add(day)
teacher_count[teacher["id"]] += 1 teacher_count[teacher["id"]] += 1
else: 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() self._refresh_result_table()
ok_count = len([m for m in self.matches if m["status"] == "成功"]) ok_count = len([m for m in self.matches if m["status"] == "成功"])
total = len(self.matches) total = len(self.matches)
@@ -569,6 +608,9 @@ class TeacherStudentMatcher:
except Exception as ex: except Exception as ex:
self.add_log(f"匹配失败: {ex}", "error") self.add_log(f"匹配失败: {ex}", "error")
self._show_snackbar(f"匹配失败: {ex}", C_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): def _refresh_result_table(self):
self.result_table.rows = [] self.result_table.rows = []