This commit is contained in:
superlishunqin 2024-11-14 15:46:37 +08:00
parent 32cbad1698
commit 68b99755ec
43 changed files with 9510 additions and 548 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

20
.env
View File

@ -1,4 +1,24 @@
SECRET_KEY=6c9d28c778888d3fc7c459155e6593f8
# 邮箱配置
EMAIL_HOST=mail.sq0715.com
EMAIL_PORT=465
MAIL_USE_SSL=True
EMAIL_USERNAME=vip@sq0715.com
EMAIL_PASSWORD=Lsq12350501.
EMAIL_FROM_NAME=Qin
EMAIL_FROM=vip@sq0715.com
# AWS 配置
AWS_ACCESS_KEY_ID=AKIAZQ3DT3KLI6N5LQUM AWS_ACCESS_KEY_ID=AKIAZQ3DT3KLI6N5LQUM
AWS_SECRET_ACCESS_KEY=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 AWS_SECRET_ACCESS_KEY=R5eLA2TjGLZT77xgACiEP39Y7JnnbPjLzL64v0tT
AWS_REGION=ap-northeast-1 AWS_REGION=ap-northeast-1
S3_BUCKET_NAME=sure-ae-upload S3_BUCKET_NAME=sure-ae-upload
# MySQL 配置
MYSQL_HOST=8.218.165.242
MYSQL_USER=sure_001
MYSQL_PASSWORD=EKKWLMmrGmG7sdPf
MYSQL_DB=sure_001

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
myenv/

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
app.py

2
.idea/AWS-sure.iml generated
View File

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv/lib/python3.10/site-packages" /> <excludeFolder url="file://$MODULE_DIR$/venv/lib/python3.10/site-packages" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

2
.idea/misc.xml generated
View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (AWS-sure)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
</project> </project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# 文件提交系统
这是一个为广东理工学院开发的文件提交系统,旨在帮助学校老师更好地收集和管理学生作业。
## 功能特性
- 学生登录和文件上传
- 教师管理班级和作业
- 管理员管理系统用户和权限
- 文件上传到AWS S3存储
- 邮件验证码功能
- 作业提交统计和导出
## 技术栈
- 后端: Python Flask
- 数据库: MySQL
- 前端: HTML, CSS, JavaScript
- 云存储: AWS S3
- 其他: Flask-Mail, Flask-Bcrypt, Flask-Session
## 安装和设置
1. 克隆仓库:
git clone https://git.sq0715.com/qin/File_upload_system_GuangdongLiGong.git
2. 安装依赖:
pip install -r requirements.txt
3. 设置环境变量:
创建一个.env文件,包含必要的配置信息(参考.env.example)
4. 初始化数据库:
运行`python insert_data_to_database.py`
5. 运行应用:
python app.py
## 使用说明
- 学生: 通过学号和密码登录,选择作业并上传文件
- 教师: 登录后可以管理班级、添加作业、查看提交情况
- 管理员: 可以添加/管理专业、年级、班级、教师和其他管理员
## 贡献
欢迎提交问题和合并请求。对于重大更改,请先开issue讨论您想要更改的内容。

Binary file not shown.

208
app.py
View File

@ -1,182 +1,50 @@
from flask import Flask, request, jsonify, send_from_directory, make_response, send_file from flask import Flask, request, jsonify, redirect, url_for, render_template, session, make_response
from flask_cors import CORS from flask_cors import CORS
import boto3 from flask_mail import Mail
from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError from flask_bcrypt import Bcrypt
import os from flask_session import Session
from dotenv import load_dotenv from dotenv import load_dotenv
import os
import logging import logging
import datetime import app_function # 引入新的文件
import pytz
from botocore.client import Config # 载入环境变量
import csv load_dotenv()
import pandas as pd
# 初始化 Flask 应用
app = Flask(__name__, static_url_path='', static_folder='.')
CORS(app, resources={r"/*": {"origins": "*"}})
bcrypt = Bcrypt(app)
# 确保 SECRET_KEY 被设置
app.secret_key = os.getenv('SECRET_KEY', 'you_will_never_guess')
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_PERMANENT'] = False
Session(app)
# 配置邮件
app.config.update(
MAIL_SERVER=os.getenv('EMAIL_HOST'),
MAIL_PORT=os.getenv('EMAIL_PORT'),
MAIL_USE_SSL=True,
MAIL_USERNAME=os.getenv('EMAIL_USERNAME'),
MAIL_PASSWORD=os.getenv('EMAIL_PASSWORD'),
MAIL_DEFAULT_SENDER=(os.getenv('EMAIL_FROM_NAME'), os.getenv('EMAIL_FROM'))
)
mail = Mail(app)
# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
load_dotenv() # 从.env文件加载环境变量 # 添加路由
app_function.add_admin_routes(app, mail, bcrypt)
app = Flask(__name__, static_url_path='', static_folder='.') app_function.add_teacher_routes(app, mail, bcrypt)
CORS(app, resources={r"/*": {"origins": "*", "methods": "GET,POST,PUT,DELETE,OPTIONS"}}) # 添加 CORS 支持
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
region_name = os.getenv('AWS_REGION')
bucket_name = os.getenv('S3_BUCKET_NAME')
# 打印环境变量 (仅用于调试,生产环境中请移除)
print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
print(f"AWS_SECRET_ACCESS_KEY: {'*' * len(aws_secret_access_key) if aws_secret_access_key else 'Not set'}")
print(f"AWS_REGION: {region_name}")
print(f"S3_BUCKET_NAME: {bucket_name}")
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name,
config=Config(signature_version='s3v4') # 使用 S3v4 签名版本
)
# 跟踪学生提交信息
submissions_file = 'submissions.csv'
# 创建或者加载提交文件
if not os.path.exists(submissions_file):
with open(submissions_file, 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['ID', '学生姓名', '学号', '提交的文件'])
def add_submission(student, student_id, filename):
with open(submissions_file, 'a', newline='') as file:
writer = csv.writer(file)
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, filename])
def generate_presigned_url(object_key, content_type, expiration=3600):
try:
current_time = datetime.datetime.now(pytz.UTC)
logging.info(f"Current UTC time before generating URL: {current_time}")
response = s3_client.generate_presigned_url('put_object',
Params={
'Bucket': bucket_name,
'Key': object_key,
'ContentType': content_type # 使用实际文件的 Content-Type
},
ExpiresIn=expiration,
HttpMethod='PUT'
)
logging.info(f"Generated presigned URL: {response}")
return response
except (NoCredentialsError, ClientError, EndpointConnectionError) as e:
logging.error(f"Error generating presigned URL: {str(e)}", exc_info=True)
return None
@app.route('/generate-url', methods=['GET'])
def get_presigned_url():
student = request.args.get('student')
student_id = request.args.get('student_id')
filename = request.args.get('filename')
content_type = request.args.get('content_type', 'application/octet-stream')
logging.info(
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}")
if not student or not filename or not student_id:
logging.warning("Missing student, student_id or filename parameter")
return jsonify({'error': 'Student, student_id and filename parameters are required'}), 400
folder_name = 'sure_homework_define_by_qin'
object_key = f'{folder_name}/{student}-{filename}'
url = generate_presigned_url(object_key, content_type) # 包含 content_type
if not url:
logging.error("Failed to generate presigned URL")
return jsonify({'error': 'Failed to generate presigned URL'}), 500
add_submission(student, student_id, filename)
logging.info(f"Generated URL: {url}")
return jsonify({'url': url, 'content_type': content_type})
@app.route('/')
def serve_index():
return send_from_directory('.', 'index.html')
@app.route('/health')
def health_check():
logging.info("Health check initiated")
try:
local_time = datetime.datetime.now()
utc_time = datetime.datetime.now(pytz.UTC)
logging.info(f"Local time: {local_time}, UTC time: {utc_time}")
logging.info("Attempting to list S3 buckets")
response = s3_client.list_buckets()
logging.info(f"Successfully listed buckets: {[bucket['Name'] for bucket in response['Buckets']]}")
return jsonify({
'status': 'healthy',
'message': 'AWS credentials are valid',
'local_time': local_time.isoformat(),
'utc_time': utc_time.isoformat()
}), 200
except NoCredentialsError:
logging.error("AWS credentials not found", exc_info=True)
return jsonify({'status': 'unhealthy', 'message': 'AWS credentials not found'}), 500
except ClientError as e:
error_code = e.response['Error']['Code']
error_message = e.response['Error']['Message']
logging.error(f"AWS client error: {error_code} - {error_message}", exc_info=True)
return jsonify({'status': 'unhealthy', 'message': f'AWS client error: {error_code} - {error_message}'}), 500
except Exception as e:
logging.error(f"Unexpected error during health check: {str(e)}", exc_info=True)
return jsonify({'status': 'unhealthy', 'message': f'Unexpected error: {str(e)}'}), 500
@app.route('/download-submissions')
def download_submissions():
df = pd.read_csv(submissions_file)
output_file = 'submissions.xlsx'
df.to_excel(output_file, index=False)
return send_file(output_file, as_attachment=True)
@app.before_request @app.before_request
def before_request_func(): def before_request_func():
if request.method == 'OPTIONS': if request.method == 'OPTIONS':
return _build_cors_preflight_response() return app_function._build_cors_preflight_response()
def _build_cors_preflight_response():
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
response.headers.add("Access-Control-Allow-Headers", "Content-Type")
return response
if __name__ == '__main__': if __name__ == '__main__':
local_time = datetime.datetime.now() app.run(debug=True, port=5005)
utc_time = datetime.datetime.now(pytz.UTC)
logging.info(f"Application starting. Local time: {local_time}, UTC time: {utc_time}")
try:
logging.info("Validating AWS credentials on startup")
sts = boto3.client('sts',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name)
response = sts.get_caller_identity()
logging.info(f"AWS credentials validated successfully. Account ID: {response['Account']}")
except Exception as e:
logging.error(f"Failed to validate AWS credentials: {str(e)}", exc_info=True)
# 如果你想在凭证验证失败时退出程序,取消注释下面两行
# import sys
# sys.exit(1)
app.run(debug=True)

86
ccc.py Normal file
View File

@ -0,0 +1,86 @@
import os
import re
from docx import Document
def find_files(start_path, extensions, exclude_dirs):
"""
遍历目录并找到指定扩展名的文件不包括特定目录
:param start_path: 起始路径
:param extensions: 需要查找的文件扩展名列表
:param exclude_dirs: 需要排除的子目录列表
:return: 文件路径列表
"""
file_list = []
for root, dirs, files in os.walk(start_path):
# 通过修改dirs可以影响os.walk的遍历从而排除特定目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for file in files:
if any(file.endswith(ext) for ext in extensions):
file_list.append(os.path.join(root, file))
return file_list
def clean_text(text):
"""
清理文本移除所有非XML兼容字符
:param text: 原始文本
:return: 清理后的文本
"""
return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', text)
def read_files(file_list):
"""
读取文件内容
:param file_list: 文件路径列表
:return: 文件内容字典
"""
content_dict = {}
for file_path in file_list:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
content = clean_text(content) # 清理文本内容
content_dict[file_path] = content
return content_dict
def save_to_docx(content_dict, output_file):
"""
将文件内容字典保存到 DOCX 文件
:param content_dict: 文件内容字典
:param output_file: 输出的 DOCX 文件名
"""
doc = Document()
for file_path, content in content_dict.items():
try:
doc.add_heading(file_path, level=1)
doc.add_paragraph(content)
doc.add_page_break() # 添加分页符
except ValueError as e:
print(f"Error processing file {file_path}: {e}")
doc.save(output_file)
if __name__ == "__main__":
# 需要遍历的目录
directory = '/Users/lishunqin/Desktop/study/pychram project/SumKim_upload/SumKim_upload_system'
# 需要排除的目录
exclude_dirs = ['myenv', 'flask_session','venv']
# 需要查找的文件扩展名
extensions = ['.py', '.html', '.env', '.css', '.js']
# 查找文件
files = find_files(directory, extensions, exclude_dirs)
# 读取文件内容
content_dict = read_files(files)
# 输出 DOCX 文件名
output_docx = 'output_files_content.docx'
# 保存到 DOCX 文件
save_to_docx(content_dict, output_docx)
print(f"找到 {len(files)} 个文件,并保存了内容到 {output_docx}")

280
database.py Normal file
View File

@ -0,0 +1,280 @@
import os
import mysql.connector
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
from flask_bcrypt import Bcrypt
from some_module_to_verify_code import verify_code # 假设你有相应的模块
import datetime
import random
import logging
from flask_mail import Mail, Message
import boto3
from botocore.client import Config
import csv
# 初始化 Flask 和 Bcrypt
app = Flask(__name__)
bcrypt = Bcrypt(app)
mail = Mail(app)
# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
# 配置 AWS S3
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
region_name = os.getenv('AWS_REGION')
bucket_name = os.getenv('S3_BUCKET_NAME')
# 初始化S3客户端
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name,
config=Config(signature_version='s3v4')
)
# 初始化 CSV 文件
submissions_file = 'submissions.csv'
if not os.path.exists(submissions_file):
with open(submissions_file, 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件'])
# 数据库连接函数
def get_db_connection():
return mysql.connector.connect(
host='8.218.165.242',
user='sure_001',
password='EKKWLMmrGmG7sdPf',
database='sure_001'
)
# 验证学生身份
def validate_student(student_id, password):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM students WHERE id = %s', (student_id,))
student = cursor.fetchone()
cursor.close()
conn.close()
if student and bcrypt.check_password_hash(student['password'], password):
return student
return None
# 检查作业提交
def check_submission(student_id, assignment_id):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM submissions WHERE student_id = %s AND assignment_id = %s',
(student_id, assignment_id))
submission = cursor.fetchone()
cursor.close()
conn.close()
return submission
# 添加或更新作业提交
def add_or_update_submission(student_id, assignment_id, filename, code_verified=False):
conn = get_db_connection()
cursor = conn.cursor()
if code_verified:
cursor.execute(
'UPDATE submissions SET submit_date = NOW(), filename = %s WHERE student_id = %s AND assignment_id = %s',
(filename, student_id, assignment_id))
else:
cursor.execute(
'INSERT INTO submissions (student_id, assignment_id, filename, submit_date) VALUES (%s, %s, %s, NOW())',
(student_id, assignment_id, filename))
conn.commit()
cursor.close()
conn.close()
def fetch_all_departments():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM departments')
departments = cursor.fetchall()
cursor.close()
conn.close()
return departments
def fetch_all_grades():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM grades')
grades = cursor.fetchall()
cursor.close()
conn.close()
return grades
def fetch_all_classes():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM classes')
classes = cursor.fetchall()
cursor.close()
conn.close()
return classes
def fetch_all_teachers():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM teachers')
teachers = cursor.fetchall()
cursor.close()
conn.close()
return teachers
# 生成预签名URL
def generate_presigned_url(object_key, content_type):
try:
url = s3_client.generate_presigned_url(
'put_object',
Params={'Bucket': bucket_name, 'Key': object_key, 'ContentType': content_type},
ExpiresIn=3600
)
return url
except Exception as e:
logging.error(f"Failed to generate presigned URL: {str(e)}")
return None
# 提交作业路由
@app.route('/submit-assignment', methods=['POST'])
def submit_assignment():
student_id = request.form.get('student_id')
assignment_id = request.form.get('assignment_id')
filename = request.form.get('filename')
submission = check_submission(student_id, assignment_id)
if submission:
# 要求用户输入验证码
email = request.form.get('email')
code = request.form.get('code')
if verify_code(email, code):
add_or_update_submission(student_id, assignment_id, filename, code_verified=True)
else:
add_or_update_submission(student_id, assignment_id, filename, code_verified=False)
return 'Submission successful'
# 登录路由
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
student_id = request.form.get('student_id')
password = request.form.get('password')
student = validate_student(student_id, password)
if student:
session['student_id'] = student['id']
session['student_name'] = student['name']
return redirect(url_for('serve_index'))
else:
return render_template('login.html', error='学号或密码错误')
return render_template('login.html')
# 重置密码路由
@app.route('/reset-password', methods=['GET', 'POST'])
def reset_password():
if request.method == 'POST':
student_id = request.form.get('student_id')
email = request.form.get('email')
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM students WHERE id = %s AND email = %s', (student_id, email))
student = cursor.fetchone()
cursor.close()
conn.close()
if student:
reset_code = ''.join(random.choices('0123456789', k=6))
session['reset_code'] = reset_code
session['reset_student_id'] = student_id
try:
msg = Message('重置密码验证码', recipients=[email])
msg.body = f'您用于重置密码的验证码是: {reset_code}'
mail.send(msg)
return render_template('reset_password.html', success='验证码已发送到您的邮箱,请检查并输入验证码')
except Exception as e:
logging.error(f"Error sending email: {str(e)}")
return render_template('reset_password.html', error='发送验证码失败,请稍后再试')
else:
return render_template('reset_password.html', error='学号和邮箱不匹配')
return render_template('reset_password.html')
# 提交记录路由
@app.route('/record-submission', methods=['POST'])
def record_submission():
data = request.json
student_id = session.get('student_id')
student_name = session.get('student_name')
assignment = data.get('assignment')
filename = data.get('filename')
if not student_id or not filename or not assignment:
return jsonify({'error': 'Student_id, assignment and filename parameters are required'}), 400
# Check if the student has already submitted this assignment
submission = check_submission(student_id, assignment)
if submission:
session['filename'] = filename
return jsonify({'error': '作业已提交过,需要验证码'}), 401
# 生成 pre-signed URL 并记录提交
new_filename = f'{student_id}_{student_name}_{assignment}'
folder_name = f'sure_homework_define_by_qin/{assignment}'
object_key = f'{folder_name}/{new_filename}'
url = generate_presigned_url(object_key, 'application/octet-stream')
if not url:
logging.error("Failed to generate presigned URL")
return jsonify({'error': 'Failed to generate presigned URL'}), 500
add_or_update_submission(student_id, assignment, filename)
return jsonify({'status': 'success', 'upload_url': url})
# 生成验证码路由
@app.route('/generate-code', methods=['POST'])
def generate_code():
student_id = session.get('student_id')
assignment = request.json.get('assignment')
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT email FROM students WHERE id = %s', (student_id,))
student = cursor.fetchone()
cursor.close()
conn.close()
if not student:
return jsonify({'error': '学生信息未找到'}), 404
email = student['email']
reset_code = ''.join(random.choices('0123456789', k=6))
session['submission_code'] = reset_code
session['submission_student_id'] = student_id
session['submission_assignment'] = assignment
try:
msg = Message('提交验证码', recipients=[email])
msg.body = f'您用于提交作业的验证码是: {reset_code}'
mail.send(msg)
return jsonify({'status': '验证码已发送到您的邮箱'})
except Exception as e:
logging.error(f"Error sending email: {str(e)}")
return jsonify({'error': '发送验证码失败,请稍后再试'}), 500
if __name__ == '__main__':
app.run(debug=True)

46
edit_admin_info.py Normal file
View File

@ -0,0 +1,46 @@
from flask_bcrypt import Bcrypt
import mysql.connector
# Initialize Bcrypt
bcrypt = Bcrypt()
# Database connection details
MYSQL_HOST = '8.218.165.242'
MYSQL_USER = 'sure_001'
MYSQL_PASSWORD = 'EKKWLMmrGmG7sdPf'
MYSQL_DB = 'sure_001'
# Function to get database connection
def get_db_connection():
return mysql.connector.connect(
host=MYSQL_HOST,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
database=MYSQL_DB
)
# Function to update admin password
def update_teacher_password(teacher_id, new_plain_password):
conn = get_db_connection()
cursor = conn.cursor()
# Generate new hashed password
hashed_password = bcrypt.generate_password_hash(new_plain_password).decode('utf-8')
# Update password in the database
cursor.execute(
'UPDATE teachers SET password = %s WHERE id = %s',
(hashed_password, teacher_id)
)
# Commit changes and close the connection
conn.commit()
cursor.close()
conn.close()
# Example: Updating the password for admin with ID 1
if __name__ == "__main__":
tid = 1 # The ID of the admin to update
new_password = 'xiaoyan99817' # The new plain password you want to set
update_teacher_password(tid, new_password)
print(f"Password for admin with ID {tid} has been updated.")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,31 +0,0 @@
import boto3
from botocore.exceptions import NoCredentialsError, ClientError
# 使用获取的访问密钥ID和秘密访问密钥来初始化S3客户端
aws_access_key_id = 'AKIAZQ3DT3KLN5WGXZOR' # 替换为你的访问密钥ID
aws_secret_access_key = '5UZb8SovTrbroT7yU1pBzaR5myLn+NMA+c87RvLH' # 替换为你的秘密访问密钥
region_name = 'ap-northeast-1' # 替换为你的S3存储桶所在区域
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
# 创建一个模拟“文件夹”的空对象
def create_s3_folder(bucket_name, folder_name):
try:
s3_client.put_object(Bucket=bucket_name, Key=(folder_name + '/'))
print(f"Folder {folder_name} created in bucket {bucket_name}")
except NoCredentialsError:
print("Credentials not available")
except ClientError as e:
print(f"Error: {e}")
# Bucket name and folder name
bucket_name = 'sure-ae-upload'
folder_name = 'sure_homework_define_by_qin'
# Create folder
create_s3_folder(bucket_name, folder_name)

2
generate_secret_key.py Normal file
View File

@ -0,0 +1,2 @@
import secrets
print(secrets.token_hex(16)) # 生成一个32字符长的随机字符串

View File

@ -1,316 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>秀儿文件提交系统</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 400px;
}
.image-container {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
}
.image-container img {
width: 100%;
height: auto;
display: block;
}
h1 {
color: #333;
font-size: 24px;
margin-bottom: 20px;
text-align: center;
}
.form-field {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.file-input-wrapper {
position: relative;
overflow: visible;
display: inline-block;
width: 100%;
}
.file-input-button {
background-color: #f0f0f0;
border: 1px solid #ddd;
color: #333;
padding: 10px 15px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
box-sizing: border-box;
transition: background-color 0.3s, border-color 0.3s;
}
.file-input-button:hover {
background-color: #e0e0e0;
border-color: #ccc;
}
.file-input-button input[type=file] {
font-size: 100px;
position: absolute;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
#file-name {
margin-top: 5px;
font-size: 14px;
color: #666;
}
button[type="submit"] {
background-color: #1890ff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
width: 100%;
transition: background-color 0.3s;
}
button[type="submit"]:hover {
background-color: #40a9ff;
}
.progress-container {
margin-top: 20px;
display: none;
}
.progress {
height: 10px;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #1890ff;
width: 0;
transition: width 0.3s ease;
}
#progress-percentage {
font-size: 14px;
color: #666;
margin-top: 5px;
}
#status {
margin-top: 15px;
font-size: 14px;
color: #666;
}
#download-link {
display: inline-block;
margin-top: 20px;
text-decoration: none;
color: #1890ff;
font-size: 14px;
}
.upload-stats {
margin-top: 10px;
font-size: 14px;
color: #666;
}
.upload-stats span {
display: inline-block;
margin-right: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="image-container">
<img src="image.jpg" alt="描述性文本">
</div>
<h1>秀儿文件提交系统</h1>
<form id="upload-form">
<div class="form-field">
<label for="student">学生姓名:</label>
<input type="text" id="student" name="student" required>
</div>
<div class="form-field">
<label for="student-id">学号:</label>
<input type="text" id="student-id" name="student-id" required>
</div>
<div class="form-field">
<label for="file">选择文件:</label>
<div class="file-input-wrapper">
<div class="file-input-button">
选择文件
<input type="file" id="file" name="file" required>
</div>
</div>
<div id="file-name">未选择文件</div>
</div>
<button type="submit">提交 (截止日期: 2024.07.03)</button>
</form>
<div id="status"></div>
<div class="progress-container" id="progress-container">
<div class="progress">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div id="progress-percentage">0%</div>
<div class="upload-stats">
<span id="upload-speed">速度: 0 KB/s</span>
<span id="upload-size">0 KB / 0 KB</span>
<span id="upload-time">剩余时间: 计算中</span>
</div>
</div>
<a href="/download-submissions" id="download-link">下载统计表格</a>
</div>
<script>
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const student = document.getElementById('student').value;
const studentId = document.getElementById('student-id').value;
const file = document.getElementById('file').files[0];
const statusDiv = document.getElementById('status');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressPercentage = document.getElementById('progress-percentage');
const uploadSpeed = document.getElementById('upload-speed');
const uploadSize = document.getElementById('upload-size');
const uploadTime = document.getElementById('upload-time');
if (!student || !studentId || !file) {
alert('请提供学生姓名、学号,并选择一个文件。');
return;
}
statusDiv.textContent = '准备上传...';
progressContainer.style.display = 'block';
try {
const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&filename=${encodeURIComponent(file.name)}&content_type=${encodeURIComponent(file.type)}`, {
method: 'GET',
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`生成预签名URL失败: ${errorData.error}`);
}
const data = await response.json();
if (!data.url) {
throw new Error('服务器未返回URL');
}
statusDiv.textContent = '正在上传文件...';
const totalSize = file.size;
let uploadedSize = 0;
let startTime = Date.now();
const updateProgress = (additionalProgress = 0) => {
uploadedSize += additionalProgress;
const percentComplete = (uploadedSize / totalSize) * 100;
const elapsedTime = (Date.now() - startTime) / 1000; // 秒
const speed = uploadedSize / elapsedTime;
const remainingSize = totalSize - uploadedSize;
const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0;
progressBar.style.width = percentComplete + '%';
progressPercentage.textContent = percentComplete.toFixed(2) + '%';
uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`;
uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`;
};
// 模拟进度更新
const progressInterval = setInterval(() => {
if (uploadedSize < totalSize) {
updateProgress(totalSize / 100); // 每次更新1%的进度
}
}, 200);
// 实际上传
const uploadResponse = await fetch(data.url, {
method: 'PUT',
body: file,
headers: {
'Content-Type': data.content_type
}
});
clearInterval(progressInterval);
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
}
// 确保进度达到100%
updateProgress(totalSize - uploadedSize);
statusDiv.textContent = '文件上传成功';
alert('文件上传成功');
} catch (error) {
console.error('错误:', error);
statusDiv.textContent = `错误: ${error.message}`;
alert('发生错误: ' + error.message);
}
});
function formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatTime(seconds) {
if (isNaN(seconds) || !isFinite(seconds)) {
return '计算中';
}
if (seconds < 60) return Math.round(seconds) + ' ';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes} 分 ${remainingSeconds} 秒`;
}
// 添加文件名显示功能
document.getElementById('file').addEventListener('change', function(e) {
const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件';
document.getElementById('file-name').textContent = fileName;
});
</script>
</body>
</html>

View File

@ -0,0 +1,39 @@
import json
import mysql.connector
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()
# 读取 JSON 数据
with open('students_config.json', 'r', encoding='utf-8') as f:
students_config = json.load(f)
# 连接到 MySQL 数据库
conn = mysql.connector.connect(
host='8.218.165.242',
user='sure_001',
password='EKKWLMmrGmG7sdPf',
database='sure_001'
)
cursor = conn.cursor()
# 创建 students 表
cursor.execute('''
CREATE TABLE IF NOT EXISTS students (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
)
''')
# 插入数据并哈希默认密码
default_password_hash = bcrypt.generate_password_hash('skd123456').decode('utf-8')
for student in students_config['students']:
cursor.execute('INSERT INTO students (id, name, email, password) VALUES (%s, %s, %s, %s)',
(student['id'], student['name'], student['email'], default_password_hash))
conn.commit()
cursor.close()
conn.close()

16
main.py
View File

@ -1,16 +0,0 @@
# 这是一个示例 Python 脚本。
# 按 ⌃R 执行或将其替换为您的代码。
# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本。
print(f'Hi, {name}') # 按 ⌘F8 切换断点。
# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
print_hi('PyCharm')
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助

7368
output.log Normal file

File diff suppressed because it is too large Load Diff

BIN
output_files_content.docx Normal file

Binary file not shown.

20
requirements.txt Normal file
View File

@ -0,0 +1,20 @@
Flask
Flask-Bcrypt
Flask-Cors
Flask-Mail
Flask-Session
Werkzeug
Jinja2
itsdangerous
MarkupSafe
bcrypt
mysql-connector-python
boto3
botocore
python-dotenv
pytz
pandas
python-docx
xlsxwriter
requests
csvkit

1
scp_to_remote_host.txt Normal file
View File

@ -0,0 +1 @@
scp -r /Users/lishunqin/Desktop/study/pychram\ project/AWS-sure root@8.218.165.242:/root

99
static/admin.css Normal file
View File

@ -0,0 +1,99 @@
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
.container {
width: 80%;
margin: auto;
overflow: hidden;
padding: 20px;
}
h1, h2 {
color: #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="password"],
input[type="date"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
display: inline-block;
background: #333;
color: #fff;
padding: 10px 20px;
border: none;
cursor: pointer;
}
button:hover {
background: #555;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
text-align: left;
padding: 10px;
}
th {
background-color: #f4f4f4;
}
.alert {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.alert-error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.logout-btn {
float: right;
background: #dc3545;
color: #fff;
padding: 5px 10px;
text-decoration: none;
border-radius: 4px;
}
.logout-btn:hover {
background: #c82333;
}

119
static/base.css Normal file
View File

@ -0,0 +1,119 @@
/* base.css */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
.container {
width: 80%;
max-width: 800px;
margin: 50px auto;
background: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1, h2 {
color: #111;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #5cb85c;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s, transform 0.3s;
}
button:hover {
background: #4cae4c;
transform: translateY(-2px);
}
.alert {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.alert-error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.logout-btn {
float: right;
background: #dc3545;
color: #fff;
padding: 8px 16px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
}
.logout-btn:hover {
background: #c82333;
}
table {
width: 100%;
margin-top: 20px;
border-collapse: collapse;
}
th, td {
padding: 10px;
border-bottom: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f4f4f4;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: url('data:image/svg+xml;utf8,<svg x...') no-repeat right .75rem center / 14px 12px;
}

61
static/class_detail.js Normal file
View File

@ -0,0 +1,61 @@
function editAssignment(assignmentId, currentDeadline) {
const newDeadline = prompt("请输入新的截止日期格式YYYY-MM-DD:", currentDeadline);
if (newDeadline) {
fetch(`/teacher/edit_assignment/${assignmentId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ deadline: newDeadline })
})
.then(response => {
if (response.ok) {
alert('截止日期已更新');
location.reload();
} else {
alert('更新失败');
}
})
.catch(error => console.error('Error:', error));
}
}
function deleteAssignment(assignmentId) {
if (confirm("您确定要删除这个作业吗?")) {
fetch(`/teacher/delete_assignment/${assignmentId}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
alert('作业已删除');
location.reload();
} else {
alert('删除失败');
}
})
.catch(error => console.error('Error:', error));
}
}
function downloadAssignment(assignmentValue) {
fetch(`/teacher/download-assignment/${assignmentValue}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.blob(); // 将响应体转换为 Blob 对象
})
.then(blob => {
const url = window.URL.createObjectURL(blob); // 为 Blob 对象生成临时的 Object URL
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${assignmentValue}.zip`; // 动态设置文件名
document.body.appendChild(a);
a.click(); // 触发下载
window.URL.revokeObjectURL(url); // 释放 Object URL
})
.catch(error => {
console.error('Download failed:', error);
});
}

BIN
static/dogking.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 493 KiB

312
static/index.css Normal file
View File

@ -0,0 +1,312 @@
:root {
--primary-color: #1890ff;
--secondary-color: #40a9ff;
--background-color: #f0f2f5;
--text-color: #333;
--border-color: #ddd;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: var(--background-color);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 480px;
text-align: center;
transition: transform 0.3s ease;
}
.container:hover {
transform: translateY(-5px);
}
.image-container {
margin-bottom: 30px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.image-container img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.loading-indicator {
display: none;
margin-top: 15px;
font-size: 16px;
color: #666;
text-align: center;
}
.image-container:hover img {
transform: scale(1.05);
}
h1 {
color: var(--text-color);
font-size: 28px;
margin-bottom: 30px;
font-weight: 600;
}
.form-field {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
}
select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
select:focus {
outline: none;
border-color: var(--primary-color);
}
.file-input-wrapper {
position: relative;
overflow: visible;
display: inline-block;
width: 100%;
}
.file-input-button {
background-color: #f0f0f0;
border: 2px solid var(--border-color);
color: var(--text-color);
padding: 12px 15px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
box-sizing: border-box;
transition: all 0.3s ease;
}
.file-input-button:hover {
background-color: #e0e0e0;
border-color: var(--primary-color);
}
.file-input-button input[type="file"] {
display: none;
}
#file-name {
margin-top: 8px;
font-size: 14px;
color: #666;
}
button[type="submit"], button[id="verify-submit-button"] {
background-color: var(--primary-color);
color: white;
border: none;
padding: 14px 20px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
transition: background-color 0.3s ease, transform 0.3s ease;
margin-top: 25px;
font-weight: 500;
}
button[type="submit"]:hover, button[id="verify-submit-button"]:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
button[type="submit"]:disabled, button[id="verify-submit-button"]:disabled {
background-color: #d3d3d3;
cursor: not-allowed;
}
.progress-container {
margin-top: 25px;
display: none;
}
.progress {
height: 12px;
background-color: #f0f0f0;
border-radius: 6px;
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-bar {
height: 100%;
background-color: var(--primary-color);
width: 0;
transition: width 0.3s ease;
}
#progress-percentage {
font-size: 14px;
color: #666;
margin-top: 8px;
font-weight: 500;
}
#status {
margin-top: 20px;
font-size: 16px;
color: #666;
font-weight: 500;
}
.upload-stats {
margin-top: 15px;
font-size: 14px;
color: #666;
display: flex;
justify-content: space-between;
}
.upload-stats span {
display: inline-block;
}
.button-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 30px;
}
.action-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.action-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.verification-container {
margin-top: 25px;
}
.verification-input {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 16px;
text-align: center;
margin-bottom: 15px;
transition: border-color 0.3s ease;
}
.verification-input:focus {
outline: none;
border-color: var(--primary-color);
}
.download-link-button, .preview-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
width: 100%;
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 14px;
color: white;
background-size: 200% 200%;
animation: rainbow 5s ease infinite;
}
.download-link-button {
background-image: linear-gradient(45deg, #ff9a9e, #fad0c4, #ffecd2, #fcb69f);
}
.preview-button {
background-image: linear-gradient(45deg, #a1c4fd, #c2e9fb, #d4fc79, #96e6a1);
}
.download-link-button:hover, .preview-button:hover {
background-size: 100% 100%;
}
.logout-button:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
.logout-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 14px 20px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
transition: background-color 0.3s ease, transform 0.3s ease;
margin-bottom: 25px; /* 适当的间距以与表单分隔开 */
font-weight: 500;
}
.button-icon {
font-size: 24px;
margin-bottom: 10px;
}
@keyframes rainbow {
0% { background-position: 0% 50% }
50% { background-position: 100% 50% }
100% { background-position: 0% 50% }
}
@media (max-width: 600px) {
.container {
padding: 30px;
max-width: 90%;
}
}
@media (max-width: 480px) {
.button-group {
grid-template-columns: 1fr;
}
}

275
static/index.js Normal file
View File

@ -0,0 +1,275 @@
document.querySelector('.file-input-button').addEventListener('click', function() {
document.getElementById('file').click();
});
document.getElementById('file').addEventListener('change', function(e) {
const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件';
document.getElementById('file-name').textContent = fileName;
});
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
handleSubmission();
});
document.getElementById('verify-submit-button').addEventListener('click', async () => {
await validateCodeAndSubmit();
});
async function handleSubmission() {
const assignment = document.getElementById('assignment').value;
const file = document.getElementById('file').files[0];
const statusDiv = document.getElementById('status');
if (!assignment || !file) {
alert('请选择作业,并选择一个文件。');
return;
}
setLoadingState(true);
try {
const recordResponse = await fetch('/record-submission', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ assignment, filename: file.name })
});
if (recordResponse.status === 401) {
setLoadingState(false);
document.getElementById('verification-container').style.display = 'block';
const emailResponse = await fetch('/generate-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ assignment })
});
if (emailResponse.ok) {
alert('作业已提交过,请输入验证码提交');
} else {
const errorData = await emailResponse.json();
throw new Error(`发送验证码失败: ${errorData.error}`);
}
} else if (!recordResponse.ok) {
const errorRecordData = await recordResponse.json();
throw new Error(`记录提交信息失败: ${errorRecordData.error}`);
} else {
const data = await recordResponse.json();
await uploadFile(data.upload_url, file, statusDiv);
alert('文件上传并记录成功');
}
} catch (error) {
console.error('错误:', error);
statusDiv.textContent = `错误: ${error.message}`;
alert('发生错误: ' + error.message);
} finally {
setLoadingState(false);
}
}
async function validateCodeAndSubmit() {
const assignment = document.getElementById('assignment').value;
const file = document.getElementById('file').files[0];
const code = document.getElementById('verification-code').value;
const statusDiv = document.getElementById('status');
setLoadingState(true, true);
try {
const validateResponse = await fetch('/validate-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ assignment, code })
});
if (validateResponse.ok) {
const { upload_url } = await validateResponse.json();
await uploadFile(upload_url, file, statusDiv);
alert('文件上传并记录成功');
document.getElementById('verification-container').style.display = 'none';
} else {
const errorData = await validateResponse.json();
throw new Error(`验证码错误: ${errorData.error}`);
}
} catch (error) {
console.error('错误:', error);
statusDiv.textContent = `错误: ${error.message}`;
alert('发生错误: ' + error.message);
} finally {
setLoadingState(false, true);
}
}
async function uploadFile(upload_url, file, statusDiv) {
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressPercentage = document.getElementById('progress-percentage');
const uploadSpeed = document.getElementById('upload-speed');
const uploadSize = document.getElementById('upload-size');
const uploadTime = document.getElementById('upload-time');
statusDiv.textContent = '正在上传文件...';
const totalSize = file.size;
let uploadedSize = 0;
let startTime = Date.now();
progressContainer.style.display = 'block';
const updateProgress = (additionalProgress = 0) => {
uploadedSize += additionalProgress;
const percentComplete = (uploadedSize / totalSize) * 100;
const elapsedTime = (Date.now() - startTime) / 1000;
const speed = uploadedSize / elapsedTime;
const remainingSize = totalSize - uploadedSize;
const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0;
progressBar.style.width = percentComplete + '%';
progressPercentage.textContent = percentComplete.toFixed(2) + '%';
uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`;
uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`;
};
const progressInterval = setInterval(() => {
if (uploadedSize < totalSize) {
updateProgress(totalSize / 100);
}
}, 200);
const uploadResponse = await fetch(upload_url, {
method: 'PUT',
body: file,
headers: {
'Content-Type': 'application/octet-stream'
}
});
clearInterval(progressInterval);
if (!uploadResponse.ok) {
throw new Error(`上传失败: ${uploadResponse.statusText}`);
}
updateProgress(totalSize - uploadedSize);
}
function setLoadingState(isLoading, isVerify = false) {
const submitButton = document.querySelector('button[type="submit"]');
const verifyButton = document.getElementById('verify-submit-button');
const container = isVerify ? verifyButton.parentElement : submitButton.parentElement;
const loadingIndicator = container.querySelector('.loading-indicator') || document.createElement('div');
loadingIndicator.className = 'loading-indicator';
loadingIndicator.innerHTML = '<span>加载中...</span>';
loadingIndicator.style.display = isLoading ? 'block' : 'none';
if (!loadingIndicator.parentElement) {
container.appendChild(loadingIndicator);
}
if (isVerify) {
verifyButton.disabled = isLoading;
} else {
submitButton.disabled = isLoading;
}
}
function formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatTime(seconds) {
if (isNaN(seconds) || !isFinite(seconds)) {
return '计算中';
}
if (seconds < 60) return Math.round(seconds) + ' 秒';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes}${remainingSeconds}`;
}
function previewTable(apiUrl, title) {
fetch(apiUrl)
.then(response => response.json())
.then(data => {
const newWindow = window.open('', '_blank');
newWindow.document.write('<html><head><title>' + title + '</title><style>');
newWindow.document.write('table { width: 100%; border-collapse: collapse; margin-top: 10px; }');
newWindow.document.write('th, td { border: 1px solid #ddd; padding: 8px; text-align: left; word-wrap: break-word; }');
newWindow.document.write('th { background-color: #f2f2f2; }');
newWindow.document.write('body { font-family: Arial, sans-serif; padding: 20px; }');
newWindow.document.write('</style></head><body>');
newWindow.document.write('<h2>' + title + '</h2>');
const table = newWindow.document.createElement('table');
if (data.length > 0) {
let headers;
if (title === '预览统计表格') {
headers = ['时间', '提交的文件', '姓名', '学号', '作业'];
} else {
headers = ['学号', '姓名', '作业', '提交情况', '提交的文件'];
}
const thead = newWindow.document.createElement('thead');
const tr = newWindow.document.createElement('tr');
headers.forEach((header, index) => {
const th = newWindow.document.createElement('th');
th.textContent = header;
// 设置列宽
if (title === '预览统计表格') {
if (index === 0) th.style.width = '20%'; // 时间
else if (index === 1) th.style.width = '30%'; // 提交的文件
else if (index === 2) th.style.width = '15%'; // 姓名
else if (index === 3) th.style.width = '15%'; // 学号
else if (index === 4) th.style.width = '20%'; // 作业
} else {
if (index === 0) th.style.width = '15%'; // 学号
else if (index === 1) th.style.width = '15%'; // 姓名
else if (index === 2) th.style.width = '20%'; // 作业
else if (index === 3) th.style.width = '15%'; // 提交情况
else if (index === 4) th.style.width = '35%'; // 提交的文件
}
tr.appendChild(th);
});
thead.appendChild(tr);
table.appendChild(thead);
}
const tbody = newWindow.document.createElement('tbody');
data.forEach(row => {
const tr = newWindow.document.createElement('tr');
if (title === '预览统计表格') {
['时间', '提交的文件', '姓名', '学号', '作业'].forEach(key => {
const td = newWindow.document.createElement('td');
td.textContent = row[key] || '';
tr.appendChild(td);
});
} else {
['学号', '姓名', '作业', '提交情况', '提交的文件'].forEach(key => {
const td = newWindow.document.createElement('td');
td.textContent = row[key] || '';
tr.appendChild(td);
});
}
tbody.appendChild(tr);
});
table.appendChild(tbody);
newWindow.document.body.appendChild(table);
newWindow.document.write('</body></html>');
newWindow.document.close();
})
.catch(error => console.error('Error fetching data:', error));
}

View File

@ -1,13 +0,0 @@
ID,学生姓名,学号,提交的文件
2024-07-02T15:38:06.844568,嗷呜呜汪汪汪钦,12345,fastgpt.txt
2024-07-03T02:01:37.364487,王中王火腿肠,123,mao.png
2024-07-03T02:02:02.087326,王中王火腿肠,123,《移动互联开发》软件设计考查材料.rar
2024-07-03T02:31:23.019179,秀儿,000,SUS 304 Stainless Steel.png
2024-07-03T02:31:55.184382,秀儿,000,阅读1.pdf
2024-07-03T02:35:50.030507,嘿!,324,计算机网络_数据链路层.ppt
2024-07-03T02:37:23.232007,CC,34534,"性学观止 (贺兰特·凯查杜里安, 胡颖翀) (Z-Library).pdf"
2024-07-03T02:40:42.161975,4324,4234,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
2024-07-03T02:42:47.614487,324,4234,男人来自火星女人来自金星套装共4册 (约翰·格雷) (Z-Library).pdf
2024-07-03T02:43:01.033652,534534,543534,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
2024-07-03T02:46:15.397526,CCC,AAA,"周易译注 (黄寿祺,张善文) (Z-Library).pdf"
2024-07-03T02:48:26.189052,2423,423423,阅读1.pdf
1 ID 学生姓名 学号 提交的文件
2 2024-07-02T15:38:06.844568 嗷呜呜汪汪汪钦 12345 fastgpt.txt
3 2024-07-03T02:01:37.364487 王中王火腿肠 123 mao.png
4 2024-07-03T02:02:02.087326 王中王火腿肠 123 《移动互联开发》软件设计考查材料.rar
5 2024-07-03T02:31:23.019179 秀儿 000 SUS 304 Stainless Steel.png
6 2024-07-03T02:31:55.184382 秀儿 000 阅读1.pdf
7 2024-07-03T02:35:50.030507 嘿! 324 计算机网络_数据链路层.ppt
8 2024-07-03T02:37:23.232007 CC 34534 性学观止 (贺兰特·凯查杜里安, 胡颖翀) (Z-Library).pdf
9 2024-07-03T02:40:42.161975 4324 4234 数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
10 2024-07-03T02:42:47.614487 324 4234 男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf
11 2024-07-03T02:43:01.033652 534534 543534 数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf
12 2024-07-03T02:46:15.397526 CCC AAA 周易译注 (黄寿祺,张善文) (Z-Library).pdf
13 2024-07-03T02:48:26.189052 2423 423423 阅读1.pdf

Binary file not shown.

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login</title>
<link rel="stylesheet" href="/static/admin.css">
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<div class="container">
<h1>Admin Login</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>

160
templates/admin_panel.html Normal file
View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理面板</title>
<link rel="stylesheet" href="/static/admin.css">
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<div class="container">
<h1>管理面板</h1>
<a href="{{ url_for('admin_logout') }}" class="logout-btn">登出</a>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>添加新专业</h2>
<form action="{{ url_for('add_department') }}" method="POST">
<div class="form-group">
<label for="department_name">专业名称:</label>
<input type="text" id="department_name" name="name" required>
</div>
<button type="submit">添加专业</button>
</form>
<h2>添加新年级</h2>
<form action="{{ url_for('add_grade') }}" method="POST">
<div class="form-group">
<label for="year">年份:</label>
<input type="number" id="year" name="year" required min="2000" max="2100">
</div>
<button type="submit">添加年级</button>
</form>
<h2>添加新班级</h2>
<form action="{{ url_for('add_class') }}" method="POST">
<div class="form-group">
<label for="class_name">班级名称:</label>
<input type="text" id="class_name" name="name" required>
</div>
<div class="form-group">
<label for="department_id">专业:</label>
<select id="department_id" name="department_id" required>
{% for department in departments %}
<option value="{{ department.id }}">{{ department.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="grade_id">年级:</label>
<select id="grade_id" name="grade_id" required>
{% for grade in grades %}
<option value="{{ grade.id }}">{{ grade.year }}</option>
{% endfor %}
</select>
</div>
<button type="submit">添加班级</button>
</form>
<h2>分配教师到班级</h2>
<form action="{{ url_for('assign_teacher') }}" method="POST">
<div class="form-group">
<label for="class_id">班级:</label>
<select id="class_id" name="class_id" required>
{% for class in classes %}
<option value="{{ class.id }}">{{ class.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="teacher_id">教师:</label>
<select id="teacher_id" name="teacher_id" required>
{% for teacher in teachers %}
<option value="{{ teacher.id }}">{{ teacher.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit">分配教师</button>
</form>
<h2>添加新教师</h2>
<form action="{{ url_for('add_teacher') }}" method="POST">
<div class="form-group">
<label for="teacher_name">教师姓名:</label>
<input type="text" id="teacher_name" name="name" required>
</div>
<div class="form-group">
<label for="teacher_email">邮箱:</label>
<input type="email" id="teacher_email" name="email" required>
</div>
<div class="form-group">
<label for="teacher_password">密码:</label>
<input type="password" id="teacher_password" name="password" required>
</div>
<button type="submit">添加教师</button>
</form>
<h2>添加新管理员</h2>
<form action="{{ url_for('add_administrator') }}" method="POST">
<div class="form-group">
<label for="admin_username">用户名:</label>
<input type="text" id="admin_username" name="username" required>
</div>
<div class="form-group">
<label for="admin_password">密码:</label>
<input type="password" id="admin_password" name="password" required>
</div>
<div class="form-group">
<label for="admin_teacher_id">教师:</label>
<select id="admin_teacher_id" name="teacher_id" required>
{% for teacher in teachers %}
<option value="{{ teacher.id }}">{{ teacher.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit">添加管理员</button>
</form>
</div>
<div id="editFormContainer" style="display: none;">
<form id="editForm" method="POST">
<input type="hidden" id="editId" name="id">
<div class="form-group">
<label for="editValue">值:</label>
<input type="text" id="editValue" name="value" required>
</div>
<div class="form-group">
<label for="editName">名称:</label>
<input type="text" id="editName" name="name" required>
</div>
<div class="form-group">
<label for="editDeadline">截止日期:</label>
<input type="date" id="editDeadline" name="deadline" required>
</div>
<button type="submit">更新任务</button>
<button type="button" onclick="hideEditForm()">取消</button>
</form>
</div>
<script>
function showEditForm(id, value, name, deadline) {
document.getElementById('editId').value = id;
document.getElementById('editValue').value = value;
document.getElementById('editName').value = name;
document.getElementById('editDeadline').value = deadline;
document.getElementById('editForm').action = `/admin/edit_assignment/${id}`;
document.getElementById('editFormContainer').style.display = 'block';
}
function hideEditForm() {
document.getElementById('editFormContainer').style.display = 'none';
}
</script>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>班级详情</title>
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<div class="container">
<h1>班级详情</h1>
<a href="{{ url_for('teacher_panel') }}">返回教师面板</a>
<h2>作业</h2>
<form action="{{ url_for('teacher_add_assignment', class_id=class_id) }}" method="POST">
<div class="form-group">
<label for="value">作业存储名称:</label>
<input type="text" id="value" name="value" required>
</div>
<div class="form-group">
<label for="name">作业显示名称:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="deadline">截止日期:</label>
<input type="date" id="deadline" name="deadline" required>
</div>
<button type="submit">添加作业</button>
</form>
<h2>现有作业</h2>
<table>
<thead>
<tr>
<th>作业存储名称</th>
<th>作业显示名称</th>
<th>截止日期</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for assignment in assignments %}
<tr>
<td>{{ assignment.value }}</td>
<td>{{ assignment.name }}</td>
<td>{{ assignment.deadline }}</td>
<td>
<button onclick="editAssignment({{ assignment.id }}, '{{ assignment.deadline }}')">修改</button>
<button onclick="deleteAssignment({{ assignment.id }})">删除</button>
<button onclick="downloadAssignment('{{ assignment.value }}')">下载</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>学生</h2>
<table>
<thead>
<tr>
<th>学生 ID</th>
<th>学生姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
{% for student in students %}
<tr>
<td>{{ student.id }}</td>
<td>{{ student.name }}</td>
<td>{{ student.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- JavaScript 文件 -->
<script src="/static/class_detail.js"></script>
</body>
</html>

86
templates/index.html Normal file
View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>秀儿文件提交系统</title>
<link rel="stylesheet" href="/static/index.css">
</head>
<body>
<div class="container">
<div class="image-container">
<img src="/static/image.jpg" alt="描述性文本">
</div>
<!-- 增加退出按钮 -->
<button class="logout-button" onclick="window.location.href='/logout'">退出账号</button>
<h1>秀儿文件提交系统</h1>
<form id="upload-form">
<div class="form-field">
<label for="assignment">选择作业:</label>
<select id="assignment" name="assignment" required>
<option value="">请选择作业</option>
{% for assignment in assignments %}
<option value="{{ assignment.value }}">{{ assignment.name }} (截止日期: {{ assignment.deadline }})</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<label for="file">选择文件:</label>
<div class="file-input-wrapper">
<button type="button" class="file-input-button">选择文件 <input type="file" id="file" name="file" required></button>
</div>
<div id="file-name">未选择文件</div>
</div>
<button type="submit">提交</button>
</form>
<div id="status"></div>
<div class="progress-container" id="progress-container">
<div class="progress">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div id="progress-percentage">0%</div>
<div class="upload-stats">
<span id="upload-speed">速度: 0 KB/s</span>
<span id="upload-size">0 KB / 0 KB</span>
<span id="upload-time">剩余时间: 计算中</span>
</div>
</div>
<!-- 验证码输入框和按钮 -->
<div id="verification-container" class="verification-container" style="display: none;">
<div class="form-field">
<label for="verification-code">输入验证码:</label>
<input type="text" id="verification-code" class="verification-input" placeholder="请输入验证码">
</div>
<button id="verify-submit-button">提交验证码</button>
</div>
<div class="button-group">
<div class="action-card">
<button class="download-link-button" onclick="window.location.href='/download-submissions'">
<span class="button-icon">📊</span>
下载统计表格
</button>
</div>
<div class="action-card">
<button class="download-link-button" onclick="window.location.href='/download-assignment-status'">
<span class="button-icon">📥</span>
下载作业提交情况
</button>
</div>
<div class="action-card">
<button class="preview-button" onclick="previewTable('/api/submissions', '预览统计表格')">
<span class="button-icon">👁️</span>
预览统计表格
</button>
</div>
<div class="action-card">
<button class="preview-button" onclick="previewTable('/api/assignment-status', '预览作业提交情况')">
<span class="button-icon">🔍</span>
预览作业提交情况
</button>
</div>
</div>
</div>
<script src="/static/index.js"></script>
</body>
</html>

121
templates/login.html Normal file
View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 480px;
text-align: center;
transition: transform 0.3s ease;
}
.container:hover {
transform: translateY(-5px);
}
.image-container {
margin-bottom: 30px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.image-container img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.image-container:hover img {
transform: scale(1.05);
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 30px;
font-weight: 600;
}
.form-field {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #1890ff;
}
button[type="submit"], button[type="button"] {
background-color: #1890ff;
color: white;
border: none;
padding: 14px 20px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
transition: background-color 0.3s ease, transform 0.3s ease;
margin-top: 25px;
font-weight: 500;
}
button[type="submit"]:hover, button[type="button"]:hover {
background-color: #40a9ff;
transform: translateY(-2px);
}
.error {
color: red;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="image-container">
<img src="../static/dogking.jpg" alt="Dog King">
</div>
<h1>登录</h1>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form action="{{ url_for('login') }}" method="post">
<div class="form-field">
<label for="student_id">学号:</label>
<input type="text" id="student_id" name="student_id" required>
</div>
<div class="form-field">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">登录</button>
<button type="button" onclick="location.href='{{ url_for('reset_password') }}'">重置密码</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 480px;
text-align: center;
transition: transform 0.3s ease;
}
.container:hover {
transform: translateY(-5px);
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 30px;
font-weight: 600;
}
.form-field {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input[type="text"], input[type="email"], input[type="password"] {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus {
outline: none;
border-color: #1890ff;
}
.error, .success {
margin-bottom: 15px;
}
.error {
color: red;
}
.success {
color: green;
}
button[type="submit"], button[type="button"] {
background-color: #1890ff;
color: white;
border: none;
padding: 14px 20px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
transition: background-color 0.3s ease, transform 0.3s ease;
margin-top: 25px;
font-weight: 500;
}
button[type="submit"]:hover, button[type="button"]:hover {
background-color: #40a9ff;
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<h1>重置密码</h1>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
{% if success %}
<div class="success">{{ success }}</div>
{% endif %}
<form action="{{ url_for('reset_password') }}" method="post">
<div class="form-field">
<label for="student_id">学号:</label>
<input type="text" id="student_id" name="student_id" required>
</div>
<div class="form-field">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-field">
<label for="code">验证码:</label>
<input type="text" id="code" name="code">
</div>
<div class="form-field">
<label for="new_password">新密码:</label>
<input type="password" id="new_password" name="new_password">
</div>
<button type="submit" name="reset_password">验证并修改密码</button>
<button type="submit" name="send_code">发送验证码</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teacher Login</title>
<link rel="stylesheet" href="/static/style.css"> <!-- 您可以在此处链接到您的全局样式表 -->
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<div class="container">
<h1>Teacher Login</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form action="{{ url_for('teacher_login') }}" method="POST">
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>教师面板</title>
<link rel="stylesheet" href="/static/teacher.css">
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<div class="container">
<h1>教师面板</h1>
<a href="{{ url_for('teacher_logout') }}" class="logout-btn">登出</a>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>选择班级</h2>
<select id="class_select" onchange="navigateToClass(this.value)">
<option value="">选择一个班级</option>
{% for class in classes %}
<option value="{{ class.id }}">{{ class.name }}</option>
{% endfor %}
</select>
<!-- 额外内容如班级作业和学生提交将在这里显示。 -->
</div>
<script>
function navigateToClass(classId) {
if (classId) {
window.location.href = `/teacher/class/${classId}`;
}
}
</script>
</body>
</html>