mobile base exam_grade forum themes

This commit is contained in:
2026-03-09 13:57:01 +08:00
parent aa012ea3aa
commit 7dec929e29
3 changed files with 21 additions and 25 deletions

View File

@@ -352,7 +352,7 @@
</script> </script>
<!-- 浮动聊天气泡 --> <!-- 浮动聊天气泡 -->
<div id="chatBubble" class="fixed bottom-6 right-6 z-40"> <div id="chatBubble" class="fixed bottom-6 right-6 z-30">
<button onclick="toggleMiniChat()" class="w-14 h-14 bg-gradient-to-tr from-primary to-blue-400 text-white rounded-full shadow-lg hover:shadow-xl hover:bg-blue-600 flex items-center justify-center relative transition-all duration-300 hover:scale-110"> <button onclick="toggleMiniChat()" class="w-14 h-14 bg-gradient-to-tr from-primary to-blue-400 text-white rounded-full shadow-lg hover:shadow-xl hover:bg-blue-600 flex items-center justify-center relative transition-all duration-300 hover:scale-110">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg> <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
<span id="bubbleBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center hidden">0</span> <span id="bubbleBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center hidden">0</span>
@@ -360,7 +360,7 @@
</div> </div>
<!-- 迷你聊天窗口 --> <!-- 迷你聊天窗口 -->
<div id="miniChat" class="fixed bottom-24 right-6 w-[380px] h-[500px] bg-white rounded-xl shadow-2xl border border-slate-200 z-40 hidden flex flex-col overflow-hidden"> <div id="miniChat" class="fixed bottom-24 right-6 w-[380px] h-[500px] bg-white rounded-xl shadow-2xl border border-slate-200 z-30 hidden flex flex-col overflow-hidden">
<div class="px-4 py-3 bg-primary text-white flex justify-between items-center rounded-t-xl"> <div class="px-4 py-3 bg-primary text-white flex justify-between items-center rounded-t-xl">
<span class="font-medium text-sm" id="miniTitle">消息</span> <span class="font-medium text-sm" id="miniTitle">消息</span>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@@ -385,7 +385,14 @@
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script> <script>
(function(){ (function(){
if (document.getElementById('chatApp')) return; // chat.html 已有自己的socket // 在聊天页面隐藏浮动气泡(已有完整聊天界面)
if (document.getElementById('chatApp')) {
var bubble = document.getElementById('chatBubble');
if (bubble) bubble.style.display = 'none';
var miniC = document.getElementById('miniChat');
if (miniC) miniC.style.display = 'none';
return;
}
const bubbleSocket = io(); const bubbleSocket = io();
let miniRoomId = null; let miniRoomId = null;
let miniRooms = []; let miniRooms = [];
@@ -422,6 +429,11 @@
}); });
window.toggleMiniChat = function() { window.toggleMiniChat = function() {
// 移动端直接跳转到完整聊天页面
if (window.innerWidth < 640) {
window.location.href = '/chat';
return;
}
const mc = document.getElementById('miniChat'); const mc = document.getElementById('miniChat');
mc.classList.toggle('hidden'); mc.classList.toggle('hidden');
if (!mc.classList.contains('hidden')) { if (!mc.classList.contains('hidden')) {

View File

@@ -117,6 +117,9 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
</div>
{% endfor %}
<div class="flex justify-between items-center bg-white shadow-sm rounded-lg p-6 border border-slate-200 sticky bottom-4"> <div class="flex justify-between items-center bg-white shadow-sm rounded-lg p-6 border border-slate-200 sticky bottom-4">
<div class="text-lg font-medium text-slate-900">总分:<span id="total-score" class="text-primary">0</span> / {{ exam.total_score }}</div> <div class="text-lg font-medium text-slate-900">总分:<span id="total-score" class="text-primary">0</span> / {{ exam.total_score }}</div>

View File

@@ -7,9 +7,6 @@
.toast{position:fixed;top:20px;right:20px;z-index:9999;padding:12px 20px;border-radius:8px;color:#fff;font-size:14px;animation:slideIn .3s ease;box-shadow:0 4px 12px rgba(0,0,0,.15)} .toast{position:fixed;top:20px;right:20px;z-index:9999;padding:12px 20px;border-radius:8px;color:#fff;font-size:14px;animation:slideIn .3s ease;box-shadow:0 4px 12px rgba(0,0,0,.15)}
.toast-success{background:#10b981}.toast-error{background:#ef4444}.toast-info{background:#3b82f6} .toast-success{background:#10b981}.toast-error{background:#ef4444}.toast-info{background:#3b82f6}
@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}} @keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
.reaction-btn{transition:all .2s;cursor:pointer;padding:2px 8px;border-radius:20px;border:1px solid #e2e8f0;font-size:13px;display:inline-flex;align-items:center;gap:3px}
.reaction-btn:hover{transform:scale(1.15);border-color:#93c5fd;background:#eff6ff}
.reaction-btn.active{background:#dbeafe;border-color:#60a5fa}
.poll-bar{height:28px;border-radius:6px;background:#f1f5f9;overflow:hidden;position:relative;margin:4px 0} .poll-bar{height:28px;border-radius:6px;background:#f1f5f9;overflow:hidden;position:relative;margin:4px 0}
.poll-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,#3b82f6,#60a5fa);transition:width .6s ease;display:flex;align-items:center;padding:0 10px;font-size:12px;color:#fff;font-weight:500;min-width:fit-content} .poll-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,#3b82f6,#60a5fa);transition:width .6s ease;display:flex;align-items:center;padding:0 10px;font-size:12px;color:#fff;font-weight:500;min-width:fit-content}
.poll-bar.voted .poll-fill{background:linear-gradient(90deg,#10b981,#34d399)} .poll-bar.voted .poll-fill{background:linear-gradient(90deg,#10b981,#34d399)}
@@ -307,7 +304,7 @@
<script> <script>
const CU = {{ user | tojson if user else 'null' }}; const CU = {{ user | tojson if user else 'null' }};
let curTag = '全部'; let curTag = '全部';
const REACTIONS = {like:'👍',love:'❤️',haha:'😂',wow:'😮',sad:'😢',angry:'😡'}; const REACTIONS = {};
const TAG_ICONS = {'官方公告':'📢','题目讨论':'📐','经验分享':'💡','求助答疑':'🙋','闲聊灌水':'☕'}; const TAG_ICONS = {'官方公告':'📢','题目讨论':'📐','经验分享':'💡','求助答疑':'🙋','闲聊灌水':'☕'};
const TAG_COLORS = {'官方公告':'red','题目讨论':'blue','经验分享':'green','求助答疑':'yellow','闲聊灌水':'purple'}; const TAG_COLORS = {'官方公告':'red','题目讨论':'blue','经验分享':'green','求助答疑':'yellow','闲聊灌水':'purple'};
@@ -443,12 +440,7 @@ function renderPosts(posts) {
posts.forEach(p => { posts.forEach(p => {
const tc = TAG_COLORS[p.tag] || 'slate'; const tc = TAG_COLORS[p.tag] || 'slate';
const ti = TAG_ICONS[p.tag] || '📋'; const ti = TAG_ICONS[p.tag] || '📋';
const reactions = p.reactions || {};
let reactHtml = '';
for (const [k, v] of Object.entries(reactions)) {
if (v > 0) reactHtml += `<span class="inline-flex items-center gap-1 text-xs bg-slate-50 border border-slate-100 px-2 py-0.5 rounded-md text-slate-600">${REACTIONS[k]||k} ${v}</span>`;
}
// 生成纯文本内容用于预览 // 生成纯文本内容用于预览
let cleanContent = p.content.replace(/\[img:[^\]]+\]/g, '[图片]'); let cleanContent = p.content.replace(/\[img:[^\]]+\]/g, '[图片]');
@@ -513,7 +505,6 @@ function renderPosts(posts) {
</div> </div>
</div> </div>
</div> </div>
${reactHtml ? `<div class="mt-4 pt-3 border-t border-slate-100/60 flex flex-wrap gap-2">${reactHtml}</div>` : ''}
</div> </div>
</div>`; </div>`;
}); });
@@ -569,12 +560,6 @@ async function openPost(pid) {
const isTeacher = CU && (CU.role === 'teacher' || CU.role === 'admin'); const isTeacher = CU && (CU.role === 'teacher' || CU.role === 'admin');
const reactions = p.reactions || {}; const reactions = p.reactions || {};
let reactBtns = '';
for (const [k, emoji] of Object.entries(REACTIONS)) {
const cnt = reactions[k] || 0;
reactBtns += `<button onclick="reactPost(${p.id},'${k}',this)" class="reaction-btn${cnt>0?' active':''}">${emoji} <span>${cnt}</span></button>`;
}
let pollHtml = ''; let pollHtml = '';
if (p.has_poll) { if (p.has_poll) {
try { try {
@@ -635,7 +620,6 @@ async function openPost(pid) {
</div> </div>
<div class="prose prose-sm max-w-none text-slate-800 whitespace-pre-wrap border-b border-slate-100 pb-5 mb-4">${renderContent(p.content)}</div> <div class="prose prose-sm max-w-none text-slate-800 whitespace-pre-wrap border-b border-slate-100 pb-5 mb-4">${renderContent(p.content)}</div>
${pollHtml} ${pollHtml}
<div class="flex items-center gap-2 flex-wrap mb-4">${reactBtns}</div>
<div class="flex items-center gap-3 mb-6 border-b border-slate-100 pb-4"> <div class="flex items-center gap-3 mb-6 border-b border-slate-100 pb-4">
<button onclick="toggleLikeModal(${p.id})" id="ml-btn" class="flex items-center gap-1.5 px-3 py-1.5 rounded-full border ${p.liked?'border-red-300 text-red-500 bg-red-50':'border-slate-200 text-slate-500'} hover:bg-red-50 text-sm">❤️ <span id="ml-cnt">${p.likes}</span></button> <button onclick="toggleLikeModal(${p.id})" id="ml-btn" class="flex items-center gap-1.5 px-3 py-1.5 rounded-full border ${p.liked?'border-red-300 text-red-500 bg-red-50':'border-slate-200 text-slate-500'} hover:bg-red-50 text-sm">❤️ <span id="ml-cnt">${p.likes}</span></button>
<button onclick="toggleBmModal(${p.id})" id="mb-btn" class="flex items-center gap-1.5 px-3 py-1.5 rounded-full border ${p.bookmarked?'border-yellow-300 text-yellow-500 bg-yellow-50':'border-slate-200 text-slate-500'} hover:bg-yellow-50 text-sm">${p.bookmarked?'⭐ 已收藏':'☆ 收藏'}</button> <button onclick="toggleBmModal(${p.id})" id="mb-btn" class="flex items-center gap-1.5 px-3 py-1.5 rounded-full border ${p.bookmarked?'border-yellow-300 text-yellow-500 bg-yellow-50':'border-slate-200 text-slate-500'} hover:bg-yellow-50 text-sm">${p.bookmarked?'⭐ 已收藏':'☆ 收藏'}</button>
@@ -732,10 +716,7 @@ async function toggleBmModal(pid) {
} }
async function reactPost(pid, reaction, btn) { async function reactPost(pid, reaction, btn) {
if (!CU) { toast('请先登录','error'); return; } // removed
const res = await fetch(`/api/posts/${pid}/react`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reaction})});
const d = await res.json();
if (d.success) openPost(pid);
} }
async function votePoll(pid) { async function votePoll(pid) {