feat: 增加loading指示器、优化Switch样式、改进新旧交替算法
This commit is contained in:
174
main.py
174
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 = []
|
||||
|
||||
Reference in New Issue
Block a user