// ==UserScript==
// @name 高级自动滚动控制器
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 固定面板高度,解决拖动晃动问题,适配日夜模式的自动滚动工具
// @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 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 = 180;
// 创建控制界面 - 固定高度设置
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;
touch-action: none;
transform: translateZ(0); /* 硬件加速,减少拖动卡顿 */
transition: width 0.2s ease, box-shadow 0.2s ease;
overflow: hidden; /* 防止内容溢出固定高度 */
`;
// 标题栏(拖动把手)
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 minBtn = document.createElement('button');
minBtn.textContent = '−';
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));
titleBar.append(title, minBtn);
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%;
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', () => {
checkBounds(panel);
});
return panel;
}
// 检查面板位置(基于固定高度计算边界)
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) => {
e.preventDefault();
e.stopPropagation();
isDragging = true;
// 缓存初始位置,避免拖动中频繁查询DOM
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) return;
e.preventDefault();
// 取消上一帧请求,避免重复绘制导致的晃动
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
// 基于初始位置和鼠标偏移计算新位置(误差最小)
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
// 仅用transform更新位置,避免top/left切换导致的布局抖动
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)';
// 固化位置到top/left,避免transform累积误差
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 toggleMinimize(panel) {
const content = document.getElementById('panelContent');
const minBtn = panel.querySelector('button');
const title = panel.querySelector('h3');
panelMinimized = !panelMinimized;
if (panelMinimized) {
// 最小化:固定小尺寸高度和宽度
content.style.display = 'none';
panel.style.width = `${MINIMIZED_WIDTH}px`;
panel.style.height = `${MINIMIZED_HEIGHT}px`; // 最小化固定高度
panel.style.paddingRight = '10px';
minBtn.textContent = '+';
title.style.fontSize = '14px';
} 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';
}
// 切换后检查位置,适配不同固定高度
checkBounds(panel);
}
// 停止滚动函数
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 minBtn = panel.querySelector('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';
minBtn.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';
minBtn.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);
}
// 初始化(确保DOM加载完成)
if (document.readyState === 'complete' || document.readyState === 'interactive') {
createControlPanel();
} else {
document.addEventListener('DOMContentLoaded', createControlPanel);
setTimeout(createControlPanel, 3000); // 超时保障,防止面板未加载
}
})();