您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 MJJBox 设置 快速收藏、话题时间显示、精确回复时间、新标签页打开、等级进度查看、返回顶部 等功能。
// ==UserScript== // @name MJJBOX 增强 // @description 为 MJJBox 设置 快速收藏、话题时间显示、精确回复时间、新标签页打开、等级进度查看、返回顶部 等功能。 // @description:en Adds features like Quick Bookmarking, Topic Time Display, Precise Reply Time, Open in New Tab, Level Progress Viewer, and Back to Top to MJJBox. // @description:zh-CN 为 MJJBox 设置 快速收藏、话题时间显示、精确回复时间、新标签页打开、等级进度查看、返回顶部 等功能。 // @version 0.3 // @author Zz // @match https://mjjbox.com/* // @icon https://www.google.com/s2/favicons?domain=mjjbox.com // @license MIT // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @namespace http://tampermonkey.net/ // ==/UserScript== (function() { 'use strict'; var settings = {}; const settingsConfig = { class_label_topic: "💠 话题内容相关:", quick_mark: { type: 'checkbox', label: '快速收藏 ', default: true, style: '', info: '在帖子上增加一个⭐用于快速收藏到书签' }, show_floor_time: { type: 'checkbox', label: '更精确的回复时间', default: true, style: '', info: '帖子的回复时间改为绝对时间并精确到分钟' }, class_label_list: "💠 话题列表相关:", show_up_time: { type: 'checkbox', label: '显示话题时间', default: true, style: '', info: '话题列表的帖子显示创建/更新时间,老的帖子会褪色泛黄' }, class_label_profile: "💠 个人信息相关:", show_level_progress: { type: 'checkbox', label: '显示等级进度', default: true, style: '', info: '在页面右下角显示等级徽章,点击可查看升级进度' }, class_label_all: "💠 通用:", open_in_new: { type: 'checkbox', label: '新标签页打开', default: false, style: '', info: '让所有链接默认从新标签页打开' }, back_to_top: { type: 'checkbox', label: '“返回顶部”按钮', default: true, style: '', info: '在页面右下角显示一个快速返回顶部的按钮' }, class_label_end: "", }; // Load settings from storage Object.keys(settingsConfig).forEach(key => { if (typeof settingsConfig[key] === 'object') { settings[key] = GM_getValue(key, settingsConfig[key].default); } }); // --- Individual Menu Commands --- Object.keys(settingsConfig).forEach(key => { const config = settingsConfig[key]; if (typeof config === 'object' && config.type === 'checkbox') { const isEnabled = settings[key]; const label = `${isEnabled ? '✅' : '❌'} ${config.label.trim()}`; GM_registerMenuCommand(label, () => { GM_setValue(key, !isEnabled); window.location.reload(); }); } }); GM_registerMenuCommand('⚙️ 打开详细设置面板 (Open Settings Panel)', openSettings); // --- Settings Panel --- function openSettings() { if (document.querySelector('#mjjbox-custom-setting')) return; const shadow = document.createElement('div'); shadow.style = `position: fixed; top: 0; left: 0; z-index: 8888; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.6);`; const panel = document.createElement('div'); panel.style = `max-width: calc(100% - 100px); width: max-content; position: fixed; top: 50%; left: 50%; z-index: 9999; transform: translate(-50%, -50%); background-color: var(--secondary); color: var(--primary); padding: 15px 25px; box-shadow: 0 0 15px rgba(0, 0, 0, 0.7); max-height: calc(95vh - 40px); overflow-y: auto; border-radius: 8px;`; panel.id = "mjjbox-custom-setting"; let html = `<style type="text/css">#mjjbox-custom-setting { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } #mjjbox-custom-setting label { font-size: 16px; display: flex; justify-content: space-between; align-items: center; margin: 12px; } #mjjbox-custom-setting label span { color: #6bc; font-size: 12px; font-weight: normal; padding: 0 6px; margin-right: auto; } #mjjbox-custom-setting label input { margin: 0 5px 0 15px; } .settings-buttons { display: flex; justify-content: space-around; margin-top: 20px; } .settings-buttons button { user-select: none; color: #333; padding: 8px 16px; border-radius: 5px; border: none; line-height: normal; cursor: pointer; } #mjjbox-custom-setting hr { display: block; height: 1px; border: 0; border-top: 1px solid var(--primary-low); margin: 1em 0; padding: 0; }</style><h2 style="text-align:center; margin-top:.5rem;">MJJBox 自定义设置</h2>`; Object.keys(settingsConfig).forEach(key => { const cfg = settingsConfig[key]; if (typeof cfg === 'string') { html += `<hr><h3 style="margin-top:5px; font-size: 1.1em;">${cfg}</h3>`; } else { const val = settings[key]; const checked = cfg.type === 'checkbox' && val ? 'checked' : ''; html += `<label style="${cfg.style}">${cfg.label}<span>${cfg.info}</span><input type="${cfg.type}" id="ujs_set_${key}" value="${val}" ${checked}></label>`; } }); html += `<div class="settings-buttons"><button id="ld_userjs_apply" style="font-weight: bold; background: var(--tertiary); color: var(--secondary)">保存并刷新</button><button id="ld_userjs_reset" style="background: #fbb;">重置</button><button id="ld_userjs_close" style="background: #ddd;">取消</button></div>`; panel.innerHTML = html; document.body.append(shadow, panel); function saveAndReload() { Object.keys(settingsConfig).forEach(key => { const element = document.getElementById(`ujs_set_${key}`); if (element) GM_setValue(key, element.type === 'checkbox' ? element.checked : element.value); }); window.location.reload(); } document.querySelector('button#ld_userjs_apply').addEventListener('click', saveAndReload); document.querySelector('button#ld_userjs_reset').addEventListener('click', () => { Object.keys(settingsConfig).forEach(key => { if (typeof settingsConfig[key] === 'object') GM_deleteValue(key); }); window.location.reload(); }); function setting_hide() { panel.remove(); shadow.remove(); } document.querySelector('button#ld_userjs_close').addEventListener('click', setting_hide); shadow.addEventListener('click', setting_hide); } // == 功能区 == // Function 1: 快速收藏 if (settings.quick_mark) { const starSvg = `<svg class="svg-icon" aria-hidden="true" style="text-indent: 1px; transform: scale(1); width:18px; height:18px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path></svg></svg> `; let markMap = new Map(); function handleResponse(xhr, s, e) { xhr.onreadystatechange = () => { if (xhr.readyState === 4) (xhr.status === 200 ? s(xhr) : e(xhr)); }; } function TryParseJson(str) { try { return JSON.stringify(JSON.parse(str), null, 1); } catch (err) { return str; } } function deleteStarMark(btn, id) { if (markMap.has(id)) { const mid = markMap.get(id); const x = new XMLHttpRequest(); x.open('DELETE', `/bookmarks/${mid}`, !0); x.setRequestHeader('Content-Type','application/json'); x.setRequestHeader('x-requested-with','XMLHttpRequest'); x.setRequestHeader("x-csrf-token",document.head.querySelector("meta[name=csrf-token]")?.content); handleResponse(x, () => { btn.style.color='#777'; btn.title="收藏"; btn.onclick=()=>addStarMark(btn,id); },(err)=>console.error('删除收藏失败!',err.statusText,TryParseJson(err.responseText))); x.send(); } } function addStarMark(btn, id) { const x = new XMLHttpRequest(); x.open('POST','/bookmarks',!0); x.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8'); x.setRequestHeader('x-requested-with','XMLHttpRequest'); x.setRequestHeader("x-csrf-token",document.head.querySelector("meta[name=csrf-token]")?.content); const postData=`name=%E6%94%B6%E8%97%8F&auto_delete_preference=3&bookmarkable_id=${id}&bookmarkable_type=Post`; handleResponse(x,(res)=>{ btn.style.color='#fdd459'; btn.title="删除收藏"; const newMark=JSON.parse(res.responseText); markMap.set(String(newMark.bookmarkable_id),String(newMark.id)); btn.onclick=()=>deleteStarMark(btn,id); },(err)=>console.error('收藏失败!',err.statusText,TryParseJson(err.responseText))); x.send(postData); } function addMarkBtn() { document.querySelectorAll("article[data-post-id]").forEach(art=>{ const target=art.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > div.actions"); if (target&&!art.querySelector("span.star-bookmark")) { const id=art.getAttribute('data-post-id'); const star=document.createElement('span'); star.innerHTML=starSvg; star.className="star-bookmark"; star.style.cssText='cursor:pointer;margin:0 12px;'; if(markMap.has(id)){star.style.color='#fdd459';star.title="删除收藏";star.onclick=()=>deleteStarMark(star,id);}else{star.style.color='#777';star.title="收藏";star.onclick=()=>addStarMark(star,id);} target.after(star); } }); } function getStarMark() { const userEl = document.querySelector('#current-user button > img[src]'); if(!userEl) return; const match = userEl.getAttribute('src').match(/\/user_avatar\/[^\/]+\/([^\/]+)\/\d+\//); const username = match?.[1]; if(!username) return; const x = new XMLHttpRequest(); x.open('GET',`/u/${username}/user-menu-bookmarks`,!0); x.setRequestHeader("x-csrf-token",document.head.querySelector("meta[name=csrf-token]")?.content); handleResponse(x,(res)=>{ var r=JSON.parse(res.responseText); markMap.clear(); r.bookmarks.forEach(m=>markMap.set(m.bookmarkable_id.toString(),m.id.toString())); addMarkBtn();},(err)=>console.error('获取收藏列表失败:',err.statusText)); x.send(); } let lastUpdateTime=0; const mainNode=document.querySelector("#main-outlet"); if(mainNode){ new MutationObserver(()=>{const now=Date.now();if(now-lastUpdateTime>5e3){getStarMark();lastUpdateTime=now;}else{addMarkBtn();}}).observe(mainNode,{childList:!0,subtree:!0}); getStarMark();} } // Function 2: 显示话题时间 if (settings.show_up_time) { const getHue=(d,c)=>{const diff=Math.abs(c-d),base=2592e6,ratio=Math.min(Math.log(diff/base+1),1);return 120-140*ratio;}; const formatDate=d=>{const p=s=>String(s).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;}; const parseDate=s=>{let p;if(p=s.match(/(\d+)\s*年\s*(\d+)\s*月\s*(\d+)\s*日\s*(\d+):(\d+)/))return new Date(p[1],p[2]-1,p[3],p[4],p[5]);if(p=s.match(/(\w+)\s*(\d+),\s*(\d+)\s*(\d+):(\d+)\s*(am|pm)/i)){const m={Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11};let h=parseInt(p[4],10);if(p[6].toLowerCase()==='pm'&&h<12)h+=12;else if(p[6].toLowerCase()==='am'&&h===12)h=0;return new Date(p[3],m[p[1]],p[2],h,p[5]);}return null;}; GM_addStyle(`.topic-list .topic-list-data.age.activity{width:12em;text-align:left;}.topic-list .topic-list-data.age.activity>a.post-activity{font-size:13px;line-height:1.5;text-wrap:nowrap;padding:4px 5px;display:block;}`); const creatTimeShow=()=>{document.querySelectorAll(".topic-list-item").forEach(row=>{const item=row.querySelector(".num.topic-list-data.age.activity");if(!item||item.dataset.customized)return;item.dataset.customized="true";const timeSpan=item.querySelector("a.post-activity");if(!timeSpan)return;const timeInfo=item.title;let createStr=timeInfo.match(/创建日期:([\s\S]+?)最新:/)||timeInfo.match(/Created: ([\s\S]+?)Latest:/);let updateStr=timeInfo.match(/最新:([\s\S]+)/)||timeInfo.match(/Latest: ([\s\S]+)/);if(!createStr)return;createStr=(createStr[1]??'').trim();const createDate=parseDate(createStr);if(!createDate)return;const now=new Date(),createHue=getHue(createDate,now),formatCreate=formatDate(createDate);timeSpan.innerHTML=`<span style="color:hsl(${createHue}, 35%, 50%);">创建: ${formatCreate}</span><br>`;if(updateStr){updateStr=(updateStr[1]??'').trim();const updateDate=parseDate(updateStr);if(updateDate){const updateHue=getHue(updateDate,now),formatNew=formatDate(updateDate);timeSpan.innerHTML+=`<span style="color:hsl(${updateHue}, 35%, 50%);">最新: ${formatNew}</span>`;}}else{timeSpan.innerHTML+=`<span style="color:#888;">最新: 暂无回复</span>`;}const pastDays=Math.abs(createDate-now)/864e5,title=row.querySelector(".main-link");if(title){if(pastDays>120)title.style.filter="sepia(90%) brightness(85%)";else if(pastDays>60){title.style.opacity=0.8;title.style.filter="sepia(40%) brightness(85%)";}else if(pastDays>30){title.style.opacity=0.9;title.style.filter="grayscale(10%) sepia(10%)";}}});}; setInterval(creatTimeShow,1e3); } // Function 3: 新窗口打开 if (settings.open_in_new) { document.addEventListener('click', e => { const a = e.target.closest('a'); if (!a || !a.href || a.target || e.button !== 0 || e.ctrlKey || e.metaKey || a.href.startsWith('javascript:')) return; const ex = ['.d-header-icons','.nav-pills','.post-controls','.topic-meta-data .contents','.topic-timeline','.user-card-actions','.category-breadcrumb','.select-kit-header','.select-kit-row','.modal-body','.actions'].join(', '); if (a.closest(ex)) return; e.preventDefault(); e.stopImmediatePropagation(); window.open(a.href, '_blank'); }, true); } // Function 4: 显示更精确的回复时间 if (settings.show_floor_time) { GM_addStyle(`.post-info.post-date > a > .relative-date {font-size: 0;} .post-info.post-date > a > .relative-date::after {content: attr(title); font-size: 14px;}`); } // Function 5: 等级进度查看器 (MODIFIED) if (settings.show_level_progress) { (function() { if (window !== window.top) return; const levelNames = { 0: '青铜会员', 1: '白银会员', 2: '黄金会员', 3: '钻石会员', 4: '星曜会员' }; const levelRequirements = { 1: { topics_entered: 5, posts_read: 30, time_read: 600 }, 2: { days_visited: 15, topics_entered: 20, posts_read: 100, time_read: 3600, posts_created: 1, likes_received: 1, likes_given: 1, has_avatar: !0, has_bio: !0 }, 3: { days_visited_in_100: 50, topics_entered: 200, posts_read: 500, posts_created_in_100: 10, likes_received: 20, likes_given: 30, flagged_posts_ratio: .05 }, 4: { manual_promotion: !0 } }; const styles = `.mjjbox-level-badge{position:fixed;bottom:28px;right:25px;width:45px;height:45px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.25);transition:transform .3s,box-shadow .3s;border-radius:50%}.mjjbox-level-badge:hover{transform:scale(1.12);box-shadow:0 8px 30px rgba(0,0,0,.35)}.mjjbox-level-badge.level-0{background:linear-gradient(135deg,#9ca3af 0%,#6b7280 100%)}.mjjbox-level-badge.level-1{background:linear-gradient(135deg,#10b981 0%,#059669 100%)}.mjjbox-level-badge.level-2{background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%)}.mjjbox-level-badge.level-3{background:linear-gradient(135deg,#8b5cf6 0%,#7c3aed 100%)}.mjjbox-level-badge.level-4{background:linear-gradient(135deg,#f59e0b 0%,#d97706 100%)}.mjjbox-level-badge .level-icon-svg{position:absolute;width:100%;height:100%;padding:8px;box-sizing:border-box;fill:#fff;opacity:.4}.mjjbox-level-badge .level-text-overlay{position:relative;font-weight:700;font-size:13px;color:#fff;user-select:none}.mjjbox-level-modal{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:10000;opacity:0;visibility:hidden;transition:opacity .35s,visibility .35s;backdrop-filter:blur(6px)}.mjjbox-level-modal.show{opacity:1;visibility:visible}.mjjbox-level-modal-content{position:absolute;background:#fff;border-radius:16px;width:420px;max-width:90vw;max-height:85vh;padding:28px 30px 36px;box-shadow:0 20px 60px rgba(0,0,0,.35);overflow-y:auto;transform:scale(.92) translateY(30px);transition:transform .35s}.mjjbox-level-modal.show .mjjbox-level-modal-content{transform:scale(1) translateY(0)}.mjjbox-close-btn{position:absolute;top:12px;right:16px;background:0 0;border:none;font-size:28px;cursor:pointer;color:#000}.mjjbox-level-header{text-align:center}.mjjbox-level-title{margin:0;font-size:24px;font-weight:700;color:#000}.mjjbox-level-subtitle,.mjjbox-level-score{margin:4px 0 0;font-size:15px;color:#000}.mjjbox-progress-section h3{margin:0 0 18px;font-size:17px;color:#000}.mjjbox-progress-item{margin-bottom:14px}.mjjbox-progress-label{display:block;font-weight:600;margin-bottom:6px;color:#000}.mjjbox-progress-bar-container{display:flex;align-items:center;gap:12px}.mjjbox-progress-bar{flex:1;height:8px;background:#f3f4f6;border-radius:4px;overflow:hidden}.mjjbox-progress-fill{height:100%;background:linear-gradient(90deg,#10b981 0%,#34d399 100%);transition:width .4s}.mjjbox-progress-fill.incomplete{background:linear-gradient(90deg,#f87171 0%,#fca5a5 100%)}.mjjbox-progress-required,.mjjbox-progress-tooltip{font-size:13px;color:#000}.mjjbox-progress-undone{color:#ef4444}.mjjbox-progress-done{color:#10b981}`; GM_addStyle(styles); const getCurrentUsername=()=>{try{if(typeof Discourse!=='undefined'&&Discourse.User&&Discourse.User.current())return Discourse.User.current()?.username||null}catch(e){console.error('获取用户名失败:',e)}return null}; const showNotification=(msg,type='info',dur=3e3)=>{const n=document.createElement('div');n.style.cssText=`position:fixed;top:90px;right:24px;padding:12px 18px;border-radius:8px;color:#fff;font-weight:600;z-index:10001;opacity:0;transform:translateX(120%);transition:all .3s;background:${type==='error'?'#ef4444':'#10b981'}`;n.textContent=msg;document.body.appendChild(n);setTimeout(()=>{n.style.opacity='1';n.style.transform='translateX(0)'},100);setTimeout(()=>{n.style.opacity='0';n.style.transform='translateX(120%)';setTimeout(()=>n.remove(),300)},dur)}; const createLevelBadge=()=>{const b=document.createElement('div');b.className='mjjbox-level-badge';const svg=`<svg viewBox="0 0 24 24" class="level-icon-svg" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-3z"/></svg>`;const text=document.createElement('span');text.className='level-text-overlay';text.textContent='LV?';b.innerHTML=svg;b.appendChild(text);b.title='点击查看等级进度';b.addEventListener('click',fetchUserLevel);document.body.appendChild(b);return b}; const updateLevelBadge=(level,username)=>{const b=document.querySelector('.mjjbox-level-badge .level-text-overlay');if(!b)return;const badge=b.closest('.mjjbox-level-badge');b.textContent=`LV ${level}`;badge.className=`mjjbox-level-badge level-${level}`;badge.title=`${username} - ${levelNames[level]||'未知等级'}(点击查看详情)`}; const fetchUserLevel=()=>{const u=getCurrentUsername();if(!u)return showNotification('❌ 无法获取当前用户名','error');let summaryData=null,userData=null,done=0;const checkDone=()=>{done++;if(done===2)processUserData(summaryData,userData,u)};GM_xmlhttpRequest({method:'GET',url:`https://mjjbox.com/u/${u}/summary.json`,timeout:15e3,headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'},onload:r=>{if(r.status===200)try{summaryData=JSON.parse(r.responseText)}catch(e){}checkDone()},onerror:checkDone});GM_xmlhttpRequest({method:'GET',url:`https://mjjbox.com/u/${u}.json`,timeout:15e3,headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'},onload:r=>{if(r.status===200)try{userData=JSON.parse(r.responseText)}catch(e){}checkDone()},onerror:checkDone})}; const processUserData=(summaryData,userData,username)=>{if(!summaryData||!userData)return showNotification('❌ 获取用户数据失败','error');const user=userData.user||summaryData.users?.[0],userSummary=summaryData.user_summary;if(user&&typeof user.trust_level==='number'){const level=user.trust_level;updateLevelBadge(level,username);showNotification(`✅ 等级信息获取成功:LV${level} ${levelNames[level]}`,'success',2e3);const modal=createLevelModal({level,username,userData:{user,userSummary,gamification_score:user.gamification_score||0}});document.body.appendChild(modal);setTimeout(()=>modal.classList.add('show'),10)}else{showNotification('❌ 无法解析用户等级信息','error')}}; const calculateLevelProgress=(level,data)=>{if(!data?.userSummary)return{items:[],achievedCount:0,totalCount:0};const us=data.userSummary,u=data.user,next=level+1,req=levelRequirements[next];if(!req)return{items:[{label:'升级方式',current:'联系管理员',required:'手动提升',isMet:!1}],achievedCount:0,totalCount:1};const items=[];let achieved=0;const add=(l,c,r,isTime=!1)=>{const met=c>=r;items.push({label:l,current:c,required:r,isMet:met,percentage:Math.min(c/r*100,100),isTime});if(met)achieved++};const daysVisited100=us.days_visited||0;if(req.topics_entered!==undefined)add('阅读主题数',us.topics_entered||0,req.topics_entered);if(req.posts_read!==undefined)add('阅读帖子数',us.posts_read_count||0,req.posts_read);if(req.time_read!==undefined)add('总阅读时间(分)',Math.floor((us.time_read||0)/60),Math.floor(req.time_read/60),!0);if(req.days_visited!==undefined)add('累计访问天数',us.days_visited||0,req.days_visited);if(req.days_visited_in_100!==undefined)add('100天内访问天数',daysVisited100,req.days_visited_in_100);if(req.posts_created!==undefined)add('累计发帖数',us.topic_count||0,req.posts_created);if(req.posts_created_in_100!==undefined)add('100天内发帖/回复数',(us.topic_count||0)+(us.post_count||0),req.posts_created_in_100);if(req.likes_received!==undefined)add('收到赞数',us.likes_received||0,req.likes_received);if(req.likes_given!==undefined)add('送出赞数',us.likes_given||0,req.likes_given);if(req.has_avatar!==undefined){const has=!!(u.avatar_template&&!u.avatar_template.includes('letter_avatar')&&!u.avatar_template.includes('system_avatar'));items.push({label:'已上传头像',current:has?'已上传':'未上传',required:'已上传',isMet:has,isBoolean:!0});if(has)achieved++}if(req.has_bio!==undefined){const has=!!(u.bio_raw&&u.bio_raw.trim());items.push({label:'已填写基本资料',current:has?'已填写':'未填写',required:'已填写',isMet:has,isBoolean:!0});if(has)achieved++}if(req.flagged_posts_ratio!==undefined){const flaggedRatio=0;items.push({label:'被举报/隐藏帖子比例',current:`${(flaggedRatio*100).toFixed(1)}%`,required:`${(req.flagged_posts_ratio*100).toFixed(0)}% 以内`,isMet:flaggedRatio<=req.flagged_posts_ratio});if(flaggedRatio<=req.flagged_posts_ratio)achieved++}items.sort((a,b)=>a.isMet?1:-1);return{items,achievedCount:achieved,totalCount:items.length}}; const createLevelModal=({level,username,userData})=>{const modal=document.createElement('div');modal.className='mjjbox-level-modal';const progress=calculateLevelProgress(level,userData),currentName=levelNames[level]||'未知等级',content=document.createElement('div');content.className='mjjbox-level-modal-content';const badgeRect=document.querySelector('.mjjbox-level-badge').getBoundingClientRect();content.style.position='absolute';content.style.right=`${window.innerWidth-badgeRect.right}px`;content.style.bottom=`${window.innerHeight-badgeRect.top+15}px`;content.innerHTML=`<button class="mjjbox-close-btn">×</button><div class="mjjbox-level-header"><h2 class="mjjbox-level-title">${username}</h2><p class="mjjbox-level-subtitle">当前等级:LV${level} ${currentName}</p><p class="mjjbox-level-score">当前积分:${userData.gamification_score}</p></div><div class="mjjbox-progress-section"><h3>${level>=4?'已达到最高等级':`晋级到 LV${level+1} ${levelNames[level+1]} 的进度 (${progress.achievedCount}/${progress.totalCount})`}</h3>${progress.items.map(item=>{const cur=item.isTime?`${item.current} 分`:item.current,need=item.isTime?`${item.required} 分`:item.required,met=item.isMet,icon=met?'✅':'❌';return`<div class="mjjbox-progress-item"><span class="mjjbox-progress-label">${item.label}</span><div class="mjjbox-progress-bar-container"><div class="mjjbox-progress-bar"><div class="mjjbox-progress-fill ${met?'':'incomplete'}" style="width:${item.percentage||0}%"></div></div><span class="mjjbox-progress-required ${met?'':'mjjbox-progress-undone'}">需要: ${item.isBoolean?item.required:need} ${icon}</span></div><div class="mjjbox-progress-tooltip">当前: <span class="${met?'mjjbox-progress-done':'mjjbox-progress-undone'}">${item.isBoolean?item.current:cur} ${icon}</span></div></div>`}).join('')}${progress.items.length===0?'<div style="text-align:center;padding:20px;color:#000;">🎉 恭喜!您已达到最高等级!</div>':''}</div>`;modal.appendChild(content);const hide=()=>{modal.classList.remove('show');setTimeout(()=>modal.remove(),300)};content.querySelector('.mjjbox-close-btn').addEventListener('click',hide);modal.addEventListener('click',e=>{if(e.target===modal)hide()});const escHandler=e=>{if(e.key==='Escape'){hide();document.removeEventListener('keydown',escHandler)}};document.addEventListener('keydown',escHandler);return modal}; (function init(){if(document.readyState==='loading')return document.addEventListener('DOMContentLoaded',init);createLevelBadge()})(); })(); } // Function 6: 返回顶部按钮 if (settings.back_to_top) { GM_addStyle(`#back-to-top-btn { position: fixed; bottom: 28px; right: 90px; width: 45px; height: 45px; background-color: var(--primary-medium, #666); color: #fff; border: none; border-radius: 50%; cursor: pointer; display: none; justify-content: center; align-items: center; font-size: 24px; z-index: 9998; opacity: 0.7; transition: opacity 0.3s, background-color 0.3s, transform 0.3s; } #back-to-top-btn:hover { opacity: 1; transform: scale(1.1); background-color: var(--tertiary, #3b82f6); }`); const btn = document.createElement('button'); btn.id = 'back-to-top-btn'; btn.innerHTML = '↑'; btn.title = '返回顶部'; document.body.appendChild(btn); window.addEventListener('scroll', () => { if (window.scrollY > 300) { btn.style.display = 'flex'; } else { btn.style.display = 'none'; } }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } })();