// ==UserScript==
// @name 卓而越助手
// @namespace http://tampermonkey.net/
// @version 2.4
// @license MIT
// @description 复制 pub.xdtech.top 页面上的特定数据,支持拖动按钮,并拦截修改 API 请求,兼容iPhone
// @author moxia
// @match https://pub.xdtech.top/*
// @grant GM_setClipboard
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// 全局标志位,确保 init 只执行一次
let isInitialized = false;
// ---------------------------
// 1. 添加自定义提示样式
// ---------------------------
const toastStyle = `
.custom-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px 25px;
border-radius: 4px;
z-index: 10001;
font-size: 16px;
text-align: center;
animation: fadeInOut 2s ease-in-out;
}
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
/* 为iOS设备添加的辅助样式 */
.copy-textarea {
position: absolute;
top: -9999px;
left: -9999px;
opacity: 0;
/* iOS默认字体大小为16px */
font-size: 16px;
z-index: -1;
width: 0;
height: 0;
padding: 0;
margin: 0;
border: none;
pointer-events: none;
}
`;
if (typeof GM_addStyle !== 'undefined') {
GM_addStyle(toastStyle);
} else {
const styleElement = document.createElement('style');
styleElement.textContent = toastStyle;
document.head.appendChild(styleElement);
}
// ---------------------------
// 2. 自定义提示函数
// ---------------------------
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = 'custom-toast';
toast.textContent = message;
toast.style.backgroundColor = type === 'error'
? 'rgba(220, 53, 69, 0.9)'
: 'rgba(40, 167, 69, 0.9)';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
// ---------------------------
// 3. 创建可拖动的复制按钮
// ---------------------------
function createDraggableButton() {
const copyButton = document.createElement('button');
copyButton.textContent = '复制文字';
copyButton.id = 'copyTextButton';
copyButton.style.cssText = `
position: fixed !important;
bottom: 5px !important;
right: 20px !important;
z-index: 10000 !important;
padding: 10px 20px !important;
background-color: #007bff !important;
color: white !important;
border: none !important;
border-radius: 5px !important;
cursor: pointer !important;
font-family: Arial, sans-serif !important;
font-size: 14px !important;
font-weight: bold !important;
outline: none !important;
user-select: none !important; /* 防止拖动时选中按钮文字 */
-webkit-user-select: none !important; /* 兼容Safari */
-webkit-touch-callout: none !important; /* 禁用iOS上的长按菜单 */
-webkit-tap-highlight-color: transparent !important; /* 禁用iOS上的点击高亮 */
pointer-events: auto !important;
`;
// 初始化状态
let isDragging = false;
let hasDragged = false; // 标记是否发生过拖动
let dragStartTime = 0; // 记录开始拖动的时间
let offsetX, offsetY;
let lastTouchEnd = 0; // 用于防止双击缩放
// 阻止页面双击缩放
document.addEventListener('touchend', function(event) {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
// 限制按钮在可视区域内的函数
function keepButtonInViewport(x, y, buttonWidth, buttonHeight) {
const minPadding = 10; // 设置按钮与边界的最小距离
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 限制 X 坐标范围
x = Math.max(minPadding, x);
x = Math.min(viewportWidth - buttonWidth - minPadding, x);
// 限制 Y 坐标范围
y = Math.max(minPadding, y);
y = Math.min(viewportHeight - buttonHeight - minPadding, y);
return { x, y };
}
// PC 端:鼠标事件
copyButton.addEventListener('mousedown', (e) => {
isDragging = true;
hasDragged = false; // 初始化拖动标记
dragStartTime = Date.now();
offsetX = e.clientX - copyButton.getBoundingClientRect().left;
offsetY = e.clientY - copyButton.getBoundingClientRect().top;
copyButton.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
// 只有移动超过5px才标记为拖动
if (!hasDragged &&
(Math.abs(e.clientX - (copyButton.getBoundingClientRect().left + offsetX)) > 5 ||
Math.abs(e.clientY - (copyButton.getBoundingClientRect().top + offsetY)) > 5)) {
hasDragged = true;
}
const buttonWidth = copyButton.offsetWidth;
const buttonHeight = copyButton.offsetHeight;
const rawX = e.clientX - offsetX;
const rawY = e.clientY - offsetY;
// 应用限制,保持按钮在可视区域内
const pos = keepButtonInViewport(rawX, rawY, buttonWidth, buttonHeight);
copyButton.style.left = `${pos.x}px`;
copyButton.style.top = `${pos.y}px`;
copyButton.style.right = 'unset';
copyButton.style.bottom = 'unset';
}
});
document.addEventListener('mouseup', (e) => {
if (isDragging) {
isDragging = false;
copyButton.style.cursor = 'pointer';
// 如果没拖动过,且时间小于200ms,视为点击
if (!hasDragged && (Date.now() - dragStartTime < 200)) {
copyData(e);
}
hasDragged = false;
}
});
// 手机端:触摸事件(修改这部分以解决iPhone兼容性问题)
copyButton.addEventListener('touchstart', (e) => {
isDragging = true;
hasDragged = false; // 初始化拖动标记
dragStartTime = Date.now();
const touch = e.touches[0];
offsetX = touch.clientX - copyButton.getBoundingClientRect().left;
offsetY = touch.clientY - copyButton.getBoundingClientRect().top;
// 不阻止默认行为,避免影响点击
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (isDragging) {
const touch = e.touches[0];
// 只有移动超过5px才标记为拖动
if (!hasDragged &&
(Math.abs(touch.clientX - (copyButton.getBoundingClientRect().left + offsetX)) > 5 ||
Math.abs(touch.clientY - (copyButton.getBoundingClientRect().top + offsetY)) > 5)) {
hasDragged = true;
// 对于确认是拖动的情况,阻止默认行为(防止页面滚动)
e.preventDefault();
}
if (hasDragged) {
const buttonWidth = copyButton.offsetWidth;
const buttonHeight = copyButton.offsetHeight;
const rawX = touch.clientX - offsetX;
const rawY = touch.clientY - offsetY;
// 应用限制,保持按钮在可视区域内
const pos = keepButtonInViewport(rawX, rawY, buttonWidth, buttonHeight);
copyButton.style.left = `${pos.x}px`;
copyButton.style.top = `${pos.y}px`;
copyButton.style.right = 'unset';
copyButton.style.bottom = 'unset';
}
}
}, { passive: false });
document.addEventListener('touchend', (e) => {
if (isDragging) {
// 如果没拖动过,且时间小于200ms,视为点击
if (!hasDragged && (Date.now() - dragStartTime < 200)) {
copyData(e);
}
isDragging = false;
hasDragged = false;
}
});
// 为iOS设备添加特殊的点击事件处理
if (isIOS()) {
copyButton.addEventListener('click', (e) => {
// 阻止默认行为,防止页面跳动
e.preventDefault();
e.stopPropagation();
// iOS上点击事件单独处理
copyData(e);
});
}
document.body.appendChild(copyButton);
}
// ---------------------------
// 检测是否为iOS设备
// ---------------------------
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// ---------------------------
// 4. 复制数据的核心逻辑(修复iPhone兼容性问题)
// ---------------------------
function copyData(e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
try {
const activeSlide = document.querySelector('.swiper-slide-active');
if (!activeSlide) {
showToast('未找到复制目标', 'error');
return;
}
// 提取 .hint 内容
const hintText = activeSlide.querySelector('.hint')?.textContent.trim() || '';
// 提取 .options 内容(精确控制顿号)
const options = Array.from(activeSlide.querySelectorAll('.options div'))
.map(div => {
const icon = (div.querySelector('.icon')?.textContent || '').trim();
const desc = (div.querySelector('.desc')?.textContent || '').trim();
return icon && desc ? `${icon}、${desc}` : ''; // 仅当两者都存在时添加顿号
})
.filter(text => text) // 过滤空选项
.join('\n'); // 选项间换行分隔
// 组合文本
const finalText = `${hintText}${options ? '\n\n' + options : ''}\n\n选择哪个?`;
// 为iOS设备使用更可靠的复制方法
if (isIOS()) {
copyTextIOSCompatible(finalText);
}
// 非iOS设备使用原有方法
else if (typeof GM_setClipboard !== 'undefined') {
GM_setClipboard(finalText);
showToast('复制成功');
} else {
navigator.clipboard.writeText(finalText)
.then(() => showToast('复制成功'))
.catch(() => {
fallbackCopyToClipboard(finalText);
});
}
} catch (error) {
console.error('复制失败:', error);
showToast('复制失败', 'error');
}
}
// ---------------------------
// iOS兼容的复制方法(修复页面跳动问题)
// ---------------------------
function copyTextIOSCompatible(text) {
// 移除之前可能存在的任何textarea
const oldTextarea = document.querySelector('.copy-textarea');
if (oldTextarea) {
oldTextarea.remove();
}
// 创建textarea并添加特殊类,避免影响页面布局
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.className = 'copy-textarea';
textarea.contentEditable = true;
textarea.readOnly = false;
// 使用绝对定位并放在视口之外,避免页面跳动
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
textarea.style.width = '0';
textarea.style.height = '0';
textarea.style.padding = '0';
textarea.style.border = 'none';
textarea.style.margin = '0';
document.body.appendChild(textarea);
// 将textarea完全放到DOM中后再选择文本,防止视图滚动
setTimeout(() => {
try {
textarea.focus({preventScroll: true});
textarea.select();
// 尝试复制
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => {
showToast('复制成功');
setTimeout(() => textarea.remove(), 50);
})
.catch(() => {
fallbackCopyToClipboard(text, textarea);
});
} else {
fallbackCopyToClipboard(text, textarea);
}
} catch (err) {
fallbackCopyToClipboard(text, textarea);
}
}, 0);
}
// ---------------------------
// 后备复制方法(优化防止页面跳动)
// ---------------------------
function fallbackCopyToClipboard(text, existingTextarea) {
try {
const textarea = existingTextarea || document.createElement('textarea');
if (!existingTextarea) {
textarea.value = text;
textarea.className = 'copy-textarea';
document.body.appendChild(textarea);
}
// 防止文本选择时页面滚动
const previousScrollPosition = window.scrollY;
textarea.focus({preventScroll: true});
textarea.select();
// 如果页面发生了滚动,恢复原来的位置
if (window.scrollY !== previousScrollPosition) {
window.scrollTo(window.scrollX, previousScrollPosition);
}
const successful = document.execCommand('copy');
if (successful) {
showToast('复制成功');
} else {
showToast('复制失败,请手动复制', 'error');
}
} catch (err) {
console.error('复制失败:', err);
showToast('复制失败,请手动复制', 'error');
} finally {
if (existingTextarea) {
setTimeout(() => {
existingTextarea.remove();
}, 50);
}
}
}
// ---------------------------
// 5. XMLHttpRequest 拦截逻辑
// ---------------------------
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (url && url.includes && url.includes('https://pub.xdapi.top/zhuoyue/api/v1/tiku/dayexercise/1626/dayrank')) {
const modifiedUrl = new URL(url);
modifiedUrl.searchParams.set('size', '10000');
arguments[1] = modifiedUrl.toString();
console.log('已修改请求参数:', arguments[1]);
}
originalOpen.apply(this, arguments);
};
// ---------------------------
// 6. 初始化脚本
// ---------------------------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
if (isInitialized) return;
isInitialized = true;
createDraggableButton();
}
})();