mobile chat themes
This commit is contained in:
@@ -375,7 +375,7 @@ function switchTab(tab) {
|
|||||||
notifPanel.classList.remove('hidden');
|
notifPanel.classList.remove('hidden');
|
||||||
tabChat.className = 'flex-1 px-3 py-2.5 text-sm font-medium text-slate-400 border-b-2 border-transparent hover:text-slate-600';
|
tabChat.className = 'flex-1 px-3 py-2.5 text-sm font-medium text-slate-400 border-b-2 border-transparent hover:text-slate-600';
|
||||||
tabNotif.className = 'flex-1 px-3 py-2.5 text-sm font-medium text-primary border-b-2 border-primary relative';
|
tabNotif.className = 'flex-1 px-3 py-2.5 text-sm font-medium text-primary border-b-2 border-primary relative';
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
|
|
||||||
// Mark all notifications as read when switching to the notification tab
|
// Mark all notifications as read when switching to the notification tab
|
||||||
fetch('/api/notifications/read-all', { method: 'POST' }).then(() => {
|
fetch('/api/notifications/read-all', { method: 'POST' }).then(() => {
|
||||||
@@ -388,7 +388,7 @@ function switchTab(tab) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNotifications() {
|
async function loadChatNotifications() {
|
||||||
const res = await fetch('/api/notifications');
|
const res = await fetch('/api/notifications');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -428,20 +428,20 @@ function renderNotifications() {
|
|||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
if (isPendingContest && currentUser.role === 'admin') {
|
if (isPendingContest && currentUser.role === 'admin') {
|
||||||
actionsHtml = `<div class="flex gap-2 mt-2">
|
actionsHtml = `<div class="flex gap-2 mt-2">
|
||||||
<button onclick="event.stopPropagation();approveContest(${n.post_id})" class="px-3 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600">批准</button>
|
<button onclick="event.stopPropagation();chatApproveContest(${n.post_id})" class="px-3 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600">批准</button>
|
||||||
<button onclick="event.stopPropagation();rejectContest(${n.post_id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">拒绝</button>
|
<button onclick="event.stopPropagation();chatRejectContest(${n.post_id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600">拒绝</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (isPendingTeacher && currentUser.role === 'admin' && n.application_id) {
|
if (isPendingTeacher && currentUser.role === 'admin' && n.application_id) {
|
||||||
actionsHtml = `<div class="flex gap-2 mt-2">
|
actionsHtml = `<div class="flex gap-2 mt-2">
|
||||||
<button onclick="event.stopPropagation();approveTeacher(${n.application_id})" class="px-3 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600">批准</button>
|
<button onclick="event.stopPropagation();chatApproveTeacher(${n.application_id})" class="px-3 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-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>
|
<button onclick="event.stopPropagation();chatRejectTeacher(${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) {
|
if (isPendingFriend && n.friend_request_id) {
|
||||||
actionsHtml = `<div class="flex gap-2 mt-2">
|
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();chatAcceptFriendReq(${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>
|
<button onclick="event.stopPropagation();chatRejectFriendReq(${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>`;
|
</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}">
|
||||||
@@ -470,65 +470,65 @@ async function markNotifRead(nid, el) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function approveContest(appId) {
|
async function chatApproveContest(appId) {
|
||||||
if (!confirm('确定批准该杯赛申请?')) return;
|
if (!confirm('确定批准该杯赛申请?')) return;
|
||||||
const res = await fetch(`/api/contest-applications/${appId}/approve`, { method: 'POST' });
|
const res = await fetch(`/api/contest-applications/${appId}/approve`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectContest(appId) {
|
async function chatRejectContest(appId) {
|
||||||
if (!confirm('确定拒绝该杯赛申请?')) return;
|
if (!confirm('确定拒绝该杯赛申请?')) return;
|
||||||
const res = await fetch(`/api/contest-applications/${appId}/reject`, { method: 'POST' });
|
const res = await fetch(`/api/contest-applications/${appId}/reject`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function approveTeacher(appId) {
|
async function chatApproveTeacher(appId) {
|
||||||
if (!confirm('确定批准该教师申请?')) return;
|
if (!confirm('确定批准该教师申请?')) return;
|
||||||
const res = await fetch(`/api/teacher-applications/${appId}/approve`, { method: 'POST' });
|
const res = await fetch(`/api/teacher-applications/${appId}/approve`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectTeacher(appId) {
|
async function chatRejectTeacher(appId) {
|
||||||
if (!confirm('确定拒绝该教师申请?')) return;
|
if (!confirm('确定拒绝该教师申请?')) return;
|
||||||
const res = await fetch(`/api/teacher-applications/${appId}/reject`, { method: 'POST' });
|
const res = await fetch(`/api/teacher-applications/${appId}/reject`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptFriendReq(reqId, notifId) {
|
async function chatAcceptFriendReq(reqId, notifId) {
|
||||||
const res = await fetch(`/api/friend/accept/${reqId}`, { method: 'POST' });
|
const res = await fetch(`/api/friend/accept/${reqId}`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectFriendReq(reqId, notifId) {
|
async function chatRejectFriendReq(reqId, notifId) {
|
||||||
const res = await fetch(`/api/friend/reject/${reqId}`, { method: 'POST' });
|
const res = await fetch(`/api/friend/reject/${reqId}`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
loadNotifications();
|
loadChatNotifications();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '操作失败');
|
alert(data.message || '操作失败');
|
||||||
}
|
}
|
||||||
@@ -606,13 +606,13 @@ function showNotifDetail(nid) {
|
|||||||
let actHtml = '';
|
let actHtml = '';
|
||||||
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') {
|
||||||
actHtml = `<div class="flex gap-3">
|
actHtml = `<div class="flex gap-3">
|
||||||
<button onclick="approveContest(${n.post_id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">批准</button>
|
<button onclick="chatApproveContest(${n.post_id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">批准</button>
|
||||||
<button onclick="rejectContest(${n.post_id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
|
<button onclick="chatRejectContest(${n.post_id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (n.type === 'teacher_application' && n.application_status === 'pending' && n.application_id && currentUser.role === 'admin') {
|
} else if (n.type === 'teacher_application' && n.application_status === 'pending' && n.application_id && currentUser.role === 'admin') {
|
||||||
actHtml = `<div class="flex gap-3">
|
actHtml = `<div class="flex gap-3">
|
||||||
<button onclick="approveTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">批准</button>
|
<button onclick="chatApproveTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">批准</button>
|
||||||
<button onclick="rejectTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
|
<button onclick="chatRejectTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (n.type === 'exam_submission' && n.exam_id && n.submission_id) {
|
} else if (n.type === 'exam_submission' && n.exam_id && n.submission_id) {
|
||||||
actHtml = `<a href="/exams/${n.exam_id}/grade/${n.submission_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">
|
actHtml = `<a href="/exams/${n.exam_id}/grade/${n.submission_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">
|
||||||
@@ -623,8 +623,8 @@ function showNotifDetail(nid) {
|
|||||||
actHtml = `<a href="/exams/${n.post_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">查看考试</a>`;
|
actHtml = `<a href="/exams/${n.post_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">查看考试</a>`;
|
||||||
} else if (n.type === 'friend_request' && n.application_status === 'pending' && n.friend_request_id) {
|
} else if (n.type === 'friend_request' && n.application_status === 'pending' && n.friend_request_id) {
|
||||||
actHtml = `<div class="flex gap-3">
|
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="chatAcceptFriendReq(${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>
|
<button onclick="chatRejectFriendReq(${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>`;
|
</div>`;
|
||||||
} else if (n.type === 'friend_request' && n.application_status === 'accepted') {
|
} else if (n.type === 'friend_request' && n.application_status === 'accepted') {
|
||||||
actHtml = '<span class="text-sm text-green-600 font-medium">✅ 已同意</span>';
|
actHtml = '<span class="text-sm text-green-600 font-medium">✅ 已同意</span>';
|
||||||
@@ -674,14 +674,14 @@ function renderRooms() {
|
|||||||
list.innerHTML = filtered.map(r => {
|
list.innerHTML = filtered.map(r => {
|
||||||
const peerInExam = r.type === 'private' && r.peer_id && peerExamStatus[r.peer_id]?.in_exam;
|
const peerInExam = r.type === 'private' && r.peer_id && peerExamStatus[r.peer_id]?.in_exam;
|
||||||
return `
|
return `
|
||||||
<div class="flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-slate-50 transition ${currentRoomId === r.id ? 'bg-blue-50 border-r-2 border-primary' : ''}" onclick="selectRoom(${r.id})">
|
<div class="flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-white/10 transition rounded-xl ${currentRoomId === r.id ? 'bg-cyan-500/15 border-r-2 border-cyan-400' : ''}" onclick="selectRoom(${r.id})">
|
||||||
<div class="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center flex-shrink-0 overflow-hidden">
|
<div class="w-10 h-10 rounded-full bg-slate-700/50 flex items-center justify-center flex-shrink-0 overflow-hidden border border-white/10">
|
||||||
${r.avatar ? `<img src="${r.avatar}" class="w-full h-full object-cover">` :
|
${r.avatar ? `<img src="${r.avatar}" class="w-full h-full object-cover">` :
|
||||||
`<span class="text-sm font-medium text-slate-500">${(r.name || '?')[0]}</span>`}
|
`<span class="text-sm font-medium text-cyan-400">${(r.name || '?')[0]}</span>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-sm font-medium text-slate-800 truncate">${escHtml(r.name || '未命名')}</span>
|
<span class="text-sm font-medium text-white truncate">${escHtml(r.name || '未命名')}</span>
|
||||||
${peerInExam ? '<span class="text-[10px] text-amber-400 bg-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">考试中</span>' : ''}
|
${peerInExam ? '<span class="text-[10px] text-amber-400 bg-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">考试中</span>' : ''}
|
||||||
<span class="text-xs text-slate-400 flex-shrink-0">${r.last_message ? formatTime(r.last_message.created_at) : ''}</span>
|
<span class="text-xs text-slate-400 flex-shrink-0">${r.last_message ? formatTime(r.last_message.created_at) : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -710,7 +710,7 @@ function filterRooms() { renderRooms(); }
|
|||||||
async function selectRoom(roomId) {
|
async function selectRoom(roomId) {
|
||||||
currentRoomId = roomId;
|
currentRoomId = roomId;
|
||||||
oldestMsgId = null;
|
oldestMsgId = null;
|
||||||
markRead(roomId); // 立即标记已读,使聊天气泡消失
|
markChatRead(roomId); // 立即标记已读,使聊天气泡消失
|
||||||
document.getElementById('emptyState').classList.add('hidden');
|
document.getElementById('emptyState').classList.add('hidden');
|
||||||
// 隐藏通知详情
|
// 隐藏通知详情
|
||||||
const nd = document.getElementById('notifDetailView');
|
const nd = document.getElementById('notifDetailView');
|
||||||
@@ -988,7 +988,7 @@ function previewImage(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 标记已读 ==========
|
// ========== 标记已读 ==========
|
||||||
function markRead(roomId) {
|
function markChatRead(roomId) {
|
||||||
fetch(`/api/chat/rooms/${roomId}/read`, { method: 'POST' });
|
fetch(`/api/chat/rooms/${roomId}/read`, { method: 'POST' });
|
||||||
socket.emit('mark_read', { room_id: roomId });
|
socket.emit('mark_read', { room_id: roomId });
|
||||||
const room = rooms.find(r => r.id === roomId);
|
const room = rooms.find(r => r.id === roomId);
|
||||||
@@ -1425,7 +1425,7 @@ socket.on('new_message', (msg) => {
|
|||||||
area.scrollTop = area.scrollHeight;
|
area.scrollTop = area.scrollHeight;
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
markRead(currentRoomId);
|
markChatRead(currentRoomId);
|
||||||
}
|
}
|
||||||
// 更新会话列表
|
// 更新会话列表
|
||||||
const room = rooms.find(r => r.id === msg.room_id);
|
const room = rooms.find(r => r.id === msg.room_id);
|
||||||
|
|||||||
Reference in New Issue
Block a user