modified app chat forum theme

This commit is contained in:
2026-02-27 14:48:37 +08:00
parent 3449f01500
commit 7976f11cf4
3 changed files with 70 additions and 6 deletions

8
app.py
View File

@@ -704,9 +704,10 @@ def api_add_friend():
return jsonify({'success': False, 'message': '已经是好友或已发送请求'}), 400 return jsonify({'success': False, 'message': '已经是好友或已发送请求'}), 400
friend_req = Friend(user_id=user_id, friend_id=friend_id, status='pending') friend_req = Friend(user_id=user_id, friend_id=friend_id, status='pending')
db.session.add(friend_req) db.session.add(friend_req)
db.session.flush()
# 发送通知给对方 # 发送通知给对方
sender_name = session['user'].get('name', '未知用户') sender_name = session['user'].get('name', '未知用户')
notif = Notification(user_id=friend_id, type='friend_request', content=f'{sender_name} 请求添加你为好友', from_user=sender_name) notif = Notification(user_id=friend_id, type='friend_request', content=f'{sender_name} 请求添加你为好友', from_user=sender_name, post_id=friend_req.id)
db.session.add(notif) db.session.add(notif)
db.session.commit() db.session.commit()
return jsonify({'success': True, 'message': '好友请求已发送'}) return jsonify({'success': True, 'message': '好友请求已发送'})
@@ -1104,6 +1105,11 @@ def api_notifications():
item['contest_name'] = contest.name if contest else '' item['contest_name'] = contest.name if contest else ''
applicant = User.query.get(ta.user_id) applicant = User.query.get(ta.user_id)
item['applicant_name'] = applicant.name if applicant else '' item['applicant_name'] = applicant.name if applicant else ''
if n.type == 'friend_request' and n.post_id:
fr = Friend.query.get(n.post_id)
if fr:
item['application_status'] = fr.status
item['friend_request_id'] = fr.id
result.append(item) result.append(item)
return jsonify({'success': True, 'notifications': result}) return jsonify({'success': True, 'notifications': result})

View File

@@ -384,17 +384,20 @@ function renderNotifications() {
list.innerHTML = notifData.map(n => { list.innerHTML = notifData.map(n => {
const isContestApp = n.type === 'contest_application'; const isContestApp = n.type === 'contest_application';
const isTeacherApp = n.type === 'teacher_application'; const isTeacherApp = n.type === 'teacher_application';
const isFriendReq = n.type === 'friend_request';
const isResult = n.type === 'contest_result' || n.type === 'teacher_result'; const isResult = n.type === 'contest_result' || n.type === 'teacher_result';
const isNewExam = n.type === 'contest_new_exam'; const isNewExam = n.type === 'contest_new_exam';
const isGraded = n.type === 'exam_graded'; const isGraded = n.type === 'exam_graded';
const isSystem = n.type === 'system_announcement'; const isSystem = n.type === 'system_announcement';
const isPendingContest = isContestApp && n.application_status === 'pending'; const isPendingContest = isContestApp && n.application_status === 'pending';
const isPendingTeacher = isTeacherApp && n.application_status === 'pending'; const isPendingTeacher = isTeacherApp && n.application_status === 'pending';
const isPendingFriend = isFriendReq && n.application_status === 'pending';
let statusHtml = ''; let statusHtml = '';
if ((isContestApp || isTeacherApp) && n.application_status === 'approved') statusHtml = '<span class="text-xs text-green-600 font-medium">已批准</span>'; if ((isContestApp || isTeacherApp) && n.application_status === 'approved') statusHtml = '<span class="text-xs text-green-600 font-medium">已批准</span>';
else if ((isContestApp || isTeacherApp) && n.application_status === 'rejected') statusHtml = '<span class="text-xs text-red-600 font-medium">已拒绝</span>'; else if ((isContestApp || isTeacherApp) && n.application_status === 'rejected') statusHtml = '<span class="text-xs text-red-600 font-medium">已拒绝</span>';
const icon = isContestApp ? '📋' : isTeacherApp ? '👨‍🏫' : isResult ? '📢' : isNewExam ? '📝' : isGraded ? '✅' : isSystem ? '📢' : '🔔'; else if (isFriendReq && n.application_status === 'accepted') statusHtml = '<span class="text-xs text-green-600 font-medium">已同意</span>';
const iconBg = isContestApp ? 'bg-orange-100' : isTeacherApp ? 'bg-purple-100' : isResult ? 'bg-green-100' : isNewExam ? 'bg-indigo-100' : isGraded ? 'bg-emerald-100' : isSystem ? 'bg-amber-100' : 'bg-blue-100'; const icon = isContestApp ? '📋' : isTeacherApp ? '👨‍🏫' : isFriendReq ? '👤' : isResult ? '📢' : isNewExam ? '📝' : isGraded ? '' : isSystem ? '📢' : '🔔';
const iconBg = isContestApp ? 'bg-orange-100' : isTeacherApp ? 'bg-purple-100' : isFriendReq ? 'bg-blue-100' : isResult ? 'bg-green-100' : isNewExam ? 'bg-indigo-100' : isGraded ? 'bg-emerald-100' : isSystem ? 'bg-amber-100' : 'bg-blue-100';
const clickAction = `showNotifDetail(${n.id})`; const clickAction = `showNotifDetail(${n.id})`;
let actionsHtml = ''; let actionsHtml = '';
if (isPendingContest && currentUser.role === 'admin') { if (isPendingContest && currentUser.role === 'admin') {
@@ -409,6 +412,12 @@ function renderNotifications() {
<button onclick="event.stopPropagation();rejectTeacher(${n.application_id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">拒绝</button> <button onclick="event.stopPropagation();rejectTeacher(${n.application_id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">拒绝</button>
</div>`; </div>`;
} }
if (isPendingFriend && n.friend_request_id) {
actionsHtml = `<div class="flex gap-2 mt-2">
<button onclick="event.stopPropagation();acceptFriendReq(${n.friend_request_id},${n.id})" class="px-3 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600">同意</button>
<button onclick="event.stopPropagation();rejectFriendReq(${n.friend_request_id},${n.id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">拒绝</button>
</div>`;
}
return `<div class="px-3 py-3 ${n.read ? '' : 'bg-blue-50'} hover:bg-slate-50 border-b border-slate-100 transition cursor-pointer" onclick="${clickAction}"> return `<div class="px-3 py-3 ${n.read ? '' : 'bg-blue-50'} hover:bg-slate-50 border-b border-slate-100 transition cursor-pointer" onclick="${clickAction}">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<div class="w-8 h-8 rounded-full ${iconBg} flex items-center justify-center flex-shrink-0 mt-0.5"> <div class="w-8 h-8 rounded-full ${iconBg} flex items-center justify-center flex-shrink-0 mt-0.5">
@@ -479,6 +488,26 @@ async function rejectTeacher(appId) {
} }
} }
async function acceptFriendReq(reqId, notifId) {
const res = await fetch(`/api/friend/accept/${reqId}`, { method: 'POST' });
const data = await res.json();
if (data.success) {
loadNotifications();
} else {
alert(data.message || '操作失败');
}
}
async function rejectFriendReq(reqId, notifId) {
const res = await fetch(`/api/friend/reject/${reqId}`, { method: 'POST' });
const data = await res.json();
if (data.success) {
loadNotifications();
} else {
alert(data.message || '操作失败');
}
}
async function checkUnreadNotifs() { async function checkUnreadNotifs() {
const res = await fetch('/api/notifications/unread-count'); const res = await fetch('/api/notifications/unread-count');
const data = await res.json(); const data = await res.json();
@@ -517,13 +546,13 @@ function showNotifDetail(nid) {
'teacher_application': '教师申请', 'teacher_result': '教师审核结果', 'teacher_application': '教师申请', 'teacher_result': '教师审核结果',
'contest_application': '杯赛申请', 'contest_result': '杯赛通知', 'contest_application': '杯赛申请', 'contest_result': '杯赛通知',
'contest_new_exam': '新考试', 'exam_graded': '成绩通知', 'contest_new_exam': '新考试', 'exam_graded': '成绩通知',
'system_announcement': '系统通知' 'system_announcement': '系统通知', 'friend_request': '好友申请'
}; };
const typeIcons = { const typeIcons = {
'teacher_application': '👨‍🏫', 'teacher_result': '🎓', 'teacher_application': '👨‍🏫', 'teacher_result': '🎓',
'contest_application': '📋', 'contest_result': '🏅', 'contest_application': '📋', 'contest_result': '🏅',
'contest_new_exam': '📝', 'exam_graded': '✅', 'contest_new_exam': '📝', 'exam_graded': '✅',
'system_announcement': '📢' 'system_announcement': '📢', 'friend_request': '👤'
}; };
document.getElementById('notifDetailIcon').textContent = typeIcons[n.type] || '🔔'; document.getElementById('notifDetailIcon').textContent = typeIcons[n.type] || '🔔';
@@ -546,6 +575,13 @@ function showNotifDetail(nid) {
</div>`; </div>`;
} else if (n.type === 'contest_new_exam' || n.type === 'exam_graded') { } else if (n.type === 'contest_new_exam' || n.type === 'exam_graded') {
actHtml = `<a href="/exams/${n.post_id}" class="inline-block px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-blue-700">查看考试</a>`; actHtml = `<a href="/exams/${n.post_id}" class="inline-block px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-blue-700">查看考试</a>`;
} else if (n.type === 'friend_request' && n.application_status === 'pending' && n.friend_request_id) {
actHtml = `<div class="flex gap-3">
<button onclick="acceptFriendReq(${n.friend_request_id},${n.id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">同意</button>
<button onclick="rejectFriendReq(${n.friend_request_id},${n.id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
</div>`;
} else if (n.type === 'friend_request' && n.application_status === 'accepted') {
actHtml = '<span class="text-sm text-green-600 font-medium">✅ 已同意</span>';
} else if ((n.type === 'contest_application' || n.type === 'teacher_application') && n.application_status === 'approved') { } else if ((n.type === 'contest_application' || n.type === 'teacher_application') && n.application_status === 'approved') {
actHtml = '<span class="text-sm text-green-600 font-medium">✅ 已批准</span>'; actHtml = '<span class="text-sm text-green-600 font-medium">✅ 已批准</span>';
} else if ((n.type === 'contest_application' || n.type === 'teacher_application') && n.application_status === 'rejected') { } else if ((n.type === 'contest_application' || n.type === 'teacher_application') && n.application_status === 'rejected') {

View File

@@ -838,12 +838,19 @@ async function showProfile(uid) {
const p = d.profile; const p = d.profile;
let badges = p.badges.map(b => `<span class="inline-flex items-center gap-1 px-2 py-1 bg-slate-50 rounded-lg text-xs" title="${b.desc}">${b.icon} ${b.name}</span>`).join(''); let badges = p.badges.map(b => `<span class="inline-flex items-center gap-1 px-2 py-1 bg-slate-50 rounded-lg text-xs" title="${b.desc}">${b.icon} ${b.name}</span>`).join('');
let posts = p.recent_posts.map(pp => `<div class="text-sm py-1.5 border-b border-slate-50 cursor-pointer hover:text-primary" onclick="document.getElementById('profile-modal').classList.add('hidden');openPost(${pp.id})">${esc(pp.title)}</div>`).join(''); let posts = p.recent_posts.map(pp => `<div class="text-sm py-1.5 border-b border-slate-50 cursor-pointer hover:text-primary" onclick="document.getElementById('profile-modal').classList.add('hidden');openPost(${pp.id})">${esc(pp.title)}</div>`).join('');
let friendBtn = '';
if (CU && CU.id !== parseInt(uid)) {
friendBtn = `<button id="addFriendBtn" onclick="forumAddFriend(${uid},this)" class="px-3 py-1.5 text-xs bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors">加好友</button>`;
}
document.getElementById('profile-content').innerHTML = ` document.getElementById('profile-content').innerHTML = `
<div class="flex items-center gap-4 mb-5"> <div class="flex items-center gap-4 mb-5">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary text-2xl font-bold">${esc(p.name.charAt(0))}</div> <div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary text-2xl font-bold">${esc(p.name.charAt(0))}</div>
<div><div class="flex items-center gap-2"><span class="text-xl font-bold">${esc(p.name)}</span><span class="level-badge level-${p.level}">Lv.${p.level} ${p.level_title}</span></div> <div><div class="flex items-center gap-2"><span class="text-xl font-bold">${esc(p.name)}</span><span class="level-badge level-${p.level}">Lv.${p.level} ${p.level_title}</span></div>
<div class="text-sm text-slate-500 mt-1">${p.points} 积分</div></div> <div class="text-sm text-slate-500 mt-1">${p.points} 积分</div></div>
<button onclick="document.getElementById('profile-modal').classList.add('hidden')" class="ml-auto text-slate-400 hover:text-slate-600 text-xl">✕</button> <div class="ml-auto flex items-center gap-2">
${friendBtn}
<button onclick="document.getElementById('profile-modal').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 text-xl">✕</button>
</div>
</div> </div>
<div class="grid grid-cols-3 gap-3 mb-5"> <div class="grid grid-cols-3 gap-3 mb-5">
<div class="text-center p-3 bg-blue-50 rounded-lg"><div class="text-xl font-bold text-blue-600">${p.posts_count}</div><div class="text-xs text-slate-500">帖子</div></div> <div class="text-center p-3 bg-blue-50 rounded-lg"><div class="text-xl font-bold text-blue-600">${p.posts_count}</div><div class="text-xs text-slate-500">帖子</div></div>
@@ -854,6 +861,21 @@ async function showProfile(uid) {
${posts?`<div><div class="text-sm font-bold text-slate-700 mb-2">📝 最近帖子</div>${posts}</div>`:''}`; ${posts?`<div><div class="text-sm font-bold text-slate-700 mb-2">📝 最近帖子</div>${posts}</div>`:''}`;
} }
async function forumAddFriend(userId, btn) {
try {
const res = await fetch('/api/friend/add', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({friend_id: userId})});
const data = await res.json();
if (data.success) {
btn.textContent = '已发送';
btn.disabled = true;
btn.classList.replace('bg-primary', 'bg-slate-400');
btn.classList.remove('hover:bg-blue-600');
} else {
alert(data.message || '操作失败');
}
} catch(e) { alert('操作失败'); }
}
// ===== 侧边栏 ===== // ===== 侧边栏 =====
async function loadSidebar() { async function loadSidebar() {
// 热门帖子 // 热门帖子