您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
引入悬浮球三态逻辑(收起/展开/面板打开),增加超时自动收起功能,交互体验登峰造极。
// ==UserScript== // @name 网页资源检测工具 (终极优化版) // @namespace https://viayoo.com/ // @version 1.5 // @description 引入悬浮球三态逻辑(收起/展开/面板打开),增加超时自动收起功能,交互体验登峰造极。 // @author Doubao (Optimized by Gemini) // @run-at document-idle // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_download // @grant GM_openInTab // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_setClipboard // ==/UserScript== (function() { 'use strict'; // --- CSS样式 --- GM_addStyle(` /* 悬浮球基础样式,默认完全隐藏 */ #resourceDetectorBall { position: fixed; top: 80px; left: -55px; width: 55px; height: 42px; border-radius: 0 21px 21px 0; background: linear-gradient(135deg, #4285f4, #34a853); color: white; display: flex; align-items: center; justify-content: flex-end; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 9999; cursor: pointer; transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1); font-weight: 500; font-size: 14px; text-align: center; padding-right: 8px; user-select: none; border: 2px solid white; border-left: none; } /* 新增:收起状态 (只露出15px) */ #resourceDetectorBall.retracted { left: -40px; /* 55px宽度 - 15px露出 = 40px隐藏 */ } /* 新增:展开状态 */ #resourceDetectorBall.expanded { left: 0; } #resourceDetectorBall:hover { transform: scale(1.05); } #resourceBallBadge { position: absolute; top: -1px; right: -1px; width: 10px; height: 10px; background-color: #FF3B30; border-radius: 50%; border: 2px solid #fff; display: none; } .tab-badge { display: inline-block; min-width: 22px; height: 22px; border-radius: 11px; background-color: #FF3B30; color: white; font-size: 12px; line-height: 22px; text-align: center; margin-left: 8px; font-weight: bold; } #resourceDetectorPanel { position: fixed; top: 65px; left: 15px; width: 320px; background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(12px); border-radius: 14px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); z-index: 9998; display: none; overflow: hidden; border: 1px solid rgba(230, 236, 245, 0.7); opacity: 0; transform: translateX(-20px) scale(0.95); transition: opacity 0.3s ease, transform 0.3s ease; max-height: 75vh; font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; } #resourceDetectorPanel.active { display: block; opacity: 1; transform: translateX(0) scale(1); } .panel-header { padding: 14px; font-size: 17px; font-weight: 600; color: #2c3e50; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f3f7; background-color: rgba(245, 248, 255, 0.8); } .close-btn { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #f1f3f9; color: #7f8c8d; font-size: 17px; cursor: pointer; transition: all 0.2s; } .close-btn:hover { background: #e5e9f2; color: #e74c3c; } .category-tabs { display: flex; background: #f5f8ff; border-bottom: 1px solid #eef2f7; padding: 0 8px; } .tab-btn { flex: 1; text-align: center; padding: 12px 8px; font-size: 13px; font-weight: 500; color: #6b7c93; cursor: pointer; transition: all 0.2s ease; border-bottom: 3px solid transparent; display: flex; justify-content: center; align-items: center; } .tab-btn.active { color: #4285f4; border-bottom-color: #4285f4; background: rgba(66, 133, 244, 0.05); } .tab-content { max-height: calc(75vh - 115px); overflow-y: auto; padding: 8px; } .resource-list { list-style: none; padding: 0; margin: 0; } .resource-item { padding: 10px 14px; border-bottom: 1px solid #f0f3f7; display: flex; align-items: center; transition: background-color 0.2s; } .resource-item:hover { background-color: rgba(235, 245, 255, 0.6); } .resource-icon { font-size: 18px; width: 30px; height: 30px; border-radius: 7px; display: flex; align-items: center; justify-content: center; margin-right: 10px; flex-shrink: 0; } .video-icon { background-color: rgba(234, 67, 53, 0.15); color: #ea4335; } .audio-icon { background-color: rgba(52, 168, 83, 0.15); color: #34a853; } .image-icon { background-color: rgba(66, 133, 244, 0.15); color: #4285f4; } .resource-info { flex: 1; min-width: 0; } .resource-name { font-weight: 500; font-size: 13px; color: #2d3748; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; } .resource-meta { font-size: 11px; color: #718096; display: flex; gap: 8px; } .resource-actions { margin-left: 8px; flex-shrink: 0; display: flex; gap: 5px; } .action-btn { width: 28px; height: 28px; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; background: #f5f7fa; color: #5c6bc0; border: none; cursor: pointer; transition: all 0.2s; font-size: 14px; } .action-btn:hover { background: #e8ebf0; transform: scale(1.05); } .empty-message { padding: 30px; text-align: center; color: #718096; font-size: 13px; } .loading-indicator { text-align: center; padding: 20px; color: #4285f4; font-size: 13px; display: flex; align-items: center; justify-content: center; } .refresh-btn { display: flex; align-items: center; justify-content: center; width: calc(100% - 16px); margin: 8px auto 0; padding: 9px; background: #f5f7fa; border: none; border-radius: 7px; color: #4285f4; cursor: pointer; font-weight: 500; transition: all 0.2s; } .refresh-btn:hover:not(:disabled) { background: #e8ebf0; } .refresh-btn:disabled { cursor: not-allowed; background: #f5f7fa; color: #a0a0a0; } .site-optimized { display: inline-block; background: rgba(66, 133, 244, 0.1); color: #4285f4; padding: 2px 7px; border-radius: 5px; font-size: 10px; margin-left: 7px; vertical-align: middle; } /* 全新高对比度分页样式 */ .pagination { display: flex; justify-content: center; gap: 8px; margin-top: 12px; padding-bottom: 5px; } .pagination-btn { padding: 6px 12px; border-radius: 7px; font-size: 12px; font-weight: 500; border: 1px solid; transition: all 0.2s ease; } .pagination-btn:not(:disabled):not(.active) { color: #4285f4; border-color: #4285f4; background: #ffffff; cursor: pointer; } .pagination-btn:not(:disabled):not(.active):hover { background: rgba(66, 133, 244, 0.08); } .pagination-btn.active { background: #4285f4; color: white; border-color: #4285f4; cursor: default; } .pagination-btn:disabled { color: #cccccc; border-color: #eeeeee; background: #fafafa; cursor: not-allowed; } .image-preview-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.85); backdrop-filter: blur(5px); z-index: 10000; display: flex; justify-content: center; align-items: center; padding: 15px; } .image-preview-content { max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; } .image-preview-content img { max-width: 100%; max-height: calc(90vh - 80px); object-fit: contain; border-radius: 7px; } .image-preview-info { background: rgba(30,30,30,0.8); padding: 10px; border-radius: 7px; font-size: 13px; color: #fff; margin-top: 10px; text-align: center; word-break: break-all; } .image-preview-controls { display: flex; justify-content: center; padding-top: 12px; gap: 12px; } .preview-action-btn { padding: 8px 16px; border-radius: 7px; background: #4a4a4a; color: #fff; border: 1px solid #666; cursor: pointer; transition: all 0.2s; font-size: 14px; } .preview-action-btn:hover { background: #5a5a5a; border-color: #888; } .loader { width: 18px; height: 18px; border: 3px solid rgba(66, 133, 244, 0.2); border-radius: 50%; border-top-color: #4285f4; animation: spin 1s ease-in-out infinite; display: inline-block; vertical-align: middle; margin-right: 8px; } @keyframes spin { to { transform: rotate(360deg); } } /* 图片预览网格 */ .preview-container { padding: 12px; } .preview-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; margin-top: 8px; } .image-preview { border-radius: 7px; overflow: hidden; position: relative; padding-top: 100%; background-color: #f0f2f5; box-shadow: 0 3px 8px rgba(0,0,0,0.05); cursor: pointer; } .image-preview img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; } .image-preview:hover img { transform: scale(1.1); } .image-info { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.6); color: white; padding: 5px 7px; font-size: 10px; line-height: 1.3; text-align: center; } `); // --- 配置信息 --- const RESOURCE_TYPES = { video: { extensions: ['m3u8', 'm3u', 'mp4', 'webm', 'avi', 'mov', 'flv', 'wmv', 'mpd', 'ts', 'f4v', 'mkv'], label: '视频', icon: '📹' }, audio: { extensions: ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'wma', 'opus'], label: '音频', icon: '🎵' }, image: { extensions: ['png', 'jpg', 'jpeg', 'gif', 'ico', 'webp', 'svg', 'bmp'], label: '图片', icon: '🖼️' } }; const SUPPORTED_SITES = { 'douyin.com': '抖音', 'tiktok.com': 'TikTok', 'ixigua.com': '西瓜视频', 'kuaishou.com': '快手', 'v.qq.com': '腾讯视频', 'iqiyi.com': '爱奇艺', 'mgtv.com': '芒果TV', 'youtube.com': 'YouTube', 'youtu.be': 'YouTube', 'bilibili.com': '哔哩哔哩', 'b23.tv': '哔哩哔哩', 'youku.com': '优酷', 'twitter.com': 'Twitter', 'instagram.com': 'Instagram', 'google.com': 'Google Images', 'baidu.com': '百度图片' }; // --- 全局状态变量 --- let floatingBall = null, resourcePanel = null; let resources = { video: [], audio: [], image: [] }; let isPanelVisible = false; let currentTab = 'video'; let isScanning = false; const currentDomain = location.hostname; let currentPage = { video: 1, audio: 1, image: 1 }; const pageSize = 20; let scanTimeout = null; const foundUrls = new Set(); // 新增:悬浮球状态管理 let ballState = 'hidden'; // 'hidden', 'retracted', 'expanded' let retractTimeout = null; // --- 核心功能函数 --- function init() { createUI(); setupEventListeners(); setTimeout(() => performScan('full'), 1500); } function createUI() { floatingBall = document.createElement('div'); floatingBall.id = 'resourceDetectorBall'; floatingBall.innerHTML = `<span><div id="resourceBallBadge"></div>资源</span>`; document.body.appendChild(floatingBall); resourcePanel = document.createElement('div'); resourcePanel.id = 'resourceDetectorPanel'; const siteName = Object.keys(SUPPORTED_SITES).find(domain => currentDomain.includes(domain)); const optimizedTag = siteName ? `<span class="site-optimized">${SUPPORTED_SITES[siteName]} 优化</span>` : ''; resourcePanel.innerHTML = ` <div class="panel-header"> <span>网页资源检测 ${optimizedTag}</span> <div class="close-btn">×</div> </div> <div class="category-tabs"> ${Object.entries(RESOURCE_TYPES).map(([type, config]) => ` <div class="tab-btn ${type === currentTab ? 'active' : ''}" data-type="${type}"> ${config.label} <span class="tab-badge" id="badge-${type}">0</span> </div> `).join('')} </div> <div class="tab-content"></div> `; document.body.appendChild(resourcePanel); } function setupEventListeners() { // 悬浮球点击事件现在管理三个状态 floatingBall.addEventListener('click', handleBallClick); resourcePanel.querySelector('.close-btn').addEventListener('click', () => togglePanelVisibility(false)); resourcePanel.querySelectorAll('.tab-btn').forEach(tab => { tab.addEventListener('click', () => switchTab(tab.dataset.type)); }); GM_registerMenuCommand('手动全量扫描资源', () => performScan('full')); GM_registerMenuCommand('打开/关闭面板', () => { if (isPanelVisible) { togglePanelVisibility(false); } else { setBallState('expanded'); togglePanelVisibility(true); } }); const observer = new MutationObserver(() => debounceScan()); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href'] }); document.addEventListener('play', (event) => { if (event.target.src) { const type = getResourceTypeByUrl(event.target.src); if (type === 'video' || type === 'audio') { if (addResource(event.target.src, type, event.target)) { updateUI(); } } } }, true); } /** * 全新的悬浮球点击处理逻辑 */ function handleBallClick() { switch (ballState) { case 'retracted': // 从收起状态 -> 展开状态 setBallState('expanded'); break; case 'expanded': // 从展开状态 -> 打开面板 togglePanelVisibility(true); break; } } /** * 统一管理悬浮球状态的函数 * @param {'hidden' | 'retracted' | 'expanded'} newState */ function setBallState(newState) { ballState = newState; clearTimeout(retractTimeout); // 任何状态变更都清除旧的计时器 floatingBall.classList.remove('retracted', 'expanded'); if (newState === 'retracted') { floatingBall.classList.add('retracted'); } else if (newState === 'expanded') { floatingBall.classList.add('expanded'); // 展开后,如果面板没打开,则启动自动收回计时器 if (!isPanelVisible) { retractTimeout = setTimeout(() => { setBallState('retracted'); }, 5000); // 5秒后自动收回 } } } function togglePanelVisibility(forceShow = null) { isPanelVisible = forceShow !== null ? forceShow : !isPanelVisible; if (isPanelVisible) { // 打开面板前,确保球是展开的,并清除自动收回计时器 setBallState('expanded'); resourcePanel.classList.add('active'); renderTabContent(currentTab); } else { resourcePanel.classList.remove('active'); // 关闭面板后,球立即回到收起状态 setBallState('retracted'); } } function switchTab(type) { currentTab = type; currentPage[type] = 1; resourcePanel.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.type === type); }); renderTabContent(type); } function renderTabContent(type) { const contentContainer = resourcePanel.querySelector('.tab-content'); if (isScanning && contentContainer.dataset.scanMode === 'full') { contentContainer.innerHTML = `<div class="loading-indicator"><div class="loader"></div>正在进行全量扫描...</div>`; return; } const items = resources[type]; const config = RESOURCE_TYPES[type]; if (items.length === 0) { contentContainer.innerHTML = ` <div class="empty-message"> 未检测到${config.label}资源 ${createRefreshButtonHTML('full')} </div>`; contentContainer.querySelector('.refresh-btn').onclick = () => performScan('full'); return; } if (type === 'image') { renderImagePreviewGrid(contentContainer, items, type); } else { renderResourceList(contentContainer, items, type); } } function renderResourceList(container, items, type) { const config = RESOURCE_TYPES[type]; const pageItems = paginate(items, currentPage[type], pageSize); container.innerHTML = ` <ul class="resource-list"> ${pageItems.map(item => ` <li class="resource-item" data-url="${item.url}"> <div class="resource-icon ${type}-icon">${config.icon}</div> <div class="resource-info"> <div class="resource-name" title="${item.url}">${getFileName(item.url)}</div> <div class="resource-meta"> <span>${getFileType(item.url)}</span> ${item.size ? `<span>${item.size}</span>` : ''} ${item.duration ? `<span>${formatDuration(item.duration)}</span>` : ''} </div> </div> <div class="resource-actions"> <button class="action-btn" data-action="copy" title="复制链接">📋</button> <button class="action-btn" data-action="download" title="下载">↓</button> <button class="action-btn" data-action="open" title="新标签页打开">↗</button> </div> </li> `).join('')} </ul> `; container.appendChild(createPagination(items.length, currentPage[type], pageSize, (page) => { currentPage[type] = page; renderResourceList(container, items, type); })); container.insertAdjacentHTML('beforeend', createRefreshButtonHTML('full')); container.querySelector('.refresh-btn').onclick = () => performScan('full'); container.querySelectorAll('.resource-item').forEach(item => { const url = item.dataset.url; item.querySelector('[data-action="copy"]').onclick = (e) => { e.stopPropagation(); copyToClipboard(url, '链接已复制'); }; item.querySelector('[data-action="download"]').onclick = (e) => { e.stopPropagation(); GM_download(url, getFileName(url)); }; item.querySelector('[data-action="open"]').onclick = (e) => { e.stopPropagation(); GM_openInTab(url, { active: true }); }; }); } function renderImagePreviewGrid(container, items, type) { const pageItems = paginate(items, currentPage[type], pageSize); container.innerHTML = ` <div class="preview-container"> <div id="imagePreviewGrid" class="preview-grid"> ${pageItems.map(item => ` <div class="image-preview" data-url="${item.url}" data-size="${item.size || '未知尺寸'}"> <img src="${item.url}" loading="lazy" alt="图片预览"> <div class="image-info">${item.size || '未知尺寸'}</div> </div> `).join('')} </div> </div> `; container.appendChild(createPagination(items.length, currentPage[type], pageSize, (page) => { currentPage[type] = page; renderImagePreviewGrid(container, items, type); })); container.insertAdjacentHTML('beforeend', createRefreshButtonHTML('full')); container.querySelector('.refresh-btn').onclick = () => performScan('full'); container.querySelectorAll('.image-preview').forEach(item => { item.onclick = () => showImagePreviewModal(item.dataset.url, item.dataset.size); }); } function showImagePreviewModal(url, size) { const overlay = document.createElement('div'); overlay.className = 'image-preview-overlay'; overlay.innerHTML = ` <div class="image-preview-content"> <img src="${url}" alt="预览图片"> <div class="image-preview-info"> 尺寸: ${size} | 格式: ${getFileType(url)} <div class="image-preview-controls"> <button class="preview-action-btn" id="copyPreviewBtn">复制链接</button> <button class="preview-action-btn" id="downloadPreviewBtn">下载</button> </div> </div> </div> `; document.body.appendChild(overlay); overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; overlay.querySelector('#copyPreviewBtn').onclick = () => copyToClipboard(url, '图片链接已复制'); overlay.querySelector('#downloadPreviewBtn').onclick = () => GM_download(url, getFileName(url)); } async function performScan(mode = 'additive') { if (isScanning) return; isScanning = true; let newResourcesFound = 0; if (mode === 'full') { foundUrls.clear(); resources = { video: [], audio: [], image: [] }; const contentContainer = resourcePanel.querySelector('.tab-content'); if(contentContainer) contentContainer.dataset.scanMode = 'full'; updateUI(); } const scanTasks = [ () => document.querySelectorAll('video, audio, source').forEach(el => { if(addResource(el.src, getResourceTypeByUrl(el.src), el)) newResourcesFound++; }), () => document.querySelectorAll('img, image, [style*="background-image"]').forEach(el => { let src = el.src || el.dataset.src || el.dataset.original || el.href?.baseVal; if (el.style.backgroundImage) { const match = el.style.backgroundImage.match(/url$['"]?(.*?)['"]?$/); if (match) src = match[1]; } if (src && getResourceTypeByUrl(src) === 'image' && addResource(src, 'image', el)) newResourcesFound++; }), () => document.querySelectorAll('a[href]').forEach(link => { if(addResource(link.href, getResourceTypeByUrl(link.href))) newResourcesFound++; }), () => document.querySelectorAll('script').forEach(script => { if(scanTextForUrls(script.textContent)) newResourcesFound++; }), () => performance.getEntriesByType('resource').forEach(res => { if(addResource(res.name, getResourceTypeByUrl(res.name))) newResourcesFound++; }), () => { // 特定网站优化 if (currentDomain.includes('google.com')) document.querySelectorAll('img[data-iurl]').forEach(img => { if(addResource(img.dataset.iurl, 'image', img)) newResourcesFound++; }); if (currentDomain.includes('baidu.com')) document.querySelectorAll('img[data-imgurl]').forEach(img => { if(addResource(img.dataset.imgurl, 'image', img)) newResourcesFound++; }); } ]; for (const task of scanTasks) { await new Promise(resolve => requestAnimationFrame(() => { task(); resolve(); })); } isScanning = false; resourcePanel.querySelector('.tab-content').dataset.scanMode = ''; if (mode === 'full') { GM_notification({ title: '全量扫描完成', text: `共发现 ${foundUrls.size} 个资源`, timeout: 2500 }); } else if (newResourcesFound > 0) { console.log(`[资源检测] 增量扫描发现 ${newResourcesFound} 个新资源。`); } updateUI(); } function scanTextForUrls(text) { const urlRegex = /https?:\/\/[^\s"'<>]+/g; const matches = text.match(urlRegex) || []; let found = false; matches.forEach(url => { if(addResource(url, getResourceTypeByUrl(url))) found = true; }); return found; } function addResource(url, type, element = null) { if (!url || !type || typeof url !== 'string') return false; const cleanUrl = url.split('?')[0]; if (foundUrls.has(cleanUrl)) return false; foundUrls.add(cleanUrl); const resource = { url, type }; if (type === 'image' && element?.naturalWidth > 1) resource.size = `${element.naturalWidth}×${element.naturalHeight}`; if ((type === 'video' || type === 'audio') && element?.duration) resource.duration = element.duration; resources[type].push(resource); resources[type].sort((a, b) => a.url.localeCompare(b.url)); return true; } function updateUI() { const total = getTotalResources(); // 更新悬浮球状态 if (total > 0 && ballState === 'hidden') { setBallState('retracted'); } else if (total === 0) { setBallState('hidden'); } document.getElementById('resourceBallBadge').style.display = total > 0 ? 'block' : 'none'; Object.keys(RESOURCE_TYPES).forEach(type => { const badge = document.getElementById(`badge-${type}`); if (badge) { const count = resources[type].length; badge.textContent = count; badge.style.display = count > 0 ? 'inline-block' : 'none'; } }); if (isPanelVisible) { renderTabContent(currentTab); } } // --- 辅助与工具函数 --- const debounceScan = (delay = 1200) => { clearTimeout(scanTimeout); scanTimeout = setTimeout(() => performScan('additive'), delay); }; const getTotalResources = () => foundUrls.size; const getResourceTypeByUrl = (url) => { if (!url) return null; try { const ext = new URL(url, location.href).pathname.split('.').pop().toLowerCase(); return Object.keys(RESOURCE_TYPES).find(type => RESOURCE_TYPES[type].extensions.includes(ext)) || null; } catch (e) { return null; } }; const getFileName = (url) => { try { return decodeURIComponent(new URL(url, location.href).pathname.split('/').pop() || '未命名资源'); } catch (e) { return '未命名资源'; }}; const getFileType = (url) => { try { return new URL(url, location.href).pathname.split('.').pop().toUpperCase() || '未知'; } catch (e) { return '未知'; }}; const formatDuration = (sec) => { if (!sec || sec === Infinity) return ''; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; }; const copyToClipboard = (text, message) => GM_setClipboard(text, 'text/plain', () => GM_notification({ title: '操作成功', text: message, timeout: 2000 })); const paginate = (items, page, size) => items.slice((page - 1) * size, page * size); function createPagination(totalItems, currentPage, pageSize, onPageChange) { const totalPages = Math.ceil(totalItems / pageSize); if (totalPages <= 1) return document.createDocumentFragment(); const pagination = document.createElement('div'); pagination.className = 'pagination'; const prevBtn = document.createElement('button'); prevBtn.className = 'pagination-btn'; prevBtn.textContent = '‹ 上一页'; prevBtn.onclick = () => onPageChange(currentPage - 1); if (currentPage === 1) { prevBtn.disabled = true; } pagination.appendChild(prevBtn); const pageInfo = document.createElement('span'); pageInfo.className = 'pagination-btn active'; pageInfo.textContent = `${currentPage} / ${totalPages}`; pagination.appendChild(pageInfo); const nextBtn = document.createElement('button'); nextBtn.className = 'pagination-btn'; nextBtn.textContent = '下一页 ›'; nextBtn.onclick = () => onPageChange(currentPage + 1); if (currentPage === totalPages) { nextBtn.disabled = true; } pagination.appendChild(nextBtn); return pagination; } function createRefreshButtonHTML(mode) { if (isScanning && mode ==='full') { return `<button class="refresh-btn" disabled><div class="loader"></div>正在扫描...</div>`; } return `<button class="refresh-btn">手动全量扫描</button>`; } // --- 启动脚本 --- if (document.readyState === 'complete') { init(); } else { window.addEventListener('load', init); } })();