agentDemo_dashboard.html 76 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760
  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. </style>
  1085. </head>
  1086. <body>
  1087. <div class="viewport">
  1088. <div class="screen" id="mainScreen">
  1089. <!-- ===================== TOP NAV ===================== -->
  1090. <div class="top-nav">
  1091. <div class="nav-title">中国安全生产科学研究院实验室安全智慧化管控中心</div>
  1092. <div class="nav-tabs">
  1093. <div class="nav-tab active" onclick="switchPage('lab')">实验室情况</div>
  1094. <div class="nav-tab" onclick="switchPage('video')">视频监控</div>
  1095. </div>
  1096. <div class="nav-right">
  1097. <span class="weather">☁ 北京 · 晴 18°C</span>
  1098. <span id="weekday"></span>
  1099. <span class="clock" id="clock"></span>
  1100. </div>
  1101. </div>
  1102. <!-- ===================== PAGE: 实验室情况 ===================== -->
  1103. <div class="page active" id="page-lab">
  1104. <!-- LEFT COLUMN -->
  1105. <div class="col-left">
  1106. <!-- 实验室基本情况统计 -->
  1107. <div class="panel" style="height: 260px;">
  1108. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1109. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1110. <div class="panel-title">实验室基本情况统计</div>
  1111. <div id="chartLabOverview" style="width:100%;height:150px;"></div>
  1112. <div class="status-cards">
  1113. <div class="status-card using">
  1114. <div class="label">使用</div><div class="value">20<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1115. </div>
  1116. <div class="status-card error">
  1117. <div class="label">异常</div><div class="value">3<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1118. </div>
  1119. <div class="status-card idle">
  1120. <div class="label">空闲</div><div class="value">35<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
  1121. </div>
  1122. </div>
  1123. </div>
  1124. <!-- 实验室安全分级统计 -->
  1125. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1126. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1127. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1128. <div class="panel-title">实验室安全分级统计</div>
  1129. <div id="chartGradeBar" style="width:100%;flex:1;min-height:0;"></div>
  1130. </div>
  1131. <!-- 实验室进入人数统计及走势 -->
  1132. <div class="panel" style="height: 260px;">
  1133. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1134. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1135. <div class="panel-title">实验室进入人数统计及走势</div>
  1136. <div class="flip-counter-row">
  1137. <div class="flip-group">
  1138. <div class="flip-label">今日总进入人数</div>
  1139. <div class="flip-digits" id="flipTotal"></div>
  1140. </div>
  1141. <div class="flip-group">
  1142. <div class="flip-label">当前正在实验人数</div>
  1143. <div class="flip-digits" id="flipCurrent"></div>
  1144. </div>
  1145. </div>
  1146. <div id="chartPeopleLine" style="width:100%;height:130px;"></div>
  1147. </div>
  1148. </div>
  1149. <!-- CENTER COLUMN -->
  1150. <div class="col-center">
  1151. <!-- 实验环境安全智能感知 -->
  1152. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1153. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1154. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1155. <div class="panel-title">实验环境安全智能感知</div>
  1156. <div class="sensor-scroll-wrap">
  1157. <div class="sensor-scroll-inner" id="sensorList"></div>
  1158. </div>
  1159. </div>
  1160. <!-- 实验室实时风险预警 -->
  1161. <div class="panel" style="height: 320px; display:flex; flex-direction:column;">
  1162. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1163. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1164. <div class="panel-title">实验室实时风险预警</div>
  1165. <div class="warning-header">
  1166. <div class="warning-count" id="warningCount">12</div>
  1167. <div class="warning-count-label">本月预警响应总数</div>
  1168. </div>
  1169. <div class="warning-scroll-wrap">
  1170. <div class="warning-scroll-inner" id="warningList"></div>
  1171. </div>
  1172. </div>
  1173. </div>
  1174. <!-- RIGHT COLUMN -->
  1175. <div class="col-right">
  1176. <!-- 智能环境感知应用设备统计 -->
  1177. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1178. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1179. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1180. <div class="panel-title">智能环境感知应用设备统计</div>
  1181. <div class="device-top">
  1182. <div class="device-stat-card">
  1183. <div class="ds-label">在线设备</div><div class="ds-value">186</div>
  1184. </div>
  1185. <div class="device-stat-card offline">
  1186. <div class="ds-label">离线设备</div><div class="ds-value">14</div>
  1187. </div>
  1188. </div>
  1189. <div class="device-bottom">
  1190. <div class="device-gauge-wrap" id="chartGauge"></div>
  1191. <div class="device-type-list">
  1192. <div class="device-type-item"><span class="dt-name">电子信息铭牌</span><span class="dt-value">58</span></div>
  1193. <div class="device-type-item"><span class="dt-name">化学品智能终端</span><span class="dt-value">32</span></div>
  1194. <div class="device-type-item"><span class="dt-name">传感器</span><span class="dt-value">76</span></div>
  1195. <div class="device-type-item"><span class="dt-name">智能设备</span><span class="dt-value">34</span></div>
  1196. </div>
  1197. </div>
  1198. </div>
  1199. <!-- 实验室设备分类及使用统计 -->
  1200. <div class="panel" style="flex:1;display:flex;flex-direction:column;">
  1201. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1202. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1203. <div class="panel-title">实验室设备分类及使用统计</div>
  1204. <div class="equip-chart-wrap">
  1205. <div id="chartEquipPie" style="width:100%;height:100%;"></div>
  1206. </div>
  1207. <div class="equip-mid">
  1208. <div class="equip-mid-item"><div class="em-value">586</div><div class="em-label">设备总数</div></div>
  1209. <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>
  1210. <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>
  1211. </div>
  1212. <div class="equip-bottom">
  1213. <div class="equip-status-card"><div class="es-value" style="color:var(--accent)">128</div><div class="es-label">使用</div></div>
  1214. <div class="equip-status-card"><div class="es-value" style="color:var(--good)">312</div><div class="es-label">空闲</div></div>
  1215. <div class="equip-status-card"><div class="es-value" style="color:var(--warn)">108</div><div class="es-label">正常</div></div>
  1216. <div class="equip-status-card"><div class="es-value" style="color:var(--danger)">38</div><div class="es-label">维修</div></div>
  1217. </div>
  1218. </div>
  1219. </div>
  1220. </div>
  1221. <!-- ===================== PAGE: 视频监控 ===================== -->
  1222. <div class="page" id="page-video">
  1223. <!-- LEFT: Tree -->
  1224. <div class="video-left panel">
  1225. <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
  1226. <div class="corner-deco bl"></div><div class="corner-deco br"></div>
  1227. <div class="panel-title">建筑结构导航</div>
  1228. <input class="video-search" type="text" placeholder="🔍 搜索楼栋 / 楼层..." />
  1229. <select class="video-filter">
  1230. <option value="">全部二级单位</option>
  1231. <option>安全技术研究所</option>
  1232. <option>职业安全研究所</option>
  1233. <option>化学品安全研究所</option>
  1234. <option>矿山安全研究所</option>
  1235. </select>
  1236. <div class="tree-container" id="treeContainer"></div>
  1237. </div>
  1238. <!-- RIGHT: Video Grid -->
  1239. <div class="video-right">
  1240. <div class="video-breadcrumb">安科院院区 → 科研楼A → 3层</div>
  1241. <div class="video-grid" id="videoGrid"></div>
  1242. <div class="video-pagination">
  1243. <button class="page-btn" onclick="videoPageChange(-1)">◀ 上一页</button>
  1244. <span class="page-info" id="videoPageInfo">第 1 / 3 页</span>
  1245. <button class="page-btn" onclick="videoPageChange(1)">下一页 ▶</button>
  1246. </div>
  1247. </div>
  1248. </div>
  1249. </div>
  1250. </div>
  1251. <!-- ===================== ALERT MODAL ===================== -->
  1252. <div class="alert-overlay" id="alertOverlay">
  1253. <div class="alert-modal">
  1254. <div class="alert-header">
  1255. <span class="alert-icon">⚠</span>
  1256. <span class="alert-title">安 全 预 警</span>
  1257. </div>
  1258. <div class="alert-body">
  1259. <div class="alert-info">
  1260. <div class="alert-info-row"><span class="a-label">实 验 室:</span><span class="a-value" id="alertLab">--</span></div>
  1261. <div class="alert-info-row"><span class="a-label">楼栋楼层:</span><span class="a-value" id="alertBuilding">--</span></div>
  1262. <div class="alert-info-row"><span class="a-label">所属单位:</span><span class="a-value" id="alertUnit">--</span></div>
  1263. <div class="alert-info-row"><span class="a-label">异常参数:</span><span class="a-value danger" id="alertParam">--</span></div>
  1264. <div class="alert-info-row"><span class="a-label">当前数值:</span><span class="a-value danger" id="alertValue">--</span></div>
  1265. <div class="alert-info-row"><span class="a-label">实验室负责人:</span><span class="a-value" id="alertPerson">--</span></div>
  1266. <div class="alert-info-row"><span class="a-label">联系电话:</span><span class="a-value" id="alertPhone">--</span></div>
  1267. <div class="alert-info-row"><span class="a-label">预警时间:</span><span class="a-value" id="alertTime">--</span></div>
  1268. </div>
  1269. <div class="alert-video">
  1270. <div class="alert-video-title">实时监控画面</div>
  1271. <div class="alert-video-feed">
  1272. <span class="cam-label"><span class="rec-dot"></span>REC <span id="alertCamName">--</span></span>
  1273. <span class="cam-placeholder">📹 实时监控画面</span>
  1274. </div>
  1275. </div>
  1276. </div>
  1277. <div class="alert-footer">
  1278. <button class="alert-close-btn" onclick="closeAlert()">确 认</button>
  1279. </div>
  1280. </div>
  1281. </div>
  1282. <script>
  1283. /* ================================================================
  1284. JavaScript - 中国安全生产科学研究院实验室安全智慧化管控中心
  1285. ================================================================ */
  1286. // ===================== Viewport Scaling =====================
  1287. function fitScreen() {
  1288. const scr = document.getElementById('mainScreen');
  1289. const sw = 1920, sh = 1080;
  1290. const vw = window.innerWidth, vh = window.innerHeight;
  1291. const scale = Math.min(vw / sw, vh / sh);
  1292. scr.style.transform = `translate(-50%, -50%) scale(${scale})`;
  1293. }
  1294. window.addEventListener('resize', fitScreen);
  1295. fitScreen();
  1296. // ===================== Auto Fullscreen =====================
  1297. document.addEventListener('click', function onFirstClick() {
  1298. if (document.documentElement.requestFullscreen) {
  1299. document.documentElement.requestFullscreen().catch(() => {});
  1300. }
  1301. document.removeEventListener('click', onFirstClick);
  1302. }, { once: true });
  1303. // ===================== Clock =====================
  1304. function updateClock() {
  1305. const now = new Date();
  1306. const hh = String(now.getHours()).padStart(2,'0');
  1307. const mm = String(now.getMinutes()).padStart(2,'0');
  1308. const ss = String(now.getSeconds()).padStart(2,'0');
  1309. document.getElementById('clock').textContent = `${hh}:${mm}:${ss}`;
  1310. const days = ['日','一','二','三','四','五','六'];
  1311. document.getElementById('weekday').textContent = `星期${days[now.getDay()]}`;
  1312. }
  1313. setInterval(updateClock, 1000);
  1314. updateClock();
  1315. // ===================== Page Switch =====================
  1316. function switchPage(page) {
  1317. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  1318. document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
  1319. if (page === 'lab') {
  1320. document.getElementById('page-lab').classList.add('active');
  1321. document.querySelectorAll('.nav-tab')[0].classList.add('active');
  1322. setTimeout(() => { initLabCharts(); }, 100);
  1323. } else {
  1324. document.getElementById('page-video').classList.add('active');
  1325. document.querySelectorAll('.nav-tab')[1].classList.add('active');
  1326. }
  1327. }
  1328. // ===================== Flip Counter =====================
  1329. function renderFlip(id, num, len) {
  1330. const str = String(num).padStart(len, '0');
  1331. const container = document.getElementById(id);
  1332. container.innerHTML = str.split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
  1333. }
  1334. renderFlip('flipTotal', 1286, 5);
  1335. renderFlip('flipCurrent', 47, 4);
  1336. // ===================== Sensor Scroll Data =====================
  1337. const labNames = [
  1338. { name: '化学实验室A101(A101)', unit: '化学品安全研究所' },
  1339. { name: '材料力学实验室B203(B203)', unit: '安全技术研究所' },
  1340. { name: '气体分析实验室A305(A305)', unit: '职业安全研究所' },
  1341. { name: '高温高压实验室C102(C102)', unit: '矿山安全研究所' },
  1342. { name: '生物安全实验室A202(A202)', unit: '职业安全研究所' },
  1343. { name: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所' },
  1344. { name: '电气安全实验室C201(C201)', unit: '安全技术研究所' },
  1345. { name: '环境模拟实验室A408(A408)', unit: '化学品安全研究所' },
  1346. { name: '爆炸安全实验室B302(B302)', unit: '安全技术研究所' },
  1347. { name: '应急救援实验室C305(C305)', unit: '职业安全研究所' },
  1348. { name: '有机化学实验室A103(A103)', unit: '化学品安全研究所' },
  1349. { name: '无损检测实验室B205(B205)', unit: '安全技术研究所' },
  1350. ];
  1351. const sensorTypes = ['温度','湿度','TVOC','CO₂','O₂'];
  1352. function randVal(type) {
  1353. switch(type) {
  1354. case '温度': return (18 + Math.random() * 15).toFixed(1) + '°C';
  1355. case '湿度': return (30 + Math.random() * 50).toFixed(0) + '%';
  1356. case 'TVOC': return (Math.random() * 2).toFixed(2) + 'mg/m³';
  1357. case 'CO₂': return (300 + Math.random() * 800).toFixed(0) + 'ppm';
  1358. case 'O₂': return (18 + Math.random() * 4).toFixed(1) + '%';
  1359. default: return '--';
  1360. }
  1361. }
  1362. function isAlarm(type, val) {
  1363. const n = parseFloat(val);
  1364. if (type === '温度' && n > 30) return true;
  1365. if (type === 'TVOC' && n > 1.5) return true;
  1366. if (type === 'CO₂' && n > 900) return true;
  1367. if (type === 'O₂' && n < 19.5) return true;
  1368. return false;
  1369. }
  1370. function buildSensorList() {
  1371. const el = document.getElementById('sensorList');
  1372. let html = '';
  1373. const items = [...labNames, ...labNames]; // duplicate for seamless scroll
  1374. items.forEach(lab => {
  1375. html += `<div class="sensor-item"><div class="lab-name">${lab.name}<span class="lab-unit">${lab.unit}</span></div><div class="sensor-values">`;
  1376. sensorTypes.forEach(s => {
  1377. const v = randVal(s);
  1378. const alarm = isAlarm(s, v);
  1379. html += `<span class="sensor-val${alarm ? ' alarm' : ''}">${alarm ? '<span class="alarm-icon">⚠</span>' : ''}${s}: ${v}</span>`;
  1380. });
  1381. html += '</div></div>';
  1382. });
  1383. el.innerHTML = html;
  1384. }
  1385. buildSensorList();
  1386. // ===================== Warning Scroll Data =====================
  1387. function buildWarningList() {
  1388. const el = document.getElementById('warningList');
  1389. const warnings = [
  1390. { lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-04 14:32:18' },
  1391. { lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-04 14:28:05' },
  1392. { lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42' },
  1393. { lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10' },
  1394. { lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-04 12:45:33' },
  1395. { lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-04 11:18:27' },
  1396. { lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-04 10:42:51' },
  1397. { lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-04 09:36:14' },
  1398. ];
  1399. const all = [...warnings, ...warnings]; // duplicate for seamless scroll
  1400. let html = '';
  1401. all.forEach(w => {
  1402. html += `<div class="warning-item">
  1403. <div class="w-lab">${w.lab} - ${w.unit}</div>
  1404. <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
  1405. <div class="w-time">${w.time}</div>
  1406. </div>`;
  1407. });
  1408. el.innerHTML = html;
  1409. }
  1410. buildWarningList();
  1411. // ===================== Video Page: Tree =====================
  1412. const treeData = {
  1413. name: '安科院院区', children: [
  1414. { name: '科研楼A', children: [
  1415. { name: '1层', children: [{name:'A101'},{name:'A102'},{name:'A103'}] },
  1416. { name: '2层', children: [{name:'A201'},{name:'A202'},{name:'A203'}] },
  1417. { name: '3层', children: [{name:'A301'},{name:'A302'},{name:'A303'},{name:'A304'},{name:'A305'}] },
  1418. { name: '4层', children: [{name:'A401'},{name:'A402'},{name:'A403'},{name:'A408'}] },
  1419. ]},
  1420. { name: '科研楼B', children: [
  1421. { name: '1层', children: [{name:'B101'},{name:'B102'}] },
  1422. { name: '2层', children: [{name:'B201'},{name:'B202'},{name:'B203'},{name:'B205'}] },
  1423. { name: '3层', children: [{name:'B301'},{name:'B302'}] },
  1424. ]},
  1425. { name: '科研楼C', children: [
  1426. { name: '1层', children: [{name:'C101'},{name:'C102'}] },
  1427. { name: '2层', children: [{name:'C201'},{name:'C202'}] },
  1428. { name: '3层', children: [{name:'C301'},{name:'C305'}] },
  1429. ]},
  1430. ]
  1431. };
  1432. function buildTree(node, depth) {
  1433. const hasChild = node.children && node.children.length > 0;
  1434. let html = `<div class="tree-node">
  1435. <div class="tree-node-content" style="padding-left:${depth * 12}px" onclick="toggleTree(this)">
  1436. <span class="tree-arrow">${hasChild ? '▶' : ''}</span>
  1437. <span>${node.name}</span>
  1438. </div>`;
  1439. if (hasChild) {
  1440. html += `<div class="tree-children">`;
  1441. node.children.forEach(c => { html += buildTree(c, depth + 1); });
  1442. html += `</div>`;
  1443. }
  1444. html += `</div>`;
  1445. return html;
  1446. }
  1447. document.getElementById('treeContainer').innerHTML = buildTree(treeData, 0);
  1448. function toggleTree(el) {
  1449. const children = el.parentElement.querySelector('.tree-children');
  1450. const arrow = el.querySelector('.tree-arrow');
  1451. if (children) {
  1452. children.classList.toggle('open');
  1453. arrow.classList.toggle('expanded');
  1454. }
  1455. // select highlight
  1456. document.querySelectorAll('.tree-node-content').forEach(n => n.classList.remove('selected'));
  1457. el.classList.add('selected');
  1458. }
  1459. // ===================== Video Grid =====================
  1460. let currentVideoPage = 1;
  1461. const totalVideoPages = 3;
  1462. function renderVideoGrid() {
  1463. const grid = document.getElementById('videoGrid');
  1464. let html = '';
  1465. for (let i = 0; i < 9; i++) {
  1466. const roomNum = (currentVideoPage - 1) * 9 + i + 1;
  1467. const isAI = i === 0;
  1468. html += `<div class="video-cell${isAI ? ' ai-cam' : ''}">
  1469. <div class="cam-frame">
  1470. <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
  1471. <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
  1472. ${isAI ? '<span class="ai-badge">AI 智能</span>' : ''}
  1473. <div class="cam-inner">
  1474. <span class="cam-label"><span class="cam-dot"></span>${isAI ? '🎯 ' : ''}A3${String(roomNum).padStart(2,'0')} 摄像头</span>
  1475. <span class="cam-placeholder">📹 实时监控画面</span>
  1476. </div>
  1477. </div>
  1478. </div>`;
  1479. }
  1480. grid.innerHTML = html;
  1481. document.getElementById('videoPageInfo').textContent = `第 ${currentVideoPage} / ${totalVideoPages} 页`;
  1482. }
  1483. renderVideoGrid();
  1484. function videoPageChange(dir) {
  1485. currentVideoPage = Math.max(1, Math.min(totalVideoPages, currentVideoPage + dir));
  1486. renderVideoGrid();
  1487. }
  1488. // ===================== Alert Modal =====================
  1489. function showAlert(lab, building, unit, param, value, person, phone) {
  1490. document.getElementById('alertLab').textContent = lab;
  1491. document.getElementById('alertBuilding').textContent = building || '--';
  1492. document.getElementById('alertUnit').textContent = unit;
  1493. document.getElementById('alertParam').textContent = param;
  1494. document.getElementById('alertValue').textContent = value;
  1495. document.getElementById('alertPerson').textContent = person || '--';
  1496. document.getElementById('alertPhone').textContent = phone || '--';
  1497. document.getElementById('alertCamName').textContent = lab;
  1498. const now = new Date();
  1499. document.getElementById('alertTime').textContent =
  1500. `${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')}`;
  1501. document.getElementById('alertOverlay').classList.add('show');
  1502. }
  1503. function closeAlert() {
  1504. document.getElementById('alertOverlay').classList.remove('show');
  1505. }
  1506. // Demo: trigger alert after 15s
  1507. setTimeout(() => {
  1508. showAlert('化学实验室A101(A101)', '科研楼A-1层', '化学品安全研究所', 'TVOC 超标', '1.82 mg/m³', '张明', '138-0012-3456');
  1509. }, 15000);
  1510. // ===================== ECharts Initialization =====================
  1511. let chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie;
  1512. function initLabCharts() {
  1513. // ---- 1. Lab Overview Ring ----
  1514. if (chartLabOverview) chartLabOverview.dispose();
  1515. chartLabOverview = echarts.init(document.getElementById('chartLabOverview'));
  1516. chartLabOverview.setOption({
  1517. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1518. legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  1519. series: [{
  1520. type: 'pie', radius: ['45%','70%'], center: ['35%','50%'],
  1521. label: {
  1522. show: true, position: 'center',
  1523. formatter: '58\n实验室总数', fontSize: 14, color: '#fff', lineHeight: 20
  1524. },
  1525. data: [
  1526. { value: 8, name: 'I级(红)', itemStyle: { color: '#ff4d4f' } },
  1527. { value: 12, name: 'II级(橙)', itemStyle: { color: '#ff8c00' } },
  1528. { value: 18, name: 'III级(黄)', itemStyle: { color: '#ffcc00' } },
  1529. { value: 20, name: 'IV级(蓝)', itemStyle: { color: '#3a7bff' } },
  1530. ],
  1531. emphasis: { scaleSize: 6 }
  1532. }]
  1533. });
  1534. // ---- 2. Grade Stacked Bar ----
  1535. if (chartGradeBar) chartGradeBar.dispose();
  1536. chartGradeBar = echarts.init(document.getElementById('chartGradeBar'));
  1537. const units = ['安全技术\n研究所','职业安全\n研究所','化学品安全\n研究所','矿山安全\n研究所','应急科学\n研究中心','信息技术\n研究所','检测检验\n中心','标准化\n研究所'];
  1538. const totals = [12, 10, 14, 8, 6, 4, 3, 1];
  1539. const gradeData = {
  1540. 'I级': [2,1,3,2,1,0,0,0],
  1541. 'II级': [3,2,4,2,1,1,1,0],
  1542. 'III级': [4,4,4,2,2,2,1,1],
  1543. 'IV级': [3,3,3,2,2,1,1,0]
  1544. };
  1545. const colors = { 'I级': '#ff4d4f', 'II级': '#ff8c00', 'III级': '#ffcc00', 'IV级': '#3a7bff' };
  1546. let barOption = {
  1547. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1548. legend: { data: Object.keys(gradeData), top: 0, textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
  1549. grid: { left: 40, right: 10, top: 35, bottom: 50 },
  1550. xAxis: {
  1551. type: 'category', data: units,
  1552. axisLabel: {
  1553. color: '#7eacc8', fontSize: 10, interval: 0,
  1554. formatter: function(v, idx) { return v + '\n(' + totals[idx] + ')'; }
  1555. },
  1556. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  1557. axisTick: { show: false }
  1558. },
  1559. yAxis: {
  1560. type: 'value',
  1561. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1562. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
  1563. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  1564. },
  1565. dataZoom: [{
  1566. type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5
  1567. }],
  1568. series: Object.keys(gradeData).map(k => ({
  1569. name: k, type: 'bar', stack: 'total', barWidth: 20,
  1570. data: gradeData[k],
  1571. itemStyle: { color: colors[k], borderRadius: [0,0,0,0] }
  1572. }))
  1573. };
  1574. chartGradeBar.setOption(barOption);
  1575. // Auto scroll X axis
  1576. let barScrollIdx = 0;
  1577. setInterval(() => {
  1578. barScrollIdx++;
  1579. if (barScrollIdx > units.length - 5) barScrollIdx = 0;
  1580. chartGradeBar.dispatchAction({ type: 'dataZoom', startValue: barScrollIdx, endValue: barScrollIdx + 5 });
  1581. }, 5000);
  1582. // ---- 3. People Line Chart ----
  1583. if (chartPeopleLine) chartPeopleLine.dispose();
  1584. chartPeopleLine = echarts.init(document.getElementById('chartPeopleLine'));
  1585. const timeSlots = ['0:00','3:00','6:00','9:00','12:00','15:00','18:00','21:00','24:00'];
  1586. chartPeopleLine.setOption({
  1587. tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1588. legend: { data: ['进入人数','实验人数'], top: 0, right: 0, textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 14, itemHeight: 2 },
  1589. grid: { left: 35, right: 10, top: 24, bottom: 20 },
  1590. xAxis: {
  1591. type: 'category', data: timeSlots, boundaryGap: false,
  1592. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1593. axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } }
  1594. },
  1595. yAxis: {
  1596. type: 'value',
  1597. axisLabel: { color: '#7eacc8', fontSize: 10 },
  1598. splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
  1599. },
  1600. series: [
  1601. {
  1602. name: '进入人数', type: 'line', smooth: true,
  1603. data: [0, 2, 5, 86, 120, 145, 98, 42, 0],
  1604. lineStyle: { color: '#48d7ff', width: 2 },
  1605. 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)'}]) },
  1606. itemStyle: { color: '#48d7ff' }, symbolSize: 4
  1607. },
  1608. {
  1609. name: '实验人数', type: 'line', smooth: true,
  1610. data: [0, 1, 3, 52, 68, 78, 55, 20, 0],
  1611. lineStyle: { color: '#3a7bff', width: 2 },
  1612. 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)'}]) },
  1613. itemStyle: { color: '#3a7bff' }, symbolSize: 4
  1614. }
  1615. ]
  1616. });
  1617. // ---- 4. Device Gauge ----
  1618. if (chartGauge) chartGauge.dispose();
  1619. chartGauge = echarts.init(document.getElementById('chartGauge'));
  1620. chartGauge.setOption({
  1621. series: [{
  1622. type: 'gauge', startAngle: 210, endAngle: -30,
  1623. radius: '90%', center: ['50%','58%'],
  1624. min: 0, max: 100,
  1625. axisLine: {
  1626. lineStyle: {
  1627. width: 14,
  1628. color: [[0.7,'rgba(72,215,255,0.2)'],[0.9,'rgba(72,215,255,0.4)'],[1,'#48d7ff']]
  1629. }
  1630. },
  1631. axisTick: { show: false },
  1632. splitLine: { show: false },
  1633. axisLabel: { show: false },
  1634. pointer: {
  1635. length: '60%', width: 4,
  1636. itemStyle: { color: '#48d7ff' }
  1637. },
  1638. title: { show: true, offsetCenter: [0, '72%'], fontSize: 12, color: '#7eacc8' },
  1639. detail: {
  1640. valueAnimation: true, fontSize: 26, color: '#48d7ff', fontWeight: 700,
  1641. offsetCenter: [0, '40%'],
  1642. formatter: '{value}%'
  1643. },
  1644. data: [{ value: 93, name: '设备在线率' }]
  1645. }]
  1646. });
  1647. // ---- 5. Equipment Pie ----
  1648. if (chartEquipPie) chartEquipPie.dispose();
  1649. chartEquipPie = echarts.init(document.getElementById('chartEquipPie'));
  1650. chartEquipPie.setOption({
  1651. tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
  1652. legend: { orient: 'vertical', right: 4, top: 'center', textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 8, itemHeight: 8 },
  1653. series: [{
  1654. type: 'pie', radius: ['30%', '60%'], center: ['35%','50%'],
  1655. label: { show: false },
  1656. data: [
  1657. { value: 120, name: '分析仪器', itemStyle: { color: '#48d7ff' } },
  1658. { value: 95, name: '安全防护', itemStyle: { color: '#3a7bff' } },
  1659. { value: 86, name: '化学试剂设备', itemStyle: { color: '#36d399' } },
  1660. { value: 78, name: '电气设备', itemStyle: { color: '#ffb020' } },
  1661. { value: 65, name: '力学测试', itemStyle: { color: '#ff8c00' } },
  1662. { value: 52, name: '环境监测', itemStyle: { color: '#ff4d4f' } },
  1663. { value: 90, name: '通用设备', itemStyle: { color: '#a78bfa' } },
  1664. ],
  1665. emphasis: { scaleSize: 4 }
  1666. }]
  1667. });
  1668. }
  1669. // Init charts on load
  1670. setTimeout(initLabCharts, 200);
  1671. // Handle resize
  1672. window.addEventListener('resize', () => {
  1673. [chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie].forEach(c => c && c.resize());
  1674. });
  1675. </script>
  1676. </body>
  1677. </html>