This commit is contained in:
2026-04-28 15:13:03 +08:00
commit 876712fe7e
6 changed files with 1015 additions and 0 deletions

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# PyInstaller
dist/
build/
*.spec
# Virtual environments
venv/
.venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
*.txt
# Excel files (示例数据)
老师信息示例.xlsx
学生信息示例.xlsx
匹配结果示例.xlsx
# Certificates
*.pfx
*.p12
*.cer
*.crt
# Build artifacts
*.exe
*.msi
*.zip
# Documentation (auto-generated)
*.html
*.md

51
build.bat Normal file
View File

@@ -0,0 +1,51 @@
@echo off
echo 开始优化打包配置...
pip install pyinstaller pandas openpyxl flet
echo 清理旧文件...
if exist "dist" rmdir /s /q "dist"
if exist "build" rmdir /s /q "build"
if exist "main.spec" del "main.spec"
echo 开始打包(优化模式)...
pyinstaller --onefile --windowed ^
--exclude-module=torch ^
--exclude-module=torchvision ^
--exclude-module=transformers ^
--exclude-module=onnxruntime ^
--exclude-module=scipy ^
--exclude-module=IPython ^
--exclude-module=pytest ^
--exclude-module=pil ^
--exclude-module=PIL ^
--exclude-module=cv2 ^
--exclude-module=sklearn ^
--exclude-module=nltk ^
--exclude-module=spacy ^
--exclude-module=plotly ^
--exclude-module=matplotlib ^
--exclude-module=seaborn ^
--exclude-module=sympy ^
--exclude-module=statsmodels ^
--exclude-module=pandas.plotting ^
--exclude-module=pandas.io.formats ^
--exclude-module=jinja2 ^
--exclude-module=packaging ^
--exclude-module=aiohttp ^
--exclude-module=websockets ^
main.py
echo 复制示例文件...
if not exist "dist" mkdir "dist"
if exist "老师信息示例.xlsx" copy "老师信息示例.xlsx" "dist\" /Y
if exist "学生信息示例.xlsx" copy "学生信息示例.xlsx" "dist\" /Y
echo.
echo ========================================
echo 打包完成!
echo ========================================
echo.
dir dist
echo.
pause

106
calculate_matches.py Normal file
View File

@@ -0,0 +1,106 @@
import pandas as pd
# 读取数据
teachers_df = pd.read_excel('老师信息示例.xlsx')
students_df = pd.read_excel('学生信息示例.xlsx')
# 准备数据
teachers = []
for _, row in teachers_df.iterrows():
teacher = {'id': row['老师ID'], 'name': row['姓名']}
availability = {}
for col in teachers_df.columns:
if col not in ['老师ID', '姓名', '其他信息']:
time_slots = []
if isinstance(row[col], str):
for slot in row[col].split(';'):
slot = slot.strip()
if slot:
start, end = slot.split('-')
time_slots.append((start, end))
availability[col] = time_slots
teacher['availability'] = availability
teachers.append(teacher)
students = []
for _, row in students_df.iterrows():
student = {'id': row['学生ID'], 'name': row['姓名']}
history = []
if isinstance(row['历史匹配老师'], str):
history = [t.strip() for t in row['历史匹配老师'].split(',')]
student['history'] = history
availability = {}
for col in students_df.columns:
if col not in ['学生ID', '姓名', '历史匹配老师', '其他信息']:
time_slots = []
if isinstance(row[col], str):
for slot in row[col].split(';'):
slot = slot.strip()
if slot:
start, end = slot.split('-')
time_slots.append((start, end))
availability[col] = time_slots
student['availability'] = availability
students.append(student)
# 创建老师字典,方便查找
teachers_dict = {teacher['id']: teacher for teacher in teachers}
# 收集所有可能的匹配
possible_matches = []
for student in students:
for teacher in teachers:
if teacher['id'] in student['history']:
continue
for day, student_slots in student['availability'].items():
if day not in teacher['availability']:
continue
teacher_slots = teacher['availability'][day]
for s_slot in student_slots:
if s_slot in teacher_slots:
possible_matches.append((student['id'], student['name'], teacher['id'], teacher['name'], day, s_slot))
# 为每个学生分配一个匹配
# 使用贪心算法,优先匹配可用选项最少的学生
students_sorted = sorted(students, key=lambda s: len([m for m in possible_matches if m[0] == s['id']]))
matched_teachers = set()
matched_slots = set()
current_matches = []
for student in students_sorted:
# 找到该学生的所有可能匹配,排除已匹配的老师和时间段
available_matches = []
for match in possible_matches:
if match[0] == student['id'] and match[2] not in matched_teachers:
slot_key = (match[2], match[4], match[5])
if slot_key not in matched_slots:
available_matches.append(match)
# 选择一个匹配
if available_matches:
# 选择一个老师,该老师还有最多的可用时间段
remaining_teachers = {}
for match in available_matches:
teacher_id = match[2]
remaining_slots = 0
for day, slots in teachers_dict[teacher_id]['availability'].items():
for slot in slots:
if (teacher_id, day, slot) not in matched_slots:
remaining_slots += 1
remaining_teachers[match] = remaining_slots
# 选择剩余时间段最多的老师
best_match_for_student = max(remaining_teachers, key=remaining_teachers.get)
current_matches.append(best_match_for_student)
matched_teachers.add(best_match_for_student[2])
matched_slots.add((best_match_for_student[2], best_match_for_student[4], best_match_for_student[5]))
# 输出结果
print('最优匹配结果:')
print('-' * 80)
print(f'{"学生ID":<10} {"学生姓名":<10} {"老师ID":<10} {"老师姓名":<10} {"日期":<10} {"时间段":<15}')
print('-' * 80)
for match in current_matches:
print(f'{match[0]:<10} {match[1]:<10} {match[2]:<10} {match[3]:<10} {match[4]:<10} {match[5][0]}-{match[5][1]:<15}')
print('-' * 80)
print(f'总匹配数: {len(current_matches)}')

61
generate_test_data.py Normal file
View File

@@ -0,0 +1,61 @@
import pandas as pd
import random
random.seed(42)
days = ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日']
time_slots = ['9:00-10:30', '10:30-12:00', '14:00-15:30', '15:30-17:00', '18:00-19:30']
teacher_count = 30
student_count = 100
teacher_names = [f"{i}老师" for i in range(1, teacher_count + 1)]
teacher_types = []
for i in range(teacher_count):
if i < teacher_count * 0.4:
teacher_types.append("")
else:
teacher_types.append("")
random.shuffle(teacher_types)
teachers = []
for i, name in enumerate(teacher_names):
teacher = {"老师姓名": name}
teacher["老师类型"] = teacher_types[i]
available_days = random.sample(days, random.randint(1, 3))
for day in days:
if day in available_days:
num_slots = random.randint(1, 3)
teacher[day] = '; '.join(random.sample(time_slots, num_slots))
else:
teacher[day] = ''
teachers.append(teacher)
df_teachers = pd.DataFrame(teachers)
df_teachers.to_excel('老师信息示例.xlsx', index=False)
print(f"已生成 {teacher_count} 位老师数据")
student_names = [f"学生{i}" for i in range(1, student_count + 1)]
students = []
for i, name in enumerate(student_names):
student = {"学生姓名": name}
available_days = random.sample(days, random.randint(2, 5))
for day in days:
if day in available_days:
num_slots = random.randint(1, 3)
student[day] = '; '.join(random.sample(time_slots, num_slots))
else:
student[day] = ''
history_count = min(5, i)
available_teachers = [t for t in teacher_names if t != name]
if available_teachers:
history_teachers = random.sample(available_teachers, history_count)
student["历史匹配老师"] = ', '.join(history_teachers)
else:
student["历史匹配老师"] = ""
students.append(student)
df_students = pd.DataFrame(students)
df_students.to_excel('学生信息示例.xlsx', index=False)
print(f"已生成 {student_count} 名学生数据")

69
installer.iss Normal file
View File

@@ -0,0 +1,69 @@
; Inno Setup Script for Teacher-Student Matching System
; Version 1.0
#define MyAppName "Teacher Student Matcher"
#define MyAppVersion "1.0"
#define MyAppPublisher "Teacher Student Matcher"
#define MyAppExeName "main.exe"
#define MyAppURL "https://github.com/teacher-student-matcher"
[Setup]
; Application Info
AppId={{8E5F2D1A-4C6B-4F3E-9A1D-7B8E2F5C4A3D}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
; Install Directory
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
; Output Settings
OutputDir=dist\output
OutputBaseFilename=TeacherStudentMatcher_Setup_v{#MyAppVersion}
Compression=lzma2
SolidCompression=yes
; Windows Version
MinVersion=10.0
; UI Settings
WizardStyle=modern
WizardResizable=no
; 64-bit
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "dist\main.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "dist\老师信息示例.xlsx"; DestDir: "{app}"; Flags: ignoreversion
Source: "dist\学生信息示例.xlsx"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
[Code]
function InitializeSetup(): Boolean;
begin
Result := True;
end;

676
main.py Normal file
View File

@@ -0,0 +1,676 @@
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)