折叠题目背景
a_small_OIer · · 科技·工程
启发是来自 这个工单,闲来无事,遂写脚本完成折叠题目背景的功能。
(鬼图警告:如果你没见过 P3934,那我建议你还是不要打开这个工单,至少我认为挺瘆人的/ll)
感觉是不是真没这么重要,实在受不了自己写个脚本屏蔽吧。
其实我认为这个功能还是蛮有必要的,毕竟 Ynoi 的题目背景还是挺长且麻烦的,所以支持设定:
- 默认折叠/不折叠题目背景(可以设定长度超过若干字符时折叠);
- 默认折叠/不折叠 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();
})();