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