mobile . themes
@@ -27,7 +27,8 @@
|
||||
"Bash(git add:*)",
|
||||
"Bash(git push)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
"Bash(git push:*)",
|
||||
"Bash(sqlite3:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
113
app.py
@@ -13,7 +13,7 @@ from functools import wraps
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, request, jsonify, session,
|
||||
redirect, url_for, flash, abort, send_from_directory
|
||||
redirect, url_for, flash, abort, send_from_directory, send_file
|
||||
)
|
||||
from flask_caching import Cache
|
||||
from werkzeug.utils import secure_filename
|
||||
@@ -27,6 +27,7 @@ from openai import OpenAI as DashScopeClient
|
||||
# 引入数据库模型(添加 TeacherApplication)
|
||||
from models import db, User, Exam, Submission, Draft, Contest, ContestMembership, ContestApplication, Post, Reply, Poll, Report, Bookmark, Reaction, Notification, EditHistory, ContestRegistration, TeacherApplication, Friend, ExamBookmark, ChatRoom, ChatRoomMember, Message, MessageReaction, QuestionBankItem, InviteCode, SystemNotification
|
||||
from flask_socketio import SocketIO, emit, join_room as sio_join, leave_room as sio_leave
|
||||
from excel_export import export_users_to_excel, export_exams_to_excel, export_submissions_to_excel, export_contests_to_excel, export_posts_to_excel
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -2761,9 +2762,18 @@ def api_update_exam_visibility(exam_id):
|
||||
if new_visibility not in ('private', 'public'):
|
||||
return jsonify({'success': False, 'message': '无效的可见性设置'}), 400
|
||||
|
||||
# 检查权限:只有教师和管理员可以设置为公开
|
||||
if new_visibility == 'public' and user.get('role') not in ('teacher', 'admin'):
|
||||
return jsonify({'success': False, 'message': '只有教师和管理员可以创建公开考试'}), 403
|
||||
# 检查权限:教师、管理员、杯赛负责人和杯赛老师可以设置为公开
|
||||
if new_visibility == 'public':
|
||||
has_permission = user.get('role') in ('teacher', 'admin')
|
||||
# 如果是杯赛考试,检查是否为杯赛负责人或老师
|
||||
if not has_permission and exam.contest_id:
|
||||
membership = ContestMembership.query.filter_by(
|
||||
user_id=user['id'], contest_id=exam.contest_id).first()
|
||||
if membership and membership.role in ('owner', 'teacher'):
|
||||
has_permission = True
|
||||
|
||||
if not has_permission:
|
||||
return jsonify({'success': False, 'message': '只有教师、管理员或杯赛负责人/老师可以创建公开考试'}), 403
|
||||
|
||||
exam.visibility = new_visibility
|
||||
db.session.commit()
|
||||
@@ -3303,6 +3313,7 @@ def api_search_posts():
|
||||
'content': p.content[:200] + ('...' if len(p.content) > 200 else ''),
|
||||
'author': p.author.name,
|
||||
'author_id': p.author_id,
|
||||
'author_avatar': p.author.avatar or '',
|
||||
'author_level': author_level,
|
||||
'tag': p.tag,
|
||||
'is_official': p.is_official,
|
||||
@@ -3360,6 +3371,7 @@ def api_get_post(post_id):
|
||||
'content': post.content,
|
||||
'author': post.author.name,
|
||||
'author_id': post.author_id,
|
||||
'author_avatar': post.author.avatar or '',
|
||||
'tag': post.tag,
|
||||
'is_official': post.is_official,
|
||||
'pinned': post.pinned,
|
||||
@@ -3380,6 +3392,7 @@ def api_get_post(post_id):
|
||||
'content': r.content,
|
||||
'author': r.author.name,
|
||||
'author_id': r.author_id,
|
||||
'author_avatar': r.author.avatar or '',
|
||||
'likes': r.likes,
|
||||
'reply_to': r.reply_to,
|
||||
'created_at': r.created_at.strftime('%Y-%m-%d %H:%M'),
|
||||
@@ -4895,5 +4908,97 @@ with app.app_context():
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
# ========== Excel导出路由 ==========
|
||||
|
||||
@app.route('/admin/export/users')
|
||||
def admin_export_users():
|
||||
"""导出用户数据为Excel"""
|
||||
user = session.get('user')
|
||||
if not user or user['role'] != 'admin':
|
||||
return jsonify({'error': '无权限'}), 403
|
||||
|
||||
users = User.query.order_by(User.created_at.desc()).all()
|
||||
output = export_users_to_excel(users)
|
||||
|
||||
filename = f"用户数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
@app.route('/admin/export/exams')
|
||||
def admin_export_exams():
|
||||
"""导出考试数据为Excel"""
|
||||
user = session.get('user')
|
||||
if not user or user['role'] != 'admin':
|
||||
return jsonify({'error': '无权限'}), 403
|
||||
|
||||
exams = Exam.query.order_by(Exam.created_at.desc()).all()
|
||||
output = export_exams_to_excel(exams)
|
||||
|
||||
filename = f"考试数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
@app.route('/admin/export/submissions')
|
||||
def admin_export_submissions():
|
||||
"""导出提交记录为Excel"""
|
||||
user = session.get('user')
|
||||
if not user or user['role'] != 'admin':
|
||||
return jsonify({'error': '无权限'}), 403
|
||||
|
||||
submissions = Submission.query.order_by(Submission.submitted_at.desc()).all()
|
||||
output = export_submissions_to_excel(submissions)
|
||||
|
||||
filename = f"提交记录_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
@app.route('/admin/export/contests')
|
||||
def admin_export_contests():
|
||||
"""导出杯赛数据为Excel"""
|
||||
user = session.get('user')
|
||||
if not user or user['role'] != 'admin':
|
||||
return jsonify({'error': '无权限'}), 403
|
||||
|
||||
contests = Contest.query.order_by(Contest.created_at.desc()).all()
|
||||
output = export_contests_to_excel(contests)
|
||||
|
||||
filename = f"杯赛数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
@app.route('/admin/export/posts')
|
||||
def admin_export_posts():
|
||||
"""导出论坛帖子为Excel"""
|
||||
user = session.get('user')
|
||||
if not user or user['role'] != 'admin':
|
||||
return jsonify({'error': '无权限'}), 403
|
||||
|
||||
posts = Post.query.order_by(Post.created_at.desc()).all()
|
||||
output = export_posts_to_excel(posts)
|
||||
|
||||
filename = f"论坛帖子_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, debug=True,host='0.0.0.0', port=5080)
|
||||
55
check_database.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
# 设置输出编码为UTF-8
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
db_path = 'instance/database.db'
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查数据库编码
|
||||
cursor.execute('PRAGMA encoding')
|
||||
encoding = cursor.fetchone()[0]
|
||||
print(f"数据库编码: {encoding}")
|
||||
print("-" * 50)
|
||||
|
||||
# 获取所有表
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
print(f"\n数据表列表 (共{len(tables)}个):")
|
||||
for table in tables:
|
||||
print(f" - {table}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
|
||||
# 检查每个表的数据
|
||||
for table in tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"\n表 '{table}': {count} 条记录")
|
||||
|
||||
# 显示表结构
|
||||
cursor.execute(f"PRAGMA table_info({table})")
|
||||
columns = cursor.fetchall()
|
||||
print(f" 字段: {', '.join([col[1] for col in columns])}")
|
||||
|
||||
# 显示前3条数据示例
|
||||
if count > 0:
|
||||
cursor.execute(f"SELECT * FROM {table} LIMIT 3")
|
||||
rows = cursor.fetchall()
|
||||
print(f" 示例数据 (前{len(rows)}条):")
|
||||
for i, row in enumerate(rows, 1):
|
||||
print(f" 记录{i}: {row[:5]}...") # 只显示前5个字段
|
||||
|
||||
conn.close()
|
||||
print("\n" + "=" * 50)
|
||||
print("数据库检查完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
217
excel_export.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# excel_export.py
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
def export_users_to_excel(users):
|
||||
"""导出用户数据到Excel"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "用户数据"
|
||||
|
||||
# 设置表头样式
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
# 表头
|
||||
headers = ["ID", "用户名", "邮箱", "手机号", "角色", "状态", "完成考试数", "注册时间"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
# 数据行
|
||||
for row, user in enumerate(users, 2):
|
||||
ws.cell(row=row, column=1, value=user.id)
|
||||
ws.cell(row=row, column=2, value=user.name)
|
||||
ws.cell(row=row, column=3, value=user.email or "")
|
||||
ws.cell(row=row, column=4, value=user.phone or "")
|
||||
ws.cell(row=row, column=5, value=user.role)
|
||||
ws.cell(row=row, column=6, value="已封禁" if user.is_banned else "正常")
|
||||
ws.cell(row=row, column=7, value=user.completed_exams)
|
||||
ws.cell(row=row, column=8, value=user.created_at.strftime("%Y-%m-%d %H:%M:%S") if user.created_at else "")
|
||||
|
||||
# 调整列宽
|
||||
ws.column_dimensions['A'].width = 8
|
||||
ws.column_dimensions['B'].width = 20
|
||||
ws.column_dimensions['C'].width = 30
|
||||
ws.column_dimensions['D'].width = 15
|
||||
ws.column_dimensions['E'].width = 12
|
||||
ws.column_dimensions['F'].width = 10
|
||||
ws.column_dimensions['G'].width = 12
|
||||
ws.column_dimensions['H'].width = 20
|
||||
|
||||
# 保存到内存
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
def export_exams_to_excel(exams):
|
||||
"""导出考试数据到Excel"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "考试数据"
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
headers = ["ID", "考试名称", "创建者ID", "总分", "时长(分钟)", "状态", "提交数", "创建时间"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
for row, exam in enumerate(exams, 2):
|
||||
ws.cell(row=row, column=1, value=exam.id)
|
||||
ws.cell(row=row, column=2, value=exam.title)
|
||||
ws.cell(row=row, column=3, value=exam.creator_id)
|
||||
ws.cell(row=row, column=4, value=exam.total_score)
|
||||
ws.cell(row=row, column=5, value=exam.duration)
|
||||
ws.cell(row=row, column=6, value=exam.status)
|
||||
ws.cell(row=row, column=7, value=len(exam.submissions) if exam.submissions else 0)
|
||||
ws.cell(row=row, column=8, value=exam.created_at.strftime("%Y-%m-%d %H:%M:%S") if exam.created_at else "")
|
||||
|
||||
ws.column_dimensions['A'].width = 8
|
||||
ws.column_dimensions['B'].width = 30
|
||||
ws.column_dimensions['C'].width = 12
|
||||
ws.column_dimensions['D'].width = 10
|
||||
ws.column_dimensions['E'].width = 15
|
||||
ws.column_dimensions['F'].width = 12
|
||||
ws.column_dimensions['G'].width = 10
|
||||
ws.column_dimensions['H'].width = 20
|
||||
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
def export_submissions_to_excel(submissions):
|
||||
"""导出提交记录到Excel"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "提交记录"
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
headers = ["ID", "考试ID", "考试名称", "用户ID", "用户名", "得分", "总分", "状态", "提交时间"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
for row, sub in enumerate(submissions, 2):
|
||||
ws.cell(row=row, column=1, value=sub.id)
|
||||
ws.cell(row=row, column=2, value=sub.exam_id)
|
||||
ws.cell(row=row, column=3, value=sub.exam.title if sub.exam else "")
|
||||
ws.cell(row=row, column=4, value=sub.user_id)
|
||||
ws.cell(row=row, column=5, value=sub.user.name if sub.user else "")
|
||||
ws.cell(row=row, column=6, value=sub.score if sub.score is not None else "")
|
||||
ws.cell(row=row, column=7, value=sub.total_score)
|
||||
ws.cell(row=row, column=8, value=sub.status)
|
||||
ws.cell(row=row, column=9, value=sub.submitted_at.strftime("%Y-%m-%d %H:%M:%S") if sub.submitted_at else "")
|
||||
|
||||
ws.column_dimensions['A'].width = 8
|
||||
ws.column_dimensions['B'].width = 10
|
||||
ws.column_dimensions['C'].width = 30
|
||||
ws.column_dimensions['D'].width = 10
|
||||
ws.column_dimensions['E'].width = 20
|
||||
ws.column_dimensions['F'].width = 10
|
||||
ws.column_dimensions['G'].width = 10
|
||||
ws.column_dimensions['H'].width = 12
|
||||
ws.column_dimensions['I'].width = 20
|
||||
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
def export_contests_to_excel(contests):
|
||||
"""导出杯赛数据到Excel"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "杯赛数据"
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
headers = ["ID", "杯赛名称", "主办方", "开始日期", "结束日期", "状态", "参与人数", "创建时间"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
for row, contest in enumerate(contests, 2):
|
||||
ws.cell(row=row, column=1, value=contest.id)
|
||||
ws.cell(row=row, column=2, value=contest.name)
|
||||
ws.cell(row=row, column=3, value=contest.organizer or "")
|
||||
ws.cell(row=row, column=4, value=contest.start_date or "")
|
||||
ws.cell(row=row, column=5, value=contest.end_date or "")
|
||||
ws.cell(row=row, column=6, value=contest.status)
|
||||
ws.cell(row=row, column=7, value=contest.participants)
|
||||
ws.cell(row=row, column=8, value=contest.created_at.strftime("%Y-%m-%d %H:%M:%S") if contest.created_at else "")
|
||||
|
||||
ws.column_dimensions['A'].width = 8
|
||||
ws.column_dimensions['B'].width = 30
|
||||
ws.column_dimensions['C'].width = 20
|
||||
ws.column_dimensions['D'].width = 15
|
||||
ws.column_dimensions['E'].width = 15
|
||||
ws.column_dimensions['F'].width = 12
|
||||
ws.column_dimensions['G'].width = 12
|
||||
ws.column_dimensions['H'].width = 20
|
||||
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
def export_posts_to_excel(posts):
|
||||
"""导出论坛帖子到Excel"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "论坛帖子"
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
headers = ["ID", "标题", "作者", "分类", "浏览数", "回复数", "点赞数", "创建时间"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = header_alignment
|
||||
|
||||
for row, post in enumerate(posts, 2):
|
||||
ws.cell(row=row, column=1, value=post.id)
|
||||
ws.cell(row=row, column=2, value=post.title)
|
||||
ws.cell(row=row, column=3, value=post.author.name if post.author else "")
|
||||
ws.cell(row=row, column=4, value=post.category or "")
|
||||
ws.cell(row=row, column=5, value=post.views)
|
||||
ws.cell(row=row, column=6, value=post.reply_count)
|
||||
ws.cell(row=row, column=7, value=post.like_count)
|
||||
ws.cell(row=row, column=8, value=post.created_at.strftime("%Y-%m-%d %H:%M:%S") if post.created_at else "")
|
||||
|
||||
ws.column_dimensions['A'].width = 8
|
||||
ws.column_dimensions['B'].width = 40
|
||||
ws.column_dimensions['C'].width = 20
|
||||
ws.column_dimensions['D'].width = 15
|
||||
ws.column_dimensions['E'].width = 10
|
||||
ws.column_dimensions['F'].width = 10
|
||||
ws.column_dimensions['G'].width = 10
|
||||
ws.column_dimensions['H'].width = 20
|
||||
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
@@ -7,3 +7,5 @@ flask-socketio>=5.3.0
|
||||
PyMuPDF>=1.24.0
|
||||
openai>=1.14.0
|
||||
Flask-Caching>=2.3.0
|
||||
openpyxl>=3.1.0
|
||||
Flask-SQLAlchemy>=3.0.0
|
||||
|
||||
@@ -463,7 +463,85 @@ button[onclick^="quickScore"]:active {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 粒子背景容器 */
|
||||
/* 聊天界面优化 */
|
||||
#messageArea {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#messageArea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
#messageArea::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#messageArea::-webkit-scrollbar-thumb {
|
||||
background: rgba(59, 130, 246, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#messageArea::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(59, 130, 246, 0.7);
|
||||
}
|
||||
|
||||
/* 聊天视图布局修复 */
|
||||
#chatView {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#chatView.flex {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
#chatView.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 确保输入框始终在底部 */
|
||||
#chatView > div:last-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* 左侧面板滚动优化 */
|
||||
#roomList::-webkit-scrollbar,
|
||||
#chatNotifPanel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#roomList::-webkit-scrollbar-track,
|
||||
#chatNotifPanel::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
}
|
||||
|
||||
#roomList::-webkit-scrollbar-thumb,
|
||||
#chatNotifPanel::-webkit-scrollbar-thumb {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 移除hide-scrollbar在聊天区域的影响 */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 但是消息区域要显示滚动条 */
|
||||
#messageArea.hide-scrollbar::-webkit-scrollbar {
|
||||
display: block !important;
|
||||
width: 8px;
|
||||
}
|
||||
.particles-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -473,3 +551,173 @@ button[onclick^="quickScore"]:active {
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 响应式表格容器 - 解决表格内容显示不全 */
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb {
|
||||
background: rgba(59, 130, 246, 0.5);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.table-responsive::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(59, 130, 246, 0.7);
|
||||
}
|
||||
|
||||
/* 表格优化 - 确保内容完整显示 */
|
||||
.table-futuristic {
|
||||
min-width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-futuristic td,
|
||||
.table-futuristic th {
|
||||
padding: 12px 16px;
|
||||
vertical-align: middle;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* 文字换行优化 */
|
||||
.text-wrap {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-ellipsis-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 管理后台内容区域优化 */
|
||||
.admin-content-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 卡片内容区域优化 */
|
||||
.card-content-scrollable {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.card-content-scrollable::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.card-content-scrollable::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-content-scrollable::-webkit-scrollbar-thumb {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.futuristic-card,
|
||||
.futuristic-card-dark {
|
||||
border-radius: 16px;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.table-futuristic td,
|
||||
.table-futuristic th {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-futuristic,
|
||||
.btn-outline-futuristic {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图片容器优化 */
|
||||
.img-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* 长文本内容优化 */
|
||||
.content-text {
|
||||
line-height: 1.8;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 统计卡片响应式优化 */
|
||||
@media (max-width: 640px) {
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 侧边栏响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-futuristic {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 防止内容溢出 */
|
||||
.overflow-safe {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 弹性容器优化 */
|
||||
.flex-container-safe {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* 网格容器优化 */
|
||||
.grid-container-safe {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
Before Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 122 KiB |
@@ -51,10 +51,12 @@
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<main class="flex-1 min-w-0">
|
||||
<div class="bg-white/80 backdrop-blur-xl rounded-3xl shadow-sm border border-slate-100 p-6 sm:p-8 relative">
|
||||
<div class="bg-white/80 backdrop-blur-xl rounded-3xl shadow-sm border border-slate-100 p-6 sm:p-8 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-64 h-64 bg-indigo-50/30 rounded-bl-full -z-10"></div>
|
||||
<div class="admin-content-wrapper">
|
||||
{% block admin_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
<h1 class="text-3xl font-extrabold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">杯赛管理</h1>
|
||||
|
||||
<div class="futuristic-card-dark overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-futuristic">
|
||||
<div class="table-responsive">
|
||||
<table class="table-futuristic" style="min-width: 900px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>主办方</th>
|
||||
<th>状态</th>
|
||||
<th>开始日期</th>
|
||||
<th>操作</th>
|
||||
<th style="min-width: 60px;">ID</th>
|
||||
<th style="min-width: 200px;">名称</th>
|
||||
<th style="min-width: 150px;">主办方</th>
|
||||
<th style="min-width: 120px;">状态</th>
|
||||
<th style="min-width: 150px;">开始日期</th>
|
||||
<th style="min-width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contests-tbody">
|
||||
@@ -56,8 +56,8 @@ async function loadContests() {
|
||||
: '<span class="text-xs text-red-500">已废止</span>';
|
||||
html += `<tr>
|
||||
<td>${c.id}</td>
|
||||
<td class="font-medium text-slate-200">${c.name}</td>
|
||||
<td class="text-slate-400">${c.organizer || '-'}</td>
|
||||
<td class="font-medium text-slate-200" style="max-width: 200px; white-space: normal; word-wrap: break-word;">${c.name}</td>
|
||||
<td class="text-slate-400" style="max-width: 150px; white-space: normal; word-wrap: break-word;">${c.organizer || '-'}</td>
|
||||
<td><span class="badge-futuristic ${statusClass}">${statusText}</span></td>
|
||||
<td class="text-slate-400">${c.start_date || '-'}</td>
|
||||
<td class="space-x-2">
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
|
||||
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform">
|
||||
<div class="absolute -right-4 -bottom-4 w-24 h-24 bg-cyan-500/20 rounded-full blur-2xl group-hover:bg-cyan-500/30 transition-colors"></div>
|
||||
<div class="flex items-center gap-3 mb-3 relative z-10">
|
||||
<div class="w-10 h-10 rounded-xl bg-cyan-500/20 text-cyan-400 flex items-center justify-center shadow-lg shadow-cyan-500/20 border border-cyan-500/30">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-slate-300">总用户数</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">总用户数</div>
|
||||
</div>
|
||||
<div id="stat-users" class="text-3xl font-black text-white relative z-10">-</div>
|
||||
<div id="stat-users" class="text-2xl sm:text-3xl font-black text-white relative z-10">-</div>
|
||||
</div>
|
||||
|
||||
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform">
|
||||
@@ -33,9 +33,9 @@
|
||||
<div class="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center shadow-lg shadow-purple-500/20 border border-purple-500/30">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-slate-300">赛事总数</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">赛事总数</div>
|
||||
</div>
|
||||
<div id="stat-contests" class="text-3xl font-black text-white relative z-10">-</div>
|
||||
<div id="stat-contests" class="text-2xl sm:text-3xl font-black text-white relative z-10">-</div>
|
||||
</div>
|
||||
|
||||
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform">
|
||||
@@ -44,9 +44,9 @@
|
||||
<div class="w-10 h-10 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center shadow-lg shadow-emerald-500/20 border border-emerald-500/30">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-slate-300">考试总数</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">考试总数</div>
|
||||
</div>
|
||||
<div id="stat-exams" class="text-3xl font-black text-white relative z-10">-</div>
|
||||
<div id="stat-exams" class="text-2xl sm:text-3xl font-black text-white relative z-10">-</div>
|
||||
</div>
|
||||
|
||||
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform">
|
||||
@@ -55,9 +55,9 @@
|
||||
<div class="w-10 h-10 rounded-xl bg-pink-500/20 text-pink-400 flex items-center justify-center shadow-lg shadow-pink-500/20 border border-pink-500/30">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/></svg>
|
||||
</div>
|
||||
<div class="text-sm font-bold text-slate-300">社区帖子</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">社区帖子</div>
|
||||
</div>
|
||||
<div id="stat-posts" class="text-3xl font-black text-white relative z-10">-</div>
|
||||
<div id="stat-posts" class="text-2xl sm:text-3xl font-black text-white relative z-10">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,34 +105,34 @@
|
||||
</div>
|
||||
<h2 class="text-lg font-bold bg-gradient-to-r from-teal-400 to-emerald-400 bg-clip-text text-transparent">快捷操作</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4">
|
||||
<a href="/admin/contests" class="futuristic-card-dark hover:border-indigo-500/50 hover:shadow-lg hover:shadow-indigo-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-indigo-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-indigo-500/30">🏆</div>
|
||||
<div class="text-sm font-bold text-slate-300">杯赛管理</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-3 sm:gap-4">
|
||||
<a href="/admin/contests" class="futuristic-card-dark hover:border-indigo-500/50 hover:shadow-lg hover:shadow-indigo-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-indigo-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-indigo-500/30">🏆</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">杯赛管理</div>
|
||||
</a>
|
||||
<a href="/admin/contest-applications" class="futuristic-card-dark hover:border-orange-500/50 hover:shadow-lg hover:shadow-orange-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-orange-500/30">📋</div>
|
||||
<div class="text-sm font-bold text-slate-300">杯赛申请</div>
|
||||
<a href="/admin/contest-applications" class="futuristic-card-dark hover:border-orange-500/50 hover:shadow-lg hover:shadow-orange-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-orange-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-orange-500/30">📋</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">杯赛申请</div>
|
||||
</a>
|
||||
<a href="/admin/teacher-applications" class="futuristic-card-dark hover:border-purple-500/50 hover:shadow-lg hover:shadow-purple-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-purple-500/30">👨🏫</div>
|
||||
<div class="text-sm font-bold text-slate-300">教师申请</div>
|
||||
<a href="/admin/teacher-applications" class="futuristic-card-dark hover:border-purple-500/50 hover:shadow-lg hover:shadow-purple-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-purple-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-purple-500/30">👨🏫</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">教师申请</div>
|
||||
</a>
|
||||
<a href="/admin/exams" class="futuristic-card-dark hover:border-emerald-500/50 hover:shadow-lg hover:shadow-emerald-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-emerald-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-emerald-500/30">📝</div>
|
||||
<div class="text-sm font-bold text-slate-300">考试管理</div>
|
||||
<a href="/admin/exams" class="futuristic-card-dark hover:border-emerald-500/50 hover:shadow-lg hover:shadow-emerald-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-emerald-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-emerald-500/30">📝</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">考试管理</div>
|
||||
</a>
|
||||
<a href="/admin/users" class="futuristic-card-dark hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-blue-500/30">👥</div>
|
||||
<div class="text-sm font-bold text-slate-300">用户管理</div>
|
||||
<a href="/admin/users" class="futuristic-card-dark hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-blue-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-blue-500/30">👥</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">用户管理</div>
|
||||
</a>
|
||||
<a href="/admin/posts" class="futuristic-card-dark hover:border-amber-500/50 hover:shadow-lg hover:shadow-amber-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-amber-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-amber-500/30">💬</div>
|
||||
<div class="text-sm font-bold text-slate-300">帖子管理</div>
|
||||
<a href="/admin/posts" class="futuristic-card-dark hover:border-amber-500/50 hover:shadow-lg hover:shadow-amber-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-amber-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-amber-500/30">💬</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">帖子管理</div>
|
||||
</a>
|
||||
<a href="/admin/notifications" class="futuristic-card-dark hover:border-rose-500/50 hover:shadow-lg hover:shadow-rose-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-5">
|
||||
<div class="w-12 h-12 rounded-xl bg-rose-500/20 flex items-center justify-center text-2xl mb-3 group-hover:scale-110 transition-transform border border-rose-500/30">📢</div>
|
||||
<div class="text-sm font-bold text-slate-300">通知管理</div>
|
||||
<a href="/admin/notifications" class="futuristic-card-dark hover:border-rose-500/50 hover:shadow-lg hover:shadow-rose-500/20 hover:-translate-y-1 transition-all text-center group flex flex-col items-center p-4 sm:p-5">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-rose-500/20 flex items-center justify-center text-xl sm:text-2xl mb-2 sm:mb-3 group-hover:scale-110 transition-transform border border-rose-500/30">📢</div>
|
||||
<div class="text-xs sm:text-sm font-bold text-slate-300">通知管理</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
<h1 class="text-3xl font-extrabold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">考试管理</h1>
|
||||
|
||||
<div class="futuristic-card-dark overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-futuristic">
|
||||
<div class="table-responsive">
|
||||
<table class="table-futuristic" style="min-width: 1000px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>科目</th>
|
||||
<th>出题人</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
<th style="min-width: 60px;">ID</th>
|
||||
<th style="min-width: 200px;">标题</th>
|
||||
<th style="min-width: 100px;">科目</th>
|
||||
<th style="min-width: 120px;">出题人</th>
|
||||
<th style="min-width: 100px;">状态</th>
|
||||
<th style="min-width: 150px;">创建时间</th>
|
||||
<th style="min-width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="exams-tbody"></tbody>
|
||||
@@ -68,7 +68,7 @@ async function loadExams() {
|
||||
const isAvailable = e.status === 'available';
|
||||
html += `<tr>
|
||||
<td>${e.id}</td>
|
||||
<td class="font-medium text-slate-200 max-w-xs truncate">${e.title}</td>
|
||||
<td class="font-medium text-slate-200" style="max-width: 200px; white-space: normal; word-wrap: break-word;">${e.title}</td>
|
||||
<td class="text-slate-400">${e.subject || '-'}</td>
|
||||
<td class="text-slate-400">${e.creator_name || '-'}</td>
|
||||
<td><span class="badge-futuristic ${statusClass}">${statusText}</span></td>
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
</div>
|
||||
|
||||
<div class="futuristic-card-dark overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-futuristic">
|
||||
<div class="table-responsive">
|
||||
<table class="table-futuristic" style="min-width: 1000px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>作者</th>
|
||||
<th>标签</th>
|
||||
<th>置顶</th>
|
||||
<th>发布时间</th>
|
||||
<th>操作</th>
|
||||
<th style="min-width: 60px;">ID</th>
|
||||
<th style="min-width: 250px;">标题</th>
|
||||
<th style="min-width: 120px;">作者</th>
|
||||
<th style="min-width: 100px;">标签</th>
|
||||
<th style="min-width: 100px;">置顶</th>
|
||||
<th style="min-width: 150px;">发布时间</th>
|
||||
<th style="min-width: 200px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="posts-tbody"></tbody>
|
||||
@@ -84,7 +84,7 @@ async function loadPosts() {
|
||||
const tagClass = tagClassMap[p.tag] || 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||
html += `<tr>
|
||||
<td>${p.id}</td>
|
||||
<td class="font-medium text-slate-200 max-w-xs truncate">${p.title}</td>
|
||||
<td class="font-medium text-slate-200" style="max-width: 250px; white-space: normal; word-wrap: break-word;">${p.title}</td>
|
||||
<td class="text-slate-400">${p.author}</td>
|
||||
<td><span class="badge-futuristic ${tagClass}">${p.tag || '-'}</span></td>
|
||||
<td>
|
||||
|
||||
@@ -31,18 +31,18 @@
|
||||
|
||||
<!-- 桌面端表格视图 -->
|
||||
<div class="hidden md:block bg-white shadow-sm rounded-lg border border-slate-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<div class="table-responsive">
|
||||
<table class="min-w-full divide-y divide-slate-200" style="min-width: 1100px;">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">申请人</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">杯赛</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">姓名</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">邮箱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">申请理由</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">申请时间</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 60px;">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 100px;">申请人</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 150px;">杯赛</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 100px;">姓名</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 150px;">邮箱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 200px;">申请理由</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 150px;">申请时间</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider" style="min-width: 150px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="applications-table-body" class="bg-white divide-y divide-slate-200">
|
||||
@@ -50,10 +50,10 @@
|
||||
<tr data-id="{{ app.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900">{{ app.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900">{{ app.user.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{{ app.contest.name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-500" style="max-width: 150px; white-space: normal; word-wrap: break-word;">{{ app.contest.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{{ app.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{{ app.email }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-500 max-w-xs truncate">{{ app.reason }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-500" style="max-width: 150px; word-wrap: break-word;">{{ app.email }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-500" style="max-width: 200px; white-space: normal; word-wrap: break-word;">{{ app.reason }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{{ app.applied_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<form action="{{ url_for('approve_teacher_application', app_id=app.id) }}" method="post" style="display:inline;">
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
</div>
|
||||
|
||||
<div class="futuristic-card-dark overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-futuristic">
|
||||
<div class="table-responsive">
|
||||
<table class="table-futuristic" style="min-width: 900px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>操作</th>
|
||||
<th style="min-width: 60px;">ID</th>
|
||||
<th style="min-width: 120px;">用户名</th>
|
||||
<th style="min-width: 180px;">邮箱</th>
|
||||
<th style="min-width: 100px;">角色</th>
|
||||
<th style="min-width: 100px;">状态</th>
|
||||
<th style="min-width: 150px;">注册时间</th>
|
||||
<th style="min-width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天视图 -->
|
||||
<div id="chatView" class="flex-1 flex-col hidden z-10 relative">
|
||||
<div id="chatView" class="flex-1 hidden z-10 relative h-full">
|
||||
<!-- 顶栏 -->
|
||||
<div id="chatHeader" class="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-slate-800/60 backdrop-blur-xl sticky top-0 z-20">
|
||||
<div id="chatHeader" class="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-slate-800/60 backdrop-blur-xl flex-shrink-0 z-20">
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="mobileBack()" class="sm:hidden w-8 h-8 rounded-lg bg-slate-700/50 text-slate-300 flex items-center justify-center mr-1 border border-white/10" aria-label="返回">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
@@ -114,9 +114,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息区域 -->
|
||||
<div id="messageArea" class="flex-1 overflow-y-auto hide-scrollbar px-6 py-4 space-y-5" onscroll="handleScroll()"></div>
|
||||
<div id="messageArea" class="flex-1 overflow-y-auto px-6 py-4 space-y-5" style="overflow-anchor: none;" onscroll="handleScroll()"></div>
|
||||
<!-- 引用回复预览 -->
|
||||
<div id="replyPreview" class="px-4 py-3 bg-cyan-500/10 backdrop-blur-md border-t border-cyan-500/30 hidden flex items-center justify-between shadow-inner">
|
||||
<div id="replyPreview" class="px-4 py-3 bg-cyan-500/10 backdrop-blur-md border-t border-cyan-500/30 hidden flex items-center justify-between shadow-inner flex-shrink-0">
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<div class="w-1 h-4 bg-cyan-400 rounded-full"></div>
|
||||
<div class="text-sm font-medium text-cyan-300 truncate"><span id="replyToText"></span></div>
|
||||
@@ -126,7 +126,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<!-- 输入区域 -->
|
||||
<div class="p-4 bg-slate-800/80 backdrop-blur-xl border-t border-white/10 shadow-[0_-4px_20px_-15px_rgba(0,0,0,0.3)]">
|
||||
<div class="p-4 bg-slate-800/80 backdrop-blur-xl border-t border-white/10 shadow-[0_-4px_20px_-15px_rgba(0,0,0,0.3)] flex-shrink-0 relative">
|
||||
<!-- Emoji 面板 -->
|
||||
<div id="emojiPanel" class="hidden absolute bottom-[calc(100%+10px)] left-4 p-3 futuristic-card-dark backdrop-blur-xl rounded-2xl shadow-xl max-h-48 overflow-y-auto w-64 z-50">
|
||||
<div class="flex flex-wrap gap-2 justify-center" id="emojiGrid"></div>
|
||||
@@ -162,7 +162,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 futuristic-card-dark rounded-2xl shadow-sm focus-within:ring-2 focus-within:ring-cyan-500/30 focus-within:border-cyan-400/50 transition-all overflow-hidden">
|
||||
<textarea id="msgInput" rows="1" placeholder="输入消息... (Enter发送, Shift+Enter换行)" class="flex-1 w-full px-4 py-3.5 text-sm bg-transparent text-white placeholder-slate-500 border-none focus:outline-none focus:ring-0 resize-none max-h-32 min-h-[48px] hide-scrollbar" oninput="handleTyping(); this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';"></textarea>
|
||||
<textarea id="msgInput" rows="1" placeholder="输入消息... (Enter发送, Shift+Enter换行)" class="flex-1 w-full px-4 py-3.5 text-sm bg-transparent text-white placeholder-slate-500 border-none focus:outline-none focus:ring-0 resize-none max-h-32 min-h-[48px] hide-scrollbar" oninput="handleTyping(); this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';" onkeydown="handleInputKey(event)"></textarea>
|
||||
</div>
|
||||
|
||||
<button onclick="sendMessage()" class="btn-futuristic w-12 h-12 rounded-2xl flex items-center justify-center hover:shadow-lg hover:shadow-cyan-500/30 transform hover:-translate-y-0.5 transition-all flex-shrink-0 group">
|
||||
@@ -703,7 +703,10 @@ async function loadMessages(roomId, beforeId) {
|
||||
if (!beforeId) {
|
||||
area.innerHTML = '';
|
||||
data.messages.forEach(m => area.appendChild(createMsgEl(m)));
|
||||
// 确保滚动到底部
|
||||
setTimeout(() => {
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}, 100);
|
||||
} else {
|
||||
const oldH = area.scrollHeight;
|
||||
data.messages.forEach((m, i) => area.insertBefore(createMsgEl(m), area.children[i]));
|
||||
@@ -813,6 +816,7 @@ function sendMessage() {
|
||||
reply_to_id: replyToId, mentions: mentionsStr
|
||||
});
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
mentionMembers = [];
|
||||
cancelReply();
|
||||
document.getElementById('emojiPanel').classList.add('hidden');
|
||||
@@ -1337,8 +1341,14 @@ async function stopRecording() {
|
||||
socket.on('new_message', (msg) => {
|
||||
if (msg.room_id === currentRoomId) {
|
||||
const area = document.getElementById('messageArea');
|
||||
const wasAtBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 100;
|
||||
area.appendChild(createMsgEl(msg));
|
||||
// 如果用户在底部或者是自己发的消息,自动滚动到底部
|
||||
if (wasAtBottom || msg.sender_id === currentUser.id) {
|
||||
setTimeout(() => {
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
markRead(currentRoomId);
|
||||
}
|
||||
// 更新会话列表
|
||||
|
||||
@@ -15,35 +15,35 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="futuristic-card p-6 p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-2">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-start gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-xl sm:text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-2 break-words">
|
||||
{{ contest.name }}
|
||||
{% if contest.status == 'abolished' %}
|
||||
<span class="ml-2 badge-futuristic text-xs rounded-full">已废止</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="flex items-center space-x-4 text-sm text-slate-400">
|
||||
<span class="flex items-center">
|
||||
<div class="flex flex-wrap items-center gap-3 sm:gap-4 text-xs sm:text-sm text-slate-400">
|
||||
<span class="flex items-center whitespace-nowrap">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
{{ contest.start_date }}
|
||||
</span>
|
||||
<span class="flex items-center" id="participants-count">
|
||||
<span class="flex items-center whitespace-nowrap" id="participants-count">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
||||
<span id="participants-value">{{ contest.participants }}</span>人已报名
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<div class="flex flex-wrap gap-3 w-full lg:w-auto">
|
||||
{% if contest.status != 'abolished' %}
|
||||
{% if user %}
|
||||
<button id="register-btn"
|
||||
onclick="toggleRegistration({{ contest.id }})"
|
||||
class="{% if registered %}btn-outline-futuristic{% else %}btn-futuristic{% endif %} px-6 py-2 font-medium">
|
||||
class="{% if registered %}btn-outline-futuristic{% else %}btn-futuristic{% endif %} px-4 sm:px-6 py-2 font-medium text-sm whitespace-nowrap">
|
||||
{% if registered %}已报名{% else %}立即报名{% endif %}
|
||||
</button>
|
||||
{% if not is_member %}
|
||||
<a href="{{ url_for('apply_teacher', contest_id=contest.id) }}" class="btn-outline-futuristic px-6 py-2 font-medium">
|
||||
<a href="{{ url_for('apply_teacher', contest_id=contest.id) }}" class="btn-outline-futuristic px-4 sm:px-6 py-2 font-medium text-sm whitespace-nowrap">
|
||||
申请成为本杯赛老师
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -149,7 +149,7 @@ async function loadQuestions() {
|
||||
<p class="text-sm text-slate-800 mb-2">${escapeHtml(q.content)}</p>`;
|
||||
if (q.type === 'choice' && q.options && q.options.length) {
|
||||
html += '<div class="text-sm text-slate-600 space-y-1 ml-4">';
|
||||
q.options.forEach(opt => { html += `<div>${escapeHtml(opt)}</div>`; });
|
||||
q.options.forEach(opt => { html += `<div class="option-text">${opt}</div>`; });
|
||||
html += '</div>';
|
||||
}
|
||||
if (q.answer) {
|
||||
@@ -162,6 +162,13 @@ async function loadQuestions() {
|
||||
html += `</div></div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
// 渲染数学公式
|
||||
if (typeof renderMathInElement === 'function') {
|
||||
renderMathInElement(container, {
|
||||
delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('question-list').innerHTML = '<div class="text-center py-8 text-red-500">加载失败</div>';
|
||||
}
|
||||
|
||||
@@ -86,26 +86,31 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 顶部信息栏 -->
|
||||
<div class="futuristic-card p-4 sticky top-0 z-20">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-lg font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">{{ exam.title }}</h1>
|
||||
<div class="mt-1 text-sm text-slate-500">
|
||||
{{ exam.subject }} · {{ exam.duration }}分钟 · 满分{{ exam.total_score }}分
|
||||
<div class="futuristic-card p-4 sticky top-0 z-20 shadow-lg">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-lg sm:text-xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent truncate">{{ exam.title }}</h1>
|
||||
<div class="mt-1 text-xs sm:text-sm text-slate-500 flex flex-wrap gap-2">
|
||||
<span>{{ exam.subject }}</span>
|
||||
<span>·</span>
|
||||
<span>{{ exam.duration }}分钟</span>
|
||||
<span>·</span>
|
||||
<span>满分{{ exam.total_score }}分</span>
|
||||
{% if exam.scheduled_end %}
|
||||
· 截止:{{ exam.scheduled_end.strftime('%m-%d %H:%M') }}
|
||||
<span>·</span>
|
||||
<span>截止:{{ exam.scheduled_end.strftime('%m-%d %H:%M') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-sm text-slate-500">
|
||||
<div class="flex items-center gap-3 sm:gap-4 flex-wrap">
|
||||
<div class="text-xs sm:text-sm text-slate-500 whitespace-nowrap">
|
||||
<span id="progress-text">0</span>/{{ questions|length }} 已答
|
||||
</div>
|
||||
<div class="flex items-center text-red-600 font-medium">
|
||||
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<div class="flex items-center text-red-600 font-medium text-sm sm:text-base whitespace-nowrap">
|
||||
<svg class="w-4 h-4 sm:w-5 sm:h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<span id="timer">--:--:--</span>
|
||||
</div>
|
||||
<div id="tab-warning" class="hidden text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded">
|
||||
<div id="tab-warning" class="hidden text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded whitespace-nowrap">
|
||||
切屏 <span id="tab-count">0</span> 次
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +185,7 @@
|
||||
{% for opt in q.options %}
|
||||
<label class="flex items-center space-x-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50 cursor-pointer transition-colors option-label" data-qid="{{ q.id }}">
|
||||
<input type="radio" name="q-{{ q.id }}" value="{{ ['A','B','C','D'][loop.index0] }}" class="h-4 w-4 text-primary border-slate-300 focus:ring-primary answer-input" data-qid="{{ q.id }}" onchange="onAnswerChange({{ q.id }})">
|
||||
<span class="text-slate-700">{{ ['A','B','C','D'][loop.index0] }}. {{ opt }}</span>
|
||||
<span class="text-slate-700 option-text">{{ ['A','B','C','D'][loop.index0] }}. {{ opt|safe }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{% if is_answer %}border-green-300 bg-green-50
|
||||
{% elif is_selected and not is_answer %}border-red-300 bg-red-50
|
||||
{% else %}border-slate-100{% endif %}">
|
||||
<span class="text-sm text-slate-700">{{ letter }}. {{ opt }}</span>
|
||||
<span class="text-sm text-slate-700 option-text">{{ letter }}. {{ opt|safe }}</span>
|
||||
{% if is_selected %}<span class="text-xs {% if is_answer %}text-green-600{% else %}text-red-500{% endif %} font-medium">← 考生选择</span>{% endif %}
|
||||
{% if is_answer %}<span class="text-xs text-green-600 font-medium">✓ 正确</span>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
.print-btn { position: fixed; top: 20px; right: 20px; padding: 10px 24px; background: #3b82f6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; z-index: 100; }
|
||||
.print-btn:hover { background: #2563eb; }
|
||||
</style>
|
||||
<!-- KaTeX 数学公式渲染 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
|
||||
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false});"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button class="print-btn no-print" onclick="window.print()">打印 / 导出PDF</button>
|
||||
@@ -64,7 +69,7 @@
|
||||
{% if q.options %}
|
||||
<div class="options">
|
||||
{% for opt in q.options %}
|
||||
<div class="opt">{{ ['A','B','C','D'][loop.index0] }}. {{ opt }}</div>
|
||||
<div class="opt">{{ ['A','B','C','D'][loop.index0] }}. {{ opt|safe }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,43 +2,43 @@
|
||||
{% block title %}考试结果 - 智联青云{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">考试结果</h1>
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
||||
<h1 class="text-xl sm:text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">考试结果</h1>
|
||||
<a href="/exams" class="text-sm text-slate-500 hover:text-slate-700">← 返回列表</a>
|
||||
</div>
|
||||
<div class="futuristic-card p-6">
|
||||
<h2 class="text-xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">{{ exam.title }}</h2>
|
||||
<div class="mt-2 flex items-center text-sm text-slate-500 space-x-4">
|
||||
<div class="futuristic-card p-4 sm:p-6">
|
||||
<h2 class="text-lg sm:text-xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent break-words">{{ exam.title }}</h2>
|
||||
<div class="mt-2 flex flex-wrap items-center text-xs sm:text-sm text-slate-500 gap-2 sm:gap-4">
|
||||
<span>{{ exam.subject }}</span>
|
||||
<span>满分{{ exam.total_score }}分</span>
|
||||
<span>提交时间:{{ submission.submitted_at }}</span>
|
||||
<span class="break-all">提交时间:{{ submission.submitted_at }}</span>
|
||||
</div>
|
||||
<div class="mt-4 p-4 rounded-lg {% if score_hidden %}bg-blue-50 border border-blue-200{% elif submission.graded %}bg-green-50 border border-green-200{% else %}bg-yellow-50 border border-yellow-200{% endif %}">
|
||||
{% if score_hidden %}
|
||||
<div class="text-lg font-medium text-blue-700">成绩尚未公布</div>
|
||||
<div class="text-sm text-blue-600 mt-1">成绩将于 {{ exam.score_release_time.strftime('%Y年%m月%d日 %H:%M') }} 公布</div>
|
||||
<div class="text-base sm:text-lg font-medium text-blue-700">成绩尚未公布</div>
|
||||
<div class="text-xs sm:text-sm text-blue-600 mt-1 break-words">成绩将于 {{ exam.score_release_time.strftime('%Y年%m月%d日 %H:%M') }} 公布</div>
|
||||
{% elif submission.graded %}
|
||||
<div class="text-3xl font-bold text-green-700">{{ submission.score }} <span class="text-lg font-normal text-green-600">/ {{ exam.total_score }}</span></div>
|
||||
<div class="text-sm text-green-600 mt-1">批改人:{{ submission.graded_by }}</div>
|
||||
<div class="text-2xl sm:text-3xl font-bold text-green-700">{{ submission.score }} <span class="text-base sm:text-lg font-normal text-green-600">/ {{ exam.total_score }}</span></div>
|
||||
<div class="text-xs sm:text-sm text-green-600 mt-1">批改人:{{ submission.graded_by }}</div>
|
||||
{% else %}
|
||||
<div class="text-lg font-medium text-yellow-700">待批改</div>
|
||||
<div class="text-sm text-yellow-600">选择题已自动批改得分:{{ submission.score }}分,主观题等待老师批改</div>
|
||||
<div class="text-base sm:text-lg font-medium text-yellow-700">待批改</div>
|
||||
<div class="text-xs sm:text-sm text-yellow-600 break-words">选择题已自动批改得分:{{ submission.score }}分,主观题等待老师批改</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% for q in questions %}
|
||||
<div class="futuristic-card p-6">
|
||||
<div class="flex items-start space-x-4">
|
||||
<span class="flex-shrink-0 w-8 h-8 bg-gradient-to-br from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-white font-medium">{{ loop.index }}</span>
|
||||
<div class="flex-1 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<p class="text-lg text-slate-900">{{ q.content }}</p>
|
||||
<span class="text-sm text-slate-400 whitespace-nowrap ml-4">({{ q.score }}分)</span>
|
||||
<div class="futuristic-card p-4 sm:p-6">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<span class="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 bg-gradient-to-br from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-white font-medium text-sm">{{ loop.index }}</span>
|
||||
<div class="flex-1 space-y-3 min-w-0">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between gap-2">
|
||||
<p class="text-base sm:text-lg text-slate-900 break-words">{{ q.content }}</p>
|
||||
<span class="text-xs sm:text-sm text-slate-400 whitespace-nowrap">({{ q.score }}分)</span>
|
||||
</div>
|
||||
{% if q.get('images') %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for img in q.images %}
|
||||
<img src="{{ img }}" class="max-h-48 rounded border border-slate-200 cursor-pointer" onclick="window.open(this.src)" alt="题目图片">
|
||||
<img src="{{ img }}" class="max-h-32 sm:max-h-48 rounded border border-slate-200 cursor-pointer max-w-full" onclick="window.open(this.src)" alt="题目图片">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -57,7 +57,7 @@
|
||||
{% else %}
|
||||
<div class="w-5 h-5"></div>
|
||||
{% endif %}
|
||||
<span class="text-slate-700">{{ letter }}. {{ opt }}</span>
|
||||
<span class="text-slate-700 option-text">{{ letter }}. {{ opt|safe }}</span>
|
||||
{% if is_answer %}<span class="badge-futuristic text-xs ml-2">✓ 正确答案</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -86,3 +86,14 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 渲染页面中的数学公式
|
||||
if (typeof renderMathInElement === 'function') {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -459,8 +459,11 @@ function renderPosts(posts) {
|
||||
|
||||
<div class="hidden sm:flex flex-col items-center gap-3 flex-shrink-0" onclick="event.stopPropagation()">
|
||||
<!-- 游戏化头像与等级 -->
|
||||
<div class="relative w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-100 to-purple-100 flex items-center justify-center text-indigo-600 font-bold border-2 border-white shadow-md transform group-hover:rotate-6 transition-transform z-10">
|
||||
<span class="text-xl">${esc(p.author.charAt(0))}</span>
|
||||
<div class="relative w-14 h-14 rounded-2xl ${p.author_avatar ? '' : 'bg-gradient-to-br from-indigo-100 to-purple-100'} flex items-center justify-center text-indigo-600 font-bold border-2 border-white shadow-md transform group-hover:rotate-6 transition-transform z-10 overflow-hidden">
|
||||
${p.author_avatar ?
|
||||
`<img src="${p.author_avatar}" alt="${esc(p.author)}" class="w-full h-full object-cover">` :
|
||||
`<span class="text-xl">${esc(p.author.charAt(0))}</span>`
|
||||
}
|
||||
${p.is_official ?
|
||||
'<div class="absolute -bottom-2 -right-2 bg-gradient-to-r from-red-500 to-rose-600 text-white text-[9px] font-black px-1.5 py-0.5 rounded-md shadow-sm border border-white">官方</div>' :
|
||||
'<div class="absolute -bottom-1.5 -right-1.5 bg-slate-800 text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full shadow-sm border border-white">Lv.' + authorLevel + '</div>'}
|
||||
@@ -612,7 +615,13 @@ async function openPost(pid) {
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-slate-900">${esc(p.title)}</h2>
|
||||
<div class="mt-2 flex items-center text-sm text-slate-400 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
${p.author_avatar ?
|
||||
`<img src="${p.author_avatar}" alt="${esc(p.author)}" class="w-6 h-6 rounded-full object-cover border border-slate-200">` :
|
||||
`<div class="w-6 h-6 rounded-full bg-gradient-to-br from-indigo-100 to-purple-100 flex items-center justify-center text-indigo-600 text-xs font-bold border border-slate-200">${esc(p.author.charAt(0))}</div>`
|
||||
}
|
||||
<span class="cursor-pointer hover:text-primary" onclick="showProfile('${p.author_id}')">${esc(p.author)}</span>
|
||||
</div>
|
||||
<span>${p.created_at}</span><span>👁 ${p.views||0}</span><span>❤️ ${p.likes}</span><span>💬 ${replies.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -635,7 +644,12 @@ async function openPost(pid) {
|
||||
<h3 class="text-sm font-bold text-slate-700 mb-4">💬 回复 (${replies.length})</h3>`;
|
||||
|
||||
if (CU) {
|
||||
h += `<div class="flex gap-3 mb-6"><div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0">${esc(CU.name.charAt(0))}</div>
|
||||
const currentUserAvatar = CU.avatar || '';
|
||||
h += `<div class="flex gap-3 mb-6">
|
||||
${currentUserAvatar ?
|
||||
`<img src="${currentUserAvatar}" alt="${esc(CU.name)}" class="w-8 h-8 rounded-full object-cover border border-slate-200 flex-shrink-0">` :
|
||||
`<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0">${esc(CU.name.charAt(0))}</div>`
|
||||
}
|
||||
<div class="flex-1"><textarea id="reply-input" rows="2" class="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="写下你的回复..."></textarea>
|
||||
<div id="reply-preview" class="flex flex-wrap gap-2 mt-1 hidden"></div>
|
||||
<div class="flex justify-between items-center mt-2"><div class="flex gap-1"><button onclick="triggerUpload('reply-input')" class="p-1 rounded hover:bg-slate-200 text-sm" title="上传图片">📷</button><button onclick="triggerCamera('reply-input')" class="p-1 rounded hover:bg-slate-200 text-sm" title="拍照上传">📸</button></div><button onclick="submitReply(${p.id})" class="px-4 py-1.5 bg-primary text-white rounded-lg text-sm hover:bg-blue-700">回复</button></div></div></div>`;
|
||||
@@ -648,7 +662,10 @@ async function openPost(pid) {
|
||||
const canDel = CU && (CU.id === r.author_id || CU.role === 'teacher');
|
||||
const canEdit = CU && CU.id === r.author_id;
|
||||
h += `<div class="flex gap-3 py-3 ${i>0?'border-t border-slate-100':''}">
|
||||
<div class="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-xs font-bold flex-shrink-0">${esc(r.author.charAt(0))}</div>
|
||||
${r.author_avatar ?
|
||||
`<img src="${r.author_avatar}" alt="${esc(r.author)}" class="w-8 h-8 rounded-full object-cover border border-slate-200 flex-shrink-0">` :
|
||||
`<div class="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-xs font-bold flex-shrink-0">${esc(r.author.charAt(0))}</div>`
|
||||
}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
|
||||