400 lines
11 KiB
HTML
400 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>指监智能场景识别系统</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);
|
||
}
|
||
.action-takeout {
|
||
background: rgba(139, 92, 246, 0.2);
|
||
color: #a78bfa;
|
||
border: 1px solid rgba(139, 92, 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>
|
||
const WS_PORT = 8765;
|
||
const WS_HOST = 'localhost';
|
||
|
||
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';
|
||
|
||
if (msg.msg_type === 'take_out') {
|
||
// 处理take_out类型的消息
|
||
title.textContent = `人员带出: `;
|
||
|
||
const tag = document.createElement('span');
|
||
tag.className = 'action-tag action-takeout';
|
||
tag.textContent = msg.person_name;
|
||
title.appendChild(tag);
|
||
|
||
const countTag = document.createElement('span');
|
||
countTag.className = 'action-tag action-face';
|
||
countTag.textContent = `第${msg.historical_alert_count}次`;
|
||
title.appendChild(countTag);
|
||
} else {
|
||
// 处理原有的异常检测消息
|
||
title.textContent = `检测到异常: `;
|
||
|
||
msg.result_type.forEach(action => {
|
||
const tag = document.createElement('span');
|
||
tag.className = 'action-tag';
|
||
|
||
if (action === 'face') {
|
||
tag.classList.add('action-face');
|
||
tag.textContent = '检测到黑名单';
|
||
} else {
|
||
tag.classList.add('action-action');
|
||
const actionMap = {
|
||
'Slap': '扇巴掌',
|
||
'LeavingPost': '离岗',
|
||
'Collision': '撞击',
|
||
'Push': '推搡',
|
||
'Lyingdown': '倒下',
|
||
'Hanging': '自缢'
|
||
};
|
||
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');
|
||
|
||
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();
|
||
|
||
const alertTypes = alertMsg.result_type.join(', ');
|
||
addLog(`检测到异常: ${alertTypes}`, 'warning');
|
||
}
|
||
} else if (msg.msg_type === 'take_out') {
|
||
// 处理take_out类型的消息
|
||
const takeOutMsg = {
|
||
msg_type: 'take_out',
|
||
person_name: msg.person_name,
|
||
historical_alert_count: msg.historical_alert_count || 0,
|
||
timestamp: Math.floor(Date.now() / 1000)
|
||
};
|
||
|
||
alerts.push(takeOutMsg);
|
||
renderMessages();
|
||
|
||
addLog(`人员带出: ${takeOutMsg.person_name} (历史告警: ${takeOutMsg.historical_alert_count})`, 'warning');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('invalid ws message', e);
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
|
||
// 初始化
|
||
addLog('AI Monitor 系统启动', 'info');
|
||
addLog('界面初始化完成', 'success');
|
||
|
||
setTimeout(() => {
|
||
connectWebSocket();
|
||
}, 500);
|
||
</script>
|
||
</body>
|
||
</html>
|