|
|
@@ -1130,6 +1130,189 @@
|
|
|
box-shadow: 0 0 8px rgba(72,215,255,0.4);
|
|
|
}
|
|
|
|
|
|
+ /* ======================== Chemical Inventory Stats ======================== */
|
|
|
+ .chem-panel-body {
|
|
|
+ flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
|
|
|
+ }
|
|
|
+ .chem-stats-grid {
|
|
|
+ width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 4px 0;
|
|
|
+ }
|
|
|
+ .chem-stat-item {
|
|
|
+ display: flex; align-items: center; gap: 10px; padding: 10px 12px;
|
|
|
+ background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
|
|
|
+ border-radius: 4px; transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .chem-stat-item:hover {
|
|
|
+ border-color: rgba(72,215,255,0.3); box-shadow: 0 0 10px rgba(72,215,255,0.08);
|
|
|
+ transform: translateY(-1px);
|
|
|
+ }
|
|
|
+ .chem-ring-wrap {
|
|
|
+ width: 48px; height: 48px; flex-shrink: 0; position: relative;
|
|
|
+ }
|
|
|
+ .chem-ring-wrap svg { width: 100%; height: 100%; }
|
|
|
+ .chem-ring-bg { fill: none; stroke: rgba(72,215,255,0.1); stroke-width: 3; }
|
|
|
+ .chem-ring-fg { fill: none; stroke-width: 3; stroke-linecap: round; transition: stroke-dashoffset 1s ease; }
|
|
|
+ .chem-ring-icon { font-size: 16px; text-anchor: middle; dominant-baseline: central; }
|
|
|
+ .chem-ring-pct { font-size: 10px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
|
|
|
+ .chem-stat-info { min-width: 0; flex: 1; }
|
|
|
+ .chem-stat-value { font-size: 18px; font-weight: 700; line-height: 1.2; }
|
|
|
+ .chem-stat-label { font-size: 11px; color: var(--text-sub); margin-top: 2px; white-space: nowrap; }
|
|
|
+ .chem-stat-pct { font-size: 11px; margin-top: 2px; }
|
|
|
+
|
|
|
+ /* Warning item chemical violation style */
|
|
|
+ .warning-item.chem-violation {
|
|
|
+ border-left-color: #a78bfa;
|
|
|
+ background: rgba(167,139,250,0.04);
|
|
|
+ }
|
|
|
+ .warning-item.chem-violation .w-sensor {
|
|
|
+ color: #a78bfa;
|
|
|
+ }
|
|
|
+ @keyframes chemViolationPulse {
|
|
|
+ 0%, 100% { border-left-color: rgba(167,139,250,0.6); }
|
|
|
+ 50% { border-left-color: #a78bfa; box-shadow: -2px 0 8px rgba(167,139,250,0.15); }
|
|
|
+ }
|
|
|
+ .warning-item.chem-violation {
|
|
|
+ animation: chemViolationPulse 3s ease-in-out infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ======================== Evacuation Modal ======================== */
|
|
|
+ .evac-overlay {
|
|
|
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
|
+ background: rgba(0,0,0,0.85); z-index: 10000;
|
|
|
+ display: none; align-items: center; justify-content: center;
|
|
|
+ }
|
|
|
+ .evac-overlay.show { display: flex; }
|
|
|
+ .evac-modal {
|
|
|
+ width: 1100px; height: 680px; border-radius: 8px; overflow: hidden;
|
|
|
+ border: 1px solid rgba(72,180,255,0.3);
|
|
|
+ background: linear-gradient(180deg, #0a1e3d, #061228);
|
|
|
+ box-shadow: 0 0 60px rgba(72,180,255,0.15);
|
|
|
+ display: flex; flex-direction: column;
|
|
|
+ }
|
|
|
+ .evac-header {
|
|
|
+ display: flex; align-items: center; justify-content: space-between;
|
|
|
+ padding: 14px 24px; border-bottom: 1px solid rgba(72,180,255,0.2);
|
|
|
+ }
|
|
|
+ .evac-header-left { display: flex; align-items: center; gap: 10px; }
|
|
|
+ .evac-header-left .evac-icon { font-size: 22px; }
|
|
|
+ .evac-header-left .evac-title { font-size: 20px; font-weight: 700; letter-spacing: 4px; color: #fff; }
|
|
|
+ .evac-close {
|
|
|
+ width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
|
|
|
+ background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15);
|
|
|
+ color: #fff; font-size: 18px; display: flex; align-items: center; justify-content: center;
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .evac-close:hover { background: rgba(255,77,79,0.3); border-color: var(--danger); }
|
|
|
+ .evac-body {
|
|
|
+ flex: 1; display: flex; min-height: 0; padding: 12px 16px; gap: 14px;
|
|
|
+ }
|
|
|
+ .evac-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
|
+ .evac-legend {
|
|
|
+ display: flex; align-items: center; gap: 20px; justify-content: flex-end;
|
|
|
+ font-size: 12px; color: var(--text-sub); margin-bottom: 6px;
|
|
|
+ }
|
|
|
+ .evac-legend-item { display: flex; align-items: center; gap: 6px; }
|
|
|
+ .evac-legend-line { width: 22px; height: 3px; border-radius: 2px; }
|
|
|
+ .evac-floorplan {
|
|
|
+ flex: 1; border: 1px solid rgba(72,180,255,0.15); border-radius: 6px;
|
|
|
+ position: relative; overflow: hidden; background: rgba(6,22,56,0.6);
|
|
|
+ }
|
|
|
+ .evac-floorplan svg { width: 100%; height: 100%; }
|
|
|
+ .evac-alert-card {
|
|
|
+ position: absolute; left: 14px; bottom: 14px;
|
|
|
+ background: rgba(20,5,5,0.92); border: 1px solid rgba(255,77,79,0.4);
|
|
|
+ border-radius: 6px; padding: 10px 14px; max-width: 240px; z-index: 2;
|
|
|
+ }
|
|
|
+ .evac-alert-card .eac-header {
|
|
|
+ display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; gap: 8px;
|
|
|
+ }
|
|
|
+ .evac-alert-card .eac-time { color: var(--text-sub); font-size: 11px; display: flex; align-items: center; gap: 4px; }
|
|
|
+ .evac-alert-card .eac-time .eac-icon { color: var(--danger); }
|
|
|
+ .evac-alert-card .eac-close-btn { cursor: pointer; color: var(--text-sub); font-size: 14px; background: none; border: none; }
|
|
|
+ .evac-alert-card .eac-badge {
|
|
|
+ display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 3px;
|
|
|
+ background: rgba(255,77,79,0.2); color: var(--danger); border: 1px solid rgba(255,77,79,0.3);
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+ .evac-alert-card .eac-desc { color: #fff; font-size: 12px; }
|
|
|
+ .evac-sidebar {
|
|
|
+ width: 190px; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .evac-sidebar-block {
|
|
|
+ background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
|
|
|
+ border-radius: 4px; padding: 10px; flex: 1; display: flex; flex-direction: column;
|
|
|
+ }
|
|
|
+ .evac-sidebar-title { font-size: 11px; color: var(--text-sub); margin-bottom: 4px; }
|
|
|
+ .evac-sidebar-label { font-size: 13px; color: #fff; font-weight: 600; margin-bottom: 6px; }
|
|
|
+ .evac-video-placeholder {
|
|
|
+ flex: 1; min-height: 60px; border-radius: 4px;
|
|
|
+ background: rgba(6,22,56,0.8); border: 1px solid rgba(72,180,255,0.1);
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 11px; color: var(--text-sub); opacity: 0.5;
|
|
|
+ }
|
|
|
+ .evac-footer {
|
|
|
+ border-top: 1px solid rgba(72,180,255,0.15); padding: 12px 20px;
|
|
|
+ display: flex; gap: 14px; align-items: flex-end;
|
|
|
+ }
|
|
|
+ .evac-metrics { display: flex; flex-direction: column; gap: 5px; min-width: 180px; }
|
|
|
+ .evac-metric-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
|
+ .evac-metric-label { color: var(--text-sub); }
|
|
|
+ .evac-metric-value { color: var(--danger); font-weight: 600; }
|
|
|
+ .evac-broadcast { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
|
|
+ .evac-broadcast-header { display: flex; align-items: center; justify-content: space-between; }
|
|
|
+ .evac-broadcast-title { font-size: 13px; color: #fff; display: flex; align-items: center; gap: 6px; }
|
|
|
+ .evac-broadcast-hint { font-size: 11px; color: var(--text-sub); }
|
|
|
+ .evac-speakers { display: flex; gap: 6px; }
|
|
|
+ .evac-speaker-btn {
|
|
|
+ flex: 1; padding: 7px 10px; border-radius: 4px; text-align: center;
|
|
|
+ font-size: 11px; cursor: pointer; transition: all 0.3s;
|
|
|
+ background: linear-gradient(135deg, rgba(72,215,255,0.15), rgba(58,123,255,0.1));
|
|
|
+ border: 1px solid rgba(72,215,255,0.3); color: var(--accent);
|
|
|
+ }
|
|
|
+ .evac-speaker-btn:hover { background: linear-gradient(135deg, rgba(72,215,255,0.25), rgba(58,123,255,0.2)); }
|
|
|
+ .evac-broadcast-input { display: flex; gap: 6px; }
|
|
|
+ .evac-broadcast-input input {
|
|
|
+ flex: 1; padding: 7px 12px; border-radius: 4px;
|
|
|
+ background: rgba(72,215,255,0.06); border: 1px solid rgba(72,180,255,0.2);
|
|
|
+ color: var(--text-main); font-size: 12px; outline: none;
|
|
|
+ }
|
|
|
+ .evac-broadcast-input input::placeholder { color: var(--text-sub); }
|
|
|
+ .evac-broadcast-input button {
|
|
|
+ padding: 7px 16px; border-radius: 4px; cursor: pointer;
|
|
|
+ background: rgba(72,215,255,0.1); border: 1px solid rgba(72,215,255,0.3);
|
|
|
+ color: var(--accent); font-size: 12px; transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .evac-broadcast-input button:hover { background: rgba(72,215,255,0.2); }
|
|
|
+ .evac-actions { display: flex; flex-direction: column; gap: 8px; min-width: 110px; }
|
|
|
+ .evac-btn-later {
|
|
|
+ padding: 10px 20px; border-radius: 4px; cursor: pointer;
|
|
|
+ background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.2);
|
|
|
+ color: var(--text-main); font-size: 13px; text-align: center; transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .evac-btn-later:hover { background: rgba(72,215,255,0.12); }
|
|
|
+ .evac-btn-exec {
|
|
|
+ padding: 10px 20px; border-radius: 4px; cursor: pointer;
|
|
|
+ background: linear-gradient(135deg, rgba(255,0,0,0.15), rgba(200,0,0,0.1));
|
|
|
+ border: 1px solid rgba(255,77,79,0.5); color: var(--danger);
|
|
|
+ font-size: 13px; font-weight: 600; text-align: center; transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .evac-btn-exec:hover { background: linear-gradient(135deg, rgba(255,0,0,0.25), rgba(200,0,0,0.2)); box-shadow: 0 0 16px rgba(255,77,79,0.3); }
|
|
|
+ .evac-route-arrow { opacity: 0; transition: opacity 0.5s; }
|
|
|
+ .evac-route-arrow.visible { animation: evacArrowFlow 1.5s ease-in-out infinite; }
|
|
|
+ @keyframes evacArrowFlow {
|
|
|
+ 0% { opacity: 0.4; }
|
|
|
+ 50% { opacity: 1; }
|
|
|
+ 100% { opacity: 0.4; }
|
|
|
+ }
|
|
|
+ .evac-room-group { cursor: pointer; }
|
|
|
+ .evac-room-group:hover rect { stroke: var(--accent); stroke-width: 2; fill: rgba(72,215,255,0.12); }
|
|
|
+ .evac-room-group.selected rect { stroke: var(--accent); stroke-width: 2; fill: rgba(72,215,255,0.15); }
|
|
|
+ .evac-room-group.alarm rect { stroke: rgba(255,77,79,0.7) !important; stroke-width: 2 !important; fill: rgba(255,77,79,0.1) !important; animation: evacRoomAlarm 1s ease-in-out infinite; }
|
|
|
+ @keyframes evacRoomAlarm {
|
|
|
+ 0%, 100% { stroke: rgba(255,77,79,0.5); fill: rgba(255,77,79,0.06); }
|
|
|
+ 50% { stroke: rgba(255,77,79,0.9); fill: rgba(255,77,79,0.15); }
|
|
|
+ }
|
|
|
+
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -1229,7 +1412,7 @@
|
|
|
<!-- RIGHT COLUMN -->
|
|
|
<div class="col-right">
|
|
|
<!-- 智能环境感知应用设备统计 -->
|
|
|
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
|
|
|
+ <div class="panel" style="flex:4;display:flex;flex-direction:column;">
|
|
|
<div class="corner-deco tl"></div><div class="corner-deco tr"></div>
|
|
|
<div class="corner-deco bl"></div><div class="corner-deco br"></div>
|
|
|
<div class="panel-title">智能环境感知应用设备统计</div>
|
|
|
@@ -1253,7 +1436,7 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 实验室设备分类及使用统计 -->
|
|
|
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
|
|
|
+ <div class="panel" style="flex:4;display:flex;flex-direction:column;">
|
|
|
<div class="corner-deco tl"></div><div class="corner-deco tr"></div>
|
|
|
<div class="corner-deco bl"></div><div class="corner-deco br"></div>
|
|
|
<div class="panel-title">实验室设备分类及使用统计</div>
|
|
|
@@ -1272,6 +1455,72 @@
|
|
|
<div class="equip-status-card"><div class="es-value" style="color:var(--danger)">38</div><div class="es-label">维修</div></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 化学品库存动态统计 -->
|
|
|
+ <div class="panel" style="flex:3;display:flex;flex-direction:column;">
|
|
|
+ <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
|
|
|
+ <div class="corner-deco bl"></div><div class="corner-deco br"></div>
|
|
|
+ <div class="panel-title">化学品库存动态统计</div>
|
|
|
+ <div class="chem-panel-body">
|
|
|
+ <div class="chem-stats-grid">
|
|
|
+ <div class="chem-stat-item">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
|
|
|
+ <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#48d7ff"
|
|
|
+ stroke-dasharray="113.1" stroke-dashoffset="0"/>
|
|
|
+ </g>
|
|
|
+ <text x="22" y="23" class="chem-ring-icon" fill="#48d7ff">🧪</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#48d7ff">2,450<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
|
|
|
+ <div class="chem-stat-label">存量化学品总量</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-item">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
|
|
|
+ <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#ff4d4f"
|
|
|
+ stroke-dasharray="113.1" stroke-dashoffset="81.4"/>
|
|
|
+ </g>
|
|
|
+ <text x="22" y="22" class="chem-ring-pct" fill="#ff4d4f">27.8%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#ff4d4f">680<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
|
|
|
+ <div class="chem-stat-label">管控类化学品</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-item">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
|
|
|
+ <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#36d399"
|
|
|
+ stroke-dasharray="113.1" stroke-dashoffset="31.4"/>
|
|
|
+ </g>
|
|
|
+ <text x="22" y="22" class="chem-ring-pct" fill="#36d399">72.2%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#36d399">1,770<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
|
|
|
+ <div class="chem-stat-label">非管控类化学品</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-item">
|
|
|
+ <div class="chem-stat-info" style="text-align:center;flex:1;">
|
|
|
+ <div class="chem-stat-value" style="color:#ffb020;font-size:22px;">156</div>
|
|
|
+ <div class="chem-stat-label">存量化学品总类目</div>
|
|
|
+ <div class="chem-stat-pct" style="color:var(--text-sub)">管控68类 / 非管控88类</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -1334,6 +1583,149 @@
|
|
|
</div>
|
|
|
<div class="alert-footer">
|
|
|
<button class="alert-close-btn" onclick="closeAlert()">确 认</button>
|
|
|
+ <button class="alert-close-btn" style="background:linear-gradient(135deg,#1a3a6a,#0d2555);border:1px solid rgba(72,180,255,0.5);margin-left:12px;" onclick="closeAlert();openEvacuation()">应急疏散</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- ===================== EVACUATION MODAL ===================== -->
|
|
|
+<div class="evac-overlay" id="evacOverlay">
|
|
|
+ <div class="evac-modal">
|
|
|
+ <div class="evac-header">
|
|
|
+ <div class="evac-header-left">
|
|
|
+ <span class="evac-icon">📋</span>
|
|
|
+ <span class="evac-title">应急疏散</span>
|
|
|
+ </div>
|
|
|
+ <div class="evac-close" onclick="closeEvacuation()">✕</div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-body">
|
|
|
+ <div class="evac-main">
|
|
|
+ <div class="evac-legend">
|
|
|
+ <div class="evac-legend-item">
|
|
|
+ <span class="evac-legend-line" style="background:linear-gradient(90deg,#3a7bff,#48d7ff);"></span>
|
|
|
+ <span>应急疏散路线图</span>
|
|
|
+ </div>
|
|
|
+ <div class="evac-legend-item">
|
|
|
+ <span>房间号→实验室名称</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-floorplan">
|
|
|
+ <svg viewBox="0 0 680 360" id="evacFloorSvg">
|
|
|
+ <!-- Top row rooms -->
|
|
|
+ <g class="evac-room-group" data-room="A101" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="40" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="105" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A101</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A102" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="180" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="245" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A102</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A103" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="320" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="385" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A103</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A104" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="460" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="525" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A104</text>
|
|
|
+ </g>
|
|
|
+ <!-- Corridor -->
|
|
|
+ <text x="340" y="155" text-anchor="middle" fill="rgba(126,172,200,0.5)" font-size="14" letter-spacing="8">走 道</text>
|
|
|
+ <circle cx="300" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
|
|
|
+ <circle cx="380" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
|
|
|
+ <!-- Right exit -->
|
|
|
+ <rect x="630" y="45" width="30" height="80" rx="2" fill="none" stroke="rgba(255,77,79,0.4)" stroke-width="1" stroke-dasharray="4,2"/>
|
|
|
+ <text x="645" y="80" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
|
|
|
+ <text x="645" y="95" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
|
|
|
+ <!-- Left exit -->
|
|
|
+ <rect x="2" y="220" width="30" height="80" rx="2" fill="none" stroke="rgba(255,77,79,0.4)" stroke-width="1" stroke-dasharray="4,2"/>
|
|
|
+ <text x="17" y="255" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
|
|
|
+ <text x="17" y="270" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
|
|
|
+ <!-- Bottom row rooms -->
|
|
|
+ <g class="evac-room-group" data-room="A105" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="40" y="200" width="110" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="95" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A105</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A106" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="160" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="210" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A106</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A107" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="270" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="320" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A107</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A108" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="380" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="430" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A108</text>
|
|
|
+ </g>
|
|
|
+ <g class="evac-room-group" data-room="A109" onclick="selectEvacRoom(this)">
|
|
|
+ <rect x="490" y="200" width="130" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
|
|
|
+ <text x="555" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A109</text>
|
|
|
+ </g>
|
|
|
+ <!-- Evacuation route (drawn dynamically) -->
|
|
|
+ <g id="evacRoutes" class="evac-route-arrow"></g>
|
|
|
+ <!-- SVG arrow marker -->
|
|
|
+ <defs>
|
|
|
+ <marker id="evacArrowHead" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
|
|
|
+ <polygon points="0,0 10,4 0,8" fill="#3a7bff"/>
|
|
|
+ </marker>
|
|
|
+ </defs>
|
|
|
+ </svg>
|
|
|
+ <!-- Alert info card overlay -->
|
|
|
+ <div class="evac-alert-card" id="evacAlertCard">
|
|
|
+ <div class="eac-header">
|
|
|
+ <div class="eac-time"><span class="eac-icon">⚠</span> <span id="evacAlertTime">2026-03-24 14:32:18</span></div>
|
|
|
+ <button class="eac-close-btn" onclick="document.getElementById('evacAlertCard').style.display='none'">✕</button>
|
|
|
+ </div>
|
|
|
+ <div class="eac-badge">触发风险</div>
|
|
|
+ <div class="eac-desc">发生风险:TVOC浓度超标</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-sidebar">
|
|
|
+ <div class="evac-sidebar-block">
|
|
|
+ <div class="evac-sidebar-title">实时视频监控</div>
|
|
|
+ <div class="evac-sidebar-label" id="evacSidebarLabel1">楼道 1 层</div>
|
|
|
+ <div class="evac-video-placeholder" id="evacSidebarVideo1">📹 实时视频监控</div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-sidebar-block">
|
|
|
+ <div class="evac-sidebar-title">实时视频监控</div>
|
|
|
+ <div class="evac-sidebar-label" id="evacSidebarLabel2">楼道 2 层</div>
|
|
|
+ <div class="evac-video-placeholder" id="evacSidebarVideo2">📹 实时视频监控</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-footer">
|
|
|
+ <div class="evac-metrics">
|
|
|
+ <div class="evac-metric-row">
|
|
|
+ <span class="evac-metric-label">告警指标</span>
|
|
|
+ <span class="evac-metric-value">TVOC 浓度超标</span>
|
|
|
+ <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
|
|
|
+ </div>
|
|
|
+ <div class="evac-metric-row">
|
|
|
+ <span class="evac-metric-label">当前值 / 安全阈值</span>
|
|
|
+ <span class="evac-metric-value">TVOC 浓度超标</span>
|
|
|
+ <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-broadcast">
|
|
|
+ <div class="evac-broadcast-header">
|
|
|
+ <div class="evac-broadcast-title">🔊 语音广播</div>
|
|
|
+ <div class="evac-broadcast-hint">选择播放设备</div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-speakers">
|
|
|
+ <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
|
|
|
+ <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
|
|
|
+ <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-broadcast-input">
|
|
|
+ <input type="text" placeholder="请输入喊话内容" />
|
|
|
+ <button>发送</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="evac-actions">
|
|
|
+ <div class="evac-btn-later" onclick="closeEvacuation()">稍后处理</div>
|
|
|
+ <div class="evac-btn-exec" onclick="executeEvacuation()">执行疏散</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1453,25 +1845,39 @@ buildSensorList();
|
|
|
function buildWarningList() {
|
|
|
const el = document.getElementById('warningList');
|
|
|
const warnings = [
|
|
|
- { lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-04 14:32:18' },
|
|
|
- { lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-04 14:28:05' },
|
|
|
- { lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42' },
|
|
|
- { lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10' },
|
|
|
- { lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-04 12:45:33' },
|
|
|
- { lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-04 11:18:27' },
|
|
|
- { lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-04 10:42:51' },
|
|
|
- { lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-04 09:36:14' },
|
|
|
+ { type: 'sensor', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-24 14:32:18' },
|
|
|
+ { type: 'chem', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', person: '王伟', time: '2026-03-24 14:28:45' },
|
|
|
+ { type: 'sensor', lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-24 14:28:05' },
|
|
|
+ { type: 'chem', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', person: '李明', time: '2026-03-24 14:15:33' },
|
|
|
+ { type: 'sensor', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-24 13:55:42' },
|
|
|
+ { type: 'sensor', lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-24 13:20:10' },
|
|
|
+ { type: 'chem', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', person: '赵磊', time: '2026-03-24 13:10:22' },
|
|
|
+ { type: 'sensor', lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-24 12:45:33' },
|
|
|
+ { type: 'chem', lab: '材料力学实验室B203(B203)', unit: '安全技术研究所', person: '陈静', time: '2026-03-24 12:30:18' },
|
|
|
+ { type: 'sensor', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-24 11:18:27' },
|
|
|
+ { type: 'sensor', lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-24 10:42:51' },
|
|
|
+ { type: 'chem', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', person: '张强', time: '2026-03-24 10:15:07' },
|
|
|
+ { type: 'sensor', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-24 09:36:14' },
|
|
|
];
|
|
|
const all = [...warnings, ...warnings]; // duplicate for seamless scroll
|
|
|
let html = '';
|
|
|
all.forEach(w => {
|
|
|
- html += `<div class="warning-item">
|
|
|
- <div class="w-lab">${w.lab} - ${w.unit}</div>
|
|
|
- <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
|
|
|
- <div class="w-time">${w.time}</div>
|
|
|
- </div>`;
|
|
|
+ if (w.type === 'chem') {
|
|
|
+ html += `<div class="warning-item chem-violation">
|
|
|
+ <div class="w-lab">${w.lab} - ${w.unit}</div>
|
|
|
+ <div class="w-sensor">化学品违规带离: ${w.person} 未正常使用违规带离</div>
|
|
|
+ <div class="w-time">${w.time}</div>
|
|
|
+ </div>`;
|
|
|
+ } else {
|
|
|
+ html += `<div class="warning-item">
|
|
|
+ <div class="w-lab">${w.lab} - ${w.unit}</div>
|
|
|
+ <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
|
|
|
+ <div class="w-time">${w.time}</div>
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
});
|
|
|
el.innerHTML = html;
|
|
|
+ document.getElementById('warningCount').textContent = warnings.length;
|
|
|
}
|
|
|
buildWarningList();
|
|
|
|
|
|
@@ -1754,6 +2160,130 @@ setTimeout(initLabCharts, 200);
|
|
|
window.addEventListener('resize', () => {
|
|
|
[chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie].forEach(c => c && c.resize());
|
|
|
});
|
|
|
+
|
|
|
+// ===================== Evacuation Modal =====================
|
|
|
+// Room name mapping
|
|
|
+const evacRoomNames = {
|
|
|
+ A101: '化学实验室A101', A102: '材料分析实验室A102',
|
|
|
+ A103: '有机化学实验室A103', A104: '气相色谱实验室A104',
|
|
|
+ A105: '粉尘检测实验室A105', A106: '光谱分析实验室A106',
|
|
|
+ A107: '质谱分析实验室A107', A108: '样品前处理室A108',
|
|
|
+ A109: '大型仪器室A109'
|
|
|
+};
|
|
|
+
|
|
|
+// Room center coordinates for route drawing
|
|
|
+const evacRoomPos = {
|
|
|
+ // Top row: room center X, corridor Y entry point
|
|
|
+ A101: { cx: 105, cy: 70, row: 'top', exitX: 105, corridorY: 130 },
|
|
|
+ A102: { cx: 245, cy: 70, row: 'top', exitX: 245, corridorY: 130 },
|
|
|
+ A103: { cx: 385, cy: 70, row: 'top', exitX: 385, corridorY: 130 },
|
|
|
+ A104: { cx: 525, cy: 70, row: 'top', exitX: 525, corridorY: 130 },
|
|
|
+ // Bottom row: room center X, corridor Y entry point
|
|
|
+ A105: { cx: 95, cy: 255, row: 'bottom', exitX: 95, corridorY: 185 },
|
|
|
+ A106: { cx: 210, cy: 255, row: 'bottom', exitX: 210, corridorY: 185 },
|
|
|
+ A107: { cx: 320, cy: 255, row: 'bottom', exitX: 320, corridorY: 185 },
|
|
|
+ A108: { cx: 430, cy: 255, row: 'bottom', exitX: 430, corridorY: 185 },
|
|
|
+ A109: { cx: 555, cy: 255, row: 'bottom', exitX: 555, corridorY: 185 },
|
|
|
+};
|
|
|
+
|
|
|
+// Current alarm room (default from the alert)
|
|
|
+let evacAlarmRoom = 'A105';
|
|
|
+
|
|
|
+function openEvacuation() {
|
|
|
+ document.getElementById('evacOverlay').classList.add('show');
|
|
|
+ // Reset state
|
|
|
+ document.getElementById('evacRoutes').innerHTML = '';
|
|
|
+ document.getElementById('evacRoutes').classList.remove('visible');
|
|
|
+ document.getElementById('evacAlertCard').style.display = 'block';
|
|
|
+ // Clear room selections & alarm highlights
|
|
|
+ document.querySelectorAll('.evac-room-group').forEach(g => {
|
|
|
+ g.classList.remove('selected', 'alarm');
|
|
|
+ });
|
|
|
+ // Reset sidebar
|
|
|
+ document.getElementById('evacSidebarLabel1').textContent = '楼道 1 层';
|
|
|
+ document.getElementById('evacSidebarVideo1').textContent = '📹 实时视频监控';
|
|
|
+ document.getElementById('evacSidebarLabel2').textContent = '楼道 2 层';
|
|
|
+ document.getElementById('evacSidebarVideo2').textContent = '📹 实时视频监控';
|
|
|
+ // Update time
|
|
|
+ const now = new Date();
|
|
|
+ const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
|
|
|
+ document.getElementById('evacAlertTime').textContent = ts;
|
|
|
+}
|
|
|
+
|
|
|
+function closeEvacuation() {
|
|
|
+ document.getElementById('evacOverlay').classList.remove('show');
|
|
|
+}
|
|
|
+
|
|
|
+function selectEvacRoom(el) {
|
|
|
+ // Clear previous selection (keep alarm class)
|
|
|
+ document.querySelectorAll('.evac-room-group.selected').forEach(g => g.classList.remove('selected'));
|
|
|
+ el.classList.add('selected');
|
|
|
+ const room = el.getAttribute('data-room');
|
|
|
+ const roomName = evacRoomNames[room] || room;
|
|
|
+ // Update sidebar: top block shows selected room camera, bottom stays corridor
|
|
|
+ document.getElementById('evacSidebarLabel1').textContent = roomName;
|
|
|
+ document.getElementById('evacSidebarVideo1').textContent = '📹 ' + room + ' 实时监控画面';
|
|
|
+ document.getElementById('evacSidebarLabel2').textContent = '楼道走廊';
|
|
|
+ document.getElementById('evacSidebarVideo2').textContent = '📹 走廊实时监控画面';
|
|
|
+}
|
|
|
+
|
|
|
+function executeEvacuation() {
|
|
|
+ const routesG = document.getElementById('evacRoutes');
|
|
|
+ routesG.innerHTML = ''; // Clear previous
|
|
|
+
|
|
|
+ // Mark alarm room
|
|
|
+ document.querySelectorAll('.evac-room-group').forEach(g => g.classList.remove('alarm'));
|
|
|
+ const alarmEl = document.querySelector(`.evac-room-group[data-room="${evacAlarmRoom}"]`);
|
|
|
+ if (alarmEl) alarmEl.classList.add('alarm');
|
|
|
+
|
|
|
+ const pos = evacRoomPos[evacAlarmRoom];
|
|
|
+ if (!pos) return;
|
|
|
+
|
|
|
+ // Build polyline path: room -> corridor -> along corridor -> exit
|
|
|
+ let pathPoints = [];
|
|
|
+
|
|
|
+ if (pos.row === 'top') {
|
|
|
+ // Top row: go down to corridor, then right to exit
|
|
|
+ pathPoints = [
|
|
|
+ [pos.exitX, pos.cy + 40], // bottom edge of room
|
|
|
+ [pos.exitX, pos.corridorY], // enter corridor
|
|
|
+ [620, pos.corridorY], // along corridor to right
|
|
|
+ [620, 85], // up to exit
|
|
|
+ [640, 85], // into exit
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ // Bottom row: go up to corridor, then left to exit
|
|
|
+ pathPoints = [
|
|
|
+ [pos.exitX, pos.cy - 55], // top edge of room
|
|
|
+ [pos.exitX, pos.corridorY], // enter corridor
|
|
|
+ [35, pos.corridorY], // along corridor to left
|
|
|
+ [35, 260], // down to exit
|
|
|
+ [17, 260], // into exit
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Draw animated polyline
|
|
|
+ const pointsStr = pathPoints.map(p => p.join(',')).join(' ');
|
|
|
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
|
+ polyline.setAttribute('points', pointsStr);
|
|
|
+ polyline.setAttribute('fill', 'none');
|
|
|
+ polyline.setAttribute('stroke', '#3a7bff');
|
|
|
+ polyline.setAttribute('stroke-width', '3');
|
|
|
+ polyline.setAttribute('stroke-dasharray', '8,4');
|
|
|
+ polyline.setAttribute('marker-end', 'url(#evacArrowHead)');
|
|
|
+ routesG.appendChild(polyline);
|
|
|
+
|
|
|
+ // Add glow effect polyline
|
|
|
+ const glowLine = polyline.cloneNode();
|
|
|
+ glowLine.setAttribute('stroke', 'rgba(58,123,255,0.3)');
|
|
|
+ glowLine.setAttribute('stroke-width', '8');
|
|
|
+ glowLine.removeAttribute('stroke-dasharray');
|
|
|
+ glowLine.removeAttribute('marker-end');
|
|
|
+ routesG.insertBefore(glowLine, polyline);
|
|
|
+
|
|
|
+ // Show with animation
|
|
|
+ routesG.classList.add('visible');
|
|
|
+}
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|