本来想着写个前端在线网页扫描飞牛设备,以实现像群晖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,点开始扫描等待即可。