大佬们好,我是刚入论坛的丁真
看了不少帖子发现好多新人对于查看等级进度有需求,目前论坛貌似还没有相关功能
hui总发过等级相关的贴 📌 社区权限对照表和升级条件
有需要的话直接扔油猴刷新页面就好啦
不知道填写基本资料这个是怎么满足的,目前是判断个人资料中自我介绍内容
以及黄金等级后,对于判断过去100 天的数据,没找到能获取数据的接口,得依赖数据库统计了,还是等论坛大佬们推出相关功能吧
黄金之前大伙儿还是能凑活用一用的
使用有问题可以用Exia佬的脚本论坛等级查看(自用版)
// ==UserScript==
// @name MJJBOX 等级进度查看器
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 为 MJJBOX 论坛添加等级进度查看功能,显示当前等级和升级进度
// @author AI Assistant
// @match https://mjjbox.com/*
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @connect mjjbox.com
// ==/UserScript==
(function() {
'use strict';
// 检查是否在iframe中运行,如果是则不执行
if (window !== window.top) {
return;
}
// 等级名称映射
const levelNames = {
0: '青铜会员',
1: '白银会员',
2: '黄金会员',
3: '钻石会员',
4: '星曜会员'
};
// 等级规则配置
const levelRequirements = {
1: { // TL0 → TL1(青铜会员 → 白银会员)
topics_entered: 5,
posts_read: 30,
time_read: 10 * 60 // 10分钟,以秒为单位
},
2: { // TL1 → TL2(白银会员 → 黄金会员)
days_visited: 15,
topics_entered: 20,
posts_read: 100,
time_read: 60 * 60, // 60分钟
posts_created: 1,
likes_received: 1,
likes_given: 1,
has_avatar: true,
has_bio: true
},
3: { // TL2 → TL3(黄金会员 → 钻石会员)
days_visited_in_100: 50, // 过去100天访问50天
topics_entered: 200,
posts_read: 500,
posts_created_in_100: 10, // 过去100天发表10篇回复
likes_received: 20,
likes_given: 30,
flagged_posts_ratio: 0.05 // 被举报帖子比例小于5%
},
4: { // TL3 → TL4(钻石会员 → 星曜会员)
manual_promotion: true // 需要管理员手动提升
}
};
// ===== 样式定义 =====
const styles = `
.mjjbox-level-badge {
position: fixed;
top: 20px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 12px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: all 0.3s ease;
border: 3px solid white;
}
.mjjbox-level-badge:hover {
transform: scale(1.1);
box-shadow: 0 6px 25px rgba(0,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-badge.level-5 {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.mjjbox-level-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.mjjbox-level-modal.show {
opacity: 1;
visibility: visible;
}
.mjjbox-level-modal-content {
position: absolute;
background: white;
border-radius: 12px;
padding: 24px;
width: 420px;
max-height: 500px;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transform: scale(0.8) translateY(-20px);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: top right;
}
.mjjbox-level-modal.show .mjjbox-level-modal-content {
transform: scale(1) translateY(0);
}
.mjjbox-level-modal-content::before {
content: '';
position: absolute;
top: -8px;
right: 30px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid white;
filter: drop-shadow(0 -2px 4px rgba(0, 0, 0, 0.1));
}
.mjjbox-level-modal-content.arrow-bottom::before {
top: auto;
bottom: -8px;
border-top: 8px solid white;
border-bottom: none;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.mjjbox-level-header {
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 15px;
}
.mjjbox-level-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0 0 8px 0;
}
.mjjbox-level-subtitle {
color: #666;
font-size: 14px;
margin: 0 0 4px 0;
}
.mjjbox-level-score {
color: #ff6b35;
font-size: 14px;
font-weight: 500;
margin: 0 0 8px 0;
}
.mjjbox-progress-section {
margin: 20px 0;
}
.mjjbox-progress-item {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.mjjbox-progress-item:last-child {
border-bottom: none;
}
.mjjbox-progress-label {
font-weight: 500;
color: #333;
margin-bottom: 8px;
display: block;
}
.mjjbox-progress-bar-container {
display: flex;
align-items: center;
gap: 12px;
}
.mjjbox-progress-bar {
flex: 1;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.mjjbox-progress-fill {
height: 100%;
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
border-radius: 4px;
transition: width 0.6s ease;
position: relative;
}
.mjjbox-progress-fill.incomplete {
background: linear-gradient(90deg, #f59e0b 0%, #d97706 100%);
}
.mjjbox-progress-required {
color: #666;
font-size: 14px;
font-weight: 500;
min-width: 40px;
text-align: right;
}
.mjjbox-progress-tooltip {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 1000;
pointer-events: none;
}
.mjjbox-progress-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-top-color: rgba(0, 0, 0, 0.9);
}
.mjjbox-progress-item:hover .mjjbox-progress-tooltip {
opacity: 1;
visibility: visible;
}
.mjjbox-close-btn {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 5px;
}
.mjjbox-close-btn:hover {
color: #333;
}
.mjjbox-loading {
text-align: center;
padding: 40px;
color: #666;
}
.mjjbox-error {
text-align: center;
padding: 40px;
color: #ef4444;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.mjjbox-level-badge.loading {
animation: pulse 2s infinite;
}
`;
// 添加样式到页面
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
// ===== 工具函数 =====
// 获取当前用户名
function getCurrentUsername() {
try {
// 从 Discourse 对象获取用户名
if (typeof Discourse !== 'undefined' && Discourse.User && Discourse.User.current()) {
const user = Discourse.User.current();
if (user && user.username) {
return user.username;
}
}
return null;
} catch (e) {
console.error('获取用户名失败:', e);
return null;
}
}
// 显示通知
function showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
padding: 12px 16px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10001;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
max-width: 300px;
`;
switch (type) {
case 'success':
notification.style.background = '#10b981';
break;
case 'error':
notification.style.background = '#ef4444';
break;
case 'warning':
notification.style.background = '#f59e0b';
break;
default:
notification.style.background = '#3b82f6';
}
notification.textContent = message;
document.body.appendChild(notification);
// 显示动画
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(0)';
}, 100);
// 自动隐藏
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, duration);
}
// ===== 核心功能函数 =====
// 创建等级徽章
function createLevelBadge() {
const badge = document.createElement('div');
badge.className = 'mjjbox-level-badge loading';
badge.textContent = 'LV ?';
badge.title = '点击查看等级进度';
badge.addEventListener('click', (event) => {
event.stopPropagation();
showLevelModal(event);
});
document.body.appendChild(badge);
return badge;
}
// 更新等级徽章
function 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] || '未知等级'} (点击查看详情)`;
}
// 获取用户等级数据
function fetchUserLevel() {
const username = getCurrentUsername();
if (!username) {
showNotification('❌ 无法获取当前用户名', 'error');
return;
}
// 并行请求两个接口
let summaryData = null;
let userData = null;
let completedRequests = 0;
const checkComplete = () => {
completedRequests++;
if (completedRequests === 2) {
processUserData(summaryData, userData, username);
}
};
// 使用GM_cookie API获取Cookie
if (typeof GM_cookie !== 'undefined') {
GM_cookie.list({domain: 'mjjbox.com'}, (cookies) => {
let cookieValue = '';
cookies.forEach(cookie => {
if (cookie.name === '_forum_session') {
cookieValue += `_forum_session=${cookie.value}; `;
} else if (cookie.name === '_t') {
cookieValue += `_t=${cookie.value}; `;
}
});
if (cookieValue) {
makeRequests(cookieValue.trim());
} else {
showNotification('❌ 无法获取登录信息,请确保已登录', 'error');
}
});
} else {
console.error('❌ GM_cookie API不可用');
showNotification('❌ Cookie API不可用', 'error');
}
// 发起请求的函数
function makeRequests(cookieValue) {
// 请求 summary.json
GM_xmlhttpRequest({
method: "GET",
url: `https://mjjbox.com/u/${username}/summary.json`,
timeout: 15000,
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Referer': 'https://mjjbox.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Cookie': cookieValue
},
onload: function(response) {
if (response.status === 200) {
try {
summaryData = JSON.parse(response.responseText);
} catch (e) {
console.error('解析 summary 数据失败:', e);
}
} else {
console.error(`summary 请求失败: HTTP ${response.status}`);
}
checkComplete();
},
onerror: function(error) {
console.error('summary 网络请求失败:', error);
checkComplete();
}
});
// 请求 user.json
GM_xmlhttpRequest({
method: "GET",
url: `https://mjjbox.com/u/${username}.json`,
timeout: 15000,
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Referer': 'https://mjjbox.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Cookie': cookieValue
},
onload: function(response) {
if (response.status === 200) {
try {
userData = JSON.parse(response.responseText);
} catch (e) {
console.error('解析 user 数据失败:', e);
}
} else {
console.error(`user 请求失败: HTTP ${response.status}`);
}
checkComplete();
},
onerror: function(error) {
console.error('user 网络请求失败:', error);
checkComplete();
}
});
}
}
// 处理用户数据
function processUserData(summaryData, userData, username) {
if (!summaryData || !userData) {
showNotification('❌ 获取用户数据失败', 'error');
return;
}
// 优先使用userData.user,它包含完整的用户信息
const user = userData.user || summaryData.users[0];
const userSummary = summaryData.user_summary;
if (user && user.trust_level !== undefined) {
const currentLevel = user.trust_level;
// 更新等级徽章
updateLevelBadge(currentLevel, username);
// 显示获取成功提示
showNotification(`✅ 等级信息获取成功: LV${currentLevel} ${levelNames[currentLevel] || '未知等级'}`, 'success', 2000);
// 直接显示弹窗
const levelData = {
level: currentLevel,
username: username,
userData: {
user: user,
userSummary: userSummary,
gamification_score: user.gamification_score || 0
}
};
// 获取徽章位置并显示弹窗
const badge = document.querySelector('.mjjbox-level-badge');
const badgeRect = badge.getBoundingClientRect();
const modal = createLevelModal(levelData, badgeRect);
document.body.appendChild(modal);
// 显示动画
setTimeout(() => {
modal.classList.add('show');
}, 10);
} else {
console.error('用户数据中未找到 trust_level');
showNotification('❌ 无法获取用户等级信息', 'error');
}
}
// 计算升级进度
function calculateLevelProgress(currentLevel, userData) {
if (!userData || !userData.userSummary) {
return { items: [], achievedCount: 0, totalCount: 0 };
}
const userSummary = userData.userSummary;
const user = userData.user;
const nextLevel = currentLevel + 1;
const requirements = levelRequirements[nextLevel];
if (!requirements) {
// 已达到最高等级或需要手动提升
if (currentLevel >= 4) {
return {
items: [{
label: '升级方式',
current: '联系管理员',
required: '手动提升',
isMet: false
}],
achievedCount: 0,
totalCount: 1
};
}
return { items: [], achievedCount: 0, totalCount: 0 };
}
const items = [];
let achievedCount = 0;
// 检查各项要求
if (requirements.topics_entered !== undefined) {
const current = userSummary.topics_entered || 0;
const required = requirements.topics_entered;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '阅读主题数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.posts_read !== undefined) {
const current = userSummary.posts_read_count || 0;
const required = requirements.posts_read;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '阅读帖子数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.time_read !== undefined) {
const current = userSummary.time_read || 0;
const required = requirements.time_read;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '阅读时间',
current: current,
required: required,
isMet: isMet,
percentage: percentage,
isTime: true
});
if (isMet) achievedCount++;
}
if (requirements.days_visited !== undefined) {
const current = userSummary.days_visited || 0;
const required = requirements.days_visited;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '访问天数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.posts_created !== undefined) {
const current = (userSummary && typeof userSummary.topic_count === 'number') ? userSummary.topic_count : 0;
const required = requirements.posts_created;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '发帖数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.likes_received !== undefined) {
const current = userSummary.likes_received || 0;
const required = requirements.likes_received;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '收到赞数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.likes_given !== undefined) {
const current = userSummary.likes_given || 0;
const required = requirements.likes_given;
const isMet = current >= required;
const percentage = Math.min((current / required) * 100, 100);
items.push({
label: '给出赞数',
current: current,
required: required,
isMet: isMet,
percentage: percentage
});
if (isMet) achievedCount++;
}
if (requirements.has_avatar !== undefined) {
// 检查是否有自定义头像
// 如果有custom_avatar_template且不是系统默认头像,则认为已上传
const hasCustomAvatar = user.custom_avatar_template &&
user.custom_avatar_template.trim() !== '' &&
!user.custom_avatar_template.includes('letter_avatar') &&
!user.custom_avatar_template.includes('system_avatar');
const isMet = hasCustomAvatar;
items.push({
label: '上传头像',
current: isMet ? '已上传' : '未上传',
required: '已上传',
isMet: isMet,
isBoolean: true
});
if (isMet) achievedCount++;
}
if (requirements.has_bio !== undefined) {
// 检查是否有个人简介
const hasBio = user.bio_raw && user.bio_raw.trim() !== '';
const isMet = hasBio;
items.push({
label: '填写基本资料',
current: isMet ? '已填写' : '未填写',
required: '已填写',
isMet: isMet,
isBoolean: true
});
if (isMet) achievedCount++;
}
// 排序:未完成的指标显示在前面
items.sort((a, b) => {
if (a.isMet === b.isMet) return 0;
return a.isMet ? 1 : -1; // 未完成(false)排在前面
});
return {
items: items,
achievedCount: achievedCount,
totalCount: items.length
};
}
// 显示等级信息弹窗
function showLevelModal(event) {
showNotification('🔍 正在获取等级信息...', 'info');
fetchUserLevel();
}
// 创建等级信息弹窗
function createLevelModal(levelData, badgeRect) {
const modal = document.createElement('div');
modal.className = 'mjjbox-level-modal';
const progress = calculateLevelProgress(levelData.level, levelData.userData);
const currentLevelName = levelNames[levelData.level] || '未知等级';
const nextLevelName = levelNames[levelData.level + 1] || '最高等级';
// 计算弹窗位置
const modalContent = document.createElement('div');
modalContent.className = 'mjjbox-level-modal-content';
// 设置弹窗位置 - 在徽章下方偏左一点
const top = badgeRect.bottom + 18; // 增加一点距离给箭头
const right = window.innerWidth - badgeRect.right;
modalContent.style.top = `${top}px`;
modalContent.style.right = `${right}px`;
// 如果弹窗会超出屏幕底部,则显示在徽章上方
if (top + 500 > window.innerHeight) {
modalContent.style.top = `${badgeRect.top - 500 - 18}px`;
modalContent.classList.add('arrow-bottom'); // 箭头指向下方
}
// 如果弹窗会超出屏幕左侧,则调整位置
if (badgeRect.right - 420 < 0) {
modalContent.style.right = '10px';
}
modalContent.innerHTML = `
<button class="mjjbox-close-btn">×</button>
<div class="mjjbox-level-header">
<h2 class="mjjbox-level-title">${levelData.username}</h2>
<p class="mjjbox-level-subtitle">当前等级: LV${levelData.level} ${currentLevelName}</p>
<p class="mjjbox-level-score">当前积分: ${levelData.userData.gamification_score || 0}</p>
</div>
<div class="mjjbox-progress-section">
<h3 style="margin-bottom: 15px; color: #333;">
${levelData.level >= 4 ? '已达到最高等级' : `升级到 LV${levelData.level + 1} ${nextLevelName} 的进度 (${progress.achievedCount}/${progress.totalCount})`}
</h3>
${progress.items.map(item => {
if (item.isBoolean) {
// 布尔值类型:显示状态图标
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 ${item.isMet ? '' : 'incomplete'}"
style="width: ${item.isMet ? 100 : 0}%"></div>
</div>
<span class="mjjbox-progress-required">${item.required}</span>
</div>
<div class="mjjbox-progress-tooltip">
状态: ${item.current}
</div>
</div>
`;
} else {
// 数值类型:显示进度条
const displayValue = item.isTime ?
`${Math.floor(item.current / 60)}分钟` :
item.current;
const displayRequired = item.isTime ?
`${Math.floor(item.required / 60)}分钟` :
item.required;
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 ${item.isMet ? '' : 'incomplete'}"
style="width: ${item.percentage}%"></div>
</div>
<span class="mjjbox-progress-required">${displayRequired}</span>
</div>
<div class="mjjbox-progress-tooltip">
当前: ${displayValue} / 需要: ${displayRequired}
</div>
</div>
`;
}
}).join('')}
${progress.items.length === 0 ? `
<div style="text-align: center; padding: 20px; color: #666;">
${levelData.level >= 4 ? '🎉 恭喜!您已达到最高等级!' : '暂无升级要求信息'}
</div>
` : ''}
</div>
`;
// 添加内容到模态框
modal.appendChild(modalContent);
// 关闭按钮事件
const closeBtn = modalContent.querySelector('.mjjbox-close-btn');
closeBtn.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);
}
});
// ESC键关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
modal.classList.remove('show');
setTimeout(() => modal.remove(), 300);
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
return modal;
}
// 初始化等级徽章
function initLevelBadge() {
// 等级徽章已初始化
}
// ===== 初始化 =====
function init() {
try {
// 等待页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
// 创建等级徽章
const badge = createLevelBadge();
// 初始化等级徽章
setTimeout(() => {
initLevelBadge();
}, 1000);
} catch (e) {
console.error('初始化失败:', e);
}
}
// 等待页面加载后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 100);
}
// 暴露全局函数用于调试
window.mjjboxLevelViewer = {
fetchUserLevel: fetchUserLevel,
showLevelModal: showLevelModal
};
})();