260 lines
13 KiB
HTML
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. 选项一 B. 选项二 C. 选项三 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 %}
|