请查看最新版:积分来源与抽奖机制同步,增加快捷签到的等级进度插件(v1.6.0)
受到新规影响,升级进度所需要的赞较之前有了巨大的变化,目前论坛暂未有官方的进度查看页面,且论坛所有的等级进度查看器脚本里的进度都是新规之前。
查看新规之前的脚本文件,得益于前辈大佬的优秀注释,仅需要修改如下几个地方:
/* ========== 官方默认晋级条件(完全同步) ========== */
const levelRequirements = {
1: {
topics_entered: 5,
posts_read: 30,
time_read: 10 * 60
},
2: {
days_visited: 15,
topics_entered: 20,
posts_read: 100,
time_read: 60 * 60,
posts_created: 1,
likes_received: 15, /* 新规为15 */
likes_given: 20, /* 新规为20 */
has_avatar: true,
has_bio: true
},
3: {
days_visited_in_100: 50,
topics_entered: 200,
posts_read: 500,
posts_created_in_100: 10,
likes_received: 50, /* 新规为50 */
likes_given: 100, /* 新规为100 */
flagged_posts_ratio: 0.05
},
4: {
manual_promotion: true
}
};
不想手动更改的,可以直接复制下面的内容:
- Update: 实际测试原版存在数据不跟着论坛更新时间及时生效的问题,修改了部分内容
// ==UserScript==
// @name MJJBOX 等级进度查看器(新规版)
// @namespace http://tampermonkey.net/
// @version 1.5.2
// @description 依据官方默认等级规则显示进度,UI美化,弹窗位置固定在徽章下方
// @author AI Assistant
// @match https://mjjbox.com/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
(() => {
'use strict';
if (window !== window.top) return;
/* ========== 等级名称(与官方同步) ========== */
const levelNames = {
0: '青铜会员',
1: '白银会员',
2: '黄金会员',
3: '钻石会员',
4: '星曜会员'
};
/* ========== 官方默认晋级条件(完全同步) ========== */
const levelRequirements = {
1: {
topics_entered: 5,
posts_read: 30,
time_read: 10 * 60
},
2: {
days_visited: 15,
topics_entered: 20,
posts_read: 100,
time_read: 60 * 60,
posts_created: 1,
likes_received: 15,
likes_given: 20,
has_avatar: true,
has_bio: true
},
3: {
days_visited_in_100: 50,
topics_entered: 200,
posts_read: 500,
posts_created_in_100: 10,
likes_received: 50,
likes_given: 100,
flagged_posts_ratio: 0.05
},
4: {
manual_promotion: true
}
};
/* ========== CSS(黑字 + 绿/红状态色) ========== */
const styles = `
.mjjbox-level-badge {
position: fixed; top: 20px; right: 20px;
width: 56px; height: 56px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 12px; color: #fff;
cursor: pointer; z-index: 9999;
box-shadow: 0 4px 20px rgba(0,0,0,.25);
transition: transform .3s,box-shadow .3s;
border: 2px solid #fff;
}
.mjjbox-level-badge:hover { transform: scale(1.1); box-shadow: 0 6px 25px rgba(0,0,0,.3); }
.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-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: 12px;
width: 360px; max-width: 90vw; max-height: 80vh;
padding: 16px 16px 24px;
box-shadow: 0 16px 40px rgba(0,0,0,.3);
overflow-y: auto;
transform: scale(.9) translateY(-20px);
transition: transform .35s;
/* 滚动条样式 (Firefox) */
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f3f3f3;
}
/* WebKit 滚动条样式 (Chrome, Edge, Safari) */
.mjjbox-level-modal-content::-webkit-scrollbar {
width: 6px;
}
.mjjbox-level-modal-content::-webkit-scrollbar-track {
background: #f3f3f3;
border-radius: 3px;
}
.mjjbox-level-modal-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.mjjbox-level-modal-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 小屏适配:在更小的屏幕上进一步缩小 */
@media (max-width: 480px) {
.mjjbox-level-modal-content {
width: 320px;
max-width: 92vw;
padding: 14px 14px 22px;
border-radius: 10px;
}
.mjjbox-level-title { font-size: 20px; }
.mjjbox-level-subtitle, .mjjbox-level-score { font-size: 14px; }
.mjjbox-progress-label { font-size: 14px; }
.mjjbox-progress-required, .mjjbox-progress-tooltip { font-size: 12px; }
.mjjbox-close-btn { font-size: 24px; top: 8px; right: 12px; }
}
.mjjbox-close-btn {
position: absolute; top: 10px; right: 14px;
background: none; border: none;
font-size: 26px; cursor: pointer; color: #000;
}
.mjjbox-level-header { text-align: center; }
.mjjbox-level-title { margin: 0; font-size: 20px; font-weight: 700; color: #000; }
.mjjbox-level-subtitle, .mjjbox-level-score { margin: 4px 0 0; font-size: 14px; color: #000; }
.mjjbox-progress-section h3 { margin: 0 0: 14px; font-size: 16px; color: #000; }
.mjjbox-progress-item { margin-bottom: 10px; }
.mjjbox-progress-label { display: block; font-weight: 600; margin-bottom: 4px; color: #000; font-size: 14px; }
.mjjbox-progress-bar-container { display: flex; align-items: center; gap: 6px; }
.mjjbox-progress-bar { flex: 1; height: 6px; background: #f3f4f6; border-radius: 3px; 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: 12px; color: #000; }
.mjjbox-progress-undone { color: #ef4444; }
.mjjbox-progress-done { color: #10b981; }
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
/* ========== 通用小工具 ========== */
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 = 3000) => {
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 badge = document.createElement('div');
badge.className = 'mjjbox-level-badge';
badge.textContent = 'LV ?';
badge.title = '点击查看等级进度';
badge.addEventListener('click', fetchUserLevel);
document.body.appendChild(badge);
return badge;
};
const updateLevelBadge = (level, username) => {
const badge = document.querySelector('.mjjbox-level-badge');
if (!badge) return;
badge.textContent = `LV ${level}`;
badge.className = `mjjbox-level-badge level-${level}`;
badge.title = `${username} - ${levelNames[level] || '未知等级'}(点击查看详情)`;
};
/* ========== 拉取数据 ========== */
const fetchUserLevel = () => {
const username = getCurrentUsername();
if (!username) return showNotification('❌ 无法获取当前用户名', 'error');
let summaryData = null;
let userData = null;
let done = 0;
const checkDone = () => { done++; if (done === 2) processUserData(summaryData, userData, username); };
GM_xmlhttpRequest({
method: 'GET',
url: `https://mjjbox.com/u/${username}/summary.json`,
timeout: 15000,
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: resp => { if (resp.status === 200) { try { summaryData = JSON.parse(resp.responseText); } catch {} } checkDone(); },
onerror: checkDone
});
GM_xmlhttpRequest({
method: 'GET',
url: `https://mjjbox.com/u/${username}.json`,
timeout: 15000,
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
onload: resp => { if (resp.status === 200) { try { userData = JSON.parse(resp.responseText); } catch {} } checkDone(); },
onerror: checkDone
});
};
/* ========== 处理数据 ========== */
const processUserData = (summaryData, userData, username) => {
if (!summaryData || !userData) return showNotification('❌ 获取用户数据失败', 'error');
const user = userData.user || summaryData.users?.[0];
const 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', 2000);
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 = (currentLevel, userData) => {
if (!userData?.userSummary) return { items: [], achievedCount: 0, totalCount: 0 };
const us = userData.userSummary;
const u = userData.user;
const next = currentLevel + 1;
const req = levelRequirements[next];
if (!req) return { items: [{ label: '升级方式', current: '联系管理员', required: '手动提升', isMet: false }], achievedCount: 0, totalCount: 1 };
const items = [];
let achieved = 0;
const add = (label, current, required, isTime = false) => {
const met = current >= required;
items.push({ label, current, required, isMet: met, percentage: Math.min((current / required) * 100, 100), isTime });
if (met) achieved++;
};
/* 新增:100 天内访问天数(官方字段目前需自行计算,这里先用 days_visited 近似) */
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), true);
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: true });
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: true });
if (has) achieved++;
}
/* 被隐藏/举报比例(官方无直接字段,这里留 0 占位) */
if (req.flagged_posts_ratio !== undefined) {
const flaggedRatio = 0; // 暂无 API,直接给 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);
const currentName = levelNames[level] || '未知等级';
const content = document.createElement('div');
content.className = 'mjjbox-level-modal-content';
// 计算位置:徽章下方
const badgeRect = document.querySelector('.mjjbox-level-badge').getBoundingClientRect();
let top = badgeRect.bottom + 18;
let right = window.innerWidth - badgeRect.right;
if (top + 500 > window.innerHeight) top = badgeRect.top - 500 - 18;
if (right - 420 < 0) right = 10;
content.style.top = `${top}px`;
content.style.right = `${right}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;
const need = item.isTime ? `${item.required} 分钟` : item.required;
const met = item.isMet;
const 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);
content.querySelector('.mjjbox-close-btn').addEventListener('click', () => {
modal.classList.remove('show');
setTimeout(() => modal.remove(), 300);
});
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.classList.remove('show');
setTimeout(() => modal.remove(), 300);
}
});
document.addEventListener('keydown', function esc(e) {
if (e.key === 'Escape') {
modal.classList.remove('show');
setTimeout(() => modal.remove(), 300);
document.removeEventListener('keydown', esc);
}
});
return modal;
};
/* ========== 启动 ========== */
const init = () => {
if (document.readyState === 'loading') return document.addEventListener('DOMContentLoaded', init);
createLevelBadge();
};
init();
})();
参考链接: