洛谷到 VJudge 跳转脚本

在洛谷问题页面的特定位置添加一个跳转到 VJudge 的链接,并与“展开”按钮样式一致

目前为 2024-10-18 提交的版本。查看 最新版本

// ==UserScript==
// @name         洛谷到 VJudge 跳转脚本
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  在洛谷问题页面的特定位置添加一个跳转到 VJudge 的链接,并与“展开”按钮样式一致
// @author       Kimi
// @match        https://www.luogu.com.cn/problem/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 函数:生成 VJudge 的问题 URL
    function generateVJudgeURL(problemCode) {
        const vjudgePrefix = "洛谷-";
        const vjudgeProblemName = encodeURIComponent(vjudgePrefix + problemCode);
        return `https://vjudge.net/problem/${vjudgeProblemName}`;
    }

    // 函数:提取洛谷的问题编号
    function getProblemCode() {
        const urlPath = window.location.pathname;
        const problemMatch = urlPath.match(/\/problem\/([PB]\d+)/i);
        if (!problemMatch) {
            console.warn("无法识别的问题编号格式。");
            return null;
        }
        return problemMatch[1].toUpperCase(); // 如 P8306 或 B1001
    }

    // 函数:创建跳转按钮
    function createVJudgeButton(vjudgeURL) {
        // 创建 <a> 元素
        const link = document.createElement('a');
        link.href = vjudgeURL;
        link.target = '_blank'; // 在新标签页打开
        link.className = 'hide-side color-default'; // 与“展开”按钮相同的类名

        // 创建 <span> 元素
        const span = document.createElement('span');

        // 创建 SVG 图标(使用与“展开”按钮相同的图标,或选择其他合适的图标)
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("aria-hidden", "true");
        svg.setAttribute("focusable", "false");
        svg.setAttribute("data-prefix", "fas");
        svg.setAttribute("data-icon", "external-link-alt"); // 选择一个合适的图标
        svg.setAttribute("role", "img");
        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("viewBox", "0 0 512 512");
        svg.classList.add("svg-inline--fa", "fa-external-link-alt");

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("fill", "currentColor");
        path.setAttribute("d", "M432 320h-32v-32c0-17.7-14.3-32-32-32h-96c-17.7 0-32 14.3-32 32v32h-32c-17.7 0-32 14.3-32 32v128c0 17.7 14.3 32 32 32h128c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32zm-80-160h96v32h-96V160z");
        svg.appendChild(path);

        // 设置图标的大小
        svg.style.width = '1em';
        svg.style.height = '1em';
        svg.style.marginRight = '5px'; // 图标与文字之间的间距

        // 设置按钮文本
        const text = document.createTextNode('在 VJudge 上查看');

        // 组装 <span>
        span.appendChild(svg);
        span.appendChild(text);

        // 组装 <a>
        link.appendChild(span);

        return link;
    }

    // 函数:插入按钮到指定的 <div class="action">
    function insertButton() {
        const problemCode = getProblemCode();
        if (!problemCode) return;

        const vjudgeURL = generateVJudgeURL(problemCode);

        // 查找目标 <div class="action">
        const actionDiv = document.querySelector('div.action');

        if (actionDiv) {
            // 检查是否已经添加过按钮,避免重复
            if (actionDiv.querySelector('.vjudge-link')) {
                console.warn("VJudge 跳转链接已存在。");
                return;
            }

            const vjudgeButton = createVJudgeButton(vjudgeURL);
            vjudgeButton.classList.add('vjudge-link'); // 添加额外类名以便识别

            actionDiv.appendChild(vjudgeButton);
            console.log(`已添加 VJudge 跳转链接: ${vjudgeURL}`);
        } else {
            console.warn("未找到 <div class=\"action\"> 元素。");
        }
    }

    // 等待页面完全加载后执行
    window.addEventListener('load', insertButton);

    // 处理单页面应用(SPA)可能的动态内容加载
    // 使用 MutationObserver 监听 DOM 变化,确保在动态加载的情况下也能插入按钮
    const observer = new MutationObserver((mutations, obs) => {
        const actionDiv = document.querySelector('div.action');
        if (actionDiv && !actionDiv.querySelector('.vjudge-link')) {
            insertButton();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();