Przeglądaj źródła

根据客户增加化学品库存统计模块、化学品违规带离预警、应急疏散等相关需求调整设计

anne 6 dni temu
rodzic
commit
aaf20ef52d

+ 8 - 1
README.md

@@ -12,7 +12,14 @@
 
 ## 1 文件目录说明
 
-所有操作请务必在prototype分支执行,不要影响其他dev或release分支
+| chinaSafety_9600    |
+|-------|-------|-------|
+| img    | design-preview-half.png | #  UI高清图 |
+| prompt | 超大屏prompt.md | #  提示词 |
+| prototype-ui | index-v2.html | #  高保真UI |
+| README.md |
+
+# 所有操作请务必在prototype分支执行,不要影响其他dev或release分支
 
 ![tree](image.png)
 

+ 22 - 8
prompt/超大屏prompt.md

@@ -55,10 +55,6 @@ CONTENT:
 数字翻牌器形式展示今日总进入人数及当前正在实验人数(两个数字翻牌器在一行),并在下方以折线图表形式统计与展示当天0-24点9个分隔时段的实验室进入人数及当前实验人数走势。
 
 **2区(区域占比1.5)**
-- 智能环境感知应用设备统计
-顶部统计显示布设的智能物联应用设备图标及在线、离线数量。
-下部左侧显示速度仪表盘样式的使用图表,展示设备在线率百分比。
-下部右侧以3+2布局(上3下2,居中对齐)展示电子信息铭牌、化学品智能终端、传感器、智慧摄像头、其他智能设备总数。
 
 - 实验室设备分类及使用统计
 分为上、下两部分区域布局按4:6展示:
@@ -70,6 +66,16 @@ CONTENT:
 使用状态统计:使用数+空闲数=正常状态数
 设备使用率(当月)=使用数➗(使用数+空闲数)
 
+- 化学品库存动态统计
+仅用一个饼图统计管控化学品与非管控化学品的库存量及占比,数据标注(分类名称+数量(L)+占比)直接显示在饼图扇区外侧引线处,不设Tab切换。
+右侧统计区展示存量化学品总量(单位L)、管控类化学品总量及占比、非管控类化学品总量及占比、存量化学品总类目数,管控类使用红色系、非管控类使用青色系、总类目数使用金色系配色,各含SVG圆弧进度环。
+所有化学品数量单位统一为L。
+
+- 智能环境感知应用设备统计
+顶部统计显示布设的智能物联应用设备图标及在线、离线数量。
+下部左侧显示速度仪表盘样式的使用图表,展示设备在线率百分比。
+下部右侧以3+2布局(上3下2,居中对齐)展示电子信息铭牌、化学品智能终端、传感器、智慧摄像头、其他智能设备总数。
+
 **3区(区域占比4.5)**
 3区固定显示实时监控视图,不再切换,分为左、右两个内容区:
 - 实时监控
@@ -90,16 +96,24 @@ CONTENT:
 每个实验室条目包括:实验室名称(房号)-二级单位、传感器图标、实时状态数值(温度、湿度、TVOC、CO₂、O₂等),发生异常时高亮红色渐变效果显示报警图标及数值。
 
 - 实验室实时风险预警(占3)
-统计展示本月预警响应总数(琥珀色高亮),并实时滚动轮播实验室发生异常预警通知,最新通知在最上面。
-每条数据包括实验室信息(实验室名称(房号)-二级单位)、异常传感器及对应值、预警时间(日期-时-分-秒)。
-预警区域字体颜色使用琥珀/橙色调,不使用红色字体。
+滚动轮播本月实验室发生异常预警通知,包括应急风险预警(🚨 应急标识)、危化品违规带离(☣️ 危化品标识)两种类型,预警区域字体颜色使用琥珀/橙色调高亮,最新通知在最上面。
+面板标题栏右侧显示两类预警的累计计数器(应急次数、违规带离次数),以大字号数值突出展示。
+  - 应急风险预警通知:每条数据包括实验室信息(实验室名称(房号)-二级单位)、异常传感器及对应值、预警时间(日期-时-分-秒)。
+  - 危化品违规带离预警:每条数据包括实验室信息(实验室名称(房号)-二级单位)、实验人员未正常使用违规带离、预警时间(日期-时-分-秒)。
+
+不使用红色字体。
 
 # 通用功能
 - 整体分辨率:设计稿要求9600×2800px,按固定像素显示,不使用transform:scale缩放。
 - 自动全屏:初次进入页面时默认全屏显示。
 - 整体框架、导航栏及每个模块等所有边框增强设计感,需要有动态的路径流动效果(border-beam动画)。
 - 四角装饰线:每个模块四角配科幻装饰线框。
-- 全屏预警弹窗:当传感器监测数据超出阈值,需全屏弹窗红色预警,左侧显示该实验室监控画面(900×700,Canvas模拟),右侧显示异常信息详情,参考科幻电影中系统报警样式。
+- 全屏预警弹窗:当传感器监测数据超出阈值,需全屏弹窗红色预警,左侧显示该实验室监控画面(900×700,Canvas模拟),右侧显示异常信息详情,参考科幻电影中系统报警样式。弹窗底部除"稍后处理"和"确认处理"外,新增琥珀色"🚪 应急疏散"按钮。
+- 应急疏散弹窗:点击"应急疏散"按钮后,弹出独立全屏弹窗(z-index更高),包含:
+  - 标题栏"应急疏散" + 关闭按钮
+  - 左侧SVG平面图(深蓝科幻风):显示楼层房间布局(A101–A109),告警房间红色边框高亮,疏散路线关键房间蓝色边框,走道区域含集合点圆形标记,蓝色虚线箭头指向两侧紧急出口,右上方展示"应急疏散路线图"图例
+  - 右侧纵向排列3路实时视频监控占位区,含"楼道 2层"楼层分区标签
+  - 底部:左侧告警指标明细(指标名称+当前值/阈值)、中部语音广播区(播放设备选择按钮+喊话内容输入框+发送)、右侧"稍后处理"与"执行疏散"操作按钮
 
 # 设计规范
 - 设计分辨率:9600×2800px(UI元素尺寸均按此分辨率设计,约为1920×1080的2.5倍)

+ 460 - 20
prototype-ui/index-v2.html

@@ -258,6 +258,54 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
 .warn-detail { font-size:25px; color:var(--text-dim); display:flex; align-items:center; gap:10px; }
 .warn-metric-val { color:#fb923c; font-weight:600; }
 
+/* ========== CHEMICAL INVENTORY TAGS ========== */
+.chem-tag-row { display:flex; gap:12px; margin-bottom:12px; }
+.chem-tag {
+  flex:1; padding:14px 18px; border-radius:8px; text-align:center; cursor:pointer;
+  font-size:26px; font-weight:600; letter-spacing:2px; border:1px solid rgba(30,144,255,0.3);
+  background:var(--bg-card); color:var(--text-dim); transition:all 0.25s;
+}
+.chem-tag.active {
+  border-color:var(--cyan); color:var(--cyan);
+  background:rgba(0,216,255,0.1); text-shadow:0 0 10px rgba(0,216,255,0.5);
+  box-shadow:0 0 12px rgba(0,216,255,0.15);
+}
+.chem-tag:hover:not(.active) { border-color:rgba(30,144,255,0.6); color:var(--text); }
+
+/* chemical inventory summary cards */
+.chem-stat-card {
+  border-radius:10px; padding:16px 20px;
+  background:rgba(4,14,38,0.7); border:1px solid rgba(30,144,255,0.15);
+  transition:border-color 0.25s;
+}
+.chem-stat-card:hover { border-color:rgba(30,144,255,0.4); }
+.chem-stat-card.total { border-color:rgba(30,144,255,0.3); background:rgba(30,144,255,0.06); text-align:center; }
+.chem-stat-card.ctrl  { border-color:rgba(239,68,68,0.25); background:rgba(239,68,68,0.05); }
+.chem-stat-card.free  { border-color:rgba(0,216,255,0.2);  background:rgba(0,216,255,0.04); }
+.chem-stat-card.cat   { border-color:rgba(255,215,64,0.22); background:rgba(255,215,64,0.04); }
+.csc-label { font-size:24px; color:var(--text-dim); letter-spacing:2px; margin-bottom:4px; }
+.csc-value { font-size:52px; font-weight:700; color:#ef4444; line-height:1.1; }
+.csc-value.total { color:var(--white); }
+.chem-stat-card.total .csc-value { color:var(--white); font-size:60px; }
+.csc-unit { font-size:24px; font-weight:400; color:var(--text-dim); margin-left:6px; }
+.csc-sub { font-size:22px; color:rgba(239,68,68,0.55); margin-top:8px; }
+
+/* warn-item type: emergency vs chemical */
+.warn-item.type-emergency {
+  background:rgba(245,158,11,0.06); border:1px solid rgba(245,158,11,0.25);
+}
+.warn-item.type-chemical {
+  background:rgba(0,216,255,0.04); border:1px solid rgba(0,216,255,0.18);
+}
+.warn-type-badge {
+  display:inline-flex; align-items:center; gap:5px;
+  padding:4px 12px; border-radius:6px; font-size:22px; font-weight:600; margin-right:10px;
+}
+.warn-type-badge.emergency { background:rgba(245,158,11,0.15); color:#fbbf24; }
+.warn-type-badge.chemical  { background:rgba(0,216,255,0.12);  color:var(--cyan); }
+.warn-lab-info { font-size:27px; font-weight:600; color:#fcd34d; flex:1; min-width:0; }
+.warn-lab-info.chemical { color:#67e8f9; }
+
 /* ========== DEVICE STATS ========== */
 .device-online-row { display:flex; gap:18px; margin-bottom:18px; }
 .device-stat-chip {
@@ -478,6 +526,88 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
 .btn-alert-confirm:hover { background:rgba(239,68,68,0.3); }
 .btn-alert-ignore { padding:22px 70px; border-radius:10px; cursor:pointer; border:1px solid var(--border); background:transparent; color:var(--text-dim); font-size:32px; transition:all 0.2s; }
 
+/* ========== EVACUATION MODAL ========== */
+#evac-modal {
+  position:fixed; inset:0; z-index:10000;
+  display:none; align-items:center; justify-content:center;
+  background:rgba(0,2,18,0.94); backdrop-filter:blur(8px);
+}
+#evac-modal.show { display:flex; animation:alertFadeIn 0.4s ease; }
+.evac-inner {
+  position:relative; width:2820px; border-radius:20px; overflow:hidden;
+  background:linear-gradient(135deg,rgba(2,8,28,0.99),rgba(1,5,20,0.99));
+  border:2px solid rgba(30,144,255,0.45);
+  box-shadow:0 0 200px rgba(30,144,255,0.22), inset 0 0 120px rgba(0,50,140,0.05);
+  display:flex; flex-direction:column; max-height:96vh;
+}
+.evac-hdr {
+  padding:36px 55px; display:flex; align-items:center; gap:28px; flex-shrink:0;
+  border-bottom:1px solid rgba(30,144,255,0.2);
+  background:linear-gradient(90deg,rgba(0,55,160,0.12),transparent);
+}
+.evac-hdr-icon { font-size:68px; }
+.evac-hdr-title { font-size:50px; font-weight:700; color:var(--white); letter-spacing:6px; }
+.evac-hdr-close {
+  margin-left:auto; width:68px; height:68px; border-radius:10px;
+  border:1px solid rgba(30,144,255,0.35); background:rgba(30,144,255,0.08);
+  color:var(--cyan); font-size:34px; cursor:pointer;
+  display:flex; align-items:center; justify-content:center; transition:background 0.2s;
+}
+.evac-hdr-close:hover { background:rgba(30,144,255,0.22); }
+.evac-body { display:flex; flex:1; min-height:0; overflow:hidden; }
+.evac-map-col { flex:1; min-width:0; padding:26px 28px; display:flex; flex-direction:column; gap:14px; }
+.evac-legend { display:flex; align-items:center; gap:12px; justify-content:flex-end; }
+.evac-legend-rect { width:36px; height:14px; background:#1e90ff; border-radius:3px; }
+.evac-legend-text { font-size:26px; color:var(--text-dim); }
+.evac-map-wrap {
+  flex:1; min-height:0; position:relative;
+  border-radius:10px; background:rgba(1,8,28,0.65);
+  border:1px solid rgba(30,144,255,0.15);
+}
+.evac-map-wrap svg { width:100%; height:100%; display:block; }
+.evac-popup {
+  position:absolute; left:3%; top:53%;
+  width:390px; border-radius:10px; padding:18px 24px 20px;
+  background:rgba(45,4,6,0.97); border:2px solid rgba(239,68,68,0.6);
+  box-shadow:0 0 50px rgba(239,68,68,0.28); z-index:5; pointer-events:none;
+}
+.evac-popup-hdr { display:flex; align-items:center; gap:14px; margin-bottom:12px; }
+.evac-popup-alarm { font-size:30px; }
+.evac-popup-time { font-size:22px; color:rgba(255,255,255,0.65); flex:1; }
+.evac-popup-x { color:rgba(255,255,255,0.45); font-size:22px; }
+.evac-popup-badge { background:#ef4444; color:#fff; font-size:20px; font-weight:700; border-radius:5px; padding:4px 14px; display:inline-block; margin-bottom:12px; letter-spacing:1px; }
+.evac-popup-msg { font-size:28px; font-weight:600; color:#fff; letter-spacing:1px; }
+.evac-right-col { flex:0 0 540px; border-left:1px solid rgba(30,144,255,0.15); display:flex; flex-direction:column; }
+.evac-right-hdr { padding:22px 28px; font-size:26px; color:var(--text-dim); letter-spacing:3px; border-bottom:1px solid rgba(30,144,255,0.12); background:rgba(30,144,255,0.04); flex-shrink:0; }
+.evac-cam-slot { flex:1; border-bottom:1px solid rgba(30,144,255,0.1); display:flex; align-items:center; justify-content:center; background:rgba(12,15,26,0.6); color:rgba(255,255,255,0.2); font-size:26px; letter-spacing:3px; min-height:180px; }
+.evac-floor-lbl { padding:14px 28px; font-size:24px; color:var(--text-dim); letter-spacing:3px; background:rgba(30,144,255,0.06); border-top:1px solid rgba(30,144,255,0.12); border-bottom:1px solid rgba(30,144,255,0.12); flex-shrink:0; }
+.evac-footer { padding:26px 50px; display:flex; gap:28px; align-items:stretch; border-top:1px solid rgba(30,144,255,0.15); flex-shrink:0; }
+.evac-metrics { flex:0 0 490px; display:flex; flex-direction:column; gap:12px; justify-content:center; }
+.evac-metric-row { background:rgba(239,68,68,0.06); border:1px solid rgba(239,68,68,0.2); border-radius:8px; padding:14px 20px; display:flex; justify-content:space-between; align-items:center; }
+.evac-metric-col .evac-ml { font-size:22px; color:var(--text-dim); margin-bottom:4px; }
+.evac-metric-col .evac-mv { font-size:28px; font-weight:600; color:#ef4444; }
+.evac-metric-rt { font-size:26px; color:rgba(255,255,255,0.75); }
+.evac-broadcast { flex:1; border:1px solid rgba(30,144,255,0.22); border-radius:12px; padding:18px 24px; display:flex; flex-direction:column; gap:14px; background:rgba(30,144,255,0.04); }
+.evac-bc-hdr { display:flex; align-items:center; gap:14px; }
+.evac-bc-icon { font-size:28px; }
+.evac-bc-title { font-size:30px; font-weight:600; color:var(--white); flex:1; }
+.evac-bc-device { font-size:24px; color:var(--text-dim); }
+.evac-speaker-row { display:flex; gap:14px; }
+.evac-speaker-btn { flex:1; padding:14px 8px; border-radius:8px; font-size:23px; border:1px solid rgba(30,144,255,0.35); background:rgba(30,144,255,0.1); color:var(--cyan); cursor:pointer; transition:background 0.2s; white-space:nowrap; }
+.evac-speaker-btn:hover, .evac-speaker-btn.on { background:rgba(30,144,255,0.25); border-color:var(--cyan); }
+.evac-input-row { display:flex; gap:14px; }
+.evac-text-input { flex:1; padding:13px 18px; border-radius:8px; font-size:26px; border:1px solid rgba(30,144,255,0.25); background:rgba(0,8,36,0.7); color:var(--white); outline:none; }
+.evac-text-input::placeholder { color:rgba(255,255,255,0.2); }
+.evac-send-btn { padding:13px 40px; border-radius:8px; font-size:26px; font-weight:600; border:1px solid rgba(30,144,255,0.4); background:rgba(30,144,255,0.14); color:var(--cyan); cursor:pointer; transition:background 0.2s; }
+.evac-send-btn:hover { background:rgba(30,144,255,0.3); }
+.evac-actions { flex:0 0 380px; display:flex; flex-direction:column; gap:14px; justify-content:center; }
+.evac-btn-later { padding:22px; border-radius:10px; font-size:30px; font-weight:600; border:1px solid rgba(255,255,255,0.2); background:rgba(255,255,255,0.05); color:rgba(255,255,255,0.55); cursor:pointer; }
+.evac-btn-later:hover { background:rgba(255,255,255,0.1); }
+.evac-btn-exec { padding:22px; border-radius:10px; font-size:30px; font-weight:700; border:2px solid #ef4444; background:rgba(239,68,68,0.16); color:#ef4444; cursor:pointer; }
+.evac-btn-exec:hover { background:rgba(239,68,68,0.32); }
+.btn-alert-evac { padding:22px 70px; border-radius:10px; cursor:pointer; border:2px solid #f59e0b; background:rgba(245,158,11,0.15); color:#f59e0b; font-size:32px; font-weight:600; transition:all 0.2s; }
+.btn-alert-evac:hover { background:rgba(245,158,11,0.3); }
+
 /* ========== SCROLLBAR ========== */
 ::-webkit-scrollbar { width:8px; height:8px; }
 ::-webkit-scrollbar-track { background:transparent; }
@@ -540,11 +670,179 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
     </div>
     <div class="alert-modal-footer">
       <button class="btn-alert-ignore" onclick="closeAlert()">稍后处理</button>
+      <button class="btn-alert-evac" onclick="openEvac()">🚪 应急疏散</button>
       <button class="btn-alert-confirm" onclick="closeAlert()">确认处理</button>
     </div>
   </div>
 </div>
 
+<!-- ===== EVACUATION MODAL ===== -->
+<div id="evac-modal">
+  <div class="evac-inner">
+    <!-- Header -->
+    <div class="evac-hdr">
+      <span class="evac-hdr-icon">🚪</span>
+      <span class="evac-hdr-title">应急疏散</span>
+      <button class="evac-hdr-close" onclick="closeEvac()">✕</button>
+    </div>
+    <!-- Body -->
+    <div class="evac-body">
+      <!-- Left: floor plan -->
+      <div class="evac-map-col">
+        <div class="evac-legend">
+          <div class="evac-legend-rect"></div>
+          <span class="evac-legend-text">应急疏散路线图</span>
+        </div>
+        <div class="evac-map-wrap">
+          <svg viewBox="0 0 1060 590" xmlns="http://www.w3.org/2000/svg">
+            <defs>
+              <marker id="ev-arrow" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto">
+                <polygon points="0 0, 9 3.5, 0 7" fill="#1e90ff"/>
+              </marker>
+            </defs>
+
+            <!-- Top row background -->
+            <rect x="45" y="40" width="758" height="185" fill="rgba(0,15,50,0.3)" stroke="rgba(30,144,255,0.28)" stroke-width="1.5"/>
+            <!-- Emergency exit zone top-right -->
+            <rect x="808" y="40" width="205" height="185" fill="rgba(30,144,255,0.04)" stroke="rgba(30,144,255,0.22)" stroke-width="1" stroke-dasharray="9,5" rx="2"/>
+            <text x="910" y="118" text-anchor="middle" fill="rgba(30,144,255,0.6)" font-size="22">紧急</text>
+            <text x="910" y="146" text-anchor="middle" fill="rgba(30,144,255,0.6)" font-size="22">出口</text>
+            <line x1="870" y1="132" x2="918" y2="132" stroke="#1e90ff" stroke-width="3" marker-end="url(#ev-arrow)" opacity="0.75"/>
+
+            <!-- A101 -->
+            <rect x="45" y="40" width="190" height="185" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="45" y1="40" x2="235" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="235" y1="40" x2="45" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="140" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A101</text>
+            <!-- A102 -->
+            <rect x="235" y="40" width="190" height="185" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="235" y1="40" x2="425" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="425" y1="40" x2="235" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="330" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A102</text>
+            <!-- A103 -->
+            <rect x="425" y="40" width="190" height="185" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="425" y1="40" x2="615" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="615" y1="40" x2="425" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="520" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A103</text>
+            <!-- A104 (blue — near top-right exit) -->
+            <rect x="615" y="40" width="193" height="185" fill="rgba(30,144,255,0.1)" stroke="#1e90ff" stroke-width="3"/>
+            <line x1="615" y1="40" x2="808" y2="225" stroke="rgba(30,144,255,0.18)" stroke-width="1"/>
+            <line x1="808" y1="40" x2="615" y2="225" stroke="rgba(30,144,255,0.18)" stroke-width="1"/>
+            <text x="711" y="140" text-anchor="middle" fill="#7ab8e8" font-size="22">A104</text>
+
+            <!-- Corridor -->
+            <rect x="45" y="225" width="968" height="85" fill="rgba(0,25,70,0.18)" stroke="rgba(30,144,255,0.2)" stroke-width="1"/>
+            <text x="350" y="277" text-anchor="middle" fill="rgba(168,204,232,0.38)" font-size="24" letter-spacing="6">走道</text>
+            <!-- Assembly points -->
+            <circle cx="645" cy="268" r="22" fill="none" stroke="#1e90ff" stroke-width="2.5" opacity="0.75"/>
+            <circle cx="755" cy="268" r="22" fill="none" stroke="#1e90ff" stroke-width="2.5" opacity="0.75"/>
+
+            <!-- Bottom section background -->
+            <rect x="45" y="310" width="968" height="242" fill="rgba(0,15,50,0.3)" stroke="rgba(30,144,255,0.28)" stroke-width="1.5"/>
+            <!-- Emergency exit zone bottom-left label -->
+            <text x="22" y="445" text-anchor="middle" fill="rgba(30,144,255,0.58)" font-size="19" transform="rotate(-90,22,445)">紧急出口</text>
+            <line x1="47" y1="432" x2="16" y2="432" stroke="#1e90ff" stroke-width="3" marker-end="url(#ev-arrow)" opacity="0.7"/>
+
+            <!-- A105 (red — alert room) -->
+            <rect x="45" y="310" width="185" height="242" fill="rgba(239,68,68,0.1)" stroke="#ef4444" stroke-width="3"/>
+            <line x1="45" y1="310" x2="230" y2="552" stroke="rgba(239,68,68,0.22)" stroke-width="1.5"/>
+            <line x1="230" y1="310" x2="45" y2="552" stroke="rgba(239,68,68,0.22)" stroke-width="1.5"/>
+            <text x="137" y="440" text-anchor="middle" fill="#ef4444" font-size="22" font-weight="600">A105</text>
+            <!-- A106 -->
+            <rect x="230" y="310" width="185" height="242" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="230" y1="310" x2="415" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="415" y1="310" x2="230" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="322" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A106</text>
+            <!-- A107 -->
+            <rect x="415" y="310" width="185" height="242" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="415" y1="310" x2="600" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="600" y1="310" x2="415" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="507" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A107</text>
+            <!-- A108 -->
+            <rect x="600" y="310" width="185" height="242" fill="rgba(0,15,50,0.25)" stroke="rgba(30,144,255,0.42)" stroke-width="1.5"/>
+            <line x1="600" y1="310" x2="785" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <line x1="785" y1="310" x2="600" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
+            <text x="692" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A108</text>
+            <!-- A109 (blue — evacuation destination) -->
+            <rect x="785" y="310" width="228" height="242" fill="rgba(30,144,255,0.1)" stroke="#1e90ff" stroke-width="3"/>
+            <line x1="785" y1="310" x2="1013" y2="552" stroke="rgba(30,144,255,0.18)" stroke-width="1.5"/>
+            <line x1="1013" y1="310" x2="785" y2="552" stroke="rgba(30,144,255,0.18)" stroke-width="1.5"/>
+            <text x="899" y="440" text-anchor="middle" fill="#7ab8e8" font-size="22" font-weight="600">A109</text>
+
+            <!-- Evacuation route: bottom-left through corridor -->
+            <line x1="230" y1="432" x2="50" y2="432" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7" marker-end="url(#ev-arrow)"/>
+            <!-- Evacuation route: top-right toward exit -->
+            <line x1="808" y1="132" x2="862" y2="132" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7" marker-end="url(#ev-arrow)"/>
+            <!-- Connector: corridor down to A104 corridor side -->
+            <line x1="711" y1="225" x2="711" y2="310" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7"/>
+          </svg>
+
+          <!-- Floating alert popup over A105 -->
+          <div class="evac-popup">
+            <div class="evac-popup-hdr">
+              <span class="evac-popup-alarm">🚨</span>
+              <span class="evac-popup-time">2026-03-23 14:32:18</span>
+              <span class="evac-popup-x">✕</span>
+            </div>
+            <div class="evac-popup-badge">触发风险</div>
+            <div class="evac-popup-msg">发生风险:TVOC浓度超标</div>
+          </div>
+        </div>
+      </div>
+      <!-- Right: camera feeds -->
+      <div class="evac-right-col">
+        <div class="evac-right-hdr">房间号-实验室名称</div>
+        <div class="evac-cam-slot">实时视频监控</div>
+        <div class="evac-cam-slot">实时视频监控</div>
+        <div class="evac-floor-lbl">楼道 2层</div>
+        <div class="evac-cam-slot">实时视频监控</div>
+      </div>
+    </div>
+    <!-- Footer -->
+    <div class="evac-footer">
+      <!-- Alert metrics -->
+      <div class="evac-metrics">
+        <div class="evac-metric-row">
+          <div class="evac-metric-col">
+            <div class="evac-ml">告警指标</div>
+            <div class="evac-mv">TVOC 浓度超标</div>
+          </div>
+          <div class="evac-metric-rt">2.85 / 0.6 mg/m³</div>
+        </div>
+        <div class="evac-metric-row">
+          <div class="evac-metric-col">
+            <div class="evac-ml">当前值 / 安全阈值</div>
+            <div class="evac-mv">TVOC 浓度超标</div>
+          </div>
+          <div class="evac-metric-rt">2.85 / 0.6 mg/m³</div>
+        </div>
+      </div>
+      <!-- Voice broadcast -->
+      <div class="evac-broadcast">
+        <div class="evac-bc-hdr">
+          <span class="evac-bc-icon">📢</span>
+          <span class="evac-bc-title">语音广播</span>
+          <span class="evac-bc-device">选择播放设备</span>
+        </div>
+        <div class="evac-speaker-row">
+          <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
+          <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
+          <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
+        </div>
+        <div class="evac-input-row">
+          <input type="text" class="evac-text-input" placeholder="请输入喊话内容"/>
+          <button class="evac-send-btn">发送</button>
+        </div>
+      </div>
+      <!-- Action buttons -->
+      <div class="evac-actions">
+        <button class="evac-btn-later" onclick="closeEvac()">稍后处理</button>
+        <button class="evac-btn-exec">执行疏散</button>
+      </div>
+    </div>
+  </div>
+</div>
+
 <!-- ===== MAIN WRAPPER ===== -->
 <div id="scale-root">
   <div class="bg-hex"></div>
@@ -799,6 +1097,83 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
         </div>
       </div>
 
+      <!-- 危化品库存统计 -->
+      <div class="panel" style="flex:0 0 auto;min-height:460px">
+        <div class="border-beam"></div>
+        <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
+        <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
+        <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
+        <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
+        <div class="panel-header">
+          <div class="panel-header-icon">⚗️</div>
+          <span class="panel-title">化学品库存动态统计</span>
+        </div>
+        <div style="padding:12px 20px 15px;display:flex;gap:15px;height:calc(100% - 82px)">
+          <!-- 左侧: 饼图 -->
+          <div style="flex:1;min-width:0;display:flex;flex-direction:column">
+            <div id="chart-chem-inventory" style="flex:1;min-height:0"></div>
+          </div>
+          <!-- 右侧: 存量统计 -->
+          <div style="flex:0 0 380px;display:flex;flex-direction:column;gap:14px;justify-content:center">
+            <!-- 总量 -->
+            <div class="chem-stat-card total">
+              <div class="csc-label">存量化学品总量</div>
+              <div class="csc-value">1,229<span class="csc-unit">L</span></div>
+            </div>
+            <!-- 管控 -->
+            <div class="chem-stat-card ctrl">
+              <div style="display:flex;justify-content:space-between;align-items:flex-start">
+                <div>
+                  <div class="csc-label">管控类化学品</div>
+                  <div class="csc-value">269<span class="csc-unit">L</span></div>
+                </div>
+                <div class="csc-pct-ring" style="--pct:21.9;--clr:#ef4444">
+                  <svg viewBox="0 0 60 60" width="80" height="80">
+                    <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(239,68,68,0.12)" stroke-width="6"/>
+                    <circle cx="30" cy="30" r="24" fill="none" stroke="#ef4444" stroke-width="6"
+                      stroke-dasharray="27.5 123" stroke-linecap="round" transform="rotate(-90 30 30)"
+                      style="filter:drop-shadow(0 0 4px rgba(239,68,68,0.7))"/>
+                    <text x="30" y="35" text-anchor="middle" fill="#ef4444" font-size="11" font-weight="700" font-family="Arial">21.9%</text>
+                  </svg>
+                </div>
+              </div>
+              <div class="csc-sub">占总量 21.9% · 共 5 类目</div>
+            </div>
+            <!-- 非管控 -->
+            <div class="chem-stat-card free">
+              <div style="display:flex;justify-content:space-between;align-items:flex-start">
+                <div>
+                  <div class="csc-label">非管控类化学品</div>
+                  <div class="csc-value" style="color:#00d8ff">960<span class="csc-unit">L</span></div>
+                </div>
+                <div>
+                  <svg viewBox="0 0 60 60" width="80" height="80">
+                    <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(0,216,255,0.12)" stroke-width="6"/>
+                    <circle cx="30" cy="30" r="24" fill="none" stroke="#00d8ff" stroke-width="6"
+                      stroke-dasharray="101 50.6" stroke-linecap="round" transform="rotate(-90 30 30)"
+                      style="filter:drop-shadow(0 0 4px rgba(0,216,255,0.7))"/>
+                    <text x="30" y="35" text-anchor="middle" fill="#00d8ff" font-size="11" font-weight="700" font-family="Arial">78.1%</text>
+                  </svg>
+                </div>
+              </div>
+              <div class="csc-sub" style="color:rgba(0,216,255,0.55)">占总量 78.1% · 共 5 类目</div>
+            </div>
+            <!-- 类目总数 -->
+            <div class="chem-stat-card cat">
+              <div class="csc-label">存量化学品总类目数</div>
+              <div style="display:flex;align-items:baseline;gap:12px;margin-top:8px">
+                <div class="csc-value" style="color:#ffd740">10<span class="csc-unit">类</span></div>
+                <div style="display:flex;gap:6px;align-items:center">
+                  <span style="font-size:22px;color:rgba(239,68,68,0.8)">管控 5类</span>
+                  <span style="font-size:20px;color:rgba(168,204,232,0.3)">|</span>
+                  <span style="font-size:22px;color:rgba(0,216,255,0.8)">非管控 5类</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
     </div><!-- /2区 -->
 
     <!-- ======== 3区: 实时监控 ======== -->
@@ -868,10 +1243,20 @@ html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep);
         <div class="panel-header">
           <div class="panel-header-icon">⚠️</div>
           <span class="panel-title">实验室实时风险预警</span>
-          <div style="margin-left:auto;display:flex;align-items:center;gap:10px;flex-shrink:0">
-            <span style="font-size:25px;color:var(--text-dim)">本月</span>
-            <span style="font-size:50px;font-weight:700;color:#f59e0b">42</span>
-            <span style="font-size:25px;color:var(--text-dim)">次</span>
+          <div style="margin-left:auto;display:flex;align-items:center;gap:18px;flex-shrink:0">
+            <div style="display:flex;align-items:center;gap:6px">
+              <span style="font-size:22px">🚨</span>
+              <span style="font-size:24px;color:var(--text-dim)">应急</span>
+              <span style="font-size:44px;font-weight:700;color:#f59e0b">28</span>
+              <span style="font-size:22px;color:var(--text-dim)">次</span>
+            </div>
+            <div style="width:1px;height:40px;background:rgba(30,144,255,0.3)"></div>
+            <div style="display:flex;align-items:center;gap:6px">
+              <span style="font-size:22px">☣️</span>
+              <span style="font-size:24px;color:var(--text-dim)">违规带离</span>
+              <span style="font-size:44px;font-weight:700;color:var(--cyan)">14</span>
+              <span style="font-size:22px;color:var(--text-dim)">次</span>
+            </div>
           </div>
         </div>
         <div style="flex:1;min-height:0;padding:15px 20px;display:flex;flex-direction:column">
@@ -960,6 +1345,44 @@ setTimeout(() => animFlip('flip-total',   0, 1284, 2000), 600);
 setTimeout(() => animFlip('flip-current', 0, 47,   1500), 900);
 
 // ====================================================
+// CHEMICAL INVENTORY (2区 化学品库存动态统计)
+// ====================================================
+const CHEM_PIE_DATA = [
+  {name:'管控化学品',   value:269,  itemStyle:{color:'#ef4444'}},
+  {name:'非管控化学品', value:960,  itemStyle:{color:'#1e90ff'}},
+];
+let chemChart = null;
+function renderChemChart() {
+  if (!chemChart) {
+    const el = document.getElementById('chart-chem-inventory');
+    if (!el) return;
+    chemChart = echarts.init(el);
+  }
+  chemChart.setOption({
+    backgroundColor:'transparent',
+    tooltip:{ trigger:'item', formatter:'{b}<br/>数量:{c} L<br/>占比:{d}%' },
+    series:[{
+      type:'pie', radius:['32%','62%'],
+      center:['50%','52%'],
+      avoidLabelOverlap:true,
+      label:{
+        show:true, position:'outside',
+        formatter:params => `{name|${params.name}}\n{val|${params.value}L  ${params.percent}%}`,
+        rich:{
+          name:{ fontSize:24, color:'#a8cce8', lineHeight:30 },
+          val:{  fontSize:22, color:'#ffd740',  lineHeight:26 },
+        },
+        distanceToLabelLine:10,
+      },
+      labelLine:{ show:true, length:25, length2:35, lineStyle:{color:'rgba(168,204,232,0.5)'} },
+      emphasis:{ label:{fontSize:26, fontWeight:'bold'}, scale:true, scaleSize:8 },
+      data: CHEM_PIE_DATA,
+    }],
+  }, true);
+}
+renderChemChart();
+
+// ====================================================
 // SENSOR LIST (4区上)
 // ====================================================
 const LABS = [
@@ -998,29 +1421,44 @@ function buildSensorList() {
 buildSensorList();
 
 // ====================================================
-// WARNING LIST (4区下)
+// WARNING LIST (4区下) — 应急预警 + 危化品违规带离
 // ====================================================
 const WARNS = [
-  {lab:'化学分析实验室', room:'A301', unit:'化学研究所', metric:'TVOC浓度超标', val:'2.85 mg/m³', time:'2026-03-05 14:32:18'},
-  {lab:'有机合成实验室', room:'B302', unit:'化学研究所', metric:'CO₂浓度偏高',  val:'725 ppm',    time:'2026-03-05 13:58:44'},
-  {lab:'高压实验室',    room:'D101', unit:'工程研究所', metric:'温度异常',      val:'38.5 °C',    time:'2026-03-05 12:15:07'},
-  {lab:'生物安全实验室', room:'B201', unit:'生物研究所', metric:'湿度超标',      val:'88% RH',     time:'2026-03-05 11:40:22'},
-  {lab:'材料测试实验室', room:'C401', unit:'材料研究所', metric:'TVOC超标',      val:'1.2 mg/m³',  time:'2026-03-05 10:08:55'},
-  {lab:'光学检测实验室', room:'C203', unit:'物理研究所', metric:'O₂浓度偏低',   val:'19.2 %',     time:'2026-03-05 09:22:33'},
-  {lab:'精密仪器实验室', room:'A205', unit:'物理研究所', metric:'温度超标',      val:'28.5 °C',    time:'2026-03-05 08:45:11'},
-  {lab:'低温实验室',    room:'D205', unit:'物理研究所', metric:'气压异常',      val:'85 kPa',     time:'2026-03-04 23:12:40'},
+  {type:'emergency', lab:'化学分析实验室', room:'A301', unit:'化学研究所', metric:'TVOC浓度超标', val:'2.85 mg/m³', time:'2026-03-23 14:32:18'},
+  {type:'chemical',  lab:'有机合成实验室', room:'B302', unit:'化学研究所', person:'李研究员',     action:'盐酸未正常使用违规带离',  time:'2026-03-23 13:58:44'},
+  {type:'emergency', lab:'有机合成实验室', room:'B302', unit:'化学研究所', metric:'CO₂浓度偏高',  val:'725 ppm',    time:'2026-03-23 12:15:07'},
+  {type:'chemical',  lab:'生物安全实验室', room:'B201', unit:'生物研究所', person:'王实验员',     action:'甲醇溶液违规带出实验室',   time:'2026-03-22 17:40:22'},
+  {type:'emergency', lab:'高压实验室',    room:'D101', unit:'工程研究所', metric:'温度异常',      val:'38.5 °C',    time:'2026-03-22 12:15:07'},
+  {type:'emergency', lab:'生物安全实验室', room:'B201', unit:'生物研究所', metric:'湿度超标',      val:'88% RH',     time:'2026-03-21 11:40:22'},
+  {type:'chemical',  lab:'材料测试实验室', room:'C401', unit:'材料研究所', person:'张工程师',     action:'硫酸未经审批带离',        time:'2026-03-21 10:08:55'},
+  {type:'emergency', lab:'光学检测实验室', room:'C203', unit:'物理研究所', metric:'O₂浓度偏低',   val:'19.2 %',     time:'2026-03-20 09:22:33'},
+  {type:'chemical',  lab:'精密仪器实验室', room:'A205', unit:'物理研究所', person:'赵研究员',     action:'丙酮溶液违规带离实验楼',   time:'2026-03-19 16:45:11'},
+  {type:'emergency', lab:'低温实验室',    room:'D205', unit:'物理研究所', metric:'气压异常',      val:'85 kPa',     time:'2026-03-18 23:12:40'},
 ];
 function buildWarnList() {
   const el = document.getElementById('warn-list'); if (!el) return;
   const all = [...WARNS, ...WARNS];
-  el.innerHTML = all.map(w => `
-    <div class="warn-item">
-      <div class="warn-item-head">
-        <span class="warn-lab">🚨 ${w.lab}(${w.room})- ${w.unit}</span>
+  el.innerHTML = all.map(w => {
+    const isEm = w.type === 'emergency';
+    const labCls = isEm ? 'warn-lab-info' : 'warn-lab-info chemical';
+    const badge = isEm
+      ? '<span class="warn-type-badge emergency">🚨 应急预警</span>'
+      : '<span class="warn-type-badge chemical">☣️ 违规带离</span>';
+    const detail = isEm
+      ? `异常传感器:<span class="warn-metric-val">${w.metric} ${w.val}</span>`
+      : `<span style="color:#67e8f9">${w.person}</span> — <span style="color:#fbbf24">${w.action}</span>`;
+    return `
+    <div class="warn-item type-${w.type}">
+      <div class="warn-item-head" style="flex-wrap:wrap;gap:8px 15px">
+        <div style="display:flex;align-items:center;flex:1;min-width:0;gap:8px">
+          ${badge}
+          <span class="${labCls}">${w.lab}(${w.room})- ${w.unit}</span>
+        </div>
         <span class="warn-time">${w.time}</span>
       </div>
-      <div class="warn-detail">异常指标:<span class="warn-metric-val">${w.metric} ${w.val}</span></div>
-    </div>`).join('');
+      <div class="warn-detail">${detail}</div>
+    </div>`;
+  }).join('');
 }
 buildWarnList();
 
@@ -1028,6 +1466,8 @@ buildWarnList();
 // ALERT MODAL + CAMERA
 // ====================================================
 function closeAlert() { document.getElementById('alert-modal').classList.remove('show'); }
+function openEvac()   { document.getElementById('evac-modal').classList.add('show'); }
+function closeEvac()  { document.getElementById('evac-modal').classList.remove('show'); }
 
 let alertCamRAF = null;
 function startAlertCam() {
@@ -1436,7 +1876,7 @@ function drawFakeCam(canvas, idx) {
 // RESIZE CHARTS
 // ====================================================
 window.addEventListener('resize', () => {
-  ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie-status','chart-pie-usage'].forEach(id => {
+  ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie-status','chart-pie-usage','chart-chem-inventory'].forEach(id => {
     const el = document.getElementById(id);
     if (el) { const c = echarts.getInstanceByDom(el); if (c) c.resize(); }
   });

BIN
实验室安全智能监测与管控系统数据可视化释义(Large screen9600).docx