您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
复制 pub.xdtech.top 页面上的特定数据,支持拖动按钮,并拦截修改 API 请求,兼容iPhone
// ==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(); } })();