Compare commits
2 Commits
7976f11cf4
...
4cd4b6ef52
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cd4b6ef52 | |||
| 17a4415cd2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ venv/
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
model/
|
||||
20
app.py
20
app.py
@@ -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():
|
||||
@@ -1789,7 +1808,6 @@ def api_send_sms():
|
||||
resp_data = {'success': True, 'message': '验证码发送成功'}
|
||||
model = result.get('Model', {})
|
||||
if model.get('VerifyCode'):
|
||||
resp_data['mockCode'] = model['VerifyCode']
|
||||
print(f'验证码: {model["VerifyCode"]}')
|
||||
return jsonify(resp_data)
|
||||
else:
|
||||
|
||||
@@ -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 %}
|
||||
@@ -922,6 +922,12 @@ function markRead(roomId) {
|
||||
// ========== 创建群聊 ==========
|
||||
async function showCreateGroup() {
|
||||
document.getElementById('createGroupModal').classList.remove('hidden');
|
||||
// 重置弹窗标题和按钮(防止被邀请好友功能覆盖)
|
||||
document.querySelector('#createGroupModal h3').textContent = '创建群聊';
|
||||
const createBtn = document.querySelector('#createGroupModal .bg-primary');
|
||||
createBtn.textContent = '创建';
|
||||
createBtn.onclick = createGroup;
|
||||
document.getElementById('groupName').value = '';
|
||||
const res = await fetch('/api/user/friends');
|
||||
const data = await res.json();
|
||||
const list = document.getElementById('friendListForGroup');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() %}
|
||||
|
||||
@@ -102,8 +102,7 @@ async function handleSendSms() {
|
||||
if (data.success) {
|
||||
countdown = 60;
|
||||
countdownTimer = setInterval(() => { countdown--; btn.textContent = countdown > 0 ? countdown+'s后重发' : '获取验证码'; if(countdown<=0){clearInterval(countdownTimer);btn.disabled=false;} }, 1000);
|
||||
if (data.mockCode) alert('验证码已发送: ' + data.mockCode);
|
||||
else alert('验证码已发送');
|
||||
alert('验证码已发送');
|
||||
} else { alert(data.message); btn.disabled = false; if(data.refreshCaptcha) fetchCaptcha('phone'); }
|
||||
} catch(e) { alert('发送失败,请确保后端已启动'); btn.disabled = false; }
|
||||
fetchCaptcha('phone');
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -148,8 +148,7 @@ async function handleSendSmsPhone() {
|
||||
btn.textContent = countdown > 0 ? countdown+'s后重发' : '获取验证码';
|
||||
if(countdown<=0){clearInterval(timer);btn.disabled=false;}
|
||||
}, 1000);
|
||||
if (data.mockCode) alert('验证码已发送: ' + data.mockCode);
|
||||
else alert('验证码已发送');
|
||||
alert('验证码已发送');
|
||||
} else {
|
||||
alert(data.message);
|
||||
btn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user