您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
LeetCodeRating The score of the weekly competition is displayed, and currently supports the tag page, question bank page, problem_list page and question page
// ==UserScript== // @name LeetCodeRating|English // @namespace https://github.com/zhang-wangz // @version 2.0.0 // @license MIT // @description LeetCodeRating The score of the weekly competition is displayed, and currently supports the tag page, question bank page, problem_list page and question page // @author 小东是个阳光蛋(Leetcode Nickname of chinese site // @leetcodehomepage https://leetcode.cn/u/runonline/ // @homepageURL https://github.com/zhang-wangz/LeetCodeRating // @contributionURL https://www.showdoc.com.cn/2069209189620830 // @match *://*leetcode.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @connect zerotrac.github.io // @connect raw.staticdn.net // @connect raw.gitmirror.com // @connect raw.githubusercontents.com // @connect raw.githubusercontent.com // @require https://gcore.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://gcore.jsdelivr.net/gh/andywang425/BLTH@4368883c643af57c07117e43785cd28adcb0cb3e/assets/js/library/layer.min.js // @resource css https://gcore.jsdelivr.net/gh/andywang425/BLTH@d25aa353c8c5b2d73d2217b1b43433a80100c61e/assets/css/layer.css // @grant unsafeWindow // @run-at document-end // @note 2022-12-29 1.1.0 add english site support // @note 2022-12-29 1.1.1 fix when the dark mode is turned on, the prompt display is abnormal // @note 2023-01-05 1.1.2 modify the cdn access address // @note 2023-08-05 1.1.3 remaintain the project // @note 2023-09-20 1.1.4 fix the error that scores are not displayed properly due to ui changes in problem page // @note 2023-12-14 1.1.5 fix the error that scores are not displayed properly due to ui changes in problem set page // @note 2025-08-21 2.0.0 refactor the plugin, change the refresh and update logic // ==/UserScript== (function () { "use strict" let t2rate = {} const version = "2.0.0" const DEBUG_MODE = false const originalConsoleLog = console.log if (!DEBUG_MODE) { try { console.log = function(...args) { } } catch (e) { window.console = Object.assign({}, console, { log: function(...args) { } }) } } // a timer manager for all pages const TimerManager = { timers: { allProblems: null, // 题目列表页 contains all the problems problem: null, // 单题页 the specific problem page problemList: null, // 题单页 the problem list page }, // 设置定时器 set timer set(type, intervalId) { this.clear(type) this.timers[type] = intervalId console.log(`[TimerManager] Set timer for ${type}: ${intervalId}`) }, // 清除指定类型的定时器 clear the timer for the specific type clear(type) { if (this.timers[type]) { clearInterval(this.timers[type]) console.log( `[TimerManager] Cleared timer for ${type}: ${this.timers[type]}` ) this.timers[type] = null } }, // 清除所有定时器 clear all timers clearAll() { Object.keys(this.timers).forEach((type) => { this.clear(type) }) console.log("[TimerManager] Cleared all timers") }, // 获取定时器ID get the timer id get(type) { return this.timers[type] }, } let preDate const allProblemsUrl = "https://leetcode.com/problemset" // the problems page, contains all problems const problemListUrl = "https://leetcode.com/problem-list" // the problem list page, such as "https://leetcode.com/problem-list/array/" const problemUrl = "https://leetcode.com/problems" // the specific problem page, such as "https://leetcode.com/problems/two-sum/description/" GM_addStyle(GM_getResourceText("css")) // 深拷贝 deep clone function deepclone(obj) { const str = JSON.stringify(obj) return JSON.parse(str) } // URL变化监听管理器 (已注释掉,改用纯定时器方式) /* const UrlChangeManager = { isInitialized: false, urlChangeHandler: null, // 初始化URL变化监听 init() { if (this.isInitialized) return this.isInitialized = true const oldPushState = history.pushState const oldReplaceState = history.replaceState history.pushState = function pushState(...args) { const res = oldPushState.apply(this, args) window.dispatchEvent(new Event("urlchange")) return res } history.replaceState = function replaceState(...args) { const res = oldReplaceState.apply(this, args) window.dispatchEvent(new Event("urlchange")) return res } window.addEventListener("popstate", () => { window.dispatchEvent(new Event("urlchange")) }) console.log("[UrlChangeManager] URL change detection initialized") }, // 设置URL变化处理器(确保只有一个) setHandler(handler) { // 移除旧的处理器 if (this.urlChangeHandler) { window.removeEventListener("urlchange", this.urlChangeHandler) console.log("[UrlChangeManager] Removed old urlchange handler") } // 设置新的处理器 this.urlChangeHandler = handler window.addEventListener("urlchange", this.urlChangeHandler) console.log("[UrlChangeManager] Set new urlchange handler") }, // 清理处理器 clearHandler() { if (this.urlChangeHandler) { window.removeEventListener("urlchange", this.urlChangeHandler) this.urlChangeHandler = null console.log("[UrlChangeManager] Cleared urlchange handler") } } } // 监听URL变化事件(保持向后兼容) function initUrlChange() { return () => UrlChangeManager.init() } */ // 获取时间 function getCurrentDate(format) { const now = new Date() const year = now.getFullYear() //得到年份 let month = now.getMonth() //得到月份 let date = now.getDate() //得到日期 let hour = now.getHours() //得到小时 let minu = now.getMinutes() //得到分钟 let sec = now.getSeconds() //得到秒 month = month + 1 if (month < 10) month = "0" + month if (date < 10) date = "0" + date if (hour < 10) hour = "0" + hour if (minu < 10) minu = "0" + minu if (sec < 10) sec = "0" + sec let time = "" // 精确到天 if (format == 1) { time = year + "年" + month + "月" + date + "日" } // 精确到分 else if (format == 2) { time = year + "-" + month + "-" + date + " " + hour + ":" + minu + ":" + sec } return time } function getProblemIndex(problem) { // we can't use problem.id because for some problems, the id here is not the problem index, so we have to extract problem index from title text const titleElement = problem.querySelector(".text-body .ellipsis") if (!titleElement) return null const titleText = titleElement.textContent || titleElement.innerText const match = titleText.match(/^(\d+)\.\s/) if (!match) return null return match[1] } let lastProcessedListContent let lastProcessedProblemId // let lastProcessedUrl = "" // URL变化检测相关 // let urlChangeTimeout = null // URL变化检测相关 function getAllProblemsData() { console.log( "[LeetCodeRating] getAllProblemsData() polling - " + new Date().toLocaleTimeString() ) try { // find the element in devtools and click "copy JS path" const problemList = document.querySelector( "#__next > div.flex.min-h-screen.min-w-\\[360px\\].flex-col.text-label-1.dark\\:text-dark-label-1 > div.mx-auto.w-full.grow.lg\\:max-w-screen-xl.dark\\:bg-dark-layer-bg.lc-dsw-xl\\:max-w-none.flex.bg-white.p-0.md\\:max-w-none.md\\:p-0 > div > div.flex.w-full.flex-1.overflow-hidden > div > div.flex.flex-1.justify-center.overflow-hidden > div > div.mt-4.flex.flex-col.items-center.gap-4 > div.w-full.flex-1 > div" ) // pb页面加载时直接返回 if (problemList == undefined) { return } // 防止过多的无效操作 if ( lastProcessedListContent != undefined && lastProcessedListContent == problemList.innerHTML ) { return } const problems = problemList.childNodes for (const problem of problems) { const problemIndex = getProblemIndex(problem) if (problemIndex == null) continue // get the difficulty display for the current problem const problemDifficulty = problem.querySelector( 'p[class*="text-sd-easy"], p[class*="text-sd-medium"], p[class*="text-sd-hard"]' ) if (problemDifficulty && t2rate[problemIndex] != undefined) { problemDifficulty.innerHTML = t2rate[problemIndex].Rating } } lastProcessedListContent = deepclone(problemList.innerHTML) } catch (e) { return } } function getProblemListData() { console.log( "[LeetCodeRating] getProblemListData() polling - " + new Date().toLocaleTimeString() ) try { const problemList = document.querySelector( "#__next > div.flex.min-h-screen.min-w-\\[360px\\].flex-col.text-label-1.dark\\:text-dark-label-1 > div.mx-auto.w-full.grow.lg\\:max-w-screen-xl.dark\\:bg-dark-layer-bg.lc-dsw-xl\\:max-w-none.flex.bg-white.p-0.md\\:max-w-none.md\\:p-0 > div > div.lc-dsw-lg\\:flex-row.lc-dsw-lg\\:px-6.lc-dsw-lg\\:gap-8.lc-dsw-lg\\:justify-center.lc-dsw-xl\\:pl-10.flex.min-h-\\[600px\\].flex-1.flex-col.justify-start.px-4 > div.lc-dsw-lg\\:max-w-\\[699px\\].mt-6.flex.w-full.flex-col.items-center.gap-4 > div.lc-dsw-lg\\:max-w-\\[699px\\].w-full.flex-1 > div > div > div.absolute.left-0.top-0.h-full.w-full > div" ) if (problemList == undefined) { return } if ( lastProcessedListContent != undefined && lastProcessedListContent == problemList.innerHTML ) { return } const problems = problemList.childNodes for (const problem of problems) { const problemIndex = getProblemIndex(problem) if (problemIndex == null) continue const problemDifficulty = problem.querySelector( 'p[class*="text-sd-easy"], p[class*="text-sd-medium"], p[class*="text-sd-hard"]' ) if (problemDifficulty && t2rate[problemIndex] != undefined) { problemDifficulty.innerHTML = t2rate[problemIndex].Rating } } lastProcessedListContent = deepclone(problemList.lastChild.innerHTML) } catch (e) { return } } function getProblemData() { console.log( "[LeetCodeRating] getProblemData() polling - " + new Date().toLocaleTimeString() ) try { const problemTitle = document.querySelector( "#qd-content > div > div.flexlayout__tab > div > div > div > div > div > a" ) console.log( "[LeetCodeRating] problemTitle:", problemTitle ? "Found" : "Not found" ) if (problemTitle == undefined) { lastProcessedProblemId = "unknown" return } const problemIndex = problemTitle.innerText.split(".")[0].trim() if ( lastProcessedProblemId != undefined && lastProcessedProblemId == problemIndex ) { return } const colorSpan = document.querySelector( "#qd-content > div > div.flexlayout__tab > div > div > div.flex.gap-1 > div" ) // 新版统计难度分数并且修改 if (t2rate[problemIndex] != undefined) { console.log( `[LeetCodeRating] Found rating for problem ${problemIndex}: ${t2rate[problemIndex].Rating}` ) colorSpan.innerHTML = t2rate[problemIndex].Rating } else { console.log( `[LeetCodeRating] No rating found for problem ${problemIndex}, restoring original difficulty` ) // 恢复原始难度显示 const problemDifficulty = colorSpan.getAttribute("class") const difficultyMap = { "text-difficulty-easy": "Easy", "text-difficulty-medium": "Medium", "text-difficulty-hard": "Hard", } // 检查class中包含哪种难度 let originalDifficulty = "Unknown" for (const diffClass in difficultyMap) { if (problemDifficulty && problemDifficulty.includes(diffClass)) { originalDifficulty = difficultyMap[diffClass] break } } colorSpan.innerHTML = originalDifficulty } lastProcessedProblemId = deepclone(problemIndex) } catch (e) { return } } t2rate = JSON.parse(GM_getValue("t2ratedb", "{}").toString()) console.log( `[Data Init] Loaded t2rate from storage, keys count: ${ Object.keys(t2rate).length }` ) // latestpb = JSON.parse(GM_getValue("latestpb", "{}").toString()) preDate = GM_getValue("preDate", "") const now = getCurrentDate(1) console.log( `[Data Init] preDate: ${preDate}, now: ${now}, tagVersion exists: ${ t2rate.tagVersion != undefined }` ) if (t2rate.tagVersion == undefined || preDate == "" || preDate != now) { console.log(`[Data Init] Need to fetch new data from server`) GM_xmlhttpRequest({ method: "get", url: "https://raw.githubusercontent.com/zerotrac/leetcode_problem_rating/main/data.json" + "?timeStamp=" + new Date().getTime(), headers: { "Content-Type": "application/x-www-form-urlencoded", }, onload: function (res) { if (res.status === 200) { console.log(`[Data Init] Successfully fetched data from server`) // 保留唯一标识 t2rate = {} const dataStr = res.response const json = eval(dataStr) console.log(`[Data Init] Parsed ${json.length} problem records`) for (const element of json) { t2rate[element.ID] = element t2rate[element.ID].Rating = Number.parseInt( Number.parseFloat(element.Rating) + 0.5 ) } t2rate.tagVersion = {} console.log( `[Data Init] Processed t2rate, final keys count: ${ Object.keys(t2rate).length }` ) console.log("everyday getdate once...") preDate = now GM_setValue("preDate", preDate) GM_setValue("t2ratedb", JSON.stringify(t2rate)) } else { console.log(`[Data Init] Failed to fetch data, status: ${res.status}`) } }, onerror: function (err) { console.log("error") console.log(err) }, }) } function startTimers(url, timeout) { console.log(`[startTimers] Starting with URL: ${url}, timeout: ${timeout}`) // 清理所有定时器 TimerManager.clearAll() // 根据URL匹配对应的页面类型和函数 const pageConfig = { allProblems: { url: allProblemsUrl, func: getAllProblemsData, name: "getAllProblemsData()", }, problem: { url: problemUrl, func: getProblemData, name: "getProblemData()", }, problemList: { url: problemListUrl, func: getProblemListData, name: "getProblemListData()", }, } console.log(`[startTimers] Page config:`, pageConfig) console.log( `[startTimers] URL patterns - allProblems: ${allProblemsUrl}, problem: ${problemUrl}, problemList: ${problemListUrl}` ) // 找到匹配的页面类型 let currentPageType = null for (const [type, config] of Object.entries(pageConfig)) { console.log( `[startTimers] Checking if ${url} starts with ${ config.url }: ${url.startsWith(config.url)}` ) if (url.startsWith(config.url)) { currentPageType = type console.log(`[startTimers] Matched page type: ${currentPageType}`) break } } if (!currentPageType) { console.log(`[startTimers] No matching page type found for URL: ${url}`) return } const config = pageConfig[currentPageType] console.log(`[startTimers] Using config for ${currentPageType}:`, config) // 立即执行一次 console.log(`[startTimers] Starting immediate execution for ${config.name}`) config.func() // 启动定时器 console.log( `[startTimers] Starting timer for ${currentPageType} with ${timeout}ms interval` ) const timerId = setInterval(config.func, timeout) TimerManager.set(currentPageType, timerId) console.log( `[startTimers] Setup complete for page type: ${currentPageType}` ) } // 原版的 clearAndStart 函数 (已注释掉,改用 startTimers) /* function clearAndStart(url, timeout, isAddEvent) { console.log( `[clearAndStart] Starting with URL: ${url}, timeout: ${timeout}` ) // 清理所有定时器 TimerManager.clearAll() // 根据URL匹配对应的页面类型和函数 const pageConfig = { allProblems: { url: allProblemsUrl, func: getAllProblemsData, name: "getAllProblemsData()", }, problem: { url: problemUrl, func: getProblemData, name: "getProblemData()", }, problemList: { url: problemListUrl, func: getProblemListData, name: "getProblemListData()", }, } console.log(`[clearAndStart] Page config:`, pageConfig) console.log( `[clearAndStart] URL patterns - allProblems: ${allProblemsUrl}, problem: ${problemUrl}, problemList: ${problemListUrl}` ) // 找到匹配的页面类型 let currentPageType = null for (const [type, config] of Object.entries(pageConfig)) { console.log( `[clearAndStart] Checking if ${url} starts with ${ config.url }: ${url.startsWith(config.url)}` ) if (url.startsWith(config.url)) { currentPageType = type console.log(`[clearAndStart] Matched page type: ${currentPageType}`) break } } if (!currentPageType) { console.log(`[clearAndStart] No matching page type found for URL: ${url}`) } if (currentPageType) { // 智能重试机制:立即执行,如果失败则短暂延迟后重试 const executeWithRetry = (func, funcName, maxRetries = 3) => { let retryCount = 0 const tryExecute = () => { console.log( `[LeetCodeRating] Immediate execution for URL change: ${funcName} (attempt ${ retryCount + 1 })` ) // 记录执行前的状态 const beforeState = { lastProcessedProblemId: lastProcessedProblemId, lastProcessedListContent: lastProcessedListContent, } func() const afterState = { lastProcessedProblemId: lastProcessedProblemId, lastProcessedListContent: lastProcessedListContent, } // 检查是否成功执行(状态有变化) const hasChanges = JSON.stringify(beforeState) !== JSON.stringify(afterState) if (!hasChanges && retryCount < maxRetries) { retryCount++ console.log( `[LeetCodeRating] ${funcName} - No changes detected, retrying in ${ 200 * retryCount }ms...` ) setTimeout(tryExecute, 200 * retryCount) // 递增延迟: 200ms, 400ms, 600ms } else if (hasChanges) { console.log( `[LeetCodeRating] ${funcName} - Successfully executed with changes` ) } else { console.log( `[LeetCodeRating] ${funcName} - Max retries reached, will rely on timer` ) } } tryExecute() } const config = pageConfig[currentPageType] console.log( `[clearAndStart] Using config for ${currentPageType}:`, config ) // 立即执行 console.log( `[clearAndStart] Starting immediate execution for ${config.name}` ) executeWithRetry(config.func, config.name) // 启动定时器 console.log( `[clearAndStart] Starting timer for ${currentPageType} with ${timeout}ms interval` ) const timerId = setInterval(config.func, timeout) TimerManager.set(currentPageType, timerId) console.log( `[clearAndStart] Setup complete for page type: ${currentPageType}` ) } else { console.log(`[clearAndStart] No page type matched, no timers started`) } // 添加URL变化监听 (已注释掉,改用纯定时器方式) if (isAddEvent) { const urlChangeHandler = () => { console.log("urlchange event happened") const newUrl = location.href // 防抖处理:如果URL没有变化,忽略此次事件 if (newUrl === lastProcessedUrl) { console.log("[UrlChangeManager] URL unchanged, ignoring event") return } // 清除之前的延时器 if (urlChangeTimeout) { clearTimeout(urlChangeTimeout) } // 延时执行,防止频繁触发 urlChangeTimeout = setTimeout(() => { console.log(`[UrlChangeManager] Processing URL change: ${lastProcessedUrl} -> ${newUrl}`) lastProcessedUrl = newUrl clearAndStart(newUrl, 2000, false) urlChangeTimeout = null }, 100) // 100ms防抖延时 } UrlChangeManager.setHandler(urlChangeHandler) } } */ [...document.querySelectorAll("*")].forEach((item) => { item.oncopy = function (e) { e.stopPropagation() } }) // 初始化URL变化监听 (已注释掉,改用纯定时器方式) // initUrlChange()() // 版本更新机制 (仅在主页检查) if (window.location.href.startsWith(allProblemsUrl)) { GM_xmlhttpRequest({ method: "get", url: "https://raw.githubusercontent.com/zhang-wangz/LeetCodeRating/english/version.json" + "?timeStamp=" + new Date().getTime(), headers: { "Content-Type": "application/x-www-form-urlencoded", }, onload: function (res) { if (res.status === 200) { console.log("enter home page check version once...") const dataStr = res.response const json = JSON.parse(dataStr) const v = json.version const upcontent = json.content if (v != version) { layer.open({ content: '<div style="color:#000; padding: 8px;">' + '<p><strong>LeetCodeRating</strong> has a new version!</p>' + '<p><strong>Update content:</strong></p>' + '<div style="background: #f5f5f5; padding: 8px; border-radius: 4px; margin: 8px 0;">' + upcontent + '</div>' + '</div>', btn: ['Install Update', 'Later'], yes: function (index) { // 打开脚本页面,让用户可以安装更新 window.open( "https://raw.githubusercontent.com/zhang-wangz/LeetCodeRating/english/leetcodeRating_greasyfork.user.js" + "?timeStamp=" + new Date().getTime(), "_blank" ) layer.close(index) }, btn2: function (index) { layer.close(index) } }) } else { console.log( "leetcodeRating difficulty plugin is currently the latest version~" ) } } }, onerror: function (err) { console.log("error") console.log(err) }, }) } // 启动主程序,使用2000ms间隔 console.log(`[Script Init] Starting LeetCodeRating script v${version}`) console.log(`[Script Init] Current URL: ${location.href}`) console.log( `[Script Init] t2rate data available: ${Object.keys(t2rate).length} entries` ) startTimers(location.href, 2000) // 2秒间隔 })()