一键复制MD格式(可用) 功能
另存图片功能 和 排序 功能
签名“图床”已放上快捷方式。
BY GEMINI 2.5PRO
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>批量网址检测与排序工具</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px; background-color: #f4f7f6; color: #333; max-width: 900px; margin: 20px auto; } h1 { text-align: center; color: #2c3e50; } #input-area { margin-bottom: 20px; } #url-input { width: 100%; height: 200px; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 14px; box-sizing: border-box; /* 确保 padding 不会撑大宽度 */ } #controls { display: flex; flex-wrap: wrap; /* 在小屏幕上换行 */ justify-content: center; gap: 10px; margin-bottom: 20px; } #controls button { border: none; padding: 10px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; transition: all 0.3s ease; white-space: nowrap; /* 防止按钮内文字换行 */ } #start-test-btn { background-color: #3498db; color: white; font-weight: bold; font-size: 16px; padding: 12px 20px; } #start-test-btn:hover { background-color: #2980b9; } #save-image-btn, #copy-md-available-btn, #copy-md-all-btn, #copy-bbs-btn { background-color: #7f8c8d; color: white; } #save-image-btn:hover, #copy-md-available-btn:hover, #copy-md-all-btn:hover, #copy-bbs-btn:hover { background-color: #95a5a6; } #results-table { width: 100%; border-collapse: collapse; margin-top: 20px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); background-color: #ffffff; border-radius: 8px; overflow: hidden; } th, td { padding: 12px 15px; border-bottom: 1px solid #ddd; text-align: left; } th { background-color: #ecf0f1; color: #34495e; font-weight: 600; } /* 可排序的表头 */ th[data-sort] { cursor: pointer; user-select: none; position: relative; } th[data-sort]:hover { background-color: #dfe6e9; } /* 排序指示器 (箭头) */ th[data-sort] .sort-indicator { float: right; opacity: 0.5; } tbody tr:nth-child(even) { background-color: #f9f9f9; } td a { color: #3498db; text-decoration: none; word-break: break-all; /* 网址过长时换行 */ } td a:hover { text-decoration: underline; } .status-ok { color: #2ecc71; font-weight: bold; } .status-fail { color: #e74c3c; font-weight: bold; } .status-pending { color: #f39c12; font-weight: bold; } </style></head><body> <h1>批量网址检测与排序工具</h1> <div id="input-area"> <textarea id="url-input" placeholder="请输入要检测的网址,每行一个..."></textarea> </div> <div id="controls"> <button id="start-test-btn">开始检测</button> <button id="save-image-btn">另存图片</button> <button id="copy-md-available-btn">复制 MD (可用)</button> <button id="copy-md-all-btn">复制 MD (全部)</button> <button id="copy-bbs-btn">复制 BBS 格式</button> </div> <table id="results-table"> <thead> <tr> <th>网址 (URL)</th> <th data-sort="status">状态 <span class="sort-indicator"></span></th> <th data-sort="speed">速度 (ms) <span class="sort-indicator"></span></th> </tr> </thead> <tbody> </tbody> </table> <script> // --------- // 1. 全局变量 // --------- let currentResults = []; // 存储结果数据,作为单一数据源 let sortState = { key: null, direction: 'asc' }; // 存储排序状态 const TIMEOUT = 10000; // 超时时间 (10秒) // --------- // 2. 核心检测函数 // --------- function testApi(url, index) { // 注意:index 是原始索引,用于更新 currentResults const statusCell = document.getElementById(`status-${index}`); const speedCell = document.getElementById(`speed-${index}`); // 查找数据源 const dataItem = currentResults.find(item => item.id === index); if (!statusCell || !speedCell || !dataItem) return; // 立即更新 UI 和数据源 statusCell.textContent = '检测中...'; statusCell.className = 'status-pending'; speedCell.textContent = '...'; dataItem.status = '检测中...'; dataItem.speed = '...'; const startTime = performance.now(); const img = new Image(); let timer; timer = setTimeout(() => { img.src = ''; updateResult(index, '超时', `> ${TIMEOUT} ms`, 'status-fail'); }, TIMEOUT); img.onload = () => { clearTimeout(timer); const endTime = performance.now(); const duration = Math.round(endTime - startTime); updateResult(index, '可用', `${duration} ms`, 'status-ok'); }; img.onerror = () => { clearTimeout(timer); updateResult(index, '失效/CORS', 'N/A', 'status-fail'); }; // 添加时间戳防止浏览器缓存 const testUrl = url + (url.includes('?') ? '&' : '?') + 't=' + new Date().getTime(); img.src = testUrl; } // 统一更新数据源和 DOM function updateResult(index, status, speed, className) { const statusCell = document.getElementById(`status-${index}`); const speedCell = document.getElementById(`speed-${index}`); const dataItem = currentResults.find(item => item.id === index); if (dataItem) { dataItem.status = status; dataItem.speed = speed; } if (statusCell) { statusCell.textContent = status; statusCell.className = className; } if (speedCell) { speedCell.textContent = speed; } } // --------- // 3. 排序逻辑 // --------- // 将速度字符串转换为可比较的数字,以便正确排序 function parseSpeed(speedString) { if (speedString === 'N/A' || speedString === '...' ) return TIMEOUT + 3; if (speedString === '失效/CORS') return TIMEOUT + 2; if (speedString === '超时' || speedString.startsWith('>')) return TIMEOUT + 1; if (speedString === '检测中...') return TIMEOUT + 4; if (speedString === '待检测') return TIMEOUT + 5; // "可用" 的情况 return parseInt(speedString, 10); } // 排序主函数 function sortTable(key) { // 确定排序方向 let direction = 'asc'; if (sortState.key === key && sortState.direction === 'asc') { direction = 'desc'; } sortState = { key, direction }; const directionMultiplier = (direction === 'asc') ? 1 : -1; // 对数据源 (currentResults) 进行排序 currentResults.sort((a, b) => { let valA, valB; if (key === 'status') { // 按状态文本排序 valA = a.status; valB = b.status; return valA.localeCompare(valB) * directionMultiplier; } else if (key === 'speed') { // 按解析后的速度数字排序 valA = parseSpeed(a.speed); valB = parseSpeed(b.speed); return (valA - valB) * directionMultiplier; } return 0; }); // 重新渲染表格 renderTable(); // 更新箭头 updateSortIndicators(); } // 重新绘制整个表格 (基于 currentResults 数据) function renderTable() { const tbody = document.getElementById('results-table').getElementsByTagName('tbody')[0]; tbody.innerHTML = ''; // 清空 currentResults.forEach(item => { const row = tbody.insertRow(); // 关键:ID 必须使用 item.id,这是它在 currentResults 中的原始索引 row.id = `row-${item.id}`; const urlCell = row.insertCell(); urlCell.innerHTML = `<a href="${item.url}" target="_blank">${item.url}</a>`; const statusCell = row.insertCell(); statusCell.id = `status-${item.id}`; // 保持 ID 对应 statusCell.textContent = item.status; // 根据状态设置样式 if (item.status === '可用') statusCell.className = 'status-ok'; else if (item.status === '待检测' || item.status === '检测中...') statusCell.className = 'status-pending'; else statusCell.className = 'status-fail'; const speedCell = row.insertCell(); speedCell.id = `speed-${item.id}`; // 保持 ID 对应 speedCell.textContent = item.speed; }); } // 更新表头的排序箭头 function updateSortIndicators() { // 1. 先清空所有箭头 document.querySelectorAll('th[data-sort] .sort-indicator').forEach(span => { span.textContent = ''; }); // 2. 找到当前激活的排序列 const activeTh = document.querySelector(`th[data-sort="${sortState.key}"]`); // 3. 设置对应的箭头 if (activeTh) { const indicator = activeTh.querySelector('.sort-indicator'); indicator.textContent = (sortState.direction === 'asc') ? ' ▲' : ' ▼'; } } // --------- // 4. 导出与复制逻辑 // --------- // 剪贴板辅助函数 function copyToClipboard(text, message) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { alert(message); }).catch(err => { console.error('复制失败: ', err); alert('复制失败,请查看控制台。'); }); } else { // 备用方案 (针对 http 或旧版浏览器) const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); alert(message); } catch (err) { console.error('复制失败: ', err); alert('复制失败。'); } document.body.removeChild(textArea); } } // 统一的文本生成函数 (根据 format 和 filter) function getResultsAsText(format, filter) { let resultsToFormat = currentResults; if (filter === 'available') { resultsToFormat = currentResults.filter(r => r.status === '可用'); } if (resultsToFormat.length === 0) { alert('没有可导出的数据。'); return null; } if (format === 'md') { let text = "| 网址 | 状态 | 速度 (ms) |\n| :--- | :--- | :--- |\n"; resultsToFormat.forEach(item => { text += `| ${item.url} | ${item.status} | ${item.speed} |\n`; }); return text; } if (format === 'bbs') { let text = "[table]\n"; text += "[tr][td][b]网址[/b][/td][td][b]状态[/b][/td][td][b]速度 (ms)[/b][/td][/tr]\n"; resultsToFormat.forEach(item => { text += `[tr][td]${item.url}[/td][td]${item.status}[/td][td]${item.speed}[/td][/tr]\n`; }); text += "[/table]"; return text; } return null; } // --------- // 5. 事件绑定 // --------- document.addEventListener('DOMContentLoaded', () => { // 获取所有操作按钮 const startButton = document.getElementById('start-test-btn'); const saveImageBtn = document.getElementById('save-image-btn'); const copyMdAvailableBtn = document.getElementById('copy-md-available-btn'); const copyMdAllBtn = document.getElementById('copy-md-all-btn'); const copyBbsBtn = document.getElementById('copy-bbs-btn'); const urlInput = document.getElementById('url-input'); const resultsTable = document.getElementById('results-table'); // 绑定“开始检测”按钮 startButton.addEventListener('click', () => { const urls = urlInput.value .split('\n') .map(url => url.trim()) .filter(url => url.length > 0); if (urls.length === 0) { alert('请输入至少一个网址!'); return; } // 重置数据和排序状态 currentResults = []; sortState = { key: null, direction: 'asc' }; updateSortIndicators(); // 清除旧箭头 urls.forEach((url, index) => { // 填充数据源 currentResults.push({ id: index, // 永久 ID url: url, status: '待检测', speed: 'N/A' }); }); // 初始渲染 renderTable(); // 开始并发检测 currentResults.forEach(item => { testApi(item.url, item.id); }); }); // 绑定“另存图片”按钮 saveImageBtn.addEventListener('click', () => { if (currentResults.length === 0) { alert('请先进行检测!'); return; } html2canvas(resultsTable, { backgroundColor: '#ffffff', // 确保截图背景是白色 useCORS: true }).then(canvas => { const imgData = canvas.toDataURL('image/png'); const link = document.createElement('a'); link.href = imgData; link.download = 'api-test-results.png'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }); // 绑定“复制 MD (可用)” copyMdAvailableBtn.addEventListener('click', () => { const text = getResultsAsText('md', 'available'); if (text) { copyToClipboard(text, 'Markdown (状态可用) 已复制到剪贴板!'); } }); // 绑定“复制 MD (全部)” copyMdAllBtn.addEventListener('click', () => { const text = getResultsAsText('md', 'all'); if (text) { copyToClipboard(text, 'Markdown (全部) 已复制到剪贴板!'); } }); // 绑定“复制为 BBS” (默认全部) copyBbsBtn.addEventListener('click', () => { const text = getResultsAsText('bbs', 'all'); if (text) { copyToClipboard(text, 'BBS 格式已复制到剪贴板!'); } }); // 绑定表头排序事件 (使用事件委托) document.querySelector('#results-table thead').addEventListener('click', (e) => { const th = e.target.closest('th[data-sort]'); if (th) { const key = th.getAttribute('data-sort'); sortTable(key); } }); }); </script></body></html>
评论 (0)