mobile app apply_contest contest_detail contest_edit notifications theme

This commit is contained in:
2026-02-27 20:36:34 +08:00
parent 17a4415cd2
commit 4cd4b6ef52
5 changed files with 53 additions and 6 deletions

19
app.py
View File

@@ -914,6 +914,12 @@ def api_change_name():
@login_required
def apply_contest():
if request.method == 'POST':
user_id = session['user']['id']
# 防止重复提交:如果已有 pending 状态的申请,不允许再提交
existing = ContestApplication.query.filter_by(user_id=user_id, status='pending').first()
if existing:
flash('您已有一个待审核的杯赛申请,请等待审核结果后再提交新申请')
return redirect(url_for('contest_list'))
name = request.form.get('name')
organizer = request.form.get('organizer')
description = request.form.get('description')
@@ -1136,6 +1142,19 @@ def api_mark_all_notifications_read():
db.session.commit()
return jsonify({'success': True})
@app.route('/api/notifications/<int:nid>', methods=['DELETE'])
@login_required
def api_delete_notification(nid):
"""删除单条通知(仅管理员可删除已处理的通知)"""
n = Notification.query.get_or_404(nid)
if n.user_id != session['user']['id']:
return jsonify({'success': False, 'message': '无权操作'}), 403
if session['user'].get('role') != 'admin':
return jsonify({'success': False, 'message': '仅管理员可删除通知'}), 403
db.session.delete(n)
db.session.commit()
return jsonify({'success': True})
@app.route('/notifications')
@login_required
def notifications_page():

View File

@@ -88,7 +88,7 @@
<a href="{{ url_for('contest_list') }}" class="px-5 py-2.5 border border-slate-300 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
取消
</a>
<button type="submit"
<button type="submit" id="submit-btn"
class="px-5 py-2.5 bg-primary text-white rounded-md text-sm font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
提交申请
</button>
@@ -97,3 +97,14 @@
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.querySelector('form').addEventListener('submit', function() {
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = '提交中...';
btn.classList.add('opacity-50', 'cursor-not-allowed');
});
</script>
{% endblock %}

View File

@@ -59,6 +59,9 @@
<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>

View File

@@ -44,7 +44,7 @@
</div>
<!-- 往届真题管理 -->
<div class="bg-white shadow-sm rounded-lg p-6 border border-slate-200">
<div id="papers" 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>
<div id="papers-list" class="space-y-2 mb-6">
{% for paper in contest.get_past_papers() %}

View File

@@ -209,8 +209,8 @@ function buildActions(n) {
<button onclick="event.stopPropagation();rejectTeacherN(${n.application_id})" class="px-4 py-1.5 text-xs font-bold bg-rose-50 text-rose-600 border border-rose-200 rounded-lg hover:bg-rose-500 hover:text-white hover:border-rose-500 transition-colors shadow-sm">❌ 拒绝申请</button>
</div>`;
}
if (n.type === 'teacher_application' && n.application_status === 'approved') return '<div class="mt-3 inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 border border-emerald-100 text-emerald-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> 已同意</div>';
if (n.type === 'teacher_application' && n.application_status === 'rejected') return '<div class="mt-3 inline-flex items-center gap-1.5 px-3 py-1 bg-rose-50 border border-rose-100 text-rose-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> 已拒绝</div>';
if (n.type === 'teacher_application' && n.application_status === 'approved') return '<div class="mt-3 flex items-center gap-2"><span class="inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 border border-emerald-100 text-emerald-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> 已同意</span>' + (currentUser.role === 'admin' ? `<button onclick="event.stopPropagation();deleteNotif(${n.id})" class="px-3 py-1 text-xs font-bold bg-slate-50 text-slate-500 border border-slate-200 rounded-lg hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors">删除</button>` : '') + '</div>';
if (n.type === 'teacher_application' && n.application_status === 'rejected') return '<div class="mt-3 flex items-center gap-2"><span class="inline-flex items-center gap-1.5 px-3 py-1 bg-rose-50 border border-rose-100 text-rose-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> 已拒绝</span>' + (currentUser.role === 'admin' ? `<button onclick="event.stopPropagation();deleteNotif(${n.id})" class="px-3 py-1 text-xs font-bold bg-slate-50 text-slate-500 border border-slate-200 rounded-lg hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors">删除</button>` : '') + '</div>';
if (n.type === 'contest_application' && n.application_status === 'pending' && n.post_id && currentUser.role === 'admin') {
return `<div class="flex gap-3 mt-4 pt-3 border-t border-slate-100">
@@ -218,8 +218,8 @@ function buildActions(n) {
<button onclick="event.stopPropagation();rejectContestN(${n.post_id})" class="px-4 py-1.5 text-xs font-bold bg-rose-50 text-rose-600 border border-rose-200 rounded-lg hover:bg-rose-500 hover:text-white hover:border-rose-500 transition-colors shadow-sm">❌ 拒绝申请</button>
</div>`;
}
if (n.type === 'contest_application' && n.application_status === 'approved') return '<div class="mt-3 inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 border border-emerald-100 text-emerald-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> 已同意</div>';
if (n.type === 'contest_application' && n.application_status === 'rejected') return '<div class="mt-3 inline-flex items-center gap-1.5 px-3 py-1 bg-rose-50 border border-rose-100 text-rose-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> 已拒绝</div>';
if (n.type === 'contest_application' && n.application_status === 'approved') return '<div class="mt-3 flex items-center gap-2"><span class="inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 border border-emerald-100 text-emerald-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> 已同意</span>' + (currentUser.role === 'admin' ? `<button onclick="event.stopPropagation();deleteNotif(${n.id})" class="px-3 py-1 text-xs font-bold bg-slate-50 text-slate-500 border border-slate-200 rounded-lg hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors">删除</button>` : '') + '</div>';
if (n.type === 'contest_application' && n.application_status === 'rejected') return '<div class="mt-3 flex items-center gap-2"><span class="inline-flex items-center gap-1.5 px-3 py-1 bg-rose-50 border border-rose-100 text-rose-600 text-xs font-bold rounded-lg"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> 已拒绝</span>' + (currentUser.role === 'admin' ? `<button onclick="event.stopPropagation();deleteNotif(${n.id})" class="px-3 py-1 text-xs font-bold bg-slate-50 text-slate-500 border border-slate-200 rounded-lg hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors">删除</button>` : '') + '</div>';
// 如果有考试关联的通知,添加快捷入口
if (n.type === 'contest_new_exam' || n.type === 'exam_graded') {
@@ -283,6 +283,20 @@ function markAllRead() {
});
}
async function deleteNotif(nid) {
if (!confirm('确定删除该通知?')) return;
try {
const res = await fetch(`/api/notifications/${nid}`, {method:'DELETE'});
const d = await res.json();
if (d.success) {
allNotifs = allNotifs.filter(n => n.id !== nid);
renderNotifications();
} else {
alert(d.message || '删除失败');
}
} catch(e) { alert('删除失败'); }
}
async function loadAll() {
const [notifRes, annRes] = await Promise.all([
fetch('/api/notifications').then(r => r.json()),