Files
HuiPaiKe/main.py

680 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import flet as ft
import pandas as pd
import re
import threading
import asyncio
C_PRIMARY = "#2563eb"
C_PRIMARY_HOVER = "#1d4ed8"
C_BG = "#f1f5f9"
C_CARD = "#ffffff"
C_TEXT = "#1e293b"
C_TEXT_SEC = "#64748b"
C_TEXT_MUTED = "#94a3b8"
C_BORDER = "#e2e8f0"
C_SUCCESS = "#22c55e"
C_WARNING = "#f59e0b"
C_ERROR = "#ef4444"
C_ROW_ALT = "#f8fafc"
C_TAG_BG = "#f1f5f9"
def _btn(text, on_click, primary=False, icon=None):
btn = ft.Button(
content=ft.Row(
controls=[ft.Icon(icon, size=16) if icon else None, ft.Text(text, size=13)],
spacing=6,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
on_click=on_click,
height=38,
style=ft.ButtonStyle(
color="#ffffff" if primary else C_TEXT,
bgcolor=C_PRIMARY if primary else None,
shape=ft.RoundedRectangleBorder(radius=8),
side=ft.BorderSide(1, C_PRIMARY if primary else C_BORDER),
padding=ft.Padding.symmetric(horizontal=16, vertical=0),
),
)
return btn
def _card(controls, **kwargs):
return ft.Container(
content=ft.Column(controls=controls, spacing=0, **kwargs),
bgcolor=C_CARD,
border_radius=12,
border=ft.Border.all(1, C_BORDER),
expand=True,
)
def _card_header(title, subtitle=None, trailing=None):
row = [ft.Text(title, size=14, weight=ft.FontWeight.W_600, color=C_TEXT)]
if subtitle:
row.append(ft.Text(subtitle, size=11, color=C_TEXT_MUTED))
header_content = ft.Column(
controls=[ft.Row(row, spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER)],
spacing=0,
)
parts = [header_content]
if trailing:
parts.insert(0, trailing)
header = ft.Container(
content=ft.Row(parts, alignment=ft.MainAxisAlignment.SPACE_BETWEEN, vertical_alignment=ft.CrossAxisAlignment.CENTER) if trailing else header_content,
padding=ft.Padding.only(left=16, right=16, top=10, bottom=8),
)
line = ft.Container(height=1, bgcolor=C_BORDER)
return header, line
def _table(columns, data_rows):
header_cells = [ft.DataColumn(ft.Text(c, size=12, weight=ft.FontWeight.W_600, color=C_TEXT_SEC)) for c in columns]
rows = []
for i, row_data in enumerate(data_rows):
bg = C_ROW_ALT if i % 2 == 0 else C_CARD
cells = [ft.DataCell(c) for c in row_data]
rows.append(ft.DataRow(cells=cells, color=bg))
return ft.DataTable(
columns=header_cells,
rows=rows,
border=ft.Border.all(0, C_BORDER),
vertical_lines=ft.BorderSide(0, C_BORDER),
horizontal_lines=ft.BorderSide(1, C_BORDER),
heading_row_height=44,
data_row_min_height=42,
data_row_max_height=42,
column_spacing=16,
horizontal_margin=8,
)
class TeacherStudentMatcher:
def __init__(self, page: ft.Page):
self.page = page
self.page.title = "老师学员匹配系统"
self.page.window_width = 1400
self.page.window_height = 840
self.page.bgcolor = C_BG
self.page.padding = 0
self.page.theme = ft.Theme(
font_family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif"
)
self.page.theme_mode = ft.ThemeMode.LIGHT
self.teachers = []
self.students = []
self.matches = []
self.logs = []
self.match_conditions = {
"time_match": True,
"all_day_match": True,
"history_avoidance": True,
"daily_unique": True,
"balanced_assignment": True,
"teacher_type_rule": True,
}
self._cond_refs = {}
self._build_ui()
def _build_ui(self):
self.page.clean()
top_bar = ft.Container(
content=ft.Row(
controls=[
ft.Row(
controls=[
ft.Container(
content=ft.Text("TS", size=16, weight=ft.FontWeight.W_800, color="#ffffff", text_align=ft.TextAlign.CENTER),
width=36, height=36, border_radius=8,
bgcolor=C_PRIMARY,
),
ft.Column(
controls=[
ft.Text("老师学员匹配系统", size=18, weight=ft.FontWeight.W_700, color=C_TEXT),
ft.Text("Teacher-Student Matching System", size=11, color=C_TEXT_MUTED),
],
spacing=1,
),
],
spacing=12,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
ft.Row(
controls=[
_btn("导入老师", self.import_teachers, icon=ft.Icons.UPLOAD_FILE_ROUNDED),
_btn("导入学生", self.import_students, icon=ft.Icons.UPLOAD_FILE_ROUNDED),
ft.Container(width=1, height=24, bgcolor=C_BORDER),
_btn("开始匹配", self.start_match, primary=True, icon=ft.Icons.PLAY_ARROW_ROUNDED),
_btn("导出结果", self.export_results, icon=ft.Icons.DOWNLOAD_ROUNDED),
_btn("清空", self.clear_all, icon=ft.Icons.DELETE_OUTLINE_ROUNDED),
],
spacing=8,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
padding=ft.Padding.only(left=24, right=24, top=10, bottom=10),
bgcolor=C_CARD,
border=ft.Border(bottom=ft.BorderSide(1, C_BORDER)),
)
cond_items = [
("time_match", "时间匹配"),
("all_day_match", "全天匹配"),
("history_avoidance", "历史规避"),
("daily_unique", "每日唯一"),
("balanced_assignment", "均衡分配"),
("teacher_type_rule", "新旧交替"),
]
self._cond_refs = {}
cond_controls = []
for key, label in cond_items:
sw = ft.Switch(
value=self.match_conditions[key],
on_change=lambda e, k=key: self._on_condition_change(k, e),
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] = sw
cond_controls.append(
ft.Container(
content=ft.Row(
controls=[sw, ft.Text(label, size=11, color=C_TEXT)],
spacing=2,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
)
)
toolbar = ft.Container(
content=ft.Row(
controls=[
ft.Icon(ft.Icons.TUNE_ROUNDED, size=14, color=C_TEXT_SEC),
*cond_controls,
ft.Container(expand=True),
ft.TextButton(
"全选",
on_click=self._select_all_conditions,
style=ft.ButtonStyle(color=C_PRIMARY, padding=ft.Padding.symmetric(horizontal=4), visual_density=ft.VisualDensity.COMPACT),
),
ft.TextButton(
"清空",
on_click=self._deselect_all_conditions,
style=ft.ButtonStyle(color=C_TEXT_MUTED, padding=ft.Padding.symmetric(horizontal=4), visual_density=ft.VisualDensity.COMPACT),
),
],
spacing=8,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
padding=ft.Padding.only(left=12, right=8, top=2, bottom=2),
margin=ft.Margin.only(left=24, right=24, top=0, bottom=4),
bgcolor=C_TAG_BG,
border_radius=6,
)
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)
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)
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)
main_columns = ft.Row(
controls=[
ft.Container(content=teacher_card, expand=True, margin=ft.Margin.only(right=8)),
ft.Container(content=student_card, expand=True, margin=ft.Margin.only(left=8, right=8)),
ft.Container(content=result_card, expand=True, margin=ft.Margin.only(left=8)),
],
spacing=0,
expand=True,
vertical_alignment=ft.CrossAxisAlignment.STRETCH,
)
self.log_list = ft.ListView(controls=[], spacing=1, padding=ft.Padding.only(left=12, right=12, top=2, bottom=4), height=80)
self._log_expanded = False
self._log_body = ft.Container(content=self.log_list, height=0, clip_behavior=ft.ClipBehavior.NONE)
self._log_toggle_icon = ft.Icon(ft.Icons.EXPAND_MORE_ROUNDED, size=14, color=C_TEXT_MUTED)
self._log_count_text = ft.Text("0", size=10, color=C_TEXT_MUTED)
self._log_bar = ft.Container(
content=ft.Row(
controls=[
ft.Icon(ft.Icons.RECEIPT_LONG_ROUNDED, size=14, color=C_TEXT_MUTED),
ft.Text("日志", size=11, weight=ft.FontWeight.W_500, color=C_TEXT_MUTED),
self._log_count_text,
ft.Container(expand=True),
ft.TextButton("清空", on_click=self.clear_logs, style=ft.ButtonStyle(color=C_TEXT_MUTED, padding=ft.Padding.symmetric(horizontal=4), visual_density=ft.VisualDensity.COMPACT)),
self._log_toggle_icon,
],
spacing=4,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
padding=ft.Padding.only(left=12, right=4, top=2, bottom=2),
on_click=self._toggle_log,
bgcolor=C_CARD,
border=ft.Border(top=ft.BorderSide(1, C_BORDER)),
)
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"}
icon_name = getattr(ft.Icons, icons.get(level, "info").upper() + "_ROUNDED", ft.Icons.INFO_ROUNDED)
self.logs.append({"message": message, "level": level})
if len(self.logs) > 100:
self.logs = self.logs[-100:]
self.log_list.controls.append(
ft.Row(
controls=[
ft.Icon(icon_name, size=12, color=colors.get(level, C_TEXT_SEC)),
ft.Text(message, size=10, color=colors.get(level, C_TEXT_SEC)),
],
spacing=3,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
)
)
self._log_count_text.value = str(len(self.logs))
self.log_list.update()
self._log_count_text.update()
def clear_logs(self, e):
self.logs = []
self.log_list.controls = []
self._log_count_text.value = "0"
self.log_list.update()
self._log_count_text.update()
def _toggle_log(self, e=None):
self._log_expanded = not self._log_expanded
self._log_body.height = 100 if self._log_expanded else 0
self._log_body.opacity = 1 if self._log_expanded else 0
self._log_toggle_icon.name = ft.Icons.EXPAND_LESS_ROUNDED if self._log_expanded else ft.Icons.EXPAND_MORE_ROUNDED
self._log_body.update()
self._log_toggle_icon.update()
def _on_condition_change(self, key, e):
self.match_conditions[key] = e.control.value
labels = {"time_match": "时间匹配", "all_day_match": "全天匹配", "history_avoidance": "历史规避", "daily_unique": "每日唯一", "balanced_assignment": "均衡分配", "teacher_type_rule": "新旧交替"}
self.add_log(f"匹配条件 [{labels[key]}] 已{'开启' if e.control.value else '关闭'}", "info")
def _select_all_conditions(self, e):
for key in self.match_conditions:
self.match_conditions[key] = True
for key, ref in self._cond_refs.items():
ref.value = True
self.add_log("已全选匹配条件", "info")
def _deselect_all_conditions(self, e):
for key in self.match_conditions:
self.match_conditions[key] = False
for key, ref in self._cond_refs.items():
ref.value = False
self.add_log("已清空匹配条件", "warning")
def _ask_file(self, title):
from tkinter import filedialog, Tk
result = {}
def ask():
root = Tk()
root.withdraw()
root.attributes('-topmost', True)
result['path'] = filedialog.askopenfilename(title=title, filetypes=[("Excel", "*.xlsx"), ("All", "*.*")])
root.destroy()
t = threading.Thread(target=ask)
t.start()
t.join()
return result.get('path', '')
def _ask_save(self):
from tkinter import filedialog, Tk
result = {}
def ask():
root = Tk()
root.withdraw()
root.attributes('-topmost', True)
result['path'] = filedialog.asksaveasfilename(title="保存匹配结果", defaultextension=".xlsx", filetypes=[("Excel", "*.xlsx"), ("All", "*.*")])
root.destroy()
t = threading.Thread(target=ask)
t.start()
t.join()
return result.get('path', '')
def _show_snackbar(self, text, color=C_SUCCESS):
self.page.snack_bar = ft.SnackBar(content=ft.Text(text), bgcolor=color)
self.page.snack_bar.open = True
self.page.update()
async def import_teachers(self, e):
path = self._ask_file("选择老师信息文件")
if not path:
return
self._set_table_loading(self.teacher_table, 2, "正在导入老师信息...")
try:
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}
ttype = row.get("老师类型", "").strip()
if ttype in ["", "新老师"]:
ttype = ""
elif ttype in ["", "旧老师"]:
ttype = ""
else:
ttype = ""
teachers.append({"id": row["老师姓名"], "name": row["老师姓名"], "type": ttype, "availability": avail})
self.teachers = teachers
self._refresh_teacher_table()
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()
async def import_students(self, e):
path = self._ask_file("选择学生信息文件")
if not path:
return
self._set_table_loading(self.student_table, 2, "正在导入学生信息...")
try:
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}
students.append({"id": row["学生姓名"], "name": row["学生姓名"], "availability": avail, "history": history})
self.students = students
self._refresh_student_table()
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 = []
for i, t in enumerate(self.teachers):
bg = C_ROW_ALT if i % 2 == 0 else C_CARD
dot_color = C_PRIMARY if t["type"] == "" else C_SUCCESS
self.teacher_table.rows.append(ft.DataRow(
color=bg,
cells=[
ft.DataCell(ft.Text(t["name"], size=13, color=C_TEXT)),
ft.DataCell(ft.Row(
controls=[
ft.Container(width=6, height=6, border_radius=3, bgcolor=dot_color),
ft.Text(t["type"], size=12, color=dot_color),
],
spacing=4,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
)),
],
))
self.page.update()
def _refresh_student_table(self):
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"][:6])
if len(s["history"]) > 6:
h += f" +{len(s['history']) - 6}"
self.student_table.rows.append(ft.DataRow(
color=bg,
cells=[
ft.DataCell(ft.Text(s["name"], size=13, color=C_TEXT)),
ft.DataCell(ft.Text(h if h else "-", size=12, color=C_TEXT_SEC)),
],
))
self.page.update()
@staticmethod
def _parse_history(text):
parts = re.split(r"[,;;、/\\。.]", text)
return [t.strip() for t in parts if t.strip()]
@staticmethod
def _parse_slots(time_str):
if not time_str:
return []
slots = []
for slot in time_str.split(';'):
slot = slot.strip()
if slot and '-' in slot:
parts = slot.split('-')
if len(parts) == 2:
slots.append((parts[0].strip(), parts[1].strip()))
return slots
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 = dict(self.match_conditions)
active = [k for k, v in c.items() if v]
self.add_log(f"开始匹配 · 启用 {len(active)} 项规则", "info")
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)
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"]:
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
if c["daily_unique"] and day in teacher_days[teacher["id"]]:
continue
ttime = teacher["availability"][day]
if not stime or not ttime:
continue
s_slots = self._parse_slots(stime)
t_slots = self._parse_slots(ttime)
for ss, se in s_slots:
for ts, te in t_slots:
ok = False
if c["time_match"]:
if c["all_day_match"]:
ok = (ss == ts and se == te) or (ss == '00:00' and se == '24:00')
else:
ok = (ss == ts and se == te)
else:
ok = True
if ok:
possible.append((teacher, day, ts, te))
if possible:
if c["balanced_assignment"]:
possible.sort(key=lambda x: teacher_count[x[0]["id"]])
teacher, day, ts, te = possible[0]
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:
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)
self.status_text.value = f"成功 {ok_count} / {total}"
self.status_text.color = C_SUCCESS if ok_count == total else C_WARNING
self.page.update()
if ok_count == total:
self.add_log(f"匹配完成 · 全部 {ok_count} 人成功", "success")
else:
self.add_log(f"匹配完成 · {ok_count}/{total} 成功", "warning")
self._show_snackbar(f"匹配完成:{ok_count}/{total} 成功")
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 = []
for i, m in enumerate(self.matches):
ok = m["status"] == "成功"
bg = C_ROW_ALT if i % 2 == 0 else C_CARD
dot_color = C_SUCCESS if ok else C_ERROR
status_icon = ft.Icons.CHECK_CIRCLE_ROUNDED if ok else ft.Icons.CANCEL_ROUNDED
self.result_table.rows.append(ft.DataRow(
color=bg,
cells=[
ft.DataCell(ft.Text(m["student_name"], size=13, color=C_TEXT)),
ft.DataCell(ft.Text(m["teacher_name"], size=13, color=C_TEXT)),
ft.DataCell(ft.Text(m["match_date"], size=12, color=C_TEXT_SEC)),
ft.DataCell(ft.Text(m["match_period"], size=12, color=C_TEXT_SEC)),
ft.DataCell(ft.Row(
controls=[
ft.Icon(status_icon, size=14, color=dot_color),
ft.Text(m["status"], size=12, color=dot_color),
],
spacing=4,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
)),
],
))
self.page.update()
def export_results(self, e):
if not self.matches:
self._show_snackbar("没有匹配结果可导出", C_WARNING)
return
path = self._ask_save()
if not path:
return
try:
df = pd.DataFrame(self.matches)
df.columns = ["学生", "老师", "日期", "时段", "状态"]
df.to_excel(path, index=False)
self.add_log(f"导出成功: {path}", "success")
self._show_snackbar("导出成功")
except PermissionError:
self.add_log(f"导出失败: 权限不足", "error")
self._show_snackbar("导出失败: 权限不足", C_ERROR)
except Exception as ex:
self.add_log(f"导出失败: {ex}", "error")
self._show_snackbar(f"导出失败: {ex}", C_ERROR)
def clear_all(self, e):
self.teachers = []
self.students = []
self.matches = []
self.teacher_table.rows = []
self.student_table.rows = []
self.result_table.rows = []
self.status_text.value = "等待匹配…"
self.status_text.color = C_TEXT_MUTED
self.page.update()
self._show_snackbar("已清空所有数据")
def main(page: ft.Page):
TeacherStudentMatcher(page)
if __name__ == "__main__":
ft.run(main)