494 lines
26 KiB
HTML
494 lines
26 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ contest.name }} - 智联青云{% endblock %}
|
||
{% 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>
|
||
{% 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>
|
||
{% endif %}
|
||
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
|
||
<div class="flex justify-between items-start">
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-slate-900 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>
|
||
{% endif %}
|
||
</h1>
|
||
<div class="flex items-center space-x-4 text-sm text-slate-500">
|
||
<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 }}
|
||
</span>
|
||
<span class="flex items-center" id="participants-count">
|
||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
||
<span id="participants-value">{{ contest.participants }}</span>人已报名
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="flex space-x-3">
|
||
{% if contest.status != 'abolished' %}
|
||
{% 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">
|
||
{% 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>
|
||
{% 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>
|
||
{% 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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
{% 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">
|
||
登录后报名
|
||
</a>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主体区域:左侧两列 + 右侧一列 -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
<!-- 左侧两列(比赛详情、历年真题) -->
|
||
<div class="lg:col-span-2 space-y-8">
|
||
<!-- 比赛详情 -->
|
||
|
||
<!-- 历年真题 -->
|
||
<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="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>
|
||
历年真题
|
||
</h2>
|
||
{% set papers = contest.get_past_papers() %}
|
||
{% 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">
|
||
<span class="text-sm font-medium text-slate-700 w-16">{{ paper.year }}</span>
|
||
<span class="text-sm text-slate-600">{{ 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">
|
||
<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>
|
||
下载
|
||
</a>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-8 text-slate-500">暂无历年真题,敬请期待!</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 考试列表 -->
|
||
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
|
||
<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>
|
||
{% 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>
|
||
{% endif %}
|
||
</div>
|
||
<div id="exam-list" class="space-y-3">
|
||
<div class="text-center py-4 text-slate-400 text-sm">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧一列(主办方信息) -->
|
||
<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="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>
|
||
{% 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="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>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
{% 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">
|
||
<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>
|
||
<div id="leaderboard" class="space-y-2">
|
||
<div class="text-center py-4 text-slate-400 text-sm">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
讨论区
|
||
</h2>
|
||
|
||
<!-- 发帖表单(仅对有权限用户显示) -->
|
||
{% if user and can_post %}
|
||
<div class="mb-6 border-b border-slate-200 pb-4">
|
||
<form id="post-form" onsubmit="submitPost(event)">
|
||
<input type="text" id="post-title" placeholder="帖子标题" class="w-full px-3 py-2 border border-slate-300 rounded-md mb-2 text-sm" required>
|
||
<textarea id="post-content" rows="3" placeholder="写下你的讨论内容..." class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm" required></textarea>
|
||
<div class="flex justify-end mt-2">
|
||
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-md text-sm hover:bg-blue-700">发布帖子</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% elif user and not can_post %}
|
||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4 text-sm text-yellow-700">
|
||
⚠️ 您需要报名该杯赛并至少参与一次考试,才能参与讨论。
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 帖子列表容器 -->
|
||
<div id="post-list" class="space-y-4">
|
||
<div class="text-center py-8 text-slate-500">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导入考试弹窗 -->
|
||
{% if is_owner %}
|
||
<div id="import-exam-modal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
||
<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">×</button>
|
||
</div>
|
||
<p class="text-sm text-slate-500 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>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
const CONTEST_ID = {{ contest.id }};
|
||
let canPost = {{ (can_post is defined and can_post) | lower }};
|
||
const isOwner = {{ (is_owner is defined and is_owner) | lower }};
|
||
|
||
// 报名切换
|
||
async function toggleRegistration(contestId) {
|
||
const btn = document.getElementById('register-btn');
|
||
const isRegistered = btn.textContent.trim() === '已报名';
|
||
const url = isRegistered ? `/api/contests/${contestId}/unregister` : `/api/contests/${contestId}/register`;
|
||
|
||
try {
|
||
const res = await fetch(url, { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
if (isRegistered) {
|
||
btn.textContent = '立即报名';
|
||
btn.className = 'px-6 py-2 bg-primary text-white rounded-md hover:bg-blue-700 font-medium';
|
||
} else {
|
||
btn.textContent = '已报名';
|
||
btn.className = 'px-6 py-2 bg-slate-100 text-slate-700 border border-slate-300 rounded-md hover:bg-slate-200 font-medium';
|
||
}
|
||
document.getElementById('participants-value').textContent = data.participants;
|
||
// 报名状态变化可能影响发帖权限,可以重新加载页面或更新 canPost
|
||
location.reload(); // 简单处理,刷新页面
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
} catch (err) {
|
||
alert('操作失败,请重试');
|
||
}
|
||
}
|
||
|
||
// 加载帖子列表
|
||
async function loadPosts() {
|
||
const container = document.getElementById('post-list');
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/posts`);
|
||
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>';
|
||
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>
|
||
<div class="flex items-center text-xs text-slate-400 space-x-3">
|
||
<span>${escapeHtml(p.author)}</span>
|
||
<span>${p.created_at}</span>
|
||
<span>❤️ ${p.likes}</span>
|
||
<span>💬 ${p.replies}</span>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
} catch (err) {
|
||
container.innerHTML = '<div class="text-center py-8 text-red-500">加载失败,请刷新重试</div>';
|
||
}
|
||
}
|
||
|
||
// 提交新帖子
|
||
async function submitPost(e) {
|
||
e.preventDefault();
|
||
if (!canPost) {
|
||
alert('您没有权限发帖');
|
||
return;
|
||
}
|
||
const title = document.getElementById('post-title').value.trim();
|
||
const content = document.getElementById('post-content').value.trim();
|
||
if (!title || !content) {
|
||
alert('标题和内容不能为空');
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/posts`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title, content })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
document.getElementById('post-title').value = '';
|
||
document.getElementById('post-content').value = '';
|
||
loadPosts(); // 重新加载列表
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
} catch (err) {
|
||
alert('发布失败');
|
||
}
|
||
}
|
||
|
||
// 简单的转义函数(防止XSS)
|
||
function escapeHtml(unsafe) {
|
||
return unsafe.replace(/[&<>"]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
if (m === '"') return '"';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
// 初始化
|
||
loadPosts();
|
||
loadExams();
|
||
loadLeaderboard();
|
||
|
||
// 发布/取消发布杯赛
|
||
async function publishContest() {
|
||
const btn = document.getElementById('publish-btn');
|
||
if (!confirm('确定发布该杯赛?发布后所有用户可见。')) return;
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/publish`, {method: 'PUT'});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(data.message || '操作失败');
|
||
}
|
||
} catch(e) { alert('网络错误'); }
|
||
}
|
||
|
||
// 加载考试列表
|
||
async function loadExams() {
|
||
const container = document.getElementById('exam-list');
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/exams`);
|
||
const data = await res.json();
|
||
if (!data.success || !data.exams.length) {
|
||
container.innerHTML = '<div class="text-center py-6 text-slate-400 text-sm">暂无考试,负责人可点击"导入考试"关联已有试卷</div>';
|
||
return;
|
||
}
|
||
let html = '';
|
||
data.exams.forEach(e => {
|
||
const statusClass = e.status === 'available' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
|
||
const statusText = e.status === 'available' ? '进行中' : '已关闭';
|
||
const subCount = e.submission_count !== null ? `<span class="text-xs text-slate-400 ml-2">${e.submission_count}人提交</span>` : '';
|
||
const removeBtn = isOwner ? `<button onclick="removeExam(${e.id}, '${escapeHtml(e.title)}')" class="ml-2 px-2 py-1 text-xs text-red-600 border border-red-300 rounded hover:bg-red-50">移除</button>` : '';
|
||
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>
|
||
<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>
|
||
<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>
|
||
${removeBtn}
|
||
</div>
|
||
</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="text-center py-4 text-red-500 text-sm">加载失败</div>';
|
||
}
|
||
}
|
||
|
||
// 移除考试(不删除,只取消关联)
|
||
async function removeExam(examId, title) {
|
||
if (!confirm('确定将考试「' + title + '」从杯赛中移除?考试本身不会被删除。')) return;
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/remove-exam`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({exam_id: examId})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) { loadExams(); } else { alert(data.message || '操作失败'); }
|
||
} catch(e) { alert('网络错误'); }
|
||
}
|
||
|
||
// 导入考试弹窗
|
||
function showImportModal() {
|
||
document.getElementById('import-exam-modal').classList.remove('hidden');
|
||
loadAvailableExams();
|
||
}
|
||
function hideImportModal() {
|
||
document.getElementById('import-exam-modal').classList.add('hidden');
|
||
}
|
||
|
||
async function loadAvailableExams() {
|
||
const container = document.getElementById('available-exams-list');
|
||
container.innerHTML = '<div class="text-center py-4 text-slate-400 text-sm">加载中...</div>';
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/available-exams`);
|
||
const data = await res.json();
|
||
if (!data.success || !data.exams.length) {
|
||
container.innerHTML = '<div class="text-center py-6 text-slate-400 text-sm">没有可导入的考试。请先在考试系统中创建试卷。</div>';
|
||
return;
|
||
}
|
||
let html = '';
|
||
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>
|
||
<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>`;
|
||
});
|
||
container.innerHTML = html;
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="text-center py-4 text-red-500 text-sm">加载失败</div>';
|
||
}
|
||
}
|
||
|
||
async function importExam(examId) {
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/import-exam`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({exam_id: examId})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
loadExams();
|
||
loadAvailableExams();
|
||
} else { alert(data.message || '导入失败'); }
|
||
} catch(e) { alert('网络错误'); }
|
||
}
|
||
|
||
// 加载排行榜
|
||
async function loadLeaderboard() {
|
||
const container = document.getElementById('leaderboard');
|
||
try {
|
||
const res = await fetch(`/api/contests/${CONTEST_ID}/leaderboard`);
|
||
const data = await res.json();
|
||
if (!data.success || !data.leaderboard.length) {
|
||
container.innerHTML = '<div class="text-center py-4 text-slate-400 text-sm">暂无成绩数据</div>';
|
||
return;
|
||
}
|
||
let html = '';
|
||
data.leaderboard.forEach(item => {
|
||
const rankClass = item.rank <= 3 ? 'font-bold text-yellow-600' : 'text-slate-500';
|
||
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'}">
|
||
<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>
|
||
</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>`;
|
||
});
|
||
container.innerHTML = html;
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="text-center py-4 text-red-500 text-sm">加载失败</div>';
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %} |