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