mobile app models chat login notifications themes

This commit is contained in:
2026-02-28 11:30:31 +08:00
parent 1d7d451c60
commit 13057c8757
5 changed files with 232 additions and 66 deletions

View File

@@ -175,7 +175,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">创建群聊</h3>
@@ -193,7 +193,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">群成员</h3>
@@ -210,7 +210,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">群公告</h3>
@@ -232,7 +232,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">搜索聊天记录</h3>
@@ -246,7 +246,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">群文件</h3>
@@ -257,7 +257,7 @@
</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="px-4 py-3 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-semibold">设置群昵称</h3>
@@ -934,7 +934,9 @@ function markRead(roomId) {
// ========== 创建群聊 ==========
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 = '创建群聊';
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() {
const name = document.getElementById('groupName').value.trim();
@@ -983,7 +989,9 @@ async function createGroup() {
// ========== 成员列表 ==========
async function showMembers() {
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 data = await res.json();
if (!data.success) return;
@@ -1026,7 +1034,11 @@ async function showMembers() {
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() {
hideMembers();
@@ -1082,7 +1094,9 @@ async function transferOwner(uid, name) {
// ========== 群公告 ==========
async function showAnnouncement() {
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 data = await res.json();
if (data.success) {
@@ -1103,7 +1117,11 @@ async function showAnnouncement() {
}
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() {
document.getElementById('announcementEditSection').classList.remove('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 hideSearchPanel() { document.getElementById('searchModal').classList.add('hidden'); }
function showSearchPanel() {
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); }
async function doSearch() {
const q = document.getElementById('msgSearchInput').value.trim();
@@ -1154,7 +1184,9 @@ function scrollToMsg(msgId) {
// ========== 群文件 ==========
async function showFileList() {
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 data = await res.json();
const container = document.getElementById('fileListContent');
@@ -1173,11 +1205,25 @@ async function showFileList() {
</div>`;
}).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 hideNicknameModal() { document.getElementById('nicknameModal').classList.add('hidden'); }
function showNicknameModal() {
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() {
const nickname = document.getElementById('nicknameInput').value.trim();
const res = await fetch(`/api/chat/rooms/${currentRoomId}/nickname`, {

View File

@@ -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>
</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>
</form>
<!-- 邮箱登录表单 -->
@@ -52,6 +56,10 @@
<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="请输入密码">
</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>
</form>
<div class="mt-6 relative">
@@ -112,8 +120,9 @@ async function handlePhoneLogin(e) {
e.preventDefault();
const phone = document.getElementById('phone').value;
const code = document.getElementById('sms-code').value;
const remember = document.getElementById('remember-phone').checked;
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();
if (data.success) { window.location.href = '/'; }
else alert(data.message);
@@ -124,8 +133,9 @@ async function handleEmailLogin(e) {
e.preventDefault();
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
const remember = document.getElementById('remember-email').checked;
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();
if (data.success) { window.location.href = '/'; }
else alert(data.message);

View File

@@ -181,7 +181,7 @@ function renderNotifications() {
'system_announcement': 'bg-blue-100 text-blue-600', 'friend_request': 'bg-cyan-100 text-cyan-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, '&quot;')})">
${!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="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,10 +205,17 @@ function renderNotifications() {
}
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">
<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>
</div>`;
}
// 杯赛负责人显示查看按钮
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();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>
<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>';
@@ -294,7 +301,7 @@ async function rejectFriendN(reqId) {
} catch(e) { alert('操作失败'); }
}
function markSingleRead(nid, el) {
function markSingleRead(nid, el, notif) {
fetch(`/api/notifications/${nid}/read`, {method:'POST'});
// 移除未读的特定样式
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');
const dot = el.querySelector('.bg-red-500.animate-pulse');
if (dot) dot.remove();
// 根据通知类型跳转
if (notif && notif.type === 'teacher_application' && notif.application_status === 'pending') {
window.location.href = '/admin/teacher-applications';
}
}
function markAllRead() {