// ==UserScript== // @name 连续下一页 - 优化版 // @namespace https://scriptcat.org/zh-CN/scripts/create // @version 1.1.0 // @description 智能预加载下一页,滚动到底部自动拼接,无感加载,右下角显示加载进度。 // @author YZD // @license MIT // @match *://*/* // @grant GM_addStyle // @run-at document-end // @icon https://images.icon-icons.com/2063/PNG/512/download_down_arrow_browser_webpage_icon_124661.png // ==/UserScript== (function () { 'use strict'; // ─── 状态面板 ─────────────────────────────────────────────── const panel = document.createElement('div'); panel.id = 'np-panel'; panel.innerHTML = `
连续下一页
第 1 页
等待滚动...
`; document.body.appendChild(panel); GM_addStyle(` #np-panel { position: fixed; bottom: 12px; right: 12px; width: 180px; background: rgba(255,255,255,0.96); border-radius: 10px; box-shadow: 0 4px 18px rgba(0,0,0,0.18); font-family: "Segoe UI", system-ui, sans-serif; font-size: 12px; color: #333; z-index: 2147483647; user-select: none; } .np-header { display: flex; align-items: center; gap: 6px; padding: 7px 10px; background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; border-radius: 10px 10px 0 0; cursor: move; } .np-title { flex: 1; font-size: 12px; font-weight: 600; } .np-toggle { cursor: pointer; font-size: 10px; padding: 2px 4px; } .np-content { padding: 8px 10px 10px; } .np-page { font-size: 11px; font-weight: bold; color: #667eea; margin-bottom: 4px; text-align: center; } .np-status { font-size: 11px; text-align: center; color: #555; line-height: 1.5; } .np-status.loading { color: #667eea; } .np-status.success { color: #2c9f67; } .np-status.error { color: #e05f66; } .np-divider { border: none; border-top: 2px dashed #667eea44; margin: 16px 0 4px; } .np-page-label { text-align: center; font-size: 11px; color: #764ba2; font-weight: bold; margin: 4px 0 8px; } `); // ─── 折叠 ──────────────────────────────────────────────────── const toggleBtn = panel.querySelector('.np-toggle'); const content = panel.querySelector('.np-content'); let collapsed = false; toggleBtn.addEventListener('click', () => { collapsed = !collapsed; content.style.display = collapsed ? 'none' : 'block'; toggleBtn.textContent = collapsed ? '▲' : '▼'; }); // ─── 拖拽 ──────────────────────────────────────────────────── const header = panel.querySelector('.np-header'); header.addEventListener('mousedown', (e) => { const sx = e.clientX, sy = e.clientY; const sr = parseInt(getComputedStyle(panel).right, 10) || 12; const sb = parseInt(getComputedStyle(panel).bottom, 10) || 12; panel.style.cursor = 'grabbing'; const move = (e) => { panel.style.right = `${Math.max(0, sr + sx - e.clientX)}px`; panel.style.bottom = `${Math.max(0, sb - sy + e.clientY)}px`; }; const up = () => { panel.style.cursor = 'default'; document.removeEventListener('mousemove', move); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up, { once: true }); }); // ─── 工具函数 ───────────────────────────────────────────────── const statusEl = panel.querySelector('.np-status'); const pageEl = panel.querySelector('.np-page'); let pageCount = 1; function setStatus(text, cls = '') { statusEl.textContent = text; statusEl.className = 'np-status ' + cls; } function setPage(n) { pageCount = n; pageEl.textContent = `第 ${n} 页`; } // ─── 下一页链接检测(多策略)──────────────────────────────── const NEXT_SELECTORS = [ 'a[rel="next"]', 'link[rel="next"]', '.next a', '.next-page a', '.pagination-next a', 'a.next', 'a.next-page', 'a.nextpage', '[class*="next"] a', '[aria-label="Next page"]', '[aria-label="下一页"]', '[aria-label="next"]', 'a[href*="page="]:last-of-type', ]; const NEXT_TEXT = ['下一页','下一篇','下页','next','next page','»','›','>>']; function findNextLink() { // 1. 标准选择器 for (const sel of NEXT_SELECTORS) { const el = document.querySelector(sel); if (el && el.href) return el.href; } // 2. 按文字内容匹配所有 const anchors = Array.from(document.querySelectorAll('a[href]')); for (const a of anchors) { const txt = a.textContent.trim().toLowerCase(); if (NEXT_TEXT.includes(txt) && a.href) return a.href; } // 3. 按当前 URL 自动推断页码 return guessNextUrl(); } function guessNextUrl() { const url = new URL(location.href); // ?page=N 或 ?p=N for (const key of ['page', 'p', 'paged', 'pg', 'pagenum', 'pageno']) { if (url.searchParams.has(key)) { const cur = parseInt(url.searchParams.get(key), 10); if (!isNaN(cur)) { url.searchParams.set(key, cur + 1); return url.toString(); } } } // 路径中的 /page/N/ 或 /p/N/ const pathMatch = url.pathname.match(/\/(page|p)\/(\d+)(\/?)$/i); if (pathMatch) { const next = parseInt(pathMatch[2], 10) + 1; url.pathname = url.pathname.replace(pathMatch[0], `/${pathMatch[1]}/${next}${pathMatch[3]}`); return url.toString(); } // 路径末尾 -N 或 _N const numMatch = url.pathname.match(/[-_](\d+)(\.html?)?$/i); if (numMatch) { const next = parseInt(numMatch[1], 10) + 1; url.pathname = url.pathname.replace(numMatch[0], `-${next}${numMatch[2] || ''}`); return url.toString(); } return null; } // ─── 内容容器检测 ───────────────────────────────────────────── const CONTENT_SELECTORS = [ 'article', 'main', '[role="main"]', '.post-content', '.article-content', '.entry-content', '.content', '#content', '#main', '.main', '.post', '.article', ]; function findContainer(doc) { for (const sel of CONTENT_SELECTORS) { const el = doc.querySelector(sel); if (el) return el; } return doc.body; } // ─── 拼接逻辑 ───────────────────────────────────────────────── let currentNextUrl = null; let isFetching = false; function fetchAndAppend(url) { if (isFetching) return; isFetching = true; setStatus('正在加载...', 'loading'); fetch(url, { credentials: 'include' }) .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.text(); }) .then((html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const srcEl = findContainer(doc); const dstEl = findContainer(document); // 添加分隔线 + 页码标签 const hr = document.createElement('hr'); hr.className = 'np-divider'; const label = document.createElement('div'); label.className = 'np-page-label'; label.textContent = `—— 第 ${pageCount + 1} 页 ——`; dstEl.appendChild(hr); dstEl.appendChild(label); // 拼接内容 Array.from(srcEl.childNodes).forEach((node) => { dstEl.appendChild(node.cloneNode(true)); }); setPage(pageCount + 1); setStatus('已拼接', 'success'); // 更新下一页 URL(从新文档中重新查找) // 临时替换 document.querySelector 上下文 const oldBody = document.body; const tmpDiv = document.createElement('div'); tmpDiv.innerHTML = doc.body.innerHTML; currentNextUrl = (() => { for (const sel of NEXT_SELECTORS) { const el = tmpDiv.querySelector(sel); if (el && el.href) return el.href; } const anchors = Array.from(tmpDiv.querySelectorAll('a[href]')); for (const a of anchors) { const txt = a.textContent.trim().toLowerCase(); if (NEXT_TEXT.includes(txt) && a.href) return a.href; } return null; })(); if (!currentNextUrl) { // 降级:继续用 URL 推断 currentNextUrl = (() => { const u = new URL(url); for (const key of ['page','p','paged','pg','pagenum','pageno']) { if (u.searchParams.has(key)) { const cur = parseInt(u.searchParams.get(key), 10); if (!isNaN(cur)) { u.searchParams.set(key, cur + 1); return u.toString(); } } } return null; })(); } if (!currentNextUrl) setStatus('已到最后一页', 'success'); }) .catch((err) => { setStatus('加载失败: ' + err.message, 'error'); }) .finally(() => { isFetching = false; }); } // ─── 滚动监听 ───────────────────────────────────────────────── let scrollTimer = null; window.addEventListener('scroll', () => { clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { const distToBottom = document.body.scrollHeight - window.innerHeight - window.scrollY; if (distToBottom > 300) return; const url = currentNextUrl || findNextLink(); if (!url) { setStatus('未找到下一页', 'error'); return; } if (url === location.href) { setStatus('已是最后一页', 'success'); return; } currentNextUrl = url; fetchAndAppend(url); }, 200); }); })();