折叠题目背景

· · 科技·工程

启发是来自 这个工单,闲来无事,遂写脚本完成折叠题目背景的功能。

(鬼图警告:如果你没见过 P3934,那我建议你还是不要打开这个工单,至少我认为挺瘆人的/ll)

感觉是不是真没这么重要,实在受不了自己写个脚本屏蔽吧。

其实我认为这个功能还是蛮有必要的,毕竟 Ynoi 的题目背景还是挺长且麻烦的,所以支持设定:

  1. 默认折叠/不折叠题目背景(可以设定长度超过若干字符时折叠);
  2. 默认折叠/不折叠 Ynoi 题目背景(优先级大于1.)。

流程:

脚本本质上是一个 DOM 操作引擎,它接收页面环境(题目内容、用户设置),经过一系列判断,最终在“题目背景”标题附近注入控制按钮和折叠容器。

源代码如下:

// ==UserScript==
// @name         洛谷 题目背景折叠
// @namespace    https://www.luogu.com.cn/
// @version      1.2
// @description  折叠洛谷题目背景
// @author       a_small_OIer
// @match        https://www.luogu.com.cn/problem/*
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

/*
 * MIT License
 *
 * Copyright (c) 2026 a_small_OIer
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

(function (){
    'use strict';
    const DEFAULTS = {
        defaultFolded: true,
        autoFoldLength: 0,
        forceFoldYnoi: true
    };
    function getSettings(){
        return {
            defaultFolded: GM_getValue('defaultFolded', DEFAULTS.defaultFolded),
            autoFoldLength: GM_getValue('autoFoldLength', DEFAULTS.autoFoldLength),
            forceFoldYnoi: GM_getValue('forceFoldYnoi', DEFAULTS.forceFoldYnoi)
        };
    }
    function saveSettings(s){
        GM_setValue('defaultFolded', s.defaultFolded);
        GM_setValue('autoFoldLength', s.autoFoldLength);
        GM_setValue('forceFoldYnoi', s.forceFoldYnoi);
    }

    // ========== 判断 Ynoi ==========
    function isYnoiProblem(){
        //额啊怎么写炸了那就直接匹配文本吧/ll
        if(document.body.innerText.toLowerCase().includes('ynoi'))
            return true;
        return false;
    }
    function getBackgroundLength(h){
        let len = 0;
        let next = h.nextElementSibling;
        while(next && !['H2', 'H3', 'H4'].includes(next.tagName)){
            len += (next.textContent || '').length;
            next = next.nextElementSibling;
        }
        return len;
    }
    // ========== 设置面板 ==========
    //额啊这 UI 设计怎么这么麻烦,算了给 DS 吧
    function ensureSettingsPanel(){
        if(document.getElementById('bg-fold-settings-panel'))
            return;
        const panel = document.createElement('div');
        panel.id = 'bg-fold-settings-panel';
        panel.innerHTML = `
            <style>
                #bg-fold-settings-panel {
                    position: absolute;
                    z-index: 9999;
                    background: white;
                    border: 1px solid #e0e0e0;
                    border-radius: 10px;
                    padding: 18px;
                    width: 280px;
                    box-shadow: 0 4px 20px rgba(0,0,0,0.15);
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                    display: none;
                    color: #333;
                }
                #bg-fold-settings-panel h4 {
                    margin: 0 0 15px 0;
                    font-size: 16px;
                    border-bottom: 1px solid #eee;
                    padding-bottom: 8px;
                }
                #bg-fold-settings-panel .setting-item {
                    margin-bottom: 14px;
                }
                #bg-fold-settings-panel label {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    font-size: 14px;
                    cursor: pointer;
                }
                #bg-fold-settings-panel input[type="number"] {
                    width: 60px;
                    padding: 3px 6px;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    text-align: center;
                }
                #bg-fold-settings-panel .btn-row {
                    display: flex;
                    justify-content: flex-end;
                    gap: 8px;
                    margin-top: 16px;
                }
                #bg-fold-settings-panel button {
                    padding: 5px 15px;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 14px;
                }
                #bg-fold-settings-panel .save-btn {
                    background: #3498db;
                    color: white;
                }
                #bg-fold-settings-panel .save-btn:hover {
                    background: #2980b9;
                }
                #bg-fold-settings-panel .close-btn {
                    background: #f0f0f0;
                    color: #333;
                }
                #bg-fold-settings-panel .close-btn:hover {
                    background: #e0e0e0;
                }
                .toggle-switch {
                    position: relative;
                    display: inline-block;
                    width: 38px;
                    height: 20px;
                }
                .toggle-switch input {
                    opacity: 0;
                    width: 0;
                    height: 0;
                }
                .slider {
                    position: absolute;
                    cursor: pointer;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background-color: #ccc;
                    transition: .3s;
                    border-radius: 20px;
                }
                .slider:before {
                    position: absolute;
                    content: "";
                    height: 14px;
                    width: 14px;
                    left: 3px;
                    bottom: 3px;
                    background-color: white;
                    transition: .3s;
                    border-radius: 50%;
                }
                input:checked + .slider {
                    background-color: #3498db;
                }
                input:checked + .slider:before {
                    transform: translateX(18px);
                }
            </style>
            <h4>背景折叠设置</h4>
            <div class="setting-item">
                <label>
                    <span>默认折叠背景</span>
                    <span class="toggle-switch">
                        <input type="checkbox" id="bg-default-folded">
                        <span class="slider"></span>
                    </span>
                </label>
            </div>
            <div class="setting-item">
                <label>
                    <span>背景长度超过(字)自动折叠</span>
                    <input type="number" id="bg-auto-length" min="0" placeholder="0 表关闭">
                </label>
            </div>
            <div class="setting-item">
                <label>
                    <span>Ynoi 题目强制折叠</span>
                    <span class="toggle-switch">
                        <input type="checkbox" id="bg-force-ynoi">
                        <span class="slider"></span>
                    </span>
                </label>
            </div>
            <div class="btn-row">
                <button class="close-btn" id="bg-close-panel">关闭</button>
                <button class="save-btn" id="bg-save-settings">保存</button>
            </div>
        `;
        document.body.appendChild(panel);
        const defaultFoldedChk = document.getElementById('bg-default-folded');
        const autoLengthInput = document.getElementById('bg-auto-length');
        const forceYnoiChk = document.getElementById('bg-force-ynoi');
        const saveBtn = document.getElementById('bg-save-settings');
        const closeBtn = document.getElementById('bg-close-panel');
        function loadUI(){
            const s = getSettings();
            defaultFoldedChk.checked = s.defaultFolded;
            autoLengthInput.value = s.autoFoldLength || 0;
            forceYnoiChk.checked = s.forceFoldYnoi;
        }
        loadUI();
        saveBtn.addEventListener('click', () => {
            const newSettings = {
                defaultFolded: defaultFoldedChk.checked,
                autoFoldLength: parseInt(autoLengthInput.value) || 0,
                forceFoldYnoi: forceYnoiChk.checked
            };
            saveSettings(newSettings);
            document.querySelectorAll('[data-folded]').forEach(h => {
                const wrapper = h.nextElementSibling;
                if(wrapper && wrapper.classList.contains('folded-bg-content')){
                    while (wrapper.firstChild) wrapper.before(wrapper.firstChild);
                    wrapper.remove();
                }
                const btn = h.querySelector('.bg-fold-btn');
                if (btn) btn.remove();
                const gear = h.querySelector('.bg-settings-btn');
                if (gear) gear.remove();
                delete h.dataset.folded;
            });
            scanAndFold();
            panel.style.display = 'none';
        });
        closeBtn.addEventListener('click', () => {
            panel.style.display = 'none';
        });
        document.addEventListener('click', (e) => {
            if (!panel.contains(e.target) && !e.target.classList.contains('bg-settings-btn')) {
                panel.style.display = 'none';
            }
        });
    }
    function foldHeading(h, settings){
        if(h.dataset.folded === 'true')
            return;
        const contents = [];
        let next = h.nextElementSibling;
        while(next && !['H2', 'H3', 'H4'].includes(next.tagName)){
            contents.push(next);
            next = next.nextElementSibling;
        }
        if(contents.length === 0)
            return;
        let shouldFold;
        if(settings.forceFoldYnoi && isYnoiProblem()){
            shouldFold = true;
        }else if(settings.autoFoldLength > 0){
            shouldFold = getBackgroundLength(h) > settings.autoFoldLength;
        }else{
            shouldFold = settings.defaultFolded;
        }
        const wrapper = document.createElement('div');
        wrapper.className = 'folded-bg-content';
        wrapper.style.display = shouldFold ? 'none' : '';
        contents.forEach(el => wrapper.appendChild(el));
        h.after(wrapper);
        //额啊怎么又有 UI
        const foldBtn = document.createElement('button');
        foldBtn.className = 'bg-fold-btn';
        foldBtn.innerHTML = shouldFold ? '▶ 展开背景' : '▼ 折叠背景';
        foldBtn.style.cssText = `
            margin-left: 8px;
            cursor: pointer;
            font-size: 14px;
            color: #3498db;
            background: none;
            border: 1px solid #3498db;
            border-radius: 4px;
            padding: 0 6px;
            transition: all 0.2s;
            line-height: 1.4;
        `;
        foldBtn.onmouseenter = () => { foldBtn.style.background = '#3498db'; foldBtn.style.color = 'white'; };
        foldBtn.onmouseleave = () => { foldBtn.style.background = 'none'; foldBtn.style.color = '#3498db'; };
        foldBtn.onclick = (e) => {
            e.stopPropagation();
            if(wrapper.style.display === 'none'){
                wrapper.style.display = '';
                foldBtn.innerHTML = '▼ 折叠背景';
            }else{
                wrapper.style.display = 'none';
                foldBtn.innerHTML = '▶ 展开背景';
            }
        };
        h.appendChild(foldBtn);
        ensureSettingsPanel();
        const settingsBtn = document.createElement('button');
        settingsBtn.className = 'bg-settings-btn';
        settingsBtn.innerHTML = '⚙';
        settingsBtn.title = '背景折叠设置';
        settingsBtn.style.cssText = `
            margin-left: 4px;
            cursor: pointer;
            font-size: 14px;
            color: #888;
            background: none;
            border: none;
            padding: 0 2px;
            transition: color 0.2s;
        `;
        settingsBtn.onmouseenter = () => { settingsBtn.style.color = '#333'; };
        settingsBtn.onmouseleave = () => { settingsBtn.style.color = '#888'; };
        settingsBtn.onclick = (e) => {
            e.stopPropagation();
            const panel = document.getElementById('bg-fold-settings-panel');
            if (!panel) return;
            const rect = settingsBtn.getBoundingClientRect();
            let left = rect.left;
            if (left + 280 > window.innerWidth) left = window.innerWidth - 290;
            panel.style.left = left + 'px';
            panel.style.top = (rect.bottom + 5) + 'px';
            panel.style.display = 'block';
        };
        h.appendChild(settingsBtn);
        h.dataset.folded = 'true';
    }
    function scanAndFold(){
        const settings = getSettings();
        const headings = document.querySelectorAll('h2, h3, h4');
        for(const h of headings)
            if(h.textContent.trim() === '题目背景')
                foldHeading(h, settings);
    }
    function init(){
        const observer = new MutationObserver(() => scanAndFold());
        observer.observe(document.body || document.documentElement, {
            childList: true,
            subtree: true
        });
        if(document.body){
            scanAndFold();
        }else{
            window.addEventListener('DOMContentLoaded', scanAndFold);
        }
    }
    init();
})();