safesight-control/.brainstorm/console-mockup.html
2026-05-12 10:13:20 +08:00

268 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>