| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>中国安全生产科学研究院实验室安全智慧化管控中心</title>
- <link rel="stylesheet" href="libs/reset.css" />
- <script src="libs/echarts.min.js"></script>
- <style>
- /* ======================== CSS Variables ======================== */
- :root {
- --screen-w: 1920;
- --screen-h: 1080;
- --bg-0: #020c1b;
- --bg-1: #061630;
- --bg-2: #0a2550;
- --panel-bg: rgba(6, 22, 56, 0.82);
- --panel-border: rgba(72, 180, 255, 0.28);
- --panel-glow: rgba(72, 180, 255, 0.06);
- --text-main: #d8f4ff;
- --text-sub: #7eacc8;
- --accent: #48d7ff;
- --accent2: #3a7bff;
- --good: #36d399;
- --warn: #ffb020;
- --danger: #ff4d4f;
- --grade-I: #ff4d4f;
- --grade-II: #ff8c00;
- --grade-III: #ffcc00;
- --grade-IV: #3a7bff;
- }
- * { box-sizing: border-box; margin: 0; padding: 0; }
- html, body {
- width: 100%; height: 100%; overflow: hidden;
- background: radial-gradient(ellipse 1400px 700px at 10% 0%, rgba(50,140,255,0.15), transparent 65%),
- radial-gradient(ellipse 1000px 500px at 90% 100%, rgba(0,200,255,0.1), transparent 60%),
- linear-gradient(150deg, var(--bg-0), var(--bg-1) 45%, var(--bg-2));
- color: var(--text-main);
- font-family: "DIN Alternate","Alibaba PuHuiTi","PingFang SC","Microsoft YaHei",sans-serif;
- }
- /* ======================== Viewport Scaling ======================== */
- .viewport { width: 100%; height: 100%; position: relative; }
- .screen {
- width: 1920px; height: 1080px;
- position: absolute; left: 50%; top: 50%;
- transform-origin: center center;
- overflow: hidden;
- }
- /* ======================== Page Toggle ======================== */
- .page { display: none; width: 100%; height: calc(100% - 72px); }
- .page.active { display: flex; }
- /* ======================== Top Nav ======================== */
- .top-nav {
- width: 100%; height: 72px; display: flex; align-items: center; justify-content: space-between;
- padding: 0 30px;
- background: linear-gradient(180deg, rgba(10,30,70,0.95) 0%, rgba(6,20,50,0.75) 100%);
- border-bottom: 1px solid var(--panel-border);
- position: relative; z-index: 100;
- }
- .top-nav::after {
- content: ''; position: absolute; bottom: 0; left: 10%; right: 10%; height: 1px;
- background: linear-gradient(90deg, transparent, var(--accent), transparent);
- }
- .nav-title {
- font-size: 26px; font-weight: 700; letter-spacing: 6px;
- background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
- text-shadow: 0 0 30px rgba(72,215,255,0.3);
- }
- .nav-tabs { display: flex; gap: 4px; }
- .nav-tab {
- padding: 8px 28px; border-radius: 4px; cursor: pointer;
- font-size: 15px; letter-spacing: 2px; transition: all 0.3s;
- border: 1px solid transparent; color: var(--text-sub);
- background: transparent;
- }
- .nav-tab:hover { color: var(--accent); border-color: var(--panel-border); }
- .nav-tab.active {
- background: linear-gradient(135deg, rgba(72,215,255,0.18), rgba(58,123,255,0.12));
- border-color: var(--accent); color: #fff;
- box-shadow: 0 0 16px rgba(72,215,255,0.2);
- }
- .nav-right { display: flex; align-items: center; gap: 20px; font-size: 14px; color: var(--text-sub); }
- .nav-right .clock { font-size: 22px; font-weight: 600; color: var(--accent); letter-spacing: 2px; }
- .nav-right .weather { display: flex; align-items: center; gap: 6px; }
- /* ======================== Panel Styles ======================== */
- .panel {
- background: var(--panel-bg);
- border: 1px solid var(--panel-border);
- border-radius: 6px; padding: 14px 16px;
- position: relative; overflow: hidden;
- backdrop-filter: blur(8px);
- }
- .panel::before {
- content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
- background: linear-gradient(90deg, transparent, var(--accent), transparent);
- opacity: 0.6;
- }
- .panel-title {
- font-size: 15px; font-weight: 600; letter-spacing: 2px; margin-bottom: 12px;
- padding-left: 12px; position: relative; color: #fff;
- }
- .panel-title::before {
- content: ''; position: absolute; left: 0; top: 2px; width: 3px; height: 14px;
- background: var(--accent); border-radius: 2px;
- box-shadow: 0 0 8px var(--accent);
- }
- /* ======================== Lab Status Page Layout ======================== */
- #page-lab { display: none; gap: 14px; padding: 14px; }
- #page-lab.active { display: flex; }
- .col-left, .col-right { width: 440px; display: flex; flex-direction: column; gap: 14px; flex-shrink: 0; }
- .col-center { flex: 1; display: flex; flex-direction: column; gap: 14px; min-width: 0; }
- /* ======================== Status Cards ======================== */
- .status-cards { display: flex; gap: 12px; margin-top: 0; }
- .status-card {
- flex: 1; text-align: center; padding: 6px 6px; border-radius: 4px;
- background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.12);
- }
- .status-card .label { font-size: 12px; color: var(--text-sub); margin-bottom: 2px; }
- .status-card .value { font-size: 22px; font-weight: 700; }
- .status-card.using .value { color: var(--accent); }
- .status-card.error .value { color: var(--danger); }
- .status-card.idle .value { color: var(--good); }
- /* ======================== Flip Counter ======================== */
- .flip-counter-row { display: flex; gap: 24px; justify-content: center; margin-bottom: 10px; }
- .flip-group { text-align: center; }
- .flip-group .flip-label { font-size: 12px; color: var(--text-sub); margin-bottom: 6px; }
- .flip-digits { display: flex; gap: 4px; justify-content: center; }
- .flip-digit {
- width: 32px; height: 42px; line-height: 42px; text-align: center;
- font-size: 26px; font-weight: 700; color: var(--accent);
- background: linear-gradient(180deg, rgba(72,215,255,0.12) 0%, rgba(6,22,56,0.9) 100%);
- border: 1px solid rgba(72,215,255,0.25); border-radius: 4px;
- }
- /* ======================== Sensor Scroll List ======================== */
- .sensor-scroll-wrap {
- flex: 1; overflow: hidden; position: relative; min-height: 0;
- }
- .sensor-scroll-inner {
- animation: scrollUp 40s linear infinite;
- }
- @keyframes scrollUp {
- 0% { transform: translateY(0); }
- 100% { transform: translateY(-50%); }
- }
- .sensor-item {
- padding: 10px 12px; margin-bottom: 6px; border-radius: 4px;
- background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
- transition: all 0.3s;
- }
- .sensor-item:hover { border-color: var(--accent); background: rgba(72,215,255,0.08); }
- .sensor-item .lab-name { font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #fff; }
- .sensor-item .lab-unit { font-size: 11px; color: var(--text-sub); margin-left: 6px; }
- .sensor-values { display: flex; gap: 10px; flex-wrap: wrap; }
- .sensor-val {
- font-size: 12px; padding: 2px 8px; border-radius: 3px;
- background: rgba(72,215,255,0.06);
- }
- .sensor-val.alarm {
- background: rgba(255,77,79,0.15); color: var(--danger); font-weight: 600;
- animation: alarmPulse 1s ease-in-out infinite alternate;
- }
- @keyframes alarmPulse {
- 0% { box-shadow: 0 0 4px rgba(255,77,79,0.3); }
- 100% { box-shadow: 0 0 12px rgba(255,77,79,0.6); }
- }
- .alarm-icon { display: inline-block; margin-right: 2px; }
- /* ======================== Warning Scroll ======================== */
- .warning-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
- .warning-count {
- font-size: 28px; font-weight: 700; color: var(--warn);
- text-shadow: 0 0 10px rgba(255,176,32,0.4);
- }
- .warning-count-label { font-size: 12px; color: var(--text-sub); }
- .warning-scroll-wrap { flex: 1; overflow: hidden; min-height: 0; }
- .warning-scroll-inner { animation: scrollUp 30s linear infinite; }
- .warning-item {
- padding: 8px 12px; margin-bottom: 6px; border-radius: 4px;
- background: rgba(255,176,32,0.04); border-left: 3px solid var(--warn);
- }
- .warning-item .w-lab { font-size: 13px; font-weight: 600; color: #fff; }
- .warning-item .w-sensor { font-size: 12px; color: var(--warn); margin: 2px 0; }
- .warning-item .w-time { font-size: 11px; color: var(--text-sub); }
- .warning-item .w-meta {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 4px;
- gap: 8px;
- }
- .warning-item .w-owner { font-size: 11px; color: var(--text-sub); }
- .warn-state {
- font-size: 11px;
- padding: 2px 8px;
- border-radius: 10px;
- border: 1px solid transparent;
- white-space: nowrap;
- }
- .warn-state.pending {
- color: #ffd5d6;
- border-color: rgba(255, 77, 79, 0.45);
- background: rgba(255, 77, 79, 0.18);
- }
- .warn-state.processing {
- color: #ffe7c0;
- border-color: rgba(255, 176, 32, 0.5);
- background: rgba(255, 176, 32, 0.16);
- }
- .warn-state.resolved {
- color: #cbf7e3;
- border-color: rgba(54, 211, 153, 0.45);
- background: rgba(54, 211, 153, 0.16);
- }
- /* ======================== Device Stats Top ======================== */
- .device-top { display: flex; gap: 12px; margin-bottom: 10px; }
- .device-stat-card {
- flex: 1; text-align: center; padding: 8px; border-radius: 4px;
- background: rgba(72,215,255,0.06); border: 1px solid rgba(72,215,255,0.1);
- }
- .device-stat-card .ds-label { font-size: 12px; color: var(--text-sub); }
- .device-stat-card .ds-value { font-size: 22px; font-weight: 700; color: var(--accent); }
- .device-stat-card.offline .ds-value { color: var(--text-sub); }
- .device-bottom { display: flex; gap: 10px; align-items: center; flex: 1; min-height: 0; }
- .device-gauge-wrap { width: 50%; height: 100%; min-height: 140px; }
- .device-type-list { flex: 1; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
- .device-type-item {
- display: flex; justify-content: space-between; align-items: center;
- padding: 10px 14px; border-radius: 4px;
- background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
- }
- .device-type-item .dt-name { font-size: 13px; color: var(--text-sub); }
- .device-type-item .dt-value { font-size: 18px; font-weight: 700; color: var(--accent); }
- /* ======================== Equipment Stats ======================== */
- .equip-chart-wrap { flex: 1; min-height: 0; }
- .equip-mid {
- display: flex; gap: 10px; justify-content: center; margin: 10px 0;
- }
- .equip-mid-item { text-align: center; flex: 1; padding: 8px 4px; border-radius: 4px;
- background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08); }
- .equip-mid-item .em-value { font-size: 24px; font-weight: 700; color: var(--accent); }
- .equip-mid-item .em-label { font-size: 12px; color: var(--text-sub); margin-top: 2px; }
- .equip-bottom {
- display: flex; gap: 10px;
- }
- .equip-status-card {
- flex: 1; text-align: center; padding: 12px 6px; border-radius: 4px;
- background: rgba(72,215,255,0.04); border: 1px solid rgba(72,215,255,0.08);
- }
- .equip-status-card .es-value { font-size: 22px; font-weight: 700; }
- .equip-status-card .es-label { font-size: 12px; color: var(--text-sub); margin-top: 2px; }
- /* ======================== Video Page ======================== */
- #page-video { display: none; gap: 14px; padding: 14px; }
- #page-video.active { display: flex; }
- .video-left {
- width: 300px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0;
- }
- .video-right { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
- .video-search {
- width: 100%; padding: 8px 12px; border-radius: 4px;
- background: rgba(72,215,255,0.06); border: 1px solid var(--panel-border);
- color: var(--text-main); font-size: 13px; outline: none;
- }
- .video-search::placeholder { color: var(--text-sub); }
- .video-filter {
- width: 100%; padding: 8px 12px; border-radius: 4px;
- background: rgba(72,215,255,0.06); border: 1px solid var(--panel-border);
- color: var(--text-main); font-size: 13px; outline: none;
- appearance: none; cursor: pointer;
- }
- .tree-container {
- flex: 1; overflow-y: auto; padding: 6px;
- }
- .tree-container::-webkit-scrollbar { width: 4px; }
- .tree-container::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 2px; }
- .tree-node {
- padding: 4px 0; cursor: pointer; user-select: none;
- }
- .tree-node-content {
- display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 4px;
- font-size: 13px; transition: all 0.2s;
- }
- .tree-node-content:hover { background: rgba(72,215,255,0.08); }
- .tree-node-content.selected { background: rgba(72,215,255,0.15); color: var(--accent); }
- .tree-arrow { display: inline-block; width: 14px; font-size: 10px; color: var(--text-sub); transition: transform 0.2s; }
- .tree-arrow.expanded { transform: rotate(90deg); }
- .tree-children { padding-left: 18px; display: none; }
- .tree-children.open { display: block; }
- .video-breadcrumb {
- font-size: 14px; color: var(--text-sub); margin-bottom: 10px; padding: 4px 0;
- border-bottom: 1px solid rgba(72,215,255,0.1);
- position: relative; z-index: 1;
- }
- .video-grid {
- flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);
- gap: 8px; min-height: 0;
- position: relative; z-index: 1;
- }
- .video-cell {
- position: relative; overflow: hidden;
- display: flex; align-items: center; justify-content: center;
- }
- /* Sci-fi outer frame */
- .cam-frame {
- position: relative; width: 100%; height: 100%; border-radius: 6px; padding: 4px;
- background: linear-gradient(145deg, rgba(72,215,255,0.35), rgba(58,123,255,0.1)), rgba(4,14,35,0.95);
- box-shadow: inset 0 0 18px rgba(72,215,255,0.2), 0 0 20px rgba(58,123,255,0.15);
- }
- .cam-frame::before {
- content: ''; position: absolute; inset: 0; border-radius: 6px; pointer-events: none;
- border: 1px solid rgba(100,210,255,0.55);
- clip-path: polygon(
- 0 0, 18px 0, 18px 2px, calc(100% - 18px) 2px, calc(100% - 18px) 0, 100% 0,
- 100% 18px, calc(100% - 2px) 18px, calc(100% - 2px) calc(100% - 18px), 100% calc(100% - 18px), 100% 100%,
- calc(100% - 18px) 100%, calc(100% - 18px) calc(100% - 2px), 18px calc(100% - 2px), 18px 100%, 0 100%,
- 0 calc(100% - 18px), 2px calc(100% - 18px), 2px 18px, 0 18px
- );
- }
- .cam-frame::after {
- content: ''; position: absolute; inset: 2px; border-radius: 5px; pointer-events: none;
- border: 1px solid rgba(120,220,255,0.18);
- }
- /* Corner glow accents */
- .cam-corner {
- position: absolute; width: 10px; height: 10px; z-index: 2;
- }
- .cam-corner::before, .cam-corner::after {
- content: ''; position: absolute; background: var(--accent);
- box-shadow: 0 0 6px var(--accent), 0 0 12px rgba(72,215,255,0.4);
- }
- .cam-corner.c-tl { top: 2px; left: 2px; }
- .cam-corner.c-tl::before { width: 14px; height: 2px; top: 0; left: 0; }
- .cam-corner.c-tl::after { width: 2px; height: 14px; top: 0; left: 0; }
- .cam-corner.c-tr { top: 2px; right: 2px; }
- .cam-corner.c-tr::before { width: 14px; height: 2px; top: 0; right: 0; }
- .cam-corner.c-tr::after { width: 2px; height: 14px; top: 0; right: 0; }
- .cam-corner.c-bl { bottom: 2px; left: 2px; }
- .cam-corner.c-bl::before { width: 14px; height: 2px; bottom: 0; left: 0; }
- .cam-corner.c-bl::after { width: 2px; height: 14px; bottom: 0; left: 0; }
- .cam-corner.c-br { bottom: 2px; right: 2px; }
- .cam-corner.c-br::before { width: 14px; height: 2px; bottom: 0; right: 0; }
- .cam-corner.c-br::after { width: 2px; height: 14px; bottom: 0; right: 0; }
- /* Inner camera view */
- .cam-inner {
- width: 100%; height: 100%; border-radius: 4px; overflow: hidden; position: relative;
- background: radial-gradient(ellipse 120px 80px at 20% 25%, rgba(72,180,255,0.2), transparent 70%),
- linear-gradient(150deg, rgba(8,30,65,0.95), rgba(4,15,35,0.92));
- }
- .cam-inner::after {
- content: ''; position: absolute; inset: 0; pointer-events: none;
- background: repeating-linear-gradient(
- to bottom, rgba(200,246,255,0.03) 0, rgba(200,246,255,0.03) 2px, transparent 2px, transparent 5px
- );
- }
- .cam-inner .cam-label {
- position: absolute; top: 6px; left: 8px; font-size: 11px;
- background: rgba(0,0,0,0.55); padding: 2px 8px; border-radius: 2px; color: var(--text-sub); z-index: 1;
- }
- .cam-inner .cam-placeholder {
- position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
- font-size: 12px; color: var(--text-sub); opacity: 0.4;
- }
- .cam-inner .cam-dot {
- display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px;
- background: #18ff6f; box-shadow: 0 0 6px #18ff6f;
- }
- /* AI camera special style */
- .video-cell.ai-cam .cam-frame {
- background: linear-gradient(145deg, rgba(255,176,32,0.3), rgba(255,120,0,0.1)), rgba(4,14,35,0.95);
- box-shadow: inset 0 0 18px rgba(255,176,32,0.2), 0 0 20px rgba(255,140,0,0.15);
- }
- .video-cell.ai-cam .cam-frame::before {
- border-color: rgba(255,190,80,0.55);
- }
- .video-cell.ai-cam .cam-corner::before, .video-cell.ai-cam .cam-corner::after {
- background: var(--warn);
- box-shadow: 0 0 6px var(--warn), 0 0 12px rgba(255,176,32,0.4);
- }
- .video-cell.ai-cam .cam-inner .cam-label { background: rgba(255,176,32,0.3); color: var(--warn); }
- .ai-badge {
- position: absolute; top: 8px; right: 10px; font-size: 10px; z-index: 2;
- background: linear-gradient(135deg, var(--warn), #ff6b00); color: #fff;
- padding: 2px 10px; border-radius: 10px; font-weight: 600;
- box-shadow: 0 0 10px rgba(255,176,32,0.4);
- }
- .video-pagination {
- display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 10px;
- position: relative; z-index: 1;
- }
- .page-btn {
- padding: 6px 20px; border-radius: 4px; cursor: pointer;
- background: rgba(72,215,255,0.08); border: 1px solid var(--panel-border);
- color: var(--text-main); font-size: 13px; transition: all 0.3s;
- }
- .page-btn:hover { background: rgba(72,215,255,0.15); border-color: var(--accent); }
- .page-info { font-size: 13px; color: var(--text-sub); }
- /* ======================== Alert Modal ======================== */
- .alert-overlay {
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- background: rgba(255,0,0,0.08); z-index: 9999;
- display: none; align-items: center; justify-content: center;
- animation: alertFlash 1.5s ease-in-out infinite;
- }
- .alert-overlay.show { display: flex; }
- /* Screen edge red glow */
- .alert-overlay::before {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
- box-shadow:
- inset 0 0 80px 30px rgba(255,0,0,0.35),
- inset 0 0 200px 60px rgba(255,0,0,0.15);
- animation: edgeRedGlow 1s ease-in-out infinite;
- }
- @keyframes edgeRedGlow {
- 0%, 100% {
- box-shadow:
- inset 0 0 60px 20px rgba(255,0,0,0.25),
- inset 0 0 150px 40px rgba(255,0,0,0.1);
- }
- 50% {
- box-shadow:
- inset 0 0 120px 50px rgba(255,0,0,0.5),
- inset 0 0 300px 80px rgba(255,0,0,0.25);
- }
- }
- /* Four edge red light bars */
- .alert-overlay::after {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
- border: 3px solid transparent;
- animation: edgeBorderFlash 0.8s ease-in-out infinite;
- }
- @keyframes edgeBorderFlash {
- 0%, 100% { border-color: rgba(255,50,50,0.15); }
- 50% { border-color: rgba(255,50,50,0.6); }
- }
- @keyframes alertFlash {
- 0%, 100% { background: rgba(255,0,0,0.04); }
- 50% { background: rgba(255,0,0,0.12); }
- }
- .alert-modal {
- width: 900px; padding: 0; border-radius: 8px; overflow: hidden;
- border: 2px solid var(--danger);
- box-shadow: 0 0 60px rgba(255,77,79,0.4), 0 0 120px rgba(255,77,79,0.2);
- animation: alertModalPulse 2s ease-in-out infinite;
- }
- @keyframes alertModalPulse {
- 0%, 100% { box-shadow: 0 0 40px rgba(255,77,79,0.3), 0 0 80px rgba(255,77,79,0.15); }
- 50% { box-shadow: 0 0 80px rgba(255,77,79,0.5), 0 0 160px rgba(255,77,79,0.25); }
- }
- .alert-header {
- background: linear-gradient(135deg, #cc0000, #ff4d4f); padding: 16px 24px;
- display: flex; align-items: center; gap: 12px;
- }
- .alert-header .alert-icon { font-size: 28px; animation: spin 2s linear infinite; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- .alert-header .alert-title { font-size: 20px; font-weight: 700; letter-spacing: 4px; color: #fff; }
- .alert-body {
- background: linear-gradient(180deg, #1a0808, #0d0404); padding: 24px;
- display: flex; gap: 20px;
- }
- .alert-info {
- flex: 1; min-width: 0;
- }
- .alert-video {
- width: 320px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px;
- }
- .alert-video-title {
- font-size: 13px; color: var(--danger); font-weight: 600; letter-spacing: 1px;
- display: flex; align-items: center; gap: 6px;
- }
- .alert-video-title::before {
- content: ''; width: 6px; height: 6px; border-radius: 50%;
- background: var(--danger); box-shadow: 0 0 8px var(--danger);
- animation: dotBlink 1.5s ease-in-out infinite;
- }
- .alert-video-feed {
- flex: 1; min-height: 180px; border-radius: 6px; position: relative; overflow: hidden;
- background: radial-gradient(ellipse 100px 60px at 20% 25%, rgba(255,77,79,0.15), transparent 70%),
- linear-gradient(150deg, rgba(15,5,5,0.95), rgba(8,2,2,0.92));
- border: 1px solid rgba(255,77,79,0.3);
- box-shadow: inset 0 0 20px rgba(255,77,79,0.1), 0 0 12px rgba(255,77,79,0.08);
- }
- .alert-video-feed::after {
- content: ''; position: absolute; inset: 0; pointer-events: none;
- background: repeating-linear-gradient(
- to bottom, rgba(255,200,200,0.03) 0, rgba(255,200,200,0.03) 2px, transparent 2px, transparent 5px
- );
- }
- .alert-video-feed .cam-label {
- position: absolute; top: 8px; left: 10px; font-size: 11px;
- background: rgba(255,0,0,0.4); padding: 2px 10px; border-radius: 2px;
- color: #fff; z-index: 1;
- display: flex; align-items: center; gap: 4px;
- }
- .alert-video-feed .cam-label .rec-dot {
- width: 6px; height: 6px; border-radius: 50%;
- background: #ff4d4f; box-shadow: 0 0 6px #ff4d4f;
- animation: dotBlink 1s ease-in-out infinite;
- }
- .alert-video-feed .cam-placeholder {
- position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
- font-size: 13px; color: rgba(255,255,255,0.3);
- }
- .alert-info-row { display: flex; margin-bottom: 10px; font-size: 14px; }
- .alert-info-row .a-label { width: 105px; color: var(--text-sub); flex-shrink: 0; }
- .alert-info-row .a-value { color: #fff; font-weight: 600; }
- .alert-info-row .a-value.danger { color: var(--danger); }
- .alert-footer {
- padding: 16px 24px; text-align: center;
- background: #0d0404; border-top: 1px solid rgba(255,77,79,0.2);
- }
- .alert-close-btn {
- padding: 8px 40px; border-radius: 4px; cursor: pointer;
- background: linear-gradient(135deg, #cc0000, #ff4d4f); border: none;
- color: #fff; font-size: 14px; font-weight: 600; letter-spacing: 2px;
- transition: all 0.3s;
- }
- .alert-close-btn:hover { box-shadow: 0 0 20px rgba(255,77,79,0.5); }
- /* ======================== Decorations ======================== */
- .corner-deco {
- position: absolute; width: 14px; height: 14px;
- border-color: var(--accent); border-style: solid;
- opacity: 0.7; z-index: 2;
- animation: cornerBreathe 3s ease-in-out infinite;
- }
- .corner-deco.tl { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
- .corner-deco.tr { top: -1px; right: -1px; border-width: 2px 2px 0 0; animation-delay: 0.5s; }
- .corner-deco.bl { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; animation-delay: 1s; }
- .corner-deco.br { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; animation-delay: 1.5s; }
- @keyframes cornerBreathe {
- 0%, 100% { opacity: 0.4; box-shadow: 0 0 4px rgba(72,215,255,0.2); }
- 50% { opacity: 1; box-shadow: 0 0 12px rgba(72,215,255,0.6), 0 0 24px rgba(72,215,255,0.2); }
- }
- /* ======================== Background Effects ======================== */
- /* Tech grid overlay */
- .screen::before {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
- background-image:
- linear-gradient(rgba(72,180,255,0.04) 1px, transparent 1px),
- linear-gradient(90deg, rgba(72,180,255,0.04) 1px, transparent 1px);
- background-size: 60px 60px;
- mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(0,0,0,0.5) 0%, transparent 100%);
- }
- /* Ambient floating particles */
- .screen::after {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
- background:
- radial-gradient(1.5px 1.5px at 15% 25%, rgba(72,215,255,0.5), transparent),
- radial-gradient(1px 1px at 85% 15%, rgba(72,215,255,0.4), transparent),
- radial-gradient(1.5px 1.5px at 45% 80%, rgba(58,123,255,0.4), transparent),
- radial-gradient(1px 1px at 70% 55%, rgba(72,215,255,0.3), transparent),
- radial-gradient(1.5px 1.5px at 25% 60%, rgba(100,200,255,0.35), transparent),
- radial-gradient(1px 1px at 90% 75%, rgba(72,215,255,0.3), transparent);
- animation: particleDrift 20s ease-in-out infinite alternate;
- }
- @keyframes particleDrift {
- 0% { transform: translate(0, 0); opacity: 0.6; }
- 50% { transform: translate(-8px, 6px); opacity: 1; }
- 100% { transform: translate(5px, -4px); opacity: 0.7; }
- }
- /* ======================== Panel Animations ======================== */
- /* Panel border breathing glow */
- .panel {
- border: 1px solid var(--panel-border);
- animation: panelGlow 4s ease-in-out infinite;
- transition: box-shadow 0.4s ease;
- }
- @keyframes panelGlow {
- 0%, 100% { box-shadow: inset 0 0 15px rgba(72,180,255,0.03), 0 0 8px rgba(72,180,255,0.05); }
- 50% { box-shadow: inset 0 0 20px rgba(72,180,255,0.08), 0 0 16px rgba(72,180,255,0.1); }
- }
- .panel:hover {
- box-shadow: inset 0 0 25px rgba(72,180,255,0.12), 0 0 24px rgba(72,180,255,0.15) !important;
- }
- /* Panel top scan line */
- .panel::after {
- content: ''; position: absolute; top: 0; left: -100%; width: 60%; height: 1px;
- background: linear-gradient(90deg, transparent, rgba(72,215,255,0.6), rgba(72,215,255,0.8), rgba(72,215,255,0.6), transparent);
- animation: panelScan 6s linear infinite;
- pointer-events: none; z-index: 3;
- }
- @keyframes panelScan {
- 0% { left: -60%; }
- 100% { left: 160%; }
- }
- /* Stagger scan animation per panel */
- .col-left .panel:nth-child(2)::after { animation-delay: 1s; }
- .col-left .panel:nth-child(3)::after { animation-delay: 2s; }
- .col-center .panel:nth-child(2)::after { animation-delay: 3s; }
- .col-right .panel:nth-child(1)::after { animation-delay: 1.5s; }
- .col-right .panel:nth-child(2)::after { animation-delay: 3.5s; }
- /* ======================== Top Nav Animations ======================== */
- /* Nav bottom edge flowing light */
- .top-nav::before {
- content: ''; position: absolute; bottom: -1px; left: 0; width: 120px; height: 2px;
- background: linear-gradient(90deg, transparent, var(--accent), rgba(72,215,255,0.8), transparent);
- animation: navFlow 4s linear infinite;
- z-index: 101;
- }
- @keyframes navFlow {
- 0% { left: -120px; }
- 100% { left: calc(100% + 120px); }
- }
- /* Title text glow pulse */
- .nav-title {
- animation: titleGlow 3s ease-in-out infinite;
- }
- @keyframes titleGlow {
- 0%, 100% { filter: drop-shadow(0 0 6px rgba(72,215,255,0.2)); }
- 50% { filter: drop-shadow(0 0 16px rgba(72,215,255,0.5)); }
- }
- /* Clock pulse */
- .nav-right .clock {
- animation: clockPulse 2s ease-in-out infinite;
- }
- @keyframes clockPulse {
- 0%, 100% { text-shadow: 0 0 6px rgba(72,215,255,0.3); }
- 50% { text-shadow: 0 0 14px rgba(72,215,255,0.6), 0 0 28px rgba(72,215,255,0.2); }
- }
- /* Active tab glow */
- .nav-tab.active {
- animation: tabGlow 2.5s ease-in-out infinite;
- }
- @keyframes tabGlow {
- 0%, 100% { box-shadow: 0 0 10px rgba(72,215,255,0.15); }
- 50% { box-shadow: 0 0 22px rgba(72,215,255,0.35), inset 0 0 10px rgba(72,215,255,0.08); }
- }
- /* ======================== Data Card Animations ======================== */
- /* Status cards breathing */
- .status-card {
- transition: all 0.3s ease;
- animation: cardBreathe 4s ease-in-out infinite;
- }
- .status-card:nth-child(2) { animation-delay: 1.3s; }
- .status-card:nth-child(3) { animation-delay: 2.6s; }
- @keyframes cardBreathe {
- 0%, 100% { border-color: rgba(72,215,255,0.12); box-shadow: 0 0 0 transparent; }
- 50% { border-color: rgba(72,215,255,0.3); box-shadow: 0 0 10px rgba(72,215,255,0.08); }
- }
- .status-card:hover {
- transform: translateY(-2px);
- border-color: var(--accent) !important;
- box-shadow: 0 4px 16px rgba(72,215,255,0.15) !important;
- }
- /* Flip digit glow */
- .flip-digit {
- animation: digitGlow 2s ease-in-out infinite alternate;
- transition: all 0.3s ease;
- }
- @keyframes digitGlow {
- 0% { box-shadow: 0 0 4px rgba(72,215,255,0.1); text-shadow: 0 0 4px rgba(72,215,255,0.3); }
- 100% { box-shadow: 0 0 10px rgba(72,215,255,0.25); text-shadow: 0 0 12px rgba(72,215,255,0.5); }
- }
- /* Device type item hover */
- .device-type-item {
- transition: all 0.3s ease;
- }
- .device-type-item:hover {
- border-color: rgba(72,215,255,0.35);
- background: rgba(72,215,255,0.08);
- box-shadow: 0 0 12px rgba(72,215,255,0.1);
- transform: translateX(3px);
- }
- /* Equipment status cards */
- .equip-status-card, .equip-mid-item, .device-stat-card {
- transition: all 0.3s ease;
- }
- .equip-status-card:hover, .equip-mid-item:hover, .device-stat-card:hover {
- border-color: rgba(72,215,255,0.3);
- box-shadow: 0 0 12px rgba(72,215,255,0.1);
- transform: translateY(-2px);
- }
- /* ======================== Sensor & Warning Animations ======================== */
- /* Sensor item enter animation */
- .sensor-item {
- animation: sensorFadeIn 0.5s ease-out;
- }
- @keyframes sensorFadeIn {
- from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: translateY(0); }
- }
- /* Warning item left border pulse */
- .warning-item {
- transition: all 0.3s ease;
- animation: warnBorderPulse 3s ease-in-out infinite;
- }
- @keyframes warnBorderPulse {
- 0%, 100% { border-left-color: rgba(255,176,32,0.6); }
- 50% { border-left-color: var(--warn); box-shadow: -2px 0 8px rgba(255,176,32,0.15); }
- }
- .warning-item:hover {
- background: rgba(255,176,32,0.08);
- transform: translateX(3px);
- }
- /* Warning count glow */
- .warning-count {
- animation: warnCountGlow 2s ease-in-out infinite;
- }
- @keyframes warnCountGlow {
- 0%, 100% { text-shadow: 0 0 8px rgba(255,176,32,0.3); }
- 50% { text-shadow: 0 0 20px rgba(255,176,32,0.6), 0 0 40px rgba(255,176,32,0.2); }
- }
- /* ======================== Camera Frame Animations ======================== */
- /* Corner glow breathing */
- .cam-corner::before, .cam-corner::after {
- animation: camCornerGlow 2.5s ease-in-out infinite;
- }
- .cam-corner.c-tr::before, .cam-corner.c-tr::after { animation-delay: 0.6s; }
- .cam-corner.c-bl::before, .cam-corner.c-bl::after { animation-delay: 1.2s; }
- .cam-corner.c-br::before, .cam-corner.c-br::after { animation-delay: 1.8s; }
- @keyframes camCornerGlow {
- 0%, 100% { opacity: 0.5; box-shadow: 0 0 4px var(--accent); }
- 50% { opacity: 1; box-shadow: 0 0 10px var(--accent), 0 0 20px rgba(72,215,255,0.3); }
- }
- /* Camera frame border breathing */
- .cam-frame {
- background:
- linear-gradient(rgba(4,14,35,0.96), rgba(4,14,35,0.96)) padding-box,
- linear-gradient(135deg,
- rgba(72,215,255,0.2) 0%,
- rgba(72,215,255,0.6) 18%,
- rgba(130,235,255,0.9) 34%,
- rgba(72,215,255,0.45) 52%,
- rgba(58,123,255,0.28) 72%,
- rgba(72,215,255,0.18) 100%
- ) border-box;
- background-size: 100% 100%, 240% 240%;
- animation: camFrameBreathe 4s ease-in-out infinite, camFrameBorderFlow 3.9s linear infinite;
- }
- @keyframes camFrameBreathe {
- 0%, 100% { box-shadow: inset 0 0 14px rgba(72,215,255,0.15), 0 0 14px rgba(58,123,255,0.1); }
- 50% { box-shadow: inset 0 0 22px rgba(72,215,255,0.3), 0 0 24px rgba(58,123,255,0.2); }
- }
- @keyframes camFrameBorderFlow {
- 0% { background-position: 0 0, 0% 50%; }
- 100% { background-position: 0 0, 220% 50%; }
- }
- /* Camera online dot blink */
- .cam-inner .cam-dot {
- animation: dotBlink 2s ease-in-out infinite;
- }
- @keyframes dotBlink {
- 0%, 100% { opacity: 1; box-shadow: 0 0 4px #18ff6f; }
- 50% { opacity: 0.4; box-shadow: 0 0 8px #18ff6f, 0 0 16px rgba(24,255,111,0.3); }
- }
- /* AI badge pulse */
- .ai-badge {
- animation: aiBadgePulse 1.5s ease-in-out infinite;
- }
- @keyframes aiBadgePulse {
- 0%, 100% { box-shadow: 0 0 8px rgba(255,176,32,0.3); }
- 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); }
- }
- /* AI camera frame special breathing */
- .video-cell.ai-cam .cam-frame {
- background:
- linear-gradient(rgba(4,14,35,0.96), rgba(4,14,35,0.96)) padding-box,
- linear-gradient(135deg,
- rgba(255,176,32,0.22) 0%,
- rgba(255,176,32,0.62) 20%,
- rgba(255,210,120,0.9) 36%,
- rgba(255,176,32,0.45) 58%,
- rgba(255,120,0,0.25) 78%,
- rgba(255,176,32,0.22) 100%
- ) border-box;
- background-size: 100% 100%, 240% 240%;
- animation: aiFrameBreathe 3s ease-in-out infinite, aiFrameBorderFlow 2.9s linear infinite;
- }
- @keyframes aiFrameBreathe {
- 0%, 100% { box-shadow: inset 0 0 14px rgba(255,176,32,0.15), 0 0 14px rgba(255,140,0,0.1); }
- 50% { box-shadow: inset 0 0 24px rgba(255,176,32,0.3), 0 0 28px rgba(255,140,0,0.2); }
- }
- @keyframes aiFrameBorderFlow {
- 0% { background-position: 0 0, 0% 50%; }
- 100% { background-position: 0 0, 240% 50%; }
- }
- /* ======================== Page Button Animations ======================== */
- .page-btn {
- transition: all 0.3s ease;
- position: relative; overflow: hidden;
- }
- .page-btn::after {
- content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
- background: linear-gradient(90deg, transparent, rgba(72,215,255,0.15), transparent);
- transition: left 0.5s ease;
- }
- .page-btn:hover::after { left: 100%; }
- .page-btn:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(72,215,255,0.15);
- }
- /* ======================== Panel Title Glow ======================== */
- .panel-title::before {
- animation: titleBarGlow 2.5s ease-in-out infinite;
- }
- @keyframes titleBarGlow {
- 0%, 100% { box-shadow: 0 0 6px var(--accent); opacity: 0.7; }
- 50% { box-shadow: 0 0 14px var(--accent), 0 0 24px rgba(72,215,255,0.3); opacity: 1; }
- }
- /* ======================== Alarm Icon Enhanced ======================== */
- .alarm-icon {
- animation: alarmIconShake 0.5s ease-in-out infinite alternate;
- }
- @keyframes alarmIconShake {
- 0% { transform: translateX(-1px) rotate(-5deg); }
- 100% { transform: translateX(1px) rotate(5deg); }
- }
- /* ================================================================ */
- /* ============= VIDEO MONITORING PAGE ANIMATIONS ================= */
- /* ================================================================ */
- /* --- Video page entrance fade-in --- */
- #page-video.active {
- animation: videoPageFadeIn 0.6s ease-out;
- }
- @keyframes videoPageFadeIn {
- from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: translateY(0); }
- }
- /* --- Video-left sidebar panel effects --- */
- .video-left {
- position: relative;
- }
- /* Sidebar vertical scan line */
- .video-left::after {
- content: ''; position: absolute; left: 0; top: -100%; width: 1px; height: 60%;
- background: linear-gradient(180deg, transparent, rgba(72,215,255,0.7), rgba(72,215,255,0.9), rgba(72,215,255,0.7), transparent);
- animation: sidebarScan 5s linear infinite;
- pointer-events: none; z-index: 3;
- }
- @keyframes sidebarScan {
- 0% { top: -60%; }
- 100% { top: 160%; }
- }
- /* Sidebar left edge glow */
- .video-left::before {
- content: ''; position: absolute; left: 0; top: 10%; bottom: 10%; width: 2px;
- background: linear-gradient(180deg, transparent, var(--accent), transparent);
- opacity: 0.4;
- animation: sidebarEdgeGlow 3s ease-in-out infinite;
- z-index: 3; pointer-events: none;
- }
- @keyframes sidebarEdgeGlow {
- 0%, 100% { opacity: 0.25; box-shadow: 0 0 4px var(--accent); }
- 50% { opacity: 0.7; box-shadow: 0 0 12px var(--accent), 0 0 24px rgba(72,215,255,0.2); }
- }
- /* --- Search input effects --- */
- .video-search {
- transition: all 0.3s ease;
- position: relative;
- }
- .video-search:focus {
- border-color: var(--accent);
- box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
- background: rgba(72,215,255,0.1);
- }
- /* Search input idle glow */
- .video-search {
- animation: searchIdleGlow 4s ease-in-out infinite;
- }
- @keyframes searchIdleGlow {
- 0%, 100% { box-shadow: 0 0 0 transparent; }
- 50% { box-shadow: 0 0 6px rgba(72,215,255,0.1); }
- }
- /* --- Filter dropdown effects --- */
- .video-filter {
- transition: all 0.3s ease;
- animation: filterIdleGlow 4s ease-in-out infinite 1s;
- }
- @keyframes filterIdleGlow {
- 0%, 100% { box-shadow: 0 0 0 transparent; }
- 50% { box-shadow: 0 0 6px rgba(72,215,255,0.1); }
- }
- .video-filter:focus {
- border-color: var(--accent);
- box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
- background: rgba(72,215,255,0.1);
- }
- /* --- Tree node animations --- */
- .tree-node-content {
- transition: all 0.3s ease;
- position: relative;
- overflow: hidden;
- }
- /* Tree node hover light sweep */
- .tree-node-content::after {
- content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
- background: linear-gradient(90deg, transparent, rgba(72,215,255,0.08), transparent);
- transition: left 0.6s ease;
- pointer-events: none;
- }
- .tree-node-content:hover::after {
- left: 100%;
- }
- .tree-node-content:hover {
- box-shadow: 0 0 8px rgba(72,215,255,0.08);
- text-shadow: 0 0 6px rgba(72,215,255,0.2);
- }
- /* Tree selected node glow pulse */
- .tree-node-content.selected {
- animation: treeSelectedGlow 2.5s ease-in-out infinite;
- border-left: 2px solid var(--accent);
- }
- @keyframes treeSelectedGlow {
- 0%, 100% { box-shadow: 0 0 6px rgba(72,215,255,0.1); background: rgba(72,215,255,0.12); }
- 50% { box-shadow: 0 0 14px rgba(72,215,255,0.2); background: rgba(72,215,255,0.2); }
- }
- /* Tree children expand animation */
- .tree-children.open {
- animation: treeExpand 0.35s ease-out;
- }
- @keyframes treeExpand {
- from { opacity: 0; max-height: 0; transform: translateY(-6px); }
- to { opacity: 1; max-height: 600px; transform: translateY(0); }
- }
- /* Tree arrow rotation glow */
- .tree-arrow {
- transition: all 0.3s ease;
- }
- .tree-arrow.expanded {
- color: var(--accent);
- text-shadow: 0 0 6px rgba(72,215,255,0.4);
- }
- /* --- Video breadcrumb flowing light --- */
- .video-breadcrumb {
- position: relative; overflow: hidden;
- transition: all 0.3s ease;
- }
- .video-breadcrumb::before {
- content: ''; position: absolute; bottom: 0; left: -80px; width: 80px; height: 1px;
- background: linear-gradient(90deg, transparent, var(--accent), rgba(72,215,255,0.8), transparent);
- animation: breadcrumbFlow 5s linear infinite;
- pointer-events: none;
- }
- @keyframes breadcrumbFlow {
- 0% { left: -80px; }
- 100% { left: calc(100% + 80px); }
- }
- /* Breadcrumb text subtle glow */
- .video-breadcrumb {
- animation: breadcrumbTextGlow 3s ease-in-out infinite;
- }
- @keyframes breadcrumbTextGlow {
- 0%, 100% { text-shadow: none; color: var(--text-sub); }
- 50% { text-shadow: 0 0 8px rgba(72,215,255,0.2); color: #a0d4e8; }
- }
- /* --- Video grid container effects --- */
- .video-grid {
- position: relative;
- border: 1px solid rgba(72,215,255,0.08);
- border-radius: 6px;
- padding: 6px;
- animation: videoGridBreathe 5s ease-in-out infinite;
- }
- @keyframes videoGridBreathe {
- 0%, 100% { border-color: rgba(72,215,255,0.06); box-shadow: 0 0 0 transparent; }
- 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); }
- }
- /* Grid background tech pattern */
- .video-grid::before {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0; border-radius: 6px;
- background: repeating-linear-gradient(
- 0deg, rgba(72,215,255,0.015) 0, rgba(72,215,255,0.015) 1px, transparent 1px, transparent 60px
- ), repeating-linear-gradient(
- 90deg, rgba(72,215,255,0.015) 0, rgba(72,215,255,0.015) 1px, transparent 1px, transparent 60px
- );
- }
- /* Grid horizontal scan line */
- .video-grid::after {
- content: ''; position: absolute; left: 0; top: -100%; width: 100%; height: 2px;
- 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%);
- animation: gridScanLine 8s linear infinite;
- pointer-events: none; z-index: 5;
- }
- @keyframes gridScanLine {
- 0% { top: -2px; }
- 100% { top: 100%; }
- }
- /* --- Camera cell staggered entrance --- */
- .video-cell {
- animation: camCellEntrance 0.5s ease-out both;
- }
- .video-cell:nth-child(1) { animation-delay: 0.05s; }
- .video-cell:nth-child(2) { animation-delay: 0.1s; }
- .video-cell:nth-child(3) { animation-delay: 0.15s; }
- .video-cell:nth-child(4) { animation-delay: 0.2s; }
- .video-cell:nth-child(5) { animation-delay: 0.25s; }
- .video-cell:nth-child(6) { animation-delay: 0.3s; }
- .video-cell:nth-child(7) { animation-delay: 0.35s; }
- .video-cell:nth-child(8) { animation-delay: 0.4s; }
- .video-cell:nth-child(9) { animation-delay: 0.45s; }
- @keyframes camCellEntrance {
- from { opacity: 0; transform: scale(0.92); }
- to { opacity: 1; transform: scale(1); }
- }
- /* --- Camera inner moving scanline --- */
- .cam-inner::before {
- content: ''; position: absolute; left: 0; width: 100%; height: 1px;
- 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%);
- animation: camInnerScan 6s linear infinite;
- pointer-events: none; z-index: 2;
- }
- @keyframes camInnerScan {
- 0% { top: 0; }
- 100% { top: 100%; }
- }
- /* AI cam inner has orange scanline */
- .video-cell.ai-cam .cam-inner::before {
- 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%);
- }
- /* --- Camera frame hover effect --- */
- .cam-frame {
- transition: box-shadow 0.3s ease, transform 0.3s ease;
- }
- .video-cell:hover .cam-frame {
- box-shadow: inset 0 0 28px rgba(72,215,255,0.35), 0 0 32px rgba(58,123,255,0.25) !important;
- transform: scale(1.01);
- }
- .video-cell.ai-cam:hover .cam-frame {
- box-shadow: inset 0 0 28px rgba(255,176,32,0.35), 0 0 32px rgba(255,140,0,0.25) !important;
- }
- /* --- Camera label typing glow --- */
- .cam-label {
- animation: camLabelGlow 3s ease-in-out infinite;
- }
- @keyframes camLabelGlow {
- 0%, 100% { box-shadow: none; }
- 50% { box-shadow: 0 0 8px rgba(0,0,0,0.3), 0 0 4px rgba(72,215,255,0.15); }
- }
- /* --- Video pagination effects --- */
- .video-pagination {
- position: relative;
- }
- .video-pagination::before {
- content: ''; position: absolute; top: 0; left: 20%; right: 20%; height: 1px;
- background: linear-gradient(90deg, transparent, rgba(72,215,255,0.2), transparent);
- animation: paginationLineGlow 3s ease-in-out infinite;
- }
- @keyframes paginationLineGlow {
- 0%, 100% { opacity: 0.3; }
- 50% { opacity: 1; }
- }
- .page-info {
- animation: pageInfoGlow 3s ease-in-out infinite;
- }
- @keyframes pageInfoGlow {
- 0%, 100% { text-shadow: none; }
- 50% { text-shadow: 0 0 8px rgba(72,215,255,0.3); color: #a0d4e8; }
- }
- /* Page button idle subtle pulse */
- .page-btn {
- animation: pageBtnPulse 4s ease-in-out infinite;
- }
- .page-btn:nth-child(3) { animation-delay: 2s; }
- @keyframes pageBtnPulse {
- 0%, 100% { border-color: var(--panel-border); }
- 50% { border-color: rgba(72,215,255,0.4); box-shadow: 0 0 8px rgba(72,215,255,0.08); }
- }
- /* --- Video-right background ambient particles --- */
- .video-right {
- position: relative;
- }
- .video-right::before {
- content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
- background-image:
- radial-gradient(1px 1px at 10% 20%, rgba(72,215,255,0.3) 50%, transparent 100%),
- radial-gradient(1px 1px at 30% 65%, rgba(72,215,255,0.25) 50%, transparent 100%),
- radial-gradient(1.5px 1.5px at 55% 15%, rgba(72,215,255,0.2) 50%, transparent 100%),
- radial-gradient(1px 1px at 75% 80%, rgba(72,215,255,0.3) 50%, transparent 100%),
- radial-gradient(1px 1px at 90% 40%, rgba(72,215,255,0.25) 50%, transparent 100%),
- radial-gradient(1.5px 1.5px at 45% 90%, rgba(72,215,255,0.2) 50%, transparent 100%);
- animation: videoParticlesDrift 12s ease-in-out infinite alternate;
- }
- @keyframes videoParticlesDrift {
- 0% { opacity: 0.3; transform: translateY(0); }
- 50% { opacity: 0.7; }
- 100% { opacity: 0.3; transform: translateY(-10px); }
- }
- /* --- Tree container scroll bar glow --- */
- .tree-container::-webkit-scrollbar-thumb {
- box-shadow: 0 0 4px rgba(72,215,255,0.2);
- }
- .tree-container::-webkit-scrollbar-thumb:hover {
- background: var(--accent);
- box-shadow: 0 0 8px rgba(72,215,255,0.4);
- }
- </style>
- </head>
- <body>
- <div class="viewport">
- <div class="screen" id="mainScreen">
- <!-- ===================== TOP NAV ===================== -->
- <div class="top-nav">
- <div class="nav-title">中国安全生产科学研究院实验室安全智慧化管控中心</div>
- <div class="nav-tabs">
- <div class="nav-tab active" onclick="switchPage('lab')">实验室情况</div>
- <div class="nav-tab" onclick="switchPage('video')">视频监控</div>
- </div>
- <div class="nav-right">
- <span class="weather">☁ 北京 · 晴 18°C</span>
- <span id="weekday"></span>
- <span class="clock" id="clock"></span>
- </div>
- </div>
- <!-- ===================== PAGE: 实验室情况 ===================== -->
- <div class="page active" id="page-lab">
- <!-- LEFT COLUMN -->
- <div class="col-left">
- <!-- 实验室基本情况统计 -->
- <div class="panel" style="height: 260px;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验室基本情况统计</div>
- <div id="chartLabOverview" style="width:100%;height:150px;"></div>
- <div class="status-cards">
- <div class="status-card using">
- <div class="label">使用</div><div class="value">20<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
- </div>
- <div class="status-card error">
- <div class="label">异常</div><div class="value">3<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
- </div>
- <div class="status-card idle">
- <div class="label">空闲</div><div class="value">35<span style="font-size:12px;color:var(--text-sub)"> 间</span></div>
- </div>
- </div>
- </div>
- <!-- 实验室安全分级统计 -->
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验室安全分级统计</div>
- <div id="chartGradeBar" style="width:100%;flex:1;min-height:0;"></div>
- </div>
- <!-- 实验室进入人数统计及走势 -->
- <div class="panel" style="height: 260px;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验室进入人数统计及走势</div>
- <div class="flip-counter-row">
- <div class="flip-group">
- <div class="flip-label">今日总进入人数</div>
- <div class="flip-digits" id="flipTotal"></div>
- </div>
- <div class="flip-group">
- <div class="flip-label">当前正在实验人数</div>
- <div class="flip-digits" id="flipCurrent"></div>
- </div>
- </div>
- <div id="chartPeopleLine" style="width:100%;height:130px;"></div>
- </div>
- </div>
- <!-- CENTER COLUMN -->
- <div class="col-center">
- <!-- 实验环境安全智能感知 -->
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验环境安全智能感知</div>
- <div class="sensor-scroll-wrap">
- <div class="sensor-scroll-inner" id="sensorList"></div>
- </div>
- </div>
- <!-- 实验室实时风险预警 -->
- <div class="panel" style="height: 320px; display:flex; flex-direction:column;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验室实时风险预警</div>
- <div class="warning-header">
- <div class="warning-count" id="warningCount">12</div>
- <div class="warning-count-label">本月预警响应总数</div>
- </div>
- <div class="warning-scroll-wrap">
- <div class="warning-scroll-inner" id="warningList"></div>
- </div>
- </div>
- </div>
- <!-- RIGHT COLUMN -->
- <div class="col-right">
- <!-- 智能环境感知应用设备统计 -->
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">智能环境感知应用设备统计</div>
- <div class="device-top">
- <div class="device-stat-card">
- <div class="ds-label">在线设备</div><div class="ds-value">186</div>
- </div>
- <div class="device-stat-card offline">
- <div class="ds-label">离线设备</div><div class="ds-value">14</div>
- </div>
- </div>
- <div class="device-bottom">
- <div class="device-gauge-wrap" id="chartGauge"></div>
- <div class="device-type-list">
- <div class="device-type-item"><span class="dt-name">电子信息铭牌</span><span class="dt-value">58</span></div>
- <div class="device-type-item"><span class="dt-name">化学品智能终端</span><span class="dt-value">32</span></div>
- <div class="device-type-item"><span class="dt-name">传感器</span><span class="dt-value">76</span></div>
- <div class="device-type-item"><span class="dt-name">智能设备</span><span class="dt-value">34</span></div>
- </div>
- </div>
- </div>
- <!-- 实验室设备分类及使用统计 -->
- <div class="panel" style="flex:1;display:flex;flex-direction:column;">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">实验室设备分类及使用统计</div>
- <div class="equip-chart-wrap">
- <div id="chartEquipPie" style="width:100%;height:100%;"></div>
- </div>
- <div class="equip-mid">
- <div class="equip-mid-item"><div class="em-value">586</div><div class="em-label">设备总数</div></div>
- <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>
- <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>
- </div>
- <div class="equip-bottom">
- <div class="equip-status-card"><div class="es-value" style="color:var(--accent)">128</div><div class="es-label">使用</div></div>
- <div class="equip-status-card"><div class="es-value" style="color:var(--good)">312</div><div class="es-label">空闲</div></div>
- <div class="equip-status-card"><div class="es-value" style="color:var(--warn)">108</div><div class="es-label">正常</div></div>
- <div class="equip-status-card"><div class="es-value" style="color:var(--danger)">38</div><div class="es-label">维修</div></div>
- </div>
- </div>
- </div>
- </div>
- <!-- ===================== PAGE: 视频监控 ===================== -->
- <div class="page" id="page-video">
- <!-- LEFT: Tree -->
- <div class="video-left panel">
- <div class="corner-deco tl"></div><div class="corner-deco tr"></div>
- <div class="corner-deco bl"></div><div class="corner-deco br"></div>
- <div class="panel-title">建筑结构导航</div>
- <input class="video-search" type="text" placeholder="🔍 搜索楼栋 / 楼层..." />
- <select class="video-filter">
- <option value="">全部二级单位</option>
- <option>安全技术研究所</option>
- <option>职业安全研究所</option>
- <option>化学品安全研究所</option>
- <option>矿山安全研究所</option>
- </select>
- <div class="tree-container" id="treeContainer"></div>
- </div>
- <!-- RIGHT: Video Grid -->
- <div class="video-right">
- <div class="video-breadcrumb">安科院院区 → 科研楼A → 3层</div>
- <div class="video-grid" id="videoGrid"></div>
- <div class="video-pagination">
- <button class="page-btn" onclick="videoPageChange(-1)">◀ 上一页</button>
- <span class="page-info" id="videoPageInfo">第 1 / 3 页</span>
- <button class="page-btn" onclick="videoPageChange(1)">下一页 ▶</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- ===================== ALERT MODAL ===================== -->
- <div class="alert-overlay" id="alertOverlay">
- <div class="alert-modal">
- <div class="alert-header">
- <span class="alert-icon">⚠</span>
- <span class="alert-title">安 全 预 警</span>
- </div>
- <div class="alert-body">
- <div class="alert-info">
- <div class="alert-info-row"><span class="a-label">实 验 室:</span><span class="a-value" id="alertLab">--</span></div>
- <div class="alert-info-row"><span class="a-label">楼栋楼层:</span><span class="a-value" id="alertBuilding">--</span></div>
- <div class="alert-info-row"><span class="a-label">所属单位:</span><span class="a-value" id="alertUnit">--</span></div>
- <div class="alert-info-row"><span class="a-label">异常参数:</span><span class="a-value danger" id="alertParam">--</span></div>
- <div class="alert-info-row"><span class="a-label">当前数值:</span><span class="a-value danger" id="alertValue">--</span></div>
- <div class="alert-info-row"><span class="a-label">实验室负责人:</span><span class="a-value" id="alertPerson">--</span></div>
- <div class="alert-info-row"><span class="a-label">联系电话:</span><span class="a-value" id="alertPhone">--</span></div>
- <div class="alert-info-row"><span class="a-label">预警时间:</span><span class="a-value" id="alertTime">--</span></div>
- </div>
- <div class="alert-video">
- <div class="alert-video-title">实时监控画面</div>
- <div class="alert-video-feed">
- <span class="cam-label"><span class="rec-dot"></span>REC <span id="alertCamName">--</span></span>
- <span class="cam-placeholder">📹 实时监控画面</span>
- </div>
- </div>
- </div>
- <div class="alert-footer">
- <button class="alert-close-btn" onclick="closeAlert()">确 认</button>
- </div>
- </div>
- </div>
- <script>
- /* ================================================================
- JavaScript - 中国安全生产科学研究院实验室安全智慧化管控中心
- ================================================================ */
- // ===================== Viewport Scaling =====================
- function fitScreen() {
- const scr = document.getElementById('mainScreen');
- const sw = 1920, sh = 1080;
- const vw = window.innerWidth, vh = window.innerHeight;
- const scale = Math.min(vw / sw, vh / sh);
- scr.style.transform = `translate(-50%, -50%) scale(${scale})`;
- }
- window.addEventListener('resize', fitScreen);
- fitScreen();
- // ===================== Auto Fullscreen =====================
- document.addEventListener('click', function onFirstClick() {
- if (document.documentElement.requestFullscreen) {
- document.documentElement.requestFullscreen().catch(() => {});
- }
- document.removeEventListener('click', onFirstClick);
- }, { once: true });
- // ===================== Clock =====================
- function updateClock() {
- const now = new Date();
- const hh = String(now.getHours()).padStart(2,'0');
- const mm = String(now.getMinutes()).padStart(2,'0');
- const ss = String(now.getSeconds()).padStart(2,'0');
- document.getElementById('clock').textContent = `${hh}:${mm}:${ss}`;
- const days = ['日','一','二','三','四','五','六'];
- document.getElementById('weekday').textContent = `星期${days[now.getDay()]}`;
- }
- setInterval(updateClock, 1000);
- updateClock();
- // ===================== Page Switch =====================
- function switchPage(page) {
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
- document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
- if (page === 'lab') {
- document.getElementById('page-lab').classList.add('active');
- document.querySelectorAll('.nav-tab')[0].classList.add('active');
- setTimeout(() => { initLabCharts(); }, 100);
- } else {
- document.getElementById('page-video').classList.add('active');
- document.querySelectorAll('.nav-tab')[1].classList.add('active');
- }
- }
- // ===================== Flip Counter =====================
- function renderFlip(id, num, len) {
- const str = String(num).padStart(len, '0');
- const container = document.getElementById(id);
- container.innerHTML = str.split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
- }
- renderFlip('flipTotal', 1286, 5);
- renderFlip('flipCurrent', 47, 4);
- // ===================== Sensor Scroll Data =====================
- const labNames = [
- { name: '化学实验室A101(A101)', unit: '化学品安全研究所' },
- { name: '材料力学实验室B203(B203)', unit: '安全技术研究所' },
- { name: '气体分析实验室A305(A305)', unit: '职业安全研究所' },
- { name: '高温高压实验室C102(C102)', unit: '矿山安全研究所' },
- { name: '生物安全实验室A202(A202)', unit: '职业安全研究所' },
- { name: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所' },
- { name: '电气安全实验室C201(C201)', unit: '安全技术研究所' },
- { name: '环境模拟实验室A408(A408)', unit: '化学品安全研究所' },
- { name: '爆炸安全实验室B302(B302)', unit: '安全技术研究所' },
- { name: '应急救援实验室C305(C305)', unit: '职业安全研究所' },
- { name: '有机化学实验室A103(A103)', unit: '化学品安全研究所' },
- { name: '无损检测实验室B205(B205)', unit: '安全技术研究所' },
- ];
- const sensorTypes = ['温度','湿度','TVOC','CO₂','O₂'];
- function randVal(type) {
- switch(type) {
- case '温度': return (18 + Math.random() * 15).toFixed(1) + '°C';
- case '湿度': return (30 + Math.random() * 50).toFixed(0) + '%';
- case 'TVOC': return (Math.random() * 2).toFixed(2) + 'mg/m³';
- case 'CO₂': return (300 + Math.random() * 800).toFixed(0) + 'ppm';
- case 'O₂': return (18 + Math.random() * 4).toFixed(1) + '%';
- default: return '--';
- }
- }
- function isAlarm(type, val) {
- const n = parseFloat(val);
- if (type === '温度' && n > 30) return true;
- if (type === 'TVOC' && n > 1.5) return true;
- if (type === 'CO₂' && n > 900) return true;
- if (type === 'O₂' && n < 19.5) return true;
- return false;
- }
- function buildSensorList() {
- const el = document.getElementById('sensorList');
- let html = '';
- const items = [...labNames, ...labNames]; // duplicate for seamless scroll
- items.forEach(lab => {
- html += `<div class="sensor-item"><div class="lab-name">${lab.name}<span class="lab-unit">${lab.unit}</span></div><div class="sensor-values">`;
- sensorTypes.forEach(s => {
- const v = randVal(s);
- const alarm = isAlarm(s, v);
- html += `<span class="sensor-val${alarm ? ' alarm' : ''}">${alarm ? '<span class="alarm-icon">⚠</span>' : ''}${s}: ${v}</span>`;
- });
- html += '</div></div>';
- });
- el.innerHTML = html;
- }
- buildSensorList();
- // ===================== Warning Scroll Data =====================
- function buildWarningList() {
- const el = document.getElementById('warningList');
- const warnings = [
- { lab: '化学实验室A101(A101)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.82mg/m³', time: '2026-03-04 14:32:18' },
- { lab: '高温高压实验室C102(C102)', unit: '矿山安全研究所', sensor: '温度', value: '35.6°C', time: '2026-03-04 14:28:05' },
- { lab: '气体分析实验室A305(A305)', unit: '职业安全研究所', sensor: 'CO₂', value: '1050ppm', time: '2026-03-04 13:55:42' },
- { lab: '粉尘检测实验室B101(B101)', unit: '矿山安全研究所', sensor: 'O₂', value: '18.2%', time: '2026-03-04 13:20:10' },
- { lab: '爆炸安全实验室B302(B302)', unit: '安全技术研究所', sensor: 'TVOC', value: '1.95mg/m³', time: '2026-03-04 12:45:33' },
- { lab: '环境模拟实验室A408(A408)', unit: '化学品安全研究所', sensor: '温度', value: '33.8°C', time: '2026-03-04 11:18:27' },
- { lab: '生物安全实验室A202(A202)', unit: '职业安全研究所', sensor: 'CO₂', value: '980ppm', time: '2026-03-04 10:42:51' },
- { lab: '有机化学实验室A103(A103)', unit: '化学品安全研究所', sensor: 'TVOC', value: '1.68mg/m³', time: '2026-03-04 09:36:14' },
- ];
- const all = [...warnings, ...warnings]; // duplicate for seamless scroll
- let html = '';
- all.forEach(w => {
- html += `<div class="warning-item">
- <div class="w-lab">${w.lab} - ${w.unit}</div>
- <div class="w-sensor">异常: ${w.sensor} ${w.value}</div>
- <div class="w-time">${w.time}</div>
- </div>`;
- });
- el.innerHTML = html;
- }
- buildWarningList();
- // ===================== Video Page: Tree =====================
- const treeData = {
- name: '安科院院区', children: [
- { name: '科研楼A', children: [
- { name: '1层', children: [{name:'A101'},{name:'A102'},{name:'A103'}] },
- { name: '2层', children: [{name:'A201'},{name:'A202'},{name:'A203'}] },
- { name: '3层', children: [{name:'A301'},{name:'A302'},{name:'A303'},{name:'A304'},{name:'A305'}] },
- { name: '4层', children: [{name:'A401'},{name:'A402'},{name:'A403'},{name:'A408'}] },
- ]},
- { name: '科研楼B', children: [
- { name: '1层', children: [{name:'B101'},{name:'B102'}] },
- { name: '2层', children: [{name:'B201'},{name:'B202'},{name:'B203'},{name:'B205'}] },
- { name: '3层', children: [{name:'B301'},{name:'B302'}] },
- ]},
- { name: '科研楼C', children: [
- { name: '1层', children: [{name:'C101'},{name:'C102'}] },
- { name: '2层', children: [{name:'C201'},{name:'C202'}] },
- { name: '3层', children: [{name:'C301'},{name:'C305'}] },
- ]},
- ]
- };
- function buildTree(node, depth) {
- const hasChild = node.children && node.children.length > 0;
- let html = `<div class="tree-node">
- <div class="tree-node-content" style="padding-left:${depth * 12}px" onclick="toggleTree(this)">
- <span class="tree-arrow">${hasChild ? '▶' : ''}</span>
- <span>${node.name}</span>
- </div>`;
- if (hasChild) {
- html += `<div class="tree-children">`;
- node.children.forEach(c => { html += buildTree(c, depth + 1); });
- html += `</div>`;
- }
- html += `</div>`;
- return html;
- }
- document.getElementById('treeContainer').innerHTML = buildTree(treeData, 0);
- function toggleTree(el) {
- const children = el.parentElement.querySelector('.tree-children');
- const arrow = el.querySelector('.tree-arrow');
- if (children) {
- children.classList.toggle('open');
- arrow.classList.toggle('expanded');
- }
- // select highlight
- document.querySelectorAll('.tree-node-content').forEach(n => n.classList.remove('selected'));
- el.classList.add('selected');
- }
- // ===================== Video Grid =====================
- let currentVideoPage = 1;
- const totalVideoPages = 3;
- function renderVideoGrid() {
- const grid = document.getElementById('videoGrid');
- let html = '';
- for (let i = 0; i < 9; i++) {
- const roomNum = (currentVideoPage - 1) * 9 + i + 1;
- const isAI = i === 0;
- html += `<div class="video-cell${isAI ? ' ai-cam' : ''}">
- <div class="cam-frame">
- <div class="cam-corner c-tl"></div><div class="cam-corner c-tr"></div>
- <div class="cam-corner c-bl"></div><div class="cam-corner c-br"></div>
- ${isAI ? '<span class="ai-badge">AI 智能</span>' : ''}
- <div class="cam-inner">
- <span class="cam-label"><span class="cam-dot"></span>${isAI ? '🎯 ' : ''}A3${String(roomNum).padStart(2,'0')} 摄像头</span>
- <span class="cam-placeholder">📹 实时监控画面</span>
- </div>
- </div>
- </div>`;
- }
- grid.innerHTML = html;
- document.getElementById('videoPageInfo').textContent = `第 ${currentVideoPage} / ${totalVideoPages} 页`;
- }
- renderVideoGrid();
- function videoPageChange(dir) {
- currentVideoPage = Math.max(1, Math.min(totalVideoPages, currentVideoPage + dir));
- renderVideoGrid();
- }
- // ===================== Alert Modal =====================
- function showAlert(lab, building, unit, param, value, person, phone) {
- document.getElementById('alertLab').textContent = lab;
- document.getElementById('alertBuilding').textContent = building || '--';
- document.getElementById('alertUnit').textContent = unit;
- document.getElementById('alertParam').textContent = param;
- document.getElementById('alertValue').textContent = value;
- document.getElementById('alertPerson').textContent = person || '--';
- document.getElementById('alertPhone').textContent = phone || '--';
- document.getElementById('alertCamName').textContent = lab;
- const now = new Date();
- document.getElementById('alertTime').textContent =
- `${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')}`;
- document.getElementById('alertOverlay').classList.add('show');
- }
- function closeAlert() {
- document.getElementById('alertOverlay').classList.remove('show');
- }
- // Demo: trigger alert after 15s
- setTimeout(() => {
- showAlert('化学实验室A101(A101)', '科研楼A-1层', '化学品安全研究所', 'TVOC 超标', '1.82 mg/m³', '张明', '138-0012-3456');
- }, 15000);
- // ===================== ECharts Initialization =====================
- let chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie;
- function initLabCharts() {
- // ---- 1. Lab Overview Ring ----
- if (chartLabOverview) chartLabOverview.dispose();
- chartLabOverview = echarts.init(document.getElementById('chartLabOverview'));
- chartLabOverview.setOption({
- tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
- legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
- series: [{
- type: 'pie', radius: ['45%','70%'], center: ['35%','50%'],
- label: {
- show: true, position: 'center',
- formatter: '58\n实验室总数', fontSize: 14, color: '#fff', lineHeight: 20
- },
- data: [
- { value: 8, name: 'I级(红)', itemStyle: { color: '#ff4d4f' } },
- { value: 12, name: 'II级(橙)', itemStyle: { color: '#ff8c00' } },
- { value: 18, name: 'III级(黄)', itemStyle: { color: '#ffcc00' } },
- { value: 20, name: 'IV级(蓝)', itemStyle: { color: '#3a7bff' } },
- ],
- emphasis: { scaleSize: 6 }
- }]
- });
- // ---- 2. Grade Stacked Bar ----
- if (chartGradeBar) chartGradeBar.dispose();
- chartGradeBar = echarts.init(document.getElementById('chartGradeBar'));
- const units = ['安全技术\n研究所','职业安全\n研究所','化学品安全\n研究所','矿山安全\n研究所','应急科学\n研究中心','信息技术\n研究所','检测检验\n中心','标准化\n研究所'];
- const totals = [12, 10, 14, 8, 6, 4, 3, 1];
- const gradeData = {
- 'I级': [2,1,3,2,1,0,0,0],
- 'II级': [3,2,4,2,1,1,1,0],
- 'III级': [4,4,4,2,2,2,1,1],
- 'IV级': [3,3,3,2,2,1,1,0]
- };
- const colors = { 'I级': '#ff4d4f', 'II级': '#ff8c00', 'III级': '#ffcc00', 'IV级': '#3a7bff' };
- let barOption = {
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
- legend: { data: Object.keys(gradeData), top: 0, textStyle: { color: '#7eacc8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
- grid: { left: 40, right: 10, top: 35, bottom: 50 },
- xAxis: {
- type: 'category', data: units,
- axisLabel: {
- color: '#7eacc8', fontSize: 10, interval: 0,
- formatter: function(v, idx) { return v + '\n(' + totals[idx] + ')'; }
- },
- axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- axisLabel: { color: '#7eacc8', fontSize: 10 },
- axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } },
- splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
- },
- dataZoom: [{
- type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5
- }],
- series: Object.keys(gradeData).map(k => ({
- name: k, type: 'bar', stack: 'total', barWidth: 20,
- data: gradeData[k],
- itemStyle: { color: colors[k], borderRadius: [0,0,0,0] }
- }))
- };
- chartGradeBar.setOption(barOption);
- // Auto scroll X axis
- let barScrollIdx = 0;
- setInterval(() => {
- barScrollIdx++;
- if (barScrollIdx > units.length - 5) barScrollIdx = 0;
- chartGradeBar.dispatchAction({ type: 'dataZoom', startValue: barScrollIdx, endValue: barScrollIdx + 5 });
- }, 5000);
- // ---- 3. People Line Chart ----
- if (chartPeopleLine) chartPeopleLine.dispose();
- chartPeopleLine = echarts.init(document.getElementById('chartPeopleLine'));
- const timeSlots = ['0:00','3:00','6:00','9:00','12:00','15:00','18:00','21:00','24:00'];
- chartPeopleLine.setOption({
- tooltip: { trigger: 'axis', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
- legend: { data: ['进入人数','实验人数'], top: 0, right: 0, textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 14, itemHeight: 2 },
- grid: { left: 35, right: 10, top: 24, bottom: 20 },
- xAxis: {
- type: 'category', data: timeSlots, boundaryGap: false,
- axisLabel: { color: '#7eacc8', fontSize: 10 },
- axisLine: { lineStyle: { color: 'rgba(72,180,255,0.2)' } }
- },
- yAxis: {
- type: 'value',
- axisLabel: { color: '#7eacc8', fontSize: 10 },
- splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
- },
- series: [
- {
- name: '进入人数', type: 'line', smooth: true,
- data: [0, 2, 5, 86, 120, 145, 98, 42, 0],
- lineStyle: { color: '#48d7ff', width: 2 },
- 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)'}]) },
- itemStyle: { color: '#48d7ff' }, symbolSize: 4
- },
- {
- name: '实验人数', type: 'line', smooth: true,
- data: [0, 1, 3, 52, 68, 78, 55, 20, 0],
- lineStyle: { color: '#3a7bff', width: 2 },
- 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)'}]) },
- itemStyle: { color: '#3a7bff' }, symbolSize: 4
- }
- ]
- });
- // ---- 4. Device Gauge ----
- if (chartGauge) chartGauge.dispose();
- chartGauge = echarts.init(document.getElementById('chartGauge'));
- chartGauge.setOption({
- series: [{
- type: 'gauge', startAngle: 210, endAngle: -30,
- radius: '90%', center: ['50%','58%'],
- min: 0, max: 100,
- axisLine: {
- lineStyle: {
- width: 14,
- color: [[0.7,'rgba(72,215,255,0.2)'],[0.9,'rgba(72,215,255,0.4)'],[1,'#48d7ff']]
- }
- },
- axisTick: { show: false },
- splitLine: { show: false },
- axisLabel: { show: false },
- pointer: {
- length: '60%', width: 4,
- itemStyle: { color: '#48d7ff' }
- },
- title: { show: true, offsetCenter: [0, '72%'], fontSize: 12, color: '#7eacc8' },
- detail: {
- valueAnimation: true, fontSize: 26, color: '#48d7ff', fontWeight: 700,
- offsetCenter: [0, '40%'],
- formatter: '{value}%'
- },
- data: [{ value: 93, name: '设备在线率' }]
- }]
- });
- // ---- 5. Equipment Pie ----
- if (chartEquipPie) chartEquipPie.dispose();
- chartEquipPie = echarts.init(document.getElementById('chartEquipPie'));
- chartEquipPie.setOption({
- tooltip: { trigger: 'item', backgroundColor: 'rgba(6,22,56,0.9)', borderColor: 'rgba(72,180,255,0.3)', textStyle: { color: '#d8f4ff' } },
- legend: { orient: 'vertical', right: 4, top: 'center', textStyle: { color: '#7eacc8', fontSize: 10 }, itemWidth: 8, itemHeight: 8 },
- series: [{
- type: 'pie', radius: ['30%', '60%'], center: ['35%','50%'],
- label: { show: false },
- data: [
- { value: 120, name: '分析仪器', itemStyle: { color: '#48d7ff' } },
- { value: 95, name: '安全防护', itemStyle: { color: '#3a7bff' } },
- { value: 86, name: '化学试剂设备', itemStyle: { color: '#36d399' } },
- { value: 78, name: '电气设备', itemStyle: { color: '#ffb020' } },
- { value: 65, name: '力学测试', itemStyle: { color: '#ff8c00' } },
- { value: 52, name: '环境监测', itemStyle: { color: '#ff4d4f' } },
- { value: 90, name: '通用设备', itemStyle: { color: '#a78bfa' } },
- ],
- emphasis: { scaleSize: 4 }
- }]
- });
- }
- // Init charts on load
- setTimeout(initLabCharts, 200);
- // Handle resize
- window.addEventListener('resize', () => {
- [chartLabOverview, chartGradeBar, chartPeopleLine, chartGauge, chartEquipPie].forEach(c => c && c.resize());
- });
- </script>
- </body>
- </html>
|