您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
实时获取并累计牌谱链接
// ==UserScript== // @name 天凤牌谱链接提取器 // @namespace http://tampermonkey.net/ // @version 0.6.1 // @description 实时获取并累计牌谱链接 // @author 馒头卡 // @match *://tenhou.net/3/* // @match *://tenhou.net/4/* // @match *://nodocchi.moe/tenhoulog/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // 全局配置 const CONFIG = { STYLES: { panel: { position: 'fixed', top: '20px', right: '20px', background: 'white', padding: '15px', border: '2px solid #ccc', borderRadius: '5px', zIndex: '9999', maxHeight: '80vh', overflowY: 'auto', boxShadow: '0 0 10px rgba(0,0,0,0.2)', minWidth: '450px' }, title: { margin: '0 0 10px', fontSize: '16px', fontWeight: 'bold', color: '#333' }, status: { margin: '5px 0', fontSize: '13px', color: '#666' }, textarea: { width: '100%', height: '200px', margin: '10px 0', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', fontFamily: 'monospace', resize: 'vertical', boxSizing: 'border-box' }, button: { padding: '8px 15px', cursor: 'pointer', border: 'none', borderRadius: '4px', transition: 'all 0.2s ease', flex: 1 } }, SELECTORS: { 'nodocchi.moe': { '/tenhoulog/': { scrollContainer: null, selector: 'td.td_rule > a[href^="http://tenhou.net/0/?log="]', transform: link => link.replace(/&/g, '&'), scrollConfig: { step: 1500, }, } }, 'tenhou.net': { '/3/': { scrollContainer: '#hlist', selector: '#hlist > div > a[href^="https://tenhou.net/0/"]', scrollConfig: { step: 600, }, }, '/4/': { scrollContainer: '#c32', selector: '#c32 > div > a.s9.s7.bt3', scrollConfig: { step: 600, }, } } }, SCROLL: { step: 800, interval: 1000, timeout: 120000, retryLimit: 3 }, COLORS: { success: '#4CAF50', error: '#f44336', info: '#2196F3', secondary: '#9C27B0' } }; const createElement = (tag, styles = {}, attributes = {}) => { const el = document.createElement(tag); Object.assign(el.style, styles); Object.entries(attributes).forEach(([k, v]) => el.setAttribute(k, v)); return el; }; class LinkExtractor { constructor() { this.isProcessing = false; this.collectedLinks = new Set(); // 新增:用于存储去重后的链接 this.initPanel(); } initPanel() { document.getElementById('th-link-panel')?.remove(); this.panel = createElement('div', CONFIG.STYLES.panel, { id: 'th-link-panel' }); this.title = createElement('div', CONFIG.STYLES.title); this.statusBar = createElement('div', CONFIG.STYLES.status); this.textarea = createElement('textarea', CONFIG.STYLES.textarea, { readonly: true }); const btnContainer = createElement('div', { display: 'flex', gap: '10px', marginTop: '10px' }); const buttons = [ { text: '开始获取', color: CONFIG.COLORS.info, action: () => this.startProcess() }, { text: '复制链接', color: CONFIG.COLORS.success, action: () => this.copyLinks() }, { text: '下载牌谱', color: CONFIG.COLORS.secondary, action: () => this.downloadLinks() } ]; this.controlButtons = buttons.map(config => { const btn = createElement('button', { ...CONFIG.STYLES.button, background: config.color }, { type: 'button' } ); btn.textContent = config.text; btn.addEventListener('click', config.action); return btn; }); btnContainer.append(...this.controlButtons); this.panel.append( this.title, this.statusBar, this.textarea, btnContainer ); document.body.appendChild(this.panel); } showStatus(text, color = CONFIG.COLORS.info) { this.statusBar.textContent = text; this.statusBar.style.color = color; } async startProcess() { if (this.isProcessing) return; try { this.toggleProcessing(true); const pageConfig = this.getCurrentPageConfig(); const scrollContainer = pageConfig.container; const scrollConfig = { ...CONFIG.SCROLL, ...pageConfig.scrollConfig }; await this.autoScroll(scrollContainer, scrollConfig); this.showStatus(`完成!共获取 ${this.collectedLinks.size} 个链接`, CONFIG.COLORS.success); } catch (error) { this.showStatus(`操作失败: ${error.message}`, CONFIG.COLORS.error); } finally { this.toggleProcessing(false); } } autoScroll(scrollContainer, scrollConfig) { return new Promise((resolve) => { const isDocumentContainer = scrollContainer === document.documentElement; let lastScrollPos = isDocumentContainer ? window.pageYOffset : scrollContainer.scrollTop; let retryCount = 0; const startTime = Date.now(); const getScrollPosition = () => { return isDocumentContainer ? window.pageYOffset + window.innerHeight : scrollContainer.scrollTop + scrollContainer.clientHeight; }; // 新增:实时提取链接的方法 const processLinks = () => { const newLinks = this.extractLinks(this.getCurrentPageConfig().selectorConfig); const prevSize = this.collectedLinks.size; newLinks.forEach(link => this.collectedLinks.add(link)); if (this.collectedLinks.size > prevSize) { this.updateUI(); // this.showStatus(`已累计发现 ${this.collectedLinks.size} 个链接`, CONFIG.COLORS.info); } }; const scrollStep = () => { if (Date.now() - startTime > scrollConfig.timeout) { this.showStatus(`滚动超时(${scrollConfig.timeout}ms)`, CONFIG.COLORS.error); resolve(); return; } processLinks(); // 每次滚动前先处理现有链接 const currentPosition = getScrollPosition(); const totalHeight = isDocumentContainer ? document.documentElement.scrollHeight : scrollContainer.scrollHeight; if (currentPosition >= totalHeight - 10) { if (totalHeight === (isDocumentContainer ? lastScrollPos + window.innerHeight : lastScrollPos)) { if (++retryCount >= scrollConfig.retryLimit) { resolve(); return; } } else { retryCount = 0; lastScrollPos = isDocumentContainer ? totalHeight - window.innerHeight : totalHeight; } } if (isDocumentContainer) { window.scrollBy(0, scrollConfig.step); } else { scrollContainer.scrollTop += scrollConfig.step; } setTimeout(scrollStep, scrollConfig.interval); }; this.showStatus('采集中,请勿操作页面...'); processLinks(); // 初始加载时处理一次 scrollStep(); }); } getCurrentPageConfig() { const { hostname, pathname } = window.location; for (const [domain, domainConfig] of Object.entries(CONFIG.SELECTORS)) { if (!hostname.includes(domain)) continue; const pathConfig = this.findPathConfig(domainConfig, pathname); if (pathConfig) { let container = document.documentElement; if (pathConfig.scrollContainer) { container = document.querySelector(pathConfig.scrollContainer) || container; } return { container: container, scrollConfig: pathConfig.scrollConfig || {}, selectorConfig: pathConfig }; } } return { container: document.documentElement, scrollConfig: {}, selectorConfig: null }; } toggleProcessing(processing) { this.isProcessing = processing; this.controlButtons.forEach(btn => btn.disabled = processing); } extractLinks(selectorConfig) { if (!selectorConfig) return []; const elements = document.querySelectorAll(selectorConfig.selector); let links = Array.from(elements).map(a => a.href); return selectorConfig.transform ? links.map(selectorConfig.transform) : links; } updateUI(links) { this.title.textContent = `累计发现 ${this.collectedLinks.size} 个牌谱链接`; this.textarea.value = Array.from(this.collectedLinks).join('\n'); } copyLinks() { this.textarea.value = Array.from(this.collectedLinks).join('\n'); // 确保复制最新数据 document.execCommand('copy'); this.showStatus('链接已复制到剪贴板', CONFIG.COLORS.success); } downloadLinks() { const content = Array.from(this.collectedLinks).join('\n'); if (!content) return; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = createElement('a', {}, { href: url, download: `牌谱-${new Date().toLocaleString().replace(/[/:]/g, '-')}.txt` }); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showStatus('文件下载已开始', CONFIG.COLORS.secondary); } findPathConfig(config, currentPath) { for (const [pathPrefix, pathConfig] of Object.entries(config)) { if (currentPath.startsWith(pathPrefix)) { return pathConfig; } } return null; } } window.addEventListener('load', () => new LinkExtractor()); })();