// ==UserScript== // @name 高级标签页跳转器 // @namespace https://docs.scriptcat.org/ // @version 1.0 // @description 自动循环跳转预设网页,支持运行状态下自动最小化、编辑锁定、保存提醒及 Alt+M/R 快捷键控制。 // @tag 标签页 自动跳转 面板操作 // @author yangwenren // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // --- 状态初始化 --- const DEFAULT_URLS = []; let state = { urls: GM_getValue('cycle_urls', DEFAULT_URLS), interval: GM_getValue('cycle_interval', 30), isRunning: GM_getValue('is_running', false), currentIndex: GM_getValue('current_index', 0), isMinimized: GM_getValue('is_running', false) ? true : GM_getValue('is_minimized', false), isHidden: false, expandedPos: GM_getValue('expanded_pos', { x: 20, y: 20 }), minimizedPos: { x: 20, y: 20 }, timer: null }; // --- 样式注入 --- GM_addStyle(` #jump-panel { position: fixed; z-index: 2147483647; background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); font-family: -apple-system, system-ui, sans-serif; width: 240px; display: flex; flex-direction: column; user-select: none; overflow: hidden; transition: width 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.2), height 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.2), bottom 0.4s ease, right 0.4s ease, opacity 0.3s ease; } #jump-panel.minimized { width: 56px !important; height: 56px !important; border-radius: 28px !important; cursor: pointer; border-color: #1890ff; } #jump-panel.minimized.running { border-color: #ff4d4f; } .panel-header { background: rgba(0,0,0,0.03); padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; cursor: move; border-bottom: 1px solid rgba(0,0,0,0.05); } .panel-title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #333; } .minimized .panel-header, .minimized .panel-content { display: none !important; } .status-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; width: 100%; } .run-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; } .run-dot.active { background: #ff4d4f; box-shadow: 0 0 8px #ff4d4f; animation: breathe 1.5s infinite; } .status-text { font-size: 10px; font-weight: 900; color: #999; margin-top: 2px; } .status-text.active { color: #ff4d4f; } @keyframes breathe { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.7; } } .panel-content { padding: 12px; display: flex; flex-direction: column; gap: 8px; } textarea { width: 100%; height: 90px; font-size: 12px; padding: 8px; border: 1px solid #ddd; border-radius: 6px; resize: none; box-sizing: border-box; outline: none; transition: background 0.3s; } textarea:disabled, input:disabled { background: #f5f5f5; color: #aaa; cursor: not-allowed; border-color: #eee; } .control-row { display: flex; justify-content: space-between; align-items: center; font-size: 12px; margin-bottom: 2px; } input[type="number"] { width: 50px; padding: 3px; border: 1px solid #ddd; border-radius: 4px; text-align: center; } button { border: none; border-radius: 6px; cursor: pointer; font-weight: bold; width: 100%; transition: all 0.2s; } #save-btn { padding: 8px; background: #52c41a; color: white; font-size: 12px; display: none; margin-bottom: 2px; } #save-btn.show { display: block; animation: slideDown 0.3s ease; } #start-btn { padding: 10px; background: #1890ff; color: white; } #start-btn.running { background: #ff4d4f; } @keyframes slideDown { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } .header-btns button { background: none; color: #888; font-size: 18px; padding: 0 4px; width: auto; } `); const panel = document.createElement('div'); panel.id = 'jump-panel'; document.body.appendChild(panel); const updatePosition = (useAnimation = true) => { panel.style.transition = useAnimation ? "" : "none"; const targetPos = state.isMinimized ? state.minimizedPos : state.expandedPos; panel.style.right = targetPos.x + 'px'; panel.style.bottom = targetPos.y + 'px'; }; const render = () => { panel.style.display = state.isHidden ? 'none' : 'flex'; panel.style.opacity = state.isHidden ? '0' : '1'; updatePosition(true); if (state.isMinimized) { panel.classList.add('minimized'); panel.innerHTML = `
${state.isRunning ? 'RUN' : 'OFF'}
`; } else { panel.classList.remove('minimized'); const isDisabled = state.isRunning ? 'disabled' : ''; panel.innerHTML = `
页面跳转器
间隔 (秒):
`; } bindEvents(); }; function executeJump() { if (!state.isRunning || state.urls.length === 0) return; let nextIndex = (state.currentIndex + 1) % state.urls.length; GM_setValue('current_index', nextIndex); window.location.href = state.urls[nextIndex]; } function bindEvents() { if (state.isMinimized) { panel.onclick = (e) => { if (!panel.hasAttribute('dragging')) toggleMin(); panel.removeAttribute('dragging'); }; } else { panel.onclick = null; const urlTextarea = document.getElementById('url-list'); const saveBtn = document.getElementById('save-btn'); const startBtn = document.getElementById('start-btn'); const timeInput = document.getElementById('time-input'); if (!state.isRunning) { urlTextarea.oninput = () => { saveBtn.classList.toggle('show', urlTextarea.value.trim() !== state.urls.join('\n').trim()); }; saveBtn.onclick = () => { state.urls = urlTextarea.value.split('\n').filter(u => u.trim() !== ''); GM_setValue('cycle_urls', state.urls); saveBtn.classList.remove('show'); }; timeInput.onchange = (e) => { state.interval = parseInt(e.target.value) || 30; GM_setValue('cycle_interval', state.interval); }; } startBtn.onclick = () => { if (!state.isRunning) { const currentUrls = urlTextarea.value.split('\n').filter(u => u.trim() !== ''); if (currentUrls.length === 0) return alert("请先输入网址!"); state.urls = currentUrls; state.interval = parseInt(timeInput.value) || 30; state.isRunning = true; state.isMinimized = true; } else { state.isRunning = false; if (state.timer) clearTimeout(state.timer); } GM_setValue('is_running', state.isRunning); GM_setValue('cycle_urls', state.urls); GM_setValue('cycle_interval', state.interval); GM_setValue('is_minimized', state.isMinimized); render(); if (state.isRunning) setTimeout(executeJump, 300); }; document.getElementById('min-btn').onclick = (e) => { e.stopPropagation(); toggleMin(); }; document.getElementById('hide-btn').onclick = (e) => { e.stopPropagation(); toggleHide(); }; } // 拖拽逻辑 const dragHandle = state.isMinimized ? panel : document.getElementById('panel-drag-handle'); if (dragHandle) { dragHandle.onmousedown = function(e) { if (['BUTTON', 'TEXTAREA', 'INPUT'].includes(e.target.tagName)) return; let isMove = false; const startX = e.clientX, startY = e.clientY; const initialRight = state.isMinimized ? state.minimizedPos.x : state.expandedPos.x; const initialBottom = state.isMinimized ? state.minimizedPos.y : state.expandedPos.y; panel.style.transition = "none"; document.onmousemove = function(ev) { if (Math.abs(ev.clientX - startX) > 5 || Math.abs(ev.clientY - startY) > 5) { isMove = true; panel.style.right = (initialRight + (startX - ev.clientX)) + 'px'; panel.style.bottom = (initialBottom + (startY - ev.clientY)) + 'px'; } }; document.onmouseup = function(ev) { document.onmousemove = document.onmouseup = null; panel.style.transition = ""; if (isMove) { panel.setAttribute('dragging', 'true'); const finalPos = { x: initialRight + (startX - ev.clientX), y: initialBottom + (startY - ev.clientY) }; if (state.isMinimized) state.minimizedPos = finalPos; else state.expandedPos = finalPos; GM_setValue(state.isMinimized ? 'minimized_pos' : 'expanded_pos', finalPos); } }; }; } } function toggleMin() { state.isMinimized = !state.isMinimized; GM_setValue('is_minimized', state.isMinimized); render(); } function toggleHide() { state.isHidden = !state.isHidden; render(); } // --- 快捷键监听 --- window.addEventListener('keydown', (e) => { // Alt + M: 最小化/展开 if (e.altKey && e.code === 'KeyM') { e.preventDefault(); toggleMin(); } // Alt + R: 隐藏/显示 if (e.altKey && e.code === 'KeyR') { e.preventDefault(); toggleHide(); } }); // 初始化 render(); if (state.isRunning) { state.timer = setTimeout(executeJump, state.interval * 1000); } })();