mobile app admin_base admin_contests admin_dashboard chat contest_detail exam_create exam_detail exam_grade exam_list profile themes
This commit is contained in:
@@ -95,6 +95,7 @@
|
||||
<span id="chatName" class="text-lg font-bold text-white tracking-tight"></span>
|
||||
<span id="chatMemberCount" class="badge-futuristic text-xs font-bold px-2 py-0.5 rounded-full"></span>
|
||||
</div>
|
||||
<div id="peerExamBanner" class="text-xs font-medium text-amber-400 bg-amber-500/20 px-2 py-1 rounded mt-1 hidden">对方正在考试中,请勿打扰</div>
|
||||
<div id="typingIndicator" class="text-[11px] font-medium text-cyan-400 hidden animate-pulse mt-0.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,6 +276,8 @@
|
||||
<div id="imagePreview" class="fixed inset-0 bg-black/80 z-[9990] hidden flex items-center justify-center cursor-pointer" onclick="this.classList.add('hidden')">
|
||||
<img id="previewImg" class="max-w-[90vw] max-h-[90vh] rounded-lg">
|
||||
</div>
|
||||
<!-- 轻提示 -->
|
||||
<div id="chatToast" class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-slate-800 text-amber-400 rounded-lg shadow-lg z-[9995] hidden text-sm font-medium"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -297,6 +300,7 @@ let recordingStartTime = null;
|
||||
let recordingInterval = null;
|
||||
let searchTimer = null;
|
||||
let mentionMembers = [];
|
||||
let peerExamStatus = {}; // {user_id: {in_exam, exam_title, end_time}}
|
||||
|
||||
function isMobile() { return window.innerWidth < 640; }
|
||||
|
||||
@@ -372,9 +376,16 @@ function switchTab(tab) {
|
||||
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';
|
||||
loadNotifications();
|
||||
|
||||
// Mark all notifications as read when switching to the notification tab
|
||||
fetch('/api/notifications/read-all', { method: 'POST' }).then(() => {
|
||||
checkUnreadNotifs();
|
||||
});
|
||||
}
|
||||
const badge = document.getElementById('chatNotifBadge');
|
||||
tabNotif.appendChild(badge);
|
||||
if (badge) {
|
||||
tabNotif.appendChild(badge);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotifications() {
|
||||
@@ -527,11 +538,24 @@ async function checkUnreadNotifs() {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const badge = document.getElementById('chatNotifBadge');
|
||||
if (data.count > 0) {
|
||||
badge.textContent = data.count > 99 ? '99+' : data.count;
|
||||
badge.classList.remove('hidden');
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
if (badge) {
|
||||
if (data.count > 0) {
|
||||
badge.textContent = data.count > 99 ? '99+' : data.count;
|
||||
badge.classList.remove('hidden');
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the global notification badge if it exists
|
||||
const globalBadge = document.getElementById('notifBadge');
|
||||
if (globalBadge) {
|
||||
if (data.count > 0) {
|
||||
globalBadge.textContent = data.count > 99 ? '99+' : data.count;
|
||||
globalBadge.classList.remove('hidden');
|
||||
} else {
|
||||
globalBadge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,7 +663,9 @@ function renderRooms() {
|
||||
const search = document.getElementById('roomSearch').value.toLowerCase();
|
||||
const list = document.getElementById('roomList');
|
||||
const filtered = rooms.filter(r => (r.name || '').toLowerCase().includes(search));
|
||||
list.innerHTML = filtered.map(r => `
|
||||
list.innerHTML = filtered.map(r => {
|
||||
const peerInExam = r.type === 'private' && r.peer_id && peerExamStatus[r.peer_id]?.in_exam;
|
||||
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="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
${r.avatar ? `<img src="${r.avatar}" class="w-full h-full object-cover">` :
|
||||
@@ -648,6 +674,7 @@ function renderRooms() {
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium text-slate-800 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>' : ''}
|
||||
<span class="text-xs text-slate-400 flex-shrink-0">${r.last_message ? formatTime(r.last_message.created_at) : ''}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-0.5">
|
||||
@@ -656,7 +683,7 @@ function renderRooms() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
function getPreview(r) {
|
||||
@@ -688,11 +715,34 @@ async function selectRoom(roomId) {
|
||||
if (room) {
|
||||
document.getElementById('chatName').textContent = room.name || '聊天';
|
||||
document.getElementById('chatMemberCount').textContent = room.type !== 'private' ? `(${room.member_count}人)` : '';
|
||||
// 私聊:获取对方考试状态
|
||||
if (room.type === 'private' && room.peer_id) {
|
||||
try {
|
||||
const res = await fetch(`/api/user/${room.peer_id}/exam-status`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
peerExamStatus[room.peer_id] = { in_exam: data.in_exam, exam_title: data.exam_title || '', end_time: data.end_time };
|
||||
}
|
||||
} catch (e) { peerExamStatus[room.peer_id] = { in_exam: false }; }
|
||||
}
|
||||
updateChatHeaderExamStatus(room);
|
||||
}
|
||||
renderRooms();
|
||||
await loadMessages(roomId);
|
||||
markRead(roomId);
|
||||
}
|
||||
|
||||
function updateChatHeaderExamStatus(room) {
|
||||
const examBanner = document.getElementById('peerExamBanner');
|
||||
if (!examBanner) return;
|
||||
if (room && room.type === 'private' && room.peer_id && peerExamStatus[room.peer_id]?.in_exam) {
|
||||
const info = peerExamStatus[room.peer_id];
|
||||
examBanner.textContent = `${room.name || '对方'} 正在考试中(${info.exam_title || '考试'}),请勿打扰`;
|
||||
examBanner.classList.remove('hidden');
|
||||
} else {
|
||||
examBanner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
// ========== 消息加载与渲染 ==========
|
||||
async function loadMessages(roomId, beforeId) {
|
||||
let url = `/api/chat/rooms/${roomId}/messages?limit=30`;
|
||||
@@ -1439,6 +1489,33 @@ socket.on('error', (data) => {
|
||||
if (data.message) alert(data.message);
|
||||
});
|
||||
|
||||
socket.on('friend_exam_status', (data) => {
|
||||
const uid = data.user_id;
|
||||
peerExamStatus[uid] = {
|
||||
in_exam: data.in_exam,
|
||||
exam_title: data.exam_title || '',
|
||||
end_time: data.end_time
|
||||
};
|
||||
const room = rooms.find(r => r.id === currentRoomId);
|
||||
if (room && room.type === 'private' && room.peer_id === uid) {
|
||||
updateChatHeaderExamStatus(room);
|
||||
}
|
||||
renderRooms();
|
||||
});
|
||||
|
||||
socket.on('peer_in_exam', (data) => {
|
||||
showToast(data.message || '对方正在考试中,消息将在考试结束后送达');
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
const el = document.getElementById('chatToast');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden');
|
||||
clearTimeout(el._toastTimer);
|
||||
el._toastTimer = setTimeout(() => el.classList.add('hidden'), 3000);
|
||||
}
|
||||
|
||||
// ========== 工具函数 ==========
|
||||
function escHtml(s) {
|
||||
if (!s) return '';
|
||||
|
||||
Reference in New Issue
Block a user