一键扫描局域网飞牛设备

本来想着写个前端在线网页扫描飞牛设备,以实现像群晖Synology Web Assistant一样功能,无奈前端限制太多无法实现,于是有了后文前后端结合智谱清言AI写的代码(前端HTML后端Python),打包成一个exe可执行文件。

一、文件结构

lan-scanner-project/
├── main.py         # 后端Python程序
├── index.html      # 前端HTML代码
├── style.css       # 前端样式代码
└── icon.ico        # exe文件图标

将相应文件(夹)创建好备用

二、前端文件

将index.html文件内写入以下内容并保存

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>飞牛设备扫描工具 v2.0</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <!-- 头部 -->
        <header class="header">
            <h1>🔍 飞牛设备扫描工具</h1>
            <p class="subtitle">支持自定义IP段和端口扫描</p>
        </header>

        <!-- 系统信息 -->
        <div class="info-panel">
            <div class="info-item">
                <span class="label">本机IP:</span>
                <span class="value" id="localIp">检测中...</span>
            </div>
            <div class="info-item">
                <span class="label">服务器端口:</span>
                <span class="value" id="serverPort">15666</span>
            </div>
        </div>

        <!-- 扫描配置 -->
        <div class="section">
            <h2>⚙️ 扫描配置</h2>
            
            <!-- 模式切换 -->
            <div class="mode-selector">
                <label class="mode-option">
                    <input type="radio" name="scanMode" value="preset" checked onchange="toggleScanMode()">
                    <span>📡 预设网络段</span>
                </label>
                <label class="mode-option">
                    <input type="radio" name="scanMode" value="custom" onchange="toggleScanMode()">
                    <span>✏️ 自定义IP段</span>
                </label>
            </div>

            <!-- 预设网络段选择 -->
            <div id="presetMode" class="scan-mode">
                <div class="network-select">
                    <label class="input-label">选择网络段:</label>
                    <select id="networkSelect">
                        <option value="">正在加载网络段...</option>
                    </select>
                </div>
                <div class="hint">常见路由器网段,如华为(192.168.3.x)、小米(192.168.31.x)等</div>
            </div>

            <!-- 自定义IP段输入 -->
            <div id="customMode" class="scan-mode hidden">
                <div class="input-group">
                    <label class="input-label">自定义IP范围:</label>
                    <input type="text" id="customRange" class="input-field" 
                           placeholder="例如: 192.168.1.1-192.168.1.100">
                </div>
                <div class="hint">支持格式: CIDR(192.168.1.0/24) 或 范围(192.168.1.1-192.168.1.100)</div>
            </div>

            <!-- 端口设置 -->
            <div class="input-group">
                <label class="input-label">扫描端口:</label>
                <input type="number" id="scanPort" class="input-field" 
                       value="5666" min="1" max="65535">
                <span class="unit">端口</span>
            </div>
            <div class="hint">默认端口 5666(飞牛设备),可修改为其他端口</div>
        </div>

        <!-- 扫描控制 -->
        <div class="section">
            <h2>⚡ 扫描控制</h2>
            <div class="scan-controls">
                <button id="scanBtn" class="btn btn-primary" onclick="startScan()">
                    <span class="btn-icon">▶</span> 开始扫描
                </button>
                <button id="refreshBtn" class="btn btn-secondary" onclick="refreshNetworks()">
                    <span class="btn-icon">🔄</span> 刷新网络段
                </button>
                <button id="resetBtn" class="btn btn-secondary" onclick="resetConfig()">
                    <span class="btn-icon">↺</span> 重置配置
                </button>
            </div>
            
            <!-- 进度显示 -->
            <div id="progressSection" class="progress-section hidden">
                <div class="progress-bar-container">
                    <div class="progress-bar" id="progressBar">
                        <span class="progress-text" id="progressText">0%</span>
                    </div>
                </div>
                <p class="progress-info" id="progressInfo">正在扫描...</p>
            </div>
        </div>

        <!-- 扫描结果 -->
        <div class="section">
            <h2>📋 扫描结果 <span class="result-count" id="resultCount">(0)</span></h2>
            <div id="resultsContainer" class="results-container">
                <div class="no-results">
                    <div class="no-results-icon">📭</div>
                    <p>暂无扫描结果</p>
                    <p class="hint">配置扫描参数后点击"开始扫描"</p>
                </div>
            </div>
        </div>

        <!-- 设备列表模板 -->
        <template id="deviceTemplate">
            <div class="device-card">
                <div class="device-icon">🖥️</div>
                <div class="device-info">
                    <div class="device-ip"></div>
                    <div class="device-port"></div>
                    <div class="device-time"></div>
                </div>
                <div class="device-actions">
                    <a href="#" class="btn btn-sm btn-success" target="_blank">
                        <span>🔗</span> 访问
                    </a>
                    <button class="btn btn-sm btn-copy" onclick="copyIp(this)">
                        <span>📋</span> 复制IP
                    </button>
                </div>
            </div>
        </template>

        <!-- 页脚 -->
        <footer class="footer">
            <p>使用 Python 标准库构建 | 无需第三方依赖</p>
            <p class="hint">提示: 扫描时间取决于IP数量,请耐心等待</p>
        </footer>
    </div>

    <script>
        // 全局变量
        let isScanning = false;
        let progressInterval = null;
        let defaultScanPort = 5666;

        // API 调用函数
        async function fetchAPI(endpoint, params = {}) {
            const url = new URL(endpoint, window.location.origin);
            Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
            
            try {
                const response = await fetch(url);
                return await response.json();
            } catch (error) {
                console.error('API调用失败:', error);
                throw error;
            }
        }

        // 初始化函数
        async function init() {
            console.log('初始化飞牛设备扫描工具 v2.0...');
            await loadSystemInfo();
            await loadNetworks();
            console.log('初始化完成');
        }

        // 系统信息
        async function loadSystemInfo() {
            try {
                const info = await fetchAPI('/api/info');
                document.getElementById('localIp').textContent = info.local_ip || '未知';
                document.getElementById('serverPort').textContent = info.server_port;
                if (info.default_scan_port) {
                    defaultScanPort = info.default_scan_port;
                    document.getElementById('scanPort').value = defaultScanPort;
                }
            } catch (e) {
                document.getElementById('localIp').textContent = '获取失败';
            }
        }

        // 网络段管理
        async function loadNetworks() {
            const select = document.getElementById('networkSelect');
            try {
                const data = await fetchAPI('/api/networks');
                const networks = data.networks || [];
                let html = '<option value="">-- 请选择网络段 --</option>';
                networks.forEach(n => {
                    const selected = n.current ? ' selected' : '';
                    html += `<option value="${n.cidr}"${selected}>${n.name}</option>`;
                });
                select.innerHTML = html;
            } catch (e) {
                select.innerHTML = '<option value="">加载失败</option>';
            }
        }

        // 切换扫描模式
        function toggleScanMode() {
            const mode = document.querySelector('input[name="scanMode"]:checked').value;
            const presetMode = document.getElementById('presetMode');
            const customMode = document.getElementById('customMode');
            
            if (mode === 'preset') {
                presetMode.classList.remove('hidden');
                customMode.classList.add('hidden');
            } else {
                presetMode.classList.add('hidden');
                customMode.classList.remove('hidden');
            }
        }

        // 扫描功能
        async function startScan() {
            if (isScanning) {
                showMessage('正在扫描中,请稍候...', 'warning');
                return;
            }
            
            const mode = document.querySelector('input[name="scanMode"]:checked').value;
            const port = document.getElementById('scanPort').value;
            const params = { port };
            
            // 根据模式设置参数
            if (mode === 'preset') {
                const network = document.getElementById('networkSelect').value;
                if (!network) {
                    showMessage('请选择网络段', 'error');
                    return;
                }
                params.network = network;
            } else {
                const customRange = document.getElementById('customRange').value.trim();
                if (!customRange) {
                    showMessage('请输入自定义IP范围', 'error');
                    return;
                }
                params.custom_range = customRange;
            }
            
            try {
                const result = await fetchAPI('/api/scan', params);
                if (result.status === 'started') {
                    isScanning = true;
                    updateScanButton(true);
                    showProgress(true);
                    document.getElementById('progressInfo').textContent = result.message;
                    startProgressMonitoring();
                    showMessage(result.message, 'success');
                } else if (result.error) {
                    showMessage(result.error, 'error');
                }
            } catch (e) {
                showMessage('启动失败: ' + e.message, 'error');
            }
        }

        function startProgressMonitoring() {
            progressInterval = setInterval(async () => {
                try {
                    const progress = await fetchAPI('/api/progress');
                    updateProgressBar(progress.progress);
                    if (progress.devices && progress.devices.length > 0) {
                        displayResults(progress.devices);
                    }
                    if (!progress.scanning) {
                        stopProgressMonitoring();
                        showMessage('扫描完成!', 'success');
                        updateScanButton(false);
                    }
                } catch (e) {
                    // 忽略错误
                }
            }, 500);
        }

        function stopProgressMonitoring() {
            if (progressInterval) {
                clearInterval(progressInterval);
                progressInterval = null;
            }
            isScanning = false;
        }

        // 进度显示
        function showProgress(show) {
            const progressSection = document.getElementById('progressSection');
            if (show) {
                progressSection.classList.remove('hidden');
            } else {
                progressSection.classList.add('hidden');
            }
        }

        function updateProgressBar(percent) {
            const progressBar = document.getElementById('progressBar');
            const progressText = document.getElementById('progressText');
            
            const displayPercent = Math.round(percent);
            progressBar.style.width = displayPercent + '%';
            progressText.textContent = displayPercent + '%';
        }

        function updateScanButton(scanning) {
            const scanBtn = document.getElementById('scanBtn');
            if (scanning) {
                scanBtn.disabled = true;
                scanBtn.innerHTML = '<span class="btn-icon">⏳</span> 扫描中...';
                scanBtn.classList.add('btn-disabled');
            } else {
                scanBtn.disabled = false;
                scanBtn.innerHTML = '<span class="btn-icon">▶</span> 开始扫描';
                scanBtn.classList.remove('btn-disabled');
            }
        }

        // 结果显示
        function displayResults(devices) {
            const resultCount = document.getElementById('resultCount');
            const container = document.getElementById('resultsContainer');
            const template = document.getElementById('deviceTemplate');
            
            resultCount.textContent = `(${devices.length})`;
            
            if (devices.length === 0) {
                container.innerHTML = `
                    <div class="no-results">
                        <div class="no-results-icon">📭</div>
                        <p>未发现设备</p>
                    </div>
                `;
                return;
            }
            
            container.innerHTML = '';
            
            devices.forEach(device => {
                const clone = template.content.cloneNode(true);
                
                const ipDiv = clone.querySelector('.device-ip');
                const portDiv = clone.querySelector('.device-port');
                const timeDiv = clone.querySelector('.device-time');
                const link = clone.querySelector('a');
                
                ipDiv.textContent = `IP: ${device.ip}`;
                portDiv.textContent = `端口: ${device.port}`;
                timeDiv.textContent = `响应: ${device.response_time}ms`;
                link.href = device.url;
                
                container.appendChild(clone);
            });
        }

        // 刷新和重置
        async function refreshNetworks() {
            await loadNetworks();
            showMessage('已刷新网络段列表');
        }

        function resetConfig() {
            document.getElementById('scanPort').value = defaultScanPort;
            document.getElementById('customRange').value = '';
            document.querySelector('input[value="preset"]').checked = true;
            toggleScanMode();
            showMessage('配置已重置');
        }

        // 工具函数
        function copyIp(button) {
            const deviceCard = button.closest('.device-card');
            const ipText = deviceCard.querySelector('.device-ip').textContent.replace('IP: ', '');
            
            navigator.clipboard.writeText(ipText).then(() => {
                const originalText = button.innerHTML;
                button.innerHTML = '<span>✅</span> 已复制';
                button.classList.add('btn-success');
                
                setTimeout(() => {
                    button.innerHTML = originalText;
                    button.classList.remove('btn-success');
                }, 1500);
            }).catch(err => {
                showMessage('复制失败: ' + err.message, 'error');
            });
        }

        function showMessage(message, type = 'info') {
            const toast = document.createElement('div');
            toast.className = `toast toast-${type}`;
            toast.textContent = message;
            
            document.body.appendChild(toast);
            
            setTimeout(() => {
                toast.classList.add('show');
            }, 10);
            
            setTimeout(() => {
                toast.classList.remove('show');
                setTimeout(() => {
                    document.body.removeChild(toast);
                }, 300);
            }, 3000);
        }

        // 页面加载完成后初始化
        document.addEventListener('DOMContentLoaded', init);
    </script>
</body>
</html>

三、后端文件

在main.py文件内写入以下内容并保存

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
飞牛设备扫描工具 v2.0 - 后端
使用Python标准库实现,无需第三方依赖
支持自定义IP段和端口扫描
"""

import http.server
import socketserver
import socket
import threading
import json
import os
import sys
import time
import ipaddress
import concurrent.futures
import webbrowser
from urllib.parse import urlparse, parse_qs

# ============================================
# 配置参数
# ============================================
PORT = 15666  # 服务器端口(修改为15666)
DEFAULT_SCAN_PORT = 5666  # 默认扫描端口(飞牛设备端口)
SCAN_TIMEOUT = 0.5  # 扫描超时时间(秒)
MAX_WORKERS = 50  # 最大并发扫描数

# ============================================
# 网络工具函数
# ============================================

def get_local_ip():
    """获取本机局域网IP地址"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        local_ip = s.getsockname()[0]
        s.close()
        return local_ip
    except Exception as e:
        print(f"获取本机IP失败: {e}")
        return None

def get_possible_networks(local_ip):
    """获取可能的局域网网段列表"""
    if not local_ip:
        return []
    
    networks = []
    try:
        ip_parts = local_ip.split('.')
        base_network = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.0/24"
        networks.append({
            "cidr": base_network,
            "name": f"本机网段 ({base_network})",
            "current": True
        })
        
        common_networks = [
            "192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24",
            "192.168.3.0/24", "192.168.4.0/24", "192.168.5.0/24",
            "192.168.10.0/24", "192.168.31.0/24", "192.168.50.0/24",
            "192.168.100.0/24", "10.0.0.0/24", "172.16.0.0/24",
        ]
        
        for net in common_networks:
            if net != base_network:
                networks.append({
                    "cidr": net, 
                    "name": net, 
                    "current": False
                })
                
    except Exception as e:
        print(f"获取网段失败: {e}")
    
    return networks

def parse_ip_range(ip_input):
    """
    解析IP范围输入
    支持格式:
    1. CIDR格式: 192.168.1.0/24
    2. 起始-结束: 192.168.1.1-192.168.1.100
    3. 单个IP: 192.168.1.1
    返回IP列表
    """
    ip_list = []
    ip_input = ip_input.strip()
    
    try:
        # 尝试CIDR格式
        if '/' in ip_input and '-' not in ip_input:
            network = ipaddress.ip_network(ip_input, strict=False)
            ip_list = [str(ip) for ip in network.hosts()]
        # 尝试范围格式: 192.168.1.1-192.168.1.100
        elif '-' in ip_input:
            parts = ip_input.split('-')
            if len(parts) == 2:
                start_ip = ipaddress.ip_address(parts[0].strip())
                end_ip = ipaddress.ip_address(parts[1].strip())
                current = start_ip
                while current <= end_ip:
                    ip_list.append(str(current))
                    current += 1
        # 尝试单个IP
        else:
            ip_obj = ipaddress.ip_address(ip_input)
            ip_list = [str(ip_obj)]
    except Exception as e:
        print(f"解析IP范围失败: {e}")
    
    return ip_list

def check_port(ip, port, timeout=SCAN_TIMEOUT):
    """检查指定IP的端口是否开放"""
    try:
        start_time = time.time()
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        result = s.connect_ex((ip, port))
        s.close()
        response_time = (time.time() - start_time) * 1000
        is_open = (result == 0)
        return (ip, port, is_open, round(response_time, 2))
    except Exception:
        return (ip, port, False, 0)

def scan_ips(ip_list, port=DEFAULT_SCAN_PORT, progress_callback=None):
    """扫描指定IP列表的端口"""
    devices = []
    total = len(ip_list)
    
    if total == 0:
        return devices
    
    print(f"开始扫描 {total} 个IP,端口: {port}...")
    
    try:
        with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
            # 提交所有扫描任务
            future_to_ip = {
                executor.submit(check_port, ip, port): ip 
                for ip in ip_list
            }
            
            scanned = 0
            # 处理结果
            for future in concurrent.futures.as_completed(future_to_ip):
                scanned += 1
                ip, port, is_open, response_time = future.result()
                
                # 更新进度
                if progress_callback:
                    progress = (scanned / total) * 100
                    progress_callback(progress, scanned, total)
                
                if is_open:
                    devices.append({
                        "ip": ip,
                        "port": port,
                        "response_time": response_time,
                        "status": "online",
                        "url": f"http://{ip}:{port}"
                    })
                    print(f"发现设备: {ip}:{port} (响应时间: {response_time}ms)")
        
        print(f"扫描完成,共发现 {len(devices)} 个设备")
        
    except Exception as e:
        print(f"扫描失败: {e}")
    
    return devices

# ============================================
# HTTP请求处理器
# ============================================

class FeniuScanHandler(http.server.SimpleHTTPRequestHandler):
    scanning = False
    scan_progress = 0
    scan_devices = []
    
    def __init__(self, *args, **kwargs):
        self.base_path = os.path.dirname(os.path.abspath(__file__))
        super().__init__(*args, **kwargs)
    
    def log_message(self, format, *args):
        print(f"[{self.log_date_time_string()}] {format % args}")
    
    def do_GET(self):
        parsed_url = urlparse(self.path)
        query = parse_qs(parsed_url.query)
        
        # 静态文件服务
        if parsed_url.path in ['/', '/index.html']:
            self.serve_file('index.html', 'text/html')
        elif parsed_url.path == '/style.css':
            self.serve_file('style.css', 'text/css')
        
        # API接口
        elif parsed_url.path == '/api/info':
            local_ip = get_local_ip()
            self.send_json({
                "local_ip": local_ip,
                "server_port": PORT,
                "default_scan_port": DEFAULT_SCAN_PORT
            })
        elif parsed_url.path == '/api/networks':
            local_ip = get_local_ip()
            self.send_json({"networks": get_possible_networks(local_ip)})
        elif parsed_url.path == '/api/scan':
            # 支持两种模式:从预设网络段扫描或自定义IP范围扫描
            network = query.get('network', [''])[0]
            custom_range = query.get('custom_range', [''])[0]
            port = query.get('port', [str(DEFAULT_SCAN_PORT)])[0]
            
            # 验证端口号
            try:
                port = int(port)
                if port < 1 or port > 65535:
                    raise ValueError("端口范围无效")
            except:
                port = DEFAULT_SCAN_PORT
            
            # 检查是否正在扫描
            if FeniuScanHandler.scanning:
                self.send_json({"error": "正在扫描中,请稍候"}, 400)
                return
            
            # 解析IP范围
            ip_list = []
            if custom_range:
                # 自定义IP范围模式
                ip_list = parse_ip_range(custom_range)
            elif network:
                # 预设网络段模式
                try:
                    net = ipaddress.ip_network(network, strict=False)
                    ip_list = [str(ip) for ip in net.hosts()]
                except Exception as e:
                    self.send_json({"error": f"网络段格式错误: {e}"}, 400)
                    return
            else:
                self.send_json({
                    "error": "请选择网络段或输入自定义IP范围"
                }, 400)
                return
            
            # 验证IP列表
            if len(ip_list) == 0:
                self.send_json({"error": "IP范围无效或为空"}, 400)
                return
            
            # 启动后台扫描线程
            FeniuScanHandler.scanning = True
            FeniuScanHandler.scan_progress = 0
            FeniuScanHandler.scan_devices = []
            
            def progress_callback(p, s, t):
                FeniuScanHandler.scan_progress = p
            
            def scan_thread():
                try:
                    FeniuScanHandler.scan_devices = scan_ips(
                        ip_list, port, progress_callback
                    )
                finally:
                    FeniuScanHandler.scanning = False
            
            threading.Thread(target=scan_thread, daemon=True).start()
            
            self.send_json({
                "status": "started",
                "message": f"开始扫描 {len(ip_list)} 个IP,端口 {port}"
            })
        elif parsed_url.path == '/api/progress':
            self.send_json({
                "scanning": FeniuScanHandler.scanning,
                "progress": FeniuScanHandler.scan_progress,
                "devices": FeniuScanHandler.scan_devices
            })
        elif parsed_url.path == '/api/results':
            self.send_json({
                "devices": FeniuScanHandler.scan_devices,
                "count": len(FeniuScanHandler.scan_devices)
            })
        else:
            self.send_error(404, "Not Found")
    
    def serve_file(self, filename, content_type):
        """服务静态文件"""
        file_path = os.path.join(self.base_path, filename)
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            self.send_response(200)
            self.send_header('Content-Type', f'{content_type}; charset=utf-8')
            self.send_header('Content-Length', len(content.encode('utf-8')))
            self.send_header('Cache-Control', 'no-cache')
            self.end_headers()
            self.wfile.write(content.encode('utf-8'))
        except FileNotFoundError:
            self.send_error(404, f"File not found: {filename}")
    
    def send_json(self, data, status_code=200):
        """发送JSON响应"""
        json_str = json.dumps(data, ensure_ascii=False, indent=2)
        self.send_response(status_code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', len(json_str.encode('utf-8')))
        self.send_header('Cache-Control', 'no-cache')
        self.end_headers()
        self.wfile.write(json_str.encode('utf-8'))

# ============================================
# 自动打开浏览器函数
# ============================================

def open_browser():
    """延迟打开浏览器"""
    time.sleep(0.5)  # 等待服务器启动
    url = f'http://localhost:{PORT}'
    try:
        webbrowser.open(url)
        print(f"✓ 已自动打开浏览器: {url}\n")
    except Exception as e:
        print(f"⚠ 无法自动打开浏览器,请手动访问: {url}\n")

# ============================================
# 主程序
# ============================================

def main():
    """主函数"""
    print("""
╔═══════════════════════════════════════════════════════════╗
║                                                           ║
║          飞牛设备扫描工具 v2.0                             ║
║                                                           ║
║    支持自定义IP段和端口扫描                                ║
║                                                           ║
╚═══════════════════════════════════════════════════════════╝
""")
    
    # 获取本机信息
    local_ip = get_local_ip()
    print(f"本机IP地址: {local_ip}")
    print(f"服务器监听端口: {PORT}")
    print(f"默认扫描端口: {DEFAULT_SCAN_PORT}")
    
    if local_ip:
        print(f"访问地址: http://{local_ip}:{PORT}")
    print(f"本地访问: http://localhost:{PORT}")
    
    print("\n" + "="*60)
    print("服务器启动中...")
    print("按 Ctrl+C 停止服务器")
    print("="*60 + "\n")
    
    try:
        # 创建服务器
        with socketserver.TCPServer(("", PORT), FeniuScanHandler) as httpd:
            # 允许端口复用
            httpd.allow_reuse_address = True
            
            print(f"✓ 服务器已启动,监听端口 {PORT}\n")
            
            # 在新线程中打开浏览器
            browser_thread = threading.Thread(target=open_browser, daemon=True)
            browser_thread.start()
            
            # 启动服务器
            httpd.serve_forever()
            
    except KeyboardInterrupt:
        print("\n\n正在停止服务器...")
        httpd.shutdown()
        print("服务器已停止")
    except OSError as e:
        print(f"\n错误: 端口 {PORT} 可能被占用")
        print(f"详细信息: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"\n发生错误: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

四、前端样式

在style.css文件内写入以下内容并保存

/* ============================================
   飞牛设备扫描工具 v2.0 - 样式表
   ============================================ */

/* 重置和基础样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 
                 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', 
                 Helvetica, Arial, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
    color: #333;
}

.container {
    max-width: 900px;
    margin: 0 auto;
}

/* 头部 */
.header {
    text-align: center;
    color: white;
    margin-bottom: 30px;
}

.header h1 {
    font-size: 2.5em;
    margin-bottom: 10px;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}

.subtitle {
    font-size: 1.1em;
    opacity: 0.9;
}

/* 信息面板 */
.info-panel {
    background: white;
    border-radius: 12px;
    padding: 20px;
    margin-bottom: 20px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    display: flex;
    justify-content: space-around;
    flex-wrap: wrap;
    gap: 15px;
}

.info-item {
    display: flex;
    align-items: center;
    gap: 10px;
}

.info-item .label {
    font-weight: 600;
    color: #666;
}

.info-item .value {
    color: #667eea;
    font-weight: bold;
    font-family: monospace;
    font-size: 1.1em;
}

/* 区块 */
.section {
    background: white;
    border-radius: 12px;
    padding: 25px;
    margin-bottom: 20px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.section h2 {
    font-size: 1.5em;
    margin-bottom: 20px;
    color: #333;
    border-bottom: 2px solid #f0f0f0;
    padding-bottom: 10px;
}

/* 模式选择器 */
.mode-selector {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}

.mode-option {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 20px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px solid transparent;
    transition: all 0.3s;
}

.mode-option:hover {
    background: #e9ecef;
}

.mode-option input:checked + span {
    color: #667eea;
    font-weight: 600;
}

.mode-option:has(input:checked) {
    border-color: #667eea;
    background: #f0f4ff;
}

/* 扫描模式区域 */
.scan-mode {
    margin-bottom: 20px;
}

.scan-mode.hidden {
    display: none;
}

/* 输入组 */
.input-group {
    margin-bottom: 15px;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.input-label {
    font-weight: 600;
    color: #333;
    min-width: 100px;
}

.input-field {
    flex: 1;
    min-width: 200px;
    padding: 10px 15px;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    font-size: 1em;
    transition: border-color 0.3s;
}

.input-field:focus {
    outline: none;
    border-color: #667eea;
}

.unit {
    color: #666;
    font-size: 0.9em;
}

/* 网络段选择 */
.network-select {
    margin-bottom: 15px;
}

.network-select select {
    width: 100%;
    padding: 10px 15px;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    font-size: 1em;
    background: white;
    cursor: pointer;
    transition: border-color 0.3s;
}

.network-select select:focus {
    outline: none;
    border-color: #667eea;
}

/* 提示文字 */
.hint {
    font-size: 0.85em;
    color: #999;
    margin-top: 5px;
}

/* 扫描控制按钮 */
.scan-controls {
    display: flex;
    gap: 15px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}

/* 按钮样式 */
.btn {
    padding: 12px 24px;
    border: none;
    border-radius: 8px;
    font-size: 1em;
    font-weight: 600;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    text-decoration: none;
    transition: all 0.3s;
}

.btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.btn:active {
    transform: translateY(0);
}

.btn-icon {
    font-size: 1.2em;
}

.btn-primary {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}

.btn-primary:hover {
    box-shadow: 0 6px 12px rgba(102, 126, 234, 0.4);
}

.btn-secondary {
    background: #f0f0f0;
    color: #333;
}

.btn-secondary:hover {
    background: #e0e0e0;
}

.btn-success {
    background: #10b981;
    color: white;
}

.btn-success:hover {
    background: #059669;
}

.btn-disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.btn-disabled:hover {
    transform: none;
    box-shadow: none;
}

.btn-sm {
    padding: 8px 16px;
    font-size: 0.9em;
}

/* 进度条 */
.progress-section.hidden {
    display: none;
}

.progress-bar-container {
    width: 100%;
    height: 30px;
    background: #f0f0f0;
    border-radius: 15px;
    overflow: hidden;
    position: relative;
    margin-bottom: 10px;
}

.progress-bar {
    height: 100%;
    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
    border-radius: 15px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: width 0.3s ease;
    min-width: 50px;
}

.progress-text {
    color: white;
    font-weight: bold;
    font-size: 0.9em;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}

.progress-info {
    text-align: center;
    color: #666;
    font-size: 0.95em;
}

/* 结果计数 */
.result-count {
    font-size: 0.8em;
    color: #666;
    margin-left: 10px;
    font-weight: normal;
}

/* 结果容器 */
.results-container {
    min-height: 100px;
}

/* 无结果提示 */
.no-results {
    text-align: center;
    padding: 40px 20px;
    color: #999;
}

.no-results-icon {
    font-size: 4em;
    margin-bottom: 15px;
}

/* 设备卡片 */
.device-card {
    display: flex;
    align-items: center;
    background: #f8f9fa;
    border-radius: 10px;
    padding: 20px;
    margin-bottom: 15px;
    border-left: 4px solid #667eea;
    transition: all 0.3s;
}

.device-card:hover {
    background: #f0f2f5;
    transform: translateX(5px);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.device-icon {
    font-size: 3em;
    margin-right: 20px;
    flex-shrink: 0;
}

.device-info {
    flex: 1;
    min-width: 0;
}

.device-info > div {
    margin-bottom: 5px;
    color: #333;
}

.device-ip {
    font-size: 1.2em;
    font-weight: bold;
    color: #667eea;
    font-family: monospace;
}

.device-port {
    color: #666;
    font-size: 0.95em;
}

.device-time {
    color: #999;
    font-size: 0.9em;
}

.device-actions {
    display: flex;
    gap: 10px;
    margin-left: 20px;
    flex-shrink: 0;
}

/* 页脚 */
.footer {
    text-align: center;
    color: white;
    padding: 20px;
    margin-top: 20px;
}

.footer p {
    margin-bottom: 8px;
    opacity: 0.9;
}

/* Toast 消息提示 */
.toast {
    position: fixed;
    top: 20px;
    right: 20px;
    padding: 15px 25px;
    border-radius: 8px;
    color: white;
    font-weight: 600;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: translateX(400px);
    opacity: 0;
    transition: all 0.3s ease;
    z-index: 1000;
}

.toast.show {
    transform: translateX(0);
    opacity: 1;
}

.toast-info {
    background: #3b82f6;
}

.toast-success {
    background: #10b981;
}

.toast-warning {
    background: #f59e0b;
}

.toast-error {
    background: #ef4444;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .header h1 {
        font-size: 1.8em;
    }
    
    .subtitle {
        font-size: 1em;
    }
    
    .info-panel {
        flex-direction: column;
        text-align: center;
    }
    
    .mode-selector,
    .scan-controls,
    .device-actions {
        flex-direction: column;
    }
    
    .mode-option,
    .btn {
        width: 100%;
        justify-content: center;
    }
    
    .input-group {
        flex-direction: column;
        align-items: flex-start;
    }
    
    .input-field {
        width: 100%;
    }
    
    .device-card {
        flex-direction: column;
        text-align: center;
        padding: 15px;
    }
    
    .device-icon {
        margin-right: 0;
        margin-bottom: 15px;
    }
    
    .device-actions {
        margin-left: 0;
        margin-top: 15px;
        width: 100%;
        justify-content: center;
        flex-wrap: wrap;
    }
    
    .toast {
        left: 20px;
        right: 20px;
        top: 10px;
    }
}

/* 动画效果 */
@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: 0.5;
    }
}

.scanning .progress-bar {
    animation: pulse 1.5s ease-in-out infinite;
}

/* 滚动条样式 */
::-webkit-scrollbar {
    width: 8px;
}

::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
}

::-webkit-scrollbar-thumb {
    background: #888;
    border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
    background: #555;
}

五、编译安装

5.1 安装Python

去官网下载安装:https://www.python.org/downloads/windows/

5.2 安装工具

PyInstaller Python 应用程序打包成独立可执行文件的工具

pip install pyinstaller

5.3 编译文件

pyinstaller --onefile -n 飞牛扫描工具 --icon=icon.ico --add-data "index.html;." --add-data "style.css;." main.py

六、成品使用

在 dist 文件夹中找到exe文件,双击运行程序,允许通过防火墙,程序会自动打开浏览器访问 http://localhost:15666

选择扫描飞牛默认端口5666,点开始扫描等待即可。

本文采用 CC BY-NC-SA 3.0 Unported 许可,转载请以超链接注明出处。
原文地址:一键扫描局域网飞牛设备 作者:松鼠小
上一篇
下一篇