Просмотр исходного кода

新增化学品库存动态统计、化学品违规带离预警、应急疏散弹窗

stoney дней назад: 6
Родитель
Сommit
a57fdac121

BIN
assets/images/cb1b9a4e0fd85f6abda374443cde2b1c.jpg


+ 42 - 0
prompt/newPromptAddChem.md

@@ -0,0 +1,42 @@
+# 新增化学品管理模块数据统计及应急疏散引导
+
+重要约束说明:所有改动在原有的内容基础上改动,仅改动本prompt提到的新增的内容,未提及到的保持原内容,不做任何更改!
+
+---
+
+增量更新模块:
+
+**实验室实时风险预警**
+
+该模块记录在保留原有实验室风险预警的情况下新增化学品违规带离报警信息,每条数据包括实验室信息(实验室名称(房号)-二级单位)、实验人员未正常使用违规带离、预警时间(日期-时-分-秒),两类数据融合在一个list中展示。
+
+---
+
+新增模块:
+
+**化学品库存动态统计**:
+
+- 要求:在实验室信息页面右侧区域新增模块:化学品库存动态统计
+
+- 位置:右侧最下方,放在实验室设备分类及使用统计下方
+
+- 展示内容:
+  - 仅用一个饼图统计管控化学品与非管控化学品的库存量及占比,数据标注(分类名称+数量(L)+占比)直接显示在饼图扇区外侧引线处,不设Tab切换。
+  - 右侧统计区展示存量化学品总量(单位L)、管控类化学品总量及占比、非管控类化学品总量及占比、存量化学品总类目数,管控类使用红色系、非管控类使用青色系、总类目数使用金色系配色,各含SVG圆弧进度环。
+  - 所有化学品数量单位统一为L。
+
+**应急疏散弹窗**
+
+- 入口:在已有预案安全报警的的弹窗页面上新增一个按钮:应急疏散,点击进入应急疏散页面
+
+- 内容:
+
+  内容及排版如下:
+
+  ![cb1b9a4e0fd85f6abda374443cde2b1c](../assets/images/cb1b9a4e0fd85f6abda374443cde2b1c.jpg)
+
+​	需要在用户点击执行疏散按钮后根据图片简单标识疏散路线,用箭头标识。
+
+
+
+重要:未提及的其他模块不要改动,未提及的其他模块不要改动!

+ 545 - 15
prototype-ui/agentDemo_dashboard.html

@@ -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">&#x1F9EA;</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>