agentDemo_dashboard.html 105 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290
  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. <link rel="stylesheet" href="libs/reset.css" />
  8. <script src="libs/echarts.min.js"></script>
  9. <style>
  10. /* ======================== CSS Variables ======================== */
  11. :root {
  12. --screen-w: 1920;
  13. --screen-h: 1080;
  14. --bg-0: #020c1b;
  15. --bg-1: #061630;
  16. --bg-2: #0a2550;
  17. --panel-bg: rgba(6, 22, 56, 0.82);
  18. --panel-border: rgba(72, 180, 255, 0.28);
  19. --panel-glow: rgba(72, 180, 255, 0.06);
  20. --text-main: #d8f4ff;
  21. --text-sub: #7eacc8;
  22. --accent: #48d7ff;
  23. --accent2: #3a7bff;
  24. --good: #36d399;
  25. --warn: #ffb020;
  26. --danger: #ff4d4f;
  27. --grade-I: #ff4d4f;
  28. --grade-II: #ff8c00;
  29. --grade-III: #ffcc00;
  30. --grade-IV: #3a7bff;
  31. }
  32. * { box-sizing: border-box; margin: 0; padding: 0; }
  33. html, body {
  34. width: 100%; height: 100%; overflow: hidden;
  35. background: radial-gradient(ellipse 1400px 700px at 10% 0%, rgba(50,140,255,0.15), transparent 65%),
  36. radial-gradient(ellipse 1000px 500px at 90% 100%, rgba(0,200,255,0.1), transparent 60%),
  37. linear-gradient(150deg, var(--bg-0), var(--bg-1) 45%, var(--bg-2));
  38. color: var(--text-main);
  39. font-family: "DIN Alternate","Alibaba PuHuiTi","PingFang SC","Microsoft YaHei",sans-serif;
  40. }
  41. /* ======================== Viewport Scaling ======================== */
  42. .viewport { width: 100%; height: 100%; position: relative; }
  43. .screen {
  44. width: 1920px; height: 1080px;
  45. position: absolute; left: 50%; top: 50%;
  46. transform-origin: center center;
  47. overflow: hidden;
  48. }
  49. /* ======================== Page Toggle ======================== */
  50. .page { display: none; width: 100%; height: calc(100% - 72px); }
  51. .page.active { display: flex; }
  52. /* ======================== Top Nav ======================== */
  53. .top-nav {
  54. width: 100%; height: 72px; display: flex; align-items: center; justify-content: space-between;
  55. padding: 0 30px;
  56. background: linear-gradient(180deg, rgba(10,30,70,0.95) 0%, rgba(6,20,50,0.75) 100%);
  57. border-bottom: 1px solid var(--panel-border);
  58. position: relative; z-index: 100;
  59. }
  60. .top-nav::after {
  61. content: ''; position: absolute; bottom: 0; left: 10%; right: 10%; height: 1px;
  62. background: linear-gradient(90deg, transparent, var(--accent), transparent);
  63. }
  64. .nav-title {
  65. font-size: 26px; font-weight: 700; letter-spacing: 6px;
  66. background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);
  67. -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  68. text-shadow: 0 0 30px rgba(72,215,255,0.3);
  69. }
  70. .nav-tabs { display: flex; gap: 4px; }
  71. .nav-tab {
  72. padding: 8px 28px; border-radius: 4px; cursor: pointer;
  73. font-size: 15px; letter-spacing: 2px; transition: all 0.3s;
  74. border: 1px solid transparent; color: var(--text-sub);
  75. background: transparent;
  76. }
  77. .nav-tab:hover { color: var(--accent); border-color: var(--panel-border); }
  78. .nav-tab.active {
  79. background: linear-gradient(135deg, rgba(72,215,255,0.18), rgba(58,123,255,0.12));
  80. border-color: var(--accent); color: #fff;
  81. box-shadow: 0 0 16px rgba(72,215,255,0.2);
  82. }
  83. .nav-right { display: flex; align-items: center; gap: 20px; font-size: 14px; color: var(--text-sub); }
  84. .nav-right .clock { font-size: 22px; font-weight: 600; color: var(--accent); letter-spacing: 2px; }
  85. .nav-right .weather { display: flex; align-items: center; gap: 6px; }
  86. /* ======================== Panel Styles ======================== */
  87. .panel {
  88. background: var(--panel-bg);
  89. border: 1px solid var(--panel-border);
  90. border-radius: 6px; padding: 14px 16px;
  91. position: relative; overflow: hidden;
  92. backdrop-filter: blur(8px);
  93. }
  94. .panel::before {
  95. content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  96. background: linear-gradient(90deg, transparent, var(--accent), transparent);
  97. opacity: 0.6;
  98. }
  99. .panel-title {
  100. font-size: 15px; font-weight: 600; letter-spacing: 2px; margin-bottom: 12px;
  101. padding-left: 12px; position: relative; color: #fff;
  102. }
  103. .panel-title::before {
  104. content: ''; position: absolute; left: 0; top: 2px; width: 3px; height: 14px;
  105. background: var(--accent); border-radius: 2px;
  106. box-shadow: 0 0 8px var(--accent);
  107. }
  108. /* ======================== Lab Status Page Layout ======================== */
  109. #page-lab { display: none; gap: 14px; padding: 14px; }
  110. #page-lab.active { display: flex; }
  111. .col-left, .col-right { width: 440px; display: flex; flex-direction: column; gap: 14px; flex-shrink: 0; }
  112. .col-center { flex: 1; display: flex; flex-direction: column; gap: 14px; min-width: 0; }
  113. /* ======================== Status Cards ======================== */
  114. .status-cards { display: flex; gap: 12px; margin-top: 0; }
  115. .status-card {
  116. flex: 1; text-align: center; padding: 6px 6px; border-radius: 4px;
  117. background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.12);
  118. }
  119. .status-card .label { font-size: 12px; color: var(--text-sub); margin-bottom: 2px; }
  120. .status-card .value { font-size: 22px; font-weight: 700; }
  121. .status-card.using .value { color: var(--accent); }
  122. .status-card.error .value { color: var(--danger); }
  123. .status-card.idle .value { color: var(--good); }
  124. /* ======================== Flip Counter ======================== */
  125. .flip-counter-row { display: flex; gap: 24px; justify-content: center; margin-bottom: 10px; }
  126. .flip-group { text-align: center; }
  127. .flip-group .flip-label { font-size: 12px; color: var(--text-sub); margin-bottom: 6px; }
  128. .flip-digits { display: flex; gap: 4px; justify-content: center; }
  129. .flip-digit {
  130. width: 32px; height: 42px; line-height: 42px; text-align: center;
  131. font-size: 26px; font-weight: 700; color: var(--accent);
  132. background: linear-gradient(180deg, rgba(72,215,255,0.12) 0%, rgba(6,22,56,0.9) 100%);
  133. border: 1px solid rgba(72,215,255,0.25); border-radius: 4px;
  134. }
  135. /* ======================== Sensor Scroll List ======================== */
  136. .sensor-scroll-wrap {
  137. flex: 1; overflow: hidden; position: relative; min-height: 0;
  138. }
  139. .sensor-scroll-inner {
  140. animation: scrollUp 40s linear infinite;
  141. }
  142. @keyframes scrollUp {
  143. 0% { transform: translateY(0); }
  144. 100% { transform: translateY(-50%); }
  145. }
  146. .sensor-item {
  147. padding: 10px 12px; margin-bottom: 6px; border-radius: 4px;
  148. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
  149. transition: all 0.3s;
  150. }
  151. .sensor-item:hover { border-color: var(--accent); background: rgba(72,215,255,0.08); }
  152. .sensor-item .lab-name { font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #fff; }
  153. .sensor-item .lab-unit { font-size: 11px; color: var(--text-sub); margin-left: 6px; }
  154. .sensor-values { display: flex; gap: 10px; flex-wrap: wrap; }
  155. .sensor-val {
  156. font-size: 12px; padding: 2px 8px; border-radius: 3px;
  157. background: rgba(72,215,255,0.06);
  158. }
  159. .sensor-val.alarm {
  160. background: rgba(255,77,79,0.15); color: var(--danger); font-weight: 600;
  161. animation: alarmPulse 1s ease-in-out infinite alternate;
  162. }
  163. @keyframes alarmPulse {
  164. 0% { box-shadow: 0 0 4px rgba(255,77,79,0.3); }
  165. 100% { box-shadow: 0 0 12px rgba(255,77,79,0.6); }
  166. }
  167. .alarm-icon { display: inline-block; margin-right: 2px; }
  168. /* ======================== Warning Scroll ======================== */
  169. .warning-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
  170. .warning-count {
  171. font-size: 28px; font-weight: 700; color: var(--warn);
  172. text-shadow: 0 0 10px rgba(255,176,32,0.4);
  173. }
  174. .warning-count-label { font-size: 12px; color: var(--text-sub); }
  175. .warning-scroll-wrap { flex: 1; overflow: hidden; min-height: 0; }
  176. .warning-scroll-inner { animation: scrollUp 30s linear infinite; }
  177. .warning-item {
  178. padding: 8px 12px; margin-bottom: 6px; border-radius: 4px;
  179. background: rgba(255,176,32,0.04); border-left: 3px solid var(--warn);
  180. }
  181. .warning-item .w-lab { font-size: 13px; font-weight: 600; color: #fff; }
  182. .warning-item .w-sensor { font-size: 12px; color: var(--warn); margin: 2px 0; }
  183. .warning-item .w-time { font-size: 11px; color: var(--text-sub); }
  184. .warning-item .w-meta {
  185. display: flex;
  186. align-items: center;
  187. justify-content: space-between;
  188. margin-top: 4px;
  189. gap: 8px;
  190. }
  191. .warning-item .w-owner { font-size: 11px; color: var(--text-sub); }
  192. .warn-state {
  193. font-size: 11px;
  194. padding: 2px 8px;
  195. border-radius: 10px;
  196. border: 1px solid transparent;
  197. white-space: nowrap;
  198. }
  199. .warn-state.pending {
  200. color: #ffd5d6;
  201. border-color: rgba(255, 77, 79, 0.45);
  202. background: rgba(255, 77, 79, 0.18);
  203. }
  204. .warn-state.processing {
  205. color: #ffe7c0;
  206. border-color: rgba(255, 176, 32, 0.5);
  207. background: rgba(255, 176, 32, 0.16);
  208. }
  209. .warn-state.resolved {
  210. color: #cbf7e3;
  211. border-color: rgba(54, 211, 153, 0.45);
  212. background: rgba(54, 211, 153, 0.16);
  213. }
  214. /* ======================== Device Stats Top ======================== */
  215. .device-top { display: flex; gap: 12px; margin-bottom: 10px; }
  216. .device-stat-card {
  217. flex: 1; text-align: center; padding: 8px; border-radius: 4px;
  218. background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.1);
  219. }
  220. .device-stat-card .ds-label { font-size: 12px; color: var(--text-sub); }
  221. .device-stat-card .ds-value { font-size: 22px; font-weight: 700; color: var(--accent); }
  222. .device-stat-card.offline .ds-value { color: var(--text-sub); }
  223. .device-bottom { display: flex; gap: 10px; align-items: center; flex: 1; min-height: 0; }
  224. .device-gauge-wrap { width: 50%; height: 100%; min-height: 140px; }
  225. .device-type-list { flex: 1; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
  226. .device-type-item {
  227. display: flex; justify-content: space-between; align-items: center;
  228. padding: 10px 14px; border-radius: 4px;
  229. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
  230. }
  231. .device-type-item .dt-name { font-size: 13px; color: var(--text-sub); }
  232. .device-type-item .dt-value { font-size: 18px; font-weight: 700; color: var(--accent); }
  233. /* ======================== Equipment Stats ======================== */
  234. .equip-chart-wrap { flex: 1; min-height: 0; }
  235. .equip-mid {
  236. display: flex; gap: 10px; justify-content: center; margin: 10px 0;
  237. }
  238. .equip-mid-item { text-align: center; flex: 1; padding: 8px 4px; border-radius: 4px;
  239. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08); }
  240. .equip-mid-item .em-value { font-size: 24px; font-weight: 700; color: var(--accent); }
  241. .equip-mid-item .em-label { font-size: 12px; color: var(--text-sub); margin-top: 2px; }
  242. .equip-bottom {
  243. display: flex; gap: 10px;
  244. }
  245. .equip-status-card {
  246. flex: 1; text-align: center; padding: 12px 6px; border-radius: 4px;
  247. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
  248. }
  249. .equip-status-card .es-value { font-size: 22px; font-weight: 700; }
  250. .equip-status-card .es-label { font-size: 12px; color: var(--text-sub); margin-top: 2px; }
  251. /* ======================== Video Page ======================== */
  252. #page-video { display: none; gap: 14px; padding: 14px; }
  253. #page-video.active { display: flex; }
  254. .video-left {
  255. width: 300px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0;
  256. }
  257. .video-right { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
  258. .video-search {
  259. width: 100%; padding: 8px 12px; border-radius: 4px;
  260. background: rgba(72,215,255,0.06); border: 1px solid var(--panel-border);
  261. color: var(--text-main); font-size: 13px; outline: none;
  262. }
  263. .video-search::placeholder { color: var(--text-sub); }
  264. .video-filter {
  265. width: 100%; padding: 8px 12px; border-radius: 4px;
  266. background: rgba(72,215,255,0.06); border: 1px solid var(--panel-border);
  267. color: var(--text-main); font-size: 13px; outline: none;
  268. appearance: none; cursor: pointer;
  269. }
  270. .tree-container {
  271. flex: 1; overflow-y: auto; padding: 6px;
  272. }
  273. .tree-container::-webkit-scrollbar { width: 4px; }
  274. .tree-container::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 2px; }
  275. .tree-node {
  276. padding: 4px 0; cursor: pointer; user-select: none;
  277. }
  278. .tree-node-content {
  279. display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 4px;
  280. font-size: 13px; transition: all 0.2s;
  281. }
  282. .tree-node-content:hover { background: rgba(72,215,255,0.08); }
  283. .tree-node-content.selected { background: rgba(72,215,255,0.15); color: var(--accent); }
  284. .tree-arrow { display: inline-block; width: 14px; font-size: 10px; color: var(--text-sub); transition: transform 0.2s; }
  285. .tree-arrow.expanded { transform: rotate(90deg); }
  286. .tree-children { padding-left: 18px; display: none; }
  287. .tree-children.open { display: block; }
  288. .video-breadcrumb {
  289. font-size: 14px; color: var(--text-sub); margin-bottom: 10px; padding: 4px 0;
  290. border-bottom: 1px solid rgba(72,215,255,0.1);
  291. position: relative; z-index: 1;
  292. }
  293. .video-grid {
  294. flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);
  295. gap: 8px; min-height: 0;
  296. position: relative; z-index: 1;
  297. }
  298. .video-cell {
  299. position: relative; overflow: hidden;
  300. display: flex; align-items: center; justify-content: center;
  301. }
  302. /* Sci-fi outer frame */
  303. .cam-frame {
  304. position: relative; width: 100%; height: 100%; border-radius: 6px; padding: 4px;
  305. background: linear-gradient(145deg, rgba(72,215,255,0.35), rgba(58,123,255,0.1)), rgba(4,14,35,0.95);
  306. box-shadow: inset 0 0 18px rgba(72,215,255,0.2), 0 0 20px rgba(58,123,255,0.15);
  307. }
  308. .cam-frame::before {
  309. content: ''; position: absolute; inset: 0; border-radius: 6px; pointer-events: none;
  310. border: 1px solid rgba(100,210,255,0.55);
  311. clip-path: polygon(
  312. 0 0, 18px 0, 18px 2px, calc(100% - 18px) 2px, calc(100% - 18px) 0, 100% 0,
  313. 100% 18px, calc(100% - 2px) 18px, calc(100% - 2px) calc(100% - 18px), 100% calc(100% - 18px), 100% 100%,
  314. calc(100% - 18px) 100%, calc(100% - 18px) calc(100% - 2px), 18px calc(100% - 2px), 18px 100%, 0 100%,
  315. 0 calc(100% - 18px), 2px calc(100% - 18px), 2px 18px, 0 18px
  316. );
  317. }
  318. .cam-frame::after {
  319. content: ''; position: absolute; inset: 2px; border-radius: 5px; pointer-events: none;
  320. border: 1px solid rgba(120,220,255,0.18);
  321. }
  322. /* Corner glow accents */
  323. .cam-corner {
  324. position: absolute; width: 10px; height: 10px; z-index: 2;
  325. }
  326. .cam-corner::before, .cam-corner::after {
  327. content: ''; position: absolute; background: var(--accent);
  328. box-shadow: 0 0 6px var(--accent), 0 0 12px rgba(72,215,255,0.4);
  329. }
  330. .cam-corner.c-tl { top: 2px; left: 2px; }
  331. .cam-corner.c-tl::before { width: 14px; height: 2px; top: 0; left: 0; }
  332. .cam-corner.c-tl::after { width: 2px; height: 14px; top: 0; left: 0; }
  333. .cam-corner.c-tr { top: 2px; right: 2px; }
  334. .cam-corner.c-tr::before { width: 14px; height: 2px; top: 0; right: 0; }
  335. .cam-corner.c-tr::after { width: 2px; height: 14px; top: 0; right: 0; }
  336. .cam-corner.c-bl { bottom: 2px; left: 2px; }
  337. .cam-corner.c-bl::before { width: 14px; height: 2px; bottom: 0; left: 0; }
  338. .cam-corner.c-bl::after { width: 2px; height: 14px; bottom: 0; left: 0; }
  339. .cam-corner.c-br { bottom: 2px; right: 2px; }
  340. .cam-corner.c-br::before { width: 14px; height: 2px; bottom: 0; right: 0; }
  341. .cam-corner.c-br::after { width: 2px; height: 14px; bottom: 0; right: 0; }
  342. /* Inner camera view */
  343. .cam-inner {
  344. width: 100%; height: 100%; border-radius: 4px; overflow: hidden; position: relative;
  345. background: radial-gradient(ellipse 120px 80px at 20% 25%, rgba(72,180,255,0.2), transparent 70%),
  346. linear-gradient(150deg, rgba(8,30,65,0.95), rgba(4,15,35,0.92));
  347. }
  348. .cam-inner::after {
  349. content: ''; position: absolute; inset: 0; pointer-events: none;
  350. background: repeating-linear-gradient(
  351. to bottom, rgba(200,246,255,0.03) 0, rgba(200,246,255,0.03) 2px, transparent 2px, transparent 5px
  352. );
  353. }
  354. .cam-inner .cam-label {
  355. position: absolute; top: 6px; left: 8px; font-size: 11px;
  356. background: rgba(0,0,0,0.55); padding: 2px 8px; border-radius: 2px; color: var(--text-sub); z-index: 1;
  357. }
  358. .cam-inner .cam-placeholder {
  359. position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
  360. font-size: 12px; color: var(--text-sub); opacity: 0.4;
  361. }
  362. .cam-inner .cam-dot {
  363. display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px;
  364. background: #18ff6f; box-shadow: 0 0 6px #18ff6f;
  365. }
  366. /* AI camera special style */
  367. .video-cell.ai-cam .cam-frame {
  368. background: linear-gradient(145deg, rgba(255,176,32,0.3), rgba(255,120,0,0.1)), rgba(4,14,35,0.95);
  369. box-shadow: inset 0 0 18px rgba(255,176,32,0.2), 0 0 20px rgba(255,140,0,0.15);
  370. }
  371. .video-cell.ai-cam .cam-frame::before {
  372. border-color: rgba(255,190,80,0.55);
  373. }
  374. .video-cell.ai-cam .cam-corner::before, .video-cell.ai-cam .cam-corner::after {
  375. background: var(--warn);
  376. box-shadow: 0 0 6px var(--warn), 0 0 12px rgba(255,176,32,0.4);
  377. }
  378. .video-cell.ai-cam .cam-inner .cam-label { background: rgba(255,176,32,0.3); color: var(--warn); }
  379. .ai-badge {
  380. position: absolute; top: 8px; right: 10px; font-size: 10px; z-index: 2;
  381. background: linear-gradient(135deg, var(--warn), #ff6b00); color: #fff;
  382. padding: 2px 10px; border-radius: 10px; font-weight: 600;
  383. box-shadow: 0 0 10px rgba(255,176,32,0.4);
  384. }
  385. .video-pagination {
  386. display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 10px;
  387. position: relative; z-index: 1;
  388. }
  389. .page-btn {
  390. padding: 6px 20px; border-radius: 4px; cursor: pointer;
  391. background: rgba(72,215,255,0.08); border: 1px solid var(--panel-border);
  392. color: var(--text-main); font-size: 13px; transition: all 0.3s;
  393. }
  394. .page-btn:hover { background: rgba(72,215,255,0.15); border-color: var(--accent); }
  395. .page-info { font-size: 13px; color: var(--text-sub); }
  396. /* ======================== Alert Modal ======================== */
  397. .alert-overlay {
  398. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  399. background: rgba(255,0,0,0.08); z-index: 9999;
  400. display: none; align-items: center; justify-content: center;
  401. animation: alertFlash 1.5s ease-in-out infinite;
  402. }
  403. .alert-overlay.show { display: flex; }
  404. /* Screen edge red glow */
  405. .alert-overlay::before {
  406. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
  407. box-shadow:
  408. inset 0 0 80px 30px rgba(255,0,0,0.35),
  409. inset 0 0 200px 60px rgba(255,0,0,0.15);
  410. animation: edgeRedGlow 1s ease-in-out infinite;
  411. }
  412. @keyframes edgeRedGlow {
  413. 0%, 100% {
  414. box-shadow:
  415. inset 0 0 60px 20px rgba(255,0,0,0.25),
  416. inset 0 0 150px 40px rgba(255,0,0,0.1);
  417. }
  418. 50% {
  419. box-shadow:
  420. inset 0 0 120px 50px rgba(255,0,0,0.5),
  421. inset 0 0 300px 80px rgba(255,0,0,0.25);
  422. }
  423. }
  424. /* Four edge red light bars */
  425. .alert-overlay::after {
  426. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
  427. border: 3px solid transparent;
  428. animation: edgeBorderFlash 0.8s ease-in-out infinite;
  429. }
  430. @keyframes edgeBorderFlash {
  431. 0%, 100% { border-color: rgba(255,50,50,0.15); }
  432. 50% { border-color: rgba(255,50,50,0.6); }
  433. }
  434. @keyframes alertFlash {
  435. 0%, 100% { background: rgba(255,0,0,0.04); }
  436. 50% { background: rgba(255,0,0,0.12); }
  437. }
  438. .alert-modal {
  439. width: 900px; padding: 0; border-radius: 8px; overflow: hidden;
  440. border: 2px solid var(--danger);
  441. box-shadow: 0 0 60px rgba(255,77,79,0.4), 0 0 120px rgba(255,77,79,0.2);
  442. animation: alertModalPulse 2s ease-in-out infinite;
  443. }
  444. @keyframes alertModalPulse {
  445. 0%, 100% { box-shadow: 0 0 40px rgba(255,77,79,0.3), 0 0 80px rgba(255,77,79,0.15); }
  446. 50% { box-shadow: 0 0 80px rgba(255,77,79,0.5), 0 0 160px rgba(255,77,79,0.25); }
  447. }
  448. .alert-header {
  449. background: linear-gradient(135deg, #cc0000, #ff4d4f); padding: 16px 24px;
  450. display: flex; align-items: center; gap: 12px;
  451. }
  452. .alert-header .alert-icon { font-size: 28px; animation: spin 2s linear infinite; }
  453. @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  454. .alert-header .alert-title { font-size: 20px; font-weight: 700; letter-spacing: 4px; color: #fff; }
  455. .alert-body {
  456. background: linear-gradient(180deg, #1a0808, #0d0404); padding: 24px;
  457. display: flex; gap: 20px;
  458. }
  459. .alert-info {
  460. flex: 1; min-width: 0;
  461. }
  462. .alert-video {
  463. width: 320px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px;
  464. }
  465. .alert-video-title {
  466. font-size: 13px; color: var(--danger); font-weight: 600; letter-spacing: 1px;
  467. display: flex; align-items: center; gap: 6px;
  468. }
  469. .alert-video-title::before {
  470. content: ''; width: 6px; height: 6px; border-radius: 50%;
  471. background: var(--danger); box-shadow: 0 0 8px var(--danger);
  472. animation: dotBlink 1.5s ease-in-out infinite;
  473. }
  474. .alert-video-feed {
  475. flex: 1; min-height: 180px; border-radius: 6px; position: relative; overflow: hidden;
  476. background: radial-gradient(ellipse 100px 60px at 20% 25%, rgba(255,77,79,0.15), transparent 70%),
  477. linear-gradient(150deg, rgba(15,5,5,0.95), rgba(8,2,2,0.92));
  478. border: 1px solid rgba(255,77,79,0.3);
  479. box-shadow: inset 0 0 20px rgba(255,77,79,0.1), 0 0 12px rgba(255,77,79,0.08);
  480. }
  481. .alert-video-feed::after {
  482. content: ''; position: absolute; inset: 0; pointer-events: none;
  483. background: repeating-linear-gradient(
  484. to bottom, rgba(255,200,200,0.03) 0, rgba(255,200,200,0.03) 2px, transparent 2px, transparent 5px
  485. );
  486. }
  487. .alert-video-feed .cam-label {
  488. position: absolute; top: 8px; left: 10px; font-size: 11px;
  489. background: rgba(255,0,0,0.4); padding: 2px 10px; border-radius: 2px;
  490. color: #fff; z-index: 1;
  491. display: flex; align-items: center; gap: 4px;
  492. }
  493. .alert-video-feed .cam-label .rec-dot {
  494. width: 6px; height: 6px; border-radius: 50%;
  495. background: #ff4d4f; box-shadow: 0 0 6px #ff4d4f;
  496. animation: dotBlink 1s ease-in-out infinite;
  497. }
  498. .alert-video-feed .cam-placeholder {
  499. position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
  500. font-size: 13px; color: rgba(255,255,255,0.3);
  501. }
  502. .alert-info-row { display: flex; margin-bottom: 10px; font-size: 14px; }
  503. .alert-info-row .a-label { width: 105px; color: var(--text-sub); flex-shrink: 0; }
  504. .alert-info-row .a-value { color: #fff; font-weight: 600; }
  505. .alert-info-row .a-value.danger { color: var(--danger); }
  506. .alert-footer {
  507. padding: 16px 24px; text-align: center;
  508. background: #0d0404; border-top: 1px solid rgba(255,77,79,0.2);
  509. }
  510. .alert-close-btn {
  511. padding: 8px 40px; border-radius: 4px; cursor: pointer;
  512. background: linear-gradient(135deg, #cc0000, #ff4d4f); border: none;
  513. color: #fff; font-size: 14px; font-weight: 600; letter-spacing: 2px;
  514. transition: all 0.3s;
  515. }
  516. .alert-close-btn:hover { box-shadow: 0 0 20px rgba(255,77,79,0.5); }
  517. /* ======================== Decorations ======================== */
  518. .corner-deco {
  519. position: absolute; width: 14px; height: 14px;
  520. border-color: var(--accent); border-style: solid;
  521. opacity: 0.7; z-index: 2;
  522. animation: cornerBreathe 3s ease-in-out infinite;
  523. }
  524. .corner-deco.tl { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
  525. .corner-deco.tr { top: -1px; right: -1px; border-width: 2px 2px 0 0; animation-delay: 0.5s; }
  526. .corner-deco.bl { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; animation-delay: 1s; }
  527. .corner-deco.br { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; animation-delay: 1.5s; }
  528. @keyframes cornerBreathe {
  529. 0%, 100% { opacity: 0.4; box-shadow: 0 0 4px rgba(72,215,255,0.2); }
  530. 50% { opacity: 1; box-shadow: 0 0 12px rgba(72,215,255,0.6), 0 0 24px rgba(72,215,255,0.2); }
  531. }
  532. /* ======================== Background Effects ======================== */
  533. /* Tech grid overlay */
  534. .screen::before {
  535. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  536. background-image:
  537. linear-gradient(rgba(72,180,255,0.04) 1px, transparent 1px),
  538. linear-gradient(90deg, rgba(72,180,255,0.04) 1px, transparent 1px);
  539. background-size: 60px 60px;
  540. mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(0,0,0,0.5) 0%, transparent 100%);
  541. }
  542. /* Ambient floating particles */
  543. .screen::after {
  544. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  545. background:
  546. radial-gradient(1.5px 1.5px at 15% 25%, rgba(72,215,255,0.5), transparent),
  547. radial-gradient(1px 1px at 85% 15%, rgba(72,215,255,0.4), transparent),
  548. radial-gradient(1.5px 1.5px at 45% 80%, rgba(58,123,255,0.4), transparent),
  549. radial-gradient(1px 1px at 70% 55%, rgba(72,215,255,0.3), transparent),
  550. radial-gradient(1.5px 1.5px at 25% 60%, rgba(100,200,255,0.35), transparent),
  551. radial-gradient(1px 1px at 90% 75%, rgba(72,215,255,0.3), transparent);
  552. animation: particleDrift 20s ease-in-out infinite alternate;
  553. }
  554. @keyframes particleDrift {
  555. 0% { transform: translate(0, 0); opacity: 0.6; }
  556. 50% { transform: translate(-8px, 6px); opacity: 1; }
  557. 100% { transform: translate(5px, -4px); opacity: 0.7; }
  558. }
  559. /* ======================== Panel Animations ======================== */
  560. /* Panel border breathing glow */
  561. .panel {
  562. border: 1px solid var(--panel-border);
  563. animation: panelGlow 4s ease-in-out infinite;
  564. transition: box-shadow 0.4s ease;
  565. }
  566. @keyframes panelGlow {
  567. 0%, 100% { box-shadow: inset 0 0 15px rgba(72,180,255,0.03), 0 0 8px rgba(72,180,255,0.05); }
  568. 50% { box-shadow: inset 0 0 20px rgba(72,180,255,0.08), 0 0 16px rgba(72,180,255,0.1); }
  569. }
  570. .panel:hover {
  571. box-shadow: inset 0 0 25px rgba(72,180,255,0.12), 0 0 24px rgba(72,180,255,0.15) !important;
  572. }
  573. /* Panel top scan line */
  574. .panel::after {
  575. content: ''; position: absolute; top: 0; left: -100%; width: 60%; height: 1px;
  576. background: linear-gradient(90deg, transparent, rgba(72,215,255,0.6), rgba(72,215,255,0.8), rgba(72,215,255,0.6), transparent);
  577. animation: panelScan 6s linear infinite;
  578. pointer-events: none; z-index: 3;
  579. }
  580. @keyframes panelScan {
  581. 0% { left: -60%; }
  582. 100% { left: 160%; }
  583. }
  584. /* Stagger scan animation per panel */
  585. .col-left .panel:nth-child(2)::after { animation-delay: 1s; }
  586. .col-left .panel:nth-child(3)::after { animation-delay: 2s; }
  587. .col-center .panel:nth-child(2)::after { animation-delay: 3s; }
  588. .col-right .panel:nth-child(1)::after { animation-delay: 1.5s; }
  589. .col-right .panel:nth-child(2)::after { animation-delay: 3.5s; }
  590. /* ======================== Top Nav Animations ======================== */
  591. /* Nav bottom edge flowing light */
  592. .top-nav::before {
  593. content: ''; position: absolute; bottom: -1px; left: 0; width: 120px; height: 2px;
  594. background: linear-gradient(90deg, transparent, var(--accent), rgba(72,215,255,0.8), transparent);
  595. animation: navFlow 4s linear infinite;
  596. z-index: 101;
  597. }
  598. @keyframes navFlow {
  599. 0% { left: -120px; }
  600. 100% { left: calc(100% + 120px); }
  601. }
  602. /* Title text glow pulse */
  603. .nav-title {
  604. animation: titleGlow 3s ease-in-out infinite;
  605. }
  606. @keyframes titleGlow {
  607. 0%, 100% { filter: drop-shadow(0 0 6px rgba(72,215,255,0.2)); }
  608. 50% { filter: drop-shadow(0 0 16px rgba(72,215,255,0.5)); }
  609. }
  610. /* Clock pulse */
  611. .nav-right .clock {
  612. animation: clockPulse 2s ease-in-out infinite;
  613. }
  614. @keyframes clockPulse {
  615. 0%, 100% { text-shadow: 0 0 6px rgba(72,215,255,0.3); }
  616. 50% { text-shadow: 0 0 14px rgba(72,215,255,0.6), 0 0 28px rgba(72,215,255,0.2); }
  617. }
  618. /* Active tab glow */
  619. .nav-tab.active {
  620. animation: tabGlow 2.5s ease-in-out infinite;
  621. }
  622. @keyframes tabGlow {
  623. 0%, 100% { box-shadow: 0 0 10px rgba(72,215,255,0.15); }
  624. 50% { box-shadow: 0 0 22px rgba(72,215,255,0.35), inset 0 0 10px rgba(72,215,255,0.08); }
  625. }
  626. /* ======================== Data Card Animations ======================== */
  627. /* Status cards breathing */
  628. .status-card {
  629. transition: all 0.3s ease;
  630. animation: cardBreathe 4s ease-in-out infinite;
  631. }
  632. .status-card:nth-child(2) { animation-delay: 1.3s; }
  633. .status-card:nth-child(3) { animation-delay: 2.6s; }
  634. @keyframes cardBreathe {
  635. 0%, 100% { border-color: rgba(72,215,255,0.12); box-shadow: 0 0 0 transparent; }
  636. 50% { border-color: rgba(72,215,255,0.3); box-shadow: 0 0 10px rgba(72,215,255,0.08); }
  637. }
  638. .status-card:hover {
  639. transform: translateY(-2px);
  640. border-color: var(--accent) !important;
  641. box-shadow: 0 4px 16px rgba(72,215,255,0.15) !important;
  642. }
  643. /* Flip digit glow */
  644. .flip-digit {
  645. animation: digitGlow 2s ease-in-out infinite alternate;
  646. transition: all 0.3s ease;
  647. }
  648. @keyframes digitGlow {
  649. 0% { box-shadow: 0 0 4px rgba(72,215,255,0.1); text-shadow: 0 0 4px rgba(72,215,255,0.3); }
  650. 100% { box-shadow: 0 0 10px rgba(72,215,255,0.25); text-shadow: 0 0 12px rgba(72,215,255,0.5); }
  651. }
  652. /* Device type item hover */
  653. .device-type-item {
  654. transition: all 0.3s ease;
  655. }
  656. .device-type-item:hover {
  657. border-color: rgba(72,215,255,0.35);
  658. background: rgba(72,215,255,0.08);
  659. box-shadow: 0 0 12px rgba(72,215,255,0.1);
  660. transform: translateX(3px);
  661. }
  662. /* Equipment status cards */
  663. .equip-status-card, .equip-mid-item, .device-stat-card {
  664. transition: all 0.3s ease;
  665. }
  666. .equip-status-card:hover, .equip-mid-item:hover, .device-stat-card:hover {
  667. border-color: rgba(72,215,255,0.3);
  668. box-shadow: 0 0 12px rgba(72,215,255,0.1);
  669. transform: translateY(-2px);
  670. }
  671. /* ======================== Sensor & Warning Animations ======================== */
  672. /* Sensor item enter animation */
  673. .sensor-item {
  674. animation: sensorFadeIn 0.5s ease-out;
  675. }
  676. @keyframes sensorFadeIn {
  677. from { opacity: 0; transform: translateY(8px); }
  678. to { opacity: 1; transform: translateY(0); }
  679. }
  680. /* Warning item left border pulse */
  681. .warning-item {
  682. transition: all 0.3s ease;
  683. animation: warnBorderPulse 3s ease-in-out infinite;
  684. }
  685. @keyframes warnBorderPulse {
  686. 0%, 100% { border-left-color: rgba(255,176,32,0.6); }
  687. 50% { border-left-color: var(--warn); box-shadow: -2px 0 8px rgba(255,176,32,0.15); }
  688. }
  689. .warning-item:hover {
  690. background: rgba(255,176,32,0.08);
  691. transform: translateX(3px);
  692. }
  693. /* Warning count glow */
  694. .warning-count {
  695. animation: warnCountGlow 2s ease-in-out infinite;
  696. }
  697. @keyframes warnCountGlow {
  698. 0%, 100% { text-shadow: 0 0 8px rgba(255,176,32,0.3); }
  699. 50% { text-shadow: 0 0 20px rgba(255,176,32,0.6), 0 0 40px rgba(255,176,32,0.2); }
  700. }
  701. /* ======================== Camera Frame Animations ======================== */
  702. /* Corner glow breathing */
  703. .cam-corner::before, .cam-corner::after {
  704. animation: camCornerGlow 2.5s ease-in-out infinite;
  705. }
  706. .cam-corner.c-tr::before, .cam-corner.c-tr::after { animation-delay: 0.6s; }
  707. .cam-corner.c-bl::before, .cam-corner.c-bl::after { animation-delay: 1.2s; }
  708. .cam-corner.c-br::before, .cam-corner.c-br::after { animation-delay: 1.8s; }
  709. @keyframes camCornerGlow {
  710. 0%, 100% { opacity: 0.5; box-shadow: 0 0 4px var(--accent); }
  711. 50% { opacity: 1; box-shadow: 0 0 10px var(--accent), 0 0 20px rgba(72,215,255,0.3); }
  712. }
  713. /* Camera frame border breathing */
  714. .cam-frame {
  715. background:
  716. linear-gradient(rgba(4,14,35,0.96), rgba(4,14,35,0.96)) padding-box,
  717. linear-gradient(135deg,
  718. rgba(72,215,255,0.2) 0%,
  719. rgba(72,215,255,0.6) 18%,
  720. rgba(130,235,255,0.9) 34%,
  721. rgba(72,215,255,0.45) 52%,
  722. rgba(58,123,255,0.28) 72%,
  723. rgba(72,215,255,0.18) 100%
  724. ) border-box;
  725. background-size: 100% 100%, 240% 240%;
  726. animation: camFrameBreathe 4s ease-in-out infinite, camFrameBorderFlow 3.9s linear infinite;
  727. }
  728. @keyframes camFrameBreathe {
  729. 0%, 100% { box-shadow: inset 0 0 14px rgba(72,215,255,0.15), 0 0 14px rgba(58,123,255,0.1); }
  730. 50% { box-shadow: inset 0 0 22px rgba(72,215,255,0.3), 0 0 24px rgba(58,123,255,0.2); }
  731. }
  732. @keyframes camFrameBorderFlow {
  733. 0% { background-position: 0 0, 0% 50%; }
  734. 100% { background-position: 0 0, 220% 50%; }
  735. }
  736. /* Camera online dot blink */
  737. .cam-inner .cam-dot {
  738. animation: dotBlink 2s ease-in-out infinite;
  739. }
  740. @keyframes dotBlink {
  741. 0%, 100% { opacity: 1; box-shadow: 0 0 4px #18ff6f; }
  742. 50% { opacity: 0.4; box-shadow: 0 0 8px #18ff6f, 0 0 16px rgba(24,255,111,0.3); }
  743. }
  744. /* AI badge pulse */
  745. .ai-badge {
  746. animation: aiBadgePulse 1.5s ease-in-out infinite;
  747. }
  748. @keyframes aiBadgePulse {
  749. 0%, 100% { box-shadow: 0 0 8px rgba(255,176,32,0.3); }
  750. 50% { box-shadow: 0 0 16px rgba(255,176,32,0.6), 0 0 32px rgba(255,176,32,0.2); transform: scale(1.05); }
  751. }
  752. /* AI camera frame special breathing */
  753. .video-cell.ai-cam .cam-frame {
  754. background:
  755. linear-gradient(rgba(4,14,35,0.96), rgba(4,14,35,0.96)) padding-box,
  756. linear-gradient(135deg,
  757. rgba(255,176,32,0.22) 0%,
  758. rgba(255,176,32,0.62) 20%,
  759. rgba(255,210,120,0.9) 36%,
  760. rgba(255,176,32,0.45) 58%,
  761. rgba(255,120,0,0.25) 78%,
  762. rgba(255,176,32,0.22) 100%
  763. ) border-box;
  764. background-size: 100% 100%, 240% 240%;
  765. animation: aiFrameBreathe 3s ease-in-out infinite, aiFrameBorderFlow 2.9s linear infinite;
  766. }
  767. @keyframes aiFrameBreathe {
  768. 0%, 100% { box-shadow: inset 0 0 14px rgba(255,176,32,0.15), 0 0 14px rgba(255,140,0,0.1); }
  769. 50% { box-shadow: inset 0 0 24px rgba(255,176,32,0.3), 0 0 28px rgba(255,140,0,0.2); }
  770. }
  771. @keyframes aiFrameBorderFlow {
  772. 0% { background-position: 0 0, 0% 50%; }
  773. 100% { background-position: 0 0, 240% 50%; }
  774. }
  775. /* ======================== Page Button Animations ======================== */
  776. .page-btn {
  777. transition: all 0.3s ease;
  778. position: relative; overflow: hidden;
  779. }
  780. .page-btn::after {
  781. content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
  782. background: linear-gradient(90deg, transparent, rgba(72,215,255,0.15), transparent);
  783. transition: left 0.5s ease;
  784. }
  785. .page-btn:hover::after { left: 100%; }
  786. .page-btn:hover {
  787. transform: translateY(-1px);
  788. box-shadow: 0 4px 12px rgba(72,215,255,0.15);
  789. }
  790. /* ======================== Panel Title Glow ======================== */
  791. .panel-title::before {
  792. animation: titleBarGlow 2.5s ease-in-out infinite;
  793. }
  794. @keyframes titleBarGlow {
  795. 0%, 100% { box-shadow: 0 0 6px var(--accent); opacity: 0.7; }
  796. 50% { box-shadow: 0 0 14px var(--accent), 0 0 24px rgba(72,215,255,0.3); opacity: 1; }
  797. }
  798. /* ======================== Alarm Icon Enhanced ======================== */
  799. .alarm-icon {
  800. animation: alarmIconShake 0.5s ease-in-out infinite alternate;
  801. }
  802. @keyframes alarmIconShake {
  803. 0% { transform: translateX(-1px) rotate(-5deg); }
  804. 100% { transform: translateX(1px) rotate(5deg); }
  805. }
  806. /* ================================================================ */
  807. /* ============= VIDEO MONITORING PAGE ANIMATIONS ================= */
  808. /* ================================================================ */
  809. /* --- Video page entrance fade-in --- */
  810. #page-video.active {
  811. animation: videoPageFadeIn 0.6s ease-out;
  812. }
  813. @keyframes videoPageFadeIn {
  814. from { opacity: 0; transform: translateY(8px); }
  815. to { opacity: 1; transform: translateY(0); }
  816. }
  817. /* --- Video-left sidebar panel effects --- */
  818. .video-left {
  819. position: relative;
  820. }
  821. /* Sidebar vertical scan line */
  822. .video-left::after {
  823. content: ''; position: absolute; left: 0; top: -100%; width: 1px; height: 60%;
  824. background: linear-gradient(180deg, transparent, rgba(72,215,255,0.7), rgba(72,215,255,0.9), rgba(72,215,255,0.7), transparent);
  825. animation: sidebarScan 5s linear infinite;
  826. pointer-events: none; z-index: 3;
  827. }
  828. @keyframes sidebarScan {
  829. 0% { top: -60%; }
  830. 100% { top: 160%; }
  831. }
  832. /* Sidebar left edge glow */
  833. .video-left::before {
  834. content: ''; position: absolute; left: 0; top: 10%; bottom: 10%; width: 2px;
  835. background: linear-gradient(180deg, transparent, var(--accent), transparent);
  836. opacity: 0.4;
  837. animation: sidebarEdgeGlow 3s ease-in-out infinite;
  838. z-index: 3; pointer-events: none;
  839. }
  840. @keyframes sidebarEdgeGlow {
  841. 0%, 100% { opacity: 0.25; box-shadow: 0 0 4px var(--accent); }
  842. 50% { opacity: 0.7; box-shadow: 0 0 12px var(--accent), 0 0 24px rgba(72,215,255,0.2); }
  843. }
  844. /* --- Search input effects --- */
  845. .video-search {
  846. transition: all 0.3s ease;
  847. position: relative;
  848. }
  849. .video-search:focus {
  850. border-color: var(--accent);
  851. box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
  852. background: rgba(72,215,255,0.1);
  853. }
  854. /* Search input idle glow */
  855. .video-search {
  856. animation: searchIdleGlow 4s ease-in-out infinite;
  857. }
  858. @keyframes searchIdleGlow {
  859. 0%, 100% { box-shadow: 0 0 0 transparent; }
  860. 50% { box-shadow: 0 0 6px rgba(72,215,255,0.1); }
  861. }
  862. /* --- Filter dropdown effects --- */
  863. .video-filter {
  864. transition: all 0.3s ease;
  865. animation: filterIdleGlow 4s ease-in-out infinite 1s;
  866. }
  867. @keyframes filterIdleGlow {
  868. 0%, 100% { box-shadow: 0 0 0 transparent; }
  869. 50% { box-shadow: 0 0 6px rgba(72,215,255,0.1); }
  870. }
  871. .video-filter:focus {
  872. border-color: var(--accent);
  873. box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
  874. background: rgba(72,215,255,0.1);
  875. }
  876. /* --- Tree node animations --- */
  877. .tree-node-content {
  878. transition: all 0.3s ease;
  879. position: relative;
  880. overflow: hidden;
  881. }
  882. /* Tree node hover light sweep */
  883. .tree-node-content::after {
  884. content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
  885. background: linear-gradient(90deg, transparent, rgba(72,215,255,0.08), transparent);
  886. transition: left 0.6s ease;
  887. pointer-events: none;
  888. }
  889. .tree-node-content:hover::after {
  890. left: 100%;
  891. }
  892. .tree-node-content:hover {
  893. box-shadow: 0 0 8px rgba(72,215,255,0.08);
  894. text-shadow: 0 0 6px rgba(72,215,255,0.2);
  895. }
  896. /* Tree selected node glow pulse */
  897. .tree-node-content.selected {
  898. animation: treeSelectedGlow 2.5s ease-in-out infinite;
  899. border-left: 2px solid var(--accent);
  900. }
  901. @keyframes treeSelectedGlow {
  902. 0%, 100% { box-shadow: 0 0 6px rgba(72,215,255,0.1); background: rgba(72,215,255,0.12); }
  903. 50% { box-shadow: 0 0 14px rgba(72,215,255,0.2); background: rgba(72,215,255,0.2); }
  904. }
  905. /* Tree children expand animation */
  906. .tree-children.open {
  907. animation: treeExpand 0.35s ease-out;
  908. }
  909. @keyframes treeExpand {
  910. from { opacity: 0; max-height: 0; transform: translateY(-6px); }
  911. to { opacity: 1; max-height: 600px; transform: translateY(0); }
  912. }
  913. /* Tree arrow rotation glow */
  914. .tree-arrow {
  915. transition: all 0.3s ease;
  916. }
  917. .tree-arrow.expanded {
  918. color: var(--accent);
  919. text-shadow: 0 0 6px rgba(72,215,255,0.4);
  920. }
  921. /* --- Video breadcrumb flowing light --- */
  922. .video-breadcrumb {
  923. position: relative; overflow: hidden;
  924. transition: all 0.3s ease;
  925. }
  926. .video-breadcrumb::before {
  927. content: ''; position: absolute; bottom: 0; left: -80px; width: 80px; height: 1px;
  928. background: linear-gradient(90deg, transparent, var(--accent), rgba(72,215,255,0.8), transparent);
  929. animation: breadcrumbFlow 5s linear infinite;
  930. pointer-events: none;
  931. }
  932. @keyframes breadcrumbFlow {
  933. 0% { left: -80px; }
  934. 100% { left: calc(100% + 80px); }
  935. }
  936. /* Breadcrumb text subtle glow */
  937. .video-breadcrumb {
  938. animation: breadcrumbTextGlow 3s ease-in-out infinite;
  939. }
  940. @keyframes breadcrumbTextGlow {
  941. 0%, 100% { text-shadow: none; color: var(--text-sub); }
  942. 50% { text-shadow: 0 0 8px rgba(72,215,255,0.2); color: #a0d4e8; }
  943. }
  944. /* --- Video grid container effects --- */
  945. .video-grid {
  946. position: relative;
  947. border: 1px solid rgba(72,215,255,0.08);
  948. border-radius: 6px;
  949. padding: 6px;
  950. animation: videoGridBreathe 5s ease-in-out infinite;
  951. }
  952. @keyframes videoGridBreathe {
  953. 0%, 100% { border-color: rgba(72,215,255,0.06); box-shadow: 0 0 0 transparent; }
  954. 50% { border-color: rgba(72,215,255,0.18); box-shadow: 0 0 16px rgba(72,215,255,0.06), inset 0 0 20px rgba(72,215,255,0.03); }
  955. }
  956. /* Grid background tech pattern */
  957. .video-grid::before {
  958. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0; border-radius: 6px;
  959. background: repeating-linear-gradient(
  960. 0deg, rgba(72,215,255,0.015) 0, rgba(72,215,255,0.015) 1px, transparent 1px, transparent 60px
  961. ), repeating-linear-gradient(
  962. 90deg, rgba(72,215,255,0.015) 0, rgba(72,215,255,0.015) 1px, transparent 1px, transparent 60px
  963. );
  964. }
  965. /* Grid horizontal scan line */
  966. .video-grid::after {
  967. content: ''; position: absolute; left: 0; top: -100%; width: 100%; height: 2px;
  968. background: linear-gradient(90deg, transparent 5%, rgba(72,215,255,0.15) 20%, rgba(72,215,255,0.35) 50%, rgba(72,215,255,0.15) 80%, transparent 95%);
  969. animation: gridScanLine 8s linear infinite;
  970. pointer-events: none; z-index: 5;
  971. }
  972. @keyframes gridScanLine {
  973. 0% { top: -2px; }
  974. 100% { top: 100%; }
  975. }
  976. /* --- Camera cell staggered entrance --- */
  977. .video-cell {
  978. animation: camCellEntrance 0.5s ease-out both;
  979. }
  980. .video-cell:nth-child(1) { animation-delay: 0.05s; }
  981. .video-cell:nth-child(2) { animation-delay: 0.1s; }
  982. .video-cell:nth-child(3) { animation-delay: 0.15s; }
  983. .video-cell:nth-child(4) { animation-delay: 0.2s; }
  984. .video-cell:nth-child(5) { animation-delay: 0.25s; }
  985. .video-cell:nth-child(6) { animation-delay: 0.3s; }
  986. .video-cell:nth-child(7) { animation-delay: 0.35s; }
  987. .video-cell:nth-child(8) { animation-delay: 0.4s; }
  988. .video-cell:nth-child(9) { animation-delay: 0.45s; }
  989. @keyframes camCellEntrance {
  990. from { opacity: 0; transform: scale(0.92); }
  991. to { opacity: 1; transform: scale(1); }
  992. }
  993. /* --- Camera inner moving scanline --- */
  994. .cam-inner::before {
  995. content: ''; position: absolute; left: 0; width: 100%; height: 1px;
  996. background: linear-gradient(90deg, transparent 10%, rgba(72,215,255,0.12) 30%, rgba(72,215,255,0.22) 50%, rgba(72,215,255,0.12) 70%, transparent 90%);
  997. animation: camInnerScan 6s linear infinite;
  998. pointer-events: none; z-index: 2;
  999. }
  1000. @keyframes camInnerScan {
  1001. 0% { top: 0; }
  1002. 100% { top: 100%; }
  1003. }
  1004. /* AI cam inner has orange scanline */
  1005. .video-cell.ai-cam .cam-inner::before {
  1006. background: linear-gradient(90deg, transparent 10%, rgba(255,176,32,0.12) 30%, rgba(255,176,32,0.25) 50%, rgba(255,176,32,0.12) 70%, transparent 90%);
  1007. }
  1008. /* --- Camera frame hover effect --- */
  1009. .cam-frame {
  1010. transition: box-shadow 0.3s ease, transform 0.3s ease;
  1011. }
  1012. .video-cell:hover .cam-frame {
  1013. box-shadow: inset 0 0 28px rgba(72,215,255,0.35), 0 0 32px rgba(58,123,255,0.25) !important;
  1014. transform: scale(1.01);
  1015. }
  1016. .video-cell.ai-cam:hover .cam-frame {
  1017. box-shadow: inset 0 0 28px rgba(255,176,32,0.35), 0 0 32px rgba(255,140,0,0.25) !important;
  1018. }
  1019. /* --- Camera label typing glow --- */
  1020. .cam-label {
  1021. animation: camLabelGlow 3s ease-in-out infinite;
  1022. }
  1023. @keyframes camLabelGlow {
  1024. 0%, 100% { box-shadow: none; }
  1025. 50% { box-shadow: 0 0 8px rgba(0,0,0,0.3), 0 0 4px rgba(72,215,255,0.15); }
  1026. }
  1027. /* --- Video pagination effects --- */
  1028. .video-pagination {
  1029. position: relative;
  1030. }
  1031. .video-pagination::before {
  1032. content: ''; position: absolute; top: 0; left: 20%; right: 20%; height: 1px;
  1033. background: linear-gradient(90deg, transparent, rgba(72,215,255,0.2), transparent);
  1034. animation: paginationLineGlow 3s ease-in-out infinite;
  1035. }
  1036. @keyframes paginationLineGlow {
  1037. 0%, 100% { opacity: 0.3; }
  1038. 50% { opacity: 1; }
  1039. }
  1040. .page-info {
  1041. animation: pageInfoGlow 3s ease-in-out infinite;
  1042. }
  1043. @keyframes pageInfoGlow {
  1044. 0%, 100% { text-shadow: none; }
  1045. 50% { text-shadow: 0 0 8px rgba(72,215,255,0.3); color: #a0d4e8; }
  1046. }
  1047. /* Page button idle subtle pulse */
  1048. .page-btn {
  1049. animation: pageBtnPulse 4s ease-in-out infinite;
  1050. }
  1051. .page-btn:nth-child(3) { animation-delay: 2s; }
  1052. @keyframes pageBtnPulse {
  1053. 0%, 100% { border-color: var(--panel-border); }
  1054. 50% { border-color: rgba(72,215,255,0.4); box-shadow: 0 0 8px rgba(72,215,255,0.08); }
  1055. }
  1056. /* --- Video-right background ambient particles --- */
  1057. .video-right {
  1058. position: relative;
  1059. }
  1060. .video-right::before {
  1061. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  1062. background-image:
  1063. radial-gradient(1px 1px at 10% 20%, rgba(72,215,255,0.3) 50%, transparent 100%),
  1064. radial-gradient(1px 1px at 30% 65%, rgba(72,215,255,0.25) 50%, transparent 100%),
  1065. radial-gradient(1.5px 1.5px at 55% 15%, rgba(72,215,255,0.2) 50%, transparent 100%),
  1066. radial-gradient(1px 1px at 75% 80%, rgba(72,215,255,0.3) 50%, transparent 100%),
  1067. radial-gradient(1px 1px at 90% 40%, rgba(72,215,255,0.25) 50%, transparent 100%),
  1068. radial-gradient(1.5px 1.5px at 45% 90%, rgba(72,215,255,0.2) 50%, transparent 100%);
  1069. animation: videoParticlesDrift 12s ease-in-out infinite alternate;
  1070. }
  1071. @keyframes videoParticlesDrift {
  1072. 0% { opacity: 0.3; transform: translateY(0); }
  1073. 50% { opacity: 0.7; }
  1074. 100% { opacity: 0.3; transform: translateY(-10px); }
  1075. }
  1076. /* --- Tree container scroll bar glow --- */
  1077. .tree-container::-webkit-scrollbar-thumb {
  1078. box-shadow: 0 0 4px rgba(72,215,255,0.2);
  1079. }
  1080. .tree-container::-webkit-scrollbar-thumb:hover {
  1081. background: var(--accent);
  1082. box-shadow: 0 0 8px rgba(72,215,255,0.4);
  1083. }
  1084. /* ======================== Chemical Inventory Stats ======================== */
  1085. .chem-panel-body {
  1086. flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
  1087. }
  1088. .chem-stats-grid {
  1089. width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 4px 0;
  1090. }
  1091. .chem-stat-item {
  1092. display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  1093. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
  1094. border-radius: 4px; transition: all 0.3s;
  1095. }
  1096. .chem-stat-item:hover {
  1097. border-color: rgba(72,215,255,0.3); box-shadow: 0 0 10px rgba(72,215,255,0.08);
  1098. transform: translateY(-1px);
  1099. }
  1100. .chem-ring-wrap {
  1101. width: 48px; height: 48px; flex-shrink: 0; position: relative;
  1102. }
  1103. .chem-ring-wrap svg { width: 100%; height: 100%; }
  1104. .chem-ring-bg { fill: none; stroke: rgba(72,215,255,0.1); stroke-width: 3; }
  1105. .chem-ring-fg { fill: none; stroke-width: 3; stroke-linecap: round; transition: stroke-dashoffset 1s ease; }
  1106. .chem-ring-icon { font-size: 16px; text-anchor: middle; dominant-baseline: central; }
  1107. .chem-ring-pct { font-size: 10px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
  1108. .chem-stat-info { min-width: 0; flex: 1; }
  1109. .chem-stat-value { font-size: 18px; font-weight: 700; line-height: 1.2; }
  1110. .chem-stat-label { font-size: 11px; color: var(--text-sub); margin-top: 2px; white-space: nowrap; }
  1111. .chem-stat-pct { font-size: 11px; margin-top: 2px; }
  1112. /* Warning item chemical violation style */
  1113. .warning-item.chem-violation {
  1114. border-left-color: #a78bfa;
  1115. background: rgba(167,139,250,0.04);
  1116. }
  1117. .warning-item.chem-violation .w-sensor {
  1118. color: #a78bfa;
  1119. }
  1120. @keyframes chemViolationPulse {
  1121. 0%, 100% { border-left-color: rgba(167,139,250,0.6); }
  1122. 50% { border-left-color: #a78bfa; box-shadow: -2px 0 8px rgba(167,139,250,0.15); }
  1123. }
  1124. .warning-item.chem-violation {
  1125. animation: chemViolationPulse 3s ease-in-out infinite;
  1126. }
  1127. /* ======================== Evacuation Modal ======================== */
  1128. .evac-overlay {
  1129. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  1130. background: rgba(0,0,0,0.85); z-index: 10000;
  1131. display: none; align-items: center; justify-content: center;
  1132. }
  1133. .evac-overlay.show { display: flex; }
  1134. .evac-modal {
  1135. width: 1100px; height: 680px; border-radius: 8px; overflow: hidden;
  1136. border: 1px solid rgba(72,180,255,0.3);
  1137. background: linear-gradient(180deg, #0a1e3d, #061228);
  1138. box-shadow: 0 0 60px rgba(72,180,255,0.15);
  1139. display: flex; flex-direction: column;
  1140. }
  1141. .evac-header {
  1142. display: flex; align-items: center; justify-content: space-between;
  1143. padding: 14px 24px; border-bottom: 1px solid rgba(72,180,255,0.2);
  1144. }
  1145. .evac-header-left { display: flex; align-items: center; gap: 10px; }
  1146. .evac-header-left .evac-icon { font-size: 22px; }
  1147. .evac-header-left .evac-title { font-size: 20px; font-weight: 700; letter-spacing: 4px; color: #fff; }
  1148. .evac-close {
  1149. width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  1150. background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15);
  1151. color: #fff; font-size: 18px; display: flex; align-items: center; justify-content: center;
  1152. transition: all 0.3s;
  1153. }
  1154. .evac-close:hover { background: rgba(255,77,79,0.3); border-color: var(--danger); }
  1155. .evac-body {
  1156. flex: 1; display: flex; min-height: 0; padding: 12px 16px; gap: 14px;
  1157. }
  1158. .evac-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
  1159. .evac-legend {
  1160. display: flex; align-items: center; gap: 20px; justify-content: flex-end;
  1161. font-size: 12px; color: var(--text-sub); margin-bottom: 6px;
  1162. }
  1163. .evac-legend-item { display: flex; align-items: center; gap: 6px; }
  1164. .evac-legend-line { width: 22px; height: 3px; border-radius: 2px; }
  1165. .evac-floorplan {
  1166. flex: 1; border: 1px solid rgba(72,180,255,0.15); border-radius: 6px;
  1167. position: relative; overflow: hidden; background: rgba(6,22,56,0.6);
  1168. }
  1169. .evac-floorplan svg { width: 100%; height: 100%; }
  1170. .evac-alert-card {
  1171. position: absolute; left: 14px; bottom: 14px;
  1172. background: rgba(20,5,5,0.92); border: 1px solid rgba(255,77,79,0.4);
  1173. border-radius: 6px; padding: 10px 14px; max-width: 240px; z-index: 2;
  1174. }
  1175. .evac-alert-card .eac-header {
  1176. display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; gap: 8px;
  1177. }
  1178. .evac-alert-card .eac-time { color: var(--text-sub); font-size: 11px; display: flex; align-items: center; gap: 4px; }
  1179. .evac-alert-card .eac-time .eac-icon { color: var(--danger); }
  1180. .evac-alert-card .eac-close-btn { cursor: pointer; color: var(--text-sub); font-size: 14px; background: none; border: none; }
  1181. .evac-alert-card .eac-badge {
  1182. display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 3px;
  1183. background: rgba(255,77,79,0.2); color: var(--danger); border: 1px solid rgba(255,77,79,0.3);
  1184. margin-bottom: 4px;
  1185. }
  1186. .evac-alert-card .eac-desc { color: #fff; font-size: 12px; }
  1187. .evac-sidebar {
  1188. width: 190px; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0;
  1189. }
  1190. .evac-sidebar-block {
  1191. background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
  1192. border-radius: 4px; padding: 10px; flex: 1; display: flex; flex-direction: column;
  1193. }
  1194. .evac-sidebar-title { font-size: 11px; color: var(--text-sub); margin-bottom: 4px; }
  1195. .evac-sidebar-label { font-size: 13px; color: #fff; font-weight: 600; margin-bottom: 6px; }
  1196. .evac-video-placeholder {
  1197. flex: 1; min-height: 60px; border-radius: 4px;
  1198. background: rgba(6,22,56,0.8); border: 1px solid rgba(72,180,255,0.1);
  1199. display: flex; align-items: center; justify-content: center;
  1200. font-size: 11px; color: var(--text-sub); opacity: 0.5;
  1201. }
  1202. .evac-footer {
  1203. border-top: 1px solid rgba(72,180,255,0.15); padding: 12px 20px;
  1204. display: flex; gap: 14px; align-items: flex-end;
  1205. }
  1206. .evac-metrics { display: flex; flex-direction: column; gap: 5px; min-width: 180px; }
  1207. .evac-metric-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
  1208. .evac-metric-label { color: var(--text-sub); }
  1209. .evac-metric-value { color: var(--danger); font-weight: 600; }
  1210. .evac-broadcast { flex: 1; display: flex; flex-direction: column; gap: 6px; }
  1211. .evac-broadcast-header { display: flex; align-items: center; justify-content: space-between; }
  1212. .evac-broadcast-title { font-size: 13px; color: #fff; display: flex; align-items: center; gap: 6px; }
  1213. .evac-broadcast-hint { font-size: 11px; color: var(--text-sub); }
  1214. .evac-speakers { display: flex; gap: 6px; }
  1215. .evac-speaker-btn {
  1216. flex: 1; padding: 7px 10px; border-radius: 4px; text-align: center;
  1217. font-size: 11px; cursor: pointer; transition: all 0.3s;
  1218. background: linear-gradient(135deg, rgba(72,215,255,0.15), rgba(58,123,255,0.1));
  1219. border: 1px solid rgba(72,215,255,0.3); color: var(--accent);
  1220. }
  1221. .evac-speaker-btn:hover { background: linear-gradient(135deg, rgba(72,215,255,0.25), rgba(58,123,255,0.2)); }
  1222. .evac-broadcast-input { display: flex; gap: 6px; }
  1223. .evac-broadcast-input input {
  1224. flex: 1; padding: 7px 12px; border-radius: 4px;
  1225. background: rgba(72,215,255,0.06); border: 1px solid rgba(72,180,255,0.2);
  1226. color: var(--text-main); font-size: 12px; outline: none;
  1227. }
  1228. .evac-broadcast-input input::placeholder { color: var(--text-sub); }
  1229. .evac-broadcast-input button {
  1230. padding: 7px 16px; border-radius: 4px; cursor: pointer;
  1231. background: rgba(72,215,255,0.1); border: 1px solid rgba(72,215,255,0.3);
  1232. color: var(--accent); font-size: 12px; transition: all 0.3s;
  1233. }
  1234. .evac-broadcast-input button:hover { background: rgba(72,215,255,0.2); }
  1235. .evac-actions { display: flex; flex-direction: column; gap: 8px; min-width: 110px; }
  1236. .evac-btn-later {
  1237. padding: 10px 20px; border-radius: 4px; cursor: pointer;
  1238. background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.2);
  1239. color: var(--text-main); font-size: 13px; text-align: center; transition: all 0.3s;
  1240. }
  1241. .evac-btn-later:hover { background: rgba(72,215,255,0.12); }
  1242. .evac-btn-exec {
  1243. padding: 10px 20px; border-radius: 4px; cursor: pointer;
  1244. background: linear-gradient(135deg, rgba(255,0,0,0.15), rgba(200,0,0,0.1));
  1245. border: 1px solid rgba(255,77,79,0.5); color: var(--danger);
  1246. font-size: 13px; font-weight: 600; text-align: center; transition: all 0.3s;
  1247. }
  1248. .evac-btn-exec:hover { background: linear-gradient(135deg, rgba(255,0,0,0.25), rgba(200,0,0,0.2)); box-shadow: 0 0 16px rgba(255,77,79,0.3); }
  1249. .evac-route-arrow { opacity: 0; transition: opacity 0.5s; }
  1250. .evac-route-arrow.visible { animation: evacArrowFlow 1.5s ease-in-out infinite; }
  1251. @keyframes evacArrowFlow {
  1252. 0% { opacity: 0.4; }
  1253. 50% { opacity: 1; }
  1254. 100% { opacity: 0.4; }
  1255. }
  1256. .evac-room-group { cursor: pointer; }
  1257. .evac-room-group:hover rect { stroke: var(--accent); stroke-width: 2; fill: rgba(72,215,255,0.12); }
  1258. .evac-room-group.selected rect { stroke: var(--accent); stroke-width: 2; fill: rgba(72,215,255,0.15); }
  1259. .evac-room-group.alarm rect { stroke: rgba(255,77,79,0.7) !important; stroke-width: 2 !important; fill: rgba(255,77,79,0.1) !important; animation: evacRoomAlarm 1s ease-in-out infinite; }
  1260. @keyframes evacRoomAlarm {
  1261. 0%, 100% { stroke: rgba(255,77,79,0.5); fill: rgba(255,77,79,0.06); }
  1262. 50% { stroke: rgba(255,77,79,0.9); fill: rgba(255,77,79,0.15); }
  1263. }
  1264. </style>
  1265. </head>
  1266. <body>
  1267. <div class="viewport">
  1268. <div class="screen" id="mainScreen">
  1269. <!-- ===================== TOP NAV ===================== -->
  1270. <div class="top-nav">
  1271. <div class="nav-title">中国安全生产科学研究院实验室安全智慧化管控中心</div>
  1272. <div class="nav-tabs">
  1273. <div class="nav-tab active" onclick="switchPage('lab')">实验室情况</div>
  1274. <div class="nav-tab" onclick="switchPage('video')">视频监控</div>
  1275. </div>
  1276. <div class="nav-right">
  1277. <span class="weather">☁ 北京 · 晴 18°C</span>
  1278. <span id="weekday"></span>
  1279. <span class="clock" id="clock"></span>
  1280. </div>
  1281. </div>
  1282. <!-- ===================== PAGE: 实验室情况 ===================== -->
  1283. <div class="page active" id="page-lab">
  1284. <!-- LEFT COLUMN -->
  1285. <div class="col-left">
  1286. <!-- 实验室基本情况统计 -->
  1287. <div class="panel" style="height: 260px;">
  1288. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1289. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1290. <div class="panel-title">实验室基本情况统计</div>
  1291. <div id="chartLabOverview" style="width:100%;height:150px;"></div>
  1292. <div class="status-cards">
  1293. <div class="status-card using">
  1294. <div class="label">使用</div><div class="value">20<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1295. </div>
  1296. <div class="status-card error">
  1297. <div class="label">异常</div><div class="value">3<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1298. </div>
  1299. <div class="status-card idle">
  1300. <div class="label">空闲</div><div class="value">35<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1301. </div>
  1302. </div>
  1303. </div>
  1304. <!-- 实验室安全分级统计 -->
  1305. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1306. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1307. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1308. <div class="panel-title">实验室安全分级统计</div>
  1309. <div id="chartGradeBar" style="width:100%;flex:1;min-height:0;"></div>
  1310. </div>
  1311. <!-- 实验室进入人数统计及走势 -->
  1312. <div class="panel" style="height: 260px;">
  1313. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1314. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1315. <div class="panel-title">实验室进入人数统计及走势</div>
  1316. <div class="flip-counter-row">
  1317. <div class="flip-group">
  1318. <div class="flip-label">今日总进入人数</div>
  1319. <div class="flip-digits" id="flipTotal"></div>
  1320. </div>
  1321. <div class="flip-group">
  1322. <div class="flip-label">当前正在实验人数</div>
  1323. <div class="flip-digits" id="flipCurrent"></div>
  1324. </div>
  1325. </div>
  1326. <div id="chartPeopleLine" style="width:100%;height:130px;"></div>
  1327. </div>
  1328. </div>
  1329. <!-- CENTER COLUMN -->
  1330. <div class="col-center">
  1331. <!-- 实验环境安全智能感知 -->
  1332. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1333. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1334. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1335. <div class="panel-title">实验环境安全智能感知</div>
  1336. <div class="sensor-scroll-wrap">
  1337. <div class="sensor-scroll-inner" id="sensorList"></div>
  1338. </div>
  1339. </div>
  1340. <!-- 实验室实时风险预警 -->
  1341. <div class="panel" style="height: 320px; display:flex; flex-direction:column;">
  1342. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1343. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1344. <div class="panel-title">实验室实时风险预警</div>
  1345. <div class="warning-header">
  1346. <div class="warning-count" id="warningCount">12</div>
  1347. <div class="warning-count-label">本月预警响应总数</div>
  1348. </div>
  1349. <div class="warning-scroll-wrap">
  1350. <div class="warning-scroll-inner" id="warningList"></div>
  1351. </div>
  1352. </div>
  1353. </div>
  1354. <!-- RIGHT COLUMN -->
  1355. <div class="col-right">
  1356. <!-- 智能环境感知应用设备统计 -->
  1357. <div class="panel" style="flex:4;display:flex;flex-direction:column;">
  1358. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1359. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1360. <div class="panel-title">智能环境感知应用设备统计</div>
  1361. <div class="device-top">
  1362. <div class="device-stat-card">
  1363. <div class="ds-label">在线设备</div><div class="ds-value">186</div>
  1364. </div>
  1365. <div class="device-stat-card offline">
  1366. <div class="ds-label">离线设备</div><div class="ds-value">14</div>
  1367. </div>
  1368. </div>
  1369. <div class="device-bottom">
  1370. <div class="device-gauge-wrap" id="chartGauge"></div>
  1371. <div class="device-type-list">
  1372. <div class="device-type-item"><span class="dt-name">电子信息铭牌</span><span class="dt-value">58</span></div>
  1373. <div class="device-type-item"><span class="dt-name">化学品智能终端</span><span class="dt-value">32</span></div>
  1374. <div class="device-type-item"><span class="dt-name">传感器</span><span class="dt-value">76</span></div>
  1375. <div class="device-type-item"><span class="dt-name">智能设备</span><span class="dt-value">34</span></div>
  1376. </div>
  1377. </div>
  1378. </div>
  1379. <!-- 实验室设备分类及使用统计 -->
  1380. <div class="panel" style="flex:4;display:flex;flex-direction:column;">
  1381. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1382. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1383. <div class="panel-title">实验室设备分类及使用统计</div>
  1384. <div class="equip-chart-wrap">
  1385. <div id="chartEquipPie" style="width:100%;height:100%;"></div>
  1386. </div>
  1387. <div class="equip-mid">
  1388. <div class="equip-mid-item"><div class="em-value">586</div><div class="em-label">设备总数</div></div>
  1389. <div class="equip-mid-item"><div class="em-value">12,480<span style="font-size:11px;color:var(--text-sub)">h</span></div><div class="em-label">使用总时长</div></div>
  1390. <div class="equip-mid-item"><div class="em-value">78.6<span style="font-size:11px;color:var(--text-sub)">%</span></div><div class="em-label">设备使用率</div></div>
  1391. </div>
  1392. <div class="equip-bottom">
  1393. <div class="equip-status-card"><div class="es-value" style="color:var(--accent)">128</div><div class="es-label">使用</div></div>
  1394. <div class="equip-status-card"><div class="es-value" style="color:var(--good)">312</div><div class="es-label">空闲</div></div>
  1395. <div class="equip-status-card"><div class="es-value" style="color:var(--warn)">108</div><div class="es-label">正常</div></div>
  1396. <div class="equip-status-card"><div class="es-value" style="color:var(--danger)">38</div><div class="es-label">维修</div></div>
  1397. </div>
  1398. </div>
  1399. <!-- 化学品库存动态统计 -->
  1400. <div class="panel" style="flex:3;display:flex;flex-direction:column;">
  1401. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1402. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1403. <div class="panel-title">化学品库存动态统计</div>
  1404. <div class="chem-panel-body">
  1405. <div class="chem-stats-grid">
  1406. <div class="chem-stat-item">
  1407. <div class="chem-ring-wrap">
  1408. <svg viewBox="0 0 44 44">
  1409. <g transform="rotate(-90 22 22)">
  1410. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1411. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#48d7ff"
  1412. stroke-dasharray="113.1" stroke-dashoffset="0"/>
  1413. </g>
  1414. <text x="22" y="23" class="chem-ring-icon" fill="#48d7ff">&#x1F9EA;</text>
  1415. </svg>
  1416. </div>
  1417. <div class="chem-stat-info">
  1418. <div class="chem-stat-value" style="color:#48d7ff">2,450<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1419. <div class="chem-stat-label">存量化学品总量</div>
  1420. </div>
  1421. </div>
  1422. <div class="chem-stat-item">
  1423. <div class="chem-ring-wrap">
  1424. <svg viewBox="0 0 44 44">
  1425. <g transform="rotate(-90 22 22)">
  1426. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1427. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#ff4d4f"
  1428. stroke-dasharray="113.1" stroke-dashoffset="81.4"/>
  1429. </g>
  1430. <text x="22" y="22" class="chem-ring-pct" fill="#ff4d4f">27.8%</text>
  1431. </svg>
  1432. </div>
  1433. <div class="chem-stat-info">
  1434. <div class="chem-stat-value" style="color:#ff4d4f">680<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1435. <div class="chem-stat-label">管控类化学品</div>
  1436. </div>
  1437. </div>
  1438. <div class="chem-stat-item">
  1439. <div class="chem-ring-wrap">
  1440. <svg viewBox="0 0 44 44">
  1441. <g transform="rotate(-90 22 22)">
  1442. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1443. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#36d399"
  1444. stroke-dasharray="113.1" stroke-dashoffset="31.4"/>
  1445. </g>
  1446. <text x="22" y="22" class="chem-ring-pct" fill="#36d399">72.2%</text>
  1447. </svg>
  1448. </div>
  1449. <div class="chem-stat-info">
  1450. <div class="chem-stat-value" style="color:#36d399">1,770<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1451. <div class="chem-stat-label">非管控类化学品</div>
  1452. </div>
  1453. </div>
  1454. <div class="chem-stat-item">
  1455. <div class="chem-stat-info" style="text-align:center;flex:1;">
  1456. <div class="chem-stat-value" style="color:#ffb020;font-size:22px;">156</div>
  1457. <div class="chem-stat-label">存量化学品总类目</div>
  1458. <div class="chem-stat-pct" style="color:var(--text-sub)">管控68类 / 非管控88类</div>
  1459. </div>
  1460. </div>
  1461. </div>
  1462. </div>
  1463. </div>
  1464. </div>
  1465. </div>
  1466. <!-- ===================== PAGE: 视频监控 ===================== -->
  1467. <div class="page" id="page-video">
  1468. <!-- LEFT: Tree -->
  1469. <div class="video-left panel">
  1470. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1471. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1472. <div class="panel-title">建筑结构导航</div>
  1473. <input class="video-search" type="text" placeholder="🔍 搜索楼栋 / 楼层..." />
  1474. <select class="video-filter">
  1475. <option value="">全部二级单位</option>
  1476. <option>安全技术研究所</option>
  1477. <option>职业安全研究所</option>
  1478. <option>化学品安全研究所</option>
  1479. <option>矿山安全研究所</option>
  1480. </select>
  1481. <div class="tree-container" id="treeContainer"></div>
  1482. </div>
  1483. <!-- RIGHT: Video Grid -->
  1484. <div class="video-right">
  1485. <div class="video-breadcrumb">安科院院区 → 科研楼A → 3层</div>
  1486. <div class="video-grid" id="videoGrid"></div>
  1487. <div class="video-pagination">
  1488. <button class="page-btn" onclick="videoPageChange(-1)">◀ 上一页</button>
  1489. <span class="page-info" id="videoPageInfo">第 1 / 3 页</span>
  1490. <button class="page-btn" onclick="videoPageChange(1)">下一页 ▶</button>
  1491. </div>
  1492. </div>
  1493. </div>
  1494. </div>
  1495. </div>
  1496. <!-- ===================== ALERT MODAL ===================== -->
  1497. <div class="alert-overlay" id="alertOverlay">
  1498. <div class="alert-modal">
  1499. <div class="alert-header">
  1500. <span class="alert-icon">⚠</span>
  1501. <span class="alert-title">安 全 预 警</span>
  1502. </div>
  1503. <div class="alert-body">
  1504. <div class="alert-info">
  1505. <div class="alert-info-row"><span class="a-label">实 验 室:</span><span class="a-value" id="alertLab">--</span></div>
  1506. <div class="alert-info-row"><span class="a-label">楼栋楼层:</span><span class="a-value" id="alertBuilding">--</span></div>
  1507. <div class="alert-info-row"><span class="a-label">所属单位:</span><span class="a-value" id="alertUnit">--</span></div>
  1508. <div class="alert-info-row"><span class="a-label">异常参数:</span><span class="a-value danger" id="alertParam">--</span></div>
  1509. <div class="alert-info-row"><span class="a-label">当前数值:</span><span class="a-value danger" id="alertValue">--</span></div>
  1510. <div class="alert-info-row"><span class="a-label">实验室负责人:</span><span class="a-value" id="alertPerson">--</span></div>
  1511. <div class="alert-info-row"><span class="a-label">联系电话:</span><span class="a-value" id="alertPhone">--</span></div>
  1512. <div class="alert-info-row"><span class="a-label">预警时间:</span><span class="a-value" id="alertTime">--</span></div>
  1513. </div>
  1514. <div class="alert-video">
  1515. <div class="alert-video-title">实时监控画面</div>
  1516. <div class="alert-video-feed">
  1517. <span class="cam-label"><span class="rec-dot"></span>REC <span id="alertCamName">--</span></span>
  1518. <span class="cam-placeholder">📹 实时监控画面</span>
  1519. </div>
  1520. </div>
  1521. </div>
  1522. <div class="alert-footer">
  1523. <button class="alert-close-btn" onclick="closeAlert()">确 认</button>
  1524. <button class="alert-close-btn" style="background:linear-gradient(135deg,#1a3a6a,#0d2555);border:1px solid rgba(72,180,255,0.5);margin-left:12px;" onclick="closeAlert();openEvacuation()">应急疏散</button>
  1525. </div>
  1526. </div>
  1527. </div>
  1528. <!-- ===================== EVACUATION MODAL ===================== -->
  1529. <div class="evac-overlay" id="evacOverlay">
  1530. <div class="evac-modal">
  1531. <div class="evac-header">
  1532. <div class="evac-header-left">
  1533. <span class="evac-icon">📋</span>
  1534. <span class="evac-title">应急疏散</span>
  1535. </div>
  1536. <div class="evac-close" onclick="closeEvacuation()">✕</div>
  1537. </div>
  1538. <div class="evac-body">
  1539. <div class="evac-main">
  1540. <div class="evac-legend">
  1541. <div class="evac-legend-item">
  1542. <span class="evac-legend-line" style="background:linear-gradient(90deg,#3a7bff,#48d7ff);"></span>
  1543. <span>应急疏散路线图</span>
  1544. </div>
  1545. <div class="evac-legend-item">
  1546. <span>房间号→实验室名称</span>
  1547. </div>
  1548. </div>
  1549. <div class="evac-floorplan">
  1550. <svg viewBox="0 0 680 360" id="evacFloorSvg">
  1551. <!-- Top row rooms -->
  1552. <g class="evac-room-group" data-room="A101" onclick="selectEvacRoom(this)">
  1553. <rect x="40" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1554. <text x="105" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A101</text>
  1555. </g>
  1556. <g class="evac-room-group" data-room="A102" onclick="selectEvacRoom(this)">
  1557. <rect x="180" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1558. <text x="245" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A102</text>
  1559. </g>
  1560. <g class="evac-room-group" data-room="A103" onclick="selectEvacRoom(this)">
  1561. <rect x="320" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1562. <text x="385" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A103</text>
  1563. </g>
  1564. <g class="evac-room-group" data-room="A104" onclick="selectEvacRoom(this)">
  1565. <rect x="460" y="30" width="130" height="80" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1566. <text x="525" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A104</text>
  1567. </g>
  1568. <!-- Corridor -->
  1569. <text x="340" y="155" text-anchor="middle" fill="rgba(126,172,200,0.5)" font-size="14" letter-spacing="8">走 道</text>
  1570. <circle cx="300" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
  1571. <circle cx="380" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
  1572. <!-- Right exit -->
  1573. <rect x="630" y="45" width="30" height="80" rx="2" fill="none" stroke="rgba(255,77,79,0.4)" stroke-width="1" stroke-dasharray="4,2"/>
  1574. <text x="645" y="80" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
  1575. <text x="645" y="95" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
  1576. <!-- Left exit -->
  1577. <rect x="2" y="220" width="30" height="80" rx="2" fill="none" stroke="rgba(255,77,79,0.4)" stroke-width="1" stroke-dasharray="4,2"/>
  1578. <text x="17" y="255" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
  1579. <text x="17" y="270" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
  1580. <!-- Bottom row rooms -->
  1581. <g class="evac-room-group" data-room="A105" onclick="selectEvacRoom(this)">
  1582. <rect x="40" y="200" width="110" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1583. <text x="95" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A105</text>
  1584. </g>
  1585. <g class="evac-room-group" data-room="A106" onclick="selectEvacRoom(this)">
  1586. <rect x="160" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1587. <text x="210" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A106</text>
  1588. </g>
  1589. <g class="evac-room-group" data-room="A107" onclick="selectEvacRoom(this)">
  1590. <rect x="270" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1591. <text x="320" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A107</text>
  1592. </g>
  1593. <g class="evac-room-group" data-room="A108" onclick="selectEvacRoom(this)">
  1594. <rect x="380" y="200" width="100" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1595. <text x="430" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A108</text>
  1596. </g>
  1597. <g class="evac-room-group" data-room="A109" onclick="selectEvacRoom(this)">
  1598. <rect x="490" y="200" width="130" height="110" rx="3" fill="rgba(72,180,255,0.06)" stroke="rgba(72,180,255,0.3)" stroke-width="1"/>
  1599. <text x="555" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A109</text>
  1600. </g>
  1601. <!-- Evacuation route (drawn dynamically) -->
  1602. <g id="evacRoutes" class="evac-route-arrow"></g>
  1603. <!-- SVG arrow marker -->
  1604. <defs>
  1605. <marker id="evacArrowHead" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
  1606. <polygon points="0,0 10,4 0,8" fill="#3a7bff"/>
  1607. </marker>
  1608. </defs>
  1609. </svg>
  1610. <!-- Alert info card overlay -->
  1611. <div class="evac-alert-card" id="evacAlertCard">
  1612. <div class="eac-header">
  1613. <div class="eac-time"><span class="eac-icon">⚠</span> <span id="evacAlertTime">2026-03-24 14:32:18</span></div>
  1614. <button class="eac-close-btn" onclick="document.getElementById('evacAlertCard').style.display='none'">✕</button>
  1615. </div>
  1616. <div class="eac-badge">触发风险</div>
  1617. <div class="eac-desc">发生风险:TVOC浓度超标</div>
  1618. </div>
  1619. </div>
  1620. </div>
  1621. <div class="evac-sidebar">
  1622. <div class="evac-sidebar-block">
  1623. <div class="evac-sidebar-title">实时视频监控</div>
  1624. <div class="evac-sidebar-label" id="evacSidebarLabel1">楼道 1 层</div>
  1625. <div class="evac-video-placeholder" id="evacSidebarVideo1">📹 实时视频监控</div>
  1626. </div>
  1627. <div class="evac-sidebar-block">
  1628. <div class="evac-sidebar-title">实时视频监控</div>
  1629. <div class="evac-sidebar-label" id="evacSidebarLabel2">楼道 2 层</div>
  1630. <div class="evac-video-placeholder" id="evacSidebarVideo2">📹 实时视频监控</div>
  1631. </div>
  1632. </div>
  1633. </div>
  1634. <div class="evac-footer">
  1635. <div class="evac-metrics">
  1636. <div class="evac-metric-row">
  1637. <span class="evac-metric-label">告警指标</span>
  1638. <span class="evac-metric-value">TVOC 浓度超标</span>
  1639. <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
  1640. </div>
  1641. <div class="evac-metric-row">
  1642. <span class="evac-metric-label">当前值 / 安全阈值</span>
  1643. <span class="evac-metric-value">TVOC 浓度超标</span>
  1644. <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
  1645. </div>
  1646. </div>
  1647. <div class="evac-broadcast">
  1648. <div class="evac-broadcast-header">
  1649. <div class="evac-broadcast-title">🔊 语音广播</div>
  1650. <div class="evac-broadcast-hint">选择播放设备</div>
  1651. </div>
  1652. <div class="evac-speakers">
  1653. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1654. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1655. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1656. </div>
  1657. <div class="evac-broadcast-input">
  1658. <input type="text" placeholder="请输入喊话内容" />
  1659. <button>发送</button>
  1660. </div>
  1661. </div>
  1662. <div class="evac-actions">
  1663. <div class="evac-btn-later" onclick="closeEvacuation()">稍后处理</div>
  1664. <div class="evac-btn-exec" onclick="executeEvacuation()">执行疏散</div>
  1665. </div>
  1666. </div>
  1667. </div>
  1668. </div>
  1669. <script>
  1670. /* ================================================================
  1671. JavaScript - 中国安全生产科学研究院实验室安全智慧化管控中心
  1672. ================================================================ */
  1673. // ===================== Viewport Scaling =====================
  1674. function fitScreen() {
  1675. const scr = document.getElementById('mainScreen');
  1676. const sw = 1920, sh = 1080;
  1677. const vw = window.innerWidth, vh = window.innerHeight;
  1678. const scale = Math.min(vw / sw, vh / sh);
  1679. scr.style.transform = `translate(-50%, -50%) scale(${scale})`;
  1680. }
  1681. window.addEventListener('resize', fitScreen);
  1682. fitScreen();
  1683. // ===================== Auto Fullscreen =====================
  1684. document.addEventListener('click', function onFirstClick() {
  1685. if (document.documentElement.requestFullscreen) {
  1686. document.documentElement.requestFullscreen().catch(() => {});
  1687. }
  1688. document.removeEventListener('click', onFirstClick);
  1689. }, { once: true });
  1690. // ===================== Clock =====================
  1691. function updateClock() {
  1692. const now = new Date();
  1693. const hh = String(now.getHours()).padStart(2,'0');
  1694. const mm = String(now.getMinutes()).padStart(2,'0');
  1695. const ss = String(now.getSeconds()).padStart(2,'0');
  1696. document.getElementById('clock').textContent = `${hh}:${mm}:${ss}`;
  1697. const days = ['日','一','二','三','四','五','六'];
  1698. document.getElementById('weekday').textContent = `星期${days[now.getDay()]}`;
  1699. }
  1700. setInterval(updateClock, 1000);
  1701. updateClock();
  1702. // ===================== Page Switch =====================
  1703. function switchPage(page) {
  1704. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1705. document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
  1706. if (page === 'lab') {
  1707. document.getElementById('page-lab').classList.add('active');
  1708. document.querySelectorAll('.nav-tab')[0].classList.add('active');
  1709. setTimeout(() => { initLabCharts(); }, 100);
  1710. } else {
  1711. document.getElementById('page-video').classList.add('active');
  1712. document.querySelectorAll('.nav-tab')[1].classList.add('active');
  1713. }
  1714. }
  1715. // ===================== Flip Counter =====================
  1716. function renderFlip(id, num, len) {
  1717. const str = String(num).padStart(len, '0');
  1718. const container = document.getElementById(id);
  1719. container.innerHTML = str.split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
  1720. }
  1721. renderFlip('flipTotal', 1286, 5);
  1722. renderFlip('flipCurrent', 47, 4);
  1723. // ===================== Sensor Scroll Data =====================
  1724. const labNames = [
  1725. { name: '化学实验室A101(A101)', unit: '化学品安全研究所' },
  1726. { name: '材料力学实验室B203(B203)', unit: '安全技术研究所' },
  1727. { name: '气体分析实验室A305(A305)', unit: '职业安全研究所' },
  1728. { name: '高温高压实验室C102(C102)', unit: '矿山安全研究所' },
  1729. { name: '生物安全实验室A202(A202)', unit: '职业安全研究所' },
  1730. { name: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所' },
  1731. { name: '电气安全实验室C201(C201)', unit: '安全技术研究所' },
  1732. { name: '环境模拟实验室A408(A408)', unit: '化学品安全研究所' },
  1733. { name: '爆炸安全实验室B302(B302)', unit: '安全技术研究所' },
  1734. { name: '应急救援实验室C305(C305)', unit: '职业安全研究所' },
  1735. { name: '有机化学实验室A103(A103)', unit: '化学品安全研究所' },
  1736. { name: '无损检测实验室B205(B205)', unit: '安全技术研究所' },
  1737. ];
  1738. const sensorTypes = ['温度','湿度','TVOC','CO₂','O₂'];
  1739. function randVal(type) {
  1740. switch(type) {
  1741. case '温度': return (18 + Math.random() * 15).toFixed(1) + '°C';
  1742. case '湿度': return (30 + Math.random() * 50).toFixed(0) + '%';
  1743. case 'TVOC': return (Math.random() * 2).toFixed(2) + 'mg/m³';
  1744. case 'CO₂': return (300 + Math.random() * 800).toFixed(0) + 'ppm';
  1745. case 'O₂': return (18 + Math.random() * 4).toFixed(1) + '%';
  1746. default: return '--';
  1747. }
  1748. }
  1749. function isAlarm(type, val) {
  1750. const n = parseFloat(val);
  1751. if (type === '温度' && n > 30) return true;
  1752. if (type === 'TVOC' && n > 1.5) return true;
  1753. if (type === 'CO₂' && n > 900) return true;
  1754. if (type === 'O₂' && n < 19.5) return true;
  1755. return false;
  1756. }
  1757. function buildSensorList() {
  1758. const el = document.getElementById('sensorList');
  1759. let html = '';
  1760. const items = [...labNames, ...labNames]; // duplicate for seamless scroll
  1761. items.forEach(lab => {
  1762. html += `<div class="sensor-item"><div class="lab-name">${lab.name}<span class="lab-unit">${lab.unit}</span></div><div class="sensor-values">`;
  1763. sensorTypes.forEach(s => {
  1764. const v = randVal(s);
  1765. const alarm = isAlarm(s, v);
  1766. html += `<span class="sensor-val${alarm ? ' alarm' : ''}">${alarm ? '<span class="alarm-icon">⚠</span>' : ''}${s}: ${v}</span>`;
  1767. });
  1768. html += '</div></div>';
  1769. });
  1770. el.innerHTML = html;
  1771. }
  1772. buildSensorList();
  1773. // ===================== Warning Scroll Data =====================
  1774. function buildWarningList() {
  1775. const el = document.getElementById('warningList');
  1776. const warnings = [
  1777. { type: 'sensor', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-24 14:32:18' },
  1778. { type: 'chem', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', person: '王伟', time: '2026-03-24 14:28:45' },
  1779. { type: 'sensor', lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-24 14:28:05' },
  1780. { type: 'chem', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', person: '李明', time: '2026-03-24 14:15:33' },
  1781. { type: 'sensor', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-24 13:55:42' },
  1782. { type: 'sensor', lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-24 13:20:10' },
  1783. { type: 'chem', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', person: '赵磊', time: '2026-03-24 13:10:22' },
  1784. { type: 'sensor', lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-24 12:45:33' },
  1785. { type: 'chem', lab: '材料力学实验室B203(B203)', unit: '安全技术研究所', person: '陈静', time: '2026-03-24 12:30:18' },
  1786. { type: 'sensor', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-24 11:18:27' },
  1787. { type: 'sensor', lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-24 10:42:51' },
  1788. { type: 'chem', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', person: '张强', time: '2026-03-24 10:15:07' },
  1789. { type: 'sensor', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-24 09:36:14' },
  1790. ];
  1791. const all = [...warnings, ...warnings]; // duplicate for seamless scroll
  1792. let html = '';
  1793. all.forEach(w => {
  1794. if (w.type === 'chem') {
  1795. html += `<div class="warning-item chem-violation">
  1796. <div class="w-lab">${w.lab} - ${w.unit}</div>
  1797. <div class="w-sensor">化学品违规带离: ${w.person} 未正常使用违规带离</div>
  1798. <div class="w-time">${w.time}</div>
  1799. </div>`;
  1800. } else {
  1801. html += `<div class="warning-item">
  1802. <div class="w-lab">${w.lab} - ${w.unit}</div>
  1803. <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
  1804. <div class="w-time">${w.time}</div>
  1805. </div>`;
  1806. }
  1807. });
  1808. el.innerHTML = html;
  1809. document.getElementById('warningCount').textContent = warnings.length;
  1810. }
  1811. buildWarningList();
  1812. // ===================== Video Page: Tree =====================
  1813. const treeData = {
  1814. name: '安科院院区', children: [
  1815. { name: '科研楼A', children: [
  1816. { name: '1层', children: [{name:'A101'},{name:'A102'},{name:'A103'}] },
  1817. { name: '2层', children: [{name:'A201'},{name:'A202'},{name:'A203'}] },
  1818. { name: '3层', children: [{name:'A301'},{name:'A302'},{name:'A303'},{name:'A304'},{name:'A305'}] },
  1819. { name: '4层', children: [{name:'A401'},{name:'A402'},{name:'A403'},{name:'A408'}] },
  1820. ]},
  1821. { name: '科研楼B', children: [
  1822. { name: '1层', children: [{name:'B101'},{name:'B102'}] },
  1823. { name: '2层', children: [{name:'B201'},{name:'B202'},{name:'B203'},{name:'B205'}] },
  1824. { name: '3层', children: [{name:'B301'},{name:'B302'}] },
  1825. ]},
  1826. { name: '科研楼C', children: [
  1827. { name: '1层', children: [{name:'C101'},{name:'C102'}] },
  1828. { name: '2层', children: [{name:'C201'},{name:'C202'}] },
  1829. { name: '3层', children: [{name:'C301'},{name:'C305'}] },
  1830. ]},
  1831. ]
  1832. };
  1833. function buildTree(node, depth) {
  1834. const hasChild = node.children && node.children.length > 0;
  1835. let html = `<div class="tree-node">
  1836. <div class="tree-node-content" style="padding-left:${depth * 12}px" onclick="toggleTree(this)">
  1837. <span class="tree-arrow">${hasChild ? '▶' : ''}</span>
  1838. <span>${node.name}</span>
  1839. </div>`;
  1840. if (hasChild) {
  1841. html += `<div class="tree-children">`;
  1842. node.children.forEach(c => { html += buildTree(c, depth + 1); });
  1843. html += `</div>`;
  1844. }
  1845. html += `</div>`;
  1846. return html;
  1847. }
  1848. document.getElementById('treeContainer').innerHTML = buildTree(treeData, 0);
  1849. function toggleTree(el) {
  1850. const children = el.parentElement.querySelector('.tree-children');
  1851. const arrow = el.querySelector('.tree-arrow');
  1852. if (children) {
  1853. children.classList.toggle('open');
  1854. arrow.classList.toggle('expanded');
  1855. }
  1856. // select highlight
  1857. document.querySelectorAll('.tree-node-content').forEach(n => n.classList.remove('selected'));
  1858. el.classList.add('selected');
  1859. }
  1860. // ===================== Video Grid =====================
  1861. let currentVideoPage = 1;
  1862. const totalVideoPages = 3;
  1863. function renderVideoGrid() {
  1864. const grid = document.getElementById('videoGrid');
  1865. let html = '';
  1866. for (let i = 0; i < 9; i++) {
  1867. const roomNum = (currentVideoPage - 1) * 9 + i + 1;
  1868. const isAI = i === 0;
  1869. html += `<div class="video-cell${isAI ? ' ai-cam' : ''}">
  1870. <div class="cam-frame">
  1871. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1872. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1873. ${isAI ? '<span class="ai-badge">AI 智能</span>' : ''}
  1874. <div class="cam-inner">
  1875. <span class="cam-label"><span class="cam-dot"></span>${isAI ? '🎯 ' : ''}A3${String(roomNum).padStart(2,'0')} 摄像头</span>
  1876. <span class="cam-placeholder">📹 实时监控画面</span>
  1877. </div>
  1878. </div>
  1879. </div>`;
  1880. }
  1881. grid.innerHTML = html;
  1882. document.getElementById('videoPageInfo').textContent = `第 ${currentVideoPage} / ${totalVideoPages} 页`;
  1883. }
  1884. renderVideoGrid();
  1885. function videoPageChange(dir) {
  1886. currentVideoPage = Math.max(1, Math.min(totalVideoPages, currentVideoPage + dir));
  1887. renderVideoGrid();
  1888. }
  1889. // ===================== Alert Modal =====================
  1890. function showAlert(lab, building, unit, param, value, person, phone) {
  1891. document.getElementById('alertLab').textContent = lab;
  1892. document.getElementById('alertBuilding').textContent = building || '--';
  1893. document.getElementById('alertUnit').textContent = unit;
  1894. document.getElementById('alertParam').textContent = param;
  1895. document.getElementById('alertValue').textContent = value;
  1896. document.getElementById('alertPerson').textContent = person || '--';
  1897. document.getElementById('alertPhone').textContent = phone || '--';
  1898. document.getElementById('alertCamName').textContent = lab;
  1899. const now = new Date();
  1900. document.getElementById('alertTime').textContent =
  1901. `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
  1902. document.getElementById('alertOverlay').classList.add('show');
  1903. }
  1904. function closeAlert() {
  1905. document.getElementById('alertOverlay').classList.remove('show');
  1906. }
  1907. // Demo: trigger alert after 15s
  1908. setTimeout(() => {
  1909. showAlert('化学实验室A101(A101)', '科研楼A-1层', '化学品安全研究所', 'TVOC 超标', '1.82 mg/m³', '张明', '138-0012-3456');
  1910. }, 15000);
  1911. // ===================== ECharts Initialization =====================
  1912. let chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie;
  1913. function initLabCharts() {
  1914. // ---- 1. Lab Overview Ring ----
  1915. if (chartLabOverview) chartLabOverview.dispose();
  1916. chartLabOverview = echarts.init(document.getElementById('chartLabOverview'));
  1917. chartLabOverview.setOption({
  1918. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1919. legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  1920. series: [{
  1921. type: 'pie', radius: ['45%','70%'], center: ['35%','50%'],
  1922. label: {
  1923. show: true, position: 'center',
  1924. formatter: '58\n实验室总数', fontSize: 14, color: '#fff', lineHeight: 20
  1925. },
  1926. data: [
  1927. { value: 8, name: 'I级(红)', itemStyle: { color: '#ff4d4f' } },
  1928. { value: 12, name: 'II级(橙)', itemStyle: { color: '#ff8c00' } },
  1929. { value: 18, name: 'III级(黄)', itemStyle: { color: '#ffcc00' } },
  1930. { value: 20, name: 'IV级(蓝)', itemStyle: { color: '#3a7bff' } },
  1931. ],
  1932. emphasis: { scaleSize: 6 }
  1933. }]
  1934. });
  1935. // ---- 2. Grade Stacked Bar ----
  1936. if (chartGradeBar) chartGradeBar.dispose();
  1937. chartGradeBar = echarts.init(document.getElementById('chartGradeBar'));
  1938. const units = ['安全技术\n研究所','职业安全\n研究所','化学品安全\n研究所','矿山安全\n研究所','应急科学\n研究中心','信息技术\n研究所','检测检验\n中心','标准化\n研究所'];
  1939. const totals = [12, 10, 14, 8, 6, 4, 3, 1];
  1940. const gradeData = {
  1941. 'I级': [2,1,3,2,1,0,0,0],
  1942. 'II级': [3,2,4,2,1,1,1,0],
  1943. 'III级': [4,4,4,2,2,2,1,1],
  1944. 'IV级': [3,3,3,2,2,1,1,0]
  1945. };
  1946. const colors = { 'I级': '#ff4d4f', 'II级': '#ff8c00', 'III级': '#ffcc00', 'IV级': '#3a7bff' };
  1947. let barOption = {
  1948. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1949. legend: { data: Object.keys(gradeData), top: 0, textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  1950. grid: { left: 40, right: 10, top: 35, bottom: 50 },
  1951. xAxis: {
  1952. type: 'category', data: units,
  1953. axisLabel: {
  1954. color: '#7eacc8', fontSize: 10, interval: 0,
  1955. formatter: function(v, idx) { return v + '\n(' + totals[idx] + ')'; }
  1956. },
  1957. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  1958. axisTick: { show: false }
  1959. },
  1960. yAxis: {
  1961. type: 'value',
  1962. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1963. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  1964. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  1965. },
  1966. dataZoom: [{
  1967. type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5
  1968. }],
  1969. series: Object.keys(gradeData).map(k => ({
  1970. name: k, type: 'bar', stack: 'total', barWidth: 20,
  1971. data: gradeData[k],
  1972. itemStyle: { color: colors[k], borderRadius: [0,0,0,0] }
  1973. }))
  1974. };
  1975. chartGradeBar.setOption(barOption);
  1976. // Auto scroll X axis
  1977. let barScrollIdx = 0;
  1978. setInterval(() => {
  1979. barScrollIdx++;
  1980. if (barScrollIdx > units.length - 5) barScrollIdx = 0;
  1981. chartGradeBar.dispatchAction({ type: 'dataZoom', startValue: barScrollIdx, endValue: barScrollIdx + 5 });
  1982. }, 5000);
  1983. // ---- 3. People Line Chart ----
  1984. if (chartPeopleLine) chartPeopleLine.dispose();
  1985. chartPeopleLine = echarts.init(document.getElementById('chartPeopleLine'));
  1986. const timeSlots = ['0:00','3:00','6:00','9:00','12:00','15:00','18:00','21:00','24:00'];
  1987. chartPeopleLine.setOption({
  1988. tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1989. legend: { data: ['进入人数','实验人数'], top: 0, right: 0, textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 14, itemHeight: 2 },
  1990. grid: { left: 35, right: 10, top: 24, bottom: 20 },
  1991. xAxis: {
  1992. type: 'category', data: timeSlots, boundaryGap: false,
  1993. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1994. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } }
  1995. },
  1996. yAxis: {
  1997. type: 'value',
  1998. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1999. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  2000. },
  2001. series: [
  2002. {
  2003. name: '进入人数', type: 'line', smooth: true,
  2004. data: [0, 2, 5, 86, 120, 145, 98, 42, 0],
  2005. lineStyle: { color: '#48d7ff', width: 2 },
  2006. areaStyle: { color: new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(72,215,255,0.25)'},{offset:1,color:'rgba(72,215,255,0)'}]) },
  2007. itemStyle: { color: '#48d7ff' }, symbolSize: 4
  2008. },
  2009. {
  2010. name: '实验人数', type: 'line', smooth: true,
  2011. data: [0, 1, 3, 52, 68, 78, 55, 20, 0],
  2012. lineStyle: { color: '#3a7bff', width: 2 },
  2013. areaStyle: { color: new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(58,123,255,0.2)'},{offset:1,color:'rgba(58,123,255,0)'}]) },
  2014. itemStyle: { color: '#3a7bff' }, symbolSize: 4
  2015. }
  2016. ]
  2017. });
  2018. // ---- 4. Device Gauge ----
  2019. if (chartGauge) chartGauge.dispose();
  2020. chartGauge = echarts.init(document.getElementById('chartGauge'));
  2021. chartGauge.setOption({
  2022. series: [{
  2023. type: 'gauge', startAngle: 210, endAngle: -30,
  2024. radius: '90%', center: ['50%','58%'],
  2025. min: 0, max: 100,
  2026. axisLine: {
  2027. lineStyle: {
  2028. width: 14,
  2029. color: [[0.7,'rgba(72,215,255,0.2)'],[0.9,'rgba(72,215,255,0.4)'],[1,'#48d7ff']]
  2030. }
  2031. },
  2032. axisTick: { show: false },
  2033. splitLine: { show: false },
  2034. axisLabel: { show: false },
  2035. pointer: {
  2036. length: '60%', width: 4,
  2037. itemStyle: { color: '#48d7ff' }
  2038. },
  2039. title: { show: true, offsetCenter: [0, '72%'], fontSize: 12, color: '#7eacc8' },
  2040. detail: {
  2041. valueAnimation: true, fontSize: 26, color: '#48d7ff', fontWeight: 700,
  2042. offsetCenter: [0, '40%'],
  2043. formatter: '{value}%'
  2044. },
  2045. data: [{ value: 93, name: '设备在线率' }]
  2046. }]
  2047. });
  2048. // ---- 5. Equipment Pie ----
  2049. if (chartEquipPie) chartEquipPie.dispose();
  2050. chartEquipPie = echarts.init(document.getElementById('chartEquipPie'));
  2051. chartEquipPie.setOption({
  2052. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  2053. legend: { orient: 'vertical', right: 4, top: 'center', textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 8, itemHeight: 8 },
  2054. series: [{
  2055. type: 'pie', radius: ['30%', '60%'], center: ['35%','50%'],
  2056. label: { show: false },
  2057. data: [
  2058. { value: 120, name: '分析仪器', itemStyle: { color: '#48d7ff' } },
  2059. { value: 95, name: '安全防护', itemStyle: { color: '#3a7bff' } },
  2060. { value: 86, name: '化学试剂设备', itemStyle: { color: '#36d399' } },
  2061. { value: 78, name: '电气设备', itemStyle: { color: '#ffb020' } },
  2062. { value: 65, name: '力学测试', itemStyle: { color: '#ff8c00' } },
  2063. { value: 52, name: '环境监测', itemStyle: { color: '#ff4d4f' } },
  2064. { value: 90, name: '通用设备', itemStyle: { color: '#a78bfa' } },
  2065. ],
  2066. emphasis: { scaleSize: 4 }
  2067. }]
  2068. });
  2069. }
  2070. // Init charts on load
  2071. setTimeout(initLabCharts, 200);
  2072. // Handle resize
  2073. window.addEventListener('resize', () => {
  2074. [chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie].forEach(c => c && c.resize());
  2075. });
  2076. // ===================== Evacuation Modal =====================
  2077. // Room name mapping
  2078. const evacRoomNames = {
  2079. A101: '化学实验室A101', A102: '材料分析实验室A102',
  2080. A103: '有机化学实验室A103', A104: '气相色谱实验室A104',
  2081. A105: '粉尘检测实验室A105', A106: '光谱分析实验室A106',
  2082. A107: '质谱分析实验室A107', A108: '样品前处理室A108',
  2083. A109: '大型仪器室A109'
  2084. };
  2085. // Room center coordinates for route drawing
  2086. const evacRoomPos = {
  2087. // Top row: room center X, corridor Y entry point
  2088. A101: { cx: 105, cy: 70, row: 'top', exitX: 105, corridorY: 130 },
  2089. A102: { cx: 245, cy: 70, row: 'top', exitX: 245, corridorY: 130 },
  2090. A103: { cx: 385, cy: 70, row: 'top', exitX: 385, corridorY: 130 },
  2091. A104: { cx: 525, cy: 70, row: 'top', exitX: 525, corridorY: 130 },
  2092. // Bottom row: room center X, corridor Y entry point
  2093. A105: { cx: 95, cy: 255, row: 'bottom', exitX: 95, corridorY: 185 },
  2094. A106: { cx: 210, cy: 255, row: 'bottom', exitX: 210, corridorY: 185 },
  2095. A107: { cx: 320, cy: 255, row: 'bottom', exitX: 320, corridorY: 185 },
  2096. A108: { cx: 430, cy: 255, row: 'bottom', exitX: 430, corridorY: 185 },
  2097. A109: { cx: 555, cy: 255, row: 'bottom', exitX: 555, corridorY: 185 },
  2098. };
  2099. // Current alarm room (default from the alert)
  2100. let evacAlarmRoom = 'A105';
  2101. function openEvacuation() {
  2102. document.getElementById('evacOverlay').classList.add('show');
  2103. // Reset state
  2104. document.getElementById('evacRoutes').innerHTML = '';
  2105. document.getElementById('evacRoutes').classList.remove('visible');
  2106. document.getElementById('evacAlertCard').style.display = 'block';
  2107. // Clear room selections & alarm highlights
  2108. document.querySelectorAll('.evac-room-group').forEach(g => {
  2109. g.classList.remove('selected', 'alarm');
  2110. });
  2111. // Reset sidebar
  2112. document.getElementById('evacSidebarLabel1').textContent = '楼道 1 层';
  2113. document.getElementById('evacSidebarVideo1').textContent = '📹 实时视频监控';
  2114. document.getElementById('evacSidebarLabel2').textContent = '楼道 2 层';
  2115. document.getElementById('evacSidebarVideo2').textContent = '📹 实时视频监控';
  2116. // Update time
  2117. const now = new Date();
  2118. const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
  2119. document.getElementById('evacAlertTime').textContent = ts;
  2120. }
  2121. function closeEvacuation() {
  2122. document.getElementById('evacOverlay').classList.remove('show');
  2123. }
  2124. function selectEvacRoom(el) {
  2125. // Clear previous selection (keep alarm class)
  2126. document.querySelectorAll('.evac-room-group.selected').forEach(g => g.classList.remove('selected'));
  2127. el.classList.add('selected');
  2128. const room = el.getAttribute('data-room');
  2129. const roomName = evacRoomNames[room] || room;
  2130. // Update sidebar: top block shows selected room camera, bottom stays corridor
  2131. document.getElementById('evacSidebarLabel1').textContent = roomName;
  2132. document.getElementById('evacSidebarVideo1').textContent = '📹 ' + room + ' 实时监控画面';
  2133. document.getElementById('evacSidebarLabel2').textContent = '楼道走廊';
  2134. document.getElementById('evacSidebarVideo2').textContent = '📹 走廊实时监控画面';
  2135. }
  2136. function executeEvacuation() {
  2137. const routesG = document.getElementById('evacRoutes');
  2138. routesG.innerHTML = ''; // Clear previous
  2139. // Mark alarm room
  2140. document.querySelectorAll('.evac-room-group').forEach(g => g.classList.remove('alarm'));
  2141. const alarmEl = document.querySelector(`.evac-room-group[data-room="${evacAlarmRoom}"]`);
  2142. if (alarmEl) alarmEl.classList.add('alarm');
  2143. const pos = evacRoomPos[evacAlarmRoom];
  2144. if (!pos) return;
  2145. // Build polyline path: room -> corridor -> along corridor -> exit
  2146. let pathPoints = [];
  2147. if (pos.row === 'top') {
  2148. // Top row: go down to corridor, then right to exit
  2149. pathPoints = [
  2150. [pos.exitX, pos.cy + 40], // bottom edge of room
  2151. [pos.exitX, pos.corridorY], // enter corridor
  2152. [620, pos.corridorY], // along corridor to right
  2153. [620, 85], // up to exit
  2154. [640, 85], // into exit
  2155. ];
  2156. } else {
  2157. // Bottom row: go up to corridor, then left to exit
  2158. pathPoints = [
  2159. [pos.exitX, pos.cy - 55], // top edge of room
  2160. [pos.exitX, pos.corridorY], // enter corridor
  2161. [35, pos.corridorY], // along corridor to left
  2162. [35, 260], // down to exit
  2163. [17, 260], // into exit
  2164. ];
  2165. }
  2166. // Draw animated polyline
  2167. const pointsStr = pathPoints.map(p => p.join(',')).join(' ');
  2168. const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
  2169. polyline.setAttribute('points', pointsStr);
  2170. polyline.setAttribute('fill', 'none');
  2171. polyline.setAttribute('stroke', '#3a7bff');
  2172. polyline.setAttribute('stroke-width', '3');
  2173. polyline.setAttribute('stroke-dasharray', '8,4');
  2174. polyline.setAttribute('marker-end', 'url(#evacArrowHead)');
  2175. routesG.appendChild(polyline);
  2176. // Add glow effect polyline
  2177. const glowLine = polyline.cloneNode();
  2178. glowLine.setAttribute('stroke', 'rgba(58,123,255,0.3)');
  2179. glowLine.setAttribute('stroke-width', '8');
  2180. glowLine.removeAttribute('stroke-dasharray');
  2181. glowLine.removeAttribute('marker-end');
  2182. routesG.insertBefore(glowLine, polyline);
  2183. // Show with animation
  2184. routesG.classList.add('visible');
  2185. }
  2186. </script>
  2187. </body>
  2188. </html>