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