0707
This commit is contained in:
parent
1760f36f7b
commit
6129cd506d
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>
|
79
app.py
79
app.py
@ -45,13 +45,13 @@ 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', '学生姓名', '学号', '提交的文件'])
|
||||
writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件'])
|
||||
|
||||
|
||||
def add_submission(student, student_id, filename):
|
||||
def add_submission(student, student_id, assignment, filename):
|
||||
with open(submissions_file, 'a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, filename])
|
||||
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, filename])
|
||||
|
||||
|
||||
def generate_presigned_url(object_key, content_type, expiration=3600):
|
||||
@ -63,7 +63,7 @@ def generate_presigned_url(object_key, content_type, expiration=3600):
|
||||
Params={
|
||||
'Bucket': bucket_name,
|
||||
'Key': object_key,
|
||||
'ContentType': content_type # 使用实际文件的 Content-Type
|
||||
'ContentType': content_type
|
||||
},
|
||||
ExpiresIn=expiration,
|
||||
HttpMethod='PUT'
|
||||
@ -81,27 +81,42 @@ def get_presigned_url():
|
||||
student_id = request.args.get('student_id')
|
||||
filename = request.args.get('filename')
|
||||
content_type = request.args.get('content_type', 'application/octet-stream')
|
||||
assignment = request.args.get('assignment')
|
||||
logging.info(
|
||||
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}")
|
||||
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}")
|
||||
|
||||
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
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
logging.warning("Missing student, student_id, assignment or filename parameter")
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
folder_name = 'sure_homework_define_by_qin'
|
||||
object_key = f'{folder_name}/{student}-{filename}'
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
new_filename = f'{student}_{student_id}_{filename}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
|
||||
url = generate_presigned_url(object_key, content_type) # 包含 content_type
|
||||
url = generate_presigned_url(object_key, 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('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student = data.get('student')
|
||||
student_id = data.get('student_id')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
add_submission(student, student_id, assignment, filename)
|
||||
|
||||
return jsonify({'status': 'success'})
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def serve_index():
|
||||
return send_from_directory('.', 'index.html')
|
||||
@ -139,10 +154,37 @@ def health_check():
|
||||
|
||||
@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)
|
||||
try:
|
||||
# 使用 csv 模块读取文件,这样可以处理不一致的行
|
||||
with open(submissions_file, 'r', newline='') as file:
|
||||
csv_reader = csv.reader(file)
|
||||
headers = next(csv_reader) # 读取标题行
|
||||
data = list(csv_reader) # 读取所有数据行
|
||||
|
||||
# 确定最大列数
|
||||
max_columns = max(len(headers), max(len(row) for row in data))
|
||||
|
||||
# 创建一个新的数据列表,确保每行都有相同数量的列
|
||||
normalized_data = []
|
||||
for row in data:
|
||||
normalized_data.append(row + [''] * (max_columns - len(row)))
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers)))
|
||||
|
||||
# 重命名列(如果需要)
|
||||
expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件']
|
||||
df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))]
|
||||
|
||||
# 删除完全为空的行和列
|
||||
df = df.dropna(how='all', axis=0).dropna(how='all', axis=1)
|
||||
|
||||
output_file = 'submissions.xlsx'
|
||||
df.to_excel(output_file, index=False)
|
||||
return send_file(output_file, as_attachment=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.before_request
|
||||
@ -179,4 +221,3 @@ if __name__ == '__main__':
|
||||
# sys.exit(1)
|
||||
|
||||
app.run(debug=True)
|
||||
|
||||
|
223
app.txt
Normal file
223
app.txt
Normal file
@ -0,0 +1,223 @@
|
||||
from flask import Flask, request, jsonify, send_from_directory, make_response, send_file
|
||||
from flask_cors import CORS
|
||||
import boto3
|
||||
from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
import datetime
|
||||
import pytz
|
||||
from botocore.client import Config
|
||||
import csv
|
||||
import pandas as pd
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
load_dotenv() # 从.env文件加载环境变量
|
||||
|
||||
app = Flask(__name__, static_url_path='', static_folder='.')
|
||||
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, assignment, filename):
|
||||
with open(submissions_file, 'a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, 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
|
||||
},
|
||||
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')
|
||||
assignment = request.args.get('assignment')
|
||||
logging.info(
|
||||
f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}")
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
logging.warning("Missing student, student_id, assignment or filename parameter")
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
folder_name = f'sure_homework_define_by_qin/{assignment}'
|
||||
new_filename = f'{student}_{student_id}_{filename}'
|
||||
object_key = f'{folder_name}/{new_filename}'
|
||||
|
||||
url = generate_presigned_url(object_key, content_type)
|
||||
if not url:
|
||||
logging.error("Failed to generate presigned URL")
|
||||
return jsonify({'error': 'Failed to generate presigned URL'}), 500
|
||||
|
||||
return jsonify({'url': url, 'content_type': content_type})
|
||||
|
||||
|
||||
@app.route('/record-submission', methods=['POST'])
|
||||
def record_submission():
|
||||
data = request.json
|
||||
student = data.get('student')
|
||||
student_id = data.get('student_id')
|
||||
assignment = data.get('assignment')
|
||||
filename = data.get('filename')
|
||||
|
||||
if not student or not filename or not student_id or not assignment:
|
||||
return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400
|
||||
|
||||
add_submission(student, student_id, assignment, filename)
|
||||
|
||||
return jsonify({'status': 'success'})
|
||||
|
||||
|
||||
@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():
|
||||
try:
|
||||
# 使用 csv 模块读取文件,这样可以处理不一致的行
|
||||
with open(submissions_file, 'r', newline='') as file:
|
||||
csv_reader = csv.reader(file)
|
||||
headers = next(csv_reader) # 读取标题行
|
||||
data = list(csv_reader) # 读取所有数据行
|
||||
|
||||
# 确定最大列数
|
||||
max_columns = max(len(headers), max(len(row) for row in data))
|
||||
|
||||
# 创建一个新的数据列表,确保每行都有相同数量的列
|
||||
normalized_data = []
|
||||
for row in data:
|
||||
normalized_data.append(row + [''] * (max_columns - len(row)))
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers)))
|
||||
|
||||
# 重命名列(如果需要)
|
||||
expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件']
|
||||
df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))]
|
||||
|
||||
# 删除完全为空的行和列
|
||||
df = df.dropna(how='all', axis=0).dropna(how='all', axis=1)
|
||||
|
||||
output_file = 'submissions.xlsx'
|
||||
df.to_excel(output_file, index=False)
|
||||
return send_file(output_file, as_attachment=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in download_submissions: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request_func():
|
||||
if request.method == 'OPTIONS':
|
||||
return _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__':
|
||||
local_time = datetime.datetime.now()
|
||||
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)
|
264
index.html
264
index.html
@ -48,7 +48,7 @@
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="text"] {
|
||||
input[type="text"], select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
@ -80,14 +80,7 @@
|
||||
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%;
|
||||
display: none;
|
||||
}
|
||||
#file-name {
|
||||
margin-top: 5px;
|
||||
@ -167,13 +160,20 @@
|
||||
<label for="student-id">学号:</label>
|
||||
<input type="text" id="student-id" name="student-id" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="assignment">选择作业:</label>
|
||||
<select id="assignment" name="assignment" required>
|
||||
<option value="">请选择作业</option>
|
||||
<option value="homework1">平时作业1</option>
|
||||
<option value="homework2">平时作业2</option>
|
||||
<option value="homework3">平时作业3</option>
|
||||
<option value="final_homework">结课大作业</option>
|
||||
</select>
|
||||
</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>
|
||||
<button type="button" class="file-input-button">选择文件 <input type="file" id="file" name="file" required></button>
|
||||
</div>
|
||||
<div id="file-name">未选择文件</div>
|
||||
</div>
|
||||
@ -195,122 +195,148 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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();
|
||||
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');
|
||||
const student = document.getElementById('student').value;
|
||||
const studentId = document.getElementById('student-id').value;
|
||||
const assignment = document.getElementById('assignment').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%的进度
|
||||
if (!student || !studentId || !assignment || !file) {
|
||||
alert('请提供学生姓名、学号、选择作业,并选择一个文件。');
|
||||
return;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// 实际上传
|
||||
const uploadResponse = await fetch(data.url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': data.content_type
|
||||
statusDiv.textContent = '准备上传...';
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&assignment=${encodeURIComponent(assignment)}&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 = '文件上传成功';
|
||||
|
||||
// 上传成功后,记录提交信息
|
||||
const recordResponse = await fetch('/record-submission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student,
|
||||
student_id: studentId,
|
||||
assignment,
|
||||
filename: file.name
|
||||
})
|
||||
});
|
||||
|
||||
if (!recordResponse.ok) {
|
||||
const errorRecordData = await recordResponse.json();
|
||||
throw new Error(`Failed to record submission: ${errorRecordData.error}`);
|
||||
}
|
||||
|
||||
alert('文件上传并记录成功');
|
||||
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
statusDiv.textContent = `错误: ${error.message}`;
|
||||
alert('发生错误: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
||||
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];
|
||||
}
|
||||
|
||||
// 确保进度达到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;
|
||||
});
|
||||
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} 秒`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
342
index.txt
Normal file
342
index.txt
Normal file
@ -0,0 +1,342 @@
|
||||
<!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"], select {
|
||||
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] {
|
||||
display: none;
|
||||
}
|
||||
#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="assignment">选择作业:</label>
|
||||
<select id="assignment" name="assignment" required>
|
||||
<option value="">请选择作业</option>
|
||||
<option value="homework1">平时作业1</option>
|
||||
<option value="homework2">平时作业2</option>
|
||||
<option value="homework3">平时作业3</option>
|
||||
<option value="final_homework">结课大作业</option>
|
||||
</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">提交 (截止日期: 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.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();
|
||||
|
||||
const student = document.getElementById('student').value;
|
||||
const studentId = document.getElementById('student-id').value;
|
||||
const assignment = document.getElementById('assignment').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 || !assignment || !file) {
|
||||
alert('请提供学生姓名、学号、选择作业,并选择一个文件。');
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.textContent = '准备上传...';
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&assignment=${encodeURIComponent(assignment)}&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 = '文件上传成功';
|
||||
|
||||
// 上传成功后,记录提交信息
|
||||
const recordResponse = await fetch('/record-submission', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student,
|
||||
student_id: studentId,
|
||||
assignment,
|
||||
filename: file.name
|
||||
})
|
||||
});
|
||||
|
||||
if (!recordResponse.ok) {
|
||||
const errorRecordData = await recordResponse.json();
|
||||
throw new Error(`Failed to record submission: ${errorRecordData.error}`);
|
||||
}
|
||||
|
||||
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} 秒`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -11,3 +11,10 @@ ID,学生姓名,学号,提交的文件
|
||||
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
|
||||
2024-07-03T03:06:53.527517,李顺,342,homework1,计算机网络_数据链路层.ppt
|
||||
2024-07-03T03:10:31.977965,Sure,324234,homework2,Sure_324234_彩虹易支付纯净源码.zip
|
||||
2024-07-03T03:15:33.951611,342,453534,homework2,342_453534_男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf
|
||||
2024-07-03T03:22:12.013752,李无天,983,homework3,李无天_983_真正全集:王阳明全集(以权威的隆庆初刻本《王文成公全书》为底本,增补2卷旧本未刊内容!王阳明的“心学哲理、文治武功、传奇人生”全在这... (Z-Library).pdf
|
||||
2024-07-03T03:24:24.223088,王刚,324,homework3,王刚_324_期末复习.pptx
|
||||
2024-07-06T19:16:46.811930,嗷呜呜0706,321,homework1,嗷呜呜0706_321_知行合一王阳明套装 (度阴山) (Z-Library).pdf
|
||||
2024-07-07T00:28:05.848994,test,432,homework1,期末复习.pptx
|
||||
|
Can't render this file because it has a wrong number of fields in line 14.
|
BIN
submissions.xlsx
BIN
submissions.xlsx
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user