mobile app apply_contest contest_detail contest_edit notifications theme
This commit is contained in:
19
app.py
19
app.py
@@ -914,6 +914,12 @@ def api_change_name():
|
|||||||
@login_required
|
@login_required
|
||||||
def apply_contest():
|
def apply_contest():
|
||||||
if request.method == 'POST':
|
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')
|
name = request.form.get('name')
|
||||||
organizer = request.form.get('organizer')
|
organizer = request.form.get('organizer')
|
||||||
description = request.form.get('description')
|
description = request.form.get('description')
|
||||||
@@ -1136,6 +1142,19 @@ def api_mark_all_notifications_read():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'success': True})
|
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')
|
@app.route('/notifications')
|
||||||
@login_required
|
@login_required
|
||||||
def notifications_page():
|
def notifications_page():
|
||||||
|
|||||||
@@ -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 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>
|
</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">
|
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>
|
</button>
|
||||||
@@ -96,4 +96,15 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
@@ -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 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>
|
||||||
|
<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 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>
|
</a>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</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>
|
<h2 class="text-lg font-semibold text-slate-900 mb-4">往届真题管理</h2>
|
||||||
<div id="papers-list" class="space-y-2 mb-6">
|
<div id="papers-list" class="space-y-2 mb-6">
|
||||||
{% for paper in contest.get_past_papers() %}
|
{% for paper in contest.get_past_papers() %}
|
||||||
|
|||||||
@@ -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>
|
<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>`;
|
</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 === '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 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 === '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') {
|
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">
|
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>
|
<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>`;
|
</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 === '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 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 === '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') {
|
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() {
|
async function loadAll() {
|
||||||
const [notifRes, annRes] = await Promise.all([
|
const [notifRes, annRes] = await Promise.all([
|
||||||
fetch('/api/notifications').then(r => r.json()),
|
fetch('/api/notifications').then(r => r.json()),
|
||||||
|
|||||||
Reference in New Issue
Block a user