mobile app models chat login notifications themes
This commit is contained in:
126
app.py
126
app.py
@@ -33,6 +33,10 @@ app = Flask(__name__)
|
|||||||
app.secret_key = os.urandom(24)
|
app.secret_key = os.urandom(24)
|
||||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
|
|
||||||
|
# Session配置:设置永久session的有效期为10天
|
||||||
|
from datetime import timedelta
|
||||||
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=10)
|
||||||
|
|
||||||
# 内存存储(用于临时验证码)
|
# 内存存储(用于临时验证码)
|
||||||
captcha_store = {}
|
captcha_store = {}
|
||||||
email_codes = {}
|
email_codes = {}
|
||||||
@@ -930,11 +934,21 @@ def api_change_name():
|
|||||||
def apply_contest():
|
def apply_contest():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user_id = session['user']['id']
|
user_id = session['user']['id']
|
||||||
# 防止重复提交:如果已有 pending 状态的申请,不允许再提交
|
|
||||||
existing = ContestApplication.query.filter_by(user_id=user_id, status='pending').first()
|
# 查找该用户的现有申请(不限状态)
|
||||||
|
existing = ContestApplication.query.filter_by(user_id=user_id).first()
|
||||||
|
|
||||||
|
# 如果存在申请,检查拒绝次数和冷却期
|
||||||
if existing:
|
if existing:
|
||||||
flash('您已有一个待审核的杯赛申请,请等待审核结果后再提交新申请')
|
# 检查是否被拒绝5次且在冷却期内
|
||||||
|
if existing.rejection_count >= 5 and existing.last_rejected_at:
|
||||||
|
from datetime import timedelta
|
||||||
|
cooldown_end = existing.last_rejected_at + timedelta(days=30)
|
||||||
|
if datetime.utcnow() < cooldown_end:
|
||||||
|
remaining_days = (cooldown_end - datetime.utcnow()).days + 1
|
||||||
|
flash(f'您的申请已被拒绝5次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||||
return redirect(url_for('contest_list'))
|
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')
|
||||||
@@ -946,6 +960,7 @@ def apply_contest():
|
|||||||
responsible_phone = request.form.get('responsible_phone')
|
responsible_phone = request.form.get('responsible_phone')
|
||||||
responsible_email = request.form.get('responsible_email')
|
responsible_email = request.form.get('responsible_email')
|
||||||
organization = request.form.get('organization')
|
organization = request.form.get('organization')
|
||||||
|
|
||||||
if not all([name, organizer, description, contact, start_date, end_date, total_score,
|
if not all([name, organizer, description, contact, start_date, end_date, total_score,
|
||||||
responsible_person, responsible_phone, responsible_email, organization]):
|
responsible_person, responsible_phone, responsible_email, organization]):
|
||||||
flash('请填写所有必填项')
|
flash('请填写所有必填项')
|
||||||
@@ -953,6 +968,28 @@ def apply_contest():
|
|||||||
if total_score < 1:
|
if total_score < 1:
|
||||||
flash('满分分数必须大于0')
|
flash('满分分数必须大于0')
|
||||||
return redirect(url_for('apply_contest'))
|
return redirect(url_for('apply_contest'))
|
||||||
|
|
||||||
|
# 如果存在申请,更新它;否则创建新申请
|
||||||
|
if existing:
|
||||||
|
# 更新现有申请
|
||||||
|
existing.name = name
|
||||||
|
existing.organizer = organizer
|
||||||
|
existing.description = description
|
||||||
|
existing.contact = contact
|
||||||
|
existing.start_date = start_date
|
||||||
|
existing.end_date = end_date
|
||||||
|
existing.total_score = total_score
|
||||||
|
existing.responsible_person = responsible_person
|
||||||
|
existing.responsible_phone = responsible_phone
|
||||||
|
existing.responsible_email = responsible_email
|
||||||
|
existing.organization = organization
|
||||||
|
existing.status = 'pending'
|
||||||
|
existing.applied_at = datetime.utcnow()
|
||||||
|
existing.reviewed_at = None
|
||||||
|
app = existing
|
||||||
|
flash_msg = '申请已更新,请等待管理员审核'
|
||||||
|
else:
|
||||||
|
# 创建新申请
|
||||||
app = ContestApplication(
|
app = ContestApplication(
|
||||||
user_id=session['user']['id'],
|
user_id=session['user']['id'],
|
||||||
name=name,
|
name=name,
|
||||||
@@ -968,7 +1005,10 @@ def apply_contest():
|
|||||||
organization=organization
|
organization=organization
|
||||||
)
|
)
|
||||||
db.session.add(app)
|
db.session.add(app)
|
||||||
|
flash_msg = '申请已提交,请等待管理员审核'
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 通知所有管理员有新的杯赛申请
|
# 通知所有管理员有新的杯赛申请
|
||||||
admins = User.query.filter_by(role='admin').all()
|
admins = User.query.filter_by(role='admin').all()
|
||||||
for admin_user in admins:
|
for admin_user in admins:
|
||||||
@@ -979,7 +1019,7 @@ def apply_contest():
|
|||||||
from_user=session['user']['name'],
|
from_user=session['user']['name'],
|
||||||
post_id=app.id
|
post_id=app.id
|
||||||
)
|
)
|
||||||
flash('申请已提交,请等待管理员审核')
|
flash(flash_msg)
|
||||||
return redirect(url_for('contest_list'))
|
return redirect(url_for('contest_list'))
|
||||||
return render_template('apply_contest.html')
|
return render_template('apply_contest.html')
|
||||||
|
|
||||||
@@ -1022,6 +1062,8 @@ def approve_contest_application(app_id):
|
|||||||
db.session.add(membership)
|
db.session.add(membership)
|
||||||
app.status = 'approved'
|
app.status = 'approved'
|
||||||
app.reviewed_at = datetime.utcnow()
|
app.reviewed_at = datetime.utcnow()
|
||||||
|
app.rejection_count = 0 # 重置拒绝次数
|
||||||
|
app.last_rejected_at = None # 清除最后拒绝时间
|
||||||
# 自动创建杯赛讨论群
|
# 自动创建杯赛讨论群
|
||||||
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
||||||
creator_id=app.user_id, contest_id=contest.id)
|
creator_id=app.user_id, contest_id=contest.id)
|
||||||
@@ -1044,6 +1086,8 @@ def reject_contest_application(app_id):
|
|||||||
return redirect(url_for('admin_contest_applications'))
|
return redirect(url_for('admin_contest_applications'))
|
||||||
app.status = 'rejected'
|
app.status = 'rejected'
|
||||||
app.reviewed_at = datetime.utcnow()
|
app.reviewed_at = datetime.utcnow()
|
||||||
|
app.rejection_count += 1
|
||||||
|
app.last_rejected_at = datetime.utcnow()
|
||||||
# 通知申请人审核未通过
|
# 通知申请人审核未通过
|
||||||
add_notification(app.user_id, 'contest_result',
|
add_notification(app.user_id, 'contest_result',
|
||||||
f'您申请举办的杯赛「{app.name}」未通过审核。', from_user='系统')
|
f'您申请举办的杯赛「{app.name}」未通过审核。', from_user='系统')
|
||||||
@@ -1287,6 +1331,8 @@ def api_approve_contest(app_id):
|
|||||||
db.session.add(ContestMembership(user_id=ca.user_id, contest_id=contest.id, role='owner'))
|
db.session.add(ContestMembership(user_id=ca.user_id, contest_id=contest.id, role='owner'))
|
||||||
ca.status = 'approved'
|
ca.status = 'approved'
|
||||||
ca.reviewed_at = datetime.utcnow()
|
ca.reviewed_at = datetime.utcnow()
|
||||||
|
ca.rejection_count = 0 # 重置拒绝次数
|
||||||
|
ca.last_rejected_at = None # 清除最后拒绝时间
|
||||||
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
||||||
creator_id=ca.user_id, contest_id=contest.id)
|
creator_id=ca.user_id, contest_id=contest.id)
|
||||||
db.session.add(chatroom)
|
db.session.add(chatroom)
|
||||||
@@ -1298,7 +1344,7 @@ def api_approve_contest(app_id):
|
|||||||
return jsonify({'success': True, 'message': '已批准'})
|
return jsonify({'success': True, 'message': '已批准'})
|
||||||
|
|
||||||
@app.route('/api/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
@app.route('/api/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
||||||
@teacher_required
|
@login_required
|
||||||
def api_approve_teacher(app_id):
|
def api_approve_teacher(app_id):
|
||||||
user = session['user']
|
user = session['user']
|
||||||
ta = TeacherApplication.query.get_or_404(app_id)
|
ta = TeacherApplication.query.get_or_404(app_id)
|
||||||
@@ -1328,6 +1374,8 @@ def api_approve_teacher(app_id):
|
|||||||
ta.status = 'approved'
|
ta.status = 'approved'
|
||||||
ta.reviewed_at = datetime.utcnow()
|
ta.reviewed_at = datetime.utcnow()
|
||||||
ta.reviewed_by = user['id']
|
ta.reviewed_by = user['id']
|
||||||
|
ta.rejection_count = 0 # 重置拒绝次数
|
||||||
|
ta.last_rejected_at = None # 清除最后拒绝时间
|
||||||
|
|
||||||
contest = Contest.query.get(ta.contest_id)
|
contest = Contest.query.get(ta.contest_id)
|
||||||
contest_name = contest.name if contest else ''
|
contest_name = contest.name if contest else ''
|
||||||
@@ -1349,7 +1397,7 @@ def api_approve_teacher(app_id):
|
|||||||
return jsonify({'success': True, 'message': '已批准,邀请码已通过私聊发送给老师'})
|
return jsonify({'success': True, 'message': '已批准,邀请码已通过私聊发送给老师'})
|
||||||
|
|
||||||
@app.route('/api/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
@app.route('/api/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
||||||
@teacher_required
|
@login_required
|
||||||
def api_reject_teacher(app_id):
|
def api_reject_teacher(app_id):
|
||||||
user = session['user']
|
user = session['user']
|
||||||
ta = TeacherApplication.query.get_or_404(app_id)
|
ta = TeacherApplication.query.get_or_404(app_id)
|
||||||
@@ -1362,6 +1410,8 @@ def api_reject_teacher(app_id):
|
|||||||
ta.status = 'rejected'
|
ta.status = 'rejected'
|
||||||
ta.reviewed_at = datetime.utcnow()
|
ta.reviewed_at = datetime.utcnow()
|
||||||
ta.reviewed_by = user['id']
|
ta.reviewed_by = user['id']
|
||||||
|
ta.rejection_count += 1
|
||||||
|
ta.last_rejected_at = datetime.utcnow()
|
||||||
contest = Contest.query.get(ta.contest_id)
|
contest = Contest.query.get(ta.contest_id)
|
||||||
add_notification(ta.user_id, 'teacher_result',
|
add_notification(ta.user_id, 'teacher_result',
|
||||||
f'您申请成为杯赛「{contest.name if contest else ""}」老师未通过审核。',
|
f'您申请成为杯赛「{contest.name if contest else ""}」老师未通过审核。',
|
||||||
@@ -1377,6 +1427,8 @@ def api_reject_contest(app_id):
|
|||||||
return jsonify({'success': False, 'message': '该申请已处理'}), 400
|
return jsonify({'success': False, 'message': '该申请已处理'}), 400
|
||||||
ca.status = 'rejected'
|
ca.status = 'rejected'
|
||||||
ca.reviewed_at = datetime.utcnow()
|
ca.reviewed_at = datetime.utcnow()
|
||||||
|
ca.rejection_count += 1
|
||||||
|
ca.last_rejected_at = datetime.utcnow()
|
||||||
add_notification(ca.user_id, 'contest_result',
|
add_notification(ca.user_id, 'contest_result',
|
||||||
f'您申请举办的杯赛「{ca.name}」未通过审核。', from_user='系统')
|
f'您申请举办的杯赛「{ca.name}」未通过审核。', from_user='系统')
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -1402,11 +1454,6 @@ def apply_teacher():
|
|||||||
return redirect(url_for('apply_teacher'))
|
return redirect(url_for('apply_teacher'))
|
||||||
|
|
||||||
user = session['user']
|
user = session['user']
|
||||||
# 检查是否已经申请过该杯赛且为pending
|
|
||||||
existing = TeacherApplication.query.filter_by(user_id=user['id'], contest_id=contest_id, status='pending').first()
|
|
||||||
if existing:
|
|
||||||
flash('您已提交过该杯赛的申请,请耐心等待审核', 'error')
|
|
||||||
return redirect(url_for('apply_teacher'))
|
|
||||||
|
|
||||||
# 检查是否已经是该杯赛的老师或负责人
|
# 检查是否已经是该杯赛的老师或负责人
|
||||||
membership = ContestMembership.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
membership = ContestMembership.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
||||||
@@ -1414,7 +1461,34 @@ def apply_teacher():
|
|||||||
flash('您已经是该杯赛的老师或负责人', 'error')
|
flash('您已经是该杯赛的老师或负责人', 'error')
|
||||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||||
|
|
||||||
# 创建申请记录
|
# 查找该用户对该杯赛的现有申请
|
||||||
|
existing = TeacherApplication.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
||||||
|
|
||||||
|
# 如果存在申请,检查拒绝次数和冷却期
|
||||||
|
if existing:
|
||||||
|
# 检查是否被拒绝5次且在冷却期内
|
||||||
|
if existing.rejection_count >= 5 and existing.last_rejected_at:
|
||||||
|
from datetime import timedelta
|
||||||
|
cooldown_end = existing.last_rejected_at + timedelta(days=30)
|
||||||
|
if datetime.utcnow() < cooldown_end:
|
||||||
|
remaining_days = (cooldown_end - datetime.utcnow()).days + 1
|
||||||
|
flash(f'您对该杯赛的申请已被拒绝5次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||||
|
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||||
|
|
||||||
|
# 如果存在申请,更新它;否则创建新申请
|
||||||
|
if existing:
|
||||||
|
# 更新现有申请
|
||||||
|
existing.name = name
|
||||||
|
existing.email = email
|
||||||
|
existing.reason = reason
|
||||||
|
existing.status = 'pending'
|
||||||
|
existing.applied_at = datetime.utcnow()
|
||||||
|
existing.reviewed_at = None
|
||||||
|
existing.reviewed_by = None
|
||||||
|
appli = existing
|
||||||
|
flash_msg = '申请已更新,管理员或杯赛负责人会尽快审核'
|
||||||
|
else:
|
||||||
|
# 创建新申请
|
||||||
appli = TeacherApplication(
|
appli = TeacherApplication(
|
||||||
user_id=user['id'],
|
user_id=user['id'],
|
||||||
contest_id=contest_id,
|
contest_id=contest_id,
|
||||||
@@ -1423,6 +1497,8 @@ def apply_teacher():
|
|||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
db.session.add(appli)
|
db.session.add(appli)
|
||||||
|
flash_msg = '申请已提交,管理员或杯赛负责人会尽快审核'
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 通知杯赛负责人
|
# 通知杯赛负责人
|
||||||
@@ -1438,7 +1514,7 @@ def apply_teacher():
|
|||||||
f'用户 {name} 申请成为杯赛「{contest.name}」的老师,请审核。',
|
f'用户 {name} 申请成为杯赛「{contest.name}」的老师,请审核。',
|
||||||
from_user=user['name'], post_id=appli.id)
|
from_user=user['name'], post_id=appli.id)
|
||||||
|
|
||||||
flash('申请已提交,管理员或杯赛负责人会尽快审核', 'success')
|
flash(flash_msg, 'success')
|
||||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||||
|
|
||||||
# GET 请求:显示申请表单,可传入 contest_id 预选
|
# GET 请求:显示申请表单,可传入 contest_id 预选
|
||||||
@@ -1449,7 +1525,7 @@ def apply_teacher():
|
|||||||
|
|
||||||
# ========== 管理后台教师申请审核 ==========
|
# ========== 管理后台教师申请审核 ==========
|
||||||
@app.route('/admin/teacher-applications')
|
@app.route('/admin/teacher-applications')
|
||||||
@teacher_required
|
@login_required
|
||||||
def admin_teacher_applications():
|
def admin_teacher_applications():
|
||||||
user = session['user']
|
user = session['user']
|
||||||
# 管理员可见所有申请,杯赛负责人只能看到自己负责杯赛的申请
|
# 管理员可见所有申请,杯赛负责人只能看到自己负责杯赛的申请
|
||||||
@@ -1468,7 +1544,7 @@ def admin_teacher_applications():
|
|||||||
return render_template('admin_teacher_applications.html', apps=apps)
|
return render_template('admin_teacher_applications.html', apps=apps)
|
||||||
|
|
||||||
@app.route('/admin/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
@app.route('/admin/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
||||||
@teacher_required
|
@login_required
|
||||||
def approve_teacher_application(app_id):
|
def approve_teacher_application(app_id):
|
||||||
user = session['user']
|
user = session['user']
|
||||||
app = TeacherApplication.query.get_or_404(app_id)
|
app = TeacherApplication.query.get_or_404(app_id)
|
||||||
@@ -1504,6 +1580,8 @@ def approve_teacher_application(app_id):
|
|||||||
app.status = 'approved'
|
app.status = 'approved'
|
||||||
app.reviewed_at = datetime.utcnow()
|
app.reviewed_at = datetime.utcnow()
|
||||||
app.reviewed_by = user['id']
|
app.reviewed_by = user['id']
|
||||||
|
app.rejection_count = 0 # 重置拒绝次数
|
||||||
|
app.last_rejected_at = None # 清除最后拒绝时间
|
||||||
|
|
||||||
contest = Contest.query.get(app.contest_id)
|
contest = Contest.query.get(app.contest_id)
|
||||||
contest_name = contest.name if contest else ''
|
contest_name = contest.name if contest else ''
|
||||||
@@ -1528,7 +1606,7 @@ def approve_teacher_application(app_id):
|
|||||||
return redirect(url_for('admin_teacher_applications'))
|
return redirect(url_for('admin_teacher_applications'))
|
||||||
|
|
||||||
@app.route('/admin/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
@app.route('/admin/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
||||||
@teacher_required
|
@login_required
|
||||||
def reject_teacher_application(app_id):
|
def reject_teacher_application(app_id):
|
||||||
user = session['user']
|
user = session['user']
|
||||||
app = TeacherApplication.query.get_or_404(app_id)
|
app = TeacherApplication.query.get_or_404(app_id)
|
||||||
@@ -1546,6 +1624,8 @@ def reject_teacher_application(app_id):
|
|||||||
app.status = 'rejected'
|
app.status = 'rejected'
|
||||||
app.reviewed_at = datetime.utcnow()
|
app.reviewed_at = datetime.utcnow()
|
||||||
app.reviewed_by = user['id']
|
app.reviewed_by = user['id']
|
||||||
|
app.rejection_count += 1
|
||||||
|
app.last_rejected_at = datetime.utcnow()
|
||||||
# 通知申请人
|
# 通知申请人
|
||||||
contest = Contest.query.get(app.contest_id)
|
contest = Contest.query.get(app.contest_id)
|
||||||
add_notification(app.user_id, 'teacher_result',
|
add_notification(app.user_id, 'teacher_result',
|
||||||
@@ -1760,6 +1840,7 @@ def api_login():
|
|||||||
return jsonify({'success': False, 'message': '请求数据格式错误'}), 400
|
return jsonify({'success': False, 'message': '请求数据格式错误'}), 400
|
||||||
email = data.get('email', '')
|
email = data.get('email', '')
|
||||||
password = data.get('password', '')
|
password = data.get('password', '')
|
||||||
|
remember = data.get('remember', False)
|
||||||
|
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
return jsonify({'success': False, 'message': '请输入邮箱和密码'}), 400
|
return jsonify({'success': False, 'message': '请输入邮箱和密码'}), 400
|
||||||
@@ -1772,6 +1853,11 @@ def api_login():
|
|||||||
|
|
||||||
user_data = {'name': user.name, 'email': user.email, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
user_data = {'name': user.name, 'email': user.email, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
||||||
session['user'] = user_data
|
session['user'] = user_data
|
||||||
|
|
||||||
|
# 如果勾选了"保持登录",设置session为永久(10天)
|
||||||
|
if remember:
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': '登录成功', 'user': user_data})
|
return jsonify({'success': True, 'message': '登录成功', 'user': user_data})
|
||||||
|
|
||||||
@app.route('/api/send-sms', methods=['POST'])
|
@app.route('/api/send-sms', methods=['POST'])
|
||||||
@@ -1837,6 +1923,7 @@ def api_verify_code():
|
|||||||
data = request.get_json(force=True, silent=True)
|
data = request.get_json(force=True, silent=True)
|
||||||
phone = data.get('phone', '')
|
phone = data.get('phone', '')
|
||||||
code = data.get('code', '')
|
code = data.get('code', '')
|
||||||
|
remember = data.get('remember', False)
|
||||||
|
|
||||||
if not phone or not code:
|
if not phone or not code:
|
||||||
return jsonify({'success': False, 'message': '手机号和验证码不能为空'}), 400
|
return jsonify({'success': False, 'message': '手机号和验证码不能为空'}), 400
|
||||||
@@ -1870,6 +1957,11 @@ def api_verify_code():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
user_data = {'name': user.name, 'phone': user.phone, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
user_data = {'name': user.name, 'phone': user.phone, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
||||||
session['user'] = user_data
|
session['user'] = user_data
|
||||||
|
|
||||||
|
# 如果勾选了"保持登录",设置session为永久(10天)
|
||||||
|
if remember:
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': '验证成功', 'user': user_data})
|
return jsonify({'success': True, 'message': '验证成功', 'user': user_data})
|
||||||
else:
|
else:
|
||||||
msg = '验证码已过期' if model.get('VerifyResult') == 'UNKNOWN' else '验证码错误'
|
msg = '验证码已过期' if model.get('VerifyResult') == 'UNKNOWN' else '验证码错误'
|
||||||
@@ -2809,7 +2901,7 @@ def api_add_question_bank(contest_id):
|
|||||||
@app.route('/api/contests/<int:contest_id>/question-bank/<int:qid>', methods=['DELETE'])
|
@app.route('/api/contests/<int:contest_id>/question-bank/<int:qid>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_delete_question_bank(contest_id, qid):
|
def api_delete_question_bank(contest_id, qid):
|
||||||
"""删除题库题目(负责人或题目贡献者)"""
|
"""删除题库题目(负责人可删除任何题目,老师只能删除自己贡献的题目)"""
|
||||||
user = session['user']
|
user = session['user']
|
||||||
item = QuestionBankItem.query.get(qid)
|
item = QuestionBankItem.query.get(qid)
|
||||||
if not item or item.contest_id != contest_id:
|
if not item or item.contest_id != contest_id:
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class ContestApplication(db.Model):
|
|||||||
responsible_phone = db.Column(db.String(20)) # 责任人电话
|
responsible_phone = db.Column(db.String(20)) # 责任人电话
|
||||||
responsible_email = db.Column(db.String(120)) # 责任人邮箱
|
responsible_email = db.Column(db.String(120)) # 责任人邮箱
|
||||||
organization = db.Column(db.String(100)) # 所属机构/学校
|
organization = db.Column(db.String(100)) # 所属机构/学校
|
||||||
|
# 拒绝次数控制
|
||||||
|
rejection_count = db.Column(db.Integer, default=0) # 被拒绝次数
|
||||||
|
last_rejected_at = db.Column(db.DateTime) # 最后一次被拒绝的时间
|
||||||
|
|
||||||
user = db.relationship('User', backref='contest_applications')
|
user = db.relationship('User', backref='contest_applications')
|
||||||
|
|
||||||
@@ -361,6 +364,9 @@ class TeacherApplication(db.Model):
|
|||||||
applied_at = db.Column(db.DateTime, default=datetime.utcnow)
|
applied_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
reviewed_at = db.Column(db.DateTime)
|
reviewed_at = db.Column(db.DateTime)
|
||||||
reviewed_by = db.Column(db.Integer, db.ForeignKey('user.id'))
|
reviewed_by = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
# 拒绝次数控制
|
||||||
|
rejection_count = db.Column(db.Integer, default=0) # 被拒绝次数
|
||||||
|
last_rejected_at = db.Column(db.DateTime) # 最后一次被拒绝的时间
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
user = db.relationship('User', foreign_keys=[user_id], backref='teacher_applications')
|
user = db.relationship('User', foreign_keys=[user_id], backref='teacher_applications')
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 创建群聊弹窗 -->
|
<!-- 创建群聊弹窗 -->
|
||||||
<div id="createGroupModal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
<div id="createGroupModal" class="fixed inset-0 bg-black/50 z-[9990] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-96 max-h-[80vh] flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-96 max-h-[80vh] flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">创建群聊</h3>
|
<h3 class="font-semibold">创建群聊</h3>
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 成员列表弹窗 -->
|
<!-- 成员列表弹窗 -->
|
||||||
<div id="membersModal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
<div id="membersModal" class="fixed inset-0 bg-black/50 z-[9990] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-[420px] max-h-[80vh] flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-[420px] max-h-[80vh] flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">群成员</h3>
|
<h3 class="font-semibold">群成员</h3>
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群公告弹窗 -->
|
<!-- 群公告弹窗 -->
|
||||||
<div id="announcementModal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
<div id="announcementModal" class="fixed inset-0 bg-black/50 z-[9990] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-96 flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-96 flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">群公告</h3>
|
<h3 class="font-semibold">群公告</h3>
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索聊天记录弹窗 -->
|
<!-- 搜索聊天记录弹窗 -->
|
||||||
<div id="searchModal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
<div id="searchModal" class="fixed inset-0 bg-black/50 z-[9990] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-[480px] max-h-[80vh] flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-[480px] max-h-[80vh] flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">搜索聊天记录</h3>
|
<h3 class="font-semibold">搜索聊天记录</h3>
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群文件弹窗 -->
|
<!-- 群文件弹窗 -->
|
||||||
<div id="fileListModal" class="fixed inset-0 bg-black/50 z-[9990] hidden flex items-center justify-center">
|
<div id="fileListModal" class="fixed inset-0 bg-black/50 z-[9990] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-[480px] max-h-[80vh] flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-[480px] max-h-[80vh] flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">群文件</h3>
|
<h3 class="font-semibold">群文件</h3>
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群昵称弹窗 -->
|
<!-- 群昵称弹窗 -->
|
||||||
<div id="nicknameModal" class="fixed inset-0 bg-black/50 z-[9991] hidden flex items-center justify-center">
|
<div id="nicknameModal" class="fixed inset-0 bg-black/50 z-[9991] hidden items-center justify-center">
|
||||||
<div class="bg-white rounded-xl shadow-xl w-80 flex flex-col">
|
<div class="bg-white rounded-xl shadow-xl w-80 flex flex-col">
|
||||||
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
<div class="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||||||
<h3 class="font-semibold">设置群昵称</h3>
|
<h3 class="font-semibold">设置群昵称</h3>
|
||||||
@@ -934,7 +934,9 @@ function markRead(roomId) {
|
|||||||
|
|
||||||
// ========== 创建群聊 ==========
|
// ========== 创建群聊 ==========
|
||||||
async function showCreateGroup() {
|
async function showCreateGroup() {
|
||||||
document.getElementById('createGroupModal').classList.remove('hidden');
|
const modal = document.getElementById('createGroupModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
// 重置弹窗标题和按钮(防止被邀请好友功能覆盖)
|
// 重置弹窗标题和按钮(防止被邀请好友功能覆盖)
|
||||||
document.querySelector('#createGroupModal h3').textContent = '创建群聊';
|
document.querySelector('#createGroupModal h3').textContent = '创建群聊';
|
||||||
const createBtn = document.querySelector('#createGroupModal .bg-primary');
|
const createBtn = document.querySelector('#createGroupModal .bg-primary');
|
||||||
@@ -959,7 +961,11 @@ async function showCreateGroup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideCreateGroup() { document.getElementById('createGroupModal').classList.add('hidden'); }
|
function hideCreateGroup() {
|
||||||
|
const modal = document.getElementById('createGroupModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
async function createGroup() {
|
async function createGroup() {
|
||||||
const name = document.getElementById('groupName').value.trim();
|
const name = document.getElementById('groupName').value.trim();
|
||||||
@@ -983,7 +989,9 @@ async function createGroup() {
|
|||||||
// ========== 成员列表 ==========
|
// ========== 成员列表 ==========
|
||||||
async function showMembers() {
|
async function showMembers() {
|
||||||
if (!currentRoomId) return;
|
if (!currentRoomId) return;
|
||||||
document.getElementById('membersModal').classList.remove('hidden');
|
const modal = document.getElementById('membersModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
const res = await fetch(`/api/chat/rooms/${currentRoomId}/members`);
|
const res = await fetch(`/api/chat/rooms/${currentRoomId}/members`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.success) return;
|
if (!data.success) return;
|
||||||
@@ -1026,7 +1034,11 @@ async function showMembers() {
|
|||||||
document.getElementById('inviteSection').classList.toggle('hidden', !isGroup);
|
document.getElementById('inviteSection').classList.toggle('hidden', !isGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideMembers() { document.getElementById('membersModal').classList.add('hidden'); }
|
function hideMembers() {
|
||||||
|
const modal = document.getElementById('membersModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
async function showInvite() {
|
async function showInvite() {
|
||||||
hideMembers();
|
hideMembers();
|
||||||
@@ -1082,7 +1094,9 @@ async function transferOwner(uid, name) {
|
|||||||
// ========== 群公告 ==========
|
// ========== 群公告 ==========
|
||||||
async function showAnnouncement() {
|
async function showAnnouncement() {
|
||||||
if (!currentRoomId) return;
|
if (!currentRoomId) return;
|
||||||
document.getElementById('announcementModal').classList.remove('hidden');
|
const modal = document.getElementById('announcementModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
const res = await fetch(`/api/chat/rooms/${currentRoomId}/announcement`);
|
const res = await fetch(`/api/chat/rooms/${currentRoomId}/announcement`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -1103,7 +1117,11 @@ async function showAnnouncement() {
|
|||||||
}
|
}
|
||||||
document.getElementById('btnEditAnnouncement').classList.toggle('hidden', myRoomRole !== 'admin');
|
document.getElementById('btnEditAnnouncement').classList.toggle('hidden', myRoomRole !== 'admin');
|
||||||
}
|
}
|
||||||
function hideAnnouncement() { document.getElementById('announcementModal').classList.add('hidden'); }
|
function hideAnnouncement() {
|
||||||
|
const modal = document.getElementById('announcementModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
function editAnnouncement() {
|
function editAnnouncement() {
|
||||||
document.getElementById('announcementEditSection').classList.remove('hidden');
|
document.getElementById('announcementEditSection').classList.remove('hidden');
|
||||||
document.getElementById('btnEditAnnouncement').classList.add('hidden');
|
document.getElementById('btnEditAnnouncement').classList.add('hidden');
|
||||||
@@ -1125,8 +1143,20 @@ async function saveAnnouncement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 搜索聊天记录 ==========
|
// ========== 搜索聊天记录 ==========
|
||||||
function showSearchPanel() { if (!currentRoomId) return; document.getElementById('searchModal').classList.remove('hidden'); document.getElementById('msgSearchInput').value = ''; document.getElementById('searchResults').innerHTML = ''; document.getElementById('msgSearchInput').focus(); }
|
function showSearchPanel() {
|
||||||
function hideSearchPanel() { document.getElementById('searchModal').classList.add('hidden'); }
|
if (!currentRoomId) return;
|
||||||
|
const modal = document.getElementById('searchModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
document.getElementById('msgSearchInput').value = '';
|
||||||
|
document.getElementById('searchResults').innerHTML = '';
|
||||||
|
document.getElementById('msgSearchInput').focus();
|
||||||
|
}
|
||||||
|
function hideSearchPanel() {
|
||||||
|
const modal = document.getElementById('searchModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
function debounceSearch() { clearTimeout(searchTimer); searchTimer = setTimeout(doSearch, 300); }
|
function debounceSearch() { clearTimeout(searchTimer); searchTimer = setTimeout(doSearch, 300); }
|
||||||
async function doSearch() {
|
async function doSearch() {
|
||||||
const q = document.getElementById('msgSearchInput').value.trim();
|
const q = document.getElementById('msgSearchInput').value.trim();
|
||||||
@@ -1154,7 +1184,9 @@ function scrollToMsg(msgId) {
|
|||||||
// ========== 群文件 ==========
|
// ========== 群文件 ==========
|
||||||
async function showFileList() {
|
async function showFileList() {
|
||||||
if (!currentRoomId) return;
|
if (!currentRoomId) return;
|
||||||
document.getElementById('fileListModal').classList.remove('hidden');
|
const modal = document.getElementById('fileListModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
const res = await fetch(`/api/chat/rooms/${currentRoomId}/files`);
|
const res = await fetch(`/api/chat/rooms/${currentRoomId}/files`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const container = document.getElementById('fileListContent');
|
const container = document.getElementById('fileListContent');
|
||||||
@@ -1173,11 +1205,25 @@ async function showFileList() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
function hideFileList() { document.getElementById('fileListModal').classList.add('hidden'); }
|
function hideFileList() {
|
||||||
|
const modal = document.getElementById('fileListModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 群昵称 ==========
|
// ========== 群昵称 ==========
|
||||||
function showNicknameModal() { document.getElementById('nicknameModal').classList.remove('hidden'); const me = currentRoomMembers.find(m => m.id === currentUser.id); document.getElementById('nicknameInput').value = me?.nickname || ''; }
|
function showNicknameModal() {
|
||||||
function hideNicknameModal() { document.getElementById('nicknameModal').classList.add('hidden'); }
|
const modal = document.getElementById('nicknameModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
const me = currentRoomMembers.find(m => m.id === currentUser.id);
|
||||||
|
document.getElementById('nicknameInput').value = me?.nickname || '';
|
||||||
|
}
|
||||||
|
function hideNicknameModal() {
|
||||||
|
const modal = document.getElementById('nicknameModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}
|
||||||
async function saveNickname() {
|
async function saveNickname() {
|
||||||
const nickname = document.getElementById('nicknameInput').value.trim();
|
const nickname = document.getElementById('nicknameInput').value.trim();
|
||||||
const res = await fetch(`/api/chat/rooms/${currentRoomId}/nickname`, {
|
const res = await fetch(`/api/chat/rooms/${currentRoomId}/nickname`, {
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
<button type="button" id="send-sms-btn" onclick="handleSendSms()" class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-medium rounded-r-md text-slate-700 bg-slate-50 hover:bg-slate-100">获取验证码</button>
|
<button type="button" id="send-sms-btn" onclick="handleSendSms()" class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-medium rounded-r-md text-slate-700 bg-slate-50 hover:bg-slate-100">获取验证码</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="remember-phone" type="checkbox" class="h-4 w-4 text-primary focus:ring-primary border-slate-300 rounded">
|
||||||
|
<label for="remember-phone" class="ml-2 block text-sm text-slate-700">保持登录10天</label>
|
||||||
|
</div>
|
||||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-blue-700">登录</button>
|
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-blue-700">登录</button>
|
||||||
</form>
|
</form>
|
||||||
<!-- 邮箱登录表单 -->
|
<!-- 邮箱登录表单 -->
|
||||||
@@ -52,6 +56,10 @@
|
|||||||
<label class="block text-sm font-medium text-slate-700">密码</label>
|
<label class="block text-sm font-medium text-slate-700">密码</label>
|
||||||
<input id="login-password" type="password" required class="mt-1 focus:ring-primary focus:border-primary block w-full pl-3 sm:text-sm border-slate-300 rounded-md py-2 border" placeholder="请输入密码">
|
<input id="login-password" type="password" required class="mt-1 focus:ring-primary focus:border-primary block w-full pl-3 sm:text-sm border-slate-300 rounded-md py-2 border" placeholder="请输入密码">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="remember-email" type="checkbox" class="h-4 w-4 text-primary focus:ring-primary border-slate-300 rounded">
|
||||||
|
<label for="remember-email" class="ml-2 block text-sm text-slate-700">保持登录10天</label>
|
||||||
|
</div>
|
||||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-blue-700">登录</button>
|
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-blue-700">登录</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-6 relative">
|
<div class="mt-6 relative">
|
||||||
@@ -112,8 +120,9 @@ async function handlePhoneLogin(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const phone = document.getElementById('phone').value;
|
const phone = document.getElementById('phone').value;
|
||||||
const code = document.getElementById('sms-code').value;
|
const code = document.getElementById('sms-code').value;
|
||||||
|
const remember = document.getElementById('remember-phone').checked;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/verify-code', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone, code}) });
|
const res = await fetch('/api/verify-code', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({phone, code, remember}) });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) { window.location.href = '/'; }
|
if (data.success) { window.location.href = '/'; }
|
||||||
else alert(data.message);
|
else alert(data.message);
|
||||||
@@ -124,8 +133,9 @@ async function handleEmailLogin(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = document.getElementById('login-email').value;
|
const email = document.getElementById('login-email').value;
|
||||||
const password = document.getElementById('login-password').value;
|
const password = document.getElementById('login-password').value;
|
||||||
|
const remember = document.getElementById('remember-email').checked;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email, password}) });
|
const res = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email, password, remember}) });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) { window.location.href = '/'; }
|
if (data.success) { window.location.href = '/'; }
|
||||||
else alert(data.message);
|
else alert(data.message);
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ function renderNotifications() {
|
|||||||
'system_announcement': 'bg-blue-100 text-blue-600', 'friend_request': 'bg-cyan-100 text-cyan-600'
|
'system_announcement': 'bg-blue-100 text-blue-600', 'friend_request': 'bg-cyan-100 text-cyan-600'
|
||||||
}[n.type] || 'bg-slate-100 text-slate-600';
|
}[n.type] || 'bg-slate-100 text-slate-600';
|
||||||
|
|
||||||
return `<div class="bg-white border ${n.read ? 'border-slate-100' : 'border-indigo-200 shadow-md shadow-indigo-100/50 relative'} rounded-2xl p-4 sm:p-5 hover:border-indigo-300 hover:shadow-lg transition-all duration-300 cursor-pointer group" onclick="markSingleRead(${n.id}, this)">
|
return `<div class="bg-white border ${n.read ? 'border-slate-100' : 'border-indigo-200 shadow-md shadow-indigo-100/50 relative'} rounded-2xl p-4 sm:p-5 hover:border-indigo-300 hover:shadow-lg transition-all duration-300 cursor-pointer group" onclick="markSingleRead(${n.id}, this, ${JSON.stringify(n).replace(/"/g, '"')})">
|
||||||
${!n.read ? '<div class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white animate-pulse"></div>' : ''}
|
${!n.read ? '<div class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white animate-pulse"></div>' : ''}
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div class="w-12 h-12 rounded-2xl ${iconBgColor} flex items-center justify-center text-2xl flex-shrink-0 shadow-inner group-hover:scale-110 group-hover:rotate-6 transition-transform">
|
<div class="w-12 h-12 rounded-2xl ${iconBgColor} flex items-center justify-center text-2xl flex-shrink-0 shadow-inner group-hover:scale-110 group-hover:rotate-6 transition-transform">
|
||||||
@@ -205,12 +205,19 @@ function renderNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildActions(n) {
|
function buildActions(n) {
|
||||||
if (n.type === 'teacher_application' && n.application_status === 'pending' && n.application_id && currentUser.role === 'admin') {
|
if (n.type === 'teacher_application' && n.application_status === 'pending' && n.application_id) {
|
||||||
|
// 管理员显示快捷操作按钮
|
||||||
|
if (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">
|
||||||
<button onclick="event.stopPropagation();approveTeacherN(${n.application_id})" class="px-4 py-1.5 text-xs font-bold bg-emerald-50 text-emerald-600 border border-emerald-200 rounded-lg hover:bg-emerald-500 hover:text-white hover:border-emerald-500 transition-colors shadow-sm">✅ 同意申请</button>
|
<button onclick="event.stopPropagation();approveTeacherN(${n.application_id})" class="px-4 py-1.5 text-xs font-bold bg-emerald-50 text-emerald-600 border border-emerald-200 rounded-lg hover:bg-emerald-500 hover:text-white hover:border-emerald-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>
|
<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>`;
|
||||||
}
|
}
|
||||||
|
// 杯赛负责人显示查看按钮
|
||||||
|
return `<div class="flex gap-3 mt-4 pt-3 border-t border-slate-100">
|
||||||
|
<a href="/admin/teacher-applications" onclick="event.stopPropagation()" class="px-4 py-1.5 text-xs font-bold bg-indigo-50 text-indigo-600 border border-indigo-200 rounded-lg hover:bg-indigo-500 hover:text-white hover:border-indigo-500 transition-colors shadow-sm">👁️ 查看申请</a>
|
||||||
|
</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 === '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 === '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>';
|
||||||
|
|
||||||
@@ -294,7 +301,7 @@ async function rejectFriendN(reqId) {
|
|||||||
} catch(e) { alert('操作失败'); }
|
} catch(e) { alert('操作失败'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function markSingleRead(nid, el) {
|
function markSingleRead(nid, el, notif) {
|
||||||
fetch(`/api/notifications/${nid}/read`, {method:'POST'});
|
fetch(`/api/notifications/${nid}/read`, {method:'POST'});
|
||||||
// 移除未读的特定样式
|
// 移除未读的特定样式
|
||||||
el.classList.remove('border-indigo-200', 'shadow-md', 'shadow-indigo-100/50');
|
el.classList.remove('border-indigo-200', 'shadow-md', 'shadow-indigo-100/50');
|
||||||
@@ -303,6 +310,11 @@ function markSingleRead(nid, el) {
|
|||||||
if(content) content.classList.remove('font-bold');
|
if(content) content.classList.remove('font-bold');
|
||||||
const dot = el.querySelector('.bg-red-500.animate-pulse');
|
const dot = el.querySelector('.bg-red-500.animate-pulse');
|
||||||
if (dot) dot.remove();
|
if (dot) dot.remove();
|
||||||
|
|
||||||
|
// 根据通知类型跳转
|
||||||
|
if (notif && notif.type === 'teacher_application' && notif.application_status === 'pending') {
|
||||||
|
window.location.href = '/admin/teacher-applications';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function markAllRead() {
|
function markAllRead() {
|
||||||
|
|||||||
Reference in New Issue
Block a user