first commit

This commit is contained in:
2026-02-27 10:37:11 +08:00
commit 74f19aad0b
86 changed files with 18642 additions and 0 deletions

192
templates/exam_grade.html Normal file
View File

@@ -0,0 +1,192 @@
{% extends "base.html" %}
{% block title %}批改试卷 - 智联青云{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto 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">{{ exam.title }} · 考生:{{ submission.user.name if submission.user else '未知' }} · 提交时间:{{ submission.submitted_at }}</p>
</div>
<div class="flex items-center space-x-3">
{% if next_ungraded %}
<a href="/exams/{{ exam.id }}/grade/{{ next_ungraded }}" class="inline-flex items-center px-3 py-1.5 bg-yellow-50 border border-yellow-300 text-yellow-700 text-sm font-medium rounded-md hover:bg-yellow-100">
下一个未批改 →
</a>
{% endif %}
<a href="/exams/{{ exam.id }}/submissions" class="text-sm text-slate-500 hover:text-slate-700">← 返回提交列表</a>
</div>
</div>
<!-- 批改状态提示 -->
{% if submission.graded %}
<div class="bg-green-50 border border-green-200 rounded-lg p-3 flex items-center justify-between">
<span class="text-sm text-green-700">该试卷已批改完成,得分:{{ submission.score }}/{{ exam.total_score }},批改人:{{ submission.graded_by }}</span>
<span class="text-xs text-green-500">可重新批改覆盖</span>
</div>
{% endif %}
{% for q in questions %}
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<div class="flex items-start space-x-4">
<span class="flex-shrink-0 w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-slate-600 font-medium">{{ loop.index }}</span>
<div class="flex-1 space-y-3">
<div class="flex justify-between items-start">
<div>
<span class="text-xs font-medium px-2 py-0.5 rounded
{% if q.type == 'choice' %}bg-blue-50 text-blue-700
{% elif q.type == 'fill' %}bg-green-50 text-green-700
{% else %}bg-purple-50 text-purple-700{% endif %}">
{% if q.type == 'choice' %}选择题{% elif q.type == 'fill' %}填空题{% else %}解答题{% endif %}
</span>
<p class="mt-2 text-lg text-slate-900">{{ q.content }}</p>
{% if q.get('images') %}
<div class="mt-2 flex flex-wrap gap-2">
{% 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="题目图片">
{% endfor %}
</div>
{% endif %}
</div>
<span class="text-sm text-slate-400 whitespace-nowrap ml-4">{{ q.score }}分)</span>
</div>
{% if q.type == 'choice' %}
<div class="space-y-2">
{% for opt in q.options %}
{% set letter = ['A','B','C','D'][loop.index0] %}
{% set is_answer = letter == q.get('answer','') %}
{% set is_selected = letter == answers.get(q.id|string,'') %}
<div class="flex items-center space-x-3 p-2 rounded border
{% if is_answer %}border-green-300 bg-green-50
{% elif is_selected and not is_answer %}border-red-300 bg-red-50
{% else %}border-slate-100{% endif %}">
<span class="text-sm text-slate-700">{{ letter }}. {{ opt }}</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_answer %}<span class="text-xs text-green-600 font-medium">✓ 正确</span>{% endif %}
</div>
{% endfor %}
<div class="text-sm text-slate-500 mt-1">
自动判分:{% if answers.get(q.id|string,'') == q.get('answer','') %}
<span class="text-green-600 font-medium">+{{ q.score }}分</span>
{% else %}
<span class="text-red-500 font-medium">0分</span>
{% endif %}
</div>
</div>
{% else %}
<div class="p-3 bg-slate-50 rounded-lg border border-slate-200">
<div class="text-sm text-slate-500 mb-1">考生答案:</div>
<div class="text-slate-800 whitespace-pre-wrap">{{ answers.get(q.id|string, '(未作答)') | render_images }}</div>
</div>
{% if q.get('answer') %}
<div class="p-3 bg-green-50 rounded-lg border border-green-200">
<div class="text-sm text-green-600 mb-1">参考答案:</div>
<div class="text-green-800 whitespace-pre-wrap">{{ q.answer }}</div>
</div>
{% endif %}
<div class="flex items-center space-x-3 mt-2">
<label class="text-sm text-slate-600">给分:</label>
<input type="number" id="score-{{ q.id }}" min="0" max="{{ q.score }}"
value="{{ question_scores.get(q.id|string, 0) }}"
class="grade-score w-20 px-2 py-1 border border-slate-300 rounded text-sm" data-max="{{ q.score }}" data-qid="{{ q.id }}">
<span class="text-sm text-slate-400">/ {{ q.score }}</span>
<!-- 快速给分按钮 -->
<div class="flex space-x-1">
<button type="button" onclick="quickScore('{{ q.id }}', 0)" class="px-2 py-0.5 text-xs rounded border border-red-200 text-red-600 hover:bg-red-50">0分</button>
<button type="button" onclick="quickScore('{{ q.id }}', {{ (q.score / 2)|int }})" class="px-2 py-0.5 text-xs rounded border border-yellow-200 text-yellow-600 hover:bg-yellow-50">{{ (q.score / 2)|int }}分</button>
<button type="button" onclick="quickScore('{{ q.id }}', {{ q.score }})" class="px-2 py-0.5 text-xs rounded border border-green-200 text-green-600 hover:bg-green-50">满分</button>
</div>
</div>
{% endif %}
{% if q.get('explanation') %}
<div class="p-3 bg-indigo-50 rounded-lg border border-indigo-200 mt-2">
<div class="text-sm text-indigo-600 mb-1 font-medium">题目解析:</div>
<div class="text-indigo-900 text-sm whitespace-pre-wrap">{{ q.explanation }}</div>
</div>
{% endif %}
</div>
<div class="flex justify-between items-center bg-white shadow-sm rounded-lg p-6 border border-slate-200 sticky bottom-4">
<div class="text-lg font-medium text-slate-900">总分:<span id="total-score" class="text-primary">0</span> / {{ exam.total_score }}</div>
<div class="flex items-center space-x-3">
{% if next_ungraded %}
<span class="text-sm text-slate-400">批改后自动跳转下一个</span>
{% endif %}
<button onclick="submitGrade()" class="px-8 py-3 bg-primary text-white rounded-lg font-medium hover:bg-blue-700">提交批改</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function quickScore(qid, score) {
document.getElementById('score-' + qid).value = score;
recalcTotal();
}
function recalcTotal() {
let total = 0;
// 选择题自动得分
{% for q in questions %}
{% if q.type == 'choice' %}
{% if answers.get(q.id|string,'') == q.get('answer','') %}
total += {{ q.score }};
{% endif %}
{% elif q.type == 'fill' %}
{% set student_ans = answers.get(q.id|string,'').strip() %}
{% set correct_answers = q.get('answer','').split('|') %}
{% if student_ans in correct_answers %}
total += {{ q.score }};
{% endif %}
{% endif %}
{% endfor %}
// 主观题手动得分
document.querySelectorAll('.grade-score').forEach(input => {
total += parseInt(input.value) || 0;
});
document.getElementById('total-score').textContent = total;
}
document.querySelectorAll('.grade-score').forEach(input => {
input.addEventListener('input', recalcTotal);
});
recalcTotal();
function submitGrade() {
const scores = {};
{% for q in questions %}
{% if q.type == 'choice' %}
{% if answers.get(q.id|string,'') == q.get('answer','') %}
scores['{{ q.id }}'] = {{ q.score }};
{% else %}
scores['{{ q.id }}'] = 0;
{% endif %}
{% elif q.type == 'fill' %}
{% set student_ans = answers.get(q.id|string,'').strip() %}
{% set correct_answers = q.get('answer','').split('|') %}
{% if student_ans in correct_answers %}
scores['{{ q.id }}'] = {{ q.score }};
{% else %}
scores['{{ q.id }}'] = 0;
{% endif %}
{% else %}
scores['{{ q.id }}'] = parseInt(document.getElementById('score-{{ q.id }}').value) || 0;
{% endif %}
{% endfor %}
fetch('/api/exams/{{ exam.id }}/grade/{{ submission.id }}', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({scores})
}).then(r=>r.json()).then(data=>{
if(data.success) {
alert('批改完成!总分:'+data.total_score);
{% if next_ungraded %}
window.location.href='/exams/{{ exam.id }}/grade/{{ next_ungraded }}';
{% else %}
window.location.href='/exams/{{ exam.id }}/submissions';
{% endif %}
}
else alert(data.message);
}).catch(()=>alert('批改失败'));
}
</script>
{% endblock %}