init
This commit is contained in:
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal 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
51
build.bat
Normal 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
106
calculate_matches.py
Normal 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
61
generate_test_data.py
Normal 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
69
installer.iss
Normal 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
676
main.py
Normal 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)
|
||||||
Reference in New Issue
Block a user