您现在的位置是:网站首页 > 博客日记 >

网站可用性扫描工具

作者: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