Files
zlqy/templates/contest_question_bank.html
2026-02-27 10:37:11 +08:00

260 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}题库管理 - {{ contest.name }} - 智联青云{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-slate-900">📚 题库管理</h1>
<p class="text-sm text-slate-500 mt-1">{{ contest.name }}</p>
</div>
<div class="flex gap-3">
<a href="/contests/{{ contest.id }}" class="px-4 py-2 border border-slate-300 rounded-md text-sm text-slate-700 hover:bg-slate-50">返回杯赛</a>
{% if is_owner %}
<button onclick="showCreateExamModal()" class="px-4 py-2 bg-green-600 text-white rounded-md text-sm hover:bg-green-700">选题组卷</button>
{% endif %}
</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">添加题目</h2>
<form id="add-question-form" onsubmit="addQuestion(event)">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">题目类型</label>
<select id="q-type" onchange="toggleOptions()" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
<option value="choice">选择题</option>
<option value="fill">填空题</option>
<option value="essay">主观题</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">建议分值</label>
<input type="number" id="q-score" value="10" min="1" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-1">题目内容</label>
<textarea id="q-content" rows="3" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm" required></textarea>
</div>
<div id="options-section" class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-1">选项(每行一个)</label>
<textarea id="q-options" rows="4" placeholder="A. 选项一&#10;B. 选项二&#10;C. 选项三&#10;D. 选项四" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm"></textarea>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-1">答案</label>
<input type="text" id="q-answer" placeholder="填写正确答案" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-md text-sm hover:bg-blue-700">添加到题库</button>
</form>
</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">题库列表 (<span id="q-count">0</span>题)</h2>
<div id="question-list" class="space-y-4">
<div class="text-center py-8 text-slate-500">加载中...</div>
</div>
</div>
</div>
<!-- 选题组卷弹窗 -->
{% if is_owner %}
<div id="create-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-2xl max-h-[80vh] overflow-y-auto p-6">
<h3 class="text-lg font-semibold mb-4">选题组卷</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">考试标题</label>
<input type="text" id="exam-title" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm" required>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">考试时长(分钟)</label>
<input type="number" id="exam-duration" value="120" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">开始时间</label>
<input type="datetime-local" id="exam-start" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">结束时间</label>
<input type="datetime-local" id="exam-end" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">成绩公布时间</label>
<input type="datetime-local" id="exam-release" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">选择题目</label>
<div id="exam-question-list" class="space-y-2 max-h-60 overflow-y-auto border border-slate-200 rounded-md p-3">
</div>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="hideCreateExamModal()" class="px-4 py-2 border border-slate-300 rounded-md text-sm">取消</button>
<button onclick="createExamFromBank()" class="px-4 py-2 bg-green-600 text-white rounded-md text-sm hover:bg-green-700">创建考试</button>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
const CONTEST_ID = {{ contest.id }};
const IS_OWNER = {{ 'true' if is_owner else 'false' }};
const CURRENT_USER_ID = {{ user.id if user else 0 }};
let allQuestions = [];
function toggleOptions() {
const type = document.getElementById('q-type').value;
document.getElementById('options-section').style.display = type === 'choice' ? 'block' : 'none';
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
async function loadQuestions() {
try {
const res = await fetch(`/api/contests/${CONTEST_ID}/question-bank`);
const data = await res.json();
if (!data.success) { alert(data.message); return; }
allQuestions = data.questions;
document.getElementById('q-count').textContent = allQuestions.length;
const container = document.getElementById('question-list');
if (allQuestions.length === 0) {
container.innerHTML = '<div class="text-center py-8 text-slate-500">题库暂无题目</div>';
return;
}
const typeMap = {'choice':'选择题','fill':'填空题','essay':'主观题'};
let html = '';
allQuestions.forEach((q, i) => {
const canDel = IS_OWNER || q.contributor_id === CURRENT_USER_ID;
html += `<div class="border border-slate-200 rounded-lg p-4">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-800">${typeMap[q.type]||q.type}</span>
<span class="text-xs text-slate-500">${q.score}分</span>
<span class="text-xs text-slate-400">贡献者: ${escapeHtml(q.contributor_name)}</span>
<span class="text-xs text-slate-400">${q.created_at}</span>
</div>
<p class="text-sm text-slate-800 mb-2">${escapeHtml(q.content)}</p>`;
if (q.type === 'choice' && q.options && q.options.length) {
html += '<div class="text-sm text-slate-600 space-y-1 ml-4">';
q.options.forEach(opt => { html += `<div>${escapeHtml(opt)}</div>`; });
html += '</div>';
}
if (q.answer) {
html += `<div class="text-sm text-green-700 mt-1">答案: ${escapeHtml(q.answer)}</div>`;
}
html += `</div>`;
if (canDel) {
html += `<button onclick="deleteQuestion(${q.id})" class="text-red-500 hover:text-red-700 text-sm shrink-0">删除</button>`;
}
html += `</div></div>`;
});
container.innerHTML = html;
} catch(e) {
document.getElementById('question-list').innerHTML = '<div class="text-center py-8 text-red-500">加载失败</div>';
}
}
async function addQuestion(e) {
e.preventDefault();
const type = document.getElementById('q-type').value;
const content = document.getElementById('q-content').value.trim();
const score = parseInt(document.getElementById('q-score').value) || 10;
const answer = document.getElementById('q-answer').value.trim();
if (!content) { alert('请填写题目内容'); return; }
let options = [];
if (type === 'choice') {
const raw = document.getElementById('q-options').value.trim();
if (raw) options = raw.split('\n').filter(l => l.trim());
}
try {
const res = await fetch(`/api/contests/${CONTEST_ID}/question-bank`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type, content, options, answer, score})
});
const data = await res.json();
if (data.success) {
document.getElementById('q-content').value = '';
document.getElementById('q-options').value = '';
document.getElementById('q-answer').value = '';
loadQuestions();
} else { alert(data.message); }
} catch(e) { alert('添加失败'); }
}
async function deleteQuestion(qid) {
if (!confirm('确定删除该题目?')) return;
try {
const res = await fetch(`/api/contests/${CONTEST_ID}/question-bank/${qid}`, {method:'DELETE'});
const data = await res.json();
if (data.success) { loadQuestions(); } else { alert(data.message); }
} catch(e) { alert('删除失败'); }
}
function showCreateExamModal() {
const list = document.getElementById('exam-question-list');
const typeMap = {'choice':'选择题','fill':'填空题','essay':'主观题'};
let html = '';
allQuestions.forEach(q => {
html += `<label class="flex items-start gap-2 p-2 hover:bg-slate-50 rounded cursor-pointer">
<input type="checkbox" value="${q.id}" class="mt-1 exam-q-check">
<div class="flex-1">
<span class="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-800">${typeMap[q.type]||q.type}</span>
<span class="text-xs text-slate-500">${q.score}分</span>
<p class="text-sm text-slate-700 mt-1">${escapeHtml(q.content)}</p>
</div>
</label>`;
});
if (!html) html = '<div class="text-center text-slate-500 text-sm py-4">题库暂无题目</div>';
list.innerHTML = html;
document.getElementById('create-exam-modal').classList.remove('hidden');
}
function hideCreateExamModal() {
document.getElementById('create-exam-modal').classList.add('hidden');
}
async function createExamFromBank() {
const title = document.getElementById('exam-title').value.trim();
const duration = parseInt(document.getElementById('exam-duration').value) || 120;
const start = document.getElementById('exam-start').value;
const end = document.getElementById('exam-end').value;
const release = document.getElementById('exam-release').value;
const checks = document.querySelectorAll('.exam-q-check:checked');
const qids = Array.from(checks).map(c => parseInt(c.value));
if (!title) { alert('请填写考试标题'); return; }
if (qids.length === 0) { alert('请至少选择一道题目'); return; }
try {
const res = await fetch(`/api/contests/${CONTEST_ID}/create-exam-from-bank`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title, duration, question_ids: qids,
scheduled_start: start, scheduled_end: end, score_release_time: release
})
});
const data = await res.json();
if (data.success) {
alert('考试创建成功!');
hideCreateExamModal();
window.location.href = `/exams/${data.exam_id}`;
} else { alert(data.message); }
} catch(e) { alert('创建失败'); }
}
loadQuestions();
</script>
{% endblock %}