index-v2.html 106 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>实验室安全智能监测与管控中心</title>
  7. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  8. <style>
  9. /* ========== RESET ========== */
  10. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  11. /* ========== DEEP BLUE SCI-FI THEME ========== */
  12. :root {
  13. --bg-deep: #030e1f;
  14. --bg-panel: rgba(5,16,42,0.95);
  15. --bg-card: rgba(7,22,54,0.85);
  16. --blue: #1e90ff;
  17. --blue-dim: rgba(30,144,255,0.14);
  18. --blue-glow: rgba(30,144,255,0.5);
  19. --cyan: #00d8ff;
  20. --cyan-dim: rgba(0,216,255,0.12);
  21. --teal: #00e5c8;
  22. --indigo: #4361ee;
  23. --red: #ff3b3b;
  24. --green: #00e676;
  25. --gold: #ffd740;
  26. --white: #ddf0ff;
  27. --text: #a8cce8;
  28. --text-dim: rgba(110,165,210,0.55);
  29. --border: rgba(30,144,255,0.22);
  30. --border-h: rgba(30,144,255,0.65);
  31. --shadow: 0 4px 24px rgba(0,80,220,0.22);
  32. --font-main: 'PingFang SC','Microsoft YaHei','Noto Sans SC',sans-serif;
  33. }
  34. html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep); font-family:var(--font-main); color:var(--text); }
  35. /* ========== FIXED SIZE WRAPPER ========== */
  36. #scale-root {
  37. width:9600px; height:2800px;
  38. position:relative;
  39. overflow:hidden;
  40. display:flex; flex-direction:column;
  41. }
  42. /* ========== BACKGROUND ========== */
  43. #nebula-canvas { position:fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:0; }
  44. .bg-hex {
  45. position:absolute; inset:0; pointer-events:none; z-index:0;
  46. background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='52' viewBox='0 0 60 52'%3E%3Cpolygon points='30,2 58,17 58,47 30,52 2,47 2,17' fill='none' stroke='rgba(30,144,255,0.05)' stroke-width='1'/%3E%3C/svg%3E");
  47. background-size:150px 130px;
  48. }
  49. .bg-hex::after { content:''; position:absolute; inset:0; background:radial-gradient(ellipse at 50% 50%, transparent 25%, var(--bg-deep) 85%); }
  50. /* ========== TOP NAV ========== */
  51. .top-nav {
  52. position:relative; z-index:10; flex-shrink:0;
  53. height:160px; display:flex; align-items:stretch;
  54. background:linear-gradient(180deg, rgba(4,18,52,0.99) 0%, rgba(3,12,36,0.97) 100%);
  55. border-bottom:1px solid rgba(30,144,255,0.3);
  56. box-shadow:0 2px 30px rgba(0,80,220,0.28), inset 0 1px 0 rgba(30,144,255,0.15);
  57. }
  58. .nav-left { display:flex; align-items:center; padding:0 50px; width:950px; gap:30px; }
  59. .nav-logo {
  60. width:100px; height:100px; border-radius:20px; flex-shrink:0;
  61. background:linear-gradient(135deg,#0d2d6b,#1565c0);
  62. display:flex; align-items:center; justify-content:center;
  63. box-shadow:0 0 40px rgba(30,144,255,0.6), inset 0 0 20px rgba(255,255,255,0.1);
  64. position:relative; overflow:visible;
  65. }
  66. .nav-logo::after { content:'安'; font-size:45px; font-weight:900; color:rgba(255,255,255,0.95); }
  67. .nav-logo-ring {
  68. position:absolute; inset:-10px; border-radius:25px;
  69. border:2px solid rgba(30,144,255,0.5);
  70. animation:ringPulse 2s ease-in-out infinite;
  71. }
  72. @keyframes ringPulse { 0%,100%{transform:scale(1);opacity:0.5} 50%{transform:scale(1.1);opacity:1} }
  73. .nav-org { font-size:30px; color:var(--text-dim); line-height:1.4; }
  74. .nav-org strong { display:block; font-size:32px; color:var(--cyan); }
  75. .nav-center {
  76. flex:1; display:flex; align-items:center; justify-content:center; gap:30px; position:relative;
  77. }
  78. .nav-title {
  79. font-size:65px; font-weight:700; letter-spacing:10px;
  80. background:linear-gradient(90deg,var(--blue),var(--cyan),var(--white),var(--cyan),var(--blue));
  81. background-size:200% auto;
  82. -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
  83. animation:titleShine 4s linear infinite;
  84. filter:drop-shadow(0 0 40px rgba(30,144,255,0.5));
  85. }
  86. @keyframes titleShine { from{background-position:0% center} to{background-position:200% center} }
  87. .nav-title-deco {
  88. position:absolute; bottom:15px; left:50%; transform:translateX(-50%);
  89. width:900px; height:2px;
  90. background:linear-gradient(90deg,transparent,var(--blue),var(--cyan),var(--blue),transparent);
  91. animation:decoGlow 3s ease-in-out infinite;
  92. }
  93. @keyframes decoGlow { 0%,100%{opacity:0.5} 50%{opacity:1;filter:blur(1px)} }
  94. .btn-nav-switch {
  95. padding:18px 55px; border-radius:50px; cursor:pointer; outline:none;
  96. background:var(--bg-deep);
  97. border:2px solid var(--blue); color:var(--cyan);
  98. font-size:32px; font-weight:600; letter-spacing:2px;
  99. transition:all 0.3s; position:relative; overflow:hidden; flex-shrink:0;
  100. }
  101. .btn-nav-switch::before {
  102. content:''; position:absolute; top:0; left:-100%; width:100%; height:100%;
  103. background:linear-gradient(90deg,transparent,rgba(30,144,255,0.25),transparent);
  104. animation:btnShine 3s linear infinite;
  105. }
  106. @keyframes btnShine { from{left:-100%} to{left:100%} }
  107. .btn-nav-switch:hover { box-shadow:0 0 60px rgba(30,144,255,0.6); transform:translateY(-2px); }
  108. .nav-right { width:950px; display:flex; align-items:center; justify-content:flex-end; padding:0 50px; gap:50px; }
  109. .nav-weather { display:flex; align-items:center; gap:20px; font-size:32px; color:var(--text-dim); }
  110. .nav-weather .icon { font-size:45px; }
  111. .nav-clock .time { font-size:55px; font-weight:300; color:var(--gold); font-variant-numeric:tabular-nums; letter-spacing:4px; }
  112. .nav-clock .date { font-size:30px; color:var(--text-dim); margin-top:5px; text-align:right; }
  113. /* ========== MAIN CONTENT: 4 COLS 2:2:4:2 ========== */
  114. .main-content {
  115. flex:1; min-height:0;
  116. display:grid;
  117. grid-template-columns:2fr 1.5fr 4.5fr 2fr;
  118. gap:0; padding:20px 20px 20px;
  119. position:relative; z-index:2;
  120. }
  121. /* ========== PANEL SYSTEM ========== */
  122. .panel-col { display:flex; flex-direction:column; gap:18px; min-height:0; padding:0 10px; }
  123. .panel {
  124. position:relative; border-radius:15px; overflow:hidden;
  125. background:var(--bg-panel);
  126. border:1px solid var(--border);
  127. flex-shrink:0;
  128. }
  129. .panel::before {
  130. content:''; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
  131. background:linear-gradient(135deg,rgba(30,144,255,0.05) 0%,transparent 50%,rgba(0,216,255,0.03) 100%);
  132. }
  133. /* Corner ornaments */
  134. .pc { position:absolute; width:35px; height:35px; z-index:3; pointer-events:none; }
  135. .pc.tl { top:0; left:0; }
  136. .pc.tr { top:0; right:0; transform:scaleX(-1); }
  137. .pc.bl { bottom:0; left:0; transform:scaleY(-1); }
  138. .pc.br { bottom:0; right:0; transform:scale(-1); }
  139. .pc svg { width:100%; height:100%; }
  140. /* Animated border beam */
  141. .border-beam { position:absolute; inset:0; pointer-events:none; border-radius:inherit; overflow:hidden; z-index:2; }
  142. .border-beam::before {
  143. content:''; position:absolute; top:0; left:-100%; width:40%; height:3px;
  144. background:linear-gradient(90deg,transparent,rgba(30,144,255,0.9),rgba(0,216,255,0.7),transparent);
  145. animation:beamTop 5s linear infinite;
  146. }
  147. .border-beam::after {
  148. content:''; position:absolute; bottom:0; right:-100%; width:40%; height:3px;
  149. background:linear-gradient(90deg,transparent,rgba(0,216,255,0.7),rgba(30,144,255,0.9),transparent);
  150. animation:beamBottom 5s linear infinite 2.5s;
  151. }
  152. @keyframes beamTop { from{left:-40%} to{left:100%} }
  153. @keyframes beamBottom { from{right:-40%} to{right:100%} }
  154. .panel-header {
  155. display:flex; align-items:center; gap:25px;
  156. padding:20px 30px 18px;
  157. border-bottom:1px solid var(--border);
  158. background:linear-gradient(90deg,rgba(0,60,160,0.18),transparent);
  159. flex-shrink:0;
  160. }
  161. .panel-header-icon {
  162. width:65px; height:65px; border-radius:12px; flex-shrink:0;
  163. display:flex; align-items:center; justify-content:center; font-size:32px;
  164. background:linear-gradient(135deg,rgba(30,144,255,0.25),rgba(0,216,255,0.15));
  165. border:1px solid rgba(30,144,255,0.35);
  166. animation:iconGlow 3s ease-in-out infinite;
  167. }
  168. @keyframes iconGlow {
  169. 0%,100%{box-shadow:0 0 15px rgba(30,144,255,0.3)}
  170. 50%{box-shadow:0 0 35px rgba(30,144,255,0.7),0 0 12px rgba(0,216,255,0.3)}
  171. }
  172. .panel-title { font-size:30px; font-weight:600; letter-spacing:2px; color:var(--cyan); }
  173. /* ========== STATUS BADGES ========== */
  174. .status-row { display:flex; gap:15px; }
  175. .status-badge {
  176. flex:1; padding:15px 10px; border-radius:10px; text-align:center;
  177. background:var(--bg-card); border:1px solid var(--border);
  178. }
  179. .status-badge.active { border-color:var(--green); background:rgba(0,230,118,0.07); }
  180. .status-badge.warning { border-color:#f59e0b; background:rgba(245,158,11,0.07); }
  181. .status-badge.idle { border-color:var(--indigo); background:rgba(67,97,238,0.07); }
  182. .status-badge .val { font-size:48px; font-weight:700; }
  183. .status-badge.active .val { color:var(--green); }
  184. .status-badge.warning .val { color:#f59e0b; }
  185. .status-badge.idle .val { color:var(--indigo); }
  186. .status-badge .lbl { font-size:25px; color:var(--text-dim); margin-top:5px; }
  187. /* ========== FLIP COUNTER ========== */
  188. .flip-counters { display:flex; gap:25px; }
  189. .flip-counter { flex:1; text-align:center; padding:18px 15px; border-radius:10px; background:var(--bg-card); border:1px solid var(--border); }
  190. .flip-counter .fc-label { font-size:25px; color:var(--text-dim); margin-bottom:12px; letter-spacing:2px; }
  191. .flip-counter .fc-digits { display:flex; gap:8px; justify-content:center; }
  192. .flip-digit {
  193. width:50px; height:75px; border-radius:8px;
  194. background:linear-gradient(180deg,rgba(4,18,55,0.9),rgba(2,10,32,0.95));
  195. border:1px solid rgba(30,144,255,0.3);
  196. display:flex; align-items:center; justify-content:center;
  197. font-size:45px; font-weight:700; color:var(--gold);
  198. font-variant-numeric:tabular-nums; position:relative; overflow:hidden;
  199. }
  200. .flip-digit::after { content:''; position:absolute; left:0; right:0; top:50%; height:2px; background:rgba(0,0,0,0.4); }
  201. /* ========== SENSOR SCROLL ========== */
  202. .sensor-scroll-wrap { overflow:hidden; flex:1; min-height:0; }
  203. .sensor-scroll-inner { animation:scrollUp 30s linear infinite; }
  204. .sensor-scroll-inner:hover { animation-play-state:paused; }
  205. @keyframes scrollUp { 0%{transform:translateY(0)} 100%{transform:translateY(-50%)} }
  206. .sensor-item {
  207. padding:18px 20px; border-radius:10px; margin-bottom:12px;
  208. background:var(--bg-card); border:1px solid var(--border);
  209. }
  210. .sensor-item.alert {
  211. border-color:rgba(255,59,59,0.6); background:rgba(255,30,30,0.07);
  212. animation:alertGlow 1.5s ease-in-out infinite;
  213. }
  214. @keyframes alertGlow { 0%,100%{box-shadow:0 0 12px rgba(255,59,59,0.25)} 50%{box-shadow:0 0 40px rgba(255,59,59,0.6)} }
  215. .sensor-item-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
  216. .sensor-name { font-size:28px; font-weight:600; color:var(--white); }
  217. .sensor-unit { font-size:25px; color:var(--text-dim); }
  218. .sensor-metrics { display:flex; gap:10px; flex-wrap:wrap; }
  219. .sensor-metric {
  220. padding:5px 15px; border-radius:5px; font-size:25px;
  221. background:rgba(4,16,45,0.7); border:1px solid rgba(30,144,255,0.2);
  222. display:flex; align-items:center; gap:8px; color:var(--text);
  223. }
  224. .sensor-metric.alarm {
  225. background:rgba(255,0,30,0.12); border-color:var(--red); color:var(--red);
  226. animation:blinkRed 0.8s ease-in-out infinite;
  227. }
  228. @keyframes blinkRed { 0%,100%{opacity:1} 50%{opacity:0.35} }
  229. /* ========== WARNING SCROLL ========== */
  230. .warn-scroll-wrap { overflow:hidden; flex:1; min-height:0; }
  231. .warn-scroll-inner { animation:scrollUp 22s linear infinite; }
  232. .warn-scroll-inner:hover { animation-play-state:paused; }
  233. .warn-item {
  234. padding:18px 20px; border-radius:10px; margin-bottom:12px;
  235. background:rgba(245,158,11,0.05); border:1px solid rgba(245,158,11,0.2);
  236. }
  237. .warn-item-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px; gap:15px; }
  238. .warn-lab { font-size:28px; font-weight:600; color:#fcd34d; flex:1; }
  239. .warn-time { font-size:25px; color:var(--text-dim); white-space:nowrap; }
  240. .warn-detail { font-size:25px; color:var(--text-dim); display:flex; align-items:center; gap:10px; }
  241. .warn-metric-val { color:#fb923c; font-weight:600; }
  242. /* ========== CHEMICAL INVENTORY TAGS ========== */
  243. .chem-tag-row { display:flex; gap:12px; margin-bottom:12px; }
  244. .chem-tag {
  245. flex:1; padding:14px 18px; border-radius:8px; text-align:center; cursor:pointer;
  246. font-size:26px; font-weight:600; letter-spacing:2px; border:1px solid rgba(30,144,255,0.3);
  247. background:var(--bg-card); color:var(--text-dim); transition:all 0.25s;
  248. }
  249. .chem-tag.active {
  250. border-color:var(--cyan); color:var(--cyan);
  251. background:rgba(0,216,255,0.1); text-shadow:0 0 10px rgba(0,216,255,0.5);
  252. box-shadow:0 0 12px rgba(0,216,255,0.15);
  253. }
  254. .chem-tag:hover:not(.active) { border-color:rgba(30,144,255,0.6); color:var(--text); }
  255. /* chemical inventory summary cards */
  256. .chem-stat-card {
  257. border-radius:10px; padding:16px 20px;
  258. background:rgba(4,14,38,0.7); border:1px solid rgba(30,144,255,0.15);
  259. transition:border-color 0.25s;
  260. }
  261. .chem-stat-card:hover { border-color:rgba(30,144,255,0.4); }
  262. .chem-stat-card.total { border-color:rgba(30,144,255,0.3); background:rgba(30,144,255,0.06); text-align:center; }
  263. .chem-stat-card.ctrl { border-color:rgba(239,68,68,0.25); background:rgba(239,68,68,0.05); }
  264. .chem-stat-card.free { border-color:rgba(0,216,255,0.2); background:rgba(0,216,255,0.04); }
  265. .chem-stat-card.cat { border-color:rgba(255,215,64,0.22); background:rgba(255,215,64,0.04); }
  266. .csc-label { font-size:24px; color:var(--text-dim); letter-spacing:2px; margin-bottom:4px; }
  267. .csc-value { font-size:52px; font-weight:700; color:#ef4444; line-height:1.1; }
  268. .csc-value.total { color:var(--white); }
  269. .chem-stat-card.total .csc-value { color:var(--white); font-size:60px; }
  270. .csc-unit { font-size:24px; font-weight:400; color:var(--text-dim); margin-left:6px; }
  271. .csc-sub { font-size:22px; color:rgba(239,68,68,0.55); margin-top:8px; }
  272. /* warn-item type: emergency vs chemical */
  273. .warn-item.type-emergency {
  274. background:rgba(245,158,11,0.06); border:1px solid rgba(245,158,11,0.25);
  275. }
  276. .warn-item.type-chemical {
  277. background:rgba(0,216,255,0.04); border:1px solid rgba(0,216,255,0.18);
  278. }
  279. .warn-type-badge {
  280. display:inline-flex; align-items:center; gap:5px;
  281. padding:4px 12px; border-radius:6px; font-size:22px; font-weight:600; margin-right:10px;
  282. }
  283. .warn-type-badge.emergency { background:rgba(245,158,11,0.15); color:#fbbf24; }
  284. .warn-type-badge.chemical { background:rgba(0,216,255,0.12); color:var(--cyan); }
  285. .warn-lab-info { font-size:27px; font-weight:600; color:#fcd34d; flex:1; min-width:0; }
  286. .warn-lab-info.chemical { color:#67e8f9; }
  287. /* ========== DEVICE STATS ========== */
  288. .device-online-row { display:flex; gap:18px; margin-bottom:18px; }
  289. .device-stat-chip {
  290. flex:1; padding:15px; border-radius:10px; text-align:center;
  291. background:var(--bg-card); border:1px solid var(--border);
  292. }
  293. .device-stat-chip.online { border-color:var(--green); }
  294. .device-stat-chip.offline { border-color:var(--red); }
  295. .device-stat-chip .dv { font-size:50px; font-weight:700; }
  296. .device-stat-chip.online .dv { color:var(--green); }
  297. .device-stat-chip.offline .dv { color:var(--red); }
  298. .device-stat-chip .dl { font-size:25px; color:var(--text-dim); }
  299. .device-list-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
  300. .device-list-item {
  301. padding:15px; border-radius:10px;
  302. background:var(--bg-card); border:1px solid var(--border);
  303. display:flex; flex-direction:column; align-items:center; gap:8px;
  304. }
  305. .device-list-item .dli-icon { font-size:40px; }
  306. .device-list-item .dli-num { font-size:38px; font-weight:700; color:var(--gold); }
  307. .device-list-item .dli-name { font-size:22px; color:var(--text-dim); text-align:center; line-height:1.3; }
  308. /* ========== EQUIP STATS ========== */
  309. .equip-mid-row { display:flex; gap:15px; }
  310. .equip-stat-item { flex:1; text-align:center; padding:15px 10px; border-radius:10px; background:var(--bg-card); border:1px solid var(--border); }
  311. .equip-stat-item .ev { font-size:38px; font-weight:700; color:var(--gold); }
  312. .equip-stat-item .el { font-size:22px; color:var(--text-dim); margin-top:5px; }
  313. /* ========== EQUIP STATUS CARDS ========== */
  314. .equip-status-card {
  315. position:relative; border-radius:12px; padding:18px 22px 15px;
  316. background:var(--bg-card);
  317. border:1px solid color-mix(in srgb, var(--sc) 35%, transparent);
  318. display:flex; flex-direction:column; justify-content:space-between;
  319. overflow:hidden; transition:border-color 0.3s;
  320. }
  321. .equip-status-card::before {
  322. content:''; position:absolute; inset:0;
  323. background:radial-gradient(ellipse at 10% 10%, color-mix(in srgb, var(--sc) 10%, transparent) 0%, transparent 65%);
  324. pointer-events:none;
  325. }
  326. .equip-status-card:hover { border-color:color-mix(in srgb, var(--sc) 70%, transparent); }
  327. .esc-icon {
  328. width:70px; height:70px; color:var(--sc);
  329. filter:drop-shadow(0 0 12px var(--sc));
  330. animation:iconGlow 3s ease-in-out infinite;
  331. flex-shrink:0;
  332. }
  333. .esc-num {
  334. font-size:55px; font-weight:700; letter-spacing:2px;
  335. color:var(--sc); line-height:1;
  336. text-shadow:0 0 30px color-mix(in srgb, var(--sc) 60%, transparent);
  337. }
  338. .esc-label { font-size:25px; color:var(--text-dim); margin-top:2px; }
  339. .esc-bar { height:5px; border-radius:2px; background:rgba(255,255,255,0.06); margin-top:10px; overflow:hidden; }
  340. .esc-bar-fill {
  341. height:100%; border-radius:2px;
  342. background:linear-gradient(90deg, var(--sc), color-mix(in srgb, var(--sc) 50%, transparent));
  343. box-shadow:0 0 10px var(--sc);
  344. }
  345. /* ========== CENTER COL (3区) ========== */
  346. .center-col { display:flex; flex-direction:column; padding:0 10px; min-height:0; position:relative; }
  347. /* Monitor view — 全时显示 */
  348. #view-monitor {
  349. flex:1; display:flex; flex-direction:column; border-radius:15px;
  350. border:1px solid var(--border); background:var(--bg-panel); overflow:hidden; min-height:0;
  351. }
  352. .monitor-header {
  353. padding:20px 30px; border-bottom:1px solid var(--border); flex-shrink:0;
  354. display:flex; align-items:center; gap:25px;
  355. background:linear-gradient(90deg,rgba(0,60,160,0.18),transparent);
  356. }
  357. .monitor-inner { flex:1; display:grid; grid-template-columns:575px 1fr; gap:0; min-height:0; overflow:hidden; }
  358. .monitor-left {
  359. display:flex; flex-direction:column; gap:15px; padding:20px; border-right:1px solid var(--border);
  360. min-height:0; overflow:hidden;
  361. }
  362. .monitor-right { padding:20px; display:flex; flex-direction:column; gap:15px; min-height:0; }
  363. .search-box {
  364. display:flex; align-items:center; gap:18px; padding:15px 25px; border-radius:10px;
  365. background:var(--bg-card); border:1px solid var(--border); flex-shrink:0;
  366. }
  367. .search-box input { flex:1; background:none; border:none; outline:none; color:var(--text); font-size:30px; font-family:var(--font-main); }
  368. .search-box input::placeholder { color:var(--text-dim); }
  369. .filter-select {
  370. padding:12px 25px; border-radius:10px; width:100%; flex-shrink:0;
  371. background:var(--bg-card); border:1px solid var(--border);
  372. color:var(--text); font-size:28px; font-family:var(--font-main); outline:none; cursor:pointer; appearance:none;
  373. background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='rgba(30,144,255,0.6)'/%3E%3C/svg%3E");
  374. background-repeat:no-repeat; background-position:right 25px center;
  375. }
  376. .tree-wrap { flex:1; overflow-y:auto; overflow-x:hidden; min-height:0; }
  377. .tree-wrap::-webkit-scrollbar { width:8px; }
  378. .tree-wrap::-webkit-scrollbar-thumb { background:var(--border); border-radius:5px; }
  379. .tree-node-label {
  380. display:flex; align-items:center; gap:15px; padding:12px 20px; border-radius:8px;
  381. font-size:28px; color:var(--text); transition:all 0.2s; user-select:none; cursor:pointer;
  382. }
  383. .tree-node-label:hover { background:var(--blue-dim); color:var(--blue); }
  384. .tree-node-label.selected { background:rgba(30,144,255,0.15); color:var(--cyan); border-left:5px solid var(--blue); }
  385. .tree-node-label .arrow { transition:transform 0.2s; font-size:25px; color:var(--text-dim); flex-shrink:0; }
  386. .tree-node-label .arrow.open { transform:rotate(90deg); }
  387. .tree-children { padding-left:35px; display:none; }
  388. .tree-children.open { display:block; }
  389. .camera-grid-header { display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
  390. .camera-breadcrumb { font-size:30px; color:var(--text-dim); }
  391. .camera-breadcrumb span { color:var(--cyan); }
  392. .camera-pager { display:flex; align-items:center; gap:15px; }
  393. .pager-btn {
  394. width:60px; height:60px; border-radius:8px; cursor:pointer;
  395. background:var(--bg-card); border:1px solid var(--border);
  396. color:var(--text-dim); font-size:32px; display:flex; align-items:center; justify-content:center; transition:all 0.2s;
  397. }
  398. .pager-btn:hover { border-color:var(--blue); color:var(--blue); }
  399. .pager-info { font-size:28px; color:var(--text-dim); }
  400. .camera-grid {
  401. display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr);
  402. gap:12px; flex:1; min-height:0;
  403. }
  404. .camera-cell {
  405. position:relative; border-radius:10px; overflow:hidden;
  406. background:#020810; border:1px solid var(--border);
  407. display:flex; align-items:center; justify-content:center;
  408. }
  409. .camera-cell:hover { border-color:var(--blue); }
  410. .camera-cell.ai-cam { border-color:rgba(0,230,118,0.4); }
  411. .camera-cell canvas { width:100%; height:100%; display:block; }
  412. .camera-overlay { position:absolute; inset:0; pointer-events:none; background:linear-gradient(180deg,rgba(0,0,0,0.4) 0%,transparent 30%,transparent 70%,rgba(0,0,0,0.5) 100%); }
  413. .camera-label { position:absolute; bottom:10px; left:15px; font-size:25px; color:rgba(255,255,255,0.75); background:rgba(0,0,0,0.5); padding:3px 10px; border-radius:5px; }
  414. .camera-rec { position:absolute; top:10px; right:15px; display:flex; align-items:center; gap:8px; font-size:25px; color:var(--red); background:rgba(0,0,0,0.5); padding:3px 10px; border-radius:5px; }
  415. .camera-rec::before { content:''; width:12px; height:12px; border-radius:50%; background:var(--red); animation:blinkRed 1s ease-in-out infinite; }
  416. .camera-ai-badge { position:absolute; top:10px; left:15px; font-size:25px; color:var(--green); background:rgba(0,30,15,0.75); padding:3px 10px; border-radius:5px; border:1px solid rgba(0,230,118,0.3); }
  417. .ai-detection-box { position:absolute; border:5px solid var(--red); background:rgba(255,59,59,0.1); border-radius:5px; animation:detBox 1.5s ease-in-out infinite; }
  418. @keyframes detBox { 0%,100%{border-color:#ff3b3b;box-shadow:0 0 20px rgba(255,59,59,0.4)} 50%{border-color:#fca5a5;box-shadow:0 0 45px rgba(255,59,59,0.7)} }
  419. .ai-detection-label { position:absolute; bottom:100%; left:0; white-space:nowrap; font-size:25px; background:#ff3b3b; color:#fff; padding:3px 10px; border-radius:5px; margin-bottom:5px; }
  420. /* ========== STATUS DOTS ========== */
  421. .status-dot { width:20px; height:20px; border-radius:50%; display:inline-block; animation:dotPulse 2s ease-in-out infinite; }
  422. .status-dot.green { background:var(--green); box-shadow:0 0 15px var(--green); }
  423. .status-dot.red { background:var(--red); box-shadow:0 0 15px var(--red); animation:blinkRed 0.8s ease-in-out infinite; }
  424. .status-dot.orange { background:#f59e0b; box-shadow:0 0 15px #f59e0b; }
  425. @keyframes dotPulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.3);opacity:0.7} }
  426. /* ========== SCAN EFFECT ========== */
  427. .scan-effect { position:absolute; inset:0; pointer-events:none; overflow:hidden; z-index:0; }
  428. .scan-effect::after {
  429. content:''; position:absolute; left:0; right:0; height:200px; top:-200px;
  430. background:linear-gradient(180deg,transparent,rgba(30,144,255,0.025),transparent);
  431. animation:scanFull 9s linear infinite;
  432. }
  433. @keyframes scanFull { from{top:-200px} to{top:100%} }
  434. /* ========== ALERT MODAL ========== */
  435. #alert-modal {
  436. position:fixed; inset:0; z-index:9999;
  437. display:none; align-items:center; justify-content:center;
  438. background:rgba(0,0,0,0.9); backdrop-filter:blur(6px);
  439. }
  440. #alert-modal.show { display:flex; animation:alertFadeIn 0.4s ease; }
  441. @keyframes alertFadeIn { from{opacity:0;transform:scale(0.88)} to{opacity:1;transform:scale(1)} }
  442. .alert-modal-inner {
  443. position:relative; width:2200px; border-radius:20px; overflow:hidden;
  444. background:linear-gradient(135deg,rgba(38,3,8,0.98),rgba(14,2,5,0.99));
  445. border:2px solid rgba(239,68,68,0.5);
  446. box-shadow:0 0 250px rgba(239,68,68,0.45),inset 0 0 150px rgba(200,0,0,0.08);
  447. }
  448. .alert-scan-line { position:absolute; left:0; right:0; height:5px; background:linear-gradient(90deg,transparent,rgba(239,68,68,0.9),transparent); animation:alertScan 1.5s linear infinite; top:0; z-index:2; }
  449. @keyframes alertScan { from{top:0} to{top:100%} }
  450. .alert-modal-header {
  451. padding:45px 60px 35px; background:linear-gradient(90deg,rgba(200,0,0,0.18),transparent);
  452. display:flex; align-items:center; gap:40px; border-bottom:2px solid rgba(239,68,68,0.25);
  453. }
  454. .alert-icon { font-size:85px; animation:alertFlash 0.6s ease-in-out infinite; }
  455. @keyframes alertFlash { 0%,100%{opacity:1} 50%{opacity:0.15} }
  456. .alert-title { font-size:55px; font-weight:700; color:#ef4444; letter-spacing:8px; }
  457. .alert-subtitle { font-size:28px; color:rgba(239,100,100,0.6); margin-top:8px; letter-spacing:5px; }
  458. /* body: flex row — left cam + right info */
  459. .alert-modal-body { padding:45px 60px; display:flex; gap:50px; align-items:flex-start; }
  460. /* LEFT: camera panel */
  461. .alert-cam-wrap {
  462. flex:0 0 900px;
  463. display:flex; flex-direction:column; gap:15px;
  464. }
  465. .alert-cam-label {
  466. font-size:28px; color:rgba(255,150,150,0.7); letter-spacing:5px;
  467. display:flex; align-items:center; gap:20px;
  468. }
  469. .alert-cam-label::before { content:''; width:15px; height:15px; border-radius:50%; background:#ef4444; box-shadow:0 0 20px #ef4444; animation:blinkRed 0.8s ease-in-out infinite; }
  470. .alert-cam-box {
  471. position:relative; width:900px; height:700px; border-radius:10px; overflow:hidden;
  472. border:2px solid rgba(239,68,68,0.5);
  473. box-shadow:0 0 50px rgba(239,68,68,0.25), inset 0 0 75px rgba(200,0,0,0.1);
  474. }
  475. .alert-cam-box canvas { display:block; width:100%; height:100%; }
  476. .alert-cam-overlay { position:absolute; inset:0; pointer-events:none; background:linear-gradient(180deg,rgba(0,0,0,0.35) 0%,transparent 25%,transparent 75%,rgba(0,0,0,0.45) 100%); }
  477. .alert-cam-rec { position:absolute; top:20px; right:25px; display:flex; align-items:center; gap:10px; font-size:28px; color:#ef4444; background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
  478. .alert-cam-rec::before { content:''; width:15px; height:15px; border-radius:50%; background:#ef4444; animation:blinkRed 1s ease-in-out infinite; }
  479. .alert-cam-id { position:absolute; top:20px; left:25px; font-size:28px; color:rgba(255,255,255,0.8); background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
  480. .alert-cam-bottom { position:absolute; bottom:20px; left:25px; right:25px; display:flex; justify-content:space-between; align-items:center; }
  481. .alert-cam-name { font-size:28px; color:rgba(255,255,255,0.8); background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
  482. .alert-cam-ai { font-size:25px; color:#00e676; background:rgba(0,30,15,0.75); padding:5px 18px; border-radius:5px; border:1px solid rgba(0,230,118,0.3); }
  483. .alert-det-box { position:absolute; border:5px solid #ef4444; background:rgba(239,68,68,0.1); border-radius:5px; animation:detBox 1.5s ease-in-out infinite; }
  484. .alert-det-label { position:absolute; bottom:100%; left:0; white-space:nowrap; font-size:25px; background:#ef4444; color:#fff; padding:5px 12px; border-radius:5px; margin-bottom:5px; }
  485. /* RIGHT: info */
  486. .alert-info-col { flex:1; display:flex; flex-direction:column; gap:35px; }
  487. .alert-info-grid { display:grid; grid-template-columns:1fr 1fr; gap:25px; }
  488. .alert-info-item { padding:25px 35px; border-radius:10px; background:rgba(239,68,68,0.07); border:1px solid rgba(239,68,68,0.2); }
  489. .alert-info-item .ail { font-size:28px; color:rgba(255,150,150,0.65); margin-bottom:10px; }
  490. .alert-info-item .aiv { font-size:40px; font-weight:700; color:#ef4444; }
  491. .alert-pulse-text { text-align:center; font-size:32px; letter-spacing:5px; color:rgba(239,100,100,0.75); animation:blinkRed 1s ease-in-out infinite; font-weight:600; padding:20px; border:1px solid rgba(239,68,68,0.2); border-radius:10px; background:rgba(239,68,68,0.05); }
  492. .alert-modal-footer { padding:30px 60px; display:flex; gap:30px; justify-content:flex-end; border-top:1px solid rgba(239,68,68,0.15); }
  493. .btn-alert-confirm { padding:22px 70px; border-radius:10px; cursor:pointer; border:2px solid #ef4444; background:rgba(239,68,68,0.15); color:#ef4444; font-size:32px; font-weight:600; transition:all 0.2s; }
  494. .btn-alert-confirm:hover { background:rgba(239,68,68,0.3); }
  495. .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; }
  496. /* ========== EVACUATION MODAL ========== */
  497. #evac-modal {
  498. position:fixed; inset:0; z-index:10000;
  499. display:none; align-items:center; justify-content:center;
  500. background:rgba(0,2,18,0.94); backdrop-filter:blur(8px);
  501. }
  502. #evac-modal.show { display:flex; animation:alertFadeIn 0.4s ease; }
  503. .evac-inner {
  504. position:relative; width:2820px; border-radius:20px; overflow:hidden;
  505. background:linear-gradient(135deg,rgba(2,8,28,0.99),rgba(1,5,20,0.99));
  506. border:2px solid rgba(30,144,255,0.45);
  507. box-shadow:0 0 200px rgba(30,144,255,0.22), inset 0 0 120px rgba(0,50,140,0.05);
  508. display:flex; flex-direction:column; max-height:96vh;
  509. }
  510. .evac-hdr {
  511. padding:36px 55px; display:flex; align-items:center; gap:28px; flex-shrink:0;
  512. border-bottom:1px solid rgba(30,144,255,0.2);
  513. background:linear-gradient(90deg,rgba(0,55,160,0.12),transparent);
  514. }
  515. .evac-hdr-icon { font-size:68px; }
  516. .evac-hdr-title { font-size:50px; font-weight:700; color:var(--white); letter-spacing:6px; }
  517. .evac-hdr-close {
  518. margin-left:auto; width:68px; height:68px; border-radius:10px;
  519. border:1px solid rgba(30,144,255,0.35); background:rgba(30,144,255,0.08);
  520. color:var(--cyan); font-size:34px; cursor:pointer;
  521. display:flex; align-items:center; justify-content:center; transition:background 0.2s;
  522. }
  523. .evac-hdr-close:hover { background:rgba(30,144,255,0.22); }
  524. .evac-body { display:flex; flex:1; min-height:0; overflow:hidden; }
  525. .evac-map-col { flex:1; min-width:0; padding:26px 28px; display:flex; flex-direction:column; gap:14px; }
  526. .evac-legend { display:flex; align-items:center; gap:12px; justify-content:flex-end; }
  527. .evac-legend-rect { width:36px; height:14px; background:#1e90ff; border-radius:3px; }
  528. .evac-legend-text { font-size:26px; color:var(--text-dim); }
  529. .evac-map-wrap {
  530. flex:1; min-height:0; position:relative;
  531. border-radius:10px; background:rgba(1,8,28,0.65);
  532. border:1px solid rgba(30,144,255,0.15);
  533. }
  534. .evac-map-wrap svg { width:100%; height:100%; display:block; }
  535. .evac-popup {
  536. position:absolute; left:3%; top:53%;
  537. width:390px; border-radius:10px; padding:18px 24px 20px;
  538. background:rgba(45,4,6,0.97); border:2px solid rgba(239,68,68,0.6);
  539. box-shadow:0 0 50px rgba(239,68,68,0.28); z-index:5; pointer-events:none;
  540. }
  541. .evac-popup-hdr { display:flex; align-items:center; gap:14px; margin-bottom:12px; }
  542. .evac-popup-alarm { font-size:30px; }
  543. .evac-popup-time { font-size:22px; color:rgba(255,255,255,0.65); flex:1; }
  544. .evac-popup-x { color:rgba(255,255,255,0.45); font-size:22px; }
  545. .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; }
  546. .evac-popup-msg { font-size:28px; font-weight:600; color:#fff; letter-spacing:1px; }
  547. .evac-right-col { flex:0 0 540px; border-left:1px solid rgba(30,144,255,0.15); display:flex; flex-direction:column; }
  548. .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; }
  549. .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; }
  550. .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; }
  551. .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; }
  552. .evac-metrics { flex:0 0 490px; display:flex; flex-direction:column; gap:12px; justify-content:center; }
  553. .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; }
  554. .evac-metric-col .evac-ml { font-size:22px; color:var(--text-dim); margin-bottom:4px; }
  555. .evac-metric-col .evac-mv { font-size:28px; font-weight:600; color:#ef4444; }
  556. .evac-metric-rt { font-size:26px; color:rgba(255,255,255,0.75); }
  557. .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); }
  558. .evac-bc-hdr { display:flex; align-items:center; gap:14px; }
  559. .evac-bc-icon { font-size:28px; }
  560. .evac-bc-title { font-size:30px; font-weight:600; color:var(--white); flex:1; }
  561. .evac-bc-device { font-size:24px; color:var(--text-dim); }
  562. .evac-speaker-row { display:flex; gap:14px; }
  563. .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; }
  564. .evac-speaker-btn:hover, .evac-speaker-btn.on { background:rgba(30,144,255,0.25); border-color:var(--cyan); }
  565. .evac-input-row { display:flex; gap:14px; }
  566. .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; }
  567. .evac-text-input::placeholder { color:rgba(255,255,255,0.2); }
  568. .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; }
  569. .evac-send-btn:hover { background:rgba(30,144,255,0.3); }
  570. .evac-actions { flex:0 0 380px; display:flex; flex-direction:column; gap:14px; justify-content:center; }
  571. .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; }
  572. .evac-btn-later:hover { background:rgba(255,255,255,0.1); }
  573. .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; }
  574. .evac-btn-exec:hover { background:rgba(239,68,68,0.32); }
  575. .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; }
  576. .btn-alert-evac:hover { background:rgba(245,158,11,0.3); }
  577. /* ========== SCROLLBAR ========== */
  578. ::-webkit-scrollbar { width:8px; height:8px; }
  579. ::-webkit-scrollbar-track { background:transparent; }
  580. ::-webkit-scrollbar-thumb { background:var(--border); border-radius:5px; }
  581. /* ========== CHART HEIGHTS ========== */
  582. #chart-donut { width:100%; height:100%; }
  583. #chart-stack { width:100%; height:100%; }
  584. #chart-line { width:100%; height:100%; }
  585. #chart-gauge { width:100%; height:100%; }
  586. #chart-ring { width:100%; height:100%; }
  587. #chart-pie-status { width:100%; flex:1; min-height:0; }
  588. #chart-pie-usage { width:100%; flex:1; min-height:0; }
  589. /* ========== MONITOR TABS (3区) ========== */
  590. .monitor-tab-bar { display:flex; gap:10px; }
  591. .monitor-tab {
  592. padding:12px 44px; border-radius:8px; cursor:pointer; outline:none;
  593. background:var(--bg-card); border:1px solid var(--border);
  594. color:var(--text-dim); font-size:28px; font-weight:600; letter-spacing:2px;
  595. transition:all 0.25s; font-family:var(--font-main);
  596. }
  597. .monitor-tab.active {
  598. border-color:var(--cyan); color:var(--cyan);
  599. background:rgba(0,216,255,0.1); text-shadow:0 0 10px rgba(0,216,255,0.45);
  600. box-shadow:0 0 14px rgba(0,216,255,0.18);
  601. }
  602. .monitor-tab:hover:not(.active) { border-color:rgba(30,144,255,0.6); color:var(--text); }
  603. .monitor-tab-content { display:none; flex:1; flex-direction:column; min-height:0; }
  604. .monitor-tab-content.active { display:flex; }
  605. /* 九宫格全屏摄像头网格 */
  606. #camera-grid-9 {
  607. display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr);
  608. gap:15px; flex:1; min-height:0; padding:15px;
  609. }
  610. </style>
  611. </head>
  612. <body>
  613. <canvas id="nebula-canvas"></canvas>
  614. <!-- ===== ALERT MODAL ===== -->
  615. <div id="alert-modal">
  616. <div class="alert-modal-inner">
  617. <div class="alert-scan-line"></div>
  618. <div class="alert-modal-header">
  619. <span class="alert-icon">🚨</span>
  620. <div>
  621. <div class="alert-title">⚡ 系统预警 · ALERT</div>
  622. <div class="alert-subtitle">LABORATORY SAFETY MONITORING SYSTEM · EMERGENCY</div>
  623. </div>
  624. </div>
  625. <div class="alert-modal-body">
  626. <!-- 左侧: 实验室实时监控画面 360×280 -->
  627. <div class="alert-cam-wrap">
  628. <div class="alert-cam-label">告警实验室实时监控画面</div>
  629. <div class="alert-cam-box">
  630. <canvas id="alert-cam-canvas" width="900" height="700"></canvas>
  631. <div class="alert-cam-overlay"></div>
  632. <div class="alert-cam-id">CAM · A301-02</div>
  633. <div class="alert-cam-rec">REC</div>
  634. <div class="alert-cam-bottom">
  635. <span class="alert-cam-name">化学分析实验室 A301</span>
  636. <span class="alert-cam-ai">🤖 AI检测</span>
  637. </div>
  638. <!-- AI危险行为框 -->
  639. <div class="alert-det-box" style="left:22%;top:15%;width:28%;height:42%">
  640. <div class="alert-det-label">危险行为: 未佩戴防护</div>
  641. </div>
  642. </div>
  643. </div>
  644. <!-- 右侧: 告警信息 -->
  645. <div class="alert-info-col">
  646. <div class="alert-info-grid">
  647. <div class="alert-info-item"><div class="ail">告警实验室</div><div class="aiv" id="alert-lab">化学分析实验室 (A301)</div></div>
  648. <div class="alert-info-item"><div class="ail">所属单位</div><div class="aiv" id="alert-unit">化学研究所</div></div>
  649. <div class="alert-info-item"><div class="ail">告警指标</div><div class="aiv" id="alert-metric">TVOC 浓度超标</div></div>
  650. <div class="alert-info-item"><div class="ail">当前值 / 安全阈值</div><div class="aiv" id="alert-value">2.85 / 0.6 mg/m³</div></div>
  651. </div>
  652. <div class="alert-pulse-text">▶▶ 请立即采取安全措施,疏散实验人员 ◀◀</div>
  653. </div>
  654. </div>
  655. <div class="alert-modal-footer">
  656. <button class="btn-alert-ignore" onclick="closeAlert()">稍后处理</button>
  657. <button class="btn-alert-evac" onclick="openEvac()">🚪 应急疏散</button>
  658. <button class="btn-alert-confirm" onclick="closeAlert()">确认处理</button>
  659. </div>
  660. </div>
  661. </div>
  662. <!-- ===== EVACUATION MODAL ===== -->
  663. <div id="evac-modal">
  664. <div class="evac-inner">
  665. <!-- Header -->
  666. <div class="evac-hdr">
  667. <span class="evac-hdr-icon">🚪</span>
  668. <span class="evac-hdr-title">应急疏散</span>
  669. <button class="evac-hdr-close" onclick="closeEvac()">✕</button>
  670. </div>
  671. <!-- Body -->
  672. <div class="evac-body">
  673. <!-- Left: floor plan -->
  674. <div class="evac-map-col">
  675. <div class="evac-legend">
  676. <div class="evac-legend-rect"></div>
  677. <span class="evac-legend-text">应急疏散路线图</span>
  678. </div>
  679. <div class="evac-map-wrap">
  680. <svg viewBox="0 0 1060 590" xmlns="http://www.w3.org/2000/svg">
  681. <defs>
  682. <marker id="ev-arrow" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto">
  683. <polygon points="0 0, 9 3.5, 0 7" fill="#1e90ff"/>
  684. </marker>
  685. </defs>
  686. <!-- Top row background -->
  687. <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"/>
  688. <!-- Emergency exit zone top-right -->
  689. <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"/>
  690. <text x="910" y="118" text-anchor="middle" fill="rgba(30,144,255,0.6)" font-size="22">紧急</text>
  691. <text x="910" y="146" text-anchor="middle" fill="rgba(30,144,255,0.6)" font-size="22">出口</text>
  692. <line x1="870" y1="132" x2="918" y2="132" stroke="#1e90ff" stroke-width="3" marker-end="url(#ev-arrow)" opacity="0.75"/>
  693. <!-- A101 -->
  694. <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"/>
  695. <line x1="45" y1="40" x2="235" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  696. <line x1="235" y1="40" x2="45" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  697. <text x="140" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A101</text>
  698. <!-- A102 -->
  699. <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"/>
  700. <line x1="235" y1="40" x2="425" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  701. <line x1="425" y1="40" x2="235" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  702. <text x="330" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A102</text>
  703. <!-- A103 -->
  704. <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"/>
  705. <line x1="425" y1="40" x2="615" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  706. <line x1="615" y1="40" x2="425" y2="225" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  707. <text x="520" y="140" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A103</text>
  708. <!-- A104 (blue — near top-right exit) -->
  709. <rect x="615" y="40" width="193" height="185" fill="rgba(30,144,255,0.1)" stroke="#1e90ff" stroke-width="3"/>
  710. <line x1="615" y1="40" x2="808" y2="225" stroke="rgba(30,144,255,0.18)" stroke-width="1"/>
  711. <line x1="808" y1="40" x2="615" y2="225" stroke="rgba(30,144,255,0.18)" stroke-width="1"/>
  712. <text x="711" y="140" text-anchor="middle" fill="#7ab8e8" font-size="22">A104</text>
  713. <!-- Corridor -->
  714. <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"/>
  715. <text x="350" y="277" text-anchor="middle" fill="rgba(168,204,232,0.38)" font-size="24" letter-spacing="6">走道</text>
  716. <!-- Assembly points -->
  717. <circle cx="645" cy="268" r="22" fill="none" stroke="#1e90ff" stroke-width="2.5" opacity="0.75"/>
  718. <circle cx="755" cy="268" r="22" fill="none" stroke="#1e90ff" stroke-width="2.5" opacity="0.75"/>
  719. <!-- Bottom section background -->
  720. <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"/>
  721. <!-- Emergency exit zone bottom-left label -->
  722. <text x="22" y="445" text-anchor="middle" fill="rgba(30,144,255,0.58)" font-size="19" transform="rotate(-90,22,445)">紧急出口</text>
  723. <line x1="47" y1="432" x2="16" y2="432" stroke="#1e90ff" stroke-width="3" marker-end="url(#ev-arrow)" opacity="0.7"/>
  724. <!-- A105 (red — alert room) -->
  725. <rect x="45" y="310" width="185" height="242" fill="rgba(239,68,68,0.1)" stroke="#ef4444" stroke-width="3"/>
  726. <line x1="45" y1="310" x2="230" y2="552" stroke="rgba(239,68,68,0.22)" stroke-width="1.5"/>
  727. <line x1="230" y1="310" x2="45" y2="552" stroke="rgba(239,68,68,0.22)" stroke-width="1.5"/>
  728. <text x="137" y="440" text-anchor="middle" fill="#ef4444" font-size="22" font-weight="600">A105</text>
  729. <!-- A106 -->
  730. <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"/>
  731. <line x1="230" y1="310" x2="415" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  732. <line x1="415" y1="310" x2="230" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  733. <text x="322" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A106</text>
  734. <!-- A107 -->
  735. <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"/>
  736. <line x1="415" y1="310" x2="600" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  737. <line x1="600" y1="310" x2="415" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  738. <text x="507" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A107</text>
  739. <!-- A108 -->
  740. <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"/>
  741. <line x1="600" y1="310" x2="785" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  742. <line x1="785" y1="310" x2="600" y2="552" stroke="rgba(30,144,255,0.12)" stroke-width="1"/>
  743. <text x="692" y="440" text-anchor="middle" fill="rgba(168,204,232,0.65)" font-size="22">A108</text>
  744. <!-- A109 (blue — evacuation destination) -->
  745. <rect x="785" y="310" width="228" height="242" fill="rgba(30,144,255,0.1)" stroke="#1e90ff" stroke-width="3"/>
  746. <line x1="785" y1="310" x2="1013" y2="552" stroke="rgba(30,144,255,0.18)" stroke-width="1.5"/>
  747. <line x1="1013" y1="310" x2="785" y2="552" stroke="rgba(30,144,255,0.18)" stroke-width="1.5"/>
  748. <text x="899" y="440" text-anchor="middle" fill="#7ab8e8" font-size="22" font-weight="600">A109</text>
  749. <!-- Evacuation route: bottom-left through corridor -->
  750. <line x1="230" y1="432" x2="50" y2="432" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7" marker-end="url(#ev-arrow)"/>
  751. <!-- Evacuation route: top-right toward exit -->
  752. <line x1="808" y1="132" x2="862" y2="132" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7" marker-end="url(#ev-arrow)"/>
  753. <!-- Connector: corridor down to A104 corridor side -->
  754. <line x1="711" y1="225" x2="711" y2="310" stroke="#1e90ff" stroke-width="3" stroke-dasharray="14,7"/>
  755. </svg>
  756. <!-- Floating alert popup over A105 -->
  757. <div class="evac-popup">
  758. <div class="evac-popup-hdr">
  759. <span class="evac-popup-alarm">🚨</span>
  760. <span class="evac-popup-time">2026-03-23 14:32:18</span>
  761. <span class="evac-popup-x">✕</span>
  762. </div>
  763. <div class="evac-popup-badge">触发风险</div>
  764. <div class="evac-popup-msg">发生风险:TVOC浓度超标</div>
  765. </div>
  766. </div>
  767. </div>
  768. <!-- Right: camera feeds -->
  769. <div class="evac-right-col">
  770. <div class="evac-right-hdr">房间号-实验室名称</div>
  771. <div class="evac-cam-slot">实时视频监控</div>
  772. <div class="evac-cam-slot">实时视频监控</div>
  773. <div class="evac-floor-lbl">楼道 2层</div>
  774. <div class="evac-cam-slot">实时视频监控</div>
  775. </div>
  776. </div>
  777. <!-- Footer -->
  778. <div class="evac-footer">
  779. <!-- Alert metrics -->
  780. <div class="evac-metrics">
  781. <div class="evac-metric-row">
  782. <div class="evac-metric-col">
  783. <div class="evac-ml">告警指标</div>
  784. <div class="evac-mv">TVOC 浓度超标</div>
  785. </div>
  786. <div class="evac-metric-rt">2.85 / 0.6 mg/m³</div>
  787. </div>
  788. <div class="evac-metric-row">
  789. <div class="evac-metric-col">
  790. <div class="evac-ml">当前值 / 安全阈值</div>
  791. <div class="evac-mv">TVOC 浓度超标</div>
  792. </div>
  793. <div class="evac-metric-rt">2.85 / 0.6 mg/m³</div>
  794. </div>
  795. </div>
  796. <!-- Voice broadcast -->
  797. <div class="evac-broadcast">
  798. <div class="evac-bc-hdr">
  799. <span class="evac-bc-icon">📢</span>
  800. <span class="evac-bc-title">语音广播</span>
  801. <span class="evac-bc-device">选择播放设备</span>
  802. </div>
  803. <div class="evac-speaker-row">
  804. <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
  805. <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
  806. <button class="evac-speaker-btn on">NKL1FB1122 喇叭</button>
  807. </div>
  808. <div class="evac-input-row">
  809. <input type="text" class="evac-text-input" placeholder="请输入喊话内容"/>
  810. <button class="evac-send-btn">发送</button>
  811. </div>
  812. </div>
  813. <!-- Action buttons -->
  814. <div class="evac-actions">
  815. <button class="evac-btn-later" onclick="closeEvac()">稍后处理</button>
  816. <button class="evac-btn-exec">执行疏散</button>
  817. </div>
  818. </div>
  819. </div>
  820. </div>
  821. <!-- ===== MAIN WRAPPER ===== -->
  822. <div id="scale-root">
  823. <div class="bg-hex"></div>
  824. <div class="scan-effect"></div>
  825. <!-- TOP NAV -->
  826. <nav class="top-nav">
  827. <!-- 左侧:实时时钟 + 星期 -->
  828. <div class="nav-left" style="justify-content:flex-start">
  829. <div class="nav-clock">
  830. <div class="time" id="clock-time">--:--:--</div>
  831. <div class="date" id="clock-date">----年--月--日 星期-</div>
  832. </div>
  833. </div>
  834. <!-- 中部:LOGO + 单位名称 + 大标题 -->
  835. <div class="nav-center">
  836. <div class="nav-logo"><div class="nav-logo-ring"></div></div>
  837. <div class="nav-org" style="margin-right:50px">
  838. <strong>中国安全生产科学研究院</strong>
  839. <span>National Institute for Occupational Safety</span>
  840. </div>
  841. <div style="width:2px;height:80px;background:linear-gradient(180deg,transparent,rgba(30,144,255,0.6),transparent);flex-shrink:0;margin-right:50px"></div>
  842. <div style="display:flex;flex-direction:column;align-items:center;position:relative">
  843. <div class="nav-title">实验室安全智能监测与管控中心</div>
  844. <div class="nav-title-deco"></div>
  845. </div>
  846. </div>
  847. <!-- 右侧:天气 -->
  848. <div class="nav-right" style="justify-content:flex-end">
  849. <div class="nav-weather">
  850. <span class="icon">⛅</span>
  851. <div>
  852. <div>北京 · 晴转多云</div>
  853. <div style="font-size:28px;color:var(--gold)">12°C / AQI 68</div>
  854. </div>
  855. </div>
  856. </div>
  857. </nav>
  858. <!-- MAIN CONTENT: 4 COLS L→R: 1区(2) 2区(2) 3区(4) 4区(2) -->
  859. <div class="main-content">
  860. <!-- ======== 1区 ======== -->
  861. <div class="panel-col">
  862. <!-- 实验室基本情况统计 -->
  863. <div class="panel" style="flex:0 0 auto;display:flex;flex-direction:column">
  864. <div class="border-beam"></div>
  865. <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>
  866. <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>
  867. <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>
  868. <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>
  869. <div class="panel-header">
  870. <div class="panel-header-icon">🏛️</div>
  871. <span class="panel-title">实验室基本情况统计</span>
  872. <div class="status-dot green" style="margin-left:auto"></div>
  873. </div>
  874. <!-- 上部 80%:左(3) 总数 + 右(7) 分级 -->
  875. <div style="flex:4;display:flex;min-height:680px;overflow:hidden">
  876. <!-- 左侧 4:实验室总数统计 -->
  877. <div style="flex:4;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:25px 20px;border-right:1px solid var(--border);background:linear-gradient(180deg,rgba(30,144,255,0.04),transparent)">
  878. <!-- SVG 圆弧仪表 -->
  879. <svg width="320" height="320" viewBox="0 0 320 320">
  880. <defs>
  881. <linearGradient id="arcG1" x1="0%" y1="0%" x2="100%" y2="100%">
  882. <stop offset="0%" stop-color="#1e90ff"/>
  883. <stop offset="100%" stop-color="#00d8ff"/>
  884. </linearGradient>
  885. <filter id="glow1">
  886. <feGaussianBlur stdDeviation="4" result="blur"/>
  887. <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
  888. </filter>
  889. </defs>
  890. <!-- 最外装饰环 -->
  891. <circle cx="160" cy="160" r="148" fill="none" stroke="rgba(30,144,255,0.08)" stroke-width="1" stroke-dasharray="6 6"/>
  892. <!-- 外轨道 -->
  893. <circle cx="160" cy="160" r="136" fill="none" stroke="rgba(30,144,255,0.14)" stroke-width="3"/>
  894. <!-- 主弧 300° -->
  895. <circle cx="160" cy="160" r="136" fill="none" stroke="url(#arcG1)" stroke-width="8"
  896. stroke-dasharray="711 143" stroke-linecap="round" transform="rotate(-240 160 160)" filter="url(#glow1)"/>
  897. <!-- 内轨道 -->
  898. <circle cx="160" cy="160" r="110" fill="rgba(3,14,31,0.7)" stroke="rgba(0,216,255,0.15)" stroke-width="2"/>
  899. <!-- 四分级色弧 小环 -->
  900. <circle cx="160" cy="160" r="88" fill="none" stroke="#cc0000" stroke-width="12" stroke-dasharray="42 485" stroke-linecap="round" transform="rotate(-90 160 160)" opacity="0.9"/>
  901. <circle cx="160" cy="160" r="88" fill="none" stroke="#ff8000" stroke-width="12" stroke-dasharray="99 428" stroke-linecap="round" transform="rotate(-57 160 160)" opacity="0.9"/>
  902. <circle cx="160" cy="160" r="88" fill="none" stroke="#ffcc00" stroke-width="12" stroke-dasharray="160 367" stroke-linecap="round" transform="rotate(22 160 160)" opacity="0.9"/>
  903. <circle cx="160" cy="160" r="88" fill="none" stroke="#0066cc" stroke-width="12" stroke-dasharray="153 374" stroke-linecap="round" transform="rotate(145 160 160)" opacity="0.9"/>
  904. <!-- 端点光晕 -->
  905. <circle cx="160" cy="24" r="6" fill="#1e90ff" opacity="0.9"/>
  906. <!-- 中心数字 -->
  907. <text x="160" y="145" text-anchor="middle" fill="#ffd740" font-size="72" font-weight="900" font-family="Arial,sans-serif" letter-spacing="-2">128</text>
  908. <text x="160" y="190" text-anchor="middle" fill="rgba(168,204,232,0.75)" font-size="26" font-family="Arial,sans-serif" letter-spacing="4">间</text>
  909. </svg>
  910. <!-- 标签 -->
  911. <div style="text-align:center">
  912. <div style="font-size:30px;font-weight:700;color:var(--cyan);letter-spacing:4px">实验室总数</div>
  913. <div style="font-size:22px;color:var(--text-dim);margin-top:6px;letter-spacing:2px">TOTAL LABORATORIES</div>
  914. </div>
  915. <!-- 分级色条 -->
  916. <div style="width:100%;padding:0 10px">
  917. <div style="display:flex;gap:5px;height:12px;border-radius:6px;overflow:hidden">
  918. <div style="flex:12;background:#cc0000;box-shadow:0 0 10px rgba(204,0,0,0.5)"></div>
  919. <div style="flex:28;background:#ff8000;box-shadow:0 0 10px rgba(255,128,0,0.4)"></div>
  920. <div style="flex:45;background:#ffcc00;box-shadow:0 0 10px rgba(255,204,0,0.4)"></div>
  921. <div style="flex:43;background:#0066cc;box-shadow:0 0 10px rgba(0,102,204,0.4)"></div>
  922. </div>
  923. <div style="display:flex;gap:5px;margin-top:8px;font-size:22px;color:var(--text-dim);text-align:center">
  924. <span style="flex:12">Ⅰ</span><span style="flex:28">Ⅱ</span><span style="flex:45">Ⅲ</span><span style="flex:43">Ⅳ</span>
  925. </div>
  926. </div>
  927. </div>
  928. <!-- 右侧 6:分级统计(环形图 + 明细列表) -->
  929. <div style="flex:6;display:flex;flex-direction:column;justify-content:center;padding:25px 25px 20px;gap:0">
  930. <!-- 环形图 -->
  931. <div style="flex:0 0 340px;width:100%" id="chart-donut"></div>
  932. <!-- 分级明细 -->
  933. <div style="display:flex;flex-direction:column;gap:10px;margin-top:10px">
  934. <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(204,0,0,0.07);border-left:4px solid #cc0000">
  935. <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#cc0000;display:inline-block;box-shadow:0 0 8px rgba(204,0,0,0.8)"></span>I 级(危险)</span>
  936. <span style="color:#ff6666;font-weight:700;font-size:28px">12间</span>
  937. </div>
  938. <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(255,128,0,0.07);border-left:4px solid #ff8000">
  939. <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#ff8000;display:inline-block;box-shadow:0 0 8px rgba(255,128,0,0.8)"></span>II 级(较危险)</span>
  940. <span style="color:#ffaa44;font-weight:700;font-size:28px">28间</span>
  941. </div>
  942. <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(255,204,0,0.07);border-left:4px solid #ffcc00">
  943. <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#ffcc00;display:inline-block;box-shadow:0 0 8px rgba(255,204,0,0.8)"></span>III 级(一般)</span>
  944. <span style="color:#ffe066;font-weight:700;font-size:28px">45间</span>
  945. </div>
  946. <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(0,102,204,0.07);border-left:4px solid #0066cc">
  947. <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#0066cc;display:inline-block;box-shadow:0 0 8px rgba(0,102,204,0.8)"></span>IV 级(较安全)</span>
  948. <span style="color:#4499ff;font-weight:700;font-size:28px">43间</span>
  949. </div>
  950. </div>
  951. </div>
  952. </div>
  953. <!-- 下部 20%:三种状态统计 -->
  954. <div style="flex:1;display:flex;align-items:center;padding:0 25px 20px;border-top:1px solid var(--border);gap:15px;min-height:170px">
  955. <div class="status-row" style="width:100%;margin:0">
  956. <div class="status-badge active"><div class="val">20</div><div class="lbl">使用(间)</div></div>
  957. <div class="status-badge warning"><div class="val">3</div><div class="lbl">异常(间)</div></div>
  958. <div class="status-badge idle"><div class="val">105</div><div class="lbl">空闲(间)</div></div>
  959. </div>
  960. </div>
  961. </div>
  962. <!-- 实验室安全分级统计 -->
  963. <div class="panel" style="flex:1;min-height:450px;display:flex;flex-direction:column">
  964. <div class="border-beam"></div>
  965. <div class="panel-header">
  966. <div class="panel-header-icon">📊</div>
  967. <span class="panel-title">实验室安全分级统计</span>
  968. </div>
  969. <div style="flex:1;min-height:0;padding:15px 20px">
  970. <div id="chart-stack" style="height:100%;width:100%"></div>
  971. </div>
  972. </div>
  973. <!-- 实验室进入人数统计及走势 -->
  974. <div class="panel" style="flex:0 0 auto">
  975. <div class="border-beam"></div>
  976. <div class="panel-header">
  977. <div class="panel-header-icon">👥</div>
  978. <span class="panel-title">实验室进入人数统计及走势</span>
  979. </div>
  980. <div style="padding:20px 25px">
  981. <div class="flip-counters">
  982. <div class="flip-counter"><div class="fc-label">今日进入总人数</div><div class="fc-digits" id="flip-total"></div></div>
  983. <div class="flip-counter"><div class="fc-label">当前在场实验人数</div><div class="fc-digits" id="flip-current"></div></div>
  984. </div>
  985. <div style="height:275px;margin-top:12px" id="chart-line"></div>
  986. </div>
  987. </div>
  988. </div><!-- /1区 -->
  989. <!-- ======== 2区 ======== -->
  990. <div class="panel-col">
  991. <!-- 智能环境感知应用设备统计 -->
  992. <div class="panel" style="flex:0 0 auto">
  993. <div class="border-beam"></div>
  994. <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>
  995. <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>
  996. <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>
  997. <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>
  998. <div class="panel-header">
  999. <div class="panel-header-icon">📡</div>
  1000. <span class="panel-title">智能环境感知应用设备统计</span>
  1001. </div>
  1002. <div style="padding:20px 25px">
  1003. <div class="device-online-row">
  1004. <div class="device-stat-chip online"><div class="dv">312</div><div class="dl">在线设备</div></div>
  1005. <div class="device-stat-chip offline"><div class="dv">18</div><div class="dl">离线设备</div></div>
  1006. </div>
  1007. <div style="display:flex;gap:20px;align-items:center">
  1008. <div id="chart-gauge" style="flex:0 0 295px;height:295px"></div>
  1009. <div style="flex:1;display:flex;flex-direction:column;gap:12px">
  1010. <div style="display:flex;gap:12px">
  1011. <div class="device-list-item" style="flex:1"><div class="dli-icon">🏷️</div><div class="dli-num">86</div><div class="dli-name">电子信息铭牌</div></div>
  1012. <div class="device-list-item" style="flex:1"><div class="dli-icon">⚗️</div><div class="dli-num">44</div><div class="dli-name">化学品智能终端</div></div>
  1013. <div class="device-list-item" style="flex:1"><div class="dli-icon">🌡️</div><div class="dli-num">128</div><div class="dli-name">传感器</div></div>
  1014. </div>
  1015. <div style="display:flex;gap:12px;justify-content:center">
  1016. <div class="device-list-item" style="flex:0 0 calc(33.33% - 6px)"><div class="dli-icon">📷</div><div class="dli-num">72</div><div class="dli-name">智慧摄像头</div></div>
  1017. <div class="device-list-item" style="flex:0 0 calc(33.33% - 6px)"><div class="dli-icon">📡</div><div class="dli-num">35</div><div class="dli-name">其他智能设备</div></div>
  1018. </div>
  1019. </div>
  1020. </div>
  1021. </div>
  1022. </div>
  1023. <!-- 实验室设备分类及使用统计 (4:6) -->
  1024. <div class="panel" style="flex:1;min-height:650px;display:flex;flex-direction:column">
  1025. <div class="border-beam"></div>
  1026. <div class="panel-header">
  1027. <div class="panel-header-icon">🔬</div>
  1028. <span class="panel-title">实验室设备分类及使用统计</span>
  1029. </div>
  1030. <!-- 4:6 layout -->
  1031. <div style="flex:1;display:flex;flex-direction:column;padding:15px 25px;gap:12px;min-height:0">
  1032. <!-- 上: 占4 环形图 - 设备分类(含图例:名称/数量/占比) -->
  1033. <div style="flex:4;min-height:0">
  1034. <div id="chart-ring" style="height:100%;width:100%"></div>
  1035. </div>
  1036. <!-- 下: 占6 统计 + 双饼图 -->
  1037. <div style="flex:6;min-height:0;display:flex;flex-direction:column;gap:10px">
  1038. <!-- 上方 3网格统计 -->
  1039. <div class="equip-mid-row" style="flex:0 0 auto">
  1040. <div class="equip-stat-item"><div class="ev">2,458</div><div class="el">设备总数(台)</div></div>
  1041. <div class="equip-stat-item"><div class="ev">18,620</div><div class="el">使用时长(h)</div></div>
  1042. <div class="equip-stat-item"><div class="ev">62.4%</div><div class="el">设备使用率</div></div>
  1043. </div>
  1044. <!-- 下方 左右双饼图 -->
  1045. <div style="flex:1;min-height:0;display:flex;gap:12px">
  1046. <div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:6px">
  1047. <div style="text-align:center;margin-top:100px;padding:5px 0 7px;font-size:22px;font-weight:600;letter-spacing:3px;color:var(--cyan);border-bottom:1px solid rgba(0,216,255,0.22);text-shadow:0 0 10px rgba(0,216,255,0.45)">设备状态统计</div>
  1048. <div id="chart-pie-status" style="flex:1;min-height:0"></div>
  1049. </div>
  1050. <div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:6px">
  1051. <div style="text-align:center;margin-top:100px;padding:5px 0 7px;font-size:22px;font-weight:600;letter-spacing:3px;color:var(--cyan);border-bottom:1px solid rgba(0,216,255,0.22);text-shadow:0 0 10px rgba(0,216,255,0.45)">使用状态统计</div>
  1052. <div id="chart-pie-usage" style="flex:1;min-height:0"></div>
  1053. </div>
  1054. </div>
  1055. </div>
  1056. </div>
  1057. </div>
  1058. <!-- 危化品库存统计 -->
  1059. <div class="panel" style="flex:0 0 auto;min-height:460px">
  1060. <div class="border-beam"></div>
  1061. <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>
  1062. <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>
  1063. <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>
  1064. <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>
  1065. <div class="panel-header">
  1066. <div class="panel-header-icon">⚗️</div>
  1067. <span class="panel-title">化学品库存动态统计</span>
  1068. </div>
  1069. <div style="padding:12px 20px 15px;display:flex;gap:15px;height:calc(100% - 82px)">
  1070. <!-- 左侧: 饼图 -->
  1071. <div style="flex:1;min-width:0;display:flex;flex-direction:column">
  1072. <div id="chart-chem-inventory" style="flex:1;min-height:0"></div>
  1073. </div>
  1074. <!-- 右侧: 存量统计 -->
  1075. <div style="flex:0 0 380px;display:flex;flex-direction:column;gap:18px;justify-content:center">
  1076. <!-- 总量 -->
  1077. <div class="chem-stat-card total" style="padding:22px 20px">
  1078. <div class="csc-label">存量化学品总量</div>
  1079. <div style="display:flex;align-items:baseline;gap:20px;margin-top:4px">
  1080. <div class="csc-value" style="font-size:68px">1,229<span class="csc-unit">L</span></div>
  1081. <div style="font-size:42px;font-weight:700;color:var(--white)">10<span style="font-size:26px;font-weight:400;color:var(--text-dim);margin-left:4px">瓶</span></div>
  1082. </div>
  1083. </div>
  1084. <!-- 管控 -->
  1085. <div class="chem-stat-card ctrl" style="padding:20px 20px">
  1086. <div style="display:flex;justify-content:space-between;align-items:flex-start">
  1087. <div>
  1088. <div class="csc-label">管控类化学品</div>
  1089. <div style="display:flex;align-items:baseline;gap:16px;margin-top:4px">
  1090. <div class="csc-value" style="font-size:58px">269<span class="csc-unit">L</span></div>
  1091. <div style="font-size:38px;font-weight:700;color:#ef4444">5<span style="font-size:24px;font-weight:400;color:rgba(239,68,68,0.6);margin-left:4px">瓶</span></div>
  1092. </div>
  1093. </div>
  1094. <div class="csc-pct-ring" style="--pct:21.9;--clr:#ef4444">
  1095. <svg viewBox="0 0 60 60" width="88" height="88">
  1096. <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(239,68,68,0.12)" stroke-width="6"/>
  1097. <circle cx="30" cy="30" r="24" fill="none" stroke="#ef4444" stroke-width="6"
  1098. stroke-dasharray="27.5 123" stroke-linecap="round" transform="rotate(-90 30 30)"
  1099. style="filter:drop-shadow(0 0 4px rgba(239,68,68,0.7))"/>
  1100. <text x="30" y="35" text-anchor="middle" fill="#ef4444" font-size="11" font-weight="700" font-family="Arial">21.9%</text>
  1101. </svg>
  1102. </div>
  1103. </div>
  1104. <div class="csc-sub" style="font-size:24px">占总量 21.9%</div>
  1105. </div>
  1106. <!-- 非管控 -->
  1107. <div class="chem-stat-card free" style="padding:20px 20px">
  1108. <div style="display:flex;justify-content:space-between;align-items:flex-start">
  1109. <div>
  1110. <div class="csc-label">非管控类化学品</div>
  1111. <div style="display:flex;align-items:baseline;gap:16px;margin-top:4px">
  1112. <div class="csc-value" style="color:#00d8ff;font-size:58px">960<span class="csc-unit">L</span></div>
  1113. <div style="font-size:38px;font-weight:700;color:#00d8ff">5<span style="font-size:24px;font-weight:400;color:rgba(0,216,255,0.6);margin-left:4px">瓶</span></div>
  1114. </div>
  1115. </div>
  1116. <div>
  1117. <svg viewBox="0 0 60 60" width="88" height="88">
  1118. <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(0,216,255,0.12)" stroke-width="6"/>
  1119. <circle cx="30" cy="30" r="24" fill="none" stroke="#00d8ff" stroke-width="6"
  1120. stroke-dasharray="101 50.6" stroke-linecap="round" transform="rotate(-90 30 30)"
  1121. style="filter:drop-shadow(0 0 4px rgba(0,216,255,0.7))"/>
  1122. <text x="30" y="35" text-anchor="middle" fill="#00d8ff" font-size="11" font-weight="700" font-family="Arial">78.1%</text>
  1123. </svg>
  1124. </div>
  1125. </div>
  1126. <div class="csc-sub" style="color:rgba(0,216,255,0.55);font-size:24px">占总量 78.1%</div>
  1127. </div>
  1128. </div>
  1129. </div>
  1130. </div>
  1131. </div><!-- /2区 -->
  1132. <!-- ======== 3区: 实时监控 ======== -->
  1133. <div class="center-col">
  1134. <!-- 实时监控(含 九宫格/全部 两种视图) -->
  1135. <div id="view-monitor">
  1136. <!-- 标题栏 + 标签切换 -->
  1137. <div class="monitor-header">
  1138. <span style="font-size:32px;font-weight:600;color:var(--cyan);letter-spacing:2px">📹 实时监控</span>
  1139. <span style="font-size:28px;color:var(--text-dim);margin-left:10px">CCTV Live Feed</span>
  1140. <div class="monitor-tab-bar" style="margin-left:auto;margin-right:20px">
  1141. <button class="monitor-tab active" onclick="switchMonitorTab('9grid',this)">默认</button>
  1142. <button class="monitor-tab" onclick="switchMonitorTab('all',this)">全部</button>
  1143. </div>
  1144. <div style="display:flex;align-items:center;gap:15px">
  1145. <div class="status-dot green"></div>
  1146. <span style="font-size:28px;color:var(--text-dim)">信号正常</span>
  1147. </div>
  1148. </div>
  1149. <!-- 九宫格视图(默认) -->
  1150. <div id="tab-9grid" class="monitor-tab-content active">
  1151. <div id="camera-grid-9"></div>
  1152. </div>
  1153. <!-- 全部视图(原监控页面内容不变) -->
  1154. <div id="tab-all" class="monitor-tab-content">
  1155. <div class="monitor-inner">
  1156. <div class="monitor-left">
  1157. <div class="search-box"><span style="font-size:32px;color:var(--text-dim)">🔍</span><input type="text" placeholder="搜索楼栋 / 楼层 / 房间…"></div>
  1158. <select class="filter-select">
  1159. <option value="">全部二级单位</option>
  1160. <option>化学研究所</option><option>物理研究所</option>
  1161. <option>生物研究所</option><option>材料研究所</option><option>工程研究所</option>
  1162. </select>
  1163. <div class="tree-wrap" id="building-tree"></div>
  1164. </div>
  1165. <div class="monitor-right">
  1166. <div class="camera-grid-header">
  1167. <div class="camera-breadcrumb"><span>安科院主园区</span> › <span>综合实验楼A</span> › <span>3层</span></div>
  1168. <div class="camera-pager">
  1169. <button class="pager-btn">‹</button>
  1170. <span class="pager-info">1 / 3 页</span>
  1171. <button class="pager-btn">›</button>
  1172. </div>
  1173. </div>
  1174. <div class="camera-grid" id="camera-grid"></div>
  1175. </div>
  1176. </div>
  1177. </div>
  1178. </div>
  1179. </div><!-- /3区 -->
  1180. <!-- ======== 4区 ======== -->
  1181. <div class="panel-col">
  1182. <!-- 实验环境安全智能感知 -->
  1183. <div class="panel" style="flex:7;min-height:0;display:flex;flex-direction:column">
  1184. <div class="border-beam"></div>
  1185. <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>
  1186. <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>
  1187. <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>
  1188. <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>
  1189. <div class="panel-header">
  1190. <div class="panel-header-icon">🌡️</div>
  1191. <span class="panel-title">实验环境安全智能感知</span>
  1192. <div class="status-dot red" style="margin-left:auto"></div>
  1193. </div>
  1194. <div style="flex:1;min-height:0;padding:15px 20px;display:flex;flex-direction:column">
  1195. <div class="sensor-scroll-wrap">
  1196. <div class="sensor-scroll-inner" id="sensor-list"></div>
  1197. </div>
  1198. </div>
  1199. </div>
  1200. <!-- 实验室实时风险预警 -->
  1201. <div class="panel" style="flex:3;min-height:0;display:flex;flex-direction:column">
  1202. <div class="border-beam"></div>
  1203. <div class="panel-header">
  1204. <div class="panel-header-icon">⚠️</div>
  1205. <span class="panel-title">实验室实时风险预警</span>
  1206. <div style="margin-left:auto;display:flex;align-items:center;gap:18px;flex-shrink:0">
  1207. <div style="display:flex;align-items:center;gap:6px">
  1208. <span style="font-size:22px">🚨</span>
  1209. <span style="font-size:24px;color:var(--text-dim)">应急</span>
  1210. <span style="font-size:44px;font-weight:700;color:#f59e0b">28</span>
  1211. <span style="font-size:22px;color:var(--text-dim)">次</span>
  1212. </div>
  1213. <div style="width:1px;height:40px;background:rgba(30,144,255,0.3)"></div>
  1214. <div style="display:flex;align-items:center;gap:6px">
  1215. <span style="font-size:22px">☣️</span>
  1216. <span style="font-size:24px;color:var(--text-dim)">违规带离</span>
  1217. <span style="font-size:44px;font-weight:700;color:var(--cyan)">14</span>
  1218. <span style="font-size:22px;color:var(--text-dim)">次</span>
  1219. </div>
  1220. </div>
  1221. </div>
  1222. <div style="flex:1;min-height:0;padding:15px 20px;display:flex;flex-direction:column">
  1223. <div class="warn-scroll-wrap">
  1224. <div class="warn-scroll-inner" id="warn-list"></div>
  1225. </div>
  1226. </div>
  1227. </div>
  1228. </div><!-- /4区 -->
  1229. </div><!-- /main-content -->
  1230. </div><!-- /scale-root -->
  1231. <script>
  1232. // ====================================================
  1233. // SCALE
  1234. // ====================================================
  1235. // ====================================================
  1236. // INIT MONITOR ON LOAD
  1237. // ====================================================
  1238. document.addEventListener('DOMContentLoaded', () => {
  1239. if (document.documentElement.requestFullscreen)
  1240. document.documentElement.requestFullscreen().catch(() => {});
  1241. buildTree();
  1242. buildCameras();
  1243. });
  1244. const WD = ['日','一','二','三','四','五','六'];
  1245. function tick() {
  1246. const n = new Date();
  1247. const t = `${String(n.getHours()).padStart(2,'0')}:${String(n.getMinutes()).padStart(2,'0')}:${String(n.getSeconds()).padStart(2,'0')}`;
  1248. const d = `${n.getFullYear()}年${n.getMonth()+1}月${n.getDate()}日 星期${WD[n.getDay()]}`;
  1249. document.getElementById('clock-time').textContent = t;
  1250. document.getElementById('clock-date').textContent = d;
  1251. }
  1252. setInterval(tick, 1000); tick();
  1253. // ====================================================
  1254. // NEBULA BACKGROUND
  1255. // ====================================================
  1256. (function () {
  1257. const c = document.getElementById('nebula-canvas');
  1258. const ctx = c.getContext('2d');
  1259. let W, H, stars = [], t = 0;
  1260. function resize() { W = c.width = window.innerWidth; H = c.height = window.innerHeight; }
  1261. window.addEventListener('resize', resize); resize();
  1262. for (let i = 0; i < 500; i++)
  1263. stars.push({ x: Math.random()*9600, y: Math.random()*2800, r: Math.random()*1.5+0.3, a: Math.random()*0.8+0.2, va: Math.random()*0.02-0.01 });
  1264. function draw() {
  1265. t++;
  1266. ctx.clearRect(0, 0, W, H);
  1267. const g1 = ctx.createRadialGradient(W*0.22, H*0.35, 40, W*0.22, H*0.35, 420);
  1268. g1.addColorStop(0, 'rgba(0,60,180,0.09)'); g1.addColorStop(1, 'transparent');
  1269. ctx.fillStyle = g1; ctx.fillRect(0,0,W,H);
  1270. const g2 = ctx.createRadialGradient(W*0.78, H*0.65, 30, W*0.78, H*0.65, 360);
  1271. g2.addColorStop(0, 'rgba(0,100,220,0.08)'); g2.addColorStop(1, 'transparent');
  1272. ctx.fillStyle = g2; ctx.fillRect(0,0,W,H);
  1273. stars.forEach(s => {
  1274. s.a += s.va; if (s.a > 1 || s.a < 0.1) s.va *= -1;
  1275. const hue = 200 + Math.sin(t * 0.01 + s.x) * 30;
  1276. ctx.beginPath(); ctx.arc(s.x*(W/9600), s.y*(H/2800), s.r, 0, Math.PI*2);
  1277. ctx.fillStyle = `hsla(${hue},80%,80%,${s.a})`; ctx.fill();
  1278. });
  1279. requestAnimationFrame(draw);
  1280. }
  1281. draw();
  1282. })();
  1283. // ====================================================
  1284. // FLIP COUNTER
  1285. // ====================================================
  1286. function renderFlip(id, val) {
  1287. const el = document.getElementById(id); if (!el) return;
  1288. el.innerHTML = String(val).padStart(4,'0').split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
  1289. }
  1290. function animFlip(id, from, to, dur) {
  1291. const s = Date.now();
  1292. (function tick() {
  1293. const p = Math.min((Date.now()-s)/dur, 1);
  1294. renderFlip(id, Math.round(from + (to-from)*p));
  1295. if (p < 1) requestAnimationFrame(tick);
  1296. })();
  1297. }
  1298. renderFlip('flip-total', 1284); renderFlip('flip-current', 47);
  1299. setTimeout(() => animFlip('flip-total', 0, 1284, 2000), 600);
  1300. setTimeout(() => animFlip('flip-current', 0, 47, 1500), 900);
  1301. // ====================================================
  1302. // CHEMICAL INVENTORY (2区 化学品库存动态统计)
  1303. // ====================================================
  1304. const CHEM_PIE_DATA = [
  1305. {name:'管控化学品', value:269, itemStyle:{color:'#ef4444'}},
  1306. {name:'非管控化学品', value:960, itemStyle:{color:'#1e90ff'}},
  1307. ];
  1308. let chemChart = null;
  1309. function renderChemChart() {
  1310. if (!chemChart) {
  1311. const el = document.getElementById('chart-chem-inventory');
  1312. if (!el) return;
  1313. chemChart = echarts.init(el);
  1314. }
  1315. chemChart.setOption({
  1316. backgroundColor:'transparent',
  1317. tooltip:{ trigger:'item', formatter:'{b}<br/>数量:{c} L<br/>占比:{d}%' },
  1318. series:[{
  1319. type:'pie', radius:['32%','62%'],
  1320. center:['50%','52%'],
  1321. avoidLabelOverlap:true,
  1322. label:{
  1323. show:true, position:'outside',
  1324. formatter:params => `{name|${params.name}}\n{val|${params.value}L ${params.percent}%}`,
  1325. rich:{
  1326. name:{ fontSize:24, color:'#a8cce8', lineHeight:30 },
  1327. val:{ fontSize:22, color:'#ffd740', lineHeight:26 },
  1328. },
  1329. distanceToLabelLine:10,
  1330. },
  1331. labelLine:{ show:true, length:25, length2:35, lineStyle:{color:'rgba(168,204,232,0.5)'} },
  1332. emphasis:{ label:{fontSize:26, fontWeight:'bold'}, scale:true, scaleSize:8 },
  1333. data: CHEM_PIE_DATA,
  1334. }],
  1335. }, true);
  1336. }
  1337. renderChemChart();
  1338. // ====================================================
  1339. // SENSOR LIST (4区上)
  1340. // ====================================================
  1341. const LABS = [
  1342. {name:'化学分析实验室', room:'A301', unit:'化学研究所', t:22.5, h:58, tvoc:0.82, co2:650, o2:20.9, alert:true},
  1343. {name:'生物安全实验室', room:'B201', unit:'生物研究所', t:20.1, h:62, tvoc:0.18, co2:580, o2:20.8, alert:false},
  1344. {name:'材料测试实验室', room:'C401', unit:'材料研究所', t:24.0, h:45, tvoc:0.09, co2:520, o2:20.9, alert:false},
  1345. {name:'精密仪器实验室', room:'A205', unit:'物理研究所', t:21.5, h:50, tvoc:0.14, co2:490, o2:20.9, alert:false},
  1346. {name:'有机合成实验室', room:'B302', unit:'化学研究所', t:23.8, h:55, tvoc:2.10, co2:725, o2:20.5, alert:true},
  1347. {name:'光学检测实验室', room:'C203', unit:'物理研究所', t:22.0, h:52, tvoc:0.11, co2:510, o2:20.9, alert:false},
  1348. {name:'高压实验室', room:'D101', unit:'工程研究所', t:25.5, h:48, tvoc:0.30, co2:600, o2:20.7, alert:false},
  1349. {name:'低温实验室', room:'D205', unit:'物理研究所', t:18.0, h:40, tvoc:0.07, co2:480, o2:20.9, alert:false},
  1350. {name:'核磁共振室', room:'A302', unit:'化学研究所', t:20.5, h:54, tvoc:0.05, co2:505, o2:20.9, alert:false},
  1351. {name:'质谱分析室', room:'A303', unit:'化学研究所', t:21.0, h:53, tvoc:0.19, co2:530, o2:20.8, alert:false},
  1352. ];
  1353. function buildSensorList() {
  1354. const el = document.getElementById('sensor-list'); if (!el) return;
  1355. const all = [...LABS, ...LABS];
  1356. el.innerHTML = all.map(lab => `
  1357. <div class="sensor-item ${lab.alert ? 'alert' : ''}">
  1358. <div class="sensor-item-head">
  1359. <span class="sensor-name">${lab.name}(${lab.room})</span>
  1360. <span class="sensor-unit">${lab.unit}</span>
  1361. ${lab.alert
  1362. ? '<span style="color:var(--red);font-size:25px;animation:blinkRed 0.8s infinite">🚨 告警</span>'
  1363. : '<span style="color:var(--green);font-size:25px">● 正常</span>'}
  1364. </div>
  1365. <div class="sensor-metrics">
  1366. <div class="sensor-metric">🌡️ ${lab.t}°C</div>
  1367. <div class="sensor-metric">💧 ${lab.h}%</div>
  1368. <div class="sensor-metric ${lab.tvoc > 0.6 ? 'alarm' : ''}">🧪 TVOC ${lab.tvoc}</div>
  1369. <div class="sensor-metric ${lab.co2 > 700 ? 'alarm' : ''}">💨 CO₂ ${lab.co2}</div>
  1370. <div class="sensor-metric">🫧 O₂ ${lab.o2}%</div>
  1371. </div>
  1372. </div>`).join('');
  1373. }
  1374. buildSensorList();
  1375. // ====================================================
  1376. // WARNING LIST (4区下) — 应急预警 + 危化品违规带离
  1377. // ====================================================
  1378. const WARNS = [
  1379. {type:'emergency', lab:'化学分析实验室', room:'A301', unit:'化学研究所', metric:'TVOC浓度超标', val:'2.85 mg/m³', time:'2026-03-23 14:32:18'},
  1380. {type:'chemical', lab:'有机合成实验室', room:'B302', unit:'化学研究所', person:'李研究员', action:'盐酸未正常使用违规带离', time:'2026-03-23 13:58:44'},
  1381. {type:'emergency', lab:'有机合成实验室', room:'B302', unit:'化学研究所', metric:'CO₂浓度偏高', val:'725 ppm', time:'2026-03-23 12:15:07'},
  1382. {type:'chemical', lab:'生物安全实验室', room:'B201', unit:'生物研究所', person:'王实验员', action:'甲醇溶液违规带出实验室', time:'2026-03-22 17:40:22'},
  1383. {type:'emergency', lab:'高压实验室', room:'D101', unit:'工程研究所', metric:'温度异常', val:'38.5 °C', time:'2026-03-22 12:15:07'},
  1384. {type:'emergency', lab:'生物安全实验室', room:'B201', unit:'生物研究所', metric:'湿度超标', val:'88% RH', time:'2026-03-21 11:40:22'},
  1385. {type:'chemical', lab:'材料测试实验室', room:'C401', unit:'材料研究所', person:'张工程师', action:'硫酸未经审批带离', time:'2026-03-21 10:08:55'},
  1386. {type:'emergency', lab:'光学检测实验室', room:'C203', unit:'物理研究所', metric:'O₂浓度偏低', val:'19.2 %', time:'2026-03-20 09:22:33'},
  1387. {type:'chemical', lab:'精密仪器实验室', room:'A205', unit:'物理研究所', person:'赵研究员', action:'丙酮溶液违规带离实验楼', time:'2026-03-19 16:45:11'},
  1388. {type:'emergency', lab:'低温实验室', room:'D205', unit:'物理研究所', metric:'气压异常', val:'85 kPa', time:'2026-03-18 23:12:40'},
  1389. ];
  1390. function buildWarnList() {
  1391. const el = document.getElementById('warn-list'); if (!el) return;
  1392. const all = [...WARNS, ...WARNS];
  1393. el.innerHTML = all.map(w => {
  1394. const isEm = w.type === 'emergency';
  1395. const labCls = isEm ? 'warn-lab-info' : 'warn-lab-info chemical';
  1396. const badge = isEm
  1397. ? '<span class="warn-type-badge emergency">🚨 应急预警</span>'
  1398. : '<span class="warn-type-badge chemical">☣️ 违规带离</span>';
  1399. const detail = isEm
  1400. ? `异常传感器:<span class="warn-metric-val">${w.metric} ${w.val}</span>`
  1401. : `<span style="color:#67e8f9">${w.person}</span> — <span style="color:#fbbf24">${w.action}</span>`;
  1402. return `
  1403. <div class="warn-item type-${w.type}">
  1404. <div class="warn-item-head" style="flex-wrap:wrap;gap:8px 15px">
  1405. <div style="display:flex;align-items:center;flex:1;min-width:0;gap:8px">
  1406. ${badge}
  1407. <span class="${labCls}">${w.lab}(${w.room})- ${w.unit}</span>
  1408. </div>
  1409. <span class="warn-time">${w.time}</span>
  1410. </div>
  1411. <div class="warn-detail">${detail}</div>
  1412. </div>`;
  1413. }).join('');
  1414. }
  1415. buildWarnList();
  1416. // ====================================================
  1417. // ALERT MODAL + CAMERA
  1418. // ====================================================
  1419. function closeAlert() { document.getElementById('alert-modal').classList.remove('show'); }
  1420. function openEvac() { document.getElementById('evac-modal').classList.add('show'); }
  1421. function closeEvac() { document.getElementById('evac-modal').classList.remove('show'); }
  1422. let alertCamRAF = null;
  1423. function startAlertCam() {
  1424. const canvas = document.getElementById('alert-cam-canvas');
  1425. if (!canvas || canvas._alertRunning) return;
  1426. canvas._alertRunning = true;
  1427. const ctx = canvas.getContext('2d');
  1428. const W = 900, H = 700;
  1429. let t = Math.random() * 1000;
  1430. const noise = Array.from({length:300}, () => ({x:Math.random(), y:Math.random(), s:Math.random()*2.5+0.5, a:Math.random()*0.25}));
  1431. function frame() {
  1432. t += 0.4;
  1433. ctx.clearRect(0, 0, W, H);
  1434. // Background gradient (dark green-tinted night-vision feel with red emergency tint)
  1435. const bg = ctx.createLinearGradient(0, 0, 0, H);
  1436. bg.addColorStop(0, 'hsl(210,45%,5%)');
  1437. bg.addColorStop(1, 'hsl(0,30%,4%)');
  1438. ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
  1439. // Perspective floor grid
  1440. ctx.strokeStyle = 'rgba(20,65,140,0.07)'; ctx.lineWidth = 0.5;
  1441. const vp = {x: W/2, y: H*0.44};
  1442. for (let gx = 0; gx <= 12; gx++) { const fx = W*gx/12; ctx.beginPath(); ctx.moveTo(fx,H); ctx.lineTo(vp.x+(fx-W/2)*0.28,vp.y); ctx.stroke(); }
  1443. for (let gy = 0; gy <= 6; gy++) { const fy = H*0.44+(H*0.56)*(gy/6); const wd = W*(0.25+0.75*(gy/6)); ctx.beginPath(); ctx.moveTo((W-wd)/2,fy); ctx.lineTo((W+wd)/2,fy); ctx.stroke(); }
  1444. // Lab furniture silhouettes
  1445. ctx.fillStyle = 'rgba(0,30,80,0.45)';
  1446. ctx.fillRect(W*0.05, H*0.52, W*0.38, H*0.12); // bench left
  1447. ctx.fillRect(W*0.57, H*0.52, W*0.38, H*0.12); // bench right
  1448. ctx.fillStyle = 'rgba(0,18,50,0.55)';
  1449. ctx.fillRect(W*0.08, H*0.35, W*0.09, H*0.17); // cabinet
  1450. ctx.fillRect(W*0.60, H*0.33, W*0.12, H*0.19); // equipment
  1451. // Person silhouette (lab worker)
  1452. const px = W*(0.4 + Math.sin(t*0.008)*0.03);
  1453. const py = H*(0.58 + Math.cos(t*0.006)*0.015);
  1454. ctx.fillStyle = 'rgba(110,160,240,0.6)';
  1455. ctx.beginPath(); ctx.arc(px, py - H*0.1, W*0.028, 0, Math.PI*2); ctx.fill(); // head
  1456. ctx.fillRect(px - W*0.022, py - H*0.08, W*0.044, H*0.1); // body
  1457. // Red emergency scan line
  1458. const scanY = (t * 0.8) % H;
  1459. const scanG = ctx.createLinearGradient(0, scanY-8, 0, scanY+8);
  1460. scanG.addColorStop(0, 'transparent');
  1461. scanG.addColorStop(0.5, 'rgba(239,68,68,0.18)');
  1462. scanG.addColorStop(1, 'transparent');
  1463. ctx.fillStyle = scanG; ctx.fillRect(0, scanY-8, W, 16);
  1464. // Noise
  1465. noise.forEach(n => {
  1466. ctx.fillStyle = `rgba(200,210,240,${n.a*0.45})`;
  1467. ctx.fillRect(n.x*W, n.y*H, n.s, n.s);
  1468. if (Math.random() < 0.015) { n.x = Math.random(); n.y = Math.random(); }
  1469. });
  1470. // Red vignette pulse (emergency)
  1471. const vig = ctx.createRadialGradient(W/2, H/2, H*0.25, W/2, H/2, H*0.85);
  1472. const vigAlpha = 0.12 + Math.sin(t*0.08)*0.06;
  1473. vig.addColorStop(0, 'transparent');
  1474. vig.addColorStop(1, `rgba(200,0,0,${vigAlpha})`);
  1475. ctx.fillStyle = vig; ctx.fillRect(0, 0, W, H);
  1476. alertCamRAF = requestAnimationFrame(frame);
  1477. }
  1478. frame();
  1479. }
  1480. setTimeout(() => {
  1481. document.getElementById('alert-modal').classList.add('show');
  1482. startAlertCam();
  1483. }, 5000);
  1484. // ====================================================
  1485. // ECHARTS
  1486. // ====================================================
  1487. function initCharts() {
  1488. const TOOLTIP_CFG = {
  1489. backgroundColor:'rgba(3,14,42,0.92)',
  1490. borderColor:'rgba(30,144,255,0.3)',
  1491. textStyle:{ color:'#a8cce8', fontSize:28 }
  1492. };
  1493. // --- 环形图: 实验室总数分级 ---
  1494. const dEl = document.getElementById('chart-donut');
  1495. if (dEl) {
  1496. const c = echarts.init(dEl, null, {renderer:'canvas', devicePixelRatio:2});
  1497. c.setOption({
  1498. backgroundColor:'transparent',
  1499. tooltip:{ trigger:'item', formatter:'{b}: {c}间 ({d}%)', ...TOOLTIP_CFG },
  1500. graphic:[{
  1501. type:'text', left:'center', top:'40%',
  1502. style:{ text:'128', fill:'#ffd740', font:'bold 80px Arial', textAlign:'center' }
  1503. },{
  1504. type:'text', left:'center', top:'58%',
  1505. style:{ text:'实验室总数(间)', fill:'rgba(168,204,232,0.65)', font:'22px Arial', textAlign:'center' }
  1506. }],
  1507. series:[{
  1508. type:'pie', radius:['42%','68%'], center:['50%','50%'], startAngle:100,
  1509. itemStyle:{ borderRadius:6, borderColor:'rgba(3,14,31,0.6)', borderWidth:3 },
  1510. label:{
  1511. show:true,
  1512. formatter: params => `{lvl|${params.name}}\n{cnt|${params.value}间}\n{pct|${params.percent}%}`,
  1513. rich:{
  1514. lvl:{ fontSize:24, fontWeight:700, color:'#ddf0ff', lineHeight:36 },
  1515. cnt:{ fontSize:30, fontWeight:900, color:'#ffd740', lineHeight:42 },
  1516. pct:{ fontSize:22, color:'rgba(168,204,232,0.6)', lineHeight:32 }
  1517. },
  1518. distanceToLabelLine:8
  1519. },
  1520. labelLine:{
  1521. show:true, length:20, length2:25,
  1522. lineStyle:{ color:'rgba(30,144,255,0.45)', width:2 }
  1523. },
  1524. data:[
  1525. {value:12, name:'I级', itemStyle:{color:'#cc0000', shadowBlur:12, shadowColor:'rgba(204,0,0,0.5)'}},
  1526. {value:28, name:'II级', itemStyle:{color:'#ff8000', shadowBlur:12, shadowColor:'rgba(255,128,0,0.5)'}},
  1527. {value:45, name:'III级', itemStyle:{color:'#ffcc00', shadowBlur:12, shadowColor:'rgba(255,204,0,0.5)'}},
  1528. {value:43, name:'IV级', itemStyle:{color:'#0066cc', shadowBlur:12, shadowColor:'rgba(0,102,204,0.5)'}}
  1529. ],
  1530. emphasis:{
  1531. scale:true, scaleSize:8,
  1532. itemStyle:{ shadowBlur:30, shadowColor:'rgba(30,144,255,0.6)' },
  1533. label:{ fontSize:28 }
  1534. }
  1535. }]
  1536. });
  1537. }
  1538. // --- 堆叠柱状图: 安全分级统计 ---
  1539. const stEl = document.getElementById('chart-stack');
  1540. if (stEl) {
  1541. const c = echarts.init(stEl, null, {renderer:'canvas', devicePixelRatio:2});
  1542. const cols = ['#cc0000','#ff8000','#ffcc00','#0066cc'];
  1543. const lvls = ['I级','II级','III级','IV级'];
  1544. const units = ['化学所','生物所','材料所','物理所','工程所','核能所','信息所','环境所','计量所','医学所'];
  1545. const totals = [28,22,18,24,16,12,20,15,10,14];
  1546. const dat = [
  1547. [2,3,5,4,2,1,3,2,1,2],
  1548. [5,6,4,7,4,3,5,4,3,4],
  1549. [9,8,6,8,5,5,7,5,4,5],
  1550. [12,5,3,5,5,3,5,4,2,3]
  1551. ];
  1552. const xL = units.map((u,i) => `${u}\n(${totals[i]})`);
  1553. c.setOption({
  1554. backgroundColor:'transparent',
  1555. tooltip:{ trigger:'axis', axisPointer:{type:'shadow'}, ...TOOLTIP_CFG },
  1556. legend:{ data:lvls, top:0, right:0, textStyle:{color:'#a8cce8',fontSize:25}, icon:'rect', itemWidth:25, itemHeight:15 },
  1557. grid:{ left:30, right:8, top:55, bottom:110, containLabel:true },
  1558. xAxis:{ type:'category', data:xL, axisLabel:{color:'#5890b8',fontSize:22,interval:0,lineHeight:35}, axisLine:{lineStyle:{color:'rgba(30,144,255,0.2)'}}, axisTick:{show:false} },
  1559. yAxis:{ type:'value', name:'间', nameTextStyle:{color:'#5890b8',fontSize:25}, axisLabel:{color:'#5890b8',fontSize:25}, axisLine:{show:false}, splitLine:{lineStyle:{color:'rgba(30,144,255,0.1)'}} },
  1560. dataZoom:[{ type:'inside', startValue:0, endValue:5 }],
  1561. series:lvls.map((n,i) => ({
  1562. name:n, type:'bar', stack:'total', barMaxWidth:55,
  1563. data:dat[i].map(v => ({value:v, itemStyle:{color:cols[i], opacity:0.88}})),
  1564. emphasis:{ itemStyle:{opacity:1} }
  1565. }))
  1566. });
  1567. let idx = 0;
  1568. setInterval(() => {
  1569. idx++;
  1570. c.dispatchAction({ type:'dataZoom', startValue:idx, endValue:idx+5 });
  1571. if (idx >= units.length-5) idx = -1;
  1572. }, 5000);
  1573. }
  1574. // --- 折线图: 进入人数走势 ---
  1575. const lnEl = document.getElementById('chart-line');
  1576. if (lnEl) {
  1577. const c = echarts.init(lnEl, null, {renderer:'canvas', devicePixelRatio:2});
  1578. c.setOption({
  1579. backgroundColor:'transparent',
  1580. tooltip:{ trigger:'axis', ...TOOLTIP_CFG },
  1581. legend:{ data:['进入人数','在场人数'], top:0, right:0, textStyle:{color:'#a8cce8',fontSize:25}, icon:'circle', itemWidth:20, itemHeight:20 },
  1582. grid:{ left:28, right:8, top:45, bottom:50, containLabel:true },
  1583. xAxis:{ type:'category', data:['00:00','03:00','06:00','09:00','12:00','15:00','18:00','21:00','24:00'], axisLabel:{color:'#5890b8',fontSize:22}, axisLine:{lineStyle:{color:'rgba(30,144,255,0.2)'}}, axisTick:{show:false} },
  1584. yAxis:{ type:'value', axisLabel:{color:'#5890b8',fontSize:22}, axisLine:{show:false}, splitLine:{lineStyle:{color:'rgba(30,144,255,0.1)'}} },
  1585. series:[
  1586. { name:'进入人数', type:'line', data:[0,0,2,86,128,145,160,90,30], smooth:true, symbol:'circle', symbolSize:12, lineStyle:{color:'#1e90ff',width:5}, itemStyle:{color:'#1e90ff'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(30,144,255,0.32)'},{offset:1,color:'rgba(30,144,255,0.02)'}])} },
  1587. { name:'在场人数', type:'line', data:[0,0,2,62,98,108,120,72,20], smooth:true, symbol:'circle', symbolSize:12, lineStyle:{color:'#ffd740',width:5}, itemStyle:{color:'#ffd740'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(255,215,64,0.22)'},{offset:1,color:'rgba(255,215,64,0.01)'}])} }
  1588. ]
  1589. });
  1590. }
  1591. // --- 仪表盘: 设备在线率 ---
  1592. const ggEl = document.getElementById('chart-gauge');
  1593. if (ggEl) {
  1594. const c = echarts.init(ggEl, null, {renderer:'canvas', devicePixelRatio:2});
  1595. c.setOption({
  1596. backgroundColor:'transparent',
  1597. series:[{
  1598. type:'gauge', radius:'90%', center:['50%','60%'],
  1599. startAngle:210, endAngle:-30, min:0, max:100, splitNumber:5,
  1600. axisLine:{ lineStyle:{ width:30, color:[[0.3,'#ef4444'],[0.6,'#f59e0b'],[1,'#1e90ff']] } },
  1601. axisTick:{show:false}, splitLine:{show:false}, axisLabel:{show:false},
  1602. pointer:{ icon:'path://M12.8,0.7l12.3,0 M0,0 l9.2,12.4 M0,0 l2.2,-1.6', offsetCenter:[0,'-60%'], width:8, length:'60%', itemStyle:{color:'#1e90ff'} },
  1603. detail:{ valueAnimation:true, formatter:'{value}%', color:'#ffd740', fontSize:45, fontWeight:700, offsetCenter:[0,'30%'] },
  1604. title:{ show:true, offsetCenter:[0,'62%'], color:'rgba(110,165,210,0.6)', fontSize:25 },
  1605. data:[{value:94.5, name:'在线率'}]
  1606. }]
  1607. });
  1608. }
  1609. // --- 环形图: 设备分类 (2区设备统计上部) ---
  1610. const rgEl = document.getElementById('chart-ring');
  1611. if (rgEl) {
  1612. const c = echarts.init(rgEl, null, {renderer:'canvas', devicePixelRatio:2});
  1613. const ringData = [
  1614. {value:680, name:'检测设备', itemStyle:{color:'#1e90ff', shadowBlur:8, shadowColor:'rgba(30,144,255,0.5)'}},
  1615. {value:520, name:'分析仪器', itemStyle:{color:'#4361ee', shadowBlur:8, shadowColor:'rgba(67,97,238,0.5)'}},
  1616. {value:380, name:'制备设备', itemStyle:{color:'#00e676', shadowBlur:8, shadowColor:'rgba(0,230,118,0.5)'}},
  1617. {value:280, name:'安全设备', itemStyle:{color:'#ffd740', shadowBlur:8, shadowColor:'rgba(255,215,64,0.5)'}},
  1618. {value:240, name:'辅助设备', itemStyle:{color:'#00e5c8', shadowBlur:8, shadowColor:'rgba(0,229,200,0.5)'}},
  1619. {value:358, name:'其他', itemStyle:{color:'#f97316', shadowBlur:8, shadowColor:'rgba(249,115,22,0.5)'}}
  1620. ];
  1621. const total = ringData.reduce((s,d)=>s+d.value,0);
  1622. const ringCountMap = Object.fromEntries(ringData.map(d=>[d.name,d.value]));
  1623. c.setOption({
  1624. backgroundColor:'transparent',
  1625. tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
  1626. legend:{
  1627. orient:'vertical', right:'1%', top:'middle',
  1628. icon:'circle', itemWidth:24, itemHeight:24, itemGap:18,
  1629. textStyle:{ color:'#a8cce8', fontSize:20 },
  1630. formatter: name => {
  1631. const v = ringCountMap[name];
  1632. const pct = (v/total*100).toFixed(1);
  1633. return `{nm|${name}} {vl|${v}台} {pt|${pct}%}`;
  1634. },
  1635. rich:{
  1636. nm:{ fontSize:20, color:'#a8cce8', width:80 },
  1637. vl:{ fontSize:23, fontWeight:700, color:'#fff', width:70 },
  1638. pt:{ fontSize:20, color:'#ffd740', width:55 }
  1639. }
  1640. },
  1641. series:[{
  1642. type:'pie', radius:['38%','62%'], center:['32%','50%'],
  1643. itemStyle:{ borderRadius:4, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
  1644. label:{ show:true, formatter:'{c}台', fontSize:20, color:'#a8cce8' },
  1645. labelLine:{ length:10, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
  1646. data: ringData,
  1647. emphasis:{ scale:true, scaleSize:5, itemStyle:{ shadowBlur:20, shadowColor:'rgba(30,144,255,0.6)' } }
  1648. }]
  1649. });
  1650. }
  1651. // --- 饼图: 设备状态(正常/维修/报废) ---
  1652. const psEl = document.getElementById('chart-pie-status');
  1653. if (psEl) {
  1654. const c = echarts.init(psEl, null, {renderer:'canvas', devicePixelRatio:2});
  1655. const statusData = [
  1656. {value:2180, name:'正常', itemStyle:{color:'#00e676', shadowBlur:10, shadowColor:'rgba(0,230,118,0.5)'}},
  1657. {value:198, name:'维修', itemStyle:{color:'#f59e0b', shadowBlur:10, shadowColor:'rgba(245,158,11,0.5)'}},
  1658. {value:80, name:'报废', itemStyle:{color:'#ef4444', shadowBlur:10, shadowColor:'rgba(239,68,68,0.5)'}}
  1659. ];
  1660. const smMap = Object.fromEntries(statusData.map(d=>[d.name,d.value]));
  1661. c.setOption({
  1662. backgroundColor:'transparent',
  1663. tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
  1664. legend:{ show:false },
  1665. series:[{
  1666. type:'pie', radius:['35%','58%'], center:['50%','55%'],
  1667. itemStyle:{ borderRadius:5, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
  1668. label:{ show:true, formatter:'{b}\n{c}台\n{d}%', fontSize:20, color:'#a8cce8', lineHeight:30 },
  1669. labelLine:{ length:14, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
  1670. data: statusData,
  1671. emphasis:{ scale:true, scaleSize:5, itemStyle:{ shadowBlur:20, shadowColor:'rgba(30,144,255,0.6)' } }
  1672. }]
  1673. });
  1674. }
  1675. // --- 饼图: 使用状态(使用/空闲) ---
  1676. const puEl = document.getElementById('chart-pie-usage');
  1677. if (puEl) {
  1678. const c = echarts.init(puEl, null, {renderer:'canvas', devicePixelRatio:2});
  1679. const usageData = [
  1680. {value:1534, name:'使用', itemStyle:{color:'#1e90ff', shadowBlur:14, shadowColor:'rgba(30,144,255,0.6)'}},
  1681. {value:924, name:'空闲', itemStyle:{color:'#00e5c8', shadowBlur:14, shadowColor:'rgba(0,229,200,0.5)'}}
  1682. ];
  1683. c.setOption({
  1684. backgroundColor:'transparent',
  1685. tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
  1686. legend:{ show:false },
  1687. series:[{
  1688. type:'pie', radius:['35%','60%'], center:['50%','55%'],
  1689. itemStyle:{ borderRadius:6, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
  1690. label:{ show:true, formatter:'{b}\n{c}台\n{d}%', fontSize:22, color:'#a8cce8', lineHeight:32 },
  1691. labelLine:{ length:16, length2:12, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
  1692. data: usageData,
  1693. emphasis:{ scale:true, scaleSize:6, itemStyle:{ shadowBlur:24, shadowColor:'rgba(30,144,255,0.7)' } }
  1694. }]
  1695. });
  1696. }
  1697. }
  1698. initCharts();
  1699. // ====================================================
  1700. // MONITOR TREE
  1701. // ====================================================
  1702. function buildTree() {
  1703. const el = document.getElementById('building-tree'); if (!el) return;
  1704. const data = {label:'安科院主园区', children:[
  1705. {label:'综合实验楼A', children:[
  1706. {label:'1层', children:[{label:'A101 化学实验室'},{label:'A102 分析室'}]},
  1707. {label:'2层', children:[{label:'A201 生物实验室'}]},
  1708. {label:'3层', children:[{label:'A301 有机合成室'},{label:'A302 核磁共振室'},{label:'A303 质谱室'}]},
  1709. {label:'4层', children:[{label:'A401 X射线室'}]}
  1710. ]},
  1711. {label:'物理实验楼B', children:[
  1712. {label:'1层', children:[{label:'B101 光学实验室'}]},
  1713. {label:'2层', children:[{label:'B201 低温实验室'}]}
  1714. ]},
  1715. {label:'工程技术楼C', children:[
  1716. {label:'1层', children:[{label:'C101 机械加工室'}]},
  1717. {label:'2层', children:[{label:'C201 材料测试室'}]}
  1718. ]}
  1719. ]};
  1720. el.innerHTML = renderT(data, 0);
  1721. el.addEventListener('click', e => {
  1722. const l = e.target.closest('.tree-node-label'); if (!l) return;
  1723. l.classList.toggle('selected');
  1724. const ch = l.parentElement.querySelector('.tree-children');
  1725. const ar = l.querySelector('.arrow');
  1726. if (ch) ch.classList.toggle('open');
  1727. if (ar) ar.classList.toggle('open');
  1728. });
  1729. }
  1730. function renderT(node, depth) {
  1731. const isLeaf = !node.children || !node.children.length;
  1732. const icon = ['🏢','🏗️','📐','🔬'][Math.min(depth,3)];
  1733. let h = `<div class="tree-node"><div class="tree-node-label" style="padding-left:${depth*12+8}px">
  1734. <span class="arrow ${depth===0?'open':''}">${isLeaf ? '&nbsp;&nbsp;' : '▶'}</span>
  1735. <span>${icon}</span><span>${node.label}</span></div>`;
  1736. if (!isLeaf) {
  1737. h += `<div class="tree-children ${depth===0?'open':''}">`;
  1738. node.children.forEach(c => h += renderT(c, depth+1));
  1739. h += '</div>';
  1740. }
  1741. return h + '</div>';
  1742. }
  1743. // ====================================================
  1744. // CAMERAS
  1745. // ====================================================
  1746. const CAMS = ['A301 有机合成室','A302 核磁共振室','A303 质谱室','A301 走廊','A302 走廊','A303 走廊','A层公共区域','A层安全通道','A层出入口'];
  1747. function buildCameras() {
  1748. buildCameraGrid('camera-grid-9');
  1749. buildCameraGrid('camera-grid');
  1750. }
  1751. function buildCameraGrid(gridId) {
  1752. const g = document.getElementById(gridId); if (!g) return;
  1753. g.innerHTML = '';
  1754. for (let i = 0; i < 9; i++) {
  1755. const cell = document.createElement('div');
  1756. cell.className = 'camera-cell' + (i===0 ? ' ai-cam' : '');
  1757. const cv = document.createElement('canvas');
  1758. cv.style.cssText = 'width:100%;height:100%;display:block';
  1759. cell.appendChild(cv);
  1760. cell.innerHTML += `<div class="camera-overlay"></div><div class="camera-label">${CAMS[i]}</div><div class="camera-rec">REC</div>${i===0?'<div class="camera-ai-badge">🤖 AI检测</div>':''}`;
  1761. if (i===0) {
  1762. const box = document.createElement('div');
  1763. box.className = 'ai-detection-box';
  1764. box.style.cssText = 'left:28%;top:18%;width:22%;height:38%';
  1765. box.innerHTML = '<div class="ai-detection-label">危险行为: 未佩戴防护</div>';
  1766. cell.appendChild(box);
  1767. }
  1768. g.appendChild(cell);
  1769. setTimeout(() => drawFakeCam(cv, i), 150);
  1770. }
  1771. }
  1772. function switchMonitorTab(tab, btn) {
  1773. document.querySelectorAll('.monitor-tab').forEach(b => b.classList.remove('active'));
  1774. document.querySelectorAll('.monitor-tab-content').forEach(c => c.classList.remove('active'));
  1775. btn.classList.add('active');
  1776. document.getElementById('tab-' + tab).classList.add('active');
  1777. }
  1778. function drawFakeCam(canvas, idx) {
  1779. const ctx = canvas.getContext('2d');
  1780. const par = canvas.parentElement;
  1781. let pw, ph;
  1782. function sz() { pw = canvas.width = par.offsetWidth; ph = canvas.height = par.offsetHeight; }
  1783. sz(); window.addEventListener('resize', sz);
  1784. let t = Math.random() * 1000;
  1785. const noise = Array.from({length:200}, () => ({x:Math.random(),y:Math.random(),s:Math.random()*3,a:Math.random()*0.3}));
  1786. function frame() {
  1787. t += 0.5; if (!pw || !ph) sz();
  1788. ctx.clearRect(0,0,pw,ph);
  1789. const bg = ctx.createLinearGradient(0,0,0,ph);
  1790. bg.addColorStop(0,'hsl(210,42%,5%)'); bg.addColorStop(1,'hsl(220,30%,3%)');
  1791. ctx.fillStyle = bg; ctx.fillRect(0,0,pw,ph);
  1792. ctx.strokeStyle = 'rgba(20,70,150,0.06)'; ctx.lineWidth = 0.5;
  1793. const vp = {x:pw/2, y:ph*0.45};
  1794. for (let gx=0;gx<=10;gx++) { const fx=pw*gx/10; ctx.beginPath();ctx.moveTo(fx,ph);ctx.lineTo(vp.x+(fx-pw/2)*0.3,vp.y);ctx.stroke(); }
  1795. for (let gy=0;gy<=5;gy++) { const fy=ph*0.45+(ph*0.55)*(gy/5); const wd=pw*(0.3+0.7*(gy/5)); ctx.beginPath();ctx.moveTo((pw-wd)/2,fy);ctx.lineTo((pw+wd)/2,fy);ctx.stroke(); }
  1796. ctx.fillStyle='rgba(0,35,90,0.4)'; ctx.fillRect(pw*0.1,ph*0.55,pw*0.35,ph*0.1); ctx.fillRect(pw*0.55,ph*0.55,pw*0.35,ph*0.1);
  1797. ctx.fillStyle='rgba(0,18,55,0.5)'; ctx.fillRect(pw*0.12,ph*0.4,pw*0.08,ph*0.15); ctx.fillRect(pw*0.58,ph*0.38,pw*0.1,ph*0.17);
  1798. const np = [2,1,3,1,2,1,0,1,2][idx]||1;
  1799. for (let p=0;p<np;p++) {
  1800. const px=pw*(0.3+p*0.15+Math.sin(t*0.01+p)*0.02);
  1801. const py=ph*(0.6+Math.cos(t*0.008+p)*0.02);
  1802. ctx.fillStyle='rgba(120,170,255,0.65)'; ctx.beginPath(); ctx.arc(px,py,pw*0.025,0,Math.PI*2); ctx.fill();
  1803. ctx.fillStyle='rgba(70,130,220,0.4)'; ctx.fillRect(px-pw*0.02,py+pw*0.025,pw*0.04,ph*0.08);
  1804. }
  1805. const sl = (t*0.5) % ph; ctx.fillStyle='rgba(30,144,255,0.04)'; ctx.fillRect(0,sl,pw,2);
  1806. noise.forEach(n => {
  1807. ctx.fillStyle=`rgba(170,205,255,${n.a*0.5})`; ctx.fillRect(n.x*pw,n.y*ph,n.s,n.s);
  1808. if (Math.random()<0.02) { n.x=Math.random(); n.y=Math.random(); }
  1809. });
  1810. requestAnimationFrame(frame);
  1811. }
  1812. frame();
  1813. }
  1814. // ====================================================
  1815. // RESIZE CHARTS
  1816. // ====================================================
  1817. window.addEventListener('resize', () => {
  1818. ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie-status','chart-pie-usage','chart-chem-inventory'].forEach(id => {
  1819. const el = document.getElementById(id);
  1820. if (el) { const c = echarts.getInstanceByDom(el); if (c) c.resize(); }
  1821. });
  1822. });
  1823. </script>
  1824. </body>
  1825. </html>