268 lines
15 KiB
HTML
268 lines
15 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>管控台布局 v2</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", sans-serif; background: #1a1d23; color: #e0e0e0; padding: 24px; }
|
||
h1 { font-size: 18px; margin-bottom: 20px; color: #ccc; }
|
||
|
||
.tabs { display: flex; gap: 0; margin-bottom: 20px; border-bottom: 1px solid #333; }
|
||
.tab { padding: 10px 24px; cursor: pointer; border-bottom: 2px solid transparent; color: #999; font-size: 14px; }
|
||
.tab.active { color: #fff; border-bottom-color: #4f9cf7; }
|
||
|
||
.stats { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
|
||
.stat { background: #23262e; padding: 10px 16px; border-radius: 8px; font-size: 12px; color: #999; }
|
||
.stat strong { display: block; font-size: 22px; color: #fff; margin-top: 2px; }
|
||
|
||
/* Device row - HORIZONTAL */
|
||
.device-row { background: #23262e; border-radius: 10px; margin-bottom: 12px; padding: 16px; }
|
||
.device-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
||
.device-status.online { width: 8px; height: 8px; border-radius: 50%; background: #4caf50; flex-shrink: 0; }
|
||
.device-status.busy { width: 8px; height: 8px; border-radius: 50%; background: #ff9800; flex-shrink: 0; animation: pulse 1s infinite; }
|
||
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
||
.device-name { font-weight: 600; font-size: 15px; white-space: nowrap; }
|
||
.device-meta { font-size: 11px; color: #888; white-space: nowrap; }
|
||
.device-busy { font-size: 11px; color: #ff9800; background: #2d323e; padding: 2px 8px; border-radius: 4px; }
|
||
|
||
/* Body: preview LEFT | controls RIGHT */
|
||
.device-body { display: flex; gap: 16px; min-height: 200px; }
|
||
|
||
/* --- LEFT: Preview area (large, 4 video tiles) --- */
|
||
.preview-area { flex: 1; min-width: 400px; background: #111; border-radius: 6px; padding: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
||
.preview-tile { background: #000; border-radius: 4px; aspect-ratio: 16/9; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #444; position: relative; overflow: hidden; }
|
||
.preview-tile .label { position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.7); color: #aaa; padding: 1px 6px; border-radius: 3px; font-size: 10px; }
|
||
.preview-tile.empty { background: #0a0a0a; }
|
||
|
||
/* --- RIGHT: Controls (fixed width) --- */
|
||
.controls { width: 340px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px; }
|
||
|
||
.feature-group { background: #2a2e38; border-radius: 6px; padding: 10px; }
|
||
.feature-group .title { font-size: 11px; color: #888; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.feature-group label { display: block; font-size: 12px; margin-bottom: 4px; cursor: pointer; color: #ccc; }
|
||
.feature-group label input { margin-right: 6px; }
|
||
|
||
.cam-assign { background: #2a2e38; border-radius: 6px; padding: 10px; flex: 1; }
|
||
.cam-assign .title { font-size: 11px; color: #888; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.cam-chip { display: inline-flex; align-items: center; gap: 4px; background: #363b48; padding: 4px 8px; border-radius: 4px; font-size: 11px; margin: 2px; }
|
||
.cam-chip .remove { cursor: pointer; color: #f66; font-weight: bold; font-size: 13px; line-height: 1; }
|
||
.cam-add { font-size: 11px; color: #4f9cf7; cursor: pointer; padding: 4px 0; display: inline-block; }
|
||
|
||
/* Unassigned pool */
|
||
.unassigned { background: #23262e; border-radius: 10px; padding: 16px; margin-top: 16px; }
|
||
.unassigned-title { font-size: 13px; color: #888; margin-bottom: 10px; }
|
||
.unassigned-grid { display: flex; gap: 10px; flex-wrap: wrap; }
|
||
.unassigned-card { background: #2d323e; border-radius: 8px; padding: 12px; width: 180px; }
|
||
.unassigned-card .name { font-size: 13px; margin-bottom: 4px; }
|
||
.unassigned-card .meta { font-size: 11px; color: #888; }
|
||
.unassigned-card .btn { margin-top: 6px; font-size: 11px; color: #4f9cf7; cursor: pointer; }
|
||
|
||
/* Save bar */
|
||
.save-bar { margin-top: 20px; display: flex; gap: 12px; align-items: center; }
|
||
.btn-primary { background: #4f9cf7; color: #fff; padding: 10px 32px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||
.btn-secondary { background: #2d323e; color: #ccc; padding: 10px 24px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
||
.save-status { font-size: 12px; color: #888; }
|
||
.save-status .dot-busy { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #ff9800; margin-right: 4px; animation: pulse 1s infinite; }
|
||
.save-status .dot-ok { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #4caf50; margin-right: 4px; }
|
||
|
||
/* Video view */
|
||
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||
.video-card { background: #23262e; border-radius: 10px; overflow: hidden; }
|
||
.video-preview { height: 160px; background: #000; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #555; }
|
||
.video-info { padding: 10px; }
|
||
.video-info .name { font-size: 13px; font-weight: 500; }
|
||
.video-info .device-tag { font-size: 11px; color: #4f9cf7; margin-top: 2px; }
|
||
.video-info .assign { font-size: 11px; color: #888; margin-top: 4px; display: flex; align-items: center; gap: 6px; }
|
||
.video-info select { background: #2d323e; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 2px 6px; font-size: 11px; }
|
||
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
.badge { display: inline-block; background: #2d323e; padding: 2px 8px; border-radius: 10px; font-size: 10px; color: #aaa; margin-left: 6px; }
|
||
.badge.unalloc { color: #ff9800; background: #2d323e; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<h1>设备管控台 · 布局 v2</h1>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchTab('device')">📋 设备视图</div>
|
||
<div class="tab" onclick="switchTab('video')">🎥 视频视图</div>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat">设备 <strong>3</strong></div>
|
||
<div class="stat">摄像头 <strong>12</strong></div>
|
||
<div class="stat">已分配 <strong>8</strong></div>
|
||
<div class="stat">未分配 <strong>4</strong></div>
|
||
</div>
|
||
|
||
<!-- ====== 设备视图 ====== -->
|
||
<div id="panel-device" class="panel active">
|
||
|
||
<!-- 设备行 1 - 正常 -->
|
||
<div class="device-row">
|
||
<div class="device-header">
|
||
<div class="device-status online"></div>
|
||
<div class="device-name">盒子A · 生产车间</div>
|
||
<div class="device-meta">10.0.0.81 | 4/6 路 | NPU 72%</div>
|
||
</div>
|
||
<div class="device-body">
|
||
<!-- LEFT: 4路视频预览 -->
|
||
<div class="preview-area">
|
||
<div class="preview-tile"><span class="label">大门</span></div>
|
||
<div class="preview-tile"><span class="label">仓库</span></div>
|
||
<div class="preview-tile"><span class="label">食堂</span></div>
|
||
<div class="preview-tile"><span class="label">安检门</span></div>
|
||
</div>
|
||
<!-- RIGHT: 功能 + 摄像头 -->
|
||
<div class="controls">
|
||
<div class="feature-group">
|
||
<div class="title">检测功能</div>
|
||
<label><input type="checkbox" checked> 人脸识别</label>
|
||
<label><input type="checkbox" checked> 劳保鞋检测</label>
|
||
<label><input type="checkbox"> 安全帽检测</label>
|
||
<label><input type="checkbox"> 抽烟检测</label>
|
||
<label><input type="checkbox"> 区域入侵</label>
|
||
</div>
|
||
<div class="cam-assign">
|
||
<div class="title">已分配摄像头(4路)</div>
|
||
<div>
|
||
<span class="cam-chip">大门 <span class="remove" onclick="this.parentElement.remove()">×</span></span>
|
||
<span class="cam-chip">仓库 <span class="remove" onclick="this.parentElement.remove()">×</span></span>
|
||
<span class="cam-chip">食堂 <span class="remove" onclick="this.parentElement.remove()">×</span></span>
|
||
<span class="cam-chip">安检门 <span class="remove" onclick="this.parentElement.remove()">×</span></span>
|
||
</div>
|
||
<div class="cam-add">+ 从未分配添加</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备行 2 - 正在下发中 -->
|
||
<div class="device-row" style="border-left:3px solid #ff9800">
|
||
<div class="device-header">
|
||
<div class="device-status busy"></div>
|
||
<div class="device-name">盒子B · 东区车间</div>
|
||
<div class="device-meta">10.0.0.82 | 2/4 路 | NPU 45%</div>
|
||
<span class="device-busy">⏳ 正在下发... 已 3s</span>
|
||
</div>
|
||
<div class="device-body">
|
||
<div class="preview-area">
|
||
<div class="preview-tile"><span class="label">北车间</span></div>
|
||
<div class="preview-tile"><span class="label">南车间</span></div>
|
||
<div class="preview-tile empty"></div>
|
||
<div class="preview-tile empty"></div>
|
||
</div>
|
||
<div class="controls" style="opacity:0.5;pointer-events:none">
|
||
<div class="feature-group">
|
||
<div class="title">检测功能</div>
|
||
<label><input type="checkbox" checked> 人脸识别</label>
|
||
<label><input type="checkbox"> 劳保鞋检测</label>
|
||
<label><input type="checkbox"> 区域入侵</label>
|
||
</div>
|
||
<div class="cam-assign">
|
||
<div class="title">已分配摄像头(2路)</div>
|
||
<div>
|
||
<span class="cam-chip">北车间 <span class="remove">×</span></span>
|
||
<span class="cam-chip">南车间 <span class="remove">×</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备行 3 - 未配置 -->
|
||
<div class="device-row" style="opacity:0.6">
|
||
<div class="device-header">
|
||
<div class="device-status online" style="background:#888"></div>
|
||
<div class="device-name">盒子C · 备用</div>
|
||
<div class="device-meta">10.0.0.83 | 0/8 路</div>
|
||
</div>
|
||
<div class="device-body">
|
||
<div class="preview-area" style="display:flex;align-items:center;justify-content:center;color:#444;font-size:12px">
|
||
空闲设备 · 暂无摄像头分配
|
||
</div>
|
||
<div class="controls">
|
||
<div class="feature-group">
|
||
<div class="title">检测功能</div>
|
||
<label><input type="checkbox"> 人脸识别</label>
|
||
<label><input type="checkbox"> 安全帽检测</label>
|
||
</div>
|
||
<div class="cam-assign">
|
||
<div class="title">已分配摄像头(0路)</div>
|
||
<div class="cam-add">+ 从未分配添加</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未分配池 -->
|
||
<div class="unassigned">
|
||
<div class="unassigned-title">📹 未分配摄像头(4路)</div>
|
||
<div class="unassigned-grid">
|
||
<div class="unassigned-card">
|
||
<div class="name">停车场入口</div>
|
||
<div class="meta">rtsp://10.0.0.100:554/...</div>
|
||
<div class="btn">+ 分配到设备</div>
|
||
</div>
|
||
<div class="unassigned-card">
|
||
<div class="name">走廊</div>
|
||
<div class="meta">rtsp://10.0.0.101:554/...</div>
|
||
<div class="btn">+ 分配到设备</div>
|
||
</div>
|
||
<div class="unassigned-card">
|
||
<div class="name">天台</div>
|
||
<div class="meta">rtsp://10.0.0.102:554/...</div>
|
||
<div class="btn">+ 分配到设备</div>
|
||
</div>
|
||
<div class="unassigned-card">
|
||
<div class="name">电梯厅</div>
|
||
<div class="meta">rtsp://10.0.0.103:554/...</div>
|
||
<div class="btn">+ 分配到设备</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存栏 -->
|
||
<div class="save-bar">
|
||
<div class="btn-primary">保存并下发</div>
|
||
<div class="btn-secondary">放弃更改</div>
|
||
<div class="save-status">
|
||
<span class="dot-ok"></span> 上次下发成功
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ====== 视频视图 ====== -->
|
||
<div id="panel-video" class="panel">
|
||
<div class="video-grid">
|
||
<div class="video-card"><div class="video-preview">📺 大门 · 实时画面</div><div class="video-info"><div class="name">大门</div><div class="device-tag">盒子A · 人脸识别</div><div class="assign">分配到 <select><option selected>盒子A</option><option>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card"><div class="video-preview">📺 仓库 · 实时画面</div><div class="video-info"><div class="name">仓库</div><div class="device-tag">盒子A · 人脸+鞋检测</div><div class="assign">分配到 <select><option selected>盒子A</option><option>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card"><div class="video-preview">📺 食堂 · 实时画面</div><div class="video-info"><div class="name">食堂</div><div class="device-tag">盒子A · 人脸识别</div><div class="assign">分配到 <select><option selected>盒子A</option><option>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card"><div class="video-preview">📺 安检门 · 实时画面</div><div class="video-info"><div class="name">安检门</div><div class="device-tag">盒子A · 劳保鞋检测</div><div class="assign">分配到 <select><option selected>盒子A</option><option>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card"><div class="video-preview">📺 北车间 · 实时画面</div><div class="video-info"><div class="name">北车间</div><div class="device-tag">盒子B · 人脸识别</div><div class="assign">分配到 <select><option>盒子A</option><option selected>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card"><div class="video-preview">📺 南车间 · 实时画面</div><div class="video-info"><div class="name">南车间</div><div class="device-tag">盒子B · 人脸识别</div><div class="assign">分配到 <select><option>盒子A</option><option selected>盒子B</option><option>盒子C</option></select></div></div></div>
|
||
<div class="video-card" style="border:1px dashed #444"><div class="video-preview" style="background:#111">⏹ 停车场 · 未分配</div><div class="video-info"><div class="name">停车场 <span class="badge unalloc">未分配</span></div><div class="assign">分配到 <select><option>盒子A</option><option>盒子B</option><option>盒子C</option><option selected>— 请选择 —</option></select></div></div></div>
|
||
<div class="video-card" style="border:1px dashed #444"><div class="video-preview" style="background:#111">⏹ 走廊 · 未分配</div><div class="video-info"><div class="name">走廊 <span class="badge unalloc">未分配</span></div><div class="assign">分配到 <select><option>盒子A</option><option>盒子B</option><option>盒子C</option><option selected>— 请选择 —</option></select></div></div></div>
|
||
</div>
|
||
<div class="save-bar">
|
||
<div class="btn-primary">保存分配</div>
|
||
<div class="save-status"><span class="dot-ok"></span> 已是最新状态</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function switchTab(name) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
document.getElementById('panel-' + name).classList.add('active');
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|