agentDemo_dashboard.html 107 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343
  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. <!-- 实验室视频监控 (2x2) -->
  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 style="flex:1;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:8px;min-height:0;">
  1337. <div class="video-cell">
  1338. <div class="cam-frame">
  1339. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1340. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1341. <div class="cam-inner">
  1342. <span class="cam-label"><span class="cam-dot"></span>A101 走廊</span>
  1343. <span class="cam-placeholder">📹 实时监控画面</span>
  1344. </div>
  1345. </div>
  1346. </div>
  1347. <div class="video-cell">
  1348. <div class="cam-frame">
  1349. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1350. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1351. <div class="cam-inner">
  1352. <span class="cam-label"><span class="cam-dot"></span>B201 走廊</span>
  1353. <span class="cam-placeholder">📹 实时监控画面</span>
  1354. </div>
  1355. </div>
  1356. </div>
  1357. <div class="video-cell ai-cam">
  1358. <div class="cam-frame">
  1359. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1360. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1361. <span class="ai-badge">AI 智能</span>
  1362. <div class="cam-inner">
  1363. <span class="cam-label"><span class="cam-dot"></span>🎯 化学实验室A101</span>
  1364. <span class="cam-placeholder">📹 实时监控画面</span>
  1365. </div>
  1366. </div>
  1367. </div>
  1368. <div class="video-cell">
  1369. <div class="cam-frame">
  1370. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1371. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1372. <div class="cam-inner">
  1373. <span class="cam-label"><span class="cam-dot"></span>C102 走廊</span>
  1374. <span class="cam-placeholder">📹 实时监控画面</span>
  1375. </div>
  1376. </div>
  1377. </div>
  1378. </div>
  1379. </div>
  1380. <!-- Bottom row: 实验环境安全智能感知 + 实验室实时风险预警 -->
  1381. <div style="height:320px;display:flex;gap:14px;flex-shrink:0;">
  1382. <!-- 实验环境安全智能感知 -->
  1383. <div class="panel" style="flex:1;display:flex;flex-direction:column;min-width:0;">
  1384. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1385. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1386. <div class="panel-title">实验环境安全智能感知</div>
  1387. <div class="sensor-scroll-wrap">
  1388. <div class="sensor-scroll-inner" id="sensorList"></div>
  1389. </div>
  1390. </div>
  1391. <!-- 实验室实时风险预警 -->
  1392. <div class="panel" style="flex:1;display:flex;flex-direction:column;min-width:0;">
  1393. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1394. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1395. <div class="panel-title">实验室实时风险预警</div>
  1396. <div class="warning-header">
  1397. <div class="warning-count" id="warningCount">12</div>
  1398. <div class="warning-count-label">本月预警响应总数</div>
  1399. </div>
  1400. <div class="warning-scroll-wrap">
  1401. <div class="warning-scroll-inner" id="warningList"></div>
  1402. </div>
  1403. </div>
  1404. </div>
  1405. </div>
  1406. <!-- RIGHT COLUMN -->
  1407. <div class="col-right">
  1408. <!-- 智能环境感知应用设备统计 -->
  1409. <div class="panel" style="flex:4;display:flex;flex-direction:column;">
  1410. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1411. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1412. <div class="panel-title">智能环境感知应用设备统计</div>
  1413. <div class="device-top">
  1414. <div class="device-stat-card">
  1415. <div class="ds-label">在线设备</div><div class="ds-value">186</div>
  1416. </div>
  1417. <div class="device-stat-card offline">
  1418. <div class="ds-label">离线设备</div><div class="ds-value">14</div>
  1419. </div>
  1420. </div>
  1421. <div class="device-bottom">
  1422. <div class="device-gauge-wrap" id="chartGauge"></div>
  1423. <div class="device-type-list">
  1424. <div class="device-type-item"><span class="dt-name">电子信息铭牌</span><span class="dt-value">58</span></div>
  1425. <div class="device-type-item"><span class="dt-name">化学品智能终端</span><span class="dt-value">32</span></div>
  1426. <div class="device-type-item"><span class="dt-name">传感器</span><span class="dt-value">76</span></div>
  1427. <div class="device-type-item"><span class="dt-name">智能设备</span><span class="dt-value">34</span></div>
  1428. </div>
  1429. </div>
  1430. </div>
  1431. <!-- 实验室设备分类及使用统计 -->
  1432. <div class="panel" style="flex:4;display:flex;flex-direction:column;">
  1433. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1434. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1435. <div class="panel-title">实验室设备分类及使用统计</div>
  1436. <div class="equip-chart-wrap">
  1437. <div id="chartEquipPie" style="width:100%;height:100%;"></div>
  1438. </div>
  1439. <div class="equip-mid">
  1440. <div class="equip-mid-item"><div class="em-value">586</div><div class="em-label">设备总数</div></div>
  1441. <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>
  1442. <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>
  1443. </div>
  1444. <div class="equip-bottom">
  1445. <div class="equip-status-card"><div class="es-value" style="color:var(--accent)">128</div><div class="es-label">使用</div></div>
  1446. <div class="equip-status-card"><div class="es-value" style="color:var(--good)">312</div><div class="es-label">空闲</div></div>
  1447. <div class="equip-status-card"><div class="es-value" style="color:var(--warn)">108</div><div class="es-label">正常</div></div>
  1448. <div class="equip-status-card"><div class="es-value" style="color:var(--danger)">38</div><div class="es-label">维修</div></div>
  1449. </div>
  1450. </div>
  1451. <!-- 化学品库存动态统计 -->
  1452. <div class="panel" style="flex:3;display:flex;flex-direction:column;">
  1453. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1454. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1455. <div class="panel-title">化学品库存动态统计</div>
  1456. <div class="chem-panel-body">
  1457. <div class="chem-stats-grid">
  1458. <div class="chem-stat-item">
  1459. <div class="chem-ring-wrap">
  1460. <svg viewBox="0 0 44 44">
  1461. <g transform="rotate(-90 22 22)">
  1462. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1463. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#48d7ff"
  1464. stroke-dasharray="113.1" stroke-dashoffset="0"/>
  1465. </g>
  1466. <text x="22" y="23" class="chem-ring-icon" fill="#48d7ff">&#x1F9EA;</text>
  1467. </svg>
  1468. </div>
  1469. <div class="chem-stat-info">
  1470. <div class="chem-stat-value" style="color:#48d7ff">2,450<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1471. <div class="chem-stat-label">存量化学品总量</div>
  1472. </div>
  1473. </div>
  1474. <div class="chem-stat-item">
  1475. <div class="chem-ring-wrap">
  1476. <svg viewBox="0 0 44 44">
  1477. <g transform="rotate(-90 22 22)">
  1478. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1479. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#ff4d4f"
  1480. stroke-dasharray="113.1" stroke-dashoffset="81.4"/>
  1481. </g>
  1482. <text x="22" y="22" class="chem-ring-pct" fill="#ff4d4f">27.8%</text>
  1483. </svg>
  1484. </div>
  1485. <div class="chem-stat-info">
  1486. <div class="chem-stat-value" style="color:#ff4d4f">680<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1487. <div class="chem-stat-label">管控类化学品</div>
  1488. </div>
  1489. </div>
  1490. <div class="chem-stat-item">
  1491. <div class="chem-ring-wrap">
  1492. <svg viewBox="0 0 44 44">
  1493. <g transform="rotate(-90 22 22)">
  1494. <circle class="chem-ring-bg" cx="22" cy="22" r="18"/>
  1495. <circle class="chem-ring-fg" cx="22" cy="22" r="18" stroke="#36d399"
  1496. stroke-dasharray="113.1" stroke-dashoffset="31.4"/>
  1497. </g>
  1498. <text x="22" y="22" class="chem-ring-pct" fill="#36d399">72.2%</text>
  1499. </svg>
  1500. </div>
  1501. <div class="chem-stat-info">
  1502. <div class="chem-stat-value" style="color:#36d399">1,770<span style="font-size:10px;color:var(--text-sub)"> L</span></div>
  1503. <div class="chem-stat-label">非管控类化学品</div>
  1504. </div>
  1505. </div>
  1506. <div class="chem-stat-item">
  1507. <div class="chem-stat-info" style="text-align:center;flex:1;">
  1508. <div class="chem-stat-value" style="color:#ffb020;font-size:22px;">156</div>
  1509. <div class="chem-stat-label">存量化学品总类目</div>
  1510. <div class="chem-stat-pct" style="color:var(--text-sub)">管控68类 / 非管控88类</div>
  1511. </div>
  1512. </div>
  1513. </div>
  1514. </div>
  1515. </div>
  1516. </div>
  1517. </div>
  1518. <!-- ===================== PAGE: 视频监控 ===================== -->
  1519. <div class="page" id="page-video">
  1520. <!-- LEFT: Tree -->
  1521. <div class="video-left panel">
  1522. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1523. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1524. <div class="panel-title">建筑结构导航</div>
  1525. <input class="video-search" type="text" placeholder="🔍 搜索楼栋 / 楼层..." />
  1526. <select class="video-filter">
  1527. <option value="">全部二级单位</option>
  1528. <option>安全技术研究所</option>
  1529. <option>职业安全研究所</option>
  1530. <option>化学品安全研究所</option>
  1531. <option>矿山安全研究所</option>
  1532. </select>
  1533. <div class="tree-container" id="treeContainer"></div>
  1534. </div>
  1535. <!-- RIGHT: Video Grid -->
  1536. <div class="video-right">
  1537. <div class="video-breadcrumb">安科院院区 → 科研楼A → 3层</div>
  1538. <div class="video-grid" id="videoGrid"></div>
  1539. <div class="video-pagination">
  1540. <button class="page-btn" onclick="videoPageChange(-1)">◀ 上一页</button>
  1541. <span class="page-info" id="videoPageInfo">第 1 / 3 页</span>
  1542. <button class="page-btn" onclick="videoPageChange(1)">下一页 ▶</button>
  1543. </div>
  1544. </div>
  1545. </div>
  1546. </div>
  1547. </div>
  1548. <!-- ===================== ALERT MODAL ===================== -->
  1549. <div class="alert-overlay" id="alertOverlay">
  1550. <div class="alert-modal">
  1551. <div class="alert-header">
  1552. <span class="alert-icon">⚠</span>
  1553. <span class="alert-title">安 全 预 警</span>
  1554. </div>
  1555. <div class="alert-body">
  1556. <div class="alert-info">
  1557. <div class="alert-info-row"><span class="a-label">实 验 室:</span><span class="a-value" id="alertLab">--</span></div>
  1558. <div class="alert-info-row"><span class="a-label">楼栋楼层:</span><span class="a-value" id="alertBuilding">--</span></div>
  1559. <div class="alert-info-row"><span class="a-label">所属单位:</span><span class="a-value" id="alertUnit">--</span></div>
  1560. <div class="alert-info-row"><span class="a-label">异常参数:</span><span class="a-value danger" id="alertParam">--</span></div>
  1561. <div class="alert-info-row"><span class="a-label">当前数值:</span><span class="a-value danger" id="alertValue">--</span></div>
  1562. <div class="alert-info-row"><span class="a-label">实验室负责人:</span><span class="a-value" id="alertPerson">--</span></div>
  1563. <div class="alert-info-row"><span class="a-label">联系电话:</span><span class="a-value" id="alertPhone">--</span></div>
  1564. <div class="alert-info-row"><span class="a-label">预警时间:</span><span class="a-value" id="alertTime">--</span></div>
  1565. </div>
  1566. <div class="alert-video">
  1567. <div class="alert-video-title">实时监控画面</div>
  1568. <div class="alert-video-feed">
  1569. <span class="cam-label"><span class="rec-dot"></span>REC <span id="alertCamName">--</span></span>
  1570. <span class="cam-placeholder">📹 实时监控画面</span>
  1571. </div>
  1572. </div>
  1573. </div>
  1574. <div class="alert-footer">
  1575. <button class="alert-close-btn" onclick="closeAlert()">确 认</button>
  1576. <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>
  1577. </div>
  1578. </div>
  1579. </div>
  1580. <!-- ===================== EVACUATION MODAL ===================== -->
  1581. <div class="evac-overlay" id="evacOverlay">
  1582. <div class="evac-modal">
  1583. <div class="evac-header">
  1584. <div class="evac-header-left">
  1585. <span class="evac-icon">📋</span>
  1586. <span class="evac-title">应急疏散</span>
  1587. </div>
  1588. <div class="evac-close" onclick="closeEvacuation()">✕</div>
  1589. </div>
  1590. <div class="evac-body">
  1591. <div class="evac-main">
  1592. <div class="evac-legend">
  1593. <div class="evac-legend-item">
  1594. <span class="evac-legend-line" style="background:linear-gradient(90deg,#3a7bff,#48d7ff);"></span>
  1595. <span>应急疏散路线图</span>
  1596. </div>
  1597. <div class="evac-legend-item">
  1598. <span>房间号→实验室名称</span>
  1599. </div>
  1600. </div>
  1601. <div class="evac-floorplan">
  1602. <svg viewBox="0 0 680 360" id="evacFloorSvg">
  1603. <!-- Top row rooms -->
  1604. <g class="evac-room-group" data-room="A101" onclick="selectEvacRoom(this)">
  1605. <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"/>
  1606. <text x="105" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A101</text>
  1607. </g>
  1608. <g class="evac-room-group" data-room="A102" onclick="selectEvacRoom(this)">
  1609. <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"/>
  1610. <text x="245" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A102</text>
  1611. </g>
  1612. <g class="evac-room-group" data-room="A103" onclick="selectEvacRoom(this)">
  1613. <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"/>
  1614. <text x="385" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A103</text>
  1615. </g>
  1616. <g class="evac-room-group" data-room="A104" onclick="selectEvacRoom(this)">
  1617. <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"/>
  1618. <text x="525" y="75" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A104</text>
  1619. </g>
  1620. <!-- Corridor -->
  1621. <text x="340" y="155" text-anchor="middle" fill="rgba(126,172,200,0.5)" font-size="14" letter-spacing="8">走 道</text>
  1622. <circle cx="300" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
  1623. <circle cx="380" cy="155" r="10" fill="none" stroke="rgba(72,180,255,0.15)" stroke-width="1"/>
  1624. <!-- Right exit -->
  1625. <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"/>
  1626. <text x="645" y="80" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
  1627. <text x="645" y="95" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
  1628. <!-- Left exit -->
  1629. <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"/>
  1630. <text x="17" y="255" text-anchor="middle" fill="var(--danger)" font-size="10">紧急</text>
  1631. <text x="17" y="270" text-anchor="middle" fill="var(--danger)" font-size="10">出口</text>
  1632. <!-- Bottom row rooms -->
  1633. <g class="evac-room-group" data-room="A105" onclick="selectEvacRoom(this)">
  1634. <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"/>
  1635. <text x="95" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A105</text>
  1636. </g>
  1637. <g class="evac-room-group" data-room="A106" onclick="selectEvacRoom(this)">
  1638. <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"/>
  1639. <text x="210" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A106</text>
  1640. </g>
  1641. <g class="evac-room-group" data-room="A107" onclick="selectEvacRoom(this)">
  1642. <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"/>
  1643. <text x="320" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A107</text>
  1644. </g>
  1645. <g class="evac-room-group" data-room="A108" onclick="selectEvacRoom(this)">
  1646. <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"/>
  1647. <text x="430" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A108</text>
  1648. </g>
  1649. <g class="evac-room-group" data-room="A109" onclick="selectEvacRoom(this)">
  1650. <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"/>
  1651. <text x="555" y="260" text-anchor="middle" fill="#7eacc8" font-size="13" pointer-events="none">A109</text>
  1652. </g>
  1653. <!-- Evacuation route (drawn dynamically) -->
  1654. <g id="evacRoutes" class="evac-route-arrow"></g>
  1655. <!-- SVG arrow marker -->
  1656. <defs>
  1657. <marker id="evacArrowHead" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
  1658. <polygon points="0,0 10,4 0,8" fill="#3a7bff"/>
  1659. </marker>
  1660. </defs>
  1661. </svg>
  1662. <!-- Alert info card overlay -->
  1663. <div class="evac-alert-card" id="evacAlertCard">
  1664. <div class="eac-header">
  1665. <div class="eac-time"><span class="eac-icon">⚠</span> <span id="evacAlertTime">2026-03-24 14:32:18</span></div>
  1666. <button class="eac-close-btn" onclick="document.getElementById('evacAlertCard').style.display='none'">✕</button>
  1667. </div>
  1668. <div class="eac-badge">触发风险</div>
  1669. <div class="eac-desc">发生风险:TVOC浓度超标</div>
  1670. </div>
  1671. </div>
  1672. </div>
  1673. <div class="evac-sidebar">
  1674. <div class="evac-sidebar-block">
  1675. <div class="evac-sidebar-title">实时视频监控</div>
  1676. <div class="evac-sidebar-label" id="evacSidebarLabel1">楼道 1 层</div>
  1677. <div class="evac-video-placeholder" id="evacSidebarVideo1">📹 实时视频监控</div>
  1678. </div>
  1679. <div class="evac-sidebar-block">
  1680. <div class="evac-sidebar-title">实时视频监控</div>
  1681. <div class="evac-sidebar-label" id="evacSidebarLabel2">楼道 2 层</div>
  1682. <div class="evac-video-placeholder" id="evacSidebarVideo2">📹 实时视频监控</div>
  1683. </div>
  1684. </div>
  1685. </div>
  1686. <div class="evac-footer">
  1687. <div class="evac-metrics">
  1688. <div class="evac-metric-row">
  1689. <span class="evac-metric-label">告警指标</span>
  1690. <span class="evac-metric-value">TVOC 浓度超标</span>
  1691. <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
  1692. </div>
  1693. <div class="evac-metric-row">
  1694. <span class="evac-metric-label">当前值 / 安全阈值</span>
  1695. <span class="evac-metric-value">TVOC 浓度超标</span>
  1696. <span style="color:var(--text-sub);font-size:12px">2.85 / 0.6 mg/m³</span>
  1697. </div>
  1698. </div>
  1699. <div class="evac-broadcast">
  1700. <div class="evac-broadcast-header">
  1701. <div class="evac-broadcast-title">🔊 语音广播</div>
  1702. <div class="evac-broadcast-hint">选择播放设备</div>
  1703. </div>
  1704. <div class="evac-speakers">
  1705. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1706. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1707. <div class="evac-speaker-btn">NKL1FB1122 喇叭</div>
  1708. </div>
  1709. <div class="evac-broadcast-input">
  1710. <input type="text" placeholder="请输入喊话内容" />
  1711. <button>发送</button>
  1712. </div>
  1713. </div>
  1714. <div class="evac-actions">
  1715. <div class="evac-btn-later" onclick="closeEvacuation()">稍后处理</div>
  1716. <div class="evac-btn-exec" onclick="executeEvacuation()">执行疏散</div>
  1717. </div>
  1718. </div>
  1719. </div>
  1720. </div>
  1721. <script>
  1722. /* ================================================================
  1723. JavaScript - 中国安全生产科学研究院实验室安全智慧化管控中心
  1724. ================================================================ */
  1725. // ===================== Viewport Scaling =====================
  1726. function fitScreen() {
  1727. const scr = document.getElementById('mainScreen');
  1728. const sw = 1920, sh = 1080;
  1729. const vw = window.innerWidth, vh = window.innerHeight;
  1730. const scale = Math.min(vw / sw, vh / sh);
  1731. scr.style.transform = `translate(-50%, -50%) scale(${scale})`;
  1732. }
  1733. window.addEventListener('resize', fitScreen);
  1734. fitScreen();
  1735. // ===================== Auto Fullscreen =====================
  1736. document.addEventListener('click', function onFirstClick() {
  1737. if (document.documentElement.requestFullscreen) {
  1738. document.documentElement.requestFullscreen().catch(() => {});
  1739. }
  1740. document.removeEventListener('click', onFirstClick);
  1741. }, { once: true });
  1742. // ===================== Clock =====================
  1743. function updateClock() {
  1744. const now = new Date();
  1745. const hh = String(now.getHours()).padStart(2,'0');
  1746. const mm = String(now.getMinutes()).padStart(2,'0');
  1747. const ss = String(now.getSeconds()).padStart(2,'0');
  1748. document.getElementById('clock').textContent = `${hh}:${mm}:${ss}`;
  1749. const days = ['日','一','二','三','四','五','六'];
  1750. document.getElementById('weekday').textContent = `星期${days[now.getDay()]}`;
  1751. }
  1752. setInterval(updateClock, 1000);
  1753. updateClock();
  1754. // ===================== Page Switch =====================
  1755. function switchPage(page) {
  1756. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1757. document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
  1758. if (page === 'lab') {
  1759. document.getElementById('page-lab').classList.add('active');
  1760. document.querySelectorAll('.nav-tab')[0].classList.add('active');
  1761. setTimeout(() => { initLabCharts(); }, 100);
  1762. } else {
  1763. document.getElementById('page-video').classList.add('active');
  1764. document.querySelectorAll('.nav-tab')[1].classList.add('active');
  1765. }
  1766. }
  1767. // ===================== Flip Counter =====================
  1768. function renderFlip(id, num, len) {
  1769. const str = String(num).padStart(len, '0');
  1770. const container = document.getElementById(id);
  1771. container.innerHTML = str.split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
  1772. }
  1773. renderFlip('flipTotal', 1286, 5);
  1774. renderFlip('flipCurrent', 47, 4);
  1775. // ===================== Sensor Scroll Data =====================
  1776. const labNames = [
  1777. { name: '化学实验室A101(A101)', unit: '化学品安全研究所' },
  1778. { name: '材料力学实验室B203(B203)', unit: '安全技术研究所' },
  1779. { name: '气体分析实验室A305(A305)', unit: '职业安全研究所' },
  1780. { name: '高温高压实验室C102(C102)', unit: '矿山安全研究所' },
  1781. { name: '生物安全实验室A202(A202)', unit: '职业安全研究所' },
  1782. { name: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所' },
  1783. { name: '电气安全实验室C201(C201)', unit: '安全技术研究所' },
  1784. { name: '环境模拟实验室A408(A408)', unit: '化学品安全研究所' },
  1785. { name: '爆炸安全实验室B302(B302)', unit: '安全技术研究所' },
  1786. { name: '应急救援实验室C305(C305)', unit: '职业安全研究所' },
  1787. { name: '有机化学实验室A103(A103)', unit: '化学品安全研究所' },
  1788. { name: '无损检测实验室B205(B205)', unit: '安全技术研究所' },
  1789. ];
  1790. const sensorTypes = ['温度','湿度','TVOC','CO₂','O₂'];
  1791. function randVal(type) {
  1792. switch(type) {
  1793. case '温度': return (18 + Math.random() * 15).toFixed(1) + '°C';
  1794. case '湿度': return (30 + Math.random() * 50).toFixed(0) + '%';
  1795. case 'TVOC': return (Math.random() * 2).toFixed(2) + 'mg/m³';
  1796. case 'CO₂': return (300 + Math.random() * 800).toFixed(0) + 'ppm';
  1797. case 'O₂': return (18 + Math.random() * 4).toFixed(1) + '%';
  1798. default: return '--';
  1799. }
  1800. }
  1801. function isAlarm(type, val) {
  1802. const n = parseFloat(val);
  1803. if (type === '温度' && n > 30) return true;
  1804. if (type === 'TVOC' && n > 1.5) return true;
  1805. if (type === 'CO₂' && n > 900) return true;
  1806. if (type === 'O₂' && n < 19.5) return true;
  1807. return false;
  1808. }
  1809. function buildSensorList() {
  1810. const el = document.getElementById('sensorList');
  1811. let html = '';
  1812. const items = [...labNames, ...labNames]; // duplicate for seamless scroll
  1813. items.forEach(lab => {
  1814. html += `<div class="sensor-item"><div class="lab-name">${lab.name}<span class="lab-unit">${lab.unit}</span></div><div class="sensor-values">`;
  1815. sensorTypes.forEach(s => {
  1816. const v = randVal(s);
  1817. const alarm = isAlarm(s, v);
  1818. html += `<span class="sensor-val${alarm ? ' alarm' : ''}">${alarm ? '<span class="alarm-icon">⚠</span>' : ''}${s}: ${v}</span>`;
  1819. });
  1820. html += '</div></div>';
  1821. });
  1822. el.innerHTML = html;
  1823. }
  1824. buildSensorList();
  1825. // ===================== Warning Scroll Data =====================
  1826. function buildWarningList() {
  1827. const el = document.getElementById('warningList');
  1828. const warnings = [
  1829. { type: 'sensor', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-24 14:32:18' },
  1830. { type: 'chem', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', person: '王伟', time: '2026-03-24 14:28:45' },
  1831. { type: 'sensor', lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-24 14:28:05' },
  1832. { type: 'chem', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', person: '李明', time: '2026-03-24 14:15:33' },
  1833. { type: 'sensor', lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-24 13:55:42' },
  1834. { type: 'sensor', lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-24 13:20:10' },
  1835. { type: 'chem', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', person: '赵磊', time: '2026-03-24 13:10:22' },
  1836. { type: 'sensor', lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-24 12:45:33' },
  1837. { type: 'chem', lab: '材料力学实验室B203(B203)', unit: '安全技术研究所', person: '陈静', time: '2026-03-24 12:30:18' },
  1838. { type: 'sensor', lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-24 11:18:27' },
  1839. { type: 'sensor', lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-24 10:42:51' },
  1840. { type: 'chem', lab: '化学实验室A101(A101)', unit: '化学品安全研究所', person: '张强', time: '2026-03-24 10:15:07' },
  1841. { type: 'sensor', lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-24 09:36:14' },
  1842. ];
  1843. const all = [...warnings, ...warnings]; // duplicate for seamless scroll
  1844. let html = '';
  1845. all.forEach(w => {
  1846. if (w.type === 'chem') {
  1847. html += `<div class="warning-item chem-violation">
  1848. <div class="w-lab">${w.lab} - ${w.unit}</div>
  1849. <div class="w-sensor">化学品违规带离: ${w.person} 未正常使用违规带离</div>
  1850. <div class="w-time">${w.time}</div>
  1851. </div>`;
  1852. } else {
  1853. html += `<div class="warning-item">
  1854. <div class="w-lab">${w.lab} - ${w.unit}</div>
  1855. <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
  1856. <div class="w-time">${w.time}</div>
  1857. </div>`;
  1858. }
  1859. });
  1860. el.innerHTML = html;
  1861. document.getElementById('warningCount').textContent = warnings.length;
  1862. }
  1863. buildWarningList();
  1864. // ===================== Video Page: Tree =====================
  1865. const treeData = {
  1866. name: '安科院院区', children: [
  1867. { name: '科研楼A', children: [
  1868. { name: '1层', children: [{name:'A101'},{name:'A102'},{name:'A103'}] },
  1869. { name: '2层', children: [{name:'A201'},{name:'A202'},{name:'A203'}] },
  1870. { name: '3层', children: [{name:'A301'},{name:'A302'},{name:'A303'},{name:'A304'},{name:'A305'}] },
  1871. { name: '4层', children: [{name:'A401'},{name:'A402'},{name:'A403'},{name:'A408'}] },
  1872. ]},
  1873. { name: '科研楼B', children: [
  1874. { name: '1层', children: [{name:'B101'},{name:'B102'}] },
  1875. { name: '2层', children: [{name:'B201'},{name:'B202'},{name:'B203'},{name:'B205'}] },
  1876. { name: '3层', children: [{name:'B301'},{name:'B302'}] },
  1877. ]},
  1878. { name: '科研楼C', children: [
  1879. { name: '1层', children: [{name:'C101'},{name:'C102'}] },
  1880. { name: '2层', children: [{name:'C201'},{name:'C202'}] },
  1881. { name: '3层', children: [{name:'C301'},{name:'C305'}] },
  1882. ]},
  1883. ]
  1884. };
  1885. function buildTree(node, depth) {
  1886. const hasChild = node.children && node.children.length > 0;
  1887. let html = `<div class="tree-node">
  1888. <div class="tree-node-content" style="padding-left:${depth * 12}px" onclick="toggleTree(this)">
  1889. <span class="tree-arrow">${hasChild ? '▶' : ''}</span>
  1890. <span>${node.name}</span>
  1891. </div>`;
  1892. if (hasChild) {
  1893. html += `<div class="tree-children">`;
  1894. node.children.forEach(c => { html += buildTree(c, depth + 1); });
  1895. html += `</div>`;
  1896. }
  1897. html += `</div>`;
  1898. return html;
  1899. }
  1900. document.getElementById('treeContainer').innerHTML = buildTree(treeData, 0);
  1901. function toggleTree(el) {
  1902. const children = el.parentElement.querySelector('.tree-children');
  1903. const arrow = el.querySelector('.tree-arrow');
  1904. if (children) {
  1905. children.classList.toggle('open');
  1906. arrow.classList.toggle('expanded');
  1907. }
  1908. // select highlight
  1909. document.querySelectorAll('.tree-node-content').forEach(n => n.classList.remove('selected'));
  1910. el.classList.add('selected');
  1911. }
  1912. // ===================== Video Grid =====================
  1913. let currentVideoPage = 1;
  1914. const totalVideoPages = 3;
  1915. function renderVideoGrid() {
  1916. const grid = document.getElementById('videoGrid');
  1917. let html = '';
  1918. for (let i = 0; i < 9; i++) {
  1919. const roomNum = (currentVideoPage - 1) * 9 + i + 1;
  1920. const isAI = i === 0;
  1921. html += `<div class="video-cell${isAI ? ' ai-cam' : ''}">
  1922. <div class="cam-frame">
  1923. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1924. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1925. ${isAI ? '<span class="ai-badge">AI 智能</span>' : ''}
  1926. <div class="cam-inner">
  1927. <span class="cam-label"><span class="cam-dot"></span>${isAI ? '🎯 ' : ''}A3${String(roomNum).padStart(2,'0')} 摄像头</span>
  1928. <span class="cam-placeholder">📹 实时监控画面</span>
  1929. </div>
  1930. </div>
  1931. </div>`;
  1932. }
  1933. grid.innerHTML = html;
  1934. document.getElementById('videoPageInfo').textContent = `第 ${currentVideoPage} / ${totalVideoPages} 页`;
  1935. }
  1936. renderVideoGrid();
  1937. function videoPageChange(dir) {
  1938. currentVideoPage = Math.max(1, Math.min(totalVideoPages, currentVideoPage + dir));
  1939. renderVideoGrid();
  1940. }
  1941. // ===================== Alert Modal =====================
  1942. function showAlert(lab, building, unit, param, value, person, phone) {
  1943. document.getElementById('alertLab').textContent = lab;
  1944. document.getElementById('alertBuilding').textContent = building || '--';
  1945. document.getElementById('alertUnit').textContent = unit;
  1946. document.getElementById('alertParam').textContent = param;
  1947. document.getElementById('alertValue').textContent = value;
  1948. document.getElementById('alertPerson').textContent = person || '--';
  1949. document.getElementById('alertPhone').textContent = phone || '--';
  1950. document.getElementById('alertCamName').textContent = lab;
  1951. const now = new Date();
  1952. document.getElementById('alertTime').textContent =
  1953. `${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')}`;
  1954. document.getElementById('alertOverlay').classList.add('show');
  1955. }
  1956. function closeAlert() {
  1957. document.getElementById('alertOverlay').classList.remove('show');
  1958. }
  1959. // Demo: trigger alert after 15s
  1960. setTimeout(() => {
  1961. showAlert('化学实验室A101(A101)', '科研楼A-1层', '化学品安全研究所', 'TVOC 超标', '1.82 mg/m³', '张明', '138-0012-3456');
  1962. }, 15000);
  1963. // ===================== ECharts Initialization =====================
  1964. let chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie;
  1965. function initLabCharts() {
  1966. // ---- 1. Lab Overview Ring ----
  1967. if (chartLabOverview) chartLabOverview.dispose();
  1968. chartLabOverview = echarts.init(document.getElementById('chartLabOverview'));
  1969. chartLabOverview.setOption({
  1970. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1971. legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  1972. series: [{
  1973. type: 'pie', radius: ['45%','70%'], center: ['35%','50%'],
  1974. label: {
  1975. show: true, position: 'center',
  1976. formatter: '58\n实验室总数', fontSize: 14, color: '#fff', lineHeight: 20
  1977. },
  1978. data: [
  1979. { value: 8, name: 'I级(红)', itemStyle: { color: '#ff4d4f' } },
  1980. { value: 12, name: 'II级(橙)', itemStyle: { color: '#ff8c00' } },
  1981. { value: 18, name: 'III级(黄)', itemStyle: { color: '#ffcc00' } },
  1982. { value: 20, name: 'IV级(蓝)', itemStyle: { color: '#3a7bff' } },
  1983. ],
  1984. emphasis: { scaleSize: 6 }
  1985. }]
  1986. });
  1987. // ---- 2. Grade Stacked Bar ----
  1988. if (chartGradeBar) chartGradeBar.dispose();
  1989. chartGradeBar = echarts.init(document.getElementById('chartGradeBar'));
  1990. const units = ['安全技术\n研究所','职业安全\n研究所','化学品安全\n研究所','矿山安全\n研究所','应急科学\n研究中心','信息技术\n研究所','检测检验\n中心','标准化\n研究所'];
  1991. const totals = [12, 10, 14, 8, 6, 4, 3, 1];
  1992. const gradeData = {
  1993. 'I级': [2,1,3,2,1,0,0,0],
  1994. 'II级': [3,2,4,2,1,1,1,0],
  1995. 'III级': [4,4,4,2,2,2,1,1],
  1996. 'IV级': [3,3,3,2,2,1,1,0]
  1997. };
  1998. const colors = { 'I级': '#ff4d4f', 'II级': '#ff8c00', 'III级': '#ffcc00', 'IV级': '#3a7bff' };
  1999. let barOption = {
  2000. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  2001. legend: { data: Object.keys(gradeData), top: 0, textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  2002. grid: { left: 40, right: 10, top: 35, bottom: 50 },
  2003. xAxis: {
  2004. type: 'category', data: units,
  2005. axisLabel: {
  2006. color: '#7eacc8', fontSize: 10, interval: 0,
  2007. formatter: function(v, idx) { return v + '\n(' + totals[idx] + ')'; }
  2008. },
  2009. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  2010. axisTick: { show: false }
  2011. },
  2012. yAxis: {
  2013. type: 'value',
  2014. axisLabel: { color: '#7eacc8', fontSize: 10 },
  2015. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  2016. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  2017. },
  2018. dataZoom: [{
  2019. type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5
  2020. }],
  2021. series: Object.keys(gradeData).map(k => ({
  2022. name: k, type: 'bar', stack: 'total', barWidth: 20,
  2023. data: gradeData[k],
  2024. itemStyle: { color: colors[k], borderRadius: [0,0,0,0] }
  2025. }))
  2026. };
  2027. chartGradeBar.setOption(barOption);
  2028. // Auto scroll X axis
  2029. let barScrollIdx = 0;
  2030. setInterval(() => {
  2031. barScrollIdx++;
  2032. if (barScrollIdx > units.length - 5) barScrollIdx = 0;
  2033. chartGradeBar.dispatchAction({ type: 'dataZoom', startValue: barScrollIdx, endValue: barScrollIdx + 5 });
  2034. }, 5000);
  2035. // ---- 3. People Line Chart ----
  2036. if (chartPeopleLine) chartPeopleLine.dispose();
  2037. chartPeopleLine = echarts.init(document.getElementById('chartPeopleLine'));
  2038. const timeSlots = ['0:00','3:00','6:00','9:00','12:00','15:00','18:00','21:00','24:00'];
  2039. chartPeopleLine.setOption({
  2040. tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  2041. legend: { data: ['进入人数','实验人数'], top: 0, right: 0, textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 14, itemHeight: 2 },
  2042. grid: { left: 35, right: 10, top: 24, bottom: 20 },
  2043. xAxis: {
  2044. type: 'category', data: timeSlots, boundaryGap: false,
  2045. axisLabel: { color: '#7eacc8', fontSize: 10 },
  2046. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } }
  2047. },
  2048. yAxis: {
  2049. type: 'value',
  2050. axisLabel: { color: '#7eacc8', fontSize: 10 },
  2051. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  2052. },
  2053. series: [
  2054. {
  2055. name: '进入人数', type: 'line', smooth: true,
  2056. data: [0, 2, 5, 86, 120, 145, 98, 42, 0],
  2057. lineStyle: { color: '#48d7ff', width: 2 },
  2058. 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)'}]) },
  2059. itemStyle: { color: '#48d7ff' }, symbolSize: 4
  2060. },
  2061. {
  2062. name: '实验人数', type: 'line', smooth: true,
  2063. data: [0, 1, 3, 52, 68, 78, 55, 20, 0],
  2064. lineStyle: { color: '#3a7bff', width: 2 },
  2065. 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)'}]) },
  2066. itemStyle: { color: '#3a7bff' }, symbolSize: 4
  2067. }
  2068. ]
  2069. });
  2070. // ---- 4. Device Gauge ----
  2071. if (chartGauge) chartGauge.dispose();
  2072. chartGauge = echarts.init(document.getElementById('chartGauge'));
  2073. chartGauge.setOption({
  2074. series: [{
  2075. type: 'gauge', startAngle: 210, endAngle: -30,
  2076. radius: '90%', center: ['50%','58%'],
  2077. min: 0, max: 100,
  2078. axisLine: {
  2079. lineStyle: {
  2080. width: 14,
  2081. color: [[0.7,'rgba(72,215,255,0.2)'],[0.9,'rgba(72,215,255,0.4)'],[1,'#48d7ff']]
  2082. }
  2083. },
  2084. axisTick: { show: false },
  2085. splitLine: { show: false },
  2086. axisLabel: { show: false },
  2087. pointer: {
  2088. length: '60%', width: 4,
  2089. itemStyle: { color: '#48d7ff' }
  2090. },
  2091. title: { show: true, offsetCenter: [0, '72%'], fontSize: 12, color: '#7eacc8' },
  2092. detail: {
  2093. valueAnimation: true, fontSize: 26, color: '#48d7ff', fontWeight: 700,
  2094. offsetCenter: [0, '40%'],
  2095. formatter: '{value}%'
  2096. },
  2097. data: [{ value: 93, name: '设备在线率' }]
  2098. }]
  2099. });
  2100. // ---- 5. Equipment Pie ----
  2101. if (chartEquipPie) chartEquipPie.dispose();
  2102. chartEquipPie = echarts.init(document.getElementById('chartEquipPie'));
  2103. chartEquipPie.setOption({
  2104. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  2105. legend: { orient: 'vertical', right: 4, top: 'center', textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 8, itemHeight: 8 },
  2106. series: [{
  2107. type: 'pie', radius: ['30%', '60%'], center: ['35%','50%'],
  2108. label: { show: false },
  2109. data: [
  2110. { value: 120, name: '分析仪器', itemStyle: { color: '#48d7ff' } },
  2111. { value: 95, name: '安全防护', itemStyle: { color: '#3a7bff' } },
  2112. { value: 86, name: '化学试剂设备', itemStyle: { color: '#36d399' } },
  2113. { value: 78, name: '电气设备', itemStyle: { color: '#ffb020' } },
  2114. { value: 65, name: '力学测试', itemStyle: { color: '#ff8c00' } },
  2115. { value: 52, name: '环境监测', itemStyle: { color: '#ff4d4f' } },
  2116. { value: 90, name: '通用设备', itemStyle: { color: '#a78bfa' } },
  2117. ],
  2118. emphasis: { scaleSize: 4 }
  2119. }]
  2120. });
  2121. }
  2122. // Init charts on load
  2123. setTimeout(initLabCharts, 200);
  2124. // Handle resize
  2125. window.addEventListener('resize', () => {
  2126. [chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie].forEach(c => c && c.resize());
  2127. });
  2128. // ===================== Evacuation Modal =====================
  2129. // Room name mapping
  2130. const evacRoomNames = {
  2131. A101: '化学实验室A101', A102: '材料分析实验室A102',
  2132. A103: '有机化学实验室A103', A104: '气相色谱实验室A104',
  2133. A105: '粉尘检测实验室A105', A106: '光谱分析实验室A106',
  2134. A107: '质谱分析实验室A107', A108: '样品前处理室A108',
  2135. A109: '大型仪器室A109'
  2136. };
  2137. // Room center coordinates for route drawing
  2138. const evacRoomPos = {
  2139. // Top row: room center X, corridor Y entry point
  2140. A101: { cx: 105, cy: 70, row: 'top', exitX: 105, corridorY: 130 },
  2141. A102: { cx: 245, cy: 70, row: 'top', exitX: 245, corridorY: 130 },
  2142. A103: { cx: 385, cy: 70, row: 'top', exitX: 385, corridorY: 130 },
  2143. A104: { cx: 525, cy: 70, row: 'top', exitX: 525, corridorY: 130 },
  2144. // Bottom row: room center X, corridor Y entry point
  2145. A105: { cx: 95, cy: 255, row: 'bottom', exitX: 95, corridorY: 185 },
  2146. A106: { cx: 210, cy: 255, row: 'bottom', exitX: 210, corridorY: 185 },
  2147. A107: { cx: 320, cy: 255, row: 'bottom', exitX: 320, corridorY: 185 },
  2148. A108: { cx: 430, cy: 255, row: 'bottom', exitX: 430, corridorY: 185 },
  2149. A109: { cx: 555, cy: 255, row: 'bottom', exitX: 555, corridorY: 185 },
  2150. };
  2151. // Current alarm room (default from the alert)
  2152. let evacAlarmRoom = 'A105';
  2153. function openEvacuation() {
  2154. document.getElementById('evacOverlay').classList.add('show');
  2155. // Reset state
  2156. document.getElementById('evacRoutes').innerHTML = '';
  2157. document.getElementById('evacRoutes').classList.remove('visible');
  2158. document.getElementById('evacAlertCard').style.display = 'block';
  2159. // Clear room selections & alarm highlights
  2160. document.querySelectorAll('.evac-room-group').forEach(g => {
  2161. g.classList.remove('selected', 'alarm');
  2162. });
  2163. // Reset sidebar
  2164. document.getElementById('evacSidebarLabel1').textContent = '楼道 1 层';
  2165. document.getElementById('evacSidebarVideo1').textContent = '📹 实时视频监控';
  2166. document.getElementById('evacSidebarLabel2').textContent = '楼道 2 层';
  2167. document.getElementById('evacSidebarVideo2').textContent = '📹 实时视频监控';
  2168. // Update time
  2169. const now = new Date();
  2170. 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')}`;
  2171. document.getElementById('evacAlertTime').textContent = ts;
  2172. }
  2173. function closeEvacuation() {
  2174. document.getElementById('evacOverlay').classList.remove('show');
  2175. }
  2176. function selectEvacRoom(el) {
  2177. // Clear previous selection (keep alarm class)
  2178. document.querySelectorAll('.evac-room-group.selected').forEach(g => g.classList.remove('selected'));
  2179. el.classList.add('selected');
  2180. const room = el.getAttribute('data-room');
  2181. const roomName = evacRoomNames[room] || room;
  2182. // Update sidebar: top block shows selected room camera, bottom stays corridor
  2183. document.getElementById('evacSidebarLabel1').textContent = roomName;
  2184. document.getElementById('evacSidebarVideo1').textContent = '📹 ' + room + ' 实时监控画面';
  2185. document.getElementById('evacSidebarLabel2').textContent = '楼道走廊';
  2186. document.getElementById('evacSidebarVideo2').textContent = '📹 走廊实时监控画面';
  2187. }
  2188. function executeEvacuation() {
  2189. const routesG = document.getElementById('evacRoutes');
  2190. routesG.innerHTML = ''; // Clear previous
  2191. // Mark alarm room
  2192. document.querySelectorAll('.evac-room-group').forEach(g => g.classList.remove('alarm'));
  2193. const alarmEl = document.querySelector(`.evac-room-group[data-room="${evacAlarmRoom}"]`);
  2194. if (alarmEl) alarmEl.classList.add('alarm');
  2195. const pos = evacRoomPos[evacAlarmRoom];
  2196. if (!pos) return;
  2197. // Build polyline path: room -> corridor -> along corridor -> exit
  2198. let pathPoints = [];
  2199. if (pos.row === 'top') {
  2200. // Top row: go down to corridor, then right to exit
  2201. pathPoints = [
  2202. [pos.exitX, pos.cy + 40], // bottom edge of room
  2203. [pos.exitX, pos.corridorY], // enter corridor
  2204. [620, pos.corridorY], // along corridor to right
  2205. [620, 85], // up to exit
  2206. [640, 85], // into exit
  2207. ];
  2208. } else {
  2209. // Bottom row: go up to corridor, then left to exit
  2210. pathPoints = [
  2211. [pos.exitX, pos.cy - 55], // top edge of room
  2212. [pos.exitX, pos.corridorY], // enter corridor
  2213. [35, pos.corridorY], // along corridor to left
  2214. [35, 260], // down to exit
  2215. [17, 260], // into exit
  2216. ];
  2217. }
  2218. // Draw animated polyline
  2219. const pointsStr = pathPoints.map(p => p.join(',')).join(' ');
  2220. const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
  2221. polyline.setAttribute('points', pointsStr);
  2222. polyline.setAttribute('fill', 'none');
  2223. polyline.setAttribute('stroke', '#3a7bff');
  2224. polyline.setAttribute('stroke-width', '3');
  2225. polyline.setAttribute('stroke-dasharray', '8,4');
  2226. polyline.setAttribute('marker-end', 'url(#evacArrowHead)');
  2227. routesG.appendChild(polyline);
  2228. // Add glow effect polyline
  2229. const glowLine = polyline.cloneNode();
  2230. glowLine.setAttribute('stroke', 'rgba(58,123,255,0.3)');
  2231. glowLine.setAttribute('stroke-width', '8');
  2232. glowLine.removeAttribute('stroke-dasharray');
  2233. glowLine.removeAttribute('marker-end');
  2234. routesG.insertBefore(glowLine, polyline);
  2235. // Show with animation
  2236. routesG.classList.add('visible');
  2237. }
  2238. </script>
  2239. </body>
  2240. </html>