// ==UserScript==
// @name 高级自动滚动控制器
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 新增显示/隐藏面板功能(快捷键Alt+R),新增最小化快捷键(Alt+M),优化面板样式
// @author yangwenren
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// 全局变量
let isScrolling = false;
let scrollSpeed = 5;
let scrollDirection = 'down';
let bottomAction = 'jump';
let panelMinimized = false;
let panelHidden = false;
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
let scrollInterval = null;
let rafId = null;
const MIN_SPEED = 1;
const MAX_SPEED = 40;
const SAFE_MARGIN = 15;
const FIXED_PANEL_HEIGHT = 260;
const MINIMIZED_HEIGHT = 45;
const PANEL_WIDTH = 280;
const MINIMIZED_WIDTH = 200;
const TOGGLE_HOTKEY = 'Alt+R'; // 显示/隐藏快捷键
const MINIMIZE_HOTKEY = 'Alt+M'; // 最小化快捷键
// 创建控制界面
function createControlPanel() {
const panel = document.createElement('div');
panel.id = 'scrollControlPanel';
panel.style.cssText = `
position: fixed;
bottom: ${SAFE_MARGIN}px;
right: ${SAFE_MARGIN}px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
width: ${PANEL_WIDTH}px;
height: ${FIXED_PANEL_HEIGHT}px;
user-select: none;
will-change: transform, width, opacity;
touch-action: none;
transform: translateZ(0);
transition: width 0.2s ease, box-shadow 0.2s ease, opacity 0.3s ease, padding-right 0.2s ease;
overflow: hidden;
padding-right: 0;
`;
// 标题栏
const titleBar = document.createElement('div');
titleBar.style.cssText = `
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
transition: background-color 0.2s ease;
height: 45px;
box-sizing: border-box;
`;
titleBar.classList.add('drag-handle');
titleBar.addEventListener('mousedown', () => {
titleBar.style.opacity = '0.9';
});
titleBar.addEventListener('mouseup', () => {
titleBar.style.opacity = '1';
});
const title = document.createElement('h3');
title.textContent = '高级滚动控制器';
title.style.cssText = 'margin: 0; font-size: 15px; font-weight: 500; letter-spacing: 0.3px; transition: font-size 0.2s ease;';
// 按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '6px';
// 最小化按钮
const minBtn = document.createElement('button');
minBtn.textContent = '−';
// 鼠标悬浮显示功能说明和快捷键
minBtn.title = `最小化面板(快捷键:${MINIMIZE_HOTKEY})`;
minBtn.style.cssText = `
width: 26px;
height: 26px;
border: none;
background: transparent;
font-size: 20px;
cursor: pointer;
line-height: 1;
padding: 0;
border-radius: 50%;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
`;
minBtn.addEventListener('mouseover', () => {
minBtn.style.backgroundColor = 'rgba(0,0,0,0.08)';
});
minBtn.addEventListener('mouseout', () => {
minBtn.style.backgroundColor = 'transparent';
});
minBtn.addEventListener('click', () => toggleMinimize(panel));
// 隐藏面板按钮
const hideBtn = document.createElement('button');
hideBtn.textContent = '⇨';
hideBtn.title = `隐藏面板(快捷键:${TOGGLE_HOTKEY})`;
hideBtn.style.cssText = `
width: 26px;
height: 26px;
border: none;
background: transparent;
font-size: 16px;
cursor: pointer;
line-height: 1;
padding: 0;
border-radius: 50%;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
`;
hideBtn.addEventListener('mouseover', () => {
hideBtn.style.backgroundColor = 'rgba(0,0,0,0.08)';
});
hideBtn.addEventListener('mouseout', () => {
hideBtn.style.backgroundColor = 'transparent';
});
hideBtn.addEventListener('click', () => togglePanelVisibility(panel));
buttonContainer.append(minBtn, hideBtn);
titleBar.append(title, buttonContainer);
panel.appendChild(titleBar);
// 内容区域
const contentArea = document.createElement('div');
contentArea.id = 'panelContent';
contentArea.style.cssText = `
padding: 18px;
transition: display 0.2s ease;
height: calc(${FIXED_PANEL_HEIGHT}px - 45px);
box-sizing: border-box;
overflow: hidden;
`;
// 速度控制
const speedDiv = document.createElement('div');
speedDiv.style.cssText = `
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 6px;
`;
const speedLabel = document.createElement('label');
speedLabel.textContent = `滚动速度: ${scrollSpeed}`;
speedLabel.id = 'speedLabel';
speedLabel.style.cssText = `
display: block;
font-size: 14px;
font-weight: 400;
`;
const speedSlider = document.createElement('input');
speedSlider.type = 'range';
speedSlider.min = MIN_SPEED;
speedSlider.max = MAX_SPEED;
speedSlider.value = scrollSpeed;
speedSlider.style.cssText = `
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
border-radius: 3px;
outline: none;
`;
speedSlider.style.setProperty('-webkit-slider-thumb', `
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
transition: all 0.2s ease;
`);
speedSlider.addEventListener('input', function() {
scrollSpeed = parseInt(this.value);
document.getElementById('speedLabel').textContent = `滚动速度: ${scrollSpeed}`;
if (isScrolling) updateScrollInterval();
});
speedDiv.append(speedLabel, speedSlider);
contentArea.appendChild(speedDiv);
// 方向控制
const directionDiv = document.createElement('div');
directionDiv.style.cssText = `
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 12px;
`;
const directionLabel = document.createElement('label');
directionLabel.textContent = '滚动方向:';
directionLabel.style.cssText = `
width: 85px;
font-size: 14px;
font-weight: 400;
`;
const directionSelect = document.createElement('select');
directionSelect.style.cssText = `
flex: 1;
padding: 7px 12px;
border-radius: 6px;
border: 1px solid;
font-size: 14px;
background-color: transparent;
cursor: pointer;
transition: all 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 14px;
padding-right: 35px;
`;
directionSelect.addEventListener('mouseover', () => {
directionSelect.style.borderColor = 'rgba(0,0,0,0.2)';
});
directionSelect.addEventListener('mouseout', () => {
updateTheme();
});
directionSelect.innerHTML = `
`;
directionSelect.value = scrollDirection;
directionSelect.addEventListener('change', function() {
scrollDirection = this.value;
if (isScrolling) updateScrollInterval();
});
directionDiv.append(directionLabel, directionSelect);
contentArea.appendChild(directionDiv);
// 底部行为控制
const actionDiv = document.createElement('div');
actionDiv.style.cssText = `
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 12px;
`;
const actionLabel = document.createElement('label');
actionLabel.textContent = '底部行为:';
actionLabel.style.cssText = `
width: 85px;
font-size: 14px;
font-weight: 400;
`;
const actionSelect = document.createElement('select');
actionSelect.style.cssText = `
flex: 1;
padding: 7px 12px;
border-radius: 6px;
border: 1px solid;
font-size: 14px;
background-color: transparent;
cursor: pointer;
transition: all 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 14px;
padding-right: 35px;
`;
actionSelect.addEventListener('mouseover', () => {
actionSelect.style.borderColor = 'rgba(0,0,0,0.2)';
});
actionSelect.addEventListener('mouseout', () => {
updateTheme();
});
actionSelect.innerHTML = `
`;
actionSelect.value = bottomAction;
actionSelect.addEventListener('change', function() {
bottomAction = this.value;
if (isScrolling) updateScrollInterval();
});
actionDiv.append(actionLabel, actionSelect);
contentArea.appendChild(actionDiv);
// 控制按钮
const buttonDiv = document.createElement('div');
buttonDiv.style.cssText = `
display: flex;
gap: 10px;
`;
const startBtn = document.createElement('button');
startBtn.id = 'startScrollBtn';
startBtn.textContent = '开始滚动';
startBtn.style.cssText = `
flex: 1;
padding: 8px 12px;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
`;
startBtn.addEventListener('mouseover', () => {
startBtn.style.transform = 'translateY(-1px)';
startBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
});
startBtn.addEventListener('mouseout', () => {
startBtn.style.transform = 'translateY(0)';
startBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
});
const stopBtn = document.createElement('button');
stopBtn.id = 'stopScrollBtn';
stopBtn.textContent = '停止滚动';
stopBtn.style.cssText = `
flex: 1;
padding: 8px 12px;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
opacity: 0.7;
`;
stopBtn.disabled = true;
stopBtn.addEventListener('mouseover', () => {
if (!stopBtn.disabled) {
stopBtn.style.transform = 'translateY(-1px)';
stopBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
}
});
stopBtn.addEventListener('mouseout', () => {
if (!stopBtn.disabled) {
stopBtn.style.transform = 'translateY(0)';
stopBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
}
});
startBtn.addEventListener('click', () => {
if (!isScrolling) {
startScrolling();
isScrolling = true;
startBtn.disabled = true;
stopBtn.disabled = false;
stopBtn.style.opacity = '1';
}
});
stopBtn.addEventListener('click', () => {
stopScrolling();
isScrolling = false;
startBtn.disabled = false;
stopBtn.disabled = true;
stopBtn.style.opacity = '0.7';
});
buttonDiv.append(startBtn, stopBtn);
contentArea.appendChild(buttonDiv);
panel.appendChild(contentArea);
document.body.appendChild(panel);
// 初始化主题
updateTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
// 初始化拖动
initDragEvents(panel, titleBar);
// 窗口大小变化监听
window.addEventListener('resize', () => {
if (!panelHidden) checkBounds(panel);
});
// 初始化快捷键
initHotkeys(panel);
return panel;
}
// 切换面板显示/隐藏状态
function togglePanelVisibility(panel) {
panelHidden = !panelHidden;
if (panelHidden) {
panel.style.opacity = '0';
panel.style.pointerEvents = 'none';
const hideBtn = panel.querySelector('button:last-of-type');
if (hideBtn) hideBtn.textContent = '⇦';
} else {
panel.style.opacity = '1';
panel.style.pointerEvents = 'auto';
const hideBtn = panel.querySelector('button:last-of-type');
if (hideBtn) hideBtn.textContent = '⇨';
checkBounds(panel);
}
}
// 切换最小化状态
function toggleMinimize(panel) {
if (panelHidden) return;
const content = document.getElementById('panelContent');
const minBtn = panel.querySelector('button:first-of-type');
const title = panel.querySelector('h3');
const buttonContainer = panel.querySelector('.drag-handle > div'); // 按钮容器
panelMinimized = !panelMinimized;
if (panelMinimized) {
content.style.display = 'none';
panel.style.width = `${MINIMIZED_WIDTH}px`;
panel.style.height = `${MINIMIZED_HEIGHT}px`;
panel.style.paddingRight = '0';
minBtn.textContent = '+';
title.style.fontSize = '15px';
buttonContainer.style.gap = '1px';
} else {
content.style.display = 'block';
panel.style.width = `${PANEL_WIDTH}px`;
panel.style.height = `${FIXED_PANEL_HEIGHT}px`;
panel.style.paddingRight = '0';
minBtn.textContent = '−';
title.style.fontSize = '15px';
buttonContainer.style.gap = '6px';
}
checkBounds(panel);
}
// 初始化所有快捷键
function initHotkeys(panel) {
document.addEventListener('keydown', (e) => {
// 显示/隐藏面板:Alt+R
if (e.altKey && e.key.toLowerCase() === 'r') {
e.preventDefault();
togglePanelVisibility(panel);
}
// 最小化/展开面板:Alt+M
if (e.altKey && e.key.toLowerCase() === 'm') {
e.preventDefault();
toggleMinimize(panel);
}
});
// 控制台提示所有快捷键
console.log(`高级滚动控制器快捷键:`);
console.log(`- ${TOGGLE_HOTKEY}:显示/隐藏面板`);
console.log(`- ${MINIMIZE_HOTKEY}:最小化/展开面板`);
}
// 检查面板位置
function checkBounds(panel) {
const panelHeight = panelMinimized ? MINIMIZED_HEIGHT : FIXED_PANEL_HEIGHT;
const panelWidth = panelMinimized ? MINIMIZED_WIDTH : PANEL_WIDTH;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const rect = panel.getBoundingClientRect();
let newLeft = rect.left;
let newTop = rect.top;
if (newLeft < SAFE_MARGIN) newLeft = SAFE_MARGIN;
else if (newLeft + panelWidth > viewportWidth - SAFE_MARGIN) {
newLeft = viewportWidth - panelWidth - SAFE_MARGIN;
}
if (newTop < SAFE_MARGIN) newTop = SAFE_MARGIN;
else if (newTop + panelHeight > viewportHeight - SAFE_MARGIN) {
newTop = viewportHeight - panelHeight - SAFE_MARGIN;
}
if (newLeft !== rect.left || newTop !== rect.top) {
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.transform = 'translateZ(0)';
}
}
// 拖动事件
function initDragEvents(panel, handle) {
let panelInitialRect;
handle.addEventListener('mousedown', (e) => {
if (panelHidden) return;
e.preventDefault();
e.stopPropagation();
isDragging = true;
panelInitialRect = panel.getBoundingClientRect();
dragOffsetX = e.clientX - panelInitialRect.left;
dragOffsetY = e.clientY - panelInitialRect.top;
document.body.style.cursor = 'grabbing';
handle.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
panel.style.boxShadow = '0 6px 25px rgba(0,0,0,0.15)';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging || panelHidden) return;
e.preventDefault();
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
panel.style.transform = `translate(
${newLeft - panelInitialRect.left}px,
${newTop - panelInitialRect.top}px
) translateZ(0)`;
});
});
function endDrag() {
if (isDragging) {
isDragging = false;
cancelAnimationFrame(rafId);
rafId = null;
document.body.style.cursor = '';
handle.style.cursor = 'move';
document.body.style.userSelect = '';
panel.style.boxShadow = '0 4px 20px rgba(0,0,0,0.12)';
const rect = panel.getBoundingClientRect();
panel.style.left = `${rect.left}px`;
panel.style.top = `${rect.top}px`;
panel.style.transform = 'translateZ(0)';
checkBounds(panel);
}
}
document.addEventListener('mouseup', endDrag);
document.addEventListener('mouseleave', endDrag);
}
// 停止滚动函数
function stopScrolling() {
if (isScrolling && scrollInterval) {
clearInterval(scrollInterval);
scrollInterval = null;
}
isScrolling = false;
const startBtn = document.getElementById('startScrollBtn');
const stopBtn = document.getElementById('stopScrollBtn');
if (startBtn) startBtn.disabled = false;
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.style.opacity = '0.7';
}
}
// 主题更新函数
function updateTheme() {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const panel = document.getElementById('scrollControlPanel');
if (!panel) return;
const titleBar = panel.querySelector('.drag-handle');
const title = panel.querySelector('h3');
const buttons = panel.querySelectorAll('.drag-handle button');
const labels = panel.querySelectorAll('label');
const selects = panel.querySelectorAll('select');
const startBtn = document.getElementById('startScrollBtn');
const stopBtn = document.getElementById('stopScrollBtn');
const speedSlider = panel.querySelector('input[type="range"]');
if (isDarkMode) {
panel.style.backgroundColor = 'rgba(28, 28, 30, 0.98)';
panel.style.border = '1px solid rgba(70, 70, 75, 0.5)';
titleBar.style.backgroundColor = 'rgba(44, 44, 46, 0.9)';
title.style.color = '#f5f5f7';
buttons.forEach(btn => btn.style.color = '#d2d2d7');
labels.forEach(label => label.style.color = '#e4e4e7');
selects.forEach(select => {
select.style.backgroundColor = 'rgba(44, 44, 46, 0.8)';
select.style.color = '#e4e4e7';
select.style.borderColor = 'rgba(70, 70, 75, 0.8)';
});
speedSlider.style.backgroundColor = 'rgba(70, 70, 75, 0.5)';
speedSlider.style.setProperty('-webkit-slider-thumb', `
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
transition: all 0.2s ease;
`);
if (startBtn) startBtn.style.backgroundColor = '#43a047';
if (stopBtn) stopBtn.style.backgroundColor = '#e53935';
} else {
panel.style.backgroundColor = 'rgba(255, 255, 255, 0.98)';
panel.style.border = '1px solid rgba(220, 220, 225, 0.8)';
titleBar.style.backgroundColor = 'rgba(249, 249, 250, 0.9)';
title.style.color = '#1d1d1f';
buttons.forEach(btn => btn.style.color = '#6e6e73');
labels.forEach(label => label.style.color = '#1d1d1f');
selects.forEach(select => {
select.style.backgroundColor = 'rgba(249, 249, 250, 0.8)';
select.style.color = '#1d1d1f';
select.style.borderColor = 'rgba(220, 220, 225, 0.8)';
});
speedSlider.style.backgroundColor = 'rgba(220, 220, 225, 0.8)';
speedSlider.style.setProperty('-webkit-slider-thumb', `
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
transition: all 0.2s ease;
`);
if (startBtn) startBtn.style.backgroundColor = '#4CAF50';
if (stopBtn) stopBtn.style.backgroundColor = '#f44336';
}
}
// 更新滚动间隔
function updateScrollInterval() {
if (scrollInterval) {
clearInterval(scrollInterval);
scrollInterval = null;
}
startScrolling();
}
// 滚动逻辑实现
function startScrolling() {
if (scrollInterval) return;
scrollInterval = setInterval(() => {
const currentPos = window.scrollY;
const windowHeight = window.innerHeight;
const docHeight = Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
);
const atBottom = currentPos + windowHeight >= docHeight - 50;
const atTop = currentPos <= 50;
let newPos = currentPos;
if (scrollDirection === 'down') {
newPos = currentPos + scrollSpeed;
if (atBottom) {
switch (bottomAction) {
case 'jump':
newPos = 0;
break;
case 'reverse':
scrollDirection = 'up';
newPos = currentPos - scrollSpeed;
break;
case 'stop':
stopScrolling();
return;
}
}
} else {
newPos = currentPos - scrollSpeed;
if (atTop) {
switch (bottomAction) {
case 'jump':
newPos = docHeight - windowHeight;
break;
case 'reverse':
scrollDirection = 'down';
newPos = currentPos + scrollSpeed;
break;
case 'stop':
stopScrolling();
return;
}
}
}
window.scrollTo(0, newPos);
}, 20);
}
// 初始化
if (document.readyState === 'complete' || document.readyState === 'interactive') {
createControlPanel();
} else {
document.addEventListener('DOMContentLoaded', createControlPanel);
setTimeout(createControlPanel, 3000);
}
})();