// ==UserScript== // @name 做题计划管理器 // @namespace http://tampermonkey.net/ // @version 1.5 // @description 洛谷做题计划怎么不支持其他OJ?哦咦game不是这么玩的!你应该写一个做题计划管理脚本,后面忘了。 // @author Nuclear_Fish_cyq // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 只在顶层窗口中运行,避免在iframe中重复创建 if (window.self !== window.top) { return; } // 预定义颜色选项 const colorOptions = [ '#FE4C61', '#F39C11', '#FFC116', '#52C41A', '#3498DB', '#9D3DCF', '#0E1D69', '#BFBFBF' ]; // 存储键名 const STORAGE_KEY = 'problemPlanner_data'; const COUNT_KEY = 'problemPlanner_completedCount'; // 状态变量 let problems = []; let completedCount = 0; let isPanelVisible = false; // 从GM存储加载数据 function loadData() { try { const savedData = GM_getValue(STORAGE_KEY); problems = savedData ? JSON.parse(savedData) : []; const savedCount = GM_getValue(COUNT_KEY); completedCount = savedCount || 0; } catch (e) { console.error('加载数据失败:', e); problems = []; completedCount = 0; } } // 保存数据到GM存储 function saveData() { try { GM_setValue(STORAGE_KEY, JSON.stringify(problems)); GM_setValue(COUNT_KEY, completedCount); } catch (e) { console.error('保存数据失败:', e); } } // 监听其他标签页的数据变化 GM_addValueChangeListener(STORAGE_KEY, function(key, oldValue, newValue, remote) { if (remote) { // 数据来自其他标签页 try { problems = newValue ? JSON.parse(newValue) : []; if (isPanelVisible) { renderProblems(); } showSyncNotification(); } catch (e) { console.error('同步数据解析失败:', e); } } }); GM_addValueChangeListener(COUNT_KEY, function(key, oldValue, newValue, remote) { if (remote) { // 数据来自其他标签页 completedCount = newValue || 0; if (isPanelVisible) { updateCounters(); } } }); // 初始化加载数据 loadData(); // 显示同步提示 function showSyncNotification() { let notification = document.querySelector('.sync-notification'); if (!notification) { notification = document.createElement('div'); notification.className = 'sync-notification'; notification.style.cssText = ` position: fixed; top: 10px; right: 10px; background: #3498DB; color: white; padding: 10px 15px; border-radius: 5px; font-family: 'Segoe UI', sans-serif; font-size: 14px; z-index: 1000000; box-shadow: 0 2px 10px rgba(0,0,0,0.2); opacity: 0; transform: translateY(-20px); transition: opacity 0.3s, transform 0.3s; `; document.body.appendChild(notification); } notification.textContent = '数据已同步更新'; notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateY(-20px)'; }, 3000); } // 创建主样式 const style = document.createElement('style'); style.textContent = ` .problem-planner-container { position: fixed; bottom: 20px; left: 20px; z-index: 999999; font-family: 'Segoe UI', sans-serif; } .problem-planner-button { width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #00d2ff, #3a7bd5); color: white; border: none; cursor: pointer; font-weight: bold; font-size: 20px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); transition: transform 0.2s, box-shadow 0.2s; font-family: 'Segoe UI', sans-serif; } .problem-planner-button:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } .problem-planner-panel { position: absolute; bottom: 70px; left: 0; width: 420px; background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); display: none; overflow: hidden; font-family: 'Segoe UI', sans-serif; } .problem-planner-panel .panel-header { background: linear-gradient(135deg, #3a7bd5, #00d2ff); color: white; padding: 20px; text-align: center; font-weight: bold; font-size: 18px; position: relative; } .problem-planner-panel .add-problem-form { padding: 20px; border-bottom: 1px solid #eee; } .problem-planner-panel .form-group { margin-bottom: 15px; } .problem-planner-panel .form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #333; } .problem-planner-panel .form-input { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-family: 'Segoe UI', sans-serif; font-size: 14px; box-sizing: border-box; } .problem-planner-panel .form-input:focus { outline: none; border-color: #3a7bd5; } .problem-planner-panel .color-selection { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; margin-top: 10px; } .problem-planner-panel .color-option { width: 30px; height: 30px; border-radius: 6px; cursor: pointer; border: 2px solid transparent; transition: transform 0.2s, border-color 0.2s; } .problem-planner-panel .color-option:hover { transform: scale(1.1); } .problem-planner-panel .color-option.selected { border-color: #333 !important; transform: scale(1.1); box-shadow: 0 0 0 2px white, 0 0 0 4px #333 !important; } .problem-planner-panel .add-button { width: 100%; padding: 12px; background: linear-gradient(135deg, #52C41A, #3498DB); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 16px; font-family: 'Segoe UI', sans-serif; transition: transform 0.2s; } .problem-planner-panel .add-button:hover { transform: translateY(-2px); } .problem-planner-panel .problems-list { max-height: 300px; overflow-y: auto; padding: 20px; } .problem-planner-panel .problem-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; margin-bottom: 10px; background: #f8f9fa; border-radius: 8px; transition: transform 0.2s; } .problem-planner-panel .problem-item:hover { transform: translateX(5px); background: #e9ecef; } .problem-planner-panel .problem-link { text-decoration: none; font-weight: bold; font-size: 14px; display: block; word-break: break-all; flex-grow: 1; margin-right: 10px; } .problem-planner-panel .problem-actions { display: flex; gap: 8px; } .problem-planner-panel .giveup-btn { background: #BFBFBF; color: white; border: none; border-radius: 4px; width: 50px; height: 30px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; transition: background 0.2s; } .problem-planner-panel .giveup-btn:hover { background: #a0a0a0; } .problem-planner-panel .complete-btn { background: #52C41A; color: white; border: none; border-radius: 4px; width: 50px; height: 30px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; transition: background 0.2s; } .problem-planner-panel .complete-btn:hover { background: #3da814; } .problem-planner-panel .panel-footer { padding: 15px 20px; background: #f8f9fa; border-top: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; font-weight: 600; } .problem-planner-panel .stats { display: flex; flex-direction: column; gap: 5px; } .problem-planner-panel .stat-item { display: flex; align-items: center; gap: 8px; } .problem-planner-panel .stat-label { color: #666; } .problem-planner-panel .stat-value { color: #3a7bd5; font-size: 18px; font-weight: bold; } .problem-planner-panel .completed-count { color: #52C41A; } .problem-planner-panel .clear-btn { background: #FE4C61; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; transition: background 0.2s; } .problem-planner-panel .clear-btn:hover { background: #e43a4d; } .problem-planner-panel .empty-message { text-align: center; color: #999; padding: 40px 20px; font-style: italic; } .problem-planner-panel .sync-badge { position: absolute; right: 20px; top: 50%; transform: translateY(-50%); background: rgba(255, 255, 255, 0.2); padding: 2px 8px; border-radius: 10px; font-size: 12px; display: flex; align-items: center; gap: 5px; } .problem-planner-panel .sync-badge::before { content: '🔄'; } .problem-planner-panel .current-url-hint { font-size: 12px; color: #3498DB; margin-top: 5px; display: flex; align-items: center; gap: 5px; } .problem-planner-panel .current-url-hint::before { content: '📌'; } .problem-planner-panel .use-current-btn { background: transparent; border: 1px solid #3498DB; color: #3498DB; border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer; margin-left: 5px; transition: all 0.2s; } .problem-planner-panel .use-current-btn:hover { background: #3498DB; color: white; } `; document.head.appendChild(style); // 创建容器 const container = document.createElement('div'); container.className = 'problem-planner-container'; // 创建主按钮 const mainButton = document.createElement('button'); mainButton.className = 'problem-planner-button'; mainButton.textContent = '题'; mainButton.title = '打开做题计划(跨网站同步)'; // 创建面板 const panel = document.createElement('div'); panel.className = 'problem-planner-panel'; // 面板HTML结构 panel.innerHTML = `
做题计划 跨站同步
当前页面: ${window.location.href}
`; // 添加到容器 container.appendChild(mainButton); container.appendChild(panel); document.body.appendChild(container); // 初始化颜色选择器 const colorSelection = panel.querySelector('#color-selection'); let selectedColor = colorOptions[0]; colorOptions.forEach(color => { const colorDiv = document.createElement('div'); colorDiv.className = 'color-option'; colorDiv.style.backgroundColor = color; colorDiv.title = color; if (color === selectedColor) { colorDiv.classList.add('selected'); } colorDiv.addEventListener('click', () => { panel.querySelectorAll('.color-option').forEach(opt => { opt.classList.remove('selected'); }); colorDiv.classList.add('selected'); selectedColor = color; }); colorSelection.appendChild(colorDiv); }); // 更新计数器显示 function updateCounters() { const pendingCount = panel.querySelector('#pending-count'); const completedCountEl = panel.querySelector('#completed-count'); pendingCount.textContent = problems.length; completedCountEl.textContent = completedCount; } // 渲染问题列表 function renderProblems() { const list = panel.querySelector('#problems-list'); updateCounters(); if (problems.length === 0) { list.innerHTML = '
暂无待做题目,请添加题目到计划中
'; return; } list.innerHTML = ''; problems.forEach((problem, index) => { const item = document.createElement('div'); item.className = 'problem-item'; const link = document.createElement('a'); link.className = 'problem-link'; link.href = problem.url; link.textContent = problem.name; link.style.color = problem.color; link.target = '_blank'; link.rel = 'noopener noreferrer'; const actions = document.createElement('div'); actions.className = 'problem-actions'; const giveupBtn = document.createElement('button'); giveupBtn.className = 'giveup-btn'; giveupBtn.textContent = '放弃'; giveupBtn.title = '放弃此题(不计入完成)'; const completeBtn = document.createElement('button'); completeBtn.className = 'complete-btn'; completeBtn.textContent = '完成'; completeBtn.title = '标记为已完成'; giveupBtn.addEventListener('click', (e) => { e.preventDefault(); if (confirm(`确定要放弃题目 "${problem.name}" 吗?`)) { problems.splice(index, 1); saveData(); renderProblems(); } }); completeBtn.addEventListener('click', (e) => { e.preventDefault(); if (confirm(`确定要标记题目 "${problem.name}" 为已完成吗?`)) { problems.splice(index, 1); completedCount++; saveData(); renderProblems(); } }); actions.appendChild(giveupBtn); actions.appendChild(completeBtn); item.appendChild(link); item.appendChild(actions); list.appendChild(item); }); } // 使用当前网址按钮功能 panel.querySelector('#use-current-url').addEventListener('click', () => { const urlInput = panel.querySelector('#problem-url'); urlInput.value = window.location.href; // 给用户视觉反馈 const useCurrentBtn = panel.querySelector('#use-current-url'); const originalText = useCurrentBtn.textContent; useCurrentBtn.textContent = '已应用!'; useCurrentBtn.style.background = '#52C41A'; useCurrentBtn.style.color = 'white'; useCurrentBtn.style.borderColor = '#52C41A'; setTimeout(() => { useCurrentBtn.textContent = originalText; useCurrentBtn.style.background = ''; useCurrentBtn.style.color = ''; useCurrentBtn.style.borderColor = ''; }, 1000); }); // 添加问题 panel.querySelector('#add-problem').addEventListener('click', () => { const urlInput = panel.querySelector('#problem-url'); const nameInput = panel.querySelector('#problem-name'); let url = urlInput.value.trim(); const name = nameInput.value.trim(); // 如果URL为空,则使用当前网址 if (!url) { url = window.location.href; urlInput.value = url; } if (!name) { alert('请输入题目名称!'); nameInput.focus(); return; } // 验证URL格式 try { new URL(url); } catch (e) { alert('请输入有效的网址!'); urlInput.focus(); return; } // 检查是否已存在相同URL的题目 if (problems.some(problem => problem.url === url)) { alert('此题目已在计划中!'); return; } problems.push({ url: url, name: name, color: selectedColor, addedDate: new Date().toISOString() }); saveData(); renderProblems(); // 清空输入 nameInput.value = ''; // URL输入框不清空,保留当前页面网址,方便用户继续添加 urlInput.value = window.location.href; // 焦点移动到题目名称输入框 nameInput.focus(); // 显示成功提示 const addBtn = panel.querySelector('#add-problem'); const originalText = addBtn.textContent; addBtn.textContent = '添加成功!'; addBtn.style.background = 'linear-gradient(135deg, #52C41A, #2ecc71)'; setTimeout(() => { addBtn.textContent = originalText; addBtn.style.background = 'linear-gradient(135deg, #52C41A, #3498DB)'; }, 1000); }); // 清空所有问题 panel.querySelector('#clear-all').addEventListener('click', () => { if (problems.length > 0 && confirm('确定要清空所有计划中的题目吗?此操作不可撤销。')) { problems = []; completedCount = 0; saveData(); renderProblems(); } }); // 允许按Enter键添加(在题目名称输入框中按Enter) panel.querySelector('#problem-name').addEventListener('keypress', (e) => { if (e.key === 'Enter') { panel.querySelector('#add-problem').click(); } }); // 切换面板显示/隐藏 mainButton.addEventListener('click', (e) => { e.stopPropagation(); isPanelVisible = !isPanelVisible; panel.style.display = isPanelVisible ? 'block' : 'none'; if (isPanelVisible) { // 每次打开面板时重新加载数据 loadData(); renderProblems(); // 更新当前网址提示 const currentUrlHint = panel.querySelector('.current-url-hint'); currentUrlHint.innerHTML = `当前页面: ${window.location.href} `; // 重新绑定使用当前网址按钮的事件 panel.querySelector('#use-current-url').addEventListener('click', () => { const urlInput = panel.querySelector('#problem-url'); urlInput.value = window.location.href; // 给用户视觉反馈 const useCurrentBtn = panel.querySelector('#use-current-url'); const originalText = useCurrentBtn.textContent; useCurrentBtn.textContent = '已应用!'; useCurrentBtn.style.background = '#52C41A'; useCurrentBtn.style.color = 'white'; useCurrentBtn.style.borderColor = '#52C41A'; setTimeout(() => { useCurrentBtn.textContent = originalText; useCurrentBtn.style.background = ''; useCurrentBtn.style.color = ''; useCurrentBtn.style.borderColor = ''; }, 1000); }); // 如果URL输入框为空,自动填入当前网址 const urlInput = panel.querySelector('#problem-url'); if (!urlInput.value.trim()) { urlInput.value = window.location.href; } // 焦点放在题目名称输入框 setTimeout(() => { panel.querySelector('#problem-name').focus(); }, 100); } }); // 点击页面其他区域关闭面板 document.addEventListener('click', (e) => { if (isPanelVisible && !panel.contains(e.target) && !mainButton.contains(e.target)) { isPanelVisible = false; panel.style.display = 'none'; } }); // 防止面板内部点击事件冒泡到document panel.addEventListener('click', (e) => { e.stopPropagation(); }); // 初始渲染 renderProblems(); // 页面可见性变化时也检查更新 document.addEventListener('visibilitychange', () => { if (!document.hidden && isPanelVisible) { loadData(); renderProblems(); } }); })();