梦熊OJ自动跳题至洛谷

修复按钮尺寸问题,统一题目页和题单页的洛谷跳转功能

当前为 2025-07-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         梦熊OJ自动跳题至洛谷
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  修复按钮尺寸问题,统一题目页和题单页的洛谷跳转功能
// @author       YourName
// @match        https://www.mxoj.net/problem/*
// @match        https://www.mxoj.net/training/problems*
// @grant        GM_addStyle
// @grant        GM_getResourceURL
// @resource     luoguIcon https://www.luogu.com.cn/favicon.ico
// @icon         https://www.luogu.com.cn/favicon.ico
// ==/UserScript==

(function() {
    'use strict';

    // 添加共用样式
    GM_addStyle(`
        /* 共用洛谷按钮样式 */
        .luogu-jump-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 6px 12px;
            background-color: #3498db;
            color: white !important;
            border-radius: 4px;
            font-size: 14px;
            font-weight: normal;
            text-decoration: none !important;
            vertical-align: middle;
            transition: all 0.2s;
            cursor: pointer;
            margin-left: 10px;
            height: 34px; /* 与提交按钮相同高度 */
            line-height: 1.42857; /* Bootstrap默认行高 */
            border: 1px solid transparent;
            white-space: nowrap;
        }
        .luogu-jump-btn:hover {
            background-color: #2980b9;
            transform: translateY(-1px);
        }
        .luogu-jump-btn::before {
            content: "";
            display: inline-block;
            width: 16px;
            height: 16px;
            background-image: url(${GM_getResourceURL('luoguIcon')});
            background-size: contain;
            background-repeat: no-repeat;
            vertical-align: middle;
            margin-right: 5px;
        }

        /* 题单页特定样式 */
        .problem-title-container {
            display: inline-flex !important;
            align-items: center !important;
        }
        .luogu-hover-btn {
            display: none;
            width: 16px;
            height: 16px;
            margin-left: 6px;
            background-image: url(${GM_getResourceURL('luoguIcon')});
            background-size: contain;
            background-repeat: no-repeat;
            cursor: pointer;
            vertical-align: middle;
        }
        .problem-title-container:hover .luogu-hover-btn {
            display: inline-block;
        }
        /* 隐藏原有箭头 */
        .problem-title::after, .problem-title::before,
        a.problem-title::after, a.problem-title::before,
        .problem-title:hover::after, .problem-title:hover::before,
        a.problem-title:hover::after, a.problem-title:hover::before {
            display: none !important;
            content: none !important;
        }
    `);

    // 判断当前页面类型并执行相应功能
    if (window.location.pathname.includes('/problem/') && !window.location.pathname.includes('/problems')) {
        initProblemPageScript(); // 题目详情页功能
    } else if (window.location.pathname.includes('/training/problems')) {
        initProblemSetScript(); // 题单/题库页功能
    }

    /**********************************/
    /* 题目详情页功能 - 提交按钮旁添加跳转 */
    /**********************************/
    function initProblemPageScript() {
        // 查找提交按钮
        function findSubmitButton() {
            const btn = document.querySelector('button.btn.btn-primary.answer[onclick^="goAnswer"]');
            if (btn) {
                // 获取计算样式以确保尺寸匹配
                const style = window.getComputedStyle(btn);
                return {
                    element: btn,
                    height: style.height,
                    padding: style.padding,
                    lineHeight: style.lineHeight,
                    fontSize: style.fontSize
                };
            }
            return null;
        }

        // 获取题目ID
        function getProblemId() {
            const submitButton = findSubmitButton();
            if (submitButton && submitButton.element) {
                const onclickContent = submitButton.element.getAttribute('onclick') || '';
                const match = onclickContent.match(/goAnswer\('([A-Za-z]+[\dA-Za-z]*)'\)/);
                if (match) return match[1];
            }

            const urlMatch = window.location.pathname.match(/\/problem\/([A-Za-z]+[\dA-Za-z]*)/i);
            return urlMatch ? urlMatch[1] : null;
        }

        // 转换为洛谷题目ID
        function convertToLuoguId(problemId) {
            if (!problemId) return null;

            if (/^CF\d+[A-Za-z]?$/i.test(problemId)) {
                return problemId.toUpperCase();
            } else if (/^L\d+$/i.test(problemId)) {
                return 'P' + problemId.substring(1);
            }
            return null;
        }

        // 插入洛谷按钮(精确匹配提交按钮样式)
        function insertLuoguButton(submitButtonInfo) {
            const submitButton = submitButtonInfo.element;
            if (!submitButton) return;

            // 避免重复添加
            if (submitButton.nextElementSibling?.classList?.contains('luogu-jump-btn')) {
                return;
            }

            const problemId = getProblemId();
            if (!problemId) return;

            const luoguId = convertToLuoguId(problemId);
            if (!luoguId) return;

            // 创建按钮并应用与提交按钮相同的尺寸
            const button = document.createElement('a');
            button.href = `https://www.luogu.com.cn/problem/${luoguId}`;
            button.textContent = '洛谷原题';
            button.className = 'luogu-jump-btn';
            button.target = '_blank';

            // 精确应用提交按钮的样式
            button.style.height = submitButtonInfo.height;
            button.style.padding = submitButtonInfo.padding;
            button.style.lineHeight = submitButtonInfo.lineHeight;
            button.style.fontSize = submitButtonInfo.fontSize;
            button.style.marginLeft = '10px';

            // 插入到提交按钮后面
            submitButton.parentNode.insertBefore(button, submitButton.nextSibling);
        }

        // 观察DOM变化
        const observer = new MutationObserver(function() {
            const submitButtonInfo = findSubmitButton();
            if (submitButtonInfo) {
                insertLuoguButton(submitButtonInfo);
                observer.disconnect();
            }
        });

        // 初始检查
        const submitButtonInfo = findSubmitButton();
        if (submitButtonInfo) {
            insertLuoguButton(submitButtonInfo);
        } else {
            observer.observe(document, {
                childList: true,
                subtree: true
            });

            // 3秒后停止观察
            setTimeout(() => {
                observer.disconnect();
                const submitButtonInfo = findSubmitButton();
                if (submitButtonInfo) {
                    insertLuoguButton(submitButtonInfo);
                }
            }, 3000);
        }
    }

    /**********************************/
    /* 题单/题库页功能 - 悬停显示跳转按钮 */
    /**********************************/
    function initProblemSetScript() {
        // 处理题目标题
        function processProblemTitles() {
            const problemTitles = document.querySelectorAll('.problem-title, a[href*="/problem/"]');

            problemTitles.forEach(title => {
                if (title.dataset.luoguProcessed) return;
                title.dataset.luoguProcessed = 'true';

                // 创建标题容器
                const container = document.createElement('div');
                container.className = 'problem-title-container';

                // 包装原有标题
                title.parentNode.insertBefore(container, title);
                container.appendChild(title);

                // 从元素提取题目ID
                const problemId = extractProblemId(title);
                if (!problemId) return;

                // 转换为洛谷题目ID
                const luoguId = convertToLuoguId(problemId);
                if (!luoguId) return;

                // 添加悬停按钮
                const luoguBtn = document.createElement('div');
                luoguBtn.className = 'luogu-hover-btn';
                luoguBtn.title = '在洛谷中查看此题';
                luoguBtn.dataset.luoguId = luoguId;
                container.appendChild(luoguBtn);
            });
        }

        // 提取题目ID
        function extractProblemId(titleElement) {
            if (titleElement.href) {
                const match = titleElement.href.match(/\/problem\/([A-Za-z]+[\dA-Za-z]*)/i);
                if (match) return match[1];
            }
            const textMatch = titleElement.textContent.match(/([A-Za-z]+[\dA-Za-z]*)/);
            return textMatch ? textMatch[1] : null;
        }

        // 转换为洛谷题目ID
        function convertToLuoguId(problemId) {
            if (/^CF\d+[A-Za-z]?$/i.test(problemId)) {
                return problemId.toUpperCase();
            } else if (/^L\d+$/i.test(problemId)) {
                return 'P' + problemId.substring(1);
            }
            return null;
        }

        // 事件委托处理点击
        document.addEventListener('click', function(e) {
            const luoguBtn = e.target.closest('.luogu-hover-btn');
            if (luoguBtn) {
                e.preventDefault();
                const luoguId = luoguBtn.dataset.luoguId;
                if (luoguId) {
                    window.open(`https://www.luogu.com.cn/problem/${luoguId}`, '_blank');
                }
            }
        });

        // 使用防抖观察器
        const observer = new MutationObserver(function() {
            if (!window.luoguDebounce) {
                window.luoguDebounce = setTimeout(() => {
                    processProblemTitles();
                    window.luoguDebounce = null;
                }, 100);
            }
        });

        // 观察题目列表区域
        const problemList = document.querySelector('.problem-list, .problems-container') || document.body;
        observer.observe(problemList, {
            childList: true,
            subtree: true
        });

        // 初始处理
        processProblemTitles();
    }
})();