// ==UserScript== // @name GenGen-RMJ // @icon https://cdn.luogu.com.cn/upload/image_hosting/3s3czya0.png // @namespace https://gengen.qzz.io/ // @version 3.1.2.12 // @description GenGen RMJ 完整版本 // @author GenGen 队 // @run-at document-start // @match https://www.luogu.com.cn/* // @match https://atcoder.jp/contests/*/submit?RMJ=1 // @match https://codeforces.com/problemset/submit/* // @match https://codeforces.com/problemset/status?my=on // @require https://cdn.bootcdn.net/ajax/libs/html2canvas/1.4.1/html2canvas.js // @require https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.23.0/sweetalert2.all.js // @require https://scriptcat.org/scripts/code/5095/GenGen-CF-RMJ-JL.user.js // @require https://scriptcat.org/scripts/code/5096/GenGen-Cloudflare-RMJ.user.js // @require https://scriptcat.org/scripts/code/5093/GenGen-AT-RMJ-JL.user.js // @require https://scriptcat.org/scripts/code/5094/GenGen-CF-RMJ.user.js // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; const VERSION = '3.1.2.12'; const BUILD_DATE = '2026/2/4'; const UPDATE_INTERVAL = 500; /* ---------------- 工具函数 ---------------- */ const ready = fn => document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', fn) : fn(); const wait = (sel, cb) => { const t = setInterval(() => { const el = document.querySelector(sel); if (el) { clearInterval(t); cb(el); } }, 100); }; /* ---------------- 样式注入 ---------------- */ GM_addStyle(` /* 首页专属按钮:直立粗体罗马体 + 天蓝色 */ .gengen-home-btn { background: linear-gradient(120deg, #4da6ff, #2196f3); color: white !important; border: none; border-radius: 6px; padding: 4px 12px; font-weight: 700; /* 粗体 */ font-style: normal; font-size: 16px; cursor: pointer; margin-right: 12px; box-shadow: 0 2px 6px rgba(0, 67, 133, 0.25); transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); height: 30px; display: inline-flex; align-items: center; justify-content: center; letter-spacing: 0.5px; } .gengen-home-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 67, 133, 0.35); background: linear-gradient(120deg, #3d8be6, #1976d2); } .gengen-home-btn:active { transform: translateY(0); } /* 面板基础样式 + 淡入淡出动画 */ #gengen-panel { position: fixed; width: 400px; backdrop-filter: blur(12px); background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 5px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18); z-index: 2147483647; /* 最高优先级 */ padding: 20px; font-size: 16px; display: flex; flex-direction: column; gap: 14px; opacity: 0; visibility: hidden; transform: translateY(-8px); transition: opacity 0.32s cubic-bezier(0.16, 1, 0.3, 1), transform 0.36s cubic-bezier(0.175, 0.885, 0.32, 1.275), visibility 0.32s; pointer-events: none; /* 隐藏时不可交互 */ } #gengen-panel.gengen-panel-visible { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: all; } /* 标题:直立粗体罗马体 + 左侧图标 */ #gengen-panel h3 { margin: 0 0 12px 0; font-size: 34px; font-weight: 700; /* 粗体 */ font-style: normal; /* 直立罗马体(关键!) */ font-family: "Times New Roman", "Georgia", "SimSun", "宋体", "Songti SC", "STSong", serif; color: #1a237e; display: flex; align-items: center; justify-content: center; gap: 10px; border-bottom: 1.5px solid #e8eaf6; padding-bottom: 10px; letter-spacing: -0.5px; } #gengen-panel h3 img { width: 30px; height: 30px; border-radius: 6px; flex-shrink: 0; } #gengen-panel p { margin: 0; line-height: 1.6; color: #37474f; } #gengen-panel a { color: #1565c0; text-decoration: none; font-weight: 500; } #gengen-panel a:hover { text-decoration: underline; } .gengen-account-row { display: flex; justify-content: space-between; padding: 6px 0; } .gengen-account-status { font-weight: 600; color: #2e7d32; } .gengen-not-bound { color: #c62828; font-weight: 500; } .gengen-bind-btn { font-size: 13px; padding: 3px 8px; background: #e3f2fd; border: 1px solid #1e88e5; border-radius: 16px; cursor: pointer; color: #0d47a1; font-weight: 500; transition: all 0.2s; } .gengen-bind-btn:hover { background: #bbdefb; transform: scale(1.05); } .gengen-status-wrapper { display: none; justify-content: center; align-items: center; min-height: 24px; width: 100%; } .gengen-managed .gengen-original-content { display: none !important; } .gengen-managed .gengen-status-wrapper { display: flex !important; } `); /* ---------------- 获取账号信息 ---------------- */ async function getAtCoderUser() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://atcoder.jp/', onload: res => { if (res.status !== 200) return resolve(null); try { const parser = new DOMParser(); const doc = parser.parseFromString(res.responseText, 'text/html'); const userEl = doc.querySelector("#navbar-collapse > ul.nav.navbar-nav.navbar-right > li:nth-child(2) > a"); if (!userEl || userEl.textContent.trim() === 'Sign Up') resolve(null); else resolve(userEl.textContent.trim()); } catch { resolve(null); } }, onerror: () => resolve(null), timeout: 8000 }); }); } function getCFHandle(callback) { GM_xmlhttpRequest({ method: 'GET', url: 'https://codeforces.com/', withCredentials: true, onload: function(res) { if (res.status !== 200) return callback(null); try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const userLink = doc.querySelector('#header > div.lang-chooser > div:nth-child(2) > a:nth-child(1)'); if (!userLink) return callback(null); const text = userLink.textContent.trim(); if (text === 'Enter') callback(null); else callback(text); } catch (e) { callback(null); } }, onerror: () => callback(null), timeout: 8000 }); } /* ---------------- 题目状态图标渲染 ---------------- */ function getOkIcon() { return ``; } function getErrorIcon() { return ``; } function getNoIcon() { return ``; } // 缓存映射 let cfSubmissionMap = new Map(); let atSubmissionMap = new Map(); let lastCFRaw = ''; let lastATRaw = ''; function refreshCache() { try { const cfRaw = GM_getValue('cf-submissions-cache', '[]'); const atRaw = GM_getValue('at-submissions-cache', '[]'); if (cfRaw !== lastCFRaw) { lastCFRaw = cfRaw; cfSubmissionMap = new Map(); if (cfRaw && cfRaw !== '[]') { const cfCache = JSON.parse(cfRaw); cfCache.forEach(sub => { if (!sub?.taskDisplay) return; const match = sub.taskDisplay.match(/CF[A-Z0-9]+/i); if (match) { const pid = match[0].toUpperCase(); if (!cfSubmissionMap.has(pid)) { cfSubmissionMap.set(pid, sub.verdict || null); } } }); } } if (atRaw !== lastATRaw) { lastATRaw = atRaw; atSubmissionMap = new Map(); if (atRaw && atRaw !== '[]') { const atCache = JSON.parse(atRaw); atCache.forEach(sub => { if (sub?.taskKey) { const key = sub.taskKey.toLowerCase(); if (!atSubmissionMap.has(key)) { atSubmissionMap.set(key, sub.status || null); } } }); } } } catch (e) {} } function initGenGenContainer(cell) { if (cell.dataset.gengenInitialized) return; const originalWrapper = document.createElement('div'); originalWrapper.className = 'gengen-original-content'; originalWrapper.innerHTML = cell.innerHTML; const statusWrapper = document.createElement('div'); statusWrapper.className = 'gengen-status-wrapper'; statusWrapper.innerHTML = getNoIcon(); cell.innerHTML = ''; cell.appendChild(originalWrapper); cell.appendChild(statusWrapper); cell.dataset.gengenInitialized = '1'; cell.dataset.gengenStatus = 'none'; } function updateRowStatus(row) { const cells = row.children; if (cells.length < 2) return false; const firstCell = cells[0]; const secondCell = cells[1]; const pidMatch = secondCell.textContent.trim().match(/^(\S+)/); if (!pidMatch) { if (firstCell.dataset.gengenInitialized) { firstCell.classList.remove('gengen-managed'); } return false; } const pid = pidMatch[1]; let isCFAT = false; let newStatus = 'none'; let iconHTML = getNoIcon(); if (pid.startsWith('CF')) { isCFAT = true; const verdict = cfSubmissionMap.get(pid.toUpperCase()); if (verdict === 'OK') { iconHTML = getOkIcon(); newStatus = 'AC'; } else if (verdict) { iconHTML = getErrorIcon(); newStatus = 'WA'; } } else if (pid.startsWith('AT_')) { isCFAT = true; const taskKey = pid.substring(3).toLowerCase(); const status = atSubmissionMap.get(taskKey); if (status === 'AC') { iconHTML = getOkIcon(); newStatus = 'AC'; } else if (status) { iconHTML = getErrorIcon(); newStatus = 'WA'; } } if (!firstCell.dataset.gengenInitialized) { initGenGenContainer(firstCell); } const oldStatus = firstCell.dataset.gengenStatus; if (isCFAT) { const statusWrapper = firstCell.querySelector('.gengen-status-wrapper'); if (statusWrapper) statusWrapper.innerHTML = iconHTML; firstCell.classList.add('gengen-managed'); firstCell.dataset.gengenStatus = newStatus; return oldStatus !== newStatus; } else { firstCell.classList.remove('gengen-managed'); firstCell.dataset.gengenStatus = 'none'; return oldStatus !== 'none'; } } let updateIntervalId = null; function startStatusUpdateLoop() { if (updateIntervalId) clearInterval(updateIntervalId); updateIntervalId = setInterval(() => { refreshCache(); const rows = document.querySelectorAll('.row'); rows.forEach(updateRowStatus); }, UPDATE_INTERVAL); } function stopStatusUpdateLoop() { if (updateIntervalId) { clearInterval(updateIntervalId); updateIntervalId = null; } } // ---------------- 面板管理逻辑 ---------------- let gengenPanel = null; let hideTimer = null; const PANEL_SHOW_DELAY = 200; // 悬浮显示延迟(防误触) const PANEL_HIDE_DELAY = 300; // 离开隐藏延迟 function createPanel() { if (gengenPanel) return; const panel = document.createElement('div'); panel.id = 'gengen-panel'; panel.innerHTML = `

GenGen GenGen RMJ 3.1

版本:${VERSION} (${BUILD_DATE})

官网:gengen.qzz.io

反馈:私信开发者

新特性:支持 CodeForces / AtCoder

工单进度:点击查看

Codeforces 账号: 加载中…
AtCoder 账号: 加载中…
`; document.body.appendChild(panel); gengenPanel = panel; // 异步加载账号状态(保持原逻辑) getCFHandle(handle => { const el = document.getElementById('cf-status'); if (handle) el.innerHTML = `${handle}`; else el.innerHTML = `未绑定 绑定`; }); getAtCoderUser().then(handle => { const el = document.getElementById('at-status'); if (handle) el.innerHTML = `${handle}`; else el.innerHTML = `未绑定 绑定`; }); // 面板悬停事件(防抖动) panel.addEventListener('mouseenter', () => clearTimeout(hideTimer)); panel.addEventListener('mouseleave', schedulePanelHide); } function positionPanel(btnRect) { if (!gengenPanel) return; // 计算理想位置(按钮正下方,左对齐) let top = btnRect.bottom + 8; // 8px 间距 let left = btnRect.left; // 边界检测:防止溢出视口 const panelWidth = 400; if (left + panelWidth > window.innerWidth - 16) { left = window.innerWidth - panelWidth - 16; // 右侧留16px边距 } if (top + 300 > window.innerHeight) { // 简易高度检测 top = btnRect.top - 320; // 超出时显示在按钮上方 if (top < 16) top = 16; } gengenPanel.style.top = `${top}px`; gengenPanel.style.left = `${left}px`; } function showPanel(btnRect) { clearTimeout(hideTimer); if (!gengenPanel) createPanel(); positionPanel(btnRect); gengenPanel.classList.add('gengen-panel-visible'); } function schedulePanelHide() { hideTimer = setTimeout(() => { if (gengenPanel) gengenPanel.classList.remove('gengen-panel-visible'); }, PANEL_HIDE_DELAY); } // ---------------- 首页按钮注入(重构版) ---------------- function injectHomeButton() { if (location.pathname !== '/' || document.querySelector('.gengen-home-btn')) return; wait('.user-nav', nav => { if (nav.dataset.gengenProcessed) return; nav.dataset.gengenProcessed = '1'; // 创建天蓝色文字按钮(直立粗体罗马体) const btn = document.createElement('div'); btn.className = 'gengen-home-btn'; btn.textContent = 'GenGen RMJ'; btn.setAttribute('aria-label', 'GenGen RMJ 工具面板'); // 悬浮触发逻辑 let showTimer = null; btn.addEventListener('mouseenter', () => { clearTimeout(showTimer); showTimer = setTimeout(() => { const rect = btn.getBoundingClientRect(); showPanel(rect); }, PANEL_SHOW_DELAY); }); btn.addEventListener('mouseleave', () => { clearTimeout(showTimer); schedulePanelHide(); }); // 插入到导航栏(替换原图标位置) nav.parentElement.insertBefore(btn, nav); // 全局窗口调整时重定位面板 window.addEventListener('resize', () => { if (gengenPanel?.classList.contains('gengen-panel-visible')) { const rect = btn.getBoundingClientRect(); positionPanel(rect); } }, { passive: true }); }); } /* ---------------- Problem 页面劫持 ---------------- */ function enhanceProblemLink() { if (!location.pathname.startsWith('/problem/CF') && !location.pathname.startsWith('/problem/AT')) return; const rmj = location.pathname.startsWith('/problem/CF') ? '1' : '2'; const problemId = location.pathname.split('/problem/')[1]; if (!problemId) return; wait('.side', (sideContainer) => { const observer = new MutationObserver(() => { const links = sideContainer.querySelectorAll('a'); for (const link of links) { if (!link.href.includes('/record/list?pid=')) continue; const span = link.querySelector('span'); if (span && span.textContent.trim() !== '点击查看') { span.textContent = '点击查看'; span.className = 'lcolor-Blue-3'; span.style.fontWeight = 'bold'; } if (link.dataset.gengenPatched) continue; link.onclick = null; link.addEventListener('click', function (e) { e.preventDefault(); e.stopImmediatePropagation(); const targetUrl = new URL('/record/list', 'https://www.luogu.com.cn'); const currentParams = new URLSearchParams(location.search); for (const [key, value] of currentParams) { targetUrl.searchParams.set(key, value); } targetUrl.searchParams.set('pid', problemId); targetUrl.searchParams.set('rmj', rmj); if (window.__LUOGU_ROUTER__) { window.__LUOGU_ROUTER__.push(targetUrl.pathname + targetUrl.search); } else { location.href = targetUrl.toString(); } }, true); link.dataset.gengenPatched = '1'; link.href = 'javascript:void(0)'; } }); observer.observe(sideContainer, { childList: true, subtree: true }); observer.takeRecords(); observer.disconnect(); observer.observe(sideContainer, { childList: true, subtree: true }); }); } /* ---------------- Record List OJ 选择器 ---------------- */ function enhanceRecordList() { if (!location.pathname.startsWith('/record/list')) return; wait('section b', b => { if (b.dataset.gengen) return; b.dataset.gengen = '1'; const sel = document.createElement('select'); sel.style.cssText = 'margin-left:8px;padding:0px 8px;border-radius:3px;'; sel.innerHTML = ` `; const p = new URLSearchParams(location.search); sel.value = p.get('rmj') || ''; sel.onchange = () => { sel.value ? p.set('rmj', sel.value) : p.delete('rmj'); location.search = p.toString(); }; b.after(sel); }); } /* ---------------- 主启动函数 ---------------- */ function main() { injectHomeButton(); enhanceProblemLink(); enhanceRecordList(); if (location.pathname.startsWith('/training') || location.pathname.startsWith('/problem/list')) { startStatusUpdateLoop(); } } ready(main); let lastPath = location.pathname; setInterval(() => { let currentPath = location.pathname; if (currentPath === lastPath) return; if (lastPath.startsWith('/training') || lastPath.startsWith('/problem/list')) { stopStatusUpdateLoop(); } if (currentPath.startsWith('/training') || currentPath.startsWith('/problem/list')) { startStatusUpdateLoop(); } lastPath = currentPath; }, 100); window.addEventListener('beforeunload', () => { stopStatusUpdateLoop(); }); })();