mobile app database models admin_contests admin_dashboard admin_exams admin_posts admin_teacher_applications admin_users apply_contest apply_teacher base chat contest_detail contest_list exam_result forum notifications profile themes

This commit is contained in:
2026-03-03 16:02:23 +08:00
parent 0764566742
commit 9a08140623
30 changed files with 1531 additions and 834 deletions

View File

@@ -3,27 +3,27 @@
{% block content %}
<div class="space-y-8">
{% if contest.status == 'abolished' %}
<div class="bg-red-50 border border-red-300 rounded-lg p-4 text-red-700 font-medium">
<div class="futuristic-card p-6 border-l-4 border-red-500 p-4 text-red-300">
⚠️ 该杯赛已被废止,所有考试已关闭,无法报名或参加考试。
</div>
{% endif %}
{% if not contest.visible and is_owner %}
<div class="bg-yellow-50 border border-yellow-300 rounded-lg p-4 flex justify-between items-center">
<span class="text-yellow-800 font-medium">该杯赛尚未发布,仅负责人和管理员可见。完善资料后请点击发布。</span>
<button onclick="publishContest()" id="publish-btn" class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-medium hover:bg-green-700">发布杯赛</button>
<div class="futuristic-card p-6 border-l-4 border-yellow-500 p-4 flex justify-between items-center">
<span class="text-yellow-300 font-medium">该杯赛尚未发布,仅负责人和管理员可见。完善资料后请点击发布。</span>
<button onclick="publishContest()" id="publish-btn" class="btn-futuristic px-4 py-2 text-sm font-medium">发布杯赛</button>
</div>
{% endif %}
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<div class="futuristic-card p-6 p-6">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold text-slate-900 mb-2">
<h1 class="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-2">
{{ contest.name }}
{% if contest.status == 'abolished' %}
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-800">已废止</span>
<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-500">
<div class="flex items-center space-x-4 text-sm text-slate-400">
<span class="flex items-center">
<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 }}
@@ -39,36 +39,36 @@
{% if user %}
<button id="register-btn"
onclick="toggleRegistration({{ contest.id }})"
class="px-6 py-2 {% if registered %}bg-slate-100 text-slate-700 border border-slate-300 hover:bg-slate-200{% else %}bg-primary text-white hover:bg-blue-700{% endif %} rounded-md font-medium">
class="{% if registered %}btn-outline-futuristic{% else %}btn-futuristic{% endif %} px-6 py-2 font-medium">
{% if registered %}已报名{% else %}立即报名{% endif %}
</button>
{% if not is_member %}
<a href="{{ url_for('apply_teacher', contest_id=contest.id) }}" class="px-6 py-2 bg-green-100 text-green-700 border border-green-300 rounded-md font-medium hover:bg-green-200">
<a href="{{ url_for('apply_teacher', contest_id=contest.id) }}" class="btn-outline-futuristic px-6 py-2 font-medium">
申请成为本杯赛老师
</a>
{% endif %}
{% if is_member %}
<a href="{{ url_for('contest_question_bank', contest_id=contest.id) }}" class="px-6 py-2 bg-purple-100 text-purple-700 border border-purple-300 rounded-md font-medium hover:bg-purple-200">
<a href="{{ url_for('contest_question_bank', contest_id=contest.id) }}" class="btn-outline-futuristic px-6 py-2 font-medium">
题库管理
</a>
{% endif %}
{% if is_owner %}
<a href="{{ url_for('exam_create', contest_id=contest.id) }}" class="px-6 py-2 bg-blue-100 text-blue-700 border border-blue-300 rounded-md font-medium hover:bg-blue-200">
<a href="{{ url_for('exam_create', contest_id=contest.id) }}" class="btn-outline-futuristic px-6 py-2 font-medium">
创建考试
</a>
<a href="{{ url_for('admin_teacher_applications') }}" class="px-6 py-2 bg-orange-100 text-orange-700 border border-orange-300 rounded-md font-medium hover:bg-orange-200">
<a href="{{ url_for('admin_teacher_applications') }}" class="btn-outline-futuristic px-6 py-2 font-medium">
审批老师申请
</a>
<a href="{{ url_for('contest_edit', contest_id=contest.id) }}#papers" class="px-6 py-2 bg-green-100 text-green-700 border border-green-300 rounded-md font-medium hover:bg-green-200">
<a href="{{ url_for('contest_edit', contest_id=contest.id) }}#papers" class="btn-outline-futuristic px-6 py-2 font-medium">
上传历年真题
</a>
<a href="{{ url_for('contest_edit', contest_id=contest.id) }}" class="px-6 py-2 bg-yellow-100 text-yellow-700 border border-yellow-300 rounded-md font-medium hover:bg-yellow-200">
<a href="{{ url_for('contest_edit', contest_id=contest.id) }}" class="btn-outline-futuristic px-6 py-2 font-medium">
编辑主页
</a>
{% endif %}
{% else %}
<a href="/login?next={{ url_for('contest_detail', contest_id=contest.id) }}"
class="px-6 py-2 bg-primary text-white rounded-md hover:bg-blue-700 font-medium">
class="btn-futuristic px-6 py-2 font-medium">
登录后报名
</a>
{% endif %}
@@ -84,9 +84,9 @@
<!-- 比赛详情 -->
<!-- 历年真题 -->
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<h2 class="text-lg font-semibold text-slate-900 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="futuristic-card p-6 p-6">
<h2 class="text-lg font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
历年真题
@@ -95,12 +95,12 @@
{% if papers %}
<div class="space-y-3">
{% for paper in papers %}
<div class="flex items-center justify-between py-2 border-b border-slate-100 last:border-0">
<div class="flex items-center justify-between py-2 border-b border-white/10 last:border-0">
<div class="flex items-center">
<span class="text-sm font-medium text-slate-700 w-16">{{ paper.year }}</span>
<span class="text-sm text-slate-600">{{ paper.title }}</span>
<span class="badge-futuristic text-sm font-medium w-16">{{ paper.year }}</span>
<span class="text-sm text-slate-300 ml-3">{{ paper.title }}</span>
</div>
<a href="{{ paper.file }}" target="_blank" class="inline-flex items-center px-3 py-1 text-xs font-medium text-primary border border-primary rounded hover:bg-blue-50">
<a href="{{ paper.file }}" target="_blank" class="btn-outline-futuristic inline-flex items-center px-3 py-1 text-xs font-medium">
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
@@ -110,19 +110,19 @@
{% endfor %}
</div>
{% else %}
<div class="text-center py-8 text-slate-500">暂无历年真题,敬请期待!</div>
<div class="text-center py-8 text-slate-400">暂无历年真题,敬请期待!</div>
{% endif %}
</div>
<!-- 考试列表 -->
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<div class="futuristic-card p-6 p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-slate-900 flex items-center">
<svg class="w-5 h-5 mr-2 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
<h2 class="text-lg font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent flex items-center">
<svg class="w-5 h-5 mr-2 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
考试列表
</h2>
{% if is_owner %}
<button onclick="showImportModal()" class="px-3 py-1.5 bg-green-600 text-white rounded-md text-sm hover:bg-green-700">导入考试</button>
<button onclick="showImportModal()" class="btn-futuristic px-3 py-1.5 text-sm">导入考试</button>
{% endif %}
</div>
<div id="exam-list" class="space-y-3">
@@ -133,48 +133,48 @@
<!-- 右侧一列(主办方信息) -->
<div class="space-y-6">
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<h3 class="text-lg font-semibold text-slate-900 mb-4">主办方信息</h3>
<div class="futuristic-card p-6">
<h3 class="text-lg font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-4">主办方信息</h3>
<div class="space-y-4">
<div>
<div class="font-medium text-slate-900">{{ contest.organizer }}</div>
<p class="text-sm text-slate-500 mt-1">{{ contest.description[:100] + '...' if contest.description|length > 100 else contest.description }}</p>
<div class="font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">{{ contest.organizer }}</div>
<p class="text-sm text-slate-400 mt-1">{{ contest.description[:100] + '...' if contest.description|length > 100 else contest.description }}</p>
</div>
{% if contest.responsible_person %}
<div class="pt-4 border-t border-slate-100">
<div class="text-sm text-slate-500 mb-1">报备信息</div>
<div class="pt-4 border-t border-white/10">
<div class="text-sm text-slate-400 mb-1">报备信息</div>
<div class="space-y-2">
<div class="flex items-center text-sm">
<span class="text-slate-500 w-16 flex-shrink-0">责任人</span>
<span class="font-medium text-slate-900">{{ contest.responsible_person }}</span>
<span class="text-slate-400 w-16 flex-shrink-0">责任人</span>
<span class="font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">{{ contest.responsible_person }}</span>
</div>
<div class="flex items-center text-sm">
<span class="text-slate-500 w-16 flex-shrink-0">电话</span>
<span class="font-medium text-slate-900">{{ contest.responsible_phone }}</span>
<span class="text-slate-400 w-16 flex-shrink-0">电话</span>
<span class="font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">{{ contest.responsible_phone }}</span>
</div>
<div class="flex items-center text-sm">
<span class="text-slate-500 w-16 flex-shrink-0">邮箱</span>
<span class="font-medium text-primary">{{ contest.responsible_email }}</span>
<span class="text-slate-400 w-16 flex-shrink-0">邮箱</span>
<span class="font-medium text-cyan-400">{{ contest.responsible_email }}</span>
</div>
<div class="flex items-center text-sm">
<span class="text-slate-500 w-16 flex-shrink-0">机构</span>
<span class="font-medium text-slate-900">{{ contest.organization }}</span>
<span class="text-slate-400 w-16 flex-shrink-0">机构</span>
<span class="font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">{{ contest.organization }}</span>
</div>
</div>
</div>
{% endif %}
{% if contest.contact %}
<div class="pt-4 border-t border-slate-100">
<div class="text-sm text-slate-500">联系方式</div>
<div class="text-sm font-medium text-primary">{{ contest.contact }}</div>
<div class="pt-4 border-t border-white/10">
<div class="text-sm text-slate-400">联系方式</div>
<div class="text-sm font-medium text-cyan-400">{{ contest.contact }}</div>
</div>
{% endif %}
</div>
</div>
<!-- 排行榜 -->
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<h3 class="text-lg font-semibold text-slate-900 mb-4 flex items-center">
<div class="futuristic-card p-6">
<h3 class="text-lg font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/></svg>
成绩排行榜
</h3>
@@ -186,9 +186,9 @@
</div>
<!-- 讨论区(动态区域) -->
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<h2 class="text-lg font-semibold text-slate-900 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
<div class="futuristic-card p-6">
<h2 class="text-lg font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
讨论区
</h2>
@@ -211,7 +211,7 @@
<!-- 帖子列表容器 -->
<div id="post-list" class="space-y-4">
<div class="text-center py-8 text-slate-500">加载中...</div>
<div class="text-center py-8 text-slate-400">加载中...</div>
</div>
</div>
</div>
@@ -222,9 +222,9 @@
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[70vh] overflow-y-auto p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">导入已有考试</h3>
<button onclick="hideImportModal()" class="text-slate-400 hover:text-slate-600 text-xl">&times;</button>
<button onclick="hideImportModal()" class="text-slate-400 hover:text-slate-300 text-xl">&times;</button>
</div>
<p class="text-sm text-slate-500 mb-4">选择您创建的未关联杯赛的考试,导入到当前杯赛。</p>
<p class="text-sm text-slate-400 mb-4">选择您创建的未关联杯赛的考试,导入到当前杯赛。</p>
<div id="available-exams-list" class="space-y-2">
<div class="text-center py-4 text-slate-400 text-sm">加载中...</div>
</div>
@@ -276,15 +276,15 @@ async function loadPosts() {
const data = await res.json();
if (!data.success) throw new Error(data.message);
if (data.data.length === 0) {
container.innerHTML = '<div class="text-center py-8 text-slate-500">暂无讨论,来抢沙发吧!</div>';
container.innerHTML = '<div class="text-center py-8 text-slate-400">暂无讨论,来抢沙发吧!</div>';
return;
}
let html = '';
data.data.forEach(p => {
html += `
<div class="border border-slate-200 rounded-lg p-4 hover:shadow-sm transition-shadow">
<h3 class="text-base font-semibold text-slate-900 mb-1">${escapeHtml(p.title)}</h3>
<p class="text-sm text-slate-600 mb-2">${escapeHtml(p.content)}</p>
<h3 class="text-base font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-1">${escapeHtml(p.title)}</h3>
<p class="text-sm text-slate-300 mb-2">${escapeHtml(p.content)}</p>
<div class="flex items-center text-xs text-slate-400 space-x-3">
<span>${escapeHtml(p.author)}</span>
<span>${p.created_at}</span>
@@ -381,14 +381,14 @@ async function loadExams() {
html += `<div class="flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<a href="/exams/${e.id}" class="text-sm font-medium text-slate-900 hover:text-primary truncate">${escapeHtml(e.title)}</a>
<a href="/exams/${e.id}" class="text-sm font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent hover:text-cyan-400 truncate">${escapeHtml(e.title)}</a>
<span class="px-2 py-0.5 text-xs rounded-full ${statusClass}">${statusText}</span>
${subCount}
</div>
<div class="text-xs text-slate-500 mt-1">${e.subject ? e.subject + ' · ' : ''}满分${e.total_score}${e.duration ? ' · ' + e.duration + '分钟' : ''}</div>
<div class="text-xs text-slate-400 mt-1">${e.subject ? e.subject + ' · ' : ''}满分${e.total_score}${e.duration ? ' · ' + e.duration + '分钟' : ''}</div>
</div>
<div class="flex items-center ml-3 shrink-0">
<a href="/exams/${e.id}" class="px-3 py-1 text-xs font-medium text-primary border border-primary rounded hover:bg-blue-50">进入考试</a>
<a href="/exams/${e.id}" class="px-3 py-1 text-xs font-medium text-cyan-400 border border-primary rounded hover:bg-blue-50">进入考试</a>
${removeBtn}
</div>
</div>`;
@@ -436,8 +436,8 @@ async function loadAvailableExams() {
data.exams.forEach(e => {
html += `<div class="flex items-center justify-between p-3 border border-slate-200 rounded-lg">
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-slate-900">${escapeHtml(e.title)}</div>
<div class="text-xs text-slate-500">${e.subject ? e.subject + ' · ' : ''}满分${e.total_score}分 · ${e.created_at}</div>
<div class="text-sm font-medium bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">${escapeHtml(e.title)}</div>
<div class="text-xs text-slate-400">${e.subject ? e.subject + ' · ' : ''}满分${e.total_score}分 · ${e.created_at}</div>
</div>
<button onclick="importExam(${e.id})" class="ml-3 px-3 py-1 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700 shrink-0">导入</button>
</div>`;
@@ -475,12 +475,12 @@ async function loadLeaderboard() {
}
let html = '';
data.leaderboard.forEach(item => {
const rankClass = item.rank <= 3 ? 'font-bold text-yellow-600' : 'text-slate-500';
const rankClass = item.rank <= 3 ? 'font-bold text-yellow-600' : 'text-slate-400';
const medal = item.rank === 1 ? '🥇' : (item.rank === 2 ? '🥈' : (item.rank === 3 ? '🥉' : item.rank));
html += `<div class="flex items-center justify-between py-2 ${item.rank <= 3 ? '' : 'border-t border-slate-100'}">
html += `<div class="flex items-center justify-between py-2 ${item.rank <= 3 ? '' : 'border-t border-white/10'}">
<div class="flex items-center gap-2">
<span class="w-8 text-center ${rankClass}">${medal}</span>
<span class="text-sm text-slate-900">${escapeHtml(item.user_name)}</span>
<span class="text-sm bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">${escapeHtml(item.user_name)}</span>
</div>
<div class="text-sm font-medium text-slate-700">${item.total_score}分 <span class="text-xs text-slate-400">(${item.exam_count}科)</span></div>
</div>`;