mobile . themes

This commit is contained in:
2026-03-05 12:57:55 +08:00
parent b5b9f7db06
commit c325007897
62 changed files with 835 additions and 150 deletions

View File

@@ -27,7 +27,8 @@
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git push)", "Bash(git push)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git push:*)" "Bash(git push:*)",
"Bash(sqlite3:*)"
] ]
} }
} }

113
app.py
View File

@@ -13,7 +13,7 @@ from functools import wraps
from flask import ( from flask import (
Flask, render_template, request, jsonify, session, 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 flask_caching import Cache
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -27,6 +27,7 @@ from openai import OpenAI as DashScopeClient
# 引入数据库模型(添加 TeacherApplication # 引入数据库模型(添加 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 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 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() load_dotenv()
@@ -2761,9 +2762,18 @@ def api_update_exam_visibility(exam_id):
if new_visibility not in ('private', 'public'): if new_visibility not in ('private', 'public'):
return jsonify({'success': False, 'message': '无效的可见性设置'}), 400 return jsonify({'success': False, 'message': '无效的可见性设置'}), 400
# 检查权限:只有教师管理员可以设置为公开 # 检查权限:教师管理员、杯赛负责人和杯赛老师可以设置为公开
if new_visibility == 'public' and user.get('role') not in ('teacher', 'admin'): if new_visibility == 'public':
return jsonify({'success': False, 'message': '只有教师和管理员可以创建公开考试'}), 403 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 exam.visibility = new_visibility
db.session.commit() db.session.commit()
@@ -3303,6 +3313,7 @@ def api_search_posts():
'content': p.content[:200] + ('...' if len(p.content) > 200 else ''), 'content': p.content[:200] + ('...' if len(p.content) > 200 else ''),
'author': p.author.name, 'author': p.author.name,
'author_id': p.author_id, 'author_id': p.author_id,
'author_avatar': p.author.avatar or '',
'author_level': author_level, 'author_level': author_level,
'tag': p.tag, 'tag': p.tag,
'is_official': p.is_official, 'is_official': p.is_official,
@@ -3360,6 +3371,7 @@ def api_get_post(post_id):
'content': post.content, 'content': post.content,
'author': post.author.name, 'author': post.author.name,
'author_id': post.author_id, 'author_id': post.author_id,
'author_avatar': post.author.avatar or '',
'tag': post.tag, 'tag': post.tag,
'is_official': post.is_official, 'is_official': post.is_official,
'pinned': post.pinned, 'pinned': post.pinned,
@@ -3380,6 +3392,7 @@ def api_get_post(post_id):
'content': r.content, 'content': r.content,
'author': r.author.name, 'author': r.author.name,
'author_id': r.author_id, 'author_id': r.author_id,
'author_avatar': r.author.avatar or '',
'likes': r.likes, 'likes': r.likes,
'reply_to': r.reply_to, 'reply_to': r.reply_to,
'created_at': r.created_at.strftime('%Y-%m-%d %H:%M'), 'created_at': r.created_at.strftime('%Y-%m-%d %H:%M'),
@@ -4895,5 +4908,97 @@ with app.app_context():
conn.commit() conn.commit()
except Exception: except Exception:
pass 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__': if __name__ == '__main__':
socketio.run(app, debug=True,host='0.0.0.0', port=5080) socketio.run(app, debug=True,host='0.0.0.0', port=5080)

55
check_database.py Normal file
View 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
View 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

Binary file not shown.

View File

@@ -7,3 +7,5 @@ flask-socketio>=5.3.0
PyMuPDF>=1.24.0 PyMuPDF>=1.24.0
openai>=1.14.0 openai>=1.14.0
Flask-Caching>=2.3.0 Flask-Caching>=2.3.0
openpyxl>=3.1.0
Flask-SQLAlchemy>=3.0.0

View File

@@ -463,7 +463,85 @@ button[onclick^="quickScore"]:active {
color: rgba(255, 255, 255, 0.8); 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 { .particles-container {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -473,3 +551,173 @@ button[onclick^="quickScore"]:active {
z-index: -1; z-index: -1;
pointer-events: none; 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));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -51,10 +51,12 @@
<!-- 主体内容区 --> <!-- 主体内容区 -->
<main class="flex-1 min-w-0"> <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="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 %} {% block admin_content %}{% endblock %}
</div> </div>
</div>
</main> </main>
</div> </div>

View File

@@ -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> <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="futuristic-card-dark overflow-hidden">
<div class="overflow-x-auto"> <div class="table-responsive">
<table class="table-futuristic"> <table class="table-futuristic" style="min-width: 900px;">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th style="min-width: 60px;">ID</th>
<th>名称</th> <th style="min-width: 200px;">名称</th>
<th>主办方</th> <th style="min-width: 150px;">主办方</th>
<th>状态</th> <th style="min-width: 120px;">状态</th>
<th>开始日期</th> <th style="min-width: 150px;">开始日期</th>
<th>操作</th> <th style="min-width: 180px;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="contests-tbody"> <tbody id="contests-tbody">
@@ -56,8 +56,8 @@ async function loadContests() {
: '<span class="text-xs text-red-500">已废止</span>'; : '<span class="text-xs text-red-500">已废止</span>';
html += `<tr> html += `<tr>
<td>${c.id}</td> <td>${c.id}</td>
<td class="font-medium text-slate-200">${c.name}</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">${c.organizer || '-'}</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><span class="badge-futuristic ${statusClass}">${statusText}</span></td>
<td class="text-slate-400">${c.start_date || '-'}</td> <td class="text-slate-400">${c.start_date || '-'}</td>
<td class="space-x-2"> <td class="space-x-2">

View File

@@ -15,16 +15,16 @@
</div> </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="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="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="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"> <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> <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>
<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>
<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>
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform"> <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"> <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> <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>
<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>
<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>
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform"> <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"> <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> <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>
<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>
<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>
<div class="futuristic-card-dark relative overflow-hidden group hover:-translate-y-1 transition-transform"> <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"> <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> <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>
<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>
<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>
</div> </div>
@@ -105,34 +105,34 @@
</div> </div>
<h2 class="text-lg font-bold bg-gradient-to-r from-teal-400 to-emerald-400 bg-clip-text text-transparent">快捷操作</h2> <h2 class="text-lg font-bold bg-gradient-to-r from-teal-400 to-emerald-400 bg-clip-text text-transparent">快捷操作</h2>
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4"> <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-5"> <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-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="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-sm font-bold text-slate-300">杯赛管理</div> <div class="text-xs sm:text-sm font-bold text-slate-300">杯赛管理</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">杯赛申请</div> <div class="text-xs sm:text-sm font-bold text-slate-300">杯赛申请</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">教师申请</div> <div class="text-xs sm:text-sm font-bold text-slate-300">教师申请</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">考试管理</div> <div class="text-xs sm:text-sm font-bold text-slate-300">考试管理</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">用户管理</div> <div class="text-xs sm:text-sm font-bold text-slate-300">用户管理</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">帖子管理</div> <div class="text-xs sm:text-sm font-bold text-slate-300">帖子管理</div>
</a> </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"> <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-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="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-sm font-bold text-slate-300">通知管理</div> <div class="text-xs sm:text-sm font-bold text-slate-300">通知管理</div>
</a> </a>
</div> </div>
</div> </div>

View File

@@ -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> <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="futuristic-card-dark overflow-hidden">
<div class="overflow-x-auto"> <div class="table-responsive">
<table class="table-futuristic"> <table class="table-futuristic" style="min-width: 1000px;">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th style="min-width: 60px;">ID</th>
<th>标题</th> <th style="min-width: 200px;">标题</th>
<th>科目</th> <th style="min-width: 100px;">科目</th>
<th>出题人</th> <th style="min-width: 120px;">出题人</th>
<th>状态</th> <th style="min-width: 100px;">状态</th>
<th>创建时间</th> <th style="min-width: 150px;">创建时间</th>
<th>操作</th> <th style="min-width: 180px;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="exams-tbody"></tbody> <tbody id="exams-tbody"></tbody>
@@ -68,7 +68,7 @@ async function loadExams() {
const isAvailable = e.status === 'available'; const isAvailable = e.status === 'available';
html += `<tr> html += `<tr>
<td>${e.id}</td> <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.subject || '-'}</td>
<td class="text-slate-400">${e.creator_name || '-'}</td> <td class="text-slate-400">${e.creator_name || '-'}</td>
<td><span class="badge-futuristic ${statusClass}">${statusText}</span></td> <td><span class="badge-futuristic ${statusClass}">${statusText}</span></td>

View File

@@ -19,17 +19,17 @@
</div> </div>
<div class="futuristic-card-dark overflow-hidden"> <div class="futuristic-card-dark overflow-hidden">
<div class="overflow-x-auto"> <div class="table-responsive">
<table class="table-futuristic"> <table class="table-futuristic" style="min-width: 1000px;">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th style="min-width: 60px;">ID</th>
<th>标题</th> <th style="min-width: 250px;">标题</th>
<th>作者</th> <th style="min-width: 120px;">作者</th>
<th>标签</th> <th style="min-width: 100px;">标签</th>
<th>置顶</th> <th style="min-width: 100px;">置顶</th>
<th>发布时间</th> <th style="min-width: 150px;">发布时间</th>
<th>操作</th> <th style="min-width: 200px;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="posts-tbody"></tbody> <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'; const tagClass = tagClassMap[p.tag] || 'bg-slate-500/20 text-slate-400 border-slate-500/30';
html += `<tr> html += `<tr>
<td>${p.id}</td> <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 class="text-slate-400">${p.author}</td>
<td><span class="badge-futuristic ${tagClass}">${p.tag || '-'}</span></td> <td><span class="badge-futuristic ${tagClass}">${p.tag || '-'}</span></td>
<td> <td>

View File

@@ -31,18 +31,18 @@
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
<div class="hidden md:block bg-white shadow-sm rounded-lg border border-slate-200 overflow-hidden"> <div class="hidden md:block bg-white shadow-sm rounded-lg border border-slate-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="table-responsive">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200" style="min-width: 1100px;">
<thead class="bg-slate-50"> <thead class="bg-slate-50">
<tr> <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" style="min-width: 60px;">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" style="min-width: 100px;">申请人</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: 150px;">杯赛</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: 100px;">姓名</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: 150px;">邮箱</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: 200px;">申请理由</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: 150px;">申请时间</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: 150px;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="applications-table-body" class="bg-white divide-y divide-slate-200"> <tbody id="applications-table-body" class="bg-white divide-y divide-slate-200">
@@ -50,10 +50,10 @@
<tr data-id="{{ app.id }}"> <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.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-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.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" style="max-width: 150px; word-wrap: break-word;">{{ 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: 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 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"> <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;"> <form action="{{ url_for('approve_teacher_application', app_id=app.id) }}" method="post" style="display:inline;">

View File

@@ -18,17 +18,17 @@
</div> </div>
<div class="futuristic-card-dark overflow-hidden"> <div class="futuristic-card-dark overflow-hidden">
<div class="overflow-x-auto"> <div class="table-responsive">
<table class="table-futuristic"> <table class="table-futuristic" style="min-width: 900px;">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th style="min-width: 60px;">ID</th>
<th>用户名</th> <th style="min-width: 120px;">用户名</th>
<th>邮箱</th> <th style="min-width: 180px;">邮箱</th>
<th>角色</th> <th style="min-width: 100px;">角色</th>
<th>状态</th> <th style="min-width: 100px;">状态</th>
<th>注册时间</th> <th style="min-width: 150px;">注册时间</th>
<th>操作</th> <th style="min-width: 180px;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="users-tbody"></tbody> <tbody id="users-tbody"></tbody>

View File

@@ -80,9 +80,9 @@
</div> </div>
</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"> <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="返回"> <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> <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> </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="flex items-center gap-2 overflow-hidden">
<div class="w-1 h-4 bg-cyan-400 rounded-full"></div> <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> <div class="text-sm font-medium text-cyan-300 truncate"><span id="replyToText"></span></div>
@@ -126,7 +126,7 @@
</button> </button>
</div> </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 面板 --> <!-- 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 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> <div class="flex flex-wrap gap-2 justify-center" id="emojiGrid"></div>
@@ -162,7 +162,7 @@
</div> </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"> <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> </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"> <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) { if (!beforeId) {
area.innerHTML = ''; area.innerHTML = '';
data.messages.forEach(m => area.appendChild(createMsgEl(m))); data.messages.forEach(m => area.appendChild(createMsgEl(m)));
// 确保滚动到底部
setTimeout(() => {
area.scrollTop = area.scrollHeight; area.scrollTop = area.scrollHeight;
}, 100);
} else { } else {
const oldH = area.scrollHeight; const oldH = area.scrollHeight;
data.messages.forEach((m, i) => area.insertBefore(createMsgEl(m), area.children[i])); data.messages.forEach((m, i) => area.insertBefore(createMsgEl(m), area.children[i]));
@@ -813,6 +816,7 @@ function sendMessage() {
reply_to_id: replyToId, mentions: mentionsStr reply_to_id: replyToId, mentions: mentionsStr
}); });
input.value = ''; input.value = '';
input.style.height = 'auto';
mentionMembers = []; mentionMembers = [];
cancelReply(); cancelReply();
document.getElementById('emojiPanel').classList.add('hidden'); document.getElementById('emojiPanel').classList.add('hidden');
@@ -1337,8 +1341,14 @@ async function stopRecording() {
socket.on('new_message', (msg) => { socket.on('new_message', (msg) => {
if (msg.room_id === currentRoomId) { if (msg.room_id === currentRoomId) {
const area = document.getElementById('messageArea'); const area = document.getElementById('messageArea');
const wasAtBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 100;
area.appendChild(createMsgEl(msg)); area.appendChild(createMsgEl(msg));
// 如果用户在底部或者是自己发的消息,自动滚动到底部
if (wasAtBottom || msg.sender_id === currentUser.id) {
setTimeout(() => {
area.scrollTop = area.scrollHeight; area.scrollTop = area.scrollHeight;
}, 50);
}
markRead(currentRoomId); markRead(currentRoomId);
} }
// 更新会话列表 // 更新会话列表

View File

@@ -15,35 +15,35 @@
</div> </div>
{% endif %} {% endif %}
<div class="futuristic-card p-6 p-6"> <div class="futuristic-card p-6 p-6">
<div class="flex justify-between items-start"> <div class="flex flex-col lg:flex-row justify-between items-start gap-4">
<div> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-2"> <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 }} {{ contest.name }}
{% if contest.status == 'abolished' %} {% if contest.status == 'abolished' %}
<span class="ml-2 badge-futuristic text-xs rounded-full">已废止</span> <span class="ml-2 badge-futuristic text-xs rounded-full">已废止</span>
{% endif %} {% endif %}
</h1> </h1>
<div class="flex items-center space-x-4 text-sm text-slate-400"> <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"> <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> <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 }} {{ contest.start_date }}
</span> </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> <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 id="participants-value">{{ contest.participants }}</span>人已报名
</span> </span>
</div> </div>
</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 contest.status != 'abolished' %}
{% if user %} {% if user %}
<button id="register-btn" <button id="register-btn"
onclick="toggleRegistration({{ contest.id }})" 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 %} {% if registered %}已报名{% else %}立即报名{% endif %}
</button> </button>
{% if not is_member %} {% 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> </a>
{% endif %} {% endif %}

View File

@@ -149,7 +149,7 @@ async function loadQuestions() {
<p class="text-sm text-slate-800 mb-2">${escapeHtml(q.content)}</p>`; <p class="text-sm text-slate-800 mb-2">${escapeHtml(q.content)}</p>`;
if (q.type === 'choice' && q.options && q.options.length) { if (q.type === 'choice' && q.options && q.options.length) {
html += '<div class="text-sm text-slate-600 space-y-1 ml-4">'; 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>'; html += '</div>';
} }
if (q.answer) { if (q.answer) {
@@ -162,6 +162,13 @@ async function loadQuestions() {
html += `</div></div>`; html += `</div></div>`;
}); });
container.innerHTML = html; container.innerHTML = html;
// 渲染数学公式
if (typeof renderMathInElement === 'function') {
renderMathInElement(container, {
delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],
throwOnError: false
});
}
} catch(e) { } catch(e) {
document.getElementById('question-list').innerHTML = '<div class="text-center py-8 text-red-500">加载失败</div>'; document.getElementById('question-list').innerHTML = '<div class="text-center py-8 text-red-500">加载失败</div>';
} }

View File

@@ -86,26 +86,31 @@
</div> </div>
{% else %} {% else %}
<!-- 顶部信息栏 --> <!-- 顶部信息栏 -->
<div class="futuristic-card p-4 sticky top-0 z-20"> <div class="futuristic-card p-4 sticky top-0 z-20 shadow-lg">
<div class="flex justify-between items-center"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<div> <div class="flex-1 min-w-0">
<h1 class="text-lg font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">{{ exam.title }}</h1> <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-sm text-slate-500"> <div class="mt-1 text-xs sm:text-sm text-slate-500 flex flex-wrap gap-2">
{{ exam.subject }} · {{ exam.duration }}分钟 · 满分{{ exam.total_score }}分 <span>{{ exam.subject }}</span>
<span>·</span>
<span>{{ exam.duration }}分钟</span>
<span>·</span>
<span>满分{{ exam.total_score }}分</span>
{% if exam.scheduled_end %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center gap-3 sm:gap-4 flex-wrap">
<div class="text-sm text-slate-500"> <div class="text-xs sm:text-sm text-slate-500 whitespace-nowrap">
<span id="progress-text">0</span>/{{ questions|length }} 已答 <span id="progress-text">0</span>/{{ questions|length }} 已答
</div> </div>
<div class="flex items-center text-red-600 font-medium"> <div class="flex items-center text-red-600 font-medium text-sm sm:text-base whitespace-nowrap">
<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> <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> <span id="timer">--:--:--</span>
</div> </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> 切屏 <span id="tab-count">0</span>
</div> </div>
</div> </div>
@@ -180,7 +185,7 @@
{% for opt in q.options %} {% 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 }}"> <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 }})"> <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> </label>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -60,7 +60,7 @@
{% if is_answer %}border-green-300 bg-green-50 {% if is_answer %}border-green-300 bg-green-50
{% elif is_selected and not is_answer %}border-red-300 bg-red-50 {% elif is_selected and not is_answer %}border-red-300 bg-red-50
{% else %}border-slate-100{% endif %}"> {% 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_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 %} {% if is_answer %}<span class="text-xs text-green-600 font-medium">✓ 正确</span>{% endif %}
</div> </div>

View File

@@ -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 { 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; } .print-btn:hover { background: #2563eb; }
</style> </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> </head>
<body> <body>
<button class="print-btn no-print" onclick="window.print()">打印 / 导出PDF</button> <button class="print-btn no-print" onclick="window.print()">打印 / 导出PDF</button>
@@ -64,7 +69,7 @@
{% if q.options %} {% if q.options %}
<div class="options"> <div class="options">
{% for opt in q.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 %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -2,43 +2,43 @@
{% block title %}考试结果 - 智联青云{% endblock %} {% block title %}考试结果 - 智联青云{% endblock %}
{% block content %} {% block content %}
<div class="max-w-4xl mx-auto space-y-6"> <div class="max-w-4xl mx-auto space-y-6">
<div class="flex justify-between items-center"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<h1 class="text-2xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">考试结果</h1> <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> <a href="/exams" class="text-sm text-slate-500 hover:text-slate-700">← 返回列表</a>
</div> </div>
<div class="futuristic-card p-6"> <div class="futuristic-card p-4 sm: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> <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 items-center text-sm text-slate-500 space-x-4"> <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.subject }}</span>
<span>满分{{ exam.total_score }}分</span> <span>满分{{ exam.total_score }}分</span>
<span>提交时间:{{ submission.submitted_at }}</span> <span class="break-all">提交时间:{{ submission.submitted_at }}</span>
</div> </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 %}"> <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 %} {% if score_hidden %}
<div class="text-lg font-medium text-blue-700">成绩尚未公布</div> <div class="text-base sm: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-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 %} {% 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-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-sm text-green-600 mt-1">批改人:{{ submission.graded_by }}</div> <div class="text-xs sm:text-sm text-green-600 mt-1">批改人:{{ submission.graded_by }}</div>
{% else %} {% else %}
<div class="text-lg font-medium text-yellow-700">待批改</div> <div class="text-base sm:text-lg font-medium text-yellow-700">待批改</div>
<div class="text-sm text-yellow-600">选择题已自动批改得分:{{ submission.score }}分,主观题等待老师批改</div> <div class="text-xs sm:text-sm text-yellow-600 break-words">选择题已自动批改得分:{{ submission.score }}分,主观题等待老师批改</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% for q in questions %} {% for q in questions %}
<div class="futuristic-card p-6"> <div class="futuristic-card p-4 sm:p-6">
<div class="flex items-start space-x-4"> <div class="flex items-start space-x-3 sm: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> <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"> <div class="flex-1 space-y-3 min-w-0">
<div class="flex justify-between"> <div class="flex flex-col sm:flex-row sm:justify-between gap-2">
<p class="text-lg text-slate-900">{{ q.content }}</p> <p class="text-base sm:text-lg text-slate-900 break-words">{{ q.content }}</p>
<span class="text-sm text-slate-400 whitespace-nowrap ml-4">{{ q.score }}分)</span> <span class="text-xs sm:text-sm text-slate-400 whitespace-nowrap">{{ q.score }}分)</span>
</div> </div>
{% if q.get('images') %} {% if q.get('images') %}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{% for img in q.images %} {% 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 %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@@ -57,7 +57,7 @@
{% else %} {% else %}
<div class="w-5 h-5"></div> <div class="w-5 h-5"></div>
{% endif %} {% 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 %} {% if is_answer %}<span class="badge-futuristic text-xs ml-2">✓ 正确答案</span>{% endif %}
</div> </div>
{% endfor %} {% endfor %}
@@ -86,3 +86,14 @@
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
// 渲染页面中的数学公式
if (typeof renderMathInElement === 'function') {
renderMathInElement(document.body, {
delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],
throwOnError: false
});
}
</script>
{% endblock %}

View File

@@ -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="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"> <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">
<span class="text-xl">${esc(p.author.charAt(0))}</span> ${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 ? ${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-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>'} '<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> </div>
<h2 class="text-xl font-bold text-slate-900">${esc(p.title)}</h2> <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="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> <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> <span>${p.created_at}</span><span>👁 ${p.views||0}</span><span>❤️ ${p.likes}</span><span>💬 ${replies.length}</span>
</div> </div>
</div> </div>
@@ -635,7 +644,12 @@ async function openPost(pid) {
<h3 class="text-sm font-bold text-slate-700 mb-4">💬 回复 (${replies.length})</h3>`; <h3 class="text-sm font-bold text-slate-700 mb-4">💬 回复 (${replies.length})</h3>`;
if (CU) { 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 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 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>`; <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 canDel = CU && (CU.id === r.author_id || CU.role === 'teacher');
const canEdit = CU && CU.id === r.author_id; const canEdit = CU && CU.id === r.author_id;
h += `<div class="flex gap-3 py-3 ${i>0?'border-t border-slate-100':''}"> 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-1 min-w-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">