洛谷提交优化

· · 科技·工程

由 AI 辅助完成。

与[工程] 洛谷修改提交记录方框背景一起使用效果更佳。

V1.0

AC 题目时会有横幅。

:::info[源代码]

// ==UserScript==
// @name         洛谷成就系统
// @namespace    http://tampermonkey.net/
// @version      V1.0
// @description  由 Gemini 辅助完成。
// @author       Otachi
// @match        *://*.luogu.com.cn/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const AIRSPACES = {
        1: { name: "入门", color: "#FE4C61", rarity: "Common" },
        2: { name: "普及-", color: "#F39C11", rarity: "Unusual" },
        3: { name: "普及/提高−", color: "#FFC116", rarity: "Rare" },
        4: { name: "普及+/提高", color: "#52C41A", rarity: "Epic" },
        5: { name: "提高+/省选−", color: "#3498DB", rarity: "Legendary" },
        6: { name: "省选/NOI−", color: "#9D3DCF", rarity: "Mythic" },
        7: { name: "NOI/NOI+/CTSC", color: "#0E1D69", rarity: "Ultra" }
    };

    let lastRecordId = "";

    function launchFireworks(color) {
        if (typeof confetti !== 'function') return;
        const duration = 3000;
        const end = Date.now() + duration;
        (function frame() {
            confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.8 }, colors: [color, '#ffffff'] });
            confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.8 }, colors: [color, '#ffffff'] });
            if (Date.now() < end) requestAnimationFrame(frame);
        }());
    }

    // --- 核心修复:平滑离场逻辑 ---
    function closeBanner(banner) {
        if (!banner || banner.classList.contains('leaving')) return;

        // 关键动作:移除所有可能干扰离场的动画类(特别是针对高难度的呼吸灯)
        banner.classList.remove('banner-enter', 'glow-pulse');

        // 强制触发重绘,确保浏览器立即停止之前的动画
        void banner.offsetWidth;

        // 标记正在离场
        banner.classList.add('leaving');

        // 400ms 后从 DOM 彻底移除
        setTimeout(() => {
            if (banner.parentNode) banner.remove();
        }, 400);
    }

    function showAchievementBanner(target, pid) {
        const oldBanner = document.getElementById('florr-achievement-banner');
        if (oldBanner) oldBanner.remove();

        const banner = document.createElement('div');
        banner.id = 'florr-achievement-banner';

        banner.style = `
            position: fixed; top: 0; left: 0; width: 100%; height: 140px;
            z-index: 1000000; display: flex; align-items: center; justify-content: center;
            background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.98) 20%, rgba(255,255,255,0.98) 80%, transparent 100%);
            border-bottom: 8px solid ${target.color};
            box-shadow: 0 15px 50px rgba(0,0,0,0.15);
            cursor: pointer;
            user-select: none;
        `;

        const isHighRank = ["Legendary", "Mythic", "Ultra"].includes(target.rarity);
        banner.className = `banner-enter ${isHighRank ? 'glow-pulse' : ''}`;

        banner.innerHTML = `
            <div style="display: flex; align-items: center; gap: 50px; width: 95%; max-width: 1300px; justify-content: center;">
                <div style="
                    flex-shrink: 0; width: 240px; height: 100px; border-radius: 20px; border: 6px solid ${target.color};
                    background: #fff; display: flex; align-items: center; justify-content: center;
                    font-size: 26px; font-weight: 900; color: ${target.color}; text-align: center;
                    line-height: 1.1; padding: 0 15px; box-sizing: border-box;
                    box-shadow: inset 0 0 20px ${target.color}22;
                    letter-spacing: 0.5px;
                ">
                    ${target.name}
                </div>
                <div style="text-align: left; font-family: 'Arial Black', sans-serif; min-width: 450px;">
                    <div style="font-size: 16px; font-weight: 900; color: #777; letter-spacing: 4px; margin-bottom: 4px;">MISSION COMPLETE</div>
                    <div style="font-size: 45px; font-weight: 900; color: ${target.color}; line-height: 1.1;">
                        Destroy Target <span style="font-family: 'Courier New', Courier, monospace; letter-spacing: -1px;">${pid}</span>
                    </div>
                    <div style="font-size: 18px; font-weight: 400; color: #aaa; margin-top: 4px; font-family: sans-serif; letter-spacing: 2px;">
                        RANK: ${target.rarity.toUpperCase()}
                    </div>
                </div>
            </div>`;

        document.body.appendChild(banner);
        banner.onclick = () => closeBanner(banner);
        setTimeout(() => closeBanner(banner), 7000);
    }

    function lockAndFire() {
        try {
            const feData = window._feInjection || window._feConfig;
            if (!feData || !feData.currentData) return;
            const record = feData.currentData.record;
            if (!record) return;

            if ((record.status === 12 || record.status === "Accepted") && record.id !== lastRecordId) {
                const currentUid = feData.currentUser?.uid || feData.currentUser?.id;
                const targetUid = record.user.uid || record.user.id;
                if (currentUid && targetUid && currentUid == targetUid) {
                    lastRecordId = record.id;
                    const target = AIRSPACES[record.problem.difficulty];
                    if (target) {
                        showAchievementBanner(target, record.problem.pid);
                        if (record.problem.difficulty >= 5) launchFireworks(target.color);
                    }
                }
            }
        } catch (e) { }
    }

    const style = document.createElement('style');
    style.innerHTML = `
        /* 进场动画 */
        .banner-enter { animation: slideIn 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; }

        /* 离场动画:增加优先级,确保覆盖无限循环的呼吸效果 */
        .leaving {
            animation: slideOut 0.4s cubic-bezier(0.6, -0.28, 0.735, 0.045) forwards !important;
            pointer-events: none;
        }

        @keyframes slideIn { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

        @keyframes slideOut {
            0% { transform: translateY(0); opacity: 1; }
            30% { transform: translateY(10px); opacity: 1; }
            100% { transform: translateY(-120%); opacity: 0; }
        }

        /* 呼吸灯动画 */
        .glow-pulse { animation: slideIn 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards, pulse 2s infinite ease-in-out; }

        @keyframes pulse {
            0% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
            50% { box-shadow: 0 15px 60px rgba(52, 152, 219, 0.3); border-bottom-color: #fff; }
            100% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
        }
    `;
    document.head.appendChild(style);
    setInterval(lockAndFire, 1000);
})();

:::

效果:

通过蓝/紫/黑题时会有烟花:

V2.0

在 V1.0 的基础上,增加了 WA、TLE 等评测状态的横幅。

感谢 @ZAR_Ferarri_F1_Team 提供的灵感。

:::info[源代码]

// ==UserScript==
// @name         洛谷提交状态优化
// @namespace    http://tampermonkey.net/
// @version      V2.0
// @description  感谢 ZAR_Ferarri_F1_Team & Gemini 的贡献。
// @author       Otachi
// @match        *://*.luogu.com.cn/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 难度定义 (仅用于 AC 时的装饰)
    const AIRSPACES = {
        1: { name: "入门", color: "#FE4C61", rarity: "COMMON" },
        2: { name: "普及-", color: "#F39C11", rarity: "UNUSUAL" },
        3: { name: "普及/提高−", color: "#FFC116", rarity: "RARE" },
        4: { name: "普及+/提高", color: "#52C41A", rarity: "EPIC" },
        5: { name: "提高+/省选−", color: "#3498DB", rarity: "LEGENDARY" },
        6: { name: "省选/NOI−", color: "#9D3DCF", rarity: "MYTHIC" },
        7: { name: "NOI/NOI+/CTSC", color: "#0E1D69", rarity: "ULTRA" }
    };

    // 核心:洛谷原色定义
    const JUDGE_STATUS = {
        "AC":  { text: "ACCEPTED", color: "#52C41A", bgColor: "#E8F5E9" },
        "WA":  { text: "WRONG ANSWER", color: "#E74C3C", bgColor: "#FDEDEC" },
        "TLE": { text: "TIME LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
        "MLE": { text: "MEMORY LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
        "RE":  { text: "RUNTIME ERROR", color: "#9D3DCF", bgColor: "#F5EEF8" },
        "CE":  { text: "COMPILE ERROR", color: "#F39C11", bgColor: "#FEF5E7" },
        "OLE": { text: "OUTPUT LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
        "UKE": { text: "UNKNOWN ERROR", color: "#0E1D69", bgColor: "#EAECEE" }
    };

    const TESTCASE_STATUS_MAP = { 3: "OLE", 4: "MLE", 5: "TLE", 6: "WA", 7: "RE", 12: "AC" };
    let lastRecordId = "";

    function launchFireworks(color) {
        if (typeof confetti !== 'function') return;
        const duration = 2000, end = Date.now() + duration;
        (function frame() {
            confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.8 }, colors: [color, '#ffffff'] });
            confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.8 }, colors: [color, '#ffffff'] });
            if (Date.now() < end) requestAnimationFrame(frame);
        }());
    }

    function closeBanner(banner) {
        if (!banner || banner.classList.contains('leaving')) return;
        banner.classList.remove('glow-pulse');
        banner.classList.add('leaving');
        banner.style.transform = 'translateY(-140px)';
        banner.style.opacity = '0';
        setTimeout(() => { if (banner.parentNode) banner.remove(); }, 600);
    }

    function getJudgeStatus(record) {
        const sc = Number(record.status);
        if (sc === 0) return "WAIT";
        if (sc === 1) return "JUDGE";
        if (sc === 2) return "CE";
        const detail = record.detail;
        if (!detail || !detail.judgeResult) return sc === 12 ? "AC" : "UKE";
        const subtasks = detail.judgeResult.subtasks || [];
        let firstErr = null;
        Object.values(subtasks).forEach(sub => {
            Object.values(sub.testCases || {}).forEach(tc => {
                if (tc.status !== 12 && !firstErr) firstErr = tc.status;
            });
        });
        return firstErr ? (TESTCASE_STATUS_MAP[firstErr] || "WA") : "AC";
    }

    function showAchievementBanner(target, pid, statusKey) {
        const old = document.getElementById('florr-achievement-banner');
        if (old) old.remove();

        const banner = document.createElement('div');
        banner.id = 'florr-achievement-banner';

        const info = JUDGE_STATUS[statusKey] || JUDGE_STATUS["UKE"];
        const isAC = statusKey === "AC";
        const color = isAC ? target.color : info.color; // AC用难度色,其余用原色

        banner.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100%; height: 140px;
            z-index: 1000000; display: flex; align-items: center; justify-content: center;
            background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.98) 20%, rgba(255,255,255,0.98) 80%, transparent 100%);
            border-bottom: 8px solid ${color};
            box-shadow: 0 15px 50px rgba(0,0,0,0.15);
            cursor: pointer; user-select: none;
            transform: translateY(-100%); opacity: 0;
            transition: transform 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28), opacity 0.4s ease;
        `;

        const isHigh = ["LEGENDARY", "MYTHIC", "ULTRA"].includes(target.rarity);
        banner.className = (isAC && isHigh) ? 'glow-pulse' : '';

        const actionText = isAC ? 'Destroyed Target' : 'Destroyed by Target';
        const missionSub = isAC ? 'MISSION COMPLETE' : 'MISSION FAILED';

        banner.innerHTML = `
            <div style="display: flex; align-items: center; gap: 50px; width: 95%; max-width: 1300px; justify-content: center;">
                <div style="flex-shrink: 0; width: 240px; height: 100px; border-radius: 20px; border: 6px solid ${color};
                    background: ${info.bgColor}; display: flex; align-items: center; justify-content: center;
                    font-size: 26px; font-weight: 900; color: ${color}; text-align: center;
                    box-shadow: inset 0 0 20px ${color}22;">
                    ${isAC ? target.name : statusKey}
                </div>
                <div style="text-align: left; font-family: 'Arial Black', sans-serif; min-width: 450px;">
                    <div style="font-size: 16px; font-weight: 900; color: #777; letter-spacing: 4px; margin-bottom: 4px;">
                        ${missionSub}
                    </div>
                    <div style="font-size: 45px; font-weight: 900; color: ${color}; line-height: 1.1;">
                        ${actionText} <span style="font-family: 'Courier New', Courier, monospace; letter-spacing: -1px;">${pid}</span>
                    </div>
                    <div style="font-size: 18px; color: #aaa; margin-top: 4px; letter-spacing: 2px;">
                        RANK: ${target.rarity}
                    </div>
                </div>
            </div>`;

        document.body.appendChild(banner);
        banner.onclick = () => closeBanner(banner);
        setTimeout(() => { banner.style.transform = 'translateY(0)'; banner.style.opacity = '1'; }, 100);

        // 动态消失时间:CE短,AC长
        let duration = isAC ? 7000 : 5000;
        if (statusKey === "CE") duration = 3000;
        setTimeout(() => closeBanner(banner), duration);
    }

    function lockAndFire() {
        try {
            const feData = window._feInjection || window._feConfig;
            if (!feData || !feData.currentData) return;
            const record = feData.currentData.record;
            if (!record || record.id === lastRecordId) return;

            if ((feData.currentUser?.uid || feData.currentUser?.id) == (record.user.uid || record.user.id)) {
                const status = getJudgeStatus(record);
                if (status === "WAIT" || status === "JUDGE") return;

                lastRecordId = record.id;
                const target = AIRSPACES[record.problem.difficulty] || AIRSPACES[1];
                showAchievementBanner(target, record.problem.pid, status);
                if (status === "AC" && record.problem.difficulty >= 5) launchFireworks(target.color);
            }
        } catch (e) {}
    }

    const style = document.createElement('style');
    style.innerHTML = `
        .glow-pulse { animation: pulse 2s infinite ease-in-out; }
        @keyframes pulse {
            0% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
            50% { box-shadow: 0 15px 70px rgba(52, 152, 219, 0.4); border-bottom-color: #fff; }
            100% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
        }
        .leaving { pointer-events: none; transition: transform 0.5s cubic-bezier(0.55, 0, 1, 0.45), opacity 0.4s ease !important; }
    `;
    document.head.appendChild(style);
    setInterval(lockAndFire, 1000);
})();

:::