import flet as ft import pandas as pd import os class TeacherStudentMatcher: def __init__(self, page: ft.Page): self.page = page self.page.title = "老师学员匹配系统" self.page.window_width = 1400 self.page.window_height = 800 self.page.theme_mode = ft.ThemeMode.LIGHT self.page.padding = 0 self.page.theme = ft.Theme( font_family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif" ) self.teachers = [] self.students = [] self.matches = [] self.setup_ui() def setup_ui(self): self.page.clean() header = ft.Container( content=ft.Row( controls=[ ft.Column( controls=[ ft.Text("老师学员匹配系统", size=28, weight=ft.FontWeight.W_600, color="#1a1a2e"), ft.Text("Teacher-Student Matching System", size=12, weight=ft.FontWeight.W_400, color="#666666"), ], spacing=2, horizontal_alignment=ft.CrossAxisAlignment.START, ), ], alignment=ft.MainAxisAlignment.START, ), padding=ft.Padding.only(left=40, right=40, top=30, bottom=20), ) button_bar = ft.Container( content=ft.Row( controls=[ ft.Text("数据操作", size=12, weight=ft.FontWeight.W_500, color="#888888"), ft.Container(width=1, height=20, bgcolor="#e0e0e0"), ft.Button( "导入老师信息", on_click=self.import_teachers, bgcolor="#ffffff", color="#333333", height=36, elevation=0, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=6), side=ft.BorderSide(1, "#d0d0d0"), ), ), ft.Button( "导入学生信息", on_click=self.import_students, bgcolor="#ffffff", color="#333333", height=36, elevation=0, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=6), side=ft.BorderSide(1, "#d0d0d0"), ), ), ft.Container(width=1, height=20, bgcolor="#e0e0e0"), ft.Text("匹配操作", size=12, weight=ft.FontWeight.W_500, color="#888888"), ft.Container(width=1, height=20, bgcolor="#e0e0e0"), ft.Button( "开始匹配", on_click=self.start_match, bgcolor="#2563eb", color="#ffffff", height=36, elevation=0, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=6), ), ), ft.Button( "导出结果", on_click=self.export_results, bgcolor="#ffffff", color="#333333", height=36, elevation=0, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=6), side=ft.BorderSide(1, "#d0d0d0"), ), ), ft.Button( "清空", on_click=self.clear_all, bgcolor="#ffffff", color="#666666", height=36, elevation=0, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=6), side=ft.BorderSide(1, "#d0d0d0"), ), ), ], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER, ), padding=ft.Padding.only(left=40, right=40, top=10, bottom=20), ) divider1 = ft.Container(height=1, bgcolor="#f0f0f0") main_content = ft.Row( controls=[ ft.Container( content=ft.Column( controls=[ ft.Container( content=ft.Column( controls=[ ft.Text("老师信息", size=14, weight=ft.FontWeight.W_700, color="#1a1a2e"), ft.Text("Teacher Information", size=10, weight=ft.FontWeight.W_500, color="#888888"), ], spacing=4, ), padding=ft.Padding.only(left=24, right=24, top=20, bottom=16), ), ft.Container(height=1, bgcolor="#f5f5f5"), ft.Container( content=self.create_teacher_table(), padding=ft.Padding.only(left=16, right=16, bottom=16), expand=True, ), ], ), bgcolor="#ffffff", border_radius=12, border=ft.Border.all(1, "#f0f0f0"), expand=True, margin=ft.Margin.only(left=20, right=10, top=0, bottom=20), ), ft.Container( content=ft.Column( controls=[ ft.Container( content=ft.Column( controls=[ ft.Text("学生信息", size=14, weight=ft.FontWeight.W_700, color="#1a1a2e"), ft.Text("Student Information", size=10, weight=ft.FontWeight.W_500, color="#888888"), ], spacing=4, ), padding=ft.Padding.only(left=24, right=24, top=20, bottom=16), ), ft.Container(height=1, bgcolor="#f5f5f5"), ft.Container( content=self.create_student_table(), padding=ft.Padding.only(left=16, right=16, bottom=16), expand=True, ), ], ), bgcolor="#ffffff", border_radius=12, border=ft.Border.all(1, "#f0f0f0"), expand=True, margin=ft.Margin.only(left=10, right=10, top=0, bottom=20), ), ft.Container( content=ft.Column( controls=[ ft.Container( content=ft.Column( controls=[ ft.Text("匹配结果", size=14, weight=ft.FontWeight.W_700, color="#1a1a2e"), ft.Text("Matching Results", size=10, weight=ft.FontWeight.W_500, color="#888888"), ], spacing=4, ), padding=ft.Padding.only(left=24, right=24, top=20, bottom=16), ), ft.Container(height=1, bgcolor="#f5f5f5"), ft.Container( content=self.result_status_text(), padding=ft.Padding.only(left=24, right=24, top=12, bottom=8), ), ft.Container(height=1, bgcolor="#f5f5f5"), ft.Container( content=self.create_result_table(), padding=ft.Padding.only(left=16, right=16, bottom=16), expand=True, ), ], ), bgcolor="#ffffff", border_radius=12, border=ft.Border.all(1, "#f0f0f0"), expand=True, margin=ft.Margin.only(left=10, right=20, top=0, bottom=20), ), ], spacing=0, expand=True, vertical_alignment=ft.CrossAxisAlignment.START, ) self.page.add(header) self.page.add(button_bar) self.page.add(divider1) self.page.add(main_content) def create_teacher_table(self): self.teacher_table = ft.DataTable( columns=[ ft.DataColumn(ft.Text("姓名", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("类型", weight=ft.FontWeight.W_500, color="#555555")), ], rows=[], heading_row_color="#fafafa", data_row_color={"hovered": "#f8f9fa", "selected": "#f0f4ff"}, border=ft.Border.all(0, "#f0f0f0"), vertical_lines=ft.border.BorderSide(0, "#f0f0f0"), horizontal_lines=ft.border.BorderSide(1, "#f5f5f5"), column_spacing=30, heading_row_height=40, data_row_min_height=40, ) return ft.ListView( controls=[self.teacher_table], spacing=0, padding=0, expand=True, ) def create_student_table(self): self.student_table = ft.DataTable( columns=[ ft.DataColumn(ft.Text("姓名", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("历史老师", weight=ft.FontWeight.W_500, color="#555555")), ], rows=[], heading_row_color="#fafafa", data_row_color={"hovered": "#f8f9fa", "selected": "#f0f4ff"}, border=ft.Border.all(0, "#f0f0f0"), vertical_lines=ft.border.BorderSide(0, "#f0f0f0"), horizontal_lines=ft.border.BorderSide(1, "#f5f5f5"), column_spacing=20, heading_row_height=40, data_row_min_height=40, ) return ft.ListView( controls=[self.student_table], spacing=0, padding=0, expand=True, ) def result_status_text(self): self.status_text = ft.Text( "等待匹配...", size=12, weight=ft.FontWeight.W_400, color="#888888", ) return self.status_text def create_result_table(self): self.result_table = ft.DataTable( columns=[ ft.DataColumn(ft.Text("学生", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("老师", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("日期", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("时段", weight=ft.FontWeight.W_500, color="#555555")), ft.DataColumn(ft.Text("状态", weight=ft.FontWeight.W_500, color="#555555")), ], rows=[], heading_row_color="#fafafa", data_row_color={"hovered": "#f8f9fa", "selected": "#f0f4ff"}, border=ft.Border.all(0, "#f0f0f0"), vertical_lines=ft.border.BorderSide(0, "#f0f0f0"), horizontal_lines=ft.border.BorderSide(1, "#f5f5f5"), column_spacing=15, heading_row_height=40, data_row_min_height=40, ) return ft.ListView( controls=[self.result_table], spacing=0, padding=0, expand=True, ) def import_teachers(self, e): import threading from tkinter import filedialog import tkinter as tk result = {} def ask_file(): root = tk.Tk() root.withdraw() root.attributes('-topmost', True) file_path = filedialog.askopenfilename( title="选择老师信息文件", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")] ) result['path'] = file_path root.destroy() thread = threading.Thread(target=ask_file) thread.start() thread.join() file_path = result.get('path', '') if file_path: try: df = pd.read_excel(file_path) self.teachers = [] fixed_columns = ["老师姓名", "老师类型"] for _, row in df.iterrows(): availability = {} for col in df.columns: if col not in fixed_columns: availability[col] = str(row.get(col, "")) teacher_type = row.get("老师类型", "").strip() if teacher_type in ["新", "新老师"]: teacher_type = "新" elif teacher_type in ["旧", "旧老师"]: teacher_type = "旧" else: teacher_type = "旧" teacher = { "id": row.get("老师姓名", ""), "name": row.get("老师姓名", ""), "type": teacher_type, "availability": availability } self.teachers.append(teacher) self.update_teacher_table() self.page.snack_bar = ft.SnackBar( content=ft.Text(f"成功导入 {len(self.teachers)} 位老师"), bgcolor="#10b981", ) self.page.snack_bar.open = True except Exception as ex: self.page.snack_bar = ft.SnackBar( content=ft.Text(f"导入失败: {str(ex)}"), bgcolor="#ef4444", ) self.page.snack_bar.open = True def import_students(self, e): import threading from tkinter import filedialog import tkinter as tk result = {} def ask_file(): root = tk.Tk() root.withdraw() root.attributes('-topmost', True) file_path = filedialog.askopenfilename( title="选择学生信息文件", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")] ) result['path'] = file_path root.destroy() thread = threading.Thread(target=ask_file) thread.start() thread.join() file_path = result.get('path', '') if file_path: try: df = pd.read_excel(file_path) self.students = [] fixed_columns = ["学生姓名", "历史匹配老师"] for _, row in df.iterrows(): history = row.get("历史匹配老师", "") history_teachers = [t.strip() for t in history.split(",")] if isinstance(history, str) else [] availability = {} for col in df.columns: if col not in fixed_columns: availability[col] = str(row.get(col, "")) student = { "id": row.get("学生姓名", ""), "name": row.get("学生姓名", ""), "availability": availability, "history": history_teachers } self.students.append(student) self.update_student_table() self.page.snack_bar = ft.SnackBar( content=ft.Text(f"成功导入 {len(self.students)} 名学生"), bgcolor="#10b981", ) self.page.snack_bar.open = True except Exception as ex: self.page.snack_bar = ft.SnackBar( content=ft.Text(f"导入失败: {str(ex)}"), bgcolor="#ef4444", ) self.page.snack_bar.open = True def update_teacher_table(self): self.teacher_table.rows = [] for teacher in self.teachers: type_color = "#2563eb" if teacher["type"] == "新" else "#10b981" self.teacher_table.rows.append( ft.DataRow( cells=[ ft.DataCell(ft.Text(teacher["name"], size=13, weight=ft.FontWeight.W_500, color="#333333")), ft.DataCell(ft.Text(teacher["type"], size=12, weight=ft.FontWeight.W_600, color=type_color)), ] ) ) self.page.update() def update_student_table(self): self.student_table.rows = [] for student in self.students: history_str = ", ".join(student["history"][:3]) if len(student["history"]) > 3: history_str += "..." self.student_table.rows.append( ft.DataRow( cells=[ ft.DataCell(ft.Text(student["name"], size=13, weight=ft.FontWeight.W_500, color="#333333")), ft.DataCell(ft.Text(history_str, size=12, weight=ft.FontWeight.W_500, color="#666666")), ] ) ) self.page.update() def parse_time_slots(self, 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: start, end = parts slots.append((start, end)) return slots def start_match(self, e): if not self.teachers: self.page.snack_bar = ft.SnackBar( content=ft.Text("请先导入老师信息"), bgcolor="#f59e0b", ) self.page.snack_bar.open = True return if not self.students: self.page.snack_bar = ft.SnackBar( content=ft.Text("请先导入学生信息"), bgcolor="#f59e0b", ) self.page.snack_bar.open = True return try: self.matches = [] teacher_assigned_days = {} for teacher in self.teachers: teacher_assigned_days[teacher["id"]] = set() teacher_match_count = {} for teacher in self.teachers: teacher_match_count[teacher["id"]] = 0 for student in self.students: matched = False possible_matches = [] history_count = len(student["history"]) expected_teacher_type = "旧" if history_count == 0 else ("新" if history_count % 2 == 1 else "旧") for teacher in self.teachers: if teacher["id"] in student["history"]: continue if teacher["type"] != expected_teacher_type: continue for day, student_time in student["availability"].items(): if day not in teacher["availability"]: continue if day in teacher_assigned_days[teacher["id"]]: continue teacher_time = teacher["availability"][day] if student_time and teacher_time: student_slots = self.parse_time_slots(student_time) teacher_slots = self.parse_time_slots(teacher_time) for s_start, s_end in student_slots: for t_start, t_end in teacher_slots: if s_start == t_start and s_end == t_end: possible_matches.append((teacher, day, s_start, s_end)) if possible_matches: possible_matches.sort(key=lambda x: teacher_match_count[x[0]["id"]]) teacher, day, s_start, s_end = possible_matches[0] match = { "student_name": student["name"], "teacher_name": teacher["name"], "match_date": day, "match_period": f"{s_start}-{s_end}", "status": "成功" } self.matches.append(match) student["history"].append(teacher["id"]) teacher_assigned_days[teacher["id"]].add(day) teacher_match_count[teacher["id"]] += 1 matched = True if not matched: match = { "student_name": student["name"], "teacher_name": "-", "match_date": "-", "match_period": "-", "status": "未匹配" } self.matches.append(match) self.update_result_table() success_count = len([m for m in self.matches if m["status"] == "成功"]) self.status_text.value = f"匹配完成 · 成功 {success_count} / {len(self.matches)}" self.status_text.color = "#10b981" if success_count == len(self.matches) else "#f59e0b" self.page.snack_bar = ft.SnackBar( content=ft.Text(f"匹配完成!成功匹配 {success_count} 名学生"), bgcolor="#10b981", ) self.page.snack_bar.open = True except Exception as ex: self.page.snack_bar = ft.SnackBar( content=ft.Text(f"匹配失败: {str(ex)}"), bgcolor="#ef4444", ) self.page.snack_bar.open = True def update_result_table(self): self.result_table.rows = [] for match in self.matches: is_success = match["status"] == "成功" bg_color = "#f0fdf4" if is_success else "#fef2f2" status_color = "#10b981" if is_success else "#ef4444" self.result_table.rows.append( ft.DataRow( cells=[ ft.DataCell(ft.Text(match["student_name"], size=13, weight=ft.FontWeight.W_500, color="#333333")), ft.DataCell(ft.Text(match["teacher_name"], size=13, weight=ft.FontWeight.W_500, color="#333333")), ft.DataCell(ft.Text(match["match_date"], size=12, weight=ft.FontWeight.W_500, color="#555555")), ft.DataCell(ft.Text(match["match_period"], size=12, weight=ft.FontWeight.W_500, color="#666666")), ft.DataCell(ft.Container( content=ft.Text(match["status"], size=12, weight=ft.FontWeight.W_600, color=status_color), bgcolor=bg_color, border_radius=4, padding=ft.padding.symmetric(horizontal=8, vertical=2), )), ] ) ) self.page.update() def export_results(self, e): if not self.matches: self.page.snack_bar = ft.SnackBar( content=ft.Text("没有匹配结果可导出"), bgcolor="#f59e0b", ) self.page.snack_bar.open = True return import threading from tkinter import filedialog import tkinter as tk result = {} def ask_save(): root = tk.Tk() root.withdraw() root.attributes('-topmost', True) file_path = filedialog.asksaveasfilename( title="保存匹配结果", defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")] ) result['path'] = file_path root.destroy() thread = threading.Thread(target=ask_save) thread.start() thread.join() file_path = result.get('path', '') if not file_path: return try: df = pd.DataFrame(self.matches) df.columns = ["学生", "老师", "日期", "时段", "状态"] df.to_excel(file_path, index=False) self.page.snack_bar = ft.SnackBar( content=ft.Text(f"结果已导出到: {file_path}"), bgcolor="#10b981", ) self.page.snack_bar.open = True except Exception as ex: self.page.snack_bar = ft.SnackBar( content=ft.Text(f"导出失败: {str(ex)}"), bgcolor="#ef4444", ) self.page.snack_bar.open = True 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 = "#888888" self.page.snack_bar = ft.SnackBar( content=ft.Text("已清空所有数据"), bgcolor="#6b7280", ) self.page.snack_bar.open = True def main(page: ft.Page): TeacherStudentMatcher(page) if __name__ == "__main__": ft.run(main)