Files
HuiPaiKe/main.py
2026-04-28 15:13:03 +08:00

677 lines
26 KiB
Python

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)