249 lines
17 KiB
HTML
249 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}考试系统 - 智联青云{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="space-y-8">
|
||
<!-- 头部区域 -->
|
||
<div class="futuristic-card flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-6">
|
||
<div>
|
||
<h1 class="text-2xl font-bold flex items-center bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 bg-clip-text text-transparent">
|
||
<span class="w-10 h-10 bg-gradient-to-br from-cyan-500 to-blue-600 text-white rounded-xl flex items-center justify-center mr-3 shadow-lg shadow-cyan-500/50">📝</span>
|
||
考试中心
|
||
</h1>
|
||
<p class="text-slate-400 text-sm mt-1 ml-13">海量真题与模拟卷,随时随地进行练习与自测。</p>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
{% if user and (user.role == 'admin' or user.role == 'teacher') %}
|
||
<a href="/exams/create" class="btn-futuristic inline-flex items-center px-4 py-2.5 text-sm font-medium">
|
||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||
创建新试卷
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 搜索区域 -->
|
||
<div class="futuristic-card p-5 flex flex-wrap gap-4 items-center justify-between sticky top-20 z-10">
|
||
<form method="GET" action="/exams" class="flex flex-wrap items-center gap-3 w-full">
|
||
<div class="relative w-full sm:w-auto flex-1">
|
||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<svg class="h-4 w-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||
</div>
|
||
<input type="text" name="q" value="{{ search_query or '' }}" placeholder="搜索试卷名称..." class="w-full pl-10 pr-4 py-2.5 border border-cyan-500/30 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 bg-slate-900/50 text-slate-200 placeholder-slate-500 transition-all">
|
||
</div>
|
||
|
||
<div class="relative w-full sm:w-auto">
|
||
<select name="subject" class="w-full sm:w-32 appearance-none px-4 py-2.5 pl-10 border border-cyan-500/30 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 bg-slate-900/50 text-slate-200 hover:bg-slate-800/50 transition-colors cursor-pointer">
|
||
<option value="">所有科目</option>
|
||
{% for subject in all_subjects %}
|
||
<option value="{{ subject }}" {% if subject_filter == subject %}selected{% endif %}>{{ subject }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<svg class="h-4 w-4 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>
|
||
</div>
|
||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||
<svg class="h-4 w-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn-futuristic w-full sm:w-auto px-5 py-2.5 text-sm font-medium">
|
||
搜索
|
||
</button>
|
||
|
||
{% if search_query or subject_filter %}
|
||
<a href="/exams" class="w-full sm:w-auto px-5 py-2.5 bg-slate-800/50 text-slate-300 rounded-xl hover:bg-slate-700/50 text-sm font-medium transition-all text-center border border-cyan-500/30 hover:border-cyan-500/50">
|
||
重置
|
||
</a>
|
||
{% endif %}
|
||
</form>
|
||
{% if search_query or subject_filter %}
|
||
<div class="mt-3 w-full text-xs text-slate-400 bg-slate-900/30 px-3 py-2 rounded-lg border border-cyan-500/20 inline-block">
|
||
<span class="font-medium text-cyan-400">筛选结果:</span>
|
||
{% if search_query %}包含 "<span class="text-cyan-300">{{ search_query }}</span>"{% endif %}
|
||
{% if subject_filter %}{% if search_query %},{% endif %}科目为 "<span class="text-cyan-300">{{ subject_filter }}</span>"{% endif %}
|
||
<span class="ml-2 text-slate-500">共找到 {{ exams|length }} 份试卷</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 试卷列表 -->
|
||
{% if exams|length == 0 %}
|
||
<div class="col-span-full py-20 flex flex-col items-center justify-center text-slate-400 futuristic-card border-dashed">
|
||
<svg class="w-16 h-16 mb-4 text-cyan-500/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||
<p>暂无符合条件的试卷</p>
|
||
{% if user and (user.role == 'admin' or user.role == 'teacher') %}
|
||
<p class="text-sm mt-2">点击上方"创建新试卷"按钮开始命题吧</p>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<div class="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||
{% for exam in exams %}
|
||
{% set subjectColor = 'blue' %}
|
||
{% set subjectGradient = 'from-cyan-500 to-blue-600' %}
|
||
{% set subjectIcon = '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' %}
|
||
|
||
{% if exam.subject == '数学' %}
|
||
{% set subjectColor = 'indigo' %}
|
||
{% set subjectGradient = 'from-purple-500 to-indigo-600' %}
|
||
{% set subjectIcon = 'M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z' %}
|
||
{% elif exam.subject == '英语' %}
|
||
{% set subjectColor = 'rose' %}
|
||
{% set subjectGradient = 'from-pink-500 to-rose-600' %}
|
||
{% set subjectIcon = 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129' %}
|
||
{% elif exam.subject in ['物理', '化学', '生物'] %}
|
||
{% set subjectColor = 'emerald' %}
|
||
{% set subjectGradient = 'from-emerald-500 to-teal-600' %}
|
||
{% set subjectIcon = 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z' %}
|
||
{% elif exam.subject in ['历史', '地理', '政治'] %}
|
||
{% set subjectColor = 'amber' %}
|
||
{% set subjectGradient = 'from-amber-500 to-orange-600' %}
|
||
{% set subjectIcon = 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z' %}
|
||
{% endif %}
|
||
|
||
<div class="futuristic-card-dark group hover-lift flex flex-col relative overflow-hidden transition-all duration-300 transform hover:scale-105">
|
||
<!-- 卡片封面海报 -->
|
||
<div class="relative h-28 bg-gradient-to-r {{ subjectGradient }} overflow-hidden">
|
||
<div class="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
||
<div class="absolute -bottom-10 -right-10 w-32 h-32 bg-white/10 blur-2xl rounded-full"></div>
|
||
|
||
{% if exam.status == 'closed' %}
|
||
<span class="badge-futuristic absolute top-4 right-4 bg-slate-900/50 backdrop-blur-md text-slate-300 border-slate-700/50">已关闭</span>
|
||
{% else %}
|
||
<span class="badge-futuristic absolute top-4 right-4 bg-white/90 backdrop-blur-md text-cyan-600 border-cyan-500/30">
|
||
<span class="w-1.5 h-1.5 rounded-full bg-cyan-500 mr-1.5 animate-pulse"></span>进行中
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 悬浮图标 -->
|
||
<div class="absolute top-[4.5rem] left-6">
|
||
<div class="w-14 h-14 bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl flex items-center justify-center text-cyan-400 shadow-lg shadow-cyan-500/30 border-2 border-cyan-500/30 group-hover:scale-110 group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{subjectIcon}}"/></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-6 pt-12 flex-1 flex flex-col">
|
||
<div class="flex items-start mb-3">
|
||
<span class="badge-futuristic">
|
||
{{ exam.subject }}
|
||
</span>
|
||
</div>
|
||
|
||
<h3 class="text-lg font-bold mb-3 line-clamp-2 bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent group-hover:from-blue-300 group-hover:via-purple-300 group-hover:to-pink-300 transition-all flex-1">{{ exam.title }}</h3>
|
||
|
||
<div class="grid grid-cols-2 gap-3 mb-5 bg-slate-900/30 p-3 rounded-xl border border-cyan-500/20">
|
||
<div>
|
||
<span class="text-[10px] text-slate-500 block mb-0.5">满分 / 题目</span>
|
||
<span class="text-xs font-semibold text-cyan-300">{{ exam.total_score }}分 / {{ exam.questions|fromjson|length }}题</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-[10px] text-slate-500 block mb-0.5">考试时长</span>
|
||
<span class="text-xs font-semibold text-cyan-300">{{ exam.duration }} 分钟</span>
|
||
</div>
|
||
</div>
|
||
|
||
{% if exam.scheduled_start or exam.scheduled_end %}
|
||
<div class="mb-4 text-xs text-slate-400 flex flex-col gap-1 border-l-2 border-cyan-500/50 pl-2">
|
||
{% if exam.scheduled_start %}
|
||
<div class="flex items-center">
|
||
<svg class="w-3.5 h-3.5 mr-1 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||
开始:{{ exam.scheduled_start.strftime('%m-%d %H:%M') }}
|
||
</div>
|
||
{% endif %}
|
||
{% if exam.scheduled_end %}
|
||
<div class="flex items-center">
|
||
<svg class="w-3.5 h-3.5 mr-1 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||
截止:{{ exam.scheduled_end.strftime('%m-%d %H:%M') }}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="mt-auto pt-4 border-t border-cyan-500/20 flex items-center justify-between gap-2 flex-wrap">
|
||
{% set sub = user_submissions.get(exam.id) %}
|
||
{% if sub %}
|
||
<div class="flex items-center bg-slate-900/30 px-2.5 py-1.5 rounded-lg border border-cyan-500/30">
|
||
{% if sub.graded %}
|
||
<span class="text-xs font-medium text-slate-400 mr-2 border-r border-slate-600 pr-2">已批改</span>
|
||
<span class="text-sm font-bold text-cyan-400">{{ sub.score }} <span class="text-[10px] text-slate-500 font-normal">/ {{ exam.total_score }}</span></span>
|
||
{% else %}
|
||
<span class="text-xs font-medium text-amber-400 flex items-center">
|
||
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||
待批改
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
<a href="/exams/{{ exam.id }}/result" class="btn-outline-futuristic ml-auto inline-flex items-center px-4 py-2 text-xs font-medium">
|
||
查看试卷
|
||
</a>
|
||
{% else %}
|
||
<div class="text-xs text-slate-400 flex items-center">
|
||
<span class="w-5 h-5 rounded-full bg-slate-800 flex items-center justify-center mr-1.5 text-[10px] text-cyan-400 border border-cyan-500/30">{{ exam.creator.name[0] if exam.creator else '?' }}</span>
|
||
{{ exam.creator.name if exam.creator else '未知出题人' }}
|
||
</div>
|
||
{% if exam.status != 'closed' %}
|
||
<a href="/exams/{{ exam.id }}" class="btn-futuristic ml-auto inline-flex items-center px-5 py-2 text-sm font-medium">
|
||
开始考试
|
||
</a>
|
||
{% else %}
|
||
<span class="ml-auto text-sm text-slate-500 font-medium px-4 py-2 bg-slate-800/50 rounded-xl border border-slate-700/50">已关闭</span>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if user and (user.role == 'admin' or user.role == 'teacher') %}
|
||
<div class="mt-4 pt-3 border-t border-cyan-500/20 flex flex-wrap gap-2 justify-end">
|
||
<a href="/exams/{{ exam.id }}/submissions" class="px-3 py-1.5 bg-slate-800/50 text-slate-300 hover:bg-slate-700/50 hover:text-slate-200 rounded-lg text-xs font-medium transition-colors border border-cyan-500/30">提交情况</a>
|
||
<a href="/exams/{{ exam.id }}/print" class="px-3 py-1.5 bg-slate-800/50 text-slate-300 hover:bg-slate-700/50 hover:text-slate-200 rounded-lg text-xs font-medium transition-colors border border-cyan-500/30">打印试卷</a>
|
||
|
||
{% if user.role == 'admin' or exam.creator_id == user.id %}
|
||
{% if exam.status == 'available' %}
|
||
<button onclick="toggleExamStatus({{ exam.id }}, 'closed')" class="px-3 py-1.5 bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 rounded-lg text-xs font-medium transition-colors border border-orange-500/30">关闭考试</button>
|
||
{% else %}
|
||
<button onclick="toggleExamStatus({{ exam.id }}, 'available')" class="px-3 py-1.5 bg-green-500/20 text-green-400 hover:bg-green-500/30 rounded-lg text-xs font-medium transition-colors border border-green-500/30">开放考试</button>
|
||
{% endif %}
|
||
<button onclick="deleteExam({{ exam.id }})" class="px-3 py-1.5 bg-red-500/20 text-red-400 hover:bg-red-500/30 rounded-lg text-xs font-medium transition-colors border border-red-500/30">删除</button>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
{% block scripts %}
|
||
<script>
|
||
function toggleExamStatus(examId, status) {
|
||
const label = status === 'closed' ? '关闭' : '开放';
|
||
if (!confirm(`确定${label}该考试?`)) return;
|
||
fetch(`/api/exams/${examId}/status`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ status: status })
|
||
}).then(r => r.json()).then(data => {
|
||
if (data.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
}).catch(() => alert('操作失败'));
|
||
}
|
||
|
||
function deleteExam(examId) {
|
||
if (!confirm('确定删除该试卷?此操作不可恢复,所有提交记录也将被删除。')) return;
|
||
fetch(`/api/exams/${examId}`, {
|
||
method: 'DELETE'
|
||
}).then(r => r.json()).then(data => {
|
||
if (data.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
}).catch(() => alert('删除失败'));
|
||
}
|
||
</script>
|
||
{% endblock %} |