脑子不想摸鱼,手却已经摸上了……
这个人平时很不自觉,喜欢水谷但不喜欢上什么其它网站。
于是她参考了这篇文章,花了一个晚上将近两周调了十几版,最终用 AI 做出了这个插件。
直接粘贴到篡改猴里面保存就能用了。
功能介绍:
- 开启计时器,将访问一切洛谷网站请求阻绝;
- 可以在设置图标自定义进度条颜色、工作及休息时长,可自由拖动番茄钟;
- 加入题目计划的题目将显示链接在设置中,支持删除;
- 当在专注时点击其他洛谷网页(除题目、提交记录及剪切板外)自动跳转到计划中第一个题目;
- 支持用本地存储倒计时进度,若当前有进度将自动继续走时(该功能容易出问题)。
- 支持勾选是否允许题解查看,否时默认为自动跳转。
那就上代码。
:::success[
// ==UserScript==
// @name 洛谷强力学术pro
// @namespace http://tampermonkey.net/
// @version 10.12
// @author wendywan
// @icon https://cdn.luogu.com.cn/upload/image_hosting/psd5a03h.png
// @match *://*/*
// @grant none
// @license MIT
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY_PID = "luogu_study_plan_pids";
const STORAGE_KEY_SOLUTION = "luogu_study_plan_solution";
const STORAGE_KEY_FOCUS_ACTIVE = "luogu_focus_active";
const STORAGE_KEY_FOCUS_END = "luogu_focus_end_time";
const STORAGE_KEY_FOCUS_DURATION = "luogu_focus_duration";
const STORAGE_KEY_FOCUS_COLOR = "luogu_focus_color";
const STORAGE_KEY_FOCUS_POS = "luogu_focus_position";
const STORAGE_KEY_FOCUS_REMAINING = "luogu_focus_remaining";
const STORAGE_KEY_FOCUS_STATE = "luogu_focus_state";
const STORAGE_KEY_REST_DURATION = "luogu_rest_duration";
const STORAGE_KEY_CURRENT_MODE = "luogu_focus_current_mode";
const DEFAULT_WORK_DURATION = 25;
const DEFAULT_REST_DURATION = 5;
const DEFAULT_COLOR = "#ff6b6b";
const DEFAULT_POSITION = { x: 20, y: 100 };
const DEFAULT_SIZE = { width: 120, height: 100 };
const MAX_PIDS = 6;
function getSavePids() {
try {
const pids = JSON.parse(localStorage.getItem(STORAGE_KEY_PID)) || [];
return pids.filter(pid => pid.trim() !== '');
} catch {
return [];
}
}
function savePids(pids) {
localStorage.setItem(STORAGE_KEY_PID, JSON.stringify(pids));
}
function removePid(pid) {
const pids = getSavePids();
const index = pids.indexOf(pid);
if (index !== -1) {
pids.splice(index, 1);
savePids(pids);
return true;
}
return false;
}
function getSolutionStatus() {
return localStorage.getItem(STORAGE_KEY_SOLUTION) !== 'false';
}
function saveSolutionStatus(status) {
localStorage.setItem(STORAGE_KEY_SOLUTION, status);
}
function isFocusActive() {
const active = localStorage.getItem(STORAGE_KEY_FOCUS_ACTIVE) === 'true';
if (!active) return false;
// 修复:暂停状态下不检查结束时间,避免刷新时重置
const state = localStorage.getItem(STORAGE_KEY_FOCUS_STATE);
if (state === 'paused') {
return true;
}
const endTime = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_END) || '0');
const now = Date.now();
if (active && now >= endTime) {
endFocusMode();
return false;
}
return active;
}
function startFocusMode(durationMinutes, isWorkMode) {
const endTime = Date.now() + durationMinutes * 60 * 1000;
localStorage.setItem(STORAGE_KEY_FOCUS_ACTIVE, 'true');
localStorage.setItem(STORAGE_KEY_FOCUS_END, endTime.toString());
if (isWorkMode) {
localStorage.setItem(STORAGE_KEY_FOCUS_DURATION, durationMinutes.toString());
}
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'running');
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
}
function pauseFocusMode(remainingSeconds) {
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'paused');
localStorage.setItem(STORAGE_KEY_FOCUS_REMAINING, remainingSeconds.toString());
localStorage.removeItem(STORAGE_KEY_FOCUS_END);
}
function endFocusMode() {
localStorage.removeItem(STORAGE_KEY_FOCUS_ACTIVE);
localStorage.removeItem(STORAGE_KEY_FOCUS_END);
localStorage.removeItem(STORAGE_KEY_FOCUS_STATE);
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
localStorage.removeItem(STORAGE_KEY_CURRENT_MODE);
}
function getWorkDuration() {
return parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_DURATION) || DEFAULT_WORK_DURATION);
}
function getRestDuration() {
return parseInt(localStorage.getItem(STORAGE_KEY_REST_DURATION) || DEFAULT_REST_DURATION);
}
function saveDurations(work, rest) {
localStorage.setItem(STORAGE_KEY_FOCUS_DURATION, work.toString());
localStorage.setItem(STORAGE_KEY_REST_DURATION, rest.toString());
}
function getFocusColor() {
return localStorage.getItem(STORAGE_KEY_FOCUS_COLOR) || DEFAULT_COLOR;
}
function saveFocusColor(color) {
localStorage.setItem(STORAGE_KEY_FOCUS_COLOR, color);
}
function getFocusPosition() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY_FOCUS_POS) || JSON.stringify(DEFAULT_POSITION));
} catch {
return DEFAULT_POSITION;
}
}
function saveFocusPosition(pos) {
localStorage.setItem(STORAGE_KEY_FOCUS_POS, JSON.stringify(pos));
}
function getRedirectUrl() {
const pids = getSavePids();
return pids.length > 0
? `https://www.luogu.com.cn/problem/${pids[0]}`
: 'https://www.luogu.com.cn/problem/list';
}
function shouldAllow() {
const currentURL = window.location.href;
const isLuoguDomain = /^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(currentURL);
if (!isLuoguDomain) return true;
if (!isFocusActive()) return true;
// 修复:休息模式下允许访问所有页面
const currentMode = localStorage.getItem(STORAGE_KEY_CURRENT_MODE);
if (currentMode === 'rest') {
return true;
}
const path = window.location.pathname.toLowerCase();
return (
path.startsWith('/problem/') ||
path.startsWith('/paste') ||
path.startsWith('/paste/') ||
path.startsWith('/record/')
);
}
function blockSolutionAccess() {
if (!/\/problem\/solution\//i.test(window.location.pathname)) {
return false;
}
if (!getSolutionStatus()) {
window.location.replace(getRedirectUrl());
return true;
}
return false;
}
function monitorSolutionElements() {
if (!getSolutionStatus()) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
const selectors = [
'a[href*="/problem/solution/"]',
'[class*="solution"]',
'[class*="answer"]'
];
try {
node.querySelectorAll(selectors.join(',')).forEach(el => {
if ((el.textContent && el.textContent.includes('题解')) ||
(el.href && el.href.includes('/problem/solution/'))) {
el.style.display = 'none !important';
el.style.visibility = 'hidden !important';
el.style.height = '0 !important';
el.style.overflow = 'hidden !important';
}
});
} catch (e) {}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (blockSolutionAccess()) return;
if (isFocusActive() && !shouldAllow()) {
window.location.replace(getRedirectUrl());
return;
}
function createPomodoroWidget() {
if (!/^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(window.location.href)) return;
const workDuration = getWorkDuration();
const restDuration = getRestDuration();
const config = {
workDuration: workDuration,
restDuration: restDuration,
color: getFocusColor(),
position: getFocusPosition()
};
const style = document.createElement('style');
style.textContent = `
#luogu-pomodoro-widget {
position: fixed;
z-index: 99999;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
user-select: none;
width: ${DEFAULT_SIZE.width}px;
height: ${DEFAULT_SIZE.height}px;
background: white;
border: 1px solid #e0e0e0;
cursor: move;
}
#luogu-pomodoro-widget:hover {
box-shadow: 0 3px 15px rgba(0,0,0,0.2);
}
#luogu-pomodoro-time {
font-size: 20px;
font-weight: bold;
text-align: center;
color: #333;
padding: 12px 0;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
#luogu-pomodoro-progress {
height: 4px;
background: #f0f0f0;
border-radius: 2px;
margin: 4px 8px;
overflow: hidden;
}
#luogu-pomodoro-progress-bar {
height: 100%;
width: 0%;
background: ${config.color};
border-radius: 2px;
transition: width 0.5s ease;
}
#luogu-pomodoro-controls {
display: flex;
justify-content: center;
gap: 6px;
padding: 6px 0;
background: #f8f9fa;
border-top: 1px solid #eee;
}
.pomodoro-btn {
width: 48%;
height: 22px;
border: none;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
user-select: none;
text-align: center;
line-height: 22px;
color: #333;
}
.pomodoro-btn:active {
transform: scale(0.95);
}
.pomodoro-btn.start {
background: #e8f5e9;
color: #2e7d32;
}
.pomodoro-btn.pause {
background: #fff8e1;
color: #ff8f00;
}
.pomodoro-btn.stop {
background: #ffebee;
color: #c62828;
}
.pomodoro-btn.start:disabled,
.pomodoro-btn.pause:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
#luogu-pomodoro-settings {
position: absolute;
top: 6px;
right: 6px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #000;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
z-index: 10;
}
#luogu-pomodoro-settings:hover {
background: #333;
transform: scale(1.1);
}
`;
document.head.appendChild(style);
const widget = document.createElement('div');
widget.id = 'luogu-pomodoro-widget';
widget.style.left = `${config.position.x}px`;
widget.style.top = `${config.position.y}px`;
widget.innerHTML = `
<div id="luogu-pomodoro-time">25:00</div>
<div id="luogu-pomodoro-progress">
<div id="luogu-pomodoro-progress-bar"></div>
</div>
<div id="luogu-pomodoro-controls">
<button class="pomodoro-btn start">开始</button>
<button class="pomodoro-btn stop">结束</button>
</div>
<div id="luogu-pomodoro-settings">⚙</div>
`;
document.body.appendChild(widget);
const settingsBtn = widget.querySelector('#luogu-pomodoro-settings');
settingsBtn.onclick = (e) => {
e.stopPropagation();
createSettingUI();
};
let timer = null;
let remainingTime = workDuration * 60;
let currentMode = 'work';
let isResuming = false;
let autoStart = false;
const savedMode = localStorage.getItem(STORAGE_KEY_CURRENT_MODE);
if (savedMode === 'rest') {
currentMode = 'rest';
remainingTime = restDuration * 60;
}
const timeDisplay = widget.querySelector('#luogu-pomodoro-time');
const progressBar = widget.querySelector('#luogu-pomodoro-progress-bar');
const startBtn = widget.querySelector('.pomodoro-btn.start');
const stopBtn = widget.querySelector('.pomodoro-btn.stop');
// 修复:统一状态恢复逻辑,支持休息模式
const savedState = localStorage.getItem(STORAGE_KEY_FOCUS_STATE);
const savedRemaining = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_REMAINING) || '0');
const endTime = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_END) || '0');
const now = Date.now();
if (savedState === 'paused' && savedRemaining > 0) {
// 恢复暂停状态(工作或休息)
remainingTime = savedRemaining;
isResuming = true;
autoStart = false;
} else if (savedState === 'running' && endTime > now) {
// 恢复运行状态(工作或休息)
remainingTime = Math.floor((endTime - now) / 1000);
if (remainingTime > 0) {
isResuming = true;
autoStart = true;
}
}
// 修复:根据保存状态初始化按钮状态(关键修复)
if (savedState === 'paused' && savedRemaining > 0) {
startBtn.textContent = '继续';
startBtn.classList.add('pause');
startBtn.classList.remove('start');
startBtn.disabled = false;
stopBtn.disabled = false;
} else if (savedState === 'running' && endTime > now && remainingTime > 0) {
startBtn.textContent = '暂停';
startBtn.classList.remove('pause');
startBtn.classList.add('start');
startBtn.disabled = false;
stopBtn.disabled = false;
} else {
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = (currentMode === 'work' && remainingTime === workDuration * 60);
}
function updateDisplay() {
const mins = Math.floor(remainingTime / 60);
const secs = remainingTime % 60;
timeDisplay.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
const totalSeconds = (currentMode === 'work' ? workDuration : restDuration) * 60;
const progress = ((totalSeconds - remainingTime) / totalSeconds) * 100;
progressBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
}
function toggleTimer() {
if (timer) {
clearInterval(timer);
timer = null;
pauseFocusMode(remainingTime);
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
isResuming = true;
startBtn.textContent = '继续';
startBtn.classList.add('pause');
startBtn.classList.remove('start');
startBtn.disabled = false;
stopBtn.disabled = false;
} else {
if (isResuming) {
const endTime = Date.now() + remainingTime * 1000;
localStorage.setItem(STORAGE_KEY_FOCUS_END, endTime.toString());
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'running');
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
isResuming = false;
} else {
remainingTime = (currentMode === 'work' ? workDuration : restDuration) * 60;
// 修复:统一设置专注模式状态,支持休息模式
startFocusMode(
currentMode === 'work' ? workDuration : restDuration,
currentMode === 'work'
);
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
}
startBtn.textContent = '暂停';
startBtn.classList.remove('pause');
startBtn.classList.add('start');
startBtn.disabled = false;
stopBtn.disabled = false;
timer = setInterval(() => {
remainingTime--;
updateDisplay();
if (remainingTime <= 0) {
clearInterval(timer);
timer = null;
if (currentMode === 'work') {
// 修复:工作结束进入休息模式,清除专注状态(不保存暂停状态)
currentMode = 'rest';
remainingTime = restDuration * 60;
endFocusMode(); // 清除工作计时状态
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, 'rest');
updateDisplay();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = false;
} else {
// 休息结束,完全结束专注模式
endFocusMode();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = true;
// 重置为工作模式
currentMode = 'work';
remainingTime = workDuration * 60;
updateDisplay();
}
}
}, 1000);
}
}
function stopTimer() {
clearInterval(timer);
timer = null;
remainingTime = workDuration * 60;
currentMode = 'work';
isResuming = false;
updateDisplay();
endFocusMode();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = true;
}
startBtn.onclick = toggleTimer;
stopBtn.onclick = stopTimer;
let isDragging = false;
let offsetX, offsetY;
widget.onmousedown = (e) => {
if (e.target.id === 'luogu-pomodoro-settings') return;
isDragging = true;
const rect = widget.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
widget.style.cursor = 'move';
e.preventDefault();
};
document.onmousemove = (e) => {
if (!isDragging) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const maxX = window.innerWidth - widget.offsetWidth;
const maxY = window.innerHeight - widget.offsetHeight;
widget.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
widget.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
};
document.onmouseup = () => {
if (isDragging) {
isDragging = false;
widget.style.cursor = 'default';
const rect = widget.getBoundingClientRect();
saveFocusPosition({
x: Math.round(rect.left),
y: Math.round(rect.top)
});
}
};
updateDisplay();
if (autoStart) {
setTimeout(() => {
toggleTimer();
}, 300);
}
}
function createSettingUI() {
if (!/^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(window.location.href)) return;
const existingMask = document.getElementById('luogu-modal-mask');
const existingBox = document.getElementById('luogu-modal-box');
if (existingMask) existingMask.remove();
if (existingBox) existingBox.remove();
const style = document.createElement('style');
style.textContent = `
#luogu-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 99997;
display: none;
}
#luogu-modal-box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 340px;
padding: 20px;
background: #fff;
border-radius: 10px;
z-index: 99998;
display: none;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
}
#luogu-modal-box h3 {
margin: 0 0 15px 0;
color: #222;
text-align: center;
font-size: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.luogu-form-group {
margin-bottom: 15px;
}
.luogu-form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
#luogu-pid-input {
width: 100%;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
#luogu-pid-input:focus {
border-color: #0099ff;
outline: none;
}
#luogu-solution-switch {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
}
#luogu-solution-switch input {
width: 18px;
height: 18px;
cursor: pointer;
}
.duration-settings {
display: flex;
gap: 10px;
}
.duration-col {
flex: 1;
}
.duration-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
margin-top: 4px;
}
.pid-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
min-height: 30px;
}
.pid-item {
background: #f0f0f0;
padding: 5px 10px;
border-radius: 12px;
font-size: 13px;
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
transition: all 0.2s;
}
.pid-item:hover {
background: #e0e0e0;
transform: scale(1.02);
}
.pid-item .remove {
color: #ff4444;
cursor: pointer;
font-size: 14px;
margin-left: 4px;
}
#luogu-color-input {
width: 100%;
height: 30px;
border: none;
border-radius: 4px;
padding: 0;
margin-top: 5px;
cursor: pointer;
}
#luogu-modal-btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #f5f5f5;
}
#luogu-save-btn, #luogu-cancel-btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
#luogu-save-btn {
background: #0099ff;
color: white;
}
#luogu-save-btn:hover {
background: #0077cc;
}
#luogu-cancel-btn {
background: #f0f0f0;
color: #666;
}
#luogu-cancel-btn:hover {
background: #e0e0e0;
}
.luogu-hint {
font-size: 12px;
color: #888;
margin-top: 5px;
line-height: 1.4;
}
`;
document.head.appendChild(style);
const mask = document.createElement('div');
mask.id = 'luogu-modal-mask';
mask.onclick = () => {
mask.style.display = 'none';
modalBox.style.display = 'none';
};
document.body.appendChild(mask);
const modalBox = document.createElement('div');
modalBox.id = 'luogu-modal-box';
modalBox.innerHTML = `
<h3>专注设置</h3>
<div class="luogu-form-group">
<label>题目计划</label>
<input type="text" id="luogu-pid-input" placeholder="输入题号(如:P1000)">
<div class="luogu-hint">最多添加6道题目,点击题号跳转</div>
<div class="pid-list" id="pid-list"></div>
</div>
<div class="luogu-form-group">
<label>题解访问</label>
<div id="luogu-solution-switch">
<input type="checkbox" id="luogu-solution-checkbox">
<span>允许查看题解(独立控制,不受计时影响)</span>
</div>
</div>
<div class="luogu-form-group">
<label>番茄钟时长(分钟)</label>
<div class="duration-settings">
<div class="duration-col">
<div>工作</div>
<input type="number" id="luogu-work-input" class="duration-input" min="1" max="120" value="${getWorkDuration()}">
</div>
<div class="duration-col">
<div>休息</div>
<input type="number" id="luogu-rest-input" class="duration-input" min="1" max="60" value="${getRestDuration()}">
</div>
</div>
</div>
<div class="luogu-form-group">
<label>番茄钟颜色</label>
<input type="color" id="luogu-color-input" value="${getFocusColor()}">
</div>
<div id="luogu-modal-btn-group">
<button id="luogu-save-btn">保存</button>
<button id="luogu-cancel-btn">取消</button>
</div>
`;
document.body.appendChild(modalBox);
function renderPidList() {
const pidList = document.getElementById('pid-list');
pidList.innerHTML = '';
const pids = getSavePids();
if (pids.length === 0) {
pidList.innerHTML = '<div style="text-align:center;color:#999;padding:5px 0">暂无题目</div>';
return;
}
pids.forEach(pid => {
const item = document.createElement('div');
item.className = 'pid-item';
item.innerHTML = `${pid}<span class="remove">×</span>`;
item.addEventListener('click', (e) => {
if (e.target.classList.contains('remove')) return;
window.location.href = `https://www.luogu.com.cn/problem/${pid}`;
});
const removeBtn = item.querySelector('.remove');
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (removePid(pid)) {
renderPidList();
}
});
pidList.appendChild(item);
});
}
document.getElementById('luogu-save-btn').onclick = () => {
const pidInput = document.getElementById('luogu-pid-input');
const pid = pidInput.value.trim().toUpperCase();
const solutionChecked = document.getElementById('luogu-solution-checkbox').checked;
const workDuration = parseInt(document.getElementById('luogu-work-input').value) || DEFAULT_WORK_DURATION;
const restDuration = parseInt(document.getElementById('luogu-rest-input').value) || DEFAULT_REST_DURATION;
const color = document.getElementById('luogu-color-input').value;
if (workDuration < 1 || workDuration > 120) {
alert('工作时长必须在 1-120 分钟之间');
return;
}
if (restDuration < 1 || restDuration > 60) {
alert('休息时长必须在 1-60 分钟之间');
return;
}
if (pid && !/^(P|SP)\d{4,6}$/i.test(pid)) {
alert('题号格式错误!\n正确格式:P1000 或 SP123');
return;
}
const pids = getSavePids();
if (pid && pids.length < MAX_PIDS && !pids.includes(pid)) {
pids.push(pid);
savePids(pids);
}
saveSolutionStatus(solutionChecked);
saveDurations(workDuration, restDuration);
saveFocusColor(color);
alert('设置已保存!');
mask.style.display = 'none';
modalBox.style.display = 'none';
renderPidList();
monitorSolutionElements();
};
document.getElementById('luogu-cancel-btn').onclick = () => {
mask.style.display = 'none';
modalBox.style.display = 'none';
};
document.getElementById('luogu-pid-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('luogu-save-btn').click();
}
});
mask.style.display = 'block';
modalBox.style.display = 'block';
document.getElementById('luogu-pid-input').value = '';
document.getElementById('luogu-solution-checkbox').checked = getSolutionStatus();
renderPidList();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
createPomodoroWidget();
});
} else {
createPomodoroWidget();
}
window.addEventListener('load', () => {
monitorSolutionElements();
});
})();
:::
\textup{Upd on 2026.3.7}
注意到我们的添加题库出了一点问题,可能是因为在原文章基础上进行开发,没有把入门与面试、CF、AT 和 UVA 题库的题目添加到计划的功能。
在完善这个功能的同时,我正添加了在学习过程中按暂停可以暂时访问其他网页的功能。
(否则暂停键没有起到任何作用吧。)
包括考虑将在未来添加一个可以选择是否拦截所有除题目外的网址的模式,但是调了好久没整出来。
:::success[
// ==UserScript==
// @name 洛谷强力学术 pro
// @namespace http://tampermonkey.net/
// @version 10.57
// @author wendywan
// @icon https://cdn.luogu.com.cn/upload/image_hosting/psd5a03h.png
// @match *://*/*
// @grant none
// @license MIT
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY_PID = "luogu_study_plan_pids";
const STORAGE_KEY_SOLUTION = "luogu_study_plan_solution";
const STORAGE_KEY_FOCUS_ACTIVE = "luogu_focus_active";
const STORAGE_KEY_FOCUS_END = "luogu_focus_end_time";
const STORAGE_KEY_FOCUS_DURATION = "luogu_focus_duration";
const STORAGE_KEY_FOCUS_COLOR = "luogu_focus_color";
const STORAGE_KEY_FOCUS_POS = "luogu_focus_position";
const STORAGE_KEY_FOCUS_REMAINING = "luogu_focus_remaining";
const STORAGE_KEY_FOCUS_STATE = "luogu_focus_state";
const STORAGE_KEY_REST_DURATION = "luogu_rest_duration";
const STORAGE_KEY_CURRENT_MODE = "luogu_focus_current_mode";
const DEFAULT_WORK_DURATION = 25;
const DEFAULT_REST_DURATION = 5;
const DEFAULT_COLOR = "#ff6b6b";
const DEFAULT_POSITION = { x: 20, y: 100 };
const DEFAULT_SIZE = { width: 120, height: 100 };
const MAX_PIDS = 6;
function getSavePids() {
try {
const pids = JSON.parse(localStorage.getItem(STORAGE_KEY_PID)) || [];
return pids.filter(pid => pid.trim() !== '');
} catch {
return [];
}
}
function savePids(pids) {
localStorage.setItem(STORAGE_KEY_PID, JSON.stringify(pids));
}
function removePid(pid) {
const pids = getSavePids();
const index = pids.indexOf(pid);
if (index !== -1) {
pids.splice(index, 1);
savePids(pids);
return true;
}
return false;
}
function getSolutionStatus() {
return localStorage.getItem(STORAGE_KEY_SOLUTION) !== 'false';
}
function saveSolutionStatus(status) {
localStorage.setItem(STORAGE_KEY_SOLUTION, status);
}
function isFocusActive() {
const active = localStorage.getItem(STORAGE_KEY_FOCUS_ACTIVE) === 'true';
if (!active) return false;
const state = localStorage.getItem(STORAGE_KEY_FOCUS_STATE);
if (state === 'paused') {
return true;
}
const endTime = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_END) || '0');
const now = Date.now();
if (active && now >= endTime) {
endFocusMode();
return false;
}
return active;
}
function startFocusMode(durationMinutes, isWorkMode) {
const endTime = Date.now() + durationMinutes * 60 * 1000;
localStorage.setItem(STORAGE_KEY_FOCUS_ACTIVE, 'true');
localStorage.setItem(STORAGE_KEY_FOCUS_END, endTime.toString());
if (isWorkMode) {
localStorage.setItem(STORAGE_KEY_FOCUS_DURATION, durationMinutes.toString());
}
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'running');
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
}
function pauseFocusMode(remainingSeconds) {
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'paused');
localStorage.setItem(STORAGE_KEY_FOCUS_REMAINING, remainingSeconds.toString());
localStorage.removeItem(STORAGE_KEY_FOCUS_END);
}
function endFocusMode() {
localStorage.removeItem(STORAGE_KEY_FOCUS_ACTIVE);
localStorage.removeItem(STORAGE_KEY_FOCUS_END);
localStorage.removeItem(STORAGE_KEY_FOCUS_STATE);
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
localStorage.removeItem(STORAGE_KEY_CURRENT_MODE);
}
function getWorkDuration() {
return parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_DURATION) || DEFAULT_WORK_DURATION);
}
function getRestDuration() {
return parseInt(localStorage.getItem(STORAGE_KEY_REST_DURATION) || DEFAULT_REST_DURATION);
}
function saveDurations(work, rest) {
localStorage.setItem(STORAGE_KEY_FOCUS_DURATION, work.toString());
localStorage.setItem(STORAGE_KEY_REST_DURATION, rest.toString());
}
function getFocusColor() {
return localStorage.getItem(STORAGE_KEY_FOCUS_COLOR) || DEFAULT_COLOR;
}
function saveFocusColor(color) {
localStorage.setItem(STORAGE_KEY_FOCUS_COLOR, color);
}
function getFocusPosition() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY_FOCUS_POS) || JSON.stringify(DEFAULT_POSITION));
} catch {
return DEFAULT_POSITION;
}
}
function saveFocusPosition(pos) {
localStorage.setItem(STORAGE_KEY_FOCUS_POS, JSON.stringify(pos));
}
function getRedirectUrl() {
const pids = getSavePids();
return pids.length > 0
? `https://www.luogu.com.cn/problem/${pids[0]}`
: 'https://www.luogu.com.cn/problem/list';
}
function shouldAllow() {
const currentURL = window.location.href;
const isLuoguDomain = /^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(currentURL);
if (!isLuoguDomain) return true;
if (!isFocusActive()) return true;
const state = localStorage.getItem(STORAGE_KEY_FOCUS_STATE);
if (state === 'paused') {
return true;
}
const currentMode = localStorage.getItem(STORAGE_KEY_CURRENT_MODE);
if (currentMode === 'rest') {
return true;
}
const path = window.location.pathname.toLowerCase();
return (
path.startsWith('/problem/') ||
path.startsWith('/paste') ||
path.startsWith('/paste/') ||
path.startsWith('/record/')
);
}
function blockSolutionAccess() {
if (!/\/problem\/solution\//i.test(window.location.pathname)) {
return false;
}
if (!getSolutionStatus()) {
window.location.replace(getRedirectUrl());
return true;
}
return false;
}
function monitorSolutionElements() {
if (!getSolutionStatus()) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
const selectors = [
'a[href*="/problem/solution/"]',
'[class*="solution"]',
'[class*="answer"]'
];
try {
node.querySelectorAll(selectors.join(',')).forEach(el => {
if ((el.textContent && el.textContent.includes('题解')) ||
(el.href && el.href.includes('/problem/solution/'))) {
el.style.display = 'none !important';
el.style.visibility = 'hidden !important';
el.style.height = '0 !important';
el.style.overflow = 'hidden !important';
}
});
} catch (e) {}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (blockSolutionAccess()) return;
if (isFocusActive() && !shouldAllow()) {
window.location.replace(getRedirectUrl());
return;
}
function createPomodoroWidget() {
if (!/^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(window.location.href)) return;
const workDuration = getWorkDuration();
const restDuration = getRestDuration();
const config = {
workDuration: workDuration,
restDuration: restDuration,
color: getFocusColor(),
position: getFocusPosition()
};
const style = document.createElement('style');
style.textContent = `
#luogu-pomodoro-widget {
position: fixed;
z-index: 99999;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
user-select: none;
width: ${DEFAULT_SIZE.width}px;
height: ${DEFAULT_SIZE.height}px;
background: white;
border: 1px solid #e0e0e0;
cursor: move;
transition: border-color 0.3s ease;
}
#luogu-pomodoro-widget:hover {
box-shadow: 0 3px 15px rgba(0,0,0,0.2);
}
#luogu-pomodoro-time {
font-size: 20px;
font-weight: bold;
text-align: center;
color: #333;
padding: 12px 0;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
#luogu-pomodoro-progress {
height: 4px;
background: #f0f0f0;
border-radius: 2px;
margin: 4px 8px;
overflow: hidden;
}
#luogu-pomodoro-progress-bar {
height: 100%;
width: 0%;
background: ${config.color};
border-radius: 2px;
transition: width 0.5s ease;
}
#luogu-pomodoro-controls {
display: flex;
justify-content: center;
gap: 6px;
padding: 6px 0;
background: #f8f9fa;
border-top: 1px solid #eee;
}
.pomodoro-btn {
width: 48%;
height: 22px;
border: none;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
user-select: none;
text-align: center;
line-height: 22px;
color: #333;
}
.pomodoro-btn:active {
transform: scale(0.95);
}
.pomodoro-btn.start {
background: #e8f5e9;
color: #2e7d32;
}
.pomodoro-btn.pause {
background: #fff8e1;
color: #ff8f00;
}
.pomodoro-btn.stop {
background: #ffebee;
color: #c62828;
}
.pomodoro-btn.start:disabled,
.pomodoro-btn.pause:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
#luogu-pomodoro-settings {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 6px;
background: ${config.color};
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
z-index: 10;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
#luogu-pomodoro-settings:hover {
transform: scale(1.1);
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
#luogu-pomodoro-settings svg {
width: 14px;
height: 14px;
fill: white;
}
#luogu-pomodoro-widget.focus-running {
border: 2px solid ${config.color} !important;
}
`;
document.head.appendChild(style);
const widget = document.createElement('div');
widget.id = 'luogu-pomodoro-widget';
widget.style.left = `${config.position.x}px`;
widget.style.top = `${config.position.y}px`;
widget.innerHTML = `
<div id="luogu-pomodoro-time">25:00</div>
<div id="luogu-pomodoro-progress">
<div id="luogu-pomodoro-progress-bar"></div>
</div>
<div id="luogu-pomodoro-controls">
<button class="pomodoro-btn start">开始</button>
<button class="pomodoro-btn stop">结束</button>
</div>
<div id="luogu-pomodoro-settings">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
</svg>
</div>
`;
document.body.appendChild(widget);
const settingsBtn = widget.querySelector('#luogu-pomodoro-settings');
settingsBtn.onclick = (e) => {
e.stopPropagation();
createSettingUI();
};
let timer = null;
let remainingTime = workDuration * 60;
let currentMode = 'work';
let isResuming = false;
let autoStart = false;
const savedMode = localStorage.getItem(STORAGE_KEY_CURRENT_MODE);
if (savedMode === 'rest') {
currentMode = 'rest';
remainingTime = restDuration * 60;
}
const timeDisplay = widget.querySelector('#luogu-pomodoro-time');
const progressBar = widget.querySelector('#luogu-pomodoro-progress-bar');
const startBtn = widget.querySelector('.pomodoro-btn.start');
const stopBtn = widget.querySelector('.pomodoro-btn.stop');
const savedState = localStorage.getItem(STORAGE_KEY_FOCUS_STATE);
const savedRemaining = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_REMAINING) || '0');
const endTime = parseInt(localStorage.getItem(STORAGE_KEY_FOCUS_END) || '0');
const now = Date.now();
if (savedState === 'paused' && savedRemaining > 0) {
remainingTime = savedRemaining;
isResuming = true;
autoStart = false;
} else if (savedState === 'running' && endTime > now) {
remainingTime = Math.floor((endTime - now) / 1000);
if (remainingTime > 0) {
isResuming = true;
autoStart = true;
}
}
if (savedState === 'paused' && savedRemaining > 0) {
startBtn.textContent = '继续';
startBtn.classList.add('pause');
startBtn.classList.remove('start');
startBtn.disabled = false;
stopBtn.disabled = false;
} else if (savedState === 'running' && endTime > now && remainingTime > 0) {
startBtn.textContent = '暂停';
startBtn.classList.remove('pause');
startBtn.classList.add('start');
startBtn.disabled = false;
stopBtn.disabled = false;
} else {
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = (currentMode === 'work' && remainingTime === workDuration * 60);
}
function updateFocusBorderStyle() {
const isRunning = timer !== null;
const isWorkMode = currentMode === 'work';
const isPaused = localStorage.getItem(STORAGE_KEY_FOCUS_STATE) === 'paused';
const isRest = currentMode === 'rest';
if (isRunning && isWorkMode && !isPaused && !isRest) {
widget.classList.add('focus-running');
} else {
widget.classList.remove('focus-running');
}
}
function updateDisplay() {
const mins = Math.floor(remainingTime / 60);
const secs = remainingTime % 60;
timeDisplay.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
const totalSeconds = (currentMode === 'work' ? workDuration : restDuration) * 60;
const progress = ((totalSeconds - remainingTime) / totalSeconds) * 100;
progressBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
updateFocusBorderStyle();
}
function toggleTimer() {
if (timer) {
clearInterval(timer);
timer = null;
pauseFocusMode(remainingTime);
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
isResuming = true;
startBtn.textContent = '继续';
startBtn.classList.add('pause');
startBtn.classList.remove('start');
startBtn.disabled = false;
stopBtn.disabled = false;
updateFocusBorderStyle();
} else {
if (isResuming) {
const endTime = Date.now() + remainingTime * 1000;
localStorage.setItem(STORAGE_KEY_FOCUS_END, endTime.toString());
localStorage.setItem(STORAGE_KEY_FOCUS_STATE, 'running');
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
localStorage.removeItem(STORAGE_KEY_FOCUS_REMAINING);
isResuming = false;
} else {
remainingTime = (currentMode === 'work' ? workDuration : restDuration) * 60;
startFocusMode(
currentMode === 'work' ? workDuration : restDuration,
currentMode === 'work'
);
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, currentMode);
}
startBtn.textContent = '暂停';
startBtn.classList.remove('pause');
startBtn.classList.add('start');
startBtn.disabled = false;
stopBtn.disabled = false;
updateFocusBorderStyle();
timer = setInterval(() => {
remainingTime--;
updateDisplay();
if (remainingTime <= 0) {
clearInterval(timer);
timer = null;
if (currentMode === 'work') {
currentMode = 'rest';
remainingTime = restDuration * 60;
endFocusMode();
localStorage.setItem(STORAGE_KEY_CURRENT_MODE, 'rest');
updateDisplay();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = false;
} else {
endFocusMode();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = true;
currentMode = 'work';
remainingTime = workDuration * 60;
updateDisplay();
}
}
}, 1000);
}
}
function stopTimer() {
clearInterval(timer);
timer = null;
remainingTime = workDuration * 60;
currentMode = 'work';
isResuming = false;
updateDisplay();
endFocusMode();
startBtn.textContent = '开始';
startBtn.classList.add('start');
startBtn.classList.remove('pause');
startBtn.disabled = false;
stopBtn.disabled = true;
}
startBtn.onclick = toggleTimer;
stopBtn.onclick = stopTimer;
let isDragging = false;
let offsetX, offsetY;
widget.onmousedown = (e) => {
if (e.target.id === 'luogu-pomodoro-settings') return;
isDragging = true;
const rect = widget.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
widget.style.cursor = 'move';
e.preventDefault();
};
document.onmousemove = (e) => {
if (!isDragging) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const maxX = window.innerWidth - widget.offsetWidth;
const maxY = window.innerHeight - widget.offsetHeight;
widget.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
widget.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
};
document.onmouseup = () => {
if (isDragging) {
isDragging = false;
widget.style.cursor = 'default';
const rect = widget.getBoundingClientRect();
saveFocusPosition({
x: Math.round(rect.left),
y: Math.round(rect.top)
});
}
};
updateDisplay();
if (autoStart) {
setTimeout(() => {
toggleTimer();
}, 300);
}
}
function createSettingUI() {
if (!/^(https?:\/\/)?(www\.)?luogu\.com\.cn/i.test(window.location.href)) return;
const existingMask = document.getElementById('luogu-modal-mask');
const existingBox = document.getElementById('luogu-modal-box');
if (existingMask) existingMask.remove();
if (existingBox) existingBox.remove();
const style = document.createElement('style');
style.textContent = `
#luogu-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 99997;
display: none;
}
#luogu-modal-box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
padding: 24px;
background: #fff;
border-radius: 12px;
z-index: 99998;
display: none;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
}
#luogu-modal-box h3 {
margin: 0 0 20px 0;
color: #222;
text-align: center;
font-size: 22px;
border-bottom: 1px solid #eee;
padding-bottom: 12px;
}
.luogu-form-group {
margin-bottom: 18px;
}
.luogu-form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 15px;
}
#luogu-pid-input {
width: 100%;
padding: 12px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
#luogu-pid-input:focus {
border-color: #0099ff;
outline: none;
}
#luogu-solution-switch {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
}
#luogu-solution-switch input {
width: 20px;
height: 20px;
cursor: pointer;
}
.duration-settings {
display: flex;
gap: 15px;
}
.duration-col {
flex: 1;
}
.duration-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
margin-top: 6px;
}
.pid-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
min-height: 35px;
}
.pid-item {
background: #f0f0f0;
padding: 6px 12px;
border-radius: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s;
}
.pid-item:hover {
background: #e0e0e0;
transform: scale(1.03);
}
.pid-item .remove {
color: #ff4444;
cursor: pointer;
font-size: 15px;
margin-left: 4px;
}
#luogu-color-input {
width: 100%;
height: 36px;
border: 1px solid #ddd;
border-radius: 6px;
padding: 2px;
margin-top: 6px;
cursor: pointer;
}
#luogu-modal-btn-group {
display: flex;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #f5f5f5;
}
#luogu-save-btn, #luogu-cancel-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
transition: all 0.2s;
}
#luogu-save-btn {
background: #0099ff;
color: white;
}
#luogu-save-btn:hover {
background: #0077cc;
}
#luogu-cancel-btn {
background: #f0f0f0;
color: #666;
}
#luogu-cancel-btn:hover {
background: #e0e0e0;
}
.luogu-hint {
font-size: 12px;
color: #888;
margin-top: 8px;
line-height: 1.5;
}
`;
document.head.appendChild(style);
const mask = document.createElement('div');
mask.id = 'luogu-modal-mask';
mask.onclick = () => {
mask.style.display = 'none';
modalBox.style.display = 'none';
};
document.body.appendChild(mask);
const modalBox = document.createElement('div');
modalBox.id = 'luogu-modal-box';
modalBox.innerHTML = `
<h3>专注设置</h3>
<div class="luogu-form-group">
<label>题目计划</label>
<input type="text" id="luogu-pid-input" placeholder="输入题号(如:P1000、B2001、CF1C、AT_abc001_a、UVA100)">
<div class="luogu-hint">最多添加 6 道题目,点击题号可直接跳转。支持洛谷、Codeforces、AtCoder、UVA 等题库编号。</div>
<div class="pid-list" id="pid-list"></div>
</div>
<div class="luogu-form-group">
<label>题解访问</label>
<div id="luogu-solution-switch">
<input type="checkbox" id="luogu-solution-checkbox">
<span>允许查看题解(独立控制,不受计时影响)</span>
</div>
</div>
<div class="luogu-form-group">
<label>番茄钟时长(分钟)</label>
<div class="duration-settings">
<div class="duration-col">
<div style="font-size:13px;color:#666;">工作时长</div>
<input type="number" id="luogu-work-input" class="duration-input" min="1" max="120" value="${getWorkDuration()}">
</div>
<div class="duration-col">
<div style="font-size:13px;color:#666;">休息时长</div>
<input type="number" id="luogu-rest-input" class="duration-input" min="1" max="60" value="${getRestDuration()}">
</div>
</div>
</div>
<div class="luogu-form-group">
<label>番茄钟颜色</label>
<input type="color" id="luogu-color-input" value="${getFocusColor()}">
<div class="luogu-hint">专注工作时,番茄钟边框会显示此颜色。</div>
</div>
<div id="luogu-modal-btn-group">
<button id="luogu-save-btn">保存</button>
<button id="luogu-cancel-btn">取消</button>
</div>
`;
document.body.appendChild(modalBox);
function renderPidList() {
const pidList = document.getElementById('pid-list');
pidList.innerHTML = '';
const pids = getSavePids();
if (pids.length === 0) {
pidList.innerHTML = '<div style="text-align:center;color:#999;padding:8px 0;width:100%">暂无题目</div>';
return;
}
pids.forEach(pid => {
const item = document.createElement('div');
item.className = 'pid-item';
item.innerHTML = `${pid}<span class="remove">×</span>`;
item.addEventListener('click', (e) => {
if (e.target.classList.contains('remove')) return;
window.location.href = `https://www.luogu.com.cn/problem/${pid}`;
});
const removeBtn = item.querySelector('.remove');
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (removePid(pid)) {
renderPidList();
}
});
pidList.appendChild(item);
});
}
document.getElementById('luogu-save-btn').onclick = () => {
const pidInput = document.getElementById('luogu-pid-input');
const rawPid = pidInput.value.trim();
const pid = rawPid.startsWith('AT_') ? rawPid : rawPid.toUpperCase();
const solutionChecked = document.getElementById('luogu-solution-checkbox').checked;
const workDuration = parseInt(document.getElementById('luogu-work-input').value) || DEFAULT_WORK_DURATION;
const restDuration = parseInt(document.getElementById('luogu-rest-input').value) || DEFAULT_REST_DURATION;
const color = document.getElementById('luogu-color-input').value;
if (workDuration < 1 || workDuration > 120) {
alert('工作时长必须在 1-120 分钟之间');
return;
}
if (restDuration < 1 || restDuration > 60) {
alert('休息时长必须在 1-60 分钟之间');
return;
}
if (pid && !/^(P\d{4,6}|SP\d{3,5}|B\d{4,5}|CF\d+[A-Z]?|AT_[a-z0-9_]+|UVA\d{3,5})$/i.test(pid)) {
alert('题号格式错误!\n正确格式:P1000, SP123, B2001, CF1C, AT_abc001_a, UVA100 等');
return;
}
const pids = getSavePids();
if (pid && pids.length < MAX_PIDS && !pids.includes(pid)) {
pids.push(pid);
savePids(pids);
}
saveSolutionStatus(solutionChecked);
saveDurations(workDuration, restDuration);
saveFocusColor(color);
alert('设置已保存!');
mask.style.display = 'none';
modalBox.style.display = 'none';
renderPidList();
monitorSolutionElements();
};
document.getElementById('luogu-cancel-btn').onclick = () => {
mask.style.display = 'none';
modalBox.style.display = 'none';
};
document.getElementById('luogu-pid-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('luogu-save-btn').click();
}
});
mask.style.display = 'block';
modalBox.style.display = 'block';
document.getElementById('luogu-pid-input').value = '';
document.getElementById('luogu-solution-checkbox').checked = getSolutionStatus();
renderPidList();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
createPomodoroWidget();
});
} else {
createPomodoroWidget();
}
window.addEventListener('load', () => {
monitorSolutionElements();
});
})();
:::
有什么建议可以评论私信,目前还不太成熟,持续更新。
谢谢你看到这里~