适用于新规的等级进度查看器 v1.5.2(新规版)

请查看最新版:积分来源与抽奖机制同步,增加快捷签到的等级进度插件(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
    }
  };

不想手动更改的,可以直接复制下面的内容:

  1. 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">&times;</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();
})();

参考链接:

  1. MJJBOX 等级进度查看器(修复版) v1.5
  2. 论坛等级查看(自用版)
  3. 随时查看等级进度!
  4. 分享【原创脚本】MJJBOX增强助手 - 等级查看器+自定义背景+美化字体,让论坛更个性!9/13更新解锁隐藏条件
21 个赞

:100:厉害

1 个赞

与时俱进了

1 个赞

好用

1 个赞

谢Mjj的分享!

1 个赞

太强了啊,学习一下

1 个赞

已更新

1 个赞

:+1:我也来更新一下

1 个赞

谢谢更新

1 个赞

感谢大佬 无私分享

1 个赞

你就是现代的及时雨

已经用起来了 赞

其实我的昨晚更新了 :rofl:

不错,试试看

更新了你不发出来~

在原贴下留言了 没有重新发帖 需要的自然会去拿 ,因为很多人跟我说没啥用 :rofl:

还是得整官方的才行..

有难度.有些条件查询需要权限
看了就懂了

怎么使用啊,这是油猴脚本吗?

对的