油猴插件

用 AI 写的神秘玩意。

QOJ系列站点-导航右侧互跳
// ==UserScript==
// @name         QOJ系列站点-导航右侧互跳
// @namespace    https://github.com/
// @version      2.0
// @description  在qoj.ac/relia.uk/huang.lt/jiang.ly原生导航栏右侧插入站点互跳链接,支持路径跟随
// @author       自定义
// @match        *://qoj.ac/*
// @match        *://relia.uk/*
// @match        *://huang.lt/*
// @match        *://jiang.ly/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 配置互跳站点(域名+简洁显示名,贴合原生导航风格)
    const SITES = [
        { domain: 'qoj.ac', name: 'qoj.ac' },
        { domain: 'huang.lt', name: 'huang.lt' },
        { domain: 'jiang.ly', name: 'jiang.ly' },
        { domain: 'relia.uk', name: 'relia.uk' },
    ];

    // 获取当前页面核心信息(域名+完整路径,含参数)
    const currentHost = window.location.host;
    const fullPath = window.location.pathname + window.location.search;

    // 核心:定位原生导航容器(匹配站点导航栏特征,兼容4个站点)
    // 适配QOJ系列站点导航栏的常见选择器,优先级从高到低
    let navContainer = null;
    const selectors = [
        'nav.navbar',
        '.nav',
        'ul[class*="nav"]',
        'div[class*="nav"]',
        'header nav'
    ];
    // 遍历选择器,找到真实的导航容器
    for (const sel of selectors) {
        navContainer = document.querySelector(sel);
        if (navContainer) break;
    }
    // 兜底:若未匹配到导航容器,定位包含目标导航文字的父容器
    if (!navContainer) {
        const navText = document.querySelector('a:contains("比赛"), a:contains("题库"), a:contains("博客")');
        if (navText) navContainer = navText.closest('ul, div, nav');
    }
    // 未找到导航容器则终止(避免脚本报错)
    if (!navContainer) return;

    // 创建互跳链接外层容器(行内布局,贴合原生导航)
    const jumpWrap = document.createElement('div');
    jumpWrap.style.cssText = `
        display: inline-flex;
        align-items: center;
        gap: 16px;
        margin-left: 20px;
        padding-left: 20px;
        border-left: 1px solid #e5e7eb;
        height: 100%;
    `;

    // 生成每个站点的跳转链接(样式贴合原生导航)
    SITES.forEach(site => {
        const a = document.createElement('a');
        a.href = `https://${site.domain}${fullPath}`;
        a.textContent = site.name;
        // 关键:样式与站点原生导航链接完全一致,无突兀感
        a.style.cssText = `
            color: ${site.domain === currentHost ? '#2563eb' : '#374151'};
            text-decoration: none;
            font-size: inherit;
            font-weight: ${site.domain === currentHost ? '600' : '400'};
            padding: 8px 0;
            line-height: inherit;
            transition: color 0.2s;
        `;
        // 自身站点禁用点击,避免无效自跳转
        if (site.domain === currentHost) {
            a.style.pointerEvents = 'none';
            a.style.opacity = '0.9';
        }
        // 悬浮效果匹配原生导航风格
        a.onmouseover = () => {
            if (site.domain !== currentHost) a.style.color = '#2563eb';
        };
        a.onmouseout = () => {
            if (site.domain !== currentHost) a.style.color = '#374151';
        };
        jumpWrap.appendChild(a);
    });

    // 核心:将互跳容器插入到原生导航栏的【最后一个子元素后方】
    // 即「比赛、比赛归档、题库...博客」的右侧,完美符合需求
    navContainer.appendChild(jumpWrap);

})();
CF perf Graph
// ==UserScript==
// @name         Codeforces Contest Perf Chart (Enhanced)
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  插入Perf列并绘制折线图。支持过滤非计分赛,前6场不显示Perf折线。
// @author       Gemini Help
// @match        https://codeforces.com/contests/with/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let hasAddedPerfColumn = false;
    let hasDrawnChart = false;

    const ratingColors = [
        { min: 0, max: 1199, color: '#808080' },
        { min: 1200, max: 1399, color: '#008000' },
        { min: 1400, max: 1599, color: '#03a89e' },
        { min: 1600, max: 1899, color: '#0000ff' },
        { min: 1900, max: 2099, color: '#aa00aa' },
        { min: 2100, max: 2299, color: '#ff8c00' },
        { min: 2300, max: 2399, color: '#ff8c00' },
        { min: 2400, max: 2599, color: '#ff0000' },
        { min: 2600, max: 2999, color: '#ff0000' },
        { min: 3000, max: Infinity, color: '#cc0000' }
    ];

    function loadChartJS(callback) {
        if (window.Chart) { callback(); return; }
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js';
        script.onload = callback;
        document.head.appendChild(script);
    }

    function extractNumber(text) {
        if (!text || text.trim() === "" || text.includes('—')) return null;
        const match = text.trim().match(/([+-−]?\d+)/);
        return match ? parseInt(match[1].replace('−', '-')) : null;
    }

    function getRatingColor(value) {
        for (const level of ratingColors) {
            if (value >= level.min && value <= level.max) return level.color;
        }
        return '#000000';
    }

    function addPerfColumn() {
    if (hasAddedPerfColumn) return;

    const contestTable = document.querySelector('.user-contests-table');
    if (!contestTable) return;

    const headerRow = contestTable.querySelector('thead tr');
    const newRatingTh = headerRow.querySelectorAll('th')[6];
    const perfTh = document.createElement('th');
    perfTh.className = 'top header';
    perfTh.style = 'padding-right: 1.5em;';
    perfTh.textContent = 'Perf';
    headerRow.insertBefore(perfTh, newRatingTh);

    const bodyRows = Array.from(contestTable.querySelectorAll('tbody tr'));

    /* === 预扫描:找所有有 rating change 的行 === */
    const ratedRows = bodyRows.filter(row => {
        const tds = row.querySelectorAll('td');
        if (tds.length < 7) return false;
        const ratingChange = extractNumber(tds[5].textContent);
        const newRating = extractNumber(tds[6].textContent);
        return ratingChange !== null && newRating !== null;
    });

    /* === 判断是否需要隐藏前 6 场 === */
    let hideFirstSix = false;
    if (ratedRows.length > 0) {
        const tds = ratedRows[0].querySelectorAll('td');
        const ratingChange = extractNumber(tds[5].textContent);
        const newRating = extractNumber(tds[6].textContent);
        const oldRating = newRating - ratingChange;
        if (oldRating !== 1500) {
            hideFirstSix = true;
        }
    }

    /* === 插入 Perf 列 === */
    let ratedIndex = 0;

    bodyRows.forEach(row => {
        const tds = row.querySelectorAll('td');
        if (tds.length < 7) return;

        const ratingChange = extractNumber(tds[5].textContent);
        const newRating = extractNumber(tds[6].textContent);

        const perfTd = document.createElement('td');
        perfTd.className = tds[5].className;
        perfTd.style.fontWeight = 'bold';

        if (ratingChange === null || newRating === null) {
            perfTd.textContent = '—';
            perfTd.style.color = 'inherit';
        } else {
            if (hideFirstSix && ratedIndex + 6 >= ratedRows.length) {
                perfTd.textContent = '—';
                perfTd.style.color = 'inherit';
            } else {
                const perfValue = newRating + 3 * ratingChange;
                const perfStr = perfValue.toString();
                const color = getRatingColor(perfValue);

                if (perfValue >= 3000) {
                    perfTd.innerHTML =
                        `<span style="color:#000">${perfStr[0]}</span>` +
                        `<span style="color:#cc0000">${perfStr.slice(1)}</span>`;
                } else {
                    perfTd.style.color = color;
                    perfTd.textContent = perfValue;
                }
            }
            ratedIndex++;
        }

        row.insertBefore(perfTd, tds[6]);
    });

    hasAddedPerfColumn = true;
}


    function getMonthNum(monthAbbr) {
        const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
        return (months.indexOf(monthAbbr) + 1).toString().padStart(2, '0');
    }

    function extractChartData() {
        const contestTable = document.querySelector('.user-contests-table');
        if (!contestTable) return [];

        const rawData = [];
        const bodyRows = contestTable.querySelectorAll('tbody tr');

        bodyRows.forEach(row => {
            const tds = row.querySelectorAll('td');
            if (tds.length < 8) return;

            const ratingChange = extractNumber(tds[5].textContent);
            const newRating = extractNumber(tds[7].textContent);

            if (ratingChange !== null && newRating !== null) {
                const rawText = tds[2].textContent.trim();
                // 匹配格式如: "Jan/20/2024"
                const dateMatch = rawText.match(/([a-zA-Z]+)\/(\d+)\/(\d+)/);

                if (dateMatch) {
                    const [_, monthAbbr, day, year] = dateMatch;
                    const date = new Date(`${year}-${getMonthNum(monthAbbr)}-${day.padStart(2, '0')}`);

                    // --- 修改点:将 label 修改为包含年份的格式 ---
                    // 如果你想要 "24/Jan/20" 这种简写,可以使用 year.slice(-2)
                    const displayLabel = `${year}/${monthAbbr}/${day}`;

                    rawData.push({
                        date: date,
                        label: displayLabel,
                        rating: newRating,
                        perf: newRating + 3 * ratingChange
                    });
                }
            }
        });

        const sortedData = rawData.sort((a, b) => a.date - b.date);

        return sortedData.map((item, index) => ({
            ...item,
            perf: (index < 6) ? null : item.perf
        }));
    }

    function drawChart() {
        if (hasDrawnChart) return;
        const chartData = extractChartData();
        if (chartData.length === 0) return;

        const labels = chartData.map(d => d.label);
        const ratingPoints = chartData.map(d => d.rating);
        const perfPoints = chartData.map(d => d.perf);

        const allValues = [...ratingPoints, ...perfPoints].filter(v => v !== null);
        const yMax = Math.max(...allValues) + 100;

        const chartContainer = document.createElement('div');
        GM_addStyle(`#rating-perf-chart { width: 95%; height: 400px; margin: 20px auto; border: 1px solid #e1e1e1; padding: 10px; background: #fff; }`);
        chartContainer.id = 'rating-perf-chart';
        const tableWrapper = document.querySelector('.datatable');
        tableWrapper.parentNode.insertBefore(chartContainer, tableWrapper);

        const ctx = document.createElement('canvas');
        chartContainer.appendChild(ctx);

        new Chart(ctx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [
                    {
                        label: 'New Rating',
                        data: ratingPoints,
                        borderColor: '#2196F3',
                        backgroundColor: '#2196F380',
                        borderWidth: 2,
                        tension: 0.1,
                        spanGaps: true
                    },
                    {
                        label: 'Perf (Post-6th)',
                        data: perfPoints,
                        borderColor: '#4CAF50',
                        backgroundColor: '#4CAF5080',
                        borderWidth: 2,
                        tension: 0.1,
                        spanGaps: false // 设置为 false,使得前六场 null 值处不连线
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: { beginAtZero: false, max: yMax },
                    x: { grid: { display: false } }
                },
                plugins: {
                    tooltip: { mode: 'index', intersect: false }
                }
            },
            plugins: [{
                id: 'rating-bg',
                beforeDraw: (chart) => {
                    const { ctx, chartArea, scales: { y } } = chart;
                    ratingColors.forEach(level => {
                        if (level.min > yMax) return;
                        const yTop = y.getPixelForValue(Math.min(level.max, yMax));
                        const yBottom = y.getPixelForValue(level.min);
                        ctx.fillStyle = level.color + '15';
                        ctx.fillRect(chartArea.left, yTop, chartArea.width, yBottom - yTop);
                    });
                }
            }]
        });
        hasDrawnChart = true;
    }

    function init() {
        addPerfColumn();
        loadChartJS(drawChart);
    }

    const observer = new MutationObserver(() => {
        if (document.querySelector('.user-contests-table')) {
            init();
            observer.disconnect();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(init, 2000);
})();
洛谷奖项盒人
// ==UserScript==
// @name         OIerDb 查询代码生成器
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  根据洛谷获奖信息生成 OIerDb 查询代码
// @author       Assistant
// @match        https://www.luogu.com.cn/user/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    /**
     * 洛谷比赛显示名 → OIerDb 查询配置
     * 基于洛谷官方支持认证比赛列表 + 137条contests数据验证
     * 官方列表: https://www.luogu.com.cn/cert/contests
     */
    const CONTEST_CONFIG = {
        // ==================== CSP 系列 (2019-2025) ====================
        'CSP-J': {
            type: 'CSP入门',
            name: (year) => `CSP${year}入门`,
            years: [2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },
        'CSP-S': {
            type: 'CSP提高',
            name: (year) => `CSP${year}提高`,
            years: [2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },

        // ==================== NOIP 系列 ====================
        // 2020年起:NOIP (type: NOIP, name: NOIP{year})
        'NOIP': {
            type: (year) => year >= 2020 ? 'NOIP' : 'NOIP提高',
            name: (year) => year >= 2020 ? `NOIP${year}` : `NOIP${year}提高`,
            years: [2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2020, 2021, 2022, 2023, 2024, 2025]
            // 注意:2019年无NOIP,被CSP替代
        },
        // 2013-2018:NOIP 普及组 (type: NOIP普及)
        'NOIP 普及组': {
            type: 'NOIP普及',
            name: (year) => `NOIP${year}普及`,
            years: [2013, 2014, 2015, 2016, 2017, 2018]
        },
        // 兼容旧显示名(部分页面可能显示为"NOIP 普及")
        'NOIP 普及': {
            type: 'NOIP普及',
            name: (year) => `NOIP${year}普及`,
            years: [2013, 2014, 2015, 2016, 2017, 2018]
        },
        // 2008-2018:NOIP 提高组 (type: NOIP提高)
        'NOIP 提高组': {
            type: 'NOIP提高',
            name: (year) => `NOIP${year}提高`,
            years: [2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018]
        },

        // ==================== NOI 系列 ====================
        'NOI': {
            type: 'NOI',
            name: (year) => `NOI${year}`,
            years: [2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },
        'NOI 冬令营': {
            type: 'WC',
            name: (year) => `WC${year}`,
            years: [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]
        },
        'NOI 夏令营': {
            // 2010-2020: name为 NOI{year}D类
            // 2021-2025: name为 NOI{year}夏令营
            type: 'NOID类',
            name: (year) => year <= 2020 ? `NOI${year}D类` : `NOI${year}夏令营`,
            years: [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },
        'NOI 春季测试': {
            type: 'NOIST',
            name: (year) => `春季测试${year}`,
            years: [2023]
        },
        'NOI 女生竞赛': {
            type: 'NOI女生竞赛',
            name: (year) => `NOI${year}女生竞赛`,
            years: [2022, 2023, 2024, 2025, 2026]
        },

        // ==================== 其他国赛 ====================
        'CTSC': {
            type: 'CTSC',
            // 2019年特殊:name是"CTS2019"而非"CTSC2019"
            name: (year) => year === 2019 ? `CTS${year}` : `CTSC${year}`,
            years: [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019]
            // 2020年起CTSC停办
        },
        'APIO': {
            type: 'APIO',
            name: (year) => `APIO${year}`,
            years: [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },
        'APIO 线上': {
            type: 'APIO',
            name: (year) => `APIO${year}线上`,
            years: [2022, 2023]
        },
        'IOI': {
            type: 'IOI',
            name: (year) => `IOI${year}`,
            years: [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
        },
        'NGOI': {
            type: 'NGOI',
            name: (year) => `NGOI${year}`,
            years: [2022, 2023, 2024, 2025, 2026]
        }
    };

    /**
     * 解析洛谷奖项数据(从 lentille-context)
     */
    function parsePrizes() {
        try {
            const ctx = JSON.parse(document.getElementById('lentille-context')?.textContent || '{}');
            const prizes = ctx?.data?.prizes;
            if (Array.isArray(prizes)) {
                return prizes
                    .map(p => p.prize)
                    .filter(p => p?.year && p?.contest && p?.prize);
            }
        } catch (e) {
            console.warn('解析 lentille-context 失败:', e);
        }
        return [];
    }

    /**
     * 模糊匹配比赛名称(处理显示名差异)
     */
    function findContestConfig(contestName) {
        // 精确匹配
        if (CONTEST_CONFIG[contestName]) {
            return { key: contestName, config: CONTEST_CONFIG[contestName] };
        }

        // 模糊匹配(处理空格、简写等差异)
        for (const [key, config] of Object.entries(CONTEST_CONFIG)) {
            // 去除空格后比较
            if (contestName.replace(/\s/g, '') === key.replace(/\s/g, '')) {
                return { key, config };
            }
            // 包含匹配
            if (contestName.includes(key) || key.includes(contestName)) {
                return { key, config };
            }
        }

        return null;
    }

    /**
     * 将单个奖项转换为 OIerDb 查询条件
     */
    function prizeToCondition(prize) {
        const { year, contest, prize: level } = prize;
        const match = findContestConfig(contest);

        // 未知比赛:降级处理
        if (!match) {
            console.warn(`⚠️ 未知比赛: "${contest}",使用 name 包含匹配`);
            const safeContest = contest.replace(/'/g, "\\'");
            return `oier.records.some(r => r.contest.name.includes('${safeContest}') && r.contest.year === ${year} && r.level === '${level}')`;
        }

        const { key, config } = match;

        // 验证年份是否在支持范围内
        if (config.years && !config.years.includes(year)) {
            console.warn(`⚠️ ${key} 不支持 ${year} 年,但仍尝试生成查询`);
        }

        // 解析 type(可能是函数)
        const contestType = typeof config.type === 'function' ? config.type(year) : config.type;
        // 解析 name(可能是函数)
        const contestName = typeof config.name === 'function' ? config.name(year) : config.name;

        // 生成精确匹配条件
        return `oier.records.some(r => r.contest.type === '${contestType}' && r.contest.name === '${contestName}' && r.level === '${level}')`;
    }

    /**
     * 生成最终查询代码
     */
    function generateCode(prizes) {
        if (!prizes.length) return '/* 无有效奖项数据 */';

        const conditions = prizes.map(prizeToCondition);

        return `/** @param {OIerDb} db */
export default (db) => {
  return db.oiers.filter((oier) => {
    // 匹配所有奖项(AND 逻辑)
    return ${conditions.join(' &&\n           ')};
  });
}`;
    }

    /**
     * 注入按钮到页面
     */
    function injectButton() {
        const header = document.querySelector('h3:has(svg[data-icon="award"])');
        if (!header) return false;
        if (header.querySelector('#oierdb-gen-btn')) return true;

        const btn = document.createElement('button');
        btn.id = 'oierdb-gen-btn';
        btn.innerText = '🔗 生成 OIerDb 查询';
        btn.style.cssText = `
            float:right; margin-right:8px; padding:4px 10px; font-size:12px;
            cursor:pointer; background:linear-gradient(135deg,#667eea,#764ba2);
            color:white; border:none; border-radius:4px;
            box-shadow:0 2px 4px rgba(0,0,0,0.2); transition:opacity 0.2s;
        `;
        btn.onmouseenter = () => btn.style.opacity = '0.9';
        btn.onmouseleave = () => btn.style.opacity = '1';

        const infoLink = header.querySelector('a[href="javascript:void 0"]');
        if (infoLink) header.insertBefore(btn, infoLink);
        else header.appendChild(btn);

        btn.onclick = () => {
            const prizes = parsePrizes();
            if (!prizes.length) {
                alert('⚠️ 未找到奖项数据,请确保页面已完全加载');
                return;
            }

            const code = generateCode(prizes);

            // 输出调试信息
            console.group('📋 OIerDb 查询代码生成详情');
            console.log('🏆 解析到的奖项:');
            prizes.forEach((p, i) => {
                const match = findContestConfig(p.contest);
                console.log(`  [${i}] ${p.year} ${p.contest} ${p.prize}`);
                if (match) {
                    const type = typeof match.config.type === 'function' ? match.config.type(p.year) : match.config.type;
                    const name = typeof match.config.name === 'function' ? match.config.name(p.year) : match.config.name;
                    console.log(`      → type: "${type}", name: "${name}"`);
                }
            });
            console.log('\n📝 生成的代码已复制到剪贴板');
            console.groupEnd();

            navigator.clipboard.writeText(code).then(() => {
                const oldText = btn.innerText;
                btn.innerText = '✅ 已复制!';
                btn.style.background = '#10b981';
                setTimeout(() => {
                    btn.innerText = oldText;
                    btn.style.background = '';
                }, 1500);
            }).catch(() => {
                prompt('复制失败,请手动复制:', code);
            });
        };
        return true;
    }

    // 初始化:SPA 兼容
    function init() {
        if (injectButton()) return;
        const observer = new MutationObserver((mutations, obs) => {
            if (injectButton()) obs.disconnect();
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setTimeout(() => observer.disconnect(), 30000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
posted @ 2026-02-02 23:13  adam01  阅读(168)  评论(0)    收藏  举报