网站可用性扫描工具
作者:YXN-js 阅读量:11 发布日期:2025-06-22
页面明亮版
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网址扫描器</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.btn-container {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
flex: 1;
}
.btn-stop {
background-color: #f44336;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.results {
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
max-height: 400px;
overflow-y: auto;
background-color: #fafafa;
}
.result-item {
padding: 8px;
border-bottom: 1px solid #eee;
font-family: monospace;
}
.result-item.success {
color: #4CAF50;
}
.result-item.failed {
color: #f44336;
}
.result-item:last-child {
border-bottom: none;
}
.status {
margin-top: 10px;
font-style: italic;
color: #666;
text-align: center;
}
.url-example {
font-size: 0.9em;
color: #666;
margin-top: 5px;
}
.progress-container {
margin-top: 15px;
background-color: #f1f1f1;
border-radius: 4px;
height: 20px;
}
.progress-bar {
height: 100%;
border-radius: 4px;
background-color: #4CAF50;
width: 0%;
transition: width 0.3s;
}
.stats {
margin-top: 10px;
display: flex;
justify-content: space-between;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>网址扫描器</h1>
<div class="form-group">
<label for="base_url">网址模板 (用{}代替数字):</label>
<input type="text" id="base_url" value="https://baidu{}.cc" required>
<div class="url-example">示例: https://baidu{}.cc 将会扫描 https://baidu2100.cc, https://baidu2101.cc 等</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_num">起始数字:</label>
<input type="number" id="start_num" min="0" value="2100" required>
</div>
<div class="form-group">
<label for="end_num">结束数字:</label>
<input type="number" id="end_num" min="0" value="2200" required>
</div>
<div class="form-group">
<label for="concurrency">并发数量:</label>
<input type="number" id="concurrency" min="1" max="50" value="20">
</div>
</div>
<div class="btn-container">
<button id="startBtn" class="btn">开始扫描</button>
<button id="stopBtn" class="btn btn-stop" disabled>停止扫描</button>
</div>
<div class="progress-container">
<div id="progressBar" class="progress-bar"></div>
</div>
<div class="stats">
<div>成功: <span id="successCount">0</span></div>
<div>失败: <span id="failedCount">0</span></div>
<div>总计: <span id="totalCount">0</span></div>
</div>
<div class="status" id="status">准备就绪</div>
<div class="results" id="results">
<h3>扫描结果:</h3>
<div id="resultList"></div>
</div>
</div>
<script>
let isScanning = false;
let stopRequested = false;
let activeRequests = 0;
let totalUrls = 0;
let completedUrls = 0;
let successCount = 0;
let failedCount = 0;
let abortControllers = new Map();
document.getElementById('startBtn').addEventListener('click', startScan);
document.getElementById('stopBtn').addEventListener('click', stopScan);
async function startScan() {
if (isScanning) return;
// 获取输入值
const baseUrl = document.getElementById('base_url').value;
const startNum = parseInt(document.getElementById('start_num').value);
const endNum = parseInt(document.getElementById('end_num').value);
const concurrency = parseInt(document.getElementById('concurrency').value);
// 验证输入
if (endNum < startNum) {
alert('结束数字必须大于或等于起始数字');
return;
}
if (!baseUrl.includes('{}')) {
alert('网址模板必须包含{}作为数字占位符');
return;
}
// 初始化状态
isScanning = true;
stopRequested = false;
activeRequests = 0;
completedUrls = 0;
successCount = 0;
failedCount = 0;
totalUrls = endNum - startNum + 1;
abortControllers = new Map();
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
document.getElementById('resultList').innerHTML = '';
document.getElementById('status').textContent = '扫描进行中...';
document.getElementById('progressBar').style.width = '0%';
document.getElementById('successCount').textContent = '0';
document.getElementById('failedCount').textContent = '0';
document.getElementById('totalCount').textContent = totalUrls;
// 生成所有URL
const urls = [];
for (let i = startNum; i <= endNum; i++) {
urls.push({
url: baseUrl.replace('{}', i),
num: i
});
}
// 使用队列控制并发
const queue = [...urls];
// 启动初始请求
const initialRequests = Math.min(concurrency, queue.length);
for (let i = 0; i < initialRequests; i++) {
if (queue.length > 0) {
processNextUrl(queue);
}
}
}
function stopScan() {
if (!isScanning) return;
stopRequested = true;
document.getElementById('status').textContent = '正在停止...';
document.getElementById('stopBtn').disabled = true;
// 中止所有进行中的请求
abortControllers.forEach(controller => {
controller.abort();
});
abortControllers.clear();
}
async function processNextUrl(queue) {
if (stopRequested || queue.length === 0) {
if (activeRequests === 0) {
finishScan();
}
return;
}
activeRequests++;
const {url, num} = queue.shift();
// 创建AbortController用于超时控制
const controller = new AbortController();
abortControllers.set(num, controller);
// 设置5秒超时
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch(url, {
method: 'GET',
mode: 'no-cors', // 处理跨域问题
cache: 'no-cache',
signal: controller.signal,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
// 由于使用了no-cors模式,我们无法读取响应状态码
// 所以只要没有抛出错误,就认为请求成功
successCount++;
document.getElementById('successCount').textContent = successCount;
addResult(url, true);
} catch (error) {
// 请求失败或超时
failedCount++;
document.getElementById('failedCount').textContent = failedCount;
addResult(url, false);
} finally {
clearTimeout(timeoutId);
abortControllers.delete(num);
activeRequests--;
completedUrls++;
// 更新进度条
const progress = (completedUrls / totalUrls) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
// 处理下一个URL
if (queue.length > 0 && !stopRequested) {
processNextUrl(queue);
} else if (activeRequests === 0) {
finishScan();
}
}
}
function addResult(url, isSuccess) {
const resultList = document.getElementById('resultList');
const resultItem = document.createElement('div');
resultItem.className = `result-item ${isSuccess ? 'success' : 'failed'}`;
if (isSuccess) {
// 成功的URL创建可点击链接
const link = document.createElement('a');
link.href = url;
link.textContent = url;
link.target = '_blank'; // 在新页面打开
link.rel = 'noopener noreferrer'; // 安全措施
resultItem.appendChild(link);
resultItem.appendChild(document.createTextNode(' - 成功'));
} else {
// 失败的URL保持文本显示
resultItem.textContent = `${url} - 失败`;
}
resultList.appendChild(resultItem);
// 滚动到底部
const resultsDiv = document.getElementById('results');
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
function finishScan() {
isScanning = false;
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
if (stopRequested) {
document.getElementById('status').textContent = '扫描已停止';
} else {
document.getElementById('status').textContent = '扫描完成!';
}
}
</script>
</body>
</html>
页面暗黑版
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网站可用性扫描工具</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--secondary: #3f37c9;
--success: #4cc9f0;
--dark: #1d3557;
--light: #f8f9fa;
--danger: #e63946;
--warning: #fca311;
--info: #90e0ef;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1d3557 0%, #457b9d 100%);
min-height: 100vh;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
color: var(--light);
}
.container {
width: 100%;
max-width: 1200px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
border-radius: 20px;
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
header {
background: rgba(29, 53, 87, 0.75);
padding: 28px 35px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
header h1 {
font-size: 2.5rem;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
letter-spacing: 0.5px;
}
header h1 i {
color: var(--success);
}
header p {
color: #a8dadc;
font-size: 1.15rem;
max-width: 800px;
margin: 0 auto;
line-height: 1.7;
}
.form-container {
padding: 35px;
}
.input-group {
margin-bottom: 28px;
}
.input-group label {
display: block;
margin-bottom: 12px;
font-weight: 600;
font-size: 1.15rem;
color: #f1faee;
}
.input-row {
display: flex;
gap: 18px;
flex-wrap: wrap;
}
.input-field {
flex: 1;
min-width: 220px;
}
.input-field input {
width: 100%;
padding: 15px 20px;
border: 2px solid rgba(168, 218, 220, 0.35);
border-radius: 14px;
background: rgba(255, 255, 255, 0.12);
color: white;
font-size: 1.15rem;
transition: all 0.35s ease;
}
.input-field input:focus {
outline: none;
border-color: var(--success);
background: rgba(76, 201, 240, 0.15);
box-shadow: 0 0 15px rgba(76, 201, 240, 0.25);
}
.input-field input::placeholder {
color: rgba(255, 255, 255, 0.55);
}
.info-text {
background: rgba(29, 53, 87, 0.45);
padding: 18px;
border-radius: 12px;
margin: 15px 0 30px;
font-size: 1rem;
line-height: 1.7;
}
.info-text i {
color: var(--success);
margin-right: 10px;
min-width: 20px;
}
.btn-group {
display: flex;
gap: 18px;
margin-top: 15px;
}
button {
padding: 16px 35px;
border: none;
border-radius: 14px;
font-size: 1.15rem;
font-weight: 600;
cursor: pointer;
transition: all 0.35s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-start {
background: var(--success);
color: var(--dark);
flex: 2;
}
.btn-start:hover {
background: #3fb8d8;
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
.btn-stop {
background: var(--danger);
color: white;
flex: 1;
}
.btn-stop:hover {
background: #c1121f;
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
.btn-stop:disabled {
background: #6a6a6a;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.results-container {
padding: 0 35px 35px;
}
.stats {
display: flex;
justify-content: space-between;
background: rgba(29, 53, 87, 0.55);
padding: 18px 25px;
border-radius: 14px;
margin-bottom: 25px;
font-size: 1.15rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--success);
}
.stat-label {
font-size: 1rem;
color: #a8dadc;
margin-top: 8px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.results-header h2 {
font-size: 1.7rem;
}
.results-box {
background: rgba(29, 53, 87, 0.35);
border-radius: 14px;
height: 350px;
overflow-y: auto;
padding: 25px;
border: 1px solid rgba(168, 218, 220, 0.25);
}
.result-item {
padding: 14px 18px;
margin-bottom: 12px;
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
display: flex;
align-items: center;
animation: fadeIn 0.5s ease;
}
.result-item.success {
border-left: 5px solid var(--success);
}
.result-item.error {
border-left: 5px solid var(--danger);
}
.result-item.timeout {
border-left: 5px solid var(--warning);
}
.result-item i {
margin-right: 14px;
font-size: 1.3rem;
min-width: 24px;
}
.result-item.success i {
color: var(--success);
}
.result-item.error i {
color: var(--danger);
}
.result-item.timeout i {
color: var(--warning);
}
.url {
font-family: 'Courier New', Courier, monospace;
word-break: break-all;
}
.url-link {
color: #4cc9f0;
text-decoration: none;
transition: all 0.3s ease;
font-weight: 500;
}
.url-link:hover {
color: #90e0ef;
text-decoration: underline;
}
.progress-container {
margin-top: 25px;
background: rgba(255, 255, 255, 0.12);
border-radius: 12px;
height: 14px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--success);
border-radius: 12px;
width: 0%;
transition: width 0.7s ease;
}
.status-indicator {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 10px;
background-color: #6c757d;
}
.status-indicator.active {
background-color: var(--success);
box-shadow: 0 0 12px var(--success);
animation: pulse 1.5s infinite;
}
.status-text {
display: inline-flex;
align-items: center;
font-size: 1.1rem;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(76, 201, 240, 0.7); }
70% { box-shadow: 0 0 0 12px rgba(76, 201, 240, 0); }
100% { box-shadow: 0 0 0 0 rgba(76, 201, 240, 0); }
}
@media (max-width: 900px) {
.input-row {
flex-direction: column;
gap: 22px;
}
.btn-group {
flex-direction: column;
}
.stats {
flex-wrap: wrap;
gap: 20px;
}
.stat-item {
flex: 1;
min-width: 45%;
}
header h1 {
font-size: 2.2rem;
}
}
@media (max-width: 600px) {
header {
padding: 22px 25px;
}
header h1 {
font-size: 1.9rem;
}
header p {
font-size: 1rem;
}
.form-container, .results-container {
padding: 25px;
}
.stat-value {
font-size: 1.8rem;
}
.results-box {
height: 300px;
padding: 20px;
}
}
.btn-export {
background: var(--info);
color: var(--dark);
flex: 1;
}
.btn-export:hover {
background: #48cae4;
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
.btn-export:disabled {
background: #6a6a6a;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-globe-americas"></i>网站可用性扫描工具</h1>
<p>快速检测网站可用性,实时显示扫描结果</p>
</header>
<div class="form-container">
<div class="input-group">
<label for="urlTemplate"><i class="fas fa-link"></i> 网址模板</label>
<div class="input-field">
<input type="text" id="urlTemplate" placeholder="例如: https://baidu{}.cc" value="https://baidu{}.cc">
</div>
</div>
<div class="input-group">
<label><i class="fas fa-ruler-combined"></i> 扫描范围</label>
<div class="input-row">
<div class="input-field">
<input type="number" id="startNum" placeholder="起始数字" value="2100">
</div>
<div class="input-field">
<input type="number" id="endNum" placeholder="结束数字" value="2200">
</div>
<div class="input-field">
<input type="number" id="concurrency" placeholder="并发量" value="20" min="1" max="100">
</div>
</div>
</div>
<div class="input-row">
<div class="input-group" style="flex: 1;">
<label><i class="fas fa-check-circle"></i> 成功状态码</label>
<div class="input-field">
<input type="text" id="statusCodes" placeholder="例如: 200,201,301,302" value="200">
</div>
</div>
<div class="input-group" style="flex: 1;">
<label><i class="fas fa-stopwatch"></i> 超时时间(秒)</label>
<div class="input-field">
<input type="number" id="timeoutSeconds" placeholder="超时时间(秒)" value="5" min="1" max="60">
</div>
</div>
</div>
<div class="info-text">
<p><i class="fas fa-info-circle"></i> 扫描器将测试指定范围内的所有URL,当网站返回指定状态码时视为可用。</p>
<p><i class="fas fa-lightbulb"></i> 提示:并发量推荐10-20,超时时间推荐3-10秒,根据网络情况调整。</p>
<p><i class="fas fa-code"></i> 状态码支持多个,用逗号分隔,例如: 200,201,301,302</p>
</div>
<div class="btn-group">
<button id="startBtn" class="btn-start">
<i class="fas fa-play"></i> 开始扫描
</button>
<button id="stopBtn" class="btn-stop" disabled>
<i class="fas fa-stop"></i> 停止扫描
</button>
<button id="exportBtn" class="btn-export" disabled>
<i class="fas fa-download"></i> 导出结果
</button>
</div>
</div>
<div class="results-container">
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="totalCount">0</div>
<div class="stat-label">总网址</div>
</div>
<div class="stat-item">
<div class="stat-value" id="scannedCount">0</div>
<div class="stat-label">已扫描</div>
</div>
<div class="stat-item">
<div class="stat-value" id="successCount">0</div>
<div class="stat-label">成功数量</div>
</div>
<div class="stat-item">
<div class="stat-value" id="activeThreads">0</div>
<div class="stat-label">活动线程</div>
</div>
</div>
<div class="results-header">
<h2><i class="fas fa-list"></i> 扫描结果</h2>
<div id="statusText" class="status-text">
<span class="status-indicator"></span>
<span>准备就绪</span>
</div>
</div>
<div class="results-box" id="resultsBox">
<div class="result-item">
<i class="fas fa-info-circle"></i>
<span>扫描结果将显示在这里...</span>
</div>
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
</div>
<script>
// 全局变量
let isScanning = false;
let activeRequests = 0;
let totalUrls = 0;
let scannedUrls = 0;
let successUrls = 0;
let stopRequested = false;
let controller = null;
let successUrlsList = [];
// DOM 元素
const urlTemplateInput = document.getElementById('urlTemplate');
const startNumInput = document.getElementById('startNum');
const endNumInput = document.getElementById('endNum');
const concurrencyInput = document.getElementById('concurrency');
const statusCodesInput = document.getElementById('statusCodes');
const timeoutSecondsInput = document.getElementById('timeoutSeconds');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const resultsBox = document.getElementById('resultsBox');
const totalCountEl = document.getElementById('totalCount');
const scannedCountEl = document.getElementById('scannedCount');
const successCountEl = document.getElementById('successCount');
const activeThreadsEl = document.getElementById('activeThreads');
const statusText = document.getElementById('statusText');
const statusIndicator = document.querySelector('.status-indicator');
const progressBar = document.getElementById('progressBar');
const exportBtn = document.getElementById('exportBtn');
// 开始扫描
startBtn.addEventListener('click', async () => {
if (isScanning) return;
// 获取输入值
const urlTemplate = urlTemplateInput.value;
const startNum = parseInt(startNumInput.value);
const endNum = parseInt(endNumInput.value);
const concurrency = parseInt(concurrencyInput.value);
const timeoutSeconds = parseInt(timeoutSecondsInput.value);
// 处理状态码输入(支持中英文逗号)
const statusCodesStr = statusCodesInput.value
.replace(/,/g, ',') // 中文逗号替换为英文逗号
.replace(/\s/g, ''); // 移除空格
const statusCodes = statusCodesStr.split(',')
.map(code => parseInt(code))
.filter(code => !isNaN(code));
// 验证输入
if (!urlTemplate.includes('{}')) {
showError('网址模板必须包含{}作为数字占位符');
return;
}
if (isNaN(startNum)) {
showError('请输入有效的起始数字');
return;
}
if (isNaN(endNum)) {
showError('请输入有效的结束数字');
return;
}
if (startNum >= endNum) {
showError('起始数字必须小于结束数字');
return;
}
if (isNaN(concurrency) || concurrency < 1 || concurrency > 100) {
showError('并发量必须在1-100之间');
return;
}
if (isNaN(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 60) {
showError('超时时间必须在1-60秒之间');
return;
}
if (statusCodes.length === 0) {
showError('请输入有效的状态码');
return;
}
// 重置状态
resetScanner();
isScanning = true;
stopRequested = false;
startBtn.disabled = true;
stopBtn.disabled = false;
statusIndicator.classList.add('active');
// 生成URL列表
const urls = [];
for (let i = startNum; i <= endNum; i++) {
urls.push(urlTemplate.replace('{}', i));
}
totalUrls = urls.length;
totalCountEl.textContent = totalUrls;
// 创建AbortController用于停止扫描
controller = new AbortController();
// 更新状态
updateStatus('扫描中...', '#4cc9f0');
try {
// 使用并发控制执行扫描
await runConcurrentScan(urls, concurrency, statusCodes, timeoutSeconds * 1000);
} catch (error) {
console.error('扫描出错:', error);
updateStatus('扫描出错: ' + error.message, '#e63946');
}
// 扫描完成
isScanning = false;
startBtn.disabled = false;
stopBtn.disabled = true;
statusIndicator.classList.remove('active');
if (stopRequested) {
updateStatus('扫描已停止', '#e63946');
} else {
updateStatus(`扫描完成!找到${successUrls}个可用网址`, '#4cc9f0');
}
});
// 停止扫描
stopBtn.addEventListener('click', () => {
if (isScanning) {
stopRequested = true;
stopBtn.disabled = true;
updateStatus('正在停止...', '#e63946');
// 中止所有请求
if (controller) {
controller.abort();
}
}
});
// 导出功能
exportBtn.addEventListener('click', () => {
if (successUrlsList.length === 0) {
alert('没有可导出的成功URL');
return;
}
// 创建TXT文件内容
const content = successUrlsList.join('\n');
// 创建Blob对象
const blob = new Blob([content], { type: 'text/plain' });
// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `网站扫描结果_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
// 触发下载
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
// 运行并发扫描
async function runConcurrentScan(urls, concurrency, statusCodes, timeoutMs) {
const queue = [...urls];
const workers = [];
// 启动指定数量的工作线程
for (let i = 0; i < concurrency; i++) {
workers.push(worker(queue, statusCodes, timeoutMs));
}
// 等待所有工作线程完成
await Promise.all(workers);
}
// 工作线程函数
async function worker(queue, statusCodes, timeoutMs) {
while (queue.length > 0 && !stopRequested) {
const url = queue.shift();
if (!url) continue;
activeRequests++;
updateStats();
try {
const isAvailable = await checkUrlAvailability(url, statusCodes, timeoutMs);
scannedUrls++;
if (isAvailable) {
successUrls++;
addResult(url, 'success');
} else {
addResult(url, 'error');
}
updateStats();
updateProgress();
} catch (error) {
scannedUrls++;
if (error.name === 'TimeoutError') {
addResult(url, 'timeout');
} else if (error.name !== 'AbortError') {
addResult(url, 'error');
}
updateStats();
updateProgress();
} finally {
activeRequests--;
updateStats();
}
}
}
// 检查URL可用性 - 带自定义状态码和超时时间
async function checkUrlAvailability(url, statusCodes, timeoutMs) {
// 创建超时控制器
const timeoutController = new AbortController();
const timeoutId = setTimeout(() => {
timeoutController.abort();
}, timeoutMs);
try {
// 使用fetch发送HEAD请求
const response = await fetch(url, {
method: 'HEAD',
signal: timeoutController.signal,
cache: 'no-store',
redirect: 'manual'
});
// 检查状态码是否在允许列表中
return statusCodes.includes(response.status);
} catch (error) {
if (error.name === 'AbortError') {
// 超时
const timeoutError = new Error('请求超时');
timeoutError.name = 'TimeoutError';
throw timeoutError;
}
// 其他错误
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// 添加结果到列表
function addResult(url, status) {
const resultItem = document.createElement('div');
resultItem.className = `result-item ${status}`;
const icon = document.createElement('i');
if (status === 'success') {
icon.className = 'fas fa-check-circle';
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'url url-link';
link.textContent = url;
resultItem.appendChild(icon);
resultItem.appendChild(link);
// 添加到成功URL列表
successUrlsList.push(url);
} else {
if (status === 'error') {
icon.className = 'fas fa-times-circle';
} else if (status === 'timeout') {
icon.className = 'fas fa-clock';
}
const urlSpan = document.createElement('span');
urlSpan.className = 'url';
urlSpan.textContent = url;
resultItem.appendChild(icon);
resultItem.appendChild(urlSpan);
if (status === 'timeout') {
const timeoutText = document.createElement('span');
timeoutText.textContent = ' (超时)';
timeoutText.style.color = '#fca311';
timeoutText.style.marginLeft = '8px';
timeoutText.style.fontWeight = '500';
resultItem.appendChild(timeoutText);
}
}
// 如果有成功URL,启用导出按钮
if (successUrlsList.length > 0) {
exportBtn.disabled = false;
}
// 添加到结果列表顶部
resultsBox.insertBefore(resultItem, resultsBox.firstChild);
// 限制结果数量
if (resultsBox.children.length > 500) {
resultsBox.removeChild(resultsBox.lastChild);
}
// 滚动到顶部
resultsBox.scrollTop = 0;
}
// 更新统计信息
function updateStats() {
scannedCountEl.textContent = scannedUrls;
successCountEl.textContent = successUrls;
activeThreadsEl.textContent = activeRequests;
}
// 更新进度条
function updateProgress() {
const progress = totalUrls > 0 ? Math.min(100, (scannedUrls / totalUrls) * 100) : 0;
progressBar.style.width = `${progress}%`;
}
// 更新状态文本
function updateStatus(text, color) {
statusText.innerHTML = `<span class="status-indicator ${isScanning ? 'active' : ''}"></span> <span>${text}</span>`;
statusText.style.color = color;
}
// 重置扫描器
function resetScanner() {
resultsBox.innerHTML = '<div class="result-item"><i class="fas fa-info-circle"></i><span>扫描结果将显示在这里...</span></div>';
totalUrls = 0;
scannedUrls = 0;
successUrls = 0;
activeRequests = 0;
totalCountEl.textContent = '0';
scannedCountEl.textContent = '0';
successCountEl.textContent = '0';
activeThreadsEl.textContent = '0';
progressBar.style.width = '0%';
//重置成功URL列表
successUrlsList = [];
exportBtn.disabled = true;
}
// 显示错误信息
function showError(message) {
updateStatus(message, '#e63946');
// 3秒后清除错误信息
setTimeout(() => {
if (!isScanning) {
updateStatus('准备就绪', '');
}
}, 3000);
}
// 初始化页面
function init() {
resetScanner();
}
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
YXN-js
2025-06-22