同步服务器web页面
This commit is contained in:
98
web_page_2/http_server.py
Normal file
98
web_page_2/http_server.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import socket
|
||||||
|
|
||||||
|
class APIHandler(SimpleHTTPRequestHandler):
|
||||||
|
# 设置超时,避免长时间占用连接
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
# 自定义日志格式
|
||||||
|
print(f"[{self.log_date_time_string()}] {self.address_string()} - {format % args}")
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
try:
|
||||||
|
# 解析路径和查询参数
|
||||||
|
parsed_path = urllib.parse.urlparse(self.path)
|
||||||
|
path = parsed_path.path
|
||||||
|
query = parsed_path.query
|
||||||
|
|
||||||
|
# 检查是否是API接口
|
||||||
|
api_param = None
|
||||||
|
if path.startswith('/api/'):
|
||||||
|
api_param = path[5:] # 提取 /api/ 后面的数字
|
||||||
|
|
||||||
|
# 生成带参数的index.html URL
|
||||||
|
if api_param:
|
||||||
|
# 使用HTML文件并附加api参数
|
||||||
|
self.serve_file('index.html', query=f'api={api_param}')
|
||||||
|
elif path == '/' or path == '/index.html':
|
||||||
|
# 默认访问使用 api=1
|
||||||
|
self.serve_file('index.html', query='api=1')
|
||||||
|
else:
|
||||||
|
self.send_error(404, 'Not Found')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling request: {e}")
|
||||||
|
self.send_error(500, 'Internal Server Error')
|
||||||
|
|
||||||
|
def serve_file(self, filename, query=None):
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 如果有查询参数,修改HTML中的URL参数解析部分
|
||||||
|
if query:
|
||||||
|
try:
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
# 修改URL参数行,替换默认值为实际参数
|
||||||
|
content = content.replace(
|
||||||
|
"const apiParam = urlParams.get('api') || '1';",
|
||||||
|
f"const apiParam = '{query.split('=')[1]}';"
|
||||||
|
)
|
||||||
|
content = content.encode('utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error modifying HTML content: {e}")
|
||||||
|
|
||||||
|
self.wfile.write(content)
|
||||||
|
else:
|
||||||
|
self.send_error(404, f'{filename} not found')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error serving file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def check_port_available(port):
|
||||||
|
"""检查端口是否可用"""
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
try:
|
||||||
|
s.bind(('', port))
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run():
|
||||||
|
port = 8086
|
||||||
|
if not check_port_available(port):
|
||||||
|
print(f"错误: 端口 {port} 已被占用")
|
||||||
|
return
|
||||||
|
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = ThreadingHTTPServer(server_address, APIHandler)
|
||||||
|
print(f'Server running on http://localhost:{port}')
|
||||||
|
print(f'支持的接口: /, /api/1, /api/2, /api/3, /api/4, /api/5, /api/6, /api/7, /api/11-16')
|
||||||
|
print('按 Ctrl+C 停止服务器')
|
||||||
|
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('\n服务器已停止')
|
||||||
|
httpd.server_close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
388
web_page_2/index.html
Normal file
388
web_page_2/index.html
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>AI督察系统</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
color: #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.app-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #111827;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.log-panel {
|
||||||
|
height: 50%;
|
||||||
|
background: #020617;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.log-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #0f172a;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.log-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.log-info {
|
||||||
|
color: #60a5fa;
|
||||||
|
background: rgba(96, 165, 250, 0.1);
|
||||||
|
}
|
||||||
|
.log-success {
|
||||||
|
color: #34d399;
|
||||||
|
background: rgba(52, 211, 153, 0.1);
|
||||||
|
}
|
||||||
|
.log-warning {
|
||||||
|
color: #fbbf24;
|
||||||
|
background: rgba(251, 191, 36, 0.1);
|
||||||
|
}
|
||||||
|
.log-error {
|
||||||
|
color: #f87171;
|
||||||
|
background: rgba(248, 113, 113, 0.1);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
width: 320px;
|
||||||
|
border-right: 1px solid #1f2937;
|
||||||
|
background: #020617;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
height: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.message-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.message-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #111827;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.message-item:hover {
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
.message-item.active {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
.message-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.message-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
#liveImage {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.status-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.action-action {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #f87171;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
.action-face {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<main>
|
||||||
|
<aside class="left-panel">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">异常消息</div>
|
||||||
|
<div id="messageList" class="message-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="log-panel">
|
||||||
|
<div class="log-header">系统日志</div>
|
||||||
|
<div id="logContent" class="log-content"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<section class="content">
|
||||||
|
<img id="liveImage" alt="live" style="display: none;" />
|
||||||
|
<div id="status" class="status-bar">连接中...</div>
|
||||||
|
<div id="waitingPlaceholder" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #6b7280; font-size: 16px; text-align: center;">
|
||||||
|
<div>正在连接 WebSocket...</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 从URL参数获取配置,默认使用 api=1 的配置
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const apiParam = urlParams.get('api') || '1';
|
||||||
|
|
||||||
|
// 配置映射表
|
||||||
|
const configMap = {
|
||||||
|
'1': { port: 8765 },
|
||||||
|
'2': { port: 8767 },
|
||||||
|
'3': { port: 8769 },
|
||||||
|
'4': { port: 8771 },
|
||||||
|
'5': { port: 8767 },
|
||||||
|
'11': { port: 8501 },
|
||||||
|
'12': { port: 8502 },
|
||||||
|
'13': { port: 8503 },
|
||||||
|
'14': { port: 8504 },
|
||||||
|
'15': { port: 8505 },
|
||||||
|
'16': { port: 8506 },
|
||||||
|
'17': { port: 8511 },
|
||||||
|
'18': { port: 8512 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前配置,如果找不到则使用默认配置
|
||||||
|
const config = configMap[apiParam] || { port: 8765 };
|
||||||
|
|
||||||
|
const WS_PORT = config.port;
|
||||||
|
const WS_HOST = '29.1.70.11';
|
||||||
|
|
||||||
|
const liveImage = document.getElementById('liveImage');
|
||||||
|
const statusBar = document.getElementById('status');
|
||||||
|
const messageListEl = document.getElementById('messageList');
|
||||||
|
const logContent = document.getElementById('logContent');
|
||||||
|
|
||||||
|
let alerts = [];
|
||||||
|
let ws = null;
|
||||||
|
let wsConnected = false;
|
||||||
|
let currentDetectedActions = [];
|
||||||
|
|
||||||
|
function setMode(newMode) {
|
||||||
|
// 模式切换功能已禁用
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessages() {
|
||||||
|
messageListEl.innerHTML = '';
|
||||||
|
for (const msg of alerts.slice().reverse()) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message-item';
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'message-title';
|
||||||
|
title.textContent = `检测到异常: `;
|
||||||
|
|
||||||
|
msg.result_type.forEach(action => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'action-tag';
|
||||||
|
tag.classList.add('action-action');
|
||||||
|
const actionMap = {
|
||||||
|
'face': '检测到黑名单',
|
||||||
|
'Slap': '扇巴掌',
|
||||||
|
'LeavingPost': '离岗',
|
||||||
|
'Collision': '撞击',
|
||||||
|
'Push': '推搡',
|
||||||
|
'Lyingdown': '倒下',
|
||||||
|
'Hanging': '自缢',
|
||||||
|
'violation': '路线违规',
|
||||||
|
'prisoner': '检测到犯人',
|
||||||
|
'Only One': '单人单检',
|
||||||
|
'Nobody': '无人检查',
|
||||||
|
'Trunk Checked': '检查后备箱',
|
||||||
|
'Playing Phone': '玩手机',
|
||||||
|
'Unvaild Uniform!!': '违规着装',
|
||||||
|
'Unchecked Trunk': '未检查后备箱',
|
||||||
|
'Ignore': '漏检'
|
||||||
|
};
|
||||||
|
tag.textContent = actionMap[action] || action;
|
||||||
|
title.appendChild(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'message-meta';
|
||||||
|
const date = new Date(msg.timestamp * 1000);
|
||||||
|
meta.textContent = date.toLocaleString();
|
||||||
|
|
||||||
|
div.appendChild(title);
|
||||||
|
div.appendChild(meta);
|
||||||
|
|
||||||
|
messageListEl.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
logEntry.className = `log-entry log-${type}`;
|
||||||
|
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||||
|
|
||||||
|
logContent.appendChild(logEntry);
|
||||||
|
logContent.scrollTop = logContent.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
const wsUrl = `ws://${WS_HOST}:${WS_PORT}`;
|
||||||
|
statusBar.textContent = '连接中...';
|
||||||
|
|
||||||
|
addLog(`正在连接WebSocket: ${wsUrl}`, 'info');
|
||||||
|
addLog(`当前配置: api=${apiParam}, port=${WS_PORT}`, 'info');
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
wsConnected = true;
|
||||||
|
statusBar.textContent = '直播中';
|
||||||
|
addLog('WebSocket连接成功', 'success');
|
||||||
|
|
||||||
|
const placeholder = document.getElementById('waitingPlaceholder');
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
liveImage.style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
wsConnected = false;
|
||||||
|
statusBar.textContent = '连接断开,重试中...';
|
||||||
|
addLog('WebSocket连接断开,2秒后重试', 'warning');
|
||||||
|
setTimeout(() => {
|
||||||
|
connectWebSocket();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
console.error('WebSocket error', err);
|
||||||
|
addLog('WebSocket连接错误', 'error');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (msg.msg_type === 'frame') {
|
||||||
|
if (msg.image_base64) {
|
||||||
|
liveImage.src = `data:image/jpeg;base64,${msg.image_base64}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDetectedActions = msg.result_type || [];
|
||||||
|
|
||||||
|
if (currentDetectedActions.length > 0) {
|
||||||
|
const actionText = currentDetectedActions.join(', ');
|
||||||
|
statusBar.textContent = `直播中 | 检测到: ${actionText}`;
|
||||||
|
} else {
|
||||||
|
statusBar.textContent = '直播中';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.result_type && msg.result_type.length > 0) {
|
||||||
|
const alertMsg = {
|
||||||
|
camera_id: msg.camera_id,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
result_type: msg.result_type
|
||||||
|
};
|
||||||
|
|
||||||
|
<!-- const now = msg.timestamp;-->
|
||||||
|
<!-- const timeThreshold = now - 10;-->
|
||||||
|
|
||||||
|
<!-- const existingAlert = alerts.find(alert =>-->
|
||||||
|
<!-- JSON.stringify(alert.result_type) === JSON.stringify(alertMsg.result_type) &&-->
|
||||||
|
<!-- alert.timestamp > timeThreshold-->
|
||||||
|
<!-- );-->
|
||||||
|
|
||||||
|
<!-- if (!existingAlert) {-->
|
||||||
|
alerts.push(alertMsg);
|
||||||
|
|
||||||
|
renderMessages();
|
||||||
|
<!-- }-->
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('invalid ws message', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
addLog('AI督察系统启动', 'info');
|
||||||
|
addLog('界面初始化完成', 'success');
|
||||||
|
addLog(`接口参数: api=${apiParam}`, 'info');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
connectWebSocket();
|
||||||
|
}, 500);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
web_page_2/sfx_alert.mp3
Normal file
BIN
web_page_2/sfx_alert.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user