LeetCode 学习计划体验增强(题目列表常驻、显示分类下题目个数)

在 LeetCode 学习计划页面为每个分类显示题目总数,并为每个题目添加序号。

目前為 2025-08-02 提交的版本,檢視 最新版本

// ==UserScript==
// @name         LeetCode 学习计划体验增强(题目列表常驻、显示分类下题目个数)
// @namespace    http://tampermonkey.net/
// @version      0.2.7
// @description  在 LeetCode 学习计划页面为每个分类显示题目总数,并为每个题目添加序号。
// @author       tianyw0
// @match        https://leetcode.cn/studyplan/*
// @match        https://leetcode.cn/problems/*
// @match        https://leetcode.com/studyplan/*
// @match        https://leetcode.com/problems/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 监听页面变化,确保在内容加载后执行脚本
    const observer = new MutationObserver((mutationsList, observer) => {
        // 查找页面上所有符合“主容器”特征的模块
        // 假设每个这样的模块都由一个 '.w-full.overflow-hidden.rounded-lg.border-\\[1\\.5px\\]' 类标识
        const allProblemModules = document.querySelectorAll('.w-full.overflow-hidden.rounded-lg.border-\\[1\\.5px\\]');
        
        // 只有当找到模块且至少有一个模块尚未处理时才执行
        if (allProblemModules.length > 0 && !allProblemModules[0].dataset.processed) {
            allProblemModules.forEach(module => {
                // 防止重复处理
                if (module.dataset.processed) {
                    return;
                }

                const categoryTitles = module.querySelectorAll('div.flex.h-10 > div.text-\\[12px\\]'); 

                const problemItemsInModule = module.querySelectorAll('div.flex.flex-col.border-b-\\[1\\.5px\\] > div.flex.h-\\[52px\\]');
                const problemCount = problemItemsInModule.length;

                // 1. 在每个分类标题的文字后显示该模块的题目个数
                categoryTitles.forEach(categoryTitleTextDiv => {
                    // 检查是否已经添加过计数,避免重复
                    if (!categoryTitleTextDiv.querySelector('.problem-count-span')) {
                        const countSpan = document.createElement('span');
                        countSpan.textContent = ` (${problemCount} 题)`;
                        countSpan.style.marginLeft = '5px';
                        countSpan.classList.add('problem-count-span'); // 添加一个类以便识别
                        categoryTitleTextDiv.appendChild(countSpan);
                    }
                });

                // 2. 在当前模块内的具体题目前显示序号
                problemItemsInModule.forEach((item, index) => {
                    const titleContainer = item.querySelector('div.relative.flex.h-full.w-full.items-center > div.flex.w-0.flex-1.items-center.space-x-2');
                    if (titleContainer && !titleContainer.querySelector('.problem-index-span')) { // 检查是否已添加序号
                        const indexSpan = document.createElement('span');
                        indexSpan.textContent = `${index + 1}. `;
                        indexSpan.classList.add('problem-index-span'); // 添加一个类以便识别
                        titleContainer.prepend(indexSpan);
                    }
                });

                // 标记此模块为已处理
                module.dataset.processed = 'true';
            });
            // 首次成功处理后,可以停止观察,如果页面后续还有动态加载,则需要更精细的控制
            // observer.disconnect(); 
        }
    });

    // 开始观察 body 元素下的 DOM 变化,特别是子节点的变化和子树的变化
    observer.observe(document.body, { childList: true, subtree: true });

    // 首次加载时也尝试执行一次,以防内容已经存在
    // 由于MutationObserver可能在初始DOM解析后才触发,直接调用一次确保立即应用
    // 稍作延迟,确保页面初始内容渲染完成
    setTimeout(() => {
        const initialModules = document.querySelectorAll('.w-full.overflow-hidden.rounded-lg.border-\\[1\\.5px\\]');
        if (initialModules.length > 0) {
             initialModules.forEach(module => {
                if (!module.dataset.processed) {
                    const categoryTitles = module.querySelectorAll('div.flex.h-10 > div.text-\\[12px\\]'); 
                    const problemItemsInModule = module.querySelectorAll('div.flex.flex-col.border-b-\\[1\\.5px\\] > div.flex.h-\\[52px\\]');
                    const problemCount = problemItemsInModule.length;

                    categoryTitles.forEach(categoryTitleTextDiv => {
                        if (!categoryTitleTextDiv.querySelector('.problem-count-span')) {
                            const countSpan = document.createElement('span');
                            countSpan.textContent = ` (${problemCount} 题)`;
                            countSpan.style.marginLeft = '5px';
                            countSpan.classList.add('problem-count-span');
                            categoryTitleTextDiv.appendChild(countSpan);
                        }
                    });

                    problemItemsInModule.forEach((item, index) => {
                        const titleContainer = item.querySelector('div.relative.flex.h-full.w-full.items-center > div.flex.w-0.flex-1.items-center.space-x-2');
                        if (titleContainer && !titleContainer.querySelector('.problem-index-span')) {
                            const indexSpan = document.createElement('span');
                            indexSpan.textContent = `${index + 1}. `;
                            indexSpan.classList.add('problem-index-span');
                            titleContainer.prepend(indexSpan);
                        }
                    });
                    module.dataset.processed = 'true';
                }
            });
        }
        // 左侧学习计划的题目列表常驻
        const left = document.querySelectorAll(".z-modal")[2];
        const progressBar = left.querySelectorAll(".cursor-pointer")[0];
        
        // 修改 .z-modal 宽度
        left.classList.remove('w-[600px]');
        left.classList.add('w-[400px]');
        // 进度条宽度减小适应 z-modal 宽度变小
        progressBar.classList.remove('w-[200px]')
        progressBar.classList.add('w-[100px]')
        
        left.classList.remove('transform');
        left.classList.add('transform-none');
        
        
        
        // 设置 body 样式
        document.body.style.marginLeft = '400px';
        document.body.style.width = `${window.innerWidth - 400}px`;
        
        // 可选:保持响应式(窗口大小变时自动更新)
        window.addEventListener('resize', () => {
          document.body.style.width = `${window.innerWidth - 400}px`;
        });
    }, 1000); // 延迟 500 毫秒执行,可以根据实际情况调整

})();