// ==UserScript== // @name 做题计划管理器 // @namespace http://tampermonkey.net/ // @version 1.7 // @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) : []; // 向下兼容:确保每个题目都有 understood 字段(默认为 false) problems = problems.map(p => ({ ...p, understood: p.understood || false })); 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) : []; // 向下兼容 problems = problems.map(p => ({ ...p, understood: p.understood || false })); 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.understood { opacity: 0.8; background: #f0f0f0; border-left: 4px solid #FFC116; } .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 .understood-badge { font-size: 12px; color: #FFC116; margin-left: 5px; font-weight: normal; } .problem-planner-panel .problem-actions { display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end; min-width: 150px; } .problem-planner-panel .giveup-btn { background: #BFBFBF; color: white; border: none; border-radius: 4px; width: 45px; height: 30px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; font-size: 12px; transition: background 0.2s; } .problem-planner-panel .giveup-btn:hover { background: #a0a0a0; } .problem-planner-panel .understand-btn { background: #FFC116; color: white; border: none; border-radius: 4px; width: 45px; height: 30px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; font-size: 12px; transition: background 0.2s; } .problem-planner-panel .understand-btn:hover { background: #e6a800; } .problem-planner-panel .complete-btn { background: #52C41A; color: white; border: none; border-radius: 4px; width: 45px; height: 30px; cursor: pointer; font-weight: bold; font-family: 'Segoe UI', sans-serif; font-size: 12px; 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 .action-buttons { display: flex; gap: 8px; } .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 .export-btn { background: #3498DB; 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 .export-btn:hover { background: #2980b9; } .problem-planner-panel .import-btn { background: #9D3DCF; 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 .import-btn:hover { background: #8e44ad; } .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; } .problem-planner-panel .file-input { display: none; } .problem-planner-panel .backup-section { padding: 15px 20px; border-top: 1px solid #eee; background: #f8f9fa; } .problem-planner-panel .backup-title { font-weight: bold; margin-bottom: 10px; color: #333; display: flex; align-items: center; gap: 8px; } .problem-planner-panel .backup-title::before { content: '💾'; } .problem-planner-panel .backup-buttons { display: flex; gap: 10px; flex-wrap: wrap; } .problem-planner-panel .backup-notice { font-size: 12px; color: #666; margin-top: 10px; font-style: italic; } .problem-planner-panel .download-link { display: none; } .problem-planner-panel .data-preview { background: #f8f9fa; border: 1px solid #ddd; border-radius: 6px; padding: 15px; margin-top: 15px; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px; display: none; } .problem-planner-panel .data-preview.show { display: block; } `; 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 notUnderstood = problems.filter(p => !p.understood).length; const understood = problems.filter(p => p.understood).length; panel.querySelector('#not-understood-count').textContent = notUnderstood; panel.querySelector('#understood-count').textContent = understood; panel.querySelector('#completed-count').textContent = completedCount; } // 渲染问题列表(未理解在上,已理解在下) function renderProblems() { const list = panel.querySelector('#problems-list'); updateCounters(); if (problems.length === 0) { list.innerHTML = '
暂无待做题目,请添加题目到计划中
'; return; } // 分离未理解和已理解,各自保持原顺序 const notUnderstood = problems.filter(p => !p.understood); const understood = problems.filter(p => p.understood); list.innerHTML = ''; // 渲染未理解题目 notUnderstood.forEach((problem, index) => { const item = createProblemItem(problem, index, false); list.appendChild(item); }); // 渲染已理解题目 understood.forEach((problem, index) => { const item = createProblemItem(problem, index, true); list.appendChild(item); }); } // 创建单个题目DOM元素 function createProblemItem(problem, index, isUnderstoodGroup) { const item = document.createElement('div'); item.className = 'problem-item'; if (problem.understood) { item.classList.add('understood'); } 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'; // 如果已理解,添加一个标记 if (problem.understood) { const badge = document.createElement('span'); badge.className = 'understood-badge'; badge.textContent = ' ✓'; link.appendChild(badge); } const actions = document.createElement('div'); actions.className = 'problem-actions'; // 放弃按钮 const giveupBtn = document.createElement('button'); giveupBtn.className = 'giveup-btn'; giveupBtn.textContent = '放弃'; giveupBtn.title = '放弃此题(不计入完成)'; giveupBtn.addEventListener('click', (e) => { e.preventDefault(); if (confirm(`确定要放弃题目 "${problem.name}" 吗?`)) { const realIndex = problems.findIndex(p => p.url === problem.url); // 通过url找到实际索引 if (realIndex !== -1) { problems.splice(realIndex, 1); saveData(); renderProblems(); } } }); // 理解按钮(仅当未理解时显示) if (!problem.understood) { const understandBtn = document.createElement('button'); understandBtn.className = 'understand-btn'; understandBtn.textContent = '理解'; understandBtn.title = '标记为已理解(移至底部)'; understandBtn.addEventListener('click', (e) => { e.preventDefault(); const realIndex = problems.findIndex(p => p.url === problem.url); if (realIndex !== -1) { problems[realIndex].understood = true; saveData(); renderProblems(); } }); actions.appendChild(understandBtn); } // 完成按钮 const completeBtn = document.createElement('button'); completeBtn.className = 'complete-btn'; completeBtn.textContent = '完成'; completeBtn.title = '标记为已完成'; completeBtn.addEventListener('click', (e) => { e.preventDefault(); if (confirm(`确定要标记题目 "${problem.name}" 为已完成吗?`)) { const realIndex = problems.findIndex(p => p.url === problem.url); if (realIndex !== -1) { problems.splice(realIndex, 1); completedCount++; saveData(); renderProblems(); } } }); actions.appendChild(giveupBtn); actions.appendChild(completeBtn); item.appendChild(link); item.appendChild(actions); return item; } // 导出数据 function exportData() { const data = { problems: problems, completedCount: completedCount, exportDate: new Date().toISOString(), version: '1.0', totalProblems: problems.length }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const downloadLink = panel.querySelector('#download-link'); const url = URL.createObjectURL(dataBlob); downloadLink.href = url; const date = new Date(); const dateStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; downloadLink.download = `做题计划备份_${dateStr}.json`; const dataPreview = panel.querySelector('#data-preview'); dataPreview.textContent = dataStr; dataPreview.classList.add('show'); setTimeout(() => { downloadLink.click(); showNotification('数据已导出,正在下载文件...', '#52C41A'); setTimeout(() => { dataPreview.classList.remove('show'); }, 3000); }, 100); } // 导入数据 function importData() { panel.querySelector('#file-input').click(); } // 处理文件选择 function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); if (!data.problems || !Array.isArray(data.problems)) { throw new Error('数据格式不正确:缺少题目列表'); } const importConfirm = confirm( `准备导入 ${data.problems.length} 个题目\n` + `完成数量:${data.completedCount || 0}\n\n` + `导入会覆盖现有数据,确定继续吗?` ); if (importConfirm) { problems = data.problems.map(p => ({ ...p, understood: p.understood || false })); completedCount = data.completedCount || 0; saveData(); renderProblems(); showNotification('数据导入成功!', '#52C41A'); } } catch (error) { alert(`导入失败:${error.message}\n\n请确保选择的是有效的备份文件。`); console.error('导入错误:', error); } event.target.value = ''; }; reader.onerror = function() { alert('读取文件失败,请重试'); event.target.value = ''; }; reader.readAsText(file); } // 显示通知 function showNotification(message, color = '#3498DB') { let notification = document.querySelector('.backup-notification'); if (!notification) { notification = document.createElement('div'); notification.className = 'backup-notification'; notification.style.cssText = ` position: fixed; top: 60px; right: 10px; 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 = message; notification.style.background = color; notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateY(-20px)'; }, 3000); } // 使用当前网址 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(); if (!url) { url = window.location.href; urlInput.value = url; } if (!name) { alert('请输入题目名称!'); nameInput.focus(); return; } try { new URL(url); } catch (e) { alert('请输入有效的网址!'); urlInput.focus(); return; } if (problems.some(problem => problem.url === url)) { alert('此题目已在计划中!'); return; } problems.push({ url: url, name: name, color: selectedColor, addedDate: new Date().toISOString(), understood: false }); saveData(); renderProblems(); nameInput.value = ''; 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); }); // 清空所有数据 function clearAllData() { if (problems.length > 0 && confirm('确定要清空所有计划中的题目吗?此操作不可撤销。')) { problems = []; completedCount = 0; saveData(); renderProblems(); showNotification('数据已清空', '#FE4C61'); } } // 绑定导入导出按钮 panel.querySelector('#export-data').addEventListener('click', exportData); panel.querySelector('#export-btn-small').addEventListener('click', exportData); panel.querySelector('#import-data').addEventListener('click', importData); panel.querySelector('#import-btn-small').addEventListener('click', importData); panel.querySelector('#clear-all').addEventListener('click', clearAllData); panel.querySelector('#clear-btn-small').addEventListener('click', clearAllData); panel.querySelector('#file-input').addEventListener('change', handleFileSelect); // 按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); }); 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'; } }); panel.addEventListener('click', (e) => { e.stopPropagation(); }); // 初始渲染 renderProblems(); // 页面可见性变化时检查更新 document.addEventListener('visibilitychange', () => { if (!document.hidden && isPanelVisible) { loadData(); renderProblems(); } }); })();