ALL
This commit is contained in:
		
							parent
							
								
									32cbad1698
								
							
						
					
					
						commit
						68b99755ec
					
				
							
								
								
									
										20
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.env
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | myenv/ | ||||||
							
								
								
									
										1
									
								
								.idea/.name
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.idea/.name
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | app.py | ||||||
							
								
								
									
										2
									
								
								.idea/AWS-sure.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/AWS-sure.iml
									
									
									
										generated
									
									
									
								
							| @ -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
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										49
									
								
								README.md
									
									
									
									
									
										Normal 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讨论您想要更改的内容。 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
							
								
								
									
										
											BIN
										
									
								
								__pycache__/app_function.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								__pycache__/app_function.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										208
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								app.py
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										86
									
								
								ccc.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										280
									
								
								database.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										46
									
								
								edit_admin_info.py
									
									
									
									
									
										Normal 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.") | ||||||
							
								
								
									
										
											BIN
										
									
								
								flask_session/145af1ef713f4de25ef68b5250b1e348
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flask_session/145af1ef713f4de25ef68b5250b1e348
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								flask_session/2029240f6d1128be89ddc32729463129
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flask_session/2029240f6d1128be89ddc32729463129
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								flask_session/51d4f28a786bedca69075165f916972c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flask_session/51d4f28a786bedca69075165f916972c
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								flask_session/7aeccfc0aea026d87e7ab01ca25258d0
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flask_session/7aeccfc0aea026d87e7ab01ca25258d0
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -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
									
								
							
							
						
						
									
										2
									
								
								generate_secret_key.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | import secrets | ||||||
|  | print(secrets.token_hex(16))  # 生成一个32字符长的随机字符串 | ||||||
							
								
								
									
										316
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										316
									
								
								index.html
									
									
									
									
									
								
							| @ -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> |  | ||||||
							
								
								
									
										39
									
								
								insert_data_to_database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								insert_data_to_database.py
									
									
									
									
									
										Normal 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
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								main.py
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										7368
									
								
								output.log
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								output_files_content.docx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								output_files_content.docx
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										20
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								requirements.txt
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								scp_to_remote_host.txt
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										99
									
								
								static/admin.css
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										119
									
								
								static/base.css
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										61
									
								
								static/class_detail.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/dogking.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 199 KiB | 
| Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB | 
							
								
								
									
										312
									
								
								static/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								static/index.css
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										275
									
								
								static/index.js
									
									
									
									
									
										Normal 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)); | ||||||
|  | } | ||||||
| @ -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 |  | ||||||
| 
 | 
							
								
								
									
										
											BIN
										
									
								
								submissions.xlsx
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								submissions.xlsx
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										33
									
								
								templates/admin_login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								templates/admin_login.html
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										160
									
								
								templates/admin_panel.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										80
									
								
								templates/class_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								templates/class_detail.html
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										86
									
								
								templates/index.html
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										121
									
								
								templates/login.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										131
									
								
								templates/reset_password.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								templates/reset_password.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										34
									
								
								templates/teacher_login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								templates/teacher_login.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										40
									
								
								templates/teacher_panel.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								templates/teacher_panel.html
									
									
									
									
									
										Normal 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> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 superlishunqin
						superlishunqin