| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>实验室安全智能监测与管控中心</title>
- <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
- <style>
- /* ========== RESET ========== */
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
- /* ========== DEEP BLUE SCI-FI THEME ========== */
- :root {
- --bg-deep: #030e1f;
- --bg-panel: rgba(5,16,42,0.95);
- --bg-card: rgba(7,22,54,0.85);
- --blue: #1e90ff;
- --blue-dim: rgba(30,144,255,0.14);
- --blue-glow: rgba(30,144,255,0.5);
- --cyan: #00d8ff;
- --cyan-dim: rgba(0,216,255,0.12);
- --teal: #00e5c8;
- --indigo: #4361ee;
- --red: #ff3b3b;
- --green: #00e676;
- --gold: #ffd740;
- --white: #ddf0ff;
- --text: #a8cce8;
- --text-dim: rgba(110,165,210,0.55);
- --border: rgba(30,144,255,0.22);
- --border-h: rgba(30,144,255,0.65);
- --shadow: 0 4px 24px rgba(0,80,220,0.22);
- --font-main: 'PingFang SC','Microsoft YaHei','Noto Sans SC',sans-serif;
- }
- html, body { width:100%; height:100%; overflow:auto; background:var(--bg-deep); font-family:var(--font-main); color:var(--text); }
- /* ========== FIXED SIZE WRAPPER ========== */
- #scale-root {
- width:9600px; height:2800px;
- position:relative;
- overflow:hidden;
- display:flex; flex-direction:column;
- }
- /* ========== BACKGROUND ========== */
- #nebula-canvas { position:fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:0; }
- .bg-hex {
- position:absolute; inset:0; pointer-events:none; z-index:0;
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='52' viewBox='0 0 60 52'%3E%3Cpolygon points='30,2 58,17 58,47 30,52 2,47 2,17' fill='none' stroke='rgba(30,144,255,0.05)' stroke-width='1'/%3E%3C/svg%3E");
- background-size:150px 130px;
- }
- .bg-hex::after { content:''; position:absolute; inset:0; background:radial-gradient(ellipse at 50% 50%, transparent 25%, var(--bg-deep) 85%); }
- /* ========== TOP NAV ========== */
- .top-nav {
- position:relative; z-index:10; flex-shrink:0;
- height:160px; display:flex; align-items:stretch;
- background:linear-gradient(180deg, rgba(4,18,52,0.99) 0%, rgba(3,12,36,0.97) 100%);
- border-bottom:1px solid rgba(30,144,255,0.3);
- box-shadow:0 2px 30px rgba(0,80,220,0.28), inset 0 1px 0 rgba(30,144,255,0.15);
- }
- .nav-left { display:flex; align-items:center; padding:0 50px; width:950px; gap:30px; }
- .nav-logo {
- width:100px; height:100px; border-radius:20px; flex-shrink:0;
- background:linear-gradient(135deg,#0d2d6b,#1565c0);
- display:flex; align-items:center; justify-content:center;
- box-shadow:0 0 40px rgba(30,144,255,0.6), inset 0 0 20px rgba(255,255,255,0.1);
- position:relative; overflow:visible;
- }
- .nav-logo::after { content:'安'; font-size:45px; font-weight:900; color:rgba(255,255,255,0.95); }
- .nav-logo-ring {
- position:absolute; inset:-10px; border-radius:25px;
- border:2px solid rgba(30,144,255,0.5);
- animation:ringPulse 2s ease-in-out infinite;
- }
- @keyframes ringPulse { 0%,100%{transform:scale(1);opacity:0.5} 50%{transform:scale(1.1);opacity:1} }
- .nav-org { font-size:30px; color:var(--text-dim); line-height:1.4; }
- .nav-org strong { display:block; font-size:32px; color:var(--cyan); }
- .nav-center {
- flex:1; display:flex; align-items:center; justify-content:center; gap:30px; position:relative;
- }
- .nav-title {
- font-size:65px; font-weight:700; letter-spacing:10px;
- background:linear-gradient(90deg,var(--blue),var(--cyan),var(--white),var(--cyan),var(--blue));
- background-size:200% auto;
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
- animation:titleShine 4s linear infinite;
- filter:drop-shadow(0 0 40px rgba(30,144,255,0.5));
- }
- @keyframes titleShine { from{background-position:0% center} to{background-position:200% center} }
- .nav-title-deco {
- position:absolute; bottom:15px; left:50%; transform:translateX(-50%);
- width:900px; height:2px;
- background:linear-gradient(90deg,transparent,var(--blue),var(--cyan),var(--blue),transparent);
- animation:decoGlow 3s ease-in-out infinite;
- }
- @keyframes decoGlow { 0%,100%{opacity:0.5} 50%{opacity:1;filter:blur(1px)} }
- .btn-nav-switch {
- padding:18px 55px; border-radius:50px; cursor:pointer; outline:none;
- background:var(--bg-deep);
- border:2px solid var(--blue); color:var(--cyan);
- font-size:32px; font-weight:600; letter-spacing:2px;
- transition:all 0.3s; position:relative; overflow:hidden; flex-shrink:0;
- }
- .btn-nav-switch::before {
- content:''; position:absolute; top:0; left:-100%; width:100%; height:100%;
- background:linear-gradient(90deg,transparent,rgba(30,144,255,0.25),transparent);
- animation:btnShine 3s linear infinite;
- }
- @keyframes btnShine { from{left:-100%} to{left:100%} }
- .btn-nav-switch:hover { box-shadow:0 0 60px rgba(30,144,255,0.6); transform:translateY(-2px); }
- .nav-right { width:950px; display:flex; align-items:center; justify-content:flex-end; padding:0 50px; gap:50px; }
- .nav-weather { display:flex; align-items:center; gap:20px; font-size:32px; color:var(--text-dim); }
- .nav-weather .icon { font-size:45px; }
- .nav-clock .time { font-size:55px; font-weight:300; color:var(--gold); font-variant-numeric:tabular-nums; letter-spacing:4px; }
- .nav-clock .date { font-size:30px; color:var(--text-dim); margin-top:5px; text-align:right; }
- /* ========== MAIN CONTENT: 4 COLS 2:2:4:2 ========== */
- .main-content {
- flex:1; min-height:0;
- display:grid;
- grid-template-columns:2fr 1.5fr 4.5fr 2fr;
- gap:0; padding:20px 20px 20px;
- position:relative; z-index:2;
- }
- /* ========== PANEL SYSTEM ========== */
- .panel-col { display:flex; flex-direction:column; gap:18px; min-height:0; padding:0 10px; }
- .panel {
- position:relative; border-radius:15px; overflow:hidden;
- background:var(--bg-panel);
- border:1px solid var(--border);
- flex-shrink:0;
- }
- .panel::before {
- content:''; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
- background:linear-gradient(135deg,rgba(30,144,255,0.05) 0%,transparent 50%,rgba(0,216,255,0.03) 100%);
- }
- /* Corner ornaments */
- .pc { position:absolute; width:35px; height:35px; z-index:3; pointer-events:none; }
- .pc.tl { top:0; left:0; }
- .pc.tr { top:0; right:0; transform:scaleX(-1); }
- .pc.bl { bottom:0; left:0; transform:scaleY(-1); }
- .pc.br { bottom:0; right:0; transform:scale(-1); }
- .pc svg { width:100%; height:100%; }
- /* Animated border beam */
- .border-beam { position:absolute; inset:0; pointer-events:none; border-radius:inherit; overflow:hidden; z-index:2; }
- .border-beam::before {
- content:''; position:absolute; top:0; left:-100%; width:40%; height:3px;
- background:linear-gradient(90deg,transparent,rgba(30,144,255,0.9),rgba(0,216,255,0.7),transparent);
- animation:beamTop 5s linear infinite;
- }
- .border-beam::after {
- content:''; position:absolute; bottom:0; right:-100%; width:40%; height:3px;
- background:linear-gradient(90deg,transparent,rgba(0,216,255,0.7),rgba(30,144,255,0.9),transparent);
- animation:beamBottom 5s linear infinite 2.5s;
- }
- @keyframes beamTop { from{left:-40%} to{left:100%} }
- @keyframes beamBottom { from{right:-40%} to{right:100%} }
- .panel-header {
- display:flex; align-items:center; gap:25px;
- padding:20px 30px 18px;
- border-bottom:1px solid var(--border);
- background:linear-gradient(90deg,rgba(0,60,160,0.18),transparent);
- flex-shrink:0;
- }
- .panel-header-icon {
- width:65px; height:65px; border-radius:12px; flex-shrink:0;
- display:flex; align-items:center; justify-content:center; font-size:32px;
- background:linear-gradient(135deg,rgba(30,144,255,0.25),rgba(0,216,255,0.15));
- border:1px solid rgba(30,144,255,0.35);
- animation:iconGlow 3s ease-in-out infinite;
- }
- @keyframes iconGlow {
- 0%,100%{box-shadow:0 0 15px rgba(30,144,255,0.3)}
- 50%{box-shadow:0 0 35px rgba(30,144,255,0.7),0 0 12px rgba(0,216,255,0.3)}
- }
- .panel-title { font-size:30px; font-weight:600; letter-spacing:2px; color:var(--cyan); }
- /* ========== STATUS BADGES ========== */
- .status-row { display:flex; gap:15px; }
- .status-badge {
- flex:1; padding:15px 10px; border-radius:10px; text-align:center;
- background:var(--bg-card); border:1px solid var(--border);
- }
- .status-badge.active { border-color:var(--green); background:rgba(0,230,118,0.07); }
- .status-badge.warning { border-color:#f59e0b; background:rgba(245,158,11,0.07); }
- .status-badge.idle { border-color:var(--indigo); background:rgba(67,97,238,0.07); }
- .status-badge .val { font-size:48px; font-weight:700; }
- .status-badge.active .val { color:var(--green); }
- .status-badge.warning .val { color:#f59e0b; }
- .status-badge.idle .val { color:var(--indigo); }
- .status-badge .lbl { font-size:25px; color:var(--text-dim); margin-top:5px; }
- /* ========== FLIP COUNTER ========== */
- .flip-counters { display:flex; gap:25px; }
- .flip-counter { flex:1; text-align:center; padding:18px 15px; border-radius:10px; background:var(--bg-card); border:1px solid var(--border); }
- .flip-counter .fc-label { font-size:25px; color:var(--text-dim); margin-bottom:12px; letter-spacing:2px; }
- .flip-counter .fc-digits { display:flex; gap:8px; justify-content:center; }
- .flip-digit {
- width:50px; height:75px; border-radius:8px;
- background:linear-gradient(180deg,rgba(4,18,55,0.9),rgba(2,10,32,0.95));
- border:1px solid rgba(30,144,255,0.3);
- display:flex; align-items:center; justify-content:center;
- font-size:45px; font-weight:700; color:var(--gold);
- font-variant-numeric:tabular-nums; position:relative; overflow:hidden;
- }
- .flip-digit::after { content:''; position:absolute; left:0; right:0; top:50%; height:2px; background:rgba(0,0,0,0.4); }
- /* ========== SENSOR SCROLL ========== */
- .sensor-scroll-wrap { overflow:hidden; flex:1; min-height:0; }
- .sensor-scroll-inner { animation:scrollUp 30s linear infinite; }
- .sensor-scroll-inner:hover { animation-play-state:paused; }
- @keyframes scrollUp { 0%{transform:translateY(0)} 100%{transform:translateY(-50%)} }
- .sensor-item {
- padding:18px 20px; border-radius:10px; margin-bottom:12px;
- background:var(--bg-card); border:1px solid var(--border);
- }
- .sensor-item.alert {
- border-color:rgba(255,59,59,0.6); background:rgba(255,30,30,0.07);
- animation:alertGlow 1.5s ease-in-out infinite;
- }
- @keyframes alertGlow { 0%,100%{box-shadow:0 0 12px rgba(255,59,59,0.25)} 50%{box-shadow:0 0 40px rgba(255,59,59,0.6)} }
- .sensor-item-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
- .sensor-name { font-size:28px; font-weight:600; color:var(--white); }
- .sensor-unit { font-size:25px; color:var(--text-dim); }
- .sensor-metrics { display:flex; gap:10px; flex-wrap:wrap; }
- .sensor-metric {
- padding:5px 15px; border-radius:5px; font-size:25px;
- background:rgba(4,16,45,0.7); border:1px solid rgba(30,144,255,0.2);
- display:flex; align-items:center; gap:8px; color:var(--text);
- }
- .sensor-metric.alarm {
- background:rgba(255,0,30,0.12); border-color:var(--red); color:var(--red);
- animation:blinkRed 0.8s ease-in-out infinite;
- }
- @keyframes blinkRed { 0%,100%{opacity:1} 50%{opacity:0.35} }
- /* ========== WARNING SCROLL ========== */
- .warn-scroll-wrap { overflow:hidden; flex:1; min-height:0; }
- .warn-scroll-inner { animation:scrollUp 22s linear infinite; }
- .warn-scroll-inner:hover { animation-play-state:paused; }
- .warn-item {
- padding:18px 20px; border-radius:10px; margin-bottom:12px;
- background:rgba(245,158,11,0.05); border:1px solid rgba(245,158,11,0.2);
- }
- .warn-item-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px; gap:15px; }
- .warn-lab { font-size:28px; font-weight:600; color:#fcd34d; flex:1; }
- .warn-time { font-size:25px; color:var(--text-dim); white-space:nowrap; }
- .warn-detail { font-size:25px; color:var(--text-dim); display:flex; align-items:center; gap:10px; }
- .warn-metric-val { color:#fb923c; font-weight:600; }
- /* ========== DEVICE STATS ========== */
- .device-online-row { display:flex; gap:18px; margin-bottom:18px; }
- .device-stat-chip {
- flex:1; padding:15px; border-radius:10px; text-align:center;
- background:var(--bg-card); border:1px solid var(--border);
- }
- .device-stat-chip.online { border-color:var(--green); }
- .device-stat-chip.offline { border-color:var(--red); }
- .device-stat-chip .dv { font-size:50px; font-weight:700; }
- .device-stat-chip.online .dv { color:var(--green); }
- .device-stat-chip.offline .dv { color:var(--red); }
- .device-stat-chip .dl { font-size:25px; color:var(--text-dim); }
- .device-list-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
- .device-list-item {
- padding:15px; border-radius:10px;
- background:var(--bg-card); border:1px solid var(--border);
- display:flex; flex-direction:column; align-items:center; gap:8px;
- }
- .device-list-item .dli-icon { font-size:40px; }
- .device-list-item .dli-num { font-size:38px; font-weight:700; color:var(--gold); }
- .device-list-item .dli-name { font-size:22px; color:var(--text-dim); text-align:center; line-height:1.3; }
- /* ========== EQUIP STATS ========== */
- .equip-mid-row { display:flex; gap:15px; }
- .equip-stat-item { flex:1; text-align:center; padding:15px 10px; border-radius:10px; background:var(--bg-card); border:1px solid var(--border); }
- .equip-stat-item .ev { font-size:38px; font-weight:700; color:var(--gold); }
- .equip-stat-item .el { font-size:22px; color:var(--text-dim); margin-top:5px; }
- /* ========== EQUIP STATUS CARDS ========== */
- .equip-status-card {
- position:relative; border-radius:12px; padding:18px 22px 15px;
- background:var(--bg-card);
- border:1px solid color-mix(in srgb, var(--sc) 35%, transparent);
- display:flex; flex-direction:column; justify-content:space-between;
- overflow:hidden; transition:border-color 0.3s;
- }
- .equip-status-card::before {
- content:''; position:absolute; inset:0;
- background:radial-gradient(ellipse at 10% 10%, color-mix(in srgb, var(--sc) 10%, transparent) 0%, transparent 65%);
- pointer-events:none;
- }
- .equip-status-card:hover { border-color:color-mix(in srgb, var(--sc) 70%, transparent); }
- .esc-icon {
- width:70px; height:70px; color:var(--sc);
- filter:drop-shadow(0 0 12px var(--sc));
- animation:iconGlow 3s ease-in-out infinite;
- flex-shrink:0;
- }
- .esc-num {
- font-size:55px; font-weight:700; letter-spacing:2px;
- color:var(--sc); line-height:1;
- text-shadow:0 0 30px color-mix(in srgb, var(--sc) 60%, transparent);
- }
- .esc-label { font-size:25px; color:var(--text-dim); margin-top:2px; }
- .esc-bar { height:5px; border-radius:2px; background:rgba(255,255,255,0.06); margin-top:10px; overflow:hidden; }
- .esc-bar-fill {
- height:100%; border-radius:2px;
- background:linear-gradient(90deg, var(--sc), color-mix(in srgb, var(--sc) 50%, transparent));
- box-shadow:0 0 10px var(--sc);
- }
- /* ========== CENTER COL (3区) ========== */
- .center-col { display:flex; flex-direction:column; padding:0 10px; min-height:0; position:relative; }
- /* Monitor view — 全时显示 */
- #view-monitor {
- flex:1; display:flex; flex-direction:column; border-radius:15px;
- border:1px solid var(--border); background:var(--bg-panel); overflow:hidden; min-height:0;
- }
- .monitor-header {
- padding:20px 30px; border-bottom:1px solid var(--border); flex-shrink:0;
- display:flex; align-items:center; gap:25px;
- background:linear-gradient(90deg,rgba(0,60,160,0.18),transparent);
- }
- .monitor-inner { flex:1; display:grid; grid-template-columns:575px 1fr; gap:0; min-height:0; overflow:hidden; }
- .monitor-left {
- display:flex; flex-direction:column; gap:15px; padding:20px; border-right:1px solid var(--border);
- min-height:0; overflow:hidden;
- }
- .monitor-right { padding:20px; display:flex; flex-direction:column; gap:15px; min-height:0; }
- .search-box {
- display:flex; align-items:center; gap:18px; padding:15px 25px; border-radius:10px;
- background:var(--bg-card); border:1px solid var(--border); flex-shrink:0;
- }
- .search-box input { flex:1; background:none; border:none; outline:none; color:var(--text); font-size:30px; font-family:var(--font-main); }
- .search-box input::placeholder { color:var(--text-dim); }
- .filter-select {
- padding:12px 25px; border-radius:10px; width:100%; flex-shrink:0;
- background:var(--bg-card); border:1px solid var(--border);
- color:var(--text); font-size:28px; font-family:var(--font-main); outline:none; cursor:pointer; appearance:none;
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='rgba(30,144,255,0.6)'/%3E%3C/svg%3E");
- background-repeat:no-repeat; background-position:right 25px center;
- }
- .tree-wrap { flex:1; overflow-y:auto; overflow-x:hidden; min-height:0; }
- .tree-wrap::-webkit-scrollbar { width:8px; }
- .tree-wrap::-webkit-scrollbar-thumb { background:var(--border); border-radius:5px; }
- .tree-node-label {
- display:flex; align-items:center; gap:15px; padding:12px 20px; border-radius:8px;
- font-size:28px; color:var(--text); transition:all 0.2s; user-select:none; cursor:pointer;
- }
- .tree-node-label:hover { background:var(--blue-dim); color:var(--blue); }
- .tree-node-label.selected { background:rgba(30,144,255,0.15); color:var(--cyan); border-left:5px solid var(--blue); }
- .tree-node-label .arrow { transition:transform 0.2s; font-size:25px; color:var(--text-dim); flex-shrink:0; }
- .tree-node-label .arrow.open { transform:rotate(90deg); }
- .tree-children { padding-left:35px; display:none; }
- .tree-children.open { display:block; }
- .camera-grid-header { display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
- .camera-breadcrumb { font-size:30px; color:var(--text-dim); }
- .camera-breadcrumb span { color:var(--cyan); }
- .camera-pager { display:flex; align-items:center; gap:15px; }
- .pager-btn {
- width:60px; height:60px; border-radius:8px; cursor:pointer;
- background:var(--bg-card); border:1px solid var(--border);
- color:var(--text-dim); font-size:32px; display:flex; align-items:center; justify-content:center; transition:all 0.2s;
- }
- .pager-btn:hover { border-color:var(--blue); color:var(--blue); }
- .pager-info { font-size:28px; color:var(--text-dim); }
- .camera-grid {
- display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr);
- gap:12px; flex:1; min-height:0;
- }
- .camera-cell {
- position:relative; border-radius:10px; overflow:hidden;
- background:#020810; border:1px solid var(--border);
- display:flex; align-items:center; justify-content:center;
- }
- .camera-cell:hover { border-color:var(--blue); }
- .camera-cell.ai-cam { border-color:rgba(0,230,118,0.4); }
- .camera-cell canvas { width:100%; height:100%; display:block; }
- .camera-overlay { position:absolute; inset:0; pointer-events:none; background:linear-gradient(180deg,rgba(0,0,0,0.4) 0%,transparent 30%,transparent 70%,rgba(0,0,0,0.5) 100%); }
- .camera-label { position:absolute; bottom:10px; left:15px; font-size:25px; color:rgba(255,255,255,0.75); background:rgba(0,0,0,0.5); padding:3px 10px; border-radius:5px; }
- .camera-rec { position:absolute; top:10px; right:15px; display:flex; align-items:center; gap:8px; font-size:25px; color:var(--red); background:rgba(0,0,0,0.5); padding:3px 10px; border-radius:5px; }
- .camera-rec::before { content:''; width:12px; height:12px; border-radius:50%; background:var(--red); animation:blinkRed 1s ease-in-out infinite; }
- .camera-ai-badge { position:absolute; top:10px; left:15px; font-size:25px; color:var(--green); background:rgba(0,30,15,0.75); padding:3px 10px; border-radius:5px; border:1px solid rgba(0,230,118,0.3); }
- .ai-detection-box { position:absolute; border:5px solid var(--red); background:rgba(255,59,59,0.1); border-radius:5px; animation:detBox 1.5s ease-in-out infinite; }
- @keyframes detBox { 0%,100%{border-color:#ff3b3b;box-shadow:0 0 20px rgba(255,59,59,0.4)} 50%{border-color:#fca5a5;box-shadow:0 0 45px rgba(255,59,59,0.7)} }
- .ai-detection-label { position:absolute; bottom:100%; left:0; white-space:nowrap; font-size:25px; background:#ff3b3b; color:#fff; padding:3px 10px; border-radius:5px; margin-bottom:5px; }
- /* ========== STATUS DOTS ========== */
- .status-dot { width:20px; height:20px; border-radius:50%; display:inline-block; animation:dotPulse 2s ease-in-out infinite; }
- .status-dot.green { background:var(--green); box-shadow:0 0 15px var(--green); }
- .status-dot.red { background:var(--red); box-shadow:0 0 15px var(--red); animation:blinkRed 0.8s ease-in-out infinite; }
- .status-dot.orange { background:#f59e0b; box-shadow:0 0 15px #f59e0b; }
- @keyframes dotPulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.3);opacity:0.7} }
- /* ========== SCAN EFFECT ========== */
- .scan-effect { position:absolute; inset:0; pointer-events:none; overflow:hidden; z-index:0; }
- .scan-effect::after {
- content:''; position:absolute; left:0; right:0; height:200px; top:-200px;
- background:linear-gradient(180deg,transparent,rgba(30,144,255,0.025),transparent);
- animation:scanFull 9s linear infinite;
- }
- @keyframes scanFull { from{top:-200px} to{top:100%} }
- /* ========== ALERT MODAL ========== */
- #alert-modal {
- position:fixed; inset:0; z-index:9999;
- display:none; align-items:center; justify-content:center;
- background:rgba(0,0,0,0.9); backdrop-filter:blur(6px);
- }
- #alert-modal.show { display:flex; animation:alertFadeIn 0.4s ease; }
- @keyframes alertFadeIn { from{opacity:0;transform:scale(0.88)} to{opacity:1;transform:scale(1)} }
- .alert-modal-inner {
- position:relative; width:2200px; border-radius:20px; overflow:hidden;
- background:linear-gradient(135deg,rgba(38,3,8,0.98),rgba(14,2,5,0.99));
- border:2px solid rgba(239,68,68,0.5);
- box-shadow:0 0 250px rgba(239,68,68,0.45),inset 0 0 150px rgba(200,0,0,0.08);
- }
- .alert-scan-line { position:absolute; left:0; right:0; height:5px; background:linear-gradient(90deg,transparent,rgba(239,68,68,0.9),transparent); animation:alertScan 1.5s linear infinite; top:0; z-index:2; }
- @keyframes alertScan { from{top:0} to{top:100%} }
- .alert-modal-header {
- padding:45px 60px 35px; background:linear-gradient(90deg,rgba(200,0,0,0.18),transparent);
- display:flex; align-items:center; gap:40px; border-bottom:2px solid rgba(239,68,68,0.25);
- }
- .alert-icon { font-size:85px; animation:alertFlash 0.6s ease-in-out infinite; }
- @keyframes alertFlash { 0%,100%{opacity:1} 50%{opacity:0.15} }
- .alert-title { font-size:55px; font-weight:700; color:#ef4444; letter-spacing:8px; }
- .alert-subtitle { font-size:28px; color:rgba(239,100,100,0.6); margin-top:8px; letter-spacing:5px; }
- /* body: flex row — left cam + right info */
- .alert-modal-body { padding:45px 60px; display:flex; gap:50px; align-items:flex-start; }
- /* LEFT: camera panel */
- .alert-cam-wrap {
- flex:0 0 900px;
- display:flex; flex-direction:column; gap:15px;
- }
- .alert-cam-label {
- font-size:28px; color:rgba(255,150,150,0.7); letter-spacing:5px;
- display:flex; align-items:center; gap:20px;
- }
- .alert-cam-label::before { content:''; width:15px; height:15px; border-radius:50%; background:#ef4444; box-shadow:0 0 20px #ef4444; animation:blinkRed 0.8s ease-in-out infinite; }
- .alert-cam-box {
- position:relative; width:900px; height:700px; border-radius:10px; overflow:hidden;
- border:2px solid rgba(239,68,68,0.5);
- box-shadow:0 0 50px rgba(239,68,68,0.25), inset 0 0 75px rgba(200,0,0,0.1);
- }
- .alert-cam-box canvas { display:block; width:100%; height:100%; }
- .alert-cam-overlay { position:absolute; inset:0; pointer-events:none; background:linear-gradient(180deg,rgba(0,0,0,0.35) 0%,transparent 25%,transparent 75%,rgba(0,0,0,0.45) 100%); }
- .alert-cam-rec { position:absolute; top:20px; right:25px; display:flex; align-items:center; gap:10px; font-size:28px; color:#ef4444; background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
- .alert-cam-rec::before { content:''; width:15px; height:15px; border-radius:50%; background:#ef4444; animation:blinkRed 1s ease-in-out infinite; }
- .alert-cam-id { position:absolute; top:20px; left:25px; font-size:28px; color:rgba(255,255,255,0.8); background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
- .alert-cam-bottom { position:absolute; bottom:20px; left:25px; right:25px; display:flex; justify-content:space-between; align-items:center; }
- .alert-cam-name { font-size:28px; color:rgba(255,255,255,0.8); background:rgba(0,0,0,0.6); padding:5px 18px; border-radius:5px; }
- .alert-cam-ai { font-size:25px; color:#00e676; background:rgba(0,30,15,0.75); padding:5px 18px; border-radius:5px; border:1px solid rgba(0,230,118,0.3); }
- .alert-det-box { position:absolute; border:5px solid #ef4444; background:rgba(239,68,68,0.1); border-radius:5px; animation:detBox 1.5s ease-in-out infinite; }
- .alert-det-label { position:absolute; bottom:100%; left:0; white-space:nowrap; font-size:25px; background:#ef4444; color:#fff; padding:5px 12px; border-radius:5px; margin-bottom:5px; }
- /* RIGHT: info */
- .alert-info-col { flex:1; display:flex; flex-direction:column; gap:35px; }
- .alert-info-grid { display:grid; grid-template-columns:1fr 1fr; gap:25px; }
- .alert-info-item { padding:25px 35px; border-radius:10px; background:rgba(239,68,68,0.07); border:1px solid rgba(239,68,68,0.2); }
- .alert-info-item .ail { font-size:28px; color:rgba(255,150,150,0.65); margin-bottom:10px; }
- .alert-info-item .aiv { font-size:40px; font-weight:700; color:#ef4444; }
- .alert-pulse-text { text-align:center; font-size:32px; letter-spacing:5px; color:rgba(239,100,100,0.75); animation:blinkRed 1s ease-in-out infinite; font-weight:600; padding:20px; border:1px solid rgba(239,68,68,0.2); border-radius:10px; background:rgba(239,68,68,0.05); }
- .alert-modal-footer { padding:30px 60px; display:flex; gap:30px; justify-content:flex-end; border-top:1px solid rgba(239,68,68,0.15); }
- .btn-alert-confirm { padding:22px 70px; border-radius:10px; cursor:pointer; border:2px solid #ef4444; background:rgba(239,68,68,0.15); color:#ef4444; font-size:32px; font-weight:600; transition:all 0.2s; }
- .btn-alert-confirm:hover { background:rgba(239,68,68,0.3); }
- .btn-alert-ignore { padding:22px 70px; border-radius:10px; cursor:pointer; border:1px solid var(--border); background:transparent; color:var(--text-dim); font-size:32px; transition:all 0.2s; }
- /* ========== SCROLLBAR ========== */
- ::-webkit-scrollbar { width:8px; height:8px; }
- ::-webkit-scrollbar-track { background:transparent; }
- ::-webkit-scrollbar-thumb { background:var(--border); border-radius:5px; }
- /* ========== CHART HEIGHTS ========== */
- #chart-donut { width:100%; height:100%; }
- #chart-stack { width:100%; height:100%; }
- #chart-line { width:100%; height:100%; }
- #chart-gauge { width:100%; height:100%; }
- #chart-ring { width:100%; height:100%; }
- #chart-pie { width:100%; height:100%; }
- </style>
- </head>
- <body>
- <canvas id="nebula-canvas"></canvas>
- <!-- ===== ALERT MODAL ===== -->
- <div id="alert-modal">
- <div class="alert-modal-inner">
- <div class="alert-scan-line"></div>
- <div class="alert-modal-header">
- <span class="alert-icon">🚨</span>
- <div>
- <div class="alert-title">⚡ 系统预警 · ALERT</div>
- <div class="alert-subtitle">LABORATORY SAFETY MONITORING SYSTEM · EMERGENCY</div>
- </div>
- </div>
- <div class="alert-modal-body">
- <!-- 左侧: 实验室实时监控画面 360×280 -->
- <div class="alert-cam-wrap">
- <div class="alert-cam-label">告警实验室实时监控画面</div>
- <div class="alert-cam-box">
- <canvas id="alert-cam-canvas" width="900" height="700"></canvas>
- <div class="alert-cam-overlay"></div>
- <div class="alert-cam-id">CAM · A301-02</div>
- <div class="alert-cam-rec">REC</div>
- <div class="alert-cam-bottom">
- <span class="alert-cam-name">化学分析实验室 A301</span>
- <span class="alert-cam-ai">🤖 AI检测</span>
- </div>
- <!-- AI危险行为框 -->
- <div class="alert-det-box" style="left:22%;top:15%;width:28%;height:42%">
- <div class="alert-det-label">危险行为: 未佩戴防护</div>
- </div>
- </div>
- </div>
- <!-- 右侧: 告警信息 -->
- <div class="alert-info-col">
- <div class="alert-info-grid">
- <div class="alert-info-item"><div class="ail">告警实验室</div><div class="aiv" id="alert-lab">化学分析实验室 (A301)</div></div>
- <div class="alert-info-item"><div class="ail">所属单位</div><div class="aiv" id="alert-unit">化学研究所</div></div>
- <div class="alert-info-item"><div class="ail">告警指标</div><div class="aiv" id="alert-metric">TVOC 浓度超标</div></div>
- <div class="alert-info-item"><div class="ail">当前值 / 安全阈值</div><div class="aiv" id="alert-value">2.85 / 0.6 mg/m³</div></div>
- </div>
- <div class="alert-pulse-text">▶▶ 请立即采取安全措施,疏散实验人员 ◀◀</div>
- </div>
- </div>
- <div class="alert-modal-footer">
- <button class="btn-alert-ignore" onclick="closeAlert()">稍后处理</button>
- <button class="btn-alert-confirm" onclick="closeAlert()">确认处理</button>
- </div>
- </div>
- </div>
- <!-- ===== MAIN WRAPPER ===== -->
- <div id="scale-root">
- <div class="bg-hex"></div>
- <div class="scan-effect"></div>
- <!-- TOP NAV -->
- <nav class="top-nav">
- <!-- 左侧:实时时钟 + 星期 -->
- <div class="nav-left" style="justify-content:flex-start">
- <div class="nav-clock">
- <div class="time" id="clock-time">--:--:--</div>
- <div class="date" id="clock-date">----年--月--日 星期-</div>
- </div>
- </div>
- <!-- 中部:LOGO + 单位名称 + 大标题 -->
- <div class="nav-center">
- <div class="nav-logo"><div class="nav-logo-ring"></div></div>
- <div class="nav-org" style="margin-right:50px">
- <strong>中国安全生产科学研究院</strong>
- <span>National Institute for Occupational Safety</span>
- </div>
- <div style="width:2px;height:80px;background:linear-gradient(180deg,transparent,rgba(30,144,255,0.6),transparent);flex-shrink:0;margin-right:50px"></div>
- <div style="display:flex;flex-direction:column;align-items:center;position:relative">
- <div class="nav-title">实验室安全智能监测与管控中心</div>
- <div class="nav-title-deco"></div>
- </div>
- </div>
- <!-- 右侧:天气 -->
- <div class="nav-right" style="justify-content:flex-end">
- <div class="nav-weather">
- <span class="icon">⛅</span>
- <div>
- <div>北京 · 晴转多云</div>
- <div style="font-size:28px;color:var(--gold)">12°C / AQI 68</div>
- </div>
- </div>
- </div>
- </nav>
- <!-- MAIN CONTENT: 4 COLS L→R: 1区(2) 2区(2) 3区(4) 4区(2) -->
- <div class="main-content">
- <!-- ======== 1区 ======== -->
- <div class="panel-col">
- <!-- 实验室基本情况统计 -->
- <div class="panel" style="flex:0 0 auto;display:flex;flex-direction:column">
- <div class="border-beam"></div>
- <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <div class="panel-header">
- <div class="panel-header-icon">🏛️</div>
- <span class="panel-title">实验室基本情况统计</span>
- <div class="status-dot green" style="margin-left:auto"></div>
- </div>
- <!-- 上部 80%:左(3) 总数 + 右(7) 分级 -->
- <div style="flex:4;display:flex;min-height:680px;overflow:hidden">
- <!-- 左侧 4:实验室总数统计 -->
- <div style="flex:4;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:25px 20px;border-right:1px solid var(--border);background:linear-gradient(180deg,rgba(30,144,255,0.04),transparent)">
- <!-- SVG 圆弧仪表 -->
- <svg width="320" height="320" viewBox="0 0 320 320">
- <defs>
- <linearGradient id="arcG1" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" stop-color="#1e90ff"/>
- <stop offset="100%" stop-color="#00d8ff"/>
- </linearGradient>
- <filter id="glow1">
- <feGaussianBlur stdDeviation="4" result="blur"/>
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
- </filter>
- </defs>
- <!-- 最外装饰环 -->
- <circle cx="160" cy="160" r="148" fill="none" stroke="rgba(30,144,255,0.08)" stroke-width="1" stroke-dasharray="6 6"/>
- <!-- 外轨道 -->
- <circle cx="160" cy="160" r="136" fill="none" stroke="rgba(30,144,255,0.14)" stroke-width="3"/>
- <!-- 主弧 300° -->
- <circle cx="160" cy="160" r="136" fill="none" stroke="url(#arcG1)" stroke-width="8"
- stroke-dasharray="711 143" stroke-linecap="round" transform="rotate(-240 160 160)" filter="url(#glow1)"/>
- <!-- 内轨道 -->
- <circle cx="160" cy="160" r="110" fill="rgba(3,14,31,0.7)" stroke="rgba(0,216,255,0.15)" stroke-width="2"/>
- <!-- 四分级色弧 小环 -->
- <circle cx="160" cy="160" r="88" fill="none" stroke="#cc0000" stroke-width="12" stroke-dasharray="42 485" stroke-linecap="round" transform="rotate(-90 160 160)" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#ff8000" stroke-width="12" stroke-dasharray="99 428" stroke-linecap="round" transform="rotate(-57 160 160)" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#ffcc00" stroke-width="12" stroke-dasharray="160 367" stroke-linecap="round" transform="rotate(22 160 160)" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#0066cc" stroke-width="12" stroke-dasharray="153 374" stroke-linecap="round" transform="rotate(145 160 160)" opacity="0.9"/>
- <!-- 端点光晕 -->
- <circle cx="160" cy="24" r="6" fill="#1e90ff" opacity="0.9"/>
- <!-- 中心数字 -->
- <text x="160" y="145" text-anchor="middle" fill="#ffd740" font-size="72" font-weight="900" font-family="Arial,sans-serif" letter-spacing="-2">128</text>
- <text x="160" y="190" text-anchor="middle" fill="rgba(168,204,232,0.75)" font-size="26" font-family="Arial,sans-serif" letter-spacing="4">间</text>
- </svg>
- <!-- 标签 -->
- <div style="text-align:center">
- <div style="font-size:30px;font-weight:700;color:var(--cyan);letter-spacing:4px">实验室总数</div>
- <div style="font-size:22px;color:var(--text-dim);margin-top:6px;letter-spacing:2px">TOTAL LABORATORIES</div>
- </div>
- <!-- 分级色条 -->
- <div style="width:100%;padding:0 10px">
- <div style="display:flex;gap:5px;height:12px;border-radius:6px;overflow:hidden">
- <div style="flex:12;background:#cc0000;box-shadow:0 0 10px rgba(204,0,0,0.5)"></div>
- <div style="flex:28;background:#ff8000;box-shadow:0 0 10px rgba(255,128,0,0.4)"></div>
- <div style="flex:45;background:#ffcc00;box-shadow:0 0 10px rgba(255,204,0,0.4)"></div>
- <div style="flex:43;background:#0066cc;box-shadow:0 0 10px rgba(0,102,204,0.4)"></div>
- </div>
- <div style="display:flex;gap:5px;margin-top:8px;font-size:22px;color:var(--text-dim);text-align:center">
- <span style="flex:12">Ⅰ</span><span style="flex:28">Ⅱ</span><span style="flex:45">Ⅲ</span><span style="flex:43">Ⅳ</span>
- </div>
- </div>
- </div>
- <!-- 右侧 6:分级统计(环形图 + 明细列表) -->
- <div style="flex:6;display:flex;flex-direction:column;justify-content:center;padding:25px 25px 20px;gap:0">
- <!-- 环形图 -->
- <div style="flex:0 0 340px;width:100%" id="chart-donut"></div>
- <!-- 分级明细 -->
- <div style="display:flex;flex-direction:column;gap:10px;margin-top:10px">
- <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(204,0,0,0.07);border-left:4px solid #cc0000">
- <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#cc0000;display:inline-block;box-shadow:0 0 8px rgba(204,0,0,0.8)"></span>I 级(危险)</span>
- <span style="color:#ff6666;font-weight:700;font-size:28px">12间</span>
- </div>
- <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(255,128,0,0.07);border-left:4px solid #ff8000">
- <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#ff8000;display:inline-block;box-shadow:0 0 8px rgba(255,128,0,0.8)"></span>II 级(较危险)</span>
- <span style="color:#ffaa44;font-weight:700;font-size:28px">28间</span>
- </div>
- <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(255,204,0,0.07);border-left:4px solid #ffcc00">
- <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#ffcc00;display:inline-block;box-shadow:0 0 8px rgba(255,204,0,0.8)"></span>III 级(一般)</span>
- <span style="color:#ffe066;font-weight:700;font-size:28px">45间</span>
- </div>
- <div style="display:flex;justify-content:space-between;align-items:center;font-size:26px;padding:10px 16px;border-radius:8px;background:rgba(0,102,204,0.07);border-left:4px solid #0066cc">
- <span style="display:flex;align-items:center;gap:12px"><span style="width:16px;height:16px;border-radius:3px;background:#0066cc;display:inline-block;box-shadow:0 0 8px rgba(0,102,204,0.8)"></span>IV 级(较安全)</span>
- <span style="color:#4499ff;font-weight:700;font-size:28px">43间</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 下部 20%:三种状态统计 -->
- <div style="flex:1;display:flex;align-items:center;padding:0 25px 20px;border-top:1px solid var(--border);gap:15px;min-height:170px">
- <div class="status-row" style="width:100%;margin:0">
- <div class="status-badge active"><div class="val">20</div><div class="lbl">使用(间)</div></div>
- <div class="status-badge warning"><div class="val">3</div><div class="lbl">异常(间)</div></div>
- <div class="status-badge idle"><div class="val">105</div><div class="lbl">空闲(间)</div></div>
- </div>
- </div>
- </div>
- <!-- 实验室安全分级统计 -->
- <div class="panel" style="flex:1;min-height:450px;display:flex;flex-direction:column">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">📊</div>
- <span class="panel-title">实验室安全分级统计</span>
- </div>
- <div style="flex:1;min-height:0;padding:15px 20px">
- <div id="chart-stack" style="height:100%;width:100%"></div>
- </div>
- </div>
- <!-- 实验室进入人数统计及走势 -->
- <div class="panel" style="flex:0 0 auto">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">👥</div>
- <span class="panel-title">实验室进入人数统计及走势</span>
- </div>
- <div style="padding:20px 25px">
- <div class="flip-counters">
- <div class="flip-counter"><div class="fc-label">今日进入总人数</div><div class="fc-digits" id="flip-total"></div></div>
- <div class="flip-counter"><div class="fc-label">当前在场实验人数</div><div class="fc-digits" id="flip-current"></div></div>
- </div>
- <div style="height:275px;margin-top:12px" id="chart-line"></div>
- </div>
- </div>
- </div><!-- /1区 -->
- <!-- ======== 2区 ======== -->
- <div class="panel-col">
- <!-- 智能环境感知应用设备统计 -->
- <div class="panel" style="flex:0 0 auto">
- <div class="border-beam"></div>
- <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <div class="panel-header">
- <div class="panel-header-icon">📡</div>
- <span class="panel-title">智能环境感知应用设备统计</span>
- </div>
- <div style="padding:20px 25px">
- <div class="device-online-row">
- <div class="device-stat-chip online"><div class="dv">312</div><div class="dl">在线设备</div></div>
- <div class="device-stat-chip offline"><div class="dv">18</div><div class="dl">离线设备</div></div>
- </div>
- <div style="display:flex;gap:20px;align-items:center">
- <div id="chart-gauge" style="flex:0 0 295px;height:295px"></div>
- <div class="device-list-row" style="flex:1">
- <div class="device-list-item"><div class="dli-icon">🏷️</div><div class="dli-num">86</div><div class="dli-name">电子信息铭牌</div></div>
- <div class="device-list-item"><div class="dli-icon">⚗️</div><div class="dli-num">44</div><div class="dli-name">化学品智能终端</div></div>
- <div class="device-list-item"><div class="dli-icon">🌡️</div><div class="dli-num">128</div><div class="dli-name">传感器套件</div></div>
- <div class="device-list-item"><div class="dli-icon">📷</div><div class="dli-num">72</div><div class="dli-name">智能摄像设备</div></div>
- </div>
- </div>
- </div>
- </div>
- <!-- 实验室设备分类及使用统计 (4:2:4) -->
- <div class="panel" style="flex:1;min-height:650px;display:flex;flex-direction:column">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">🔬</div>
- <span class="panel-title">实验室设备分类及使用统计</span>
- </div>
- <!-- 4:2:4 layout -->
- <div style="flex:1;display:flex;flex-direction:column;padding:15px 25px;gap:12px;min-height:0">
- <!-- 上: 占4 环形图 - 设备分类 -->
- <div style="flex:4;min-height:0">
- <div id="chart-ring" style="height:100%;width:100%"></div>
- </div>
- <!-- 中: 占2 统计数字 -->
- <div style="flex:2;min-height:0;display:flex;align-items:center">
- <div class="equip-mid-row" style="width:100%">
- <div class="equip-stat-item"><div class="ev">2,458</div><div class="el">设备总数(台)</div></div>
- <div class="equip-stat-item"><div class="ev">18,620</div><div class="el">使用时长(h)</div></div>
- <div class="equip-stat-item"><div class="ev">62.4%</div><div class="el">设备使用率</div></div>
- </div>
- </div>
- <!-- 下: 占4 饼图 - 使用状态 -->
- <div style="flex:4;min-height:0">
- <div id="chart-pie" style="height:100%;width:100%"></div>
- </div>
- </div>
- </div>
- </div><!-- /2区 -->
- <!-- ======== 3区: 实时监控 ======== -->
- <div class="center-col">
- <!-- 实时监控(常显) -->
- <div id="view-monitor">
- <div class="monitor-header">
- <span style="font-size:32px;font-weight:600;color:var(--cyan);letter-spacing:2px">📹 实时监控</span>
- <span style="font-size:28px;color:var(--text-dim);margin-left:10px">CCTV Live Feed</span>
- <div style="margin-left:auto;display:flex;align-items:center;gap:15px">
- <div class="status-dot green"></div>
- <span style="font-size:28px;color:var(--text-dim)">信号正常</span>
- </div>
- </div>
- <div class="monitor-inner">
- <div class="monitor-left">
- <div class="search-box"><span style="font-size:32px;color:var(--text-dim)">🔍</span><input type="text" placeholder="搜索楼栋 / 楼层 / 房间…"></div>
- <select class="filter-select">
- <option value="">全部二级单位</option>
- <option>化学研究所</option><option>物理研究所</option>
- <option>生物研究所</option><option>材料研究所</option><option>工程研究所</option>
- </select>
- <div class="tree-wrap" id="building-tree"></div>
- </div>
- <div class="monitor-right">
- <div class="camera-grid-header">
- <div class="camera-breadcrumb"><span>安科院主园区</span> › <span>综合实验楼A</span> › <span>3层</span></div>
- <div class="camera-pager">
- <button class="pager-btn">‹</button>
- <span class="pager-info">1 / 3 页</span>
- <button class="pager-btn">›</button>
- </div>
- </div>
- <div class="camera-grid" id="camera-grid"></div>
- </div>
- </div>
- </div>
- </div><!-- /3区 -->
- <!-- ======== 4区 ======== -->
- <div class="panel-col">
- <!-- 实验环境安全智能感知 -->
- <div class="panel" style="flex:7;min-height:0;display:flex;flex-direction:column">
- <div class="border-beam"></div>
- <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <div class="panel-header">
- <div class="panel-header-icon">🌡️</div>
- <span class="panel-title">实验环境安全智能感知</span>
- <div class="status-dot red" style="margin-left:auto"></div>
- </div>
- <div style="flex:1;min-height:0;padding:15px 20px;display:flex;flex-direction:column">
- <div class="sensor-scroll-wrap">
- <div class="sensor-scroll-inner" id="sensor-list"></div>
- </div>
- </div>
- </div>
- <!-- 实验室实时风险预警 -->
- <div class="panel" style="flex:3;min-height:0;display:flex;flex-direction:column">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">⚠️</div>
- <span class="panel-title">实验室实时风险预警</span>
- <div style="margin-left:auto;display:flex;align-items:center;gap:10px;flex-shrink:0">
- <span style="font-size:25px;color:var(--text-dim)">本月</span>
- <span style="font-size:50px;font-weight:700;color:#f59e0b">42</span>
- <span style="font-size:25px;color:var(--text-dim)">次</span>
- </div>
- </div>
- <div style="flex:1;min-height:0;padding:15px 20px;display:flex;flex-direction:column">
- <div class="warn-scroll-wrap">
- <div class="warn-scroll-inner" id="warn-list"></div>
- </div>
- </div>
- </div>
- </div><!-- /4区 -->
- </div><!-- /main-content -->
- </div><!-- /scale-root -->
- <script>
- // ====================================================
- // SCALE
- // ====================================================
- // ====================================================
- // INIT MONITOR ON LOAD
- // ====================================================
- document.addEventListener('DOMContentLoaded', () => {
- if (document.documentElement.requestFullscreen)
- document.documentElement.requestFullscreen().catch(() => {});
- buildTree();
- buildCameras();
- });
- const WD = ['日','一','二','三','四','五','六'];
- function tick() {
- const n = new Date();
- const t = `${String(n.getHours()).padStart(2,'0')}:${String(n.getMinutes()).padStart(2,'0')}:${String(n.getSeconds()).padStart(2,'0')}`;
- const d = `${n.getFullYear()}年${n.getMonth()+1}月${n.getDate()}日 星期${WD[n.getDay()]}`;
- document.getElementById('clock-time').textContent = t;
- document.getElementById('clock-date').textContent = d;
- }
- setInterval(tick, 1000); tick();
- // ====================================================
- // NEBULA BACKGROUND
- // ====================================================
- (function () {
- const c = document.getElementById('nebula-canvas');
- const ctx = c.getContext('2d');
- let W, H, stars = [], t = 0;
- function resize() { W = c.width = window.innerWidth; H = c.height = window.innerHeight; }
- window.addEventListener('resize', resize); resize();
- for (let i = 0; i < 500; i++)
- stars.push({ x: Math.random()*9600, y: Math.random()*2800, r: Math.random()*1.5+0.3, a: Math.random()*0.8+0.2, va: Math.random()*0.02-0.01 });
- function draw() {
- t++;
- ctx.clearRect(0, 0, W, H);
- const g1 = ctx.createRadialGradient(W*0.22, H*0.35, 40, W*0.22, H*0.35, 420);
- g1.addColorStop(0, 'rgba(0,60,180,0.09)'); g1.addColorStop(1, 'transparent');
- ctx.fillStyle = g1; ctx.fillRect(0,0,W,H);
- const g2 = ctx.createRadialGradient(W*0.78, H*0.65, 30, W*0.78, H*0.65, 360);
- g2.addColorStop(0, 'rgba(0,100,220,0.08)'); g2.addColorStop(1, 'transparent');
- ctx.fillStyle = g2; ctx.fillRect(0,0,W,H);
- stars.forEach(s => {
- s.a += s.va; if (s.a > 1 || s.a < 0.1) s.va *= -1;
- const hue = 200 + Math.sin(t * 0.01 + s.x) * 30;
- ctx.beginPath(); ctx.arc(s.x*(W/9600), s.y*(H/2800), s.r, 0, Math.PI*2);
- ctx.fillStyle = `hsla(${hue},80%,80%,${s.a})`; ctx.fill();
- });
- requestAnimationFrame(draw);
- }
- draw();
- })();
- // ====================================================
- // FLIP COUNTER
- // ====================================================
- function renderFlip(id, val) {
- const el = document.getElementById(id); if (!el) return;
- el.innerHTML = String(val).padStart(4,'0').split('').map(d => `<div class="flip-digit">${d}</div>`).join('');
- }
- function animFlip(id, from, to, dur) {
- const s = Date.now();
- (function tick() {
- const p = Math.min((Date.now()-s)/dur, 1);
- renderFlip(id, Math.round(from + (to-from)*p));
- if (p < 1) requestAnimationFrame(tick);
- })();
- }
- renderFlip('flip-total', 1284); renderFlip('flip-current', 47);
- setTimeout(() => animFlip('flip-total', 0, 1284, 2000), 600);
- setTimeout(() => animFlip('flip-current', 0, 47, 1500), 900);
- // ====================================================
- // SENSOR LIST (4区上)
- // ====================================================
- const LABS = [
- {name:'化学分析实验室', room:'A301', unit:'化学研究所', t:22.5, h:58, tvoc:0.82, co2:650, o2:20.9, alert:true},
- {name:'生物安全实验室', room:'B201', unit:'生物研究所', t:20.1, h:62, tvoc:0.18, co2:580, o2:20.8, alert:false},
- {name:'材料测试实验室', room:'C401', unit:'材料研究所', t:24.0, h:45, tvoc:0.09, co2:520, o2:20.9, alert:false},
- {name:'精密仪器实验室', room:'A205', unit:'物理研究所', t:21.5, h:50, tvoc:0.14, co2:490, o2:20.9, alert:false},
- {name:'有机合成实验室', room:'B302', unit:'化学研究所', t:23.8, h:55, tvoc:2.10, co2:725, o2:20.5, alert:true},
- {name:'光学检测实验室', room:'C203', unit:'物理研究所', t:22.0, h:52, tvoc:0.11, co2:510, o2:20.9, alert:false},
- {name:'高压实验室', room:'D101', unit:'工程研究所', t:25.5, h:48, tvoc:0.30, co2:600, o2:20.7, alert:false},
- {name:'低温实验室', room:'D205', unit:'物理研究所', t:18.0, h:40, tvoc:0.07, co2:480, o2:20.9, alert:false},
- {name:'核磁共振室', room:'A302', unit:'化学研究所', t:20.5, h:54, tvoc:0.05, co2:505, o2:20.9, alert:false},
- {name:'质谱分析室', room:'A303', unit:'化学研究所', t:21.0, h:53, tvoc:0.19, co2:530, o2:20.8, alert:false},
- ];
- function buildSensorList() {
- const el = document.getElementById('sensor-list'); if (!el) return;
- const all = [...LABS, ...LABS];
- el.innerHTML = all.map(lab => `
- <div class="sensor-item ${lab.alert ? 'alert' : ''}">
- <div class="sensor-item-head">
- <span class="sensor-name">${lab.name}(${lab.room})</span>
- <span class="sensor-unit">${lab.unit}</span>
- ${lab.alert
- ? '<span style="color:var(--red);font-size:25px;animation:blinkRed 0.8s infinite">🚨 告警</span>'
- : '<span style="color:var(--green);font-size:25px">● 正常</span>'}
- </div>
- <div class="sensor-metrics">
- <div class="sensor-metric">🌡️ ${lab.t}°C</div>
- <div class="sensor-metric">💧 ${lab.h}%</div>
- <div class="sensor-metric ${lab.tvoc > 0.6 ? 'alarm' : ''}">🧪 TVOC ${lab.tvoc}</div>
- <div class="sensor-metric ${lab.co2 > 700 ? 'alarm' : ''}">💨 CO₂ ${lab.co2}</div>
- <div class="sensor-metric">🫧 O₂ ${lab.o2}%</div>
- </div>
- </div>`).join('');
- }
- buildSensorList();
- // ====================================================
- // WARNING LIST (4区下)
- // ====================================================
- const WARNS = [
- {lab:'化学分析实验室', room:'A301', unit:'化学研究所', metric:'TVOC浓度超标', val:'2.85 mg/m³', time:'2026-03-05 14:32:18'},
- {lab:'有机合成实验室', room:'B302', unit:'化学研究所', metric:'CO₂浓度偏高', val:'725 ppm', time:'2026-03-05 13:58:44'},
- {lab:'高压实验室', room:'D101', unit:'工程研究所', metric:'温度异常', val:'38.5 °C', time:'2026-03-05 12:15:07'},
- {lab:'生物安全实验室', room:'B201', unit:'生物研究所', metric:'湿度超标', val:'88% RH', time:'2026-03-05 11:40:22'},
- {lab:'材料测试实验室', room:'C401', unit:'材料研究所', metric:'TVOC超标', val:'1.2 mg/m³', time:'2026-03-05 10:08:55'},
- {lab:'光学检测实验室', room:'C203', unit:'物理研究所', metric:'O₂浓度偏低', val:'19.2 %', time:'2026-03-05 09:22:33'},
- {lab:'精密仪器实验室', room:'A205', unit:'物理研究所', metric:'温度超标', val:'28.5 °C', time:'2026-03-05 08:45:11'},
- {lab:'低温实验室', room:'D205', unit:'物理研究所', metric:'气压异常', val:'85 kPa', time:'2026-03-04 23:12:40'},
- ];
- function buildWarnList() {
- const el = document.getElementById('warn-list'); if (!el) return;
- const all = [...WARNS, ...WARNS];
- el.innerHTML = all.map(w => `
- <div class="warn-item">
- <div class="warn-item-head">
- <span class="warn-lab">🚨 ${w.lab}(${w.room})- ${w.unit}</span>
- <span class="warn-time">${w.time}</span>
- </div>
- <div class="warn-detail">异常指标:<span class="warn-metric-val">${w.metric} ${w.val}</span></div>
- </div>`).join('');
- }
- buildWarnList();
- // ====================================================
- // ALERT MODAL + CAMERA
- // ====================================================
- function closeAlert() { document.getElementById('alert-modal').classList.remove('show'); }
- let alertCamRAF = null;
- function startAlertCam() {
- const canvas = document.getElementById('alert-cam-canvas');
- if (!canvas || canvas._alertRunning) return;
- canvas._alertRunning = true;
- const ctx = canvas.getContext('2d');
- const W = 900, H = 700;
- let t = Math.random() * 1000;
- const noise = Array.from({length:300}, () => ({x:Math.random(), y:Math.random(), s:Math.random()*2.5+0.5, a:Math.random()*0.25}));
- function frame() {
- t += 0.4;
- ctx.clearRect(0, 0, W, H);
- // Background gradient (dark green-tinted night-vision feel with red emergency tint)
- const bg = ctx.createLinearGradient(0, 0, 0, H);
- bg.addColorStop(0, 'hsl(210,45%,5%)');
- bg.addColorStop(1, 'hsl(0,30%,4%)');
- ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
- // Perspective floor grid
- ctx.strokeStyle = 'rgba(20,65,140,0.07)'; ctx.lineWidth = 0.5;
- const vp = {x: W/2, y: H*0.44};
- for (let gx = 0; gx <= 12; gx++) { const fx = W*gx/12; ctx.beginPath(); ctx.moveTo(fx,H); ctx.lineTo(vp.x+(fx-W/2)*0.28,vp.y); ctx.stroke(); }
- for (let gy = 0; gy <= 6; gy++) { const fy = H*0.44+(H*0.56)*(gy/6); const wd = W*(0.25+0.75*(gy/6)); ctx.beginPath(); ctx.moveTo((W-wd)/2,fy); ctx.lineTo((W+wd)/2,fy); ctx.stroke(); }
- // Lab furniture silhouettes
- ctx.fillStyle = 'rgba(0,30,80,0.45)';
- ctx.fillRect(W*0.05, H*0.52, W*0.38, H*0.12); // bench left
- ctx.fillRect(W*0.57, H*0.52, W*0.38, H*0.12); // bench right
- ctx.fillStyle = 'rgba(0,18,50,0.55)';
- ctx.fillRect(W*0.08, H*0.35, W*0.09, H*0.17); // cabinet
- ctx.fillRect(W*0.60, H*0.33, W*0.12, H*0.19); // equipment
- // Person silhouette (lab worker)
- const px = W*(0.4 + Math.sin(t*0.008)*0.03);
- const py = H*(0.58 + Math.cos(t*0.006)*0.015);
- ctx.fillStyle = 'rgba(110,160,240,0.6)';
- ctx.beginPath(); ctx.arc(px, py - H*0.1, W*0.028, 0, Math.PI*2); ctx.fill(); // head
- ctx.fillRect(px - W*0.022, py - H*0.08, W*0.044, H*0.1); // body
- // Red emergency scan line
- const scanY = (t * 0.8) % H;
- const scanG = ctx.createLinearGradient(0, scanY-8, 0, scanY+8);
- scanG.addColorStop(0, 'transparent');
- scanG.addColorStop(0.5, 'rgba(239,68,68,0.18)');
- scanG.addColorStop(1, 'transparent');
- ctx.fillStyle = scanG; ctx.fillRect(0, scanY-8, W, 16);
- // Noise
- noise.forEach(n => {
- ctx.fillStyle = `rgba(200,210,240,${n.a*0.45})`;
- ctx.fillRect(n.x*W, n.y*H, n.s, n.s);
- if (Math.random() < 0.015) { n.x = Math.random(); n.y = Math.random(); }
- });
- // Red vignette pulse (emergency)
- const vig = ctx.createRadialGradient(W/2, H/2, H*0.25, W/2, H/2, H*0.85);
- const vigAlpha = 0.12 + Math.sin(t*0.08)*0.06;
- vig.addColorStop(0, 'transparent');
- vig.addColorStop(1, `rgba(200,0,0,${vigAlpha})`);
- ctx.fillStyle = vig; ctx.fillRect(0, 0, W, H);
- alertCamRAF = requestAnimationFrame(frame);
- }
- frame();
- }
- setTimeout(() => {
- document.getElementById('alert-modal').classList.add('show');
- startAlertCam();
- }, 5000);
- // ====================================================
- // ECHARTS
- // ====================================================
- function initCharts() {
- const TOOLTIP_CFG = {
- backgroundColor:'rgba(3,14,42,0.92)',
- borderColor:'rgba(30,144,255,0.3)',
- textStyle:{ color:'#a8cce8', fontSize:28 }
- };
- // --- 环形图: 实验室总数分级 ---
- const dEl = document.getElementById('chart-donut');
- if (dEl) {
- const c = echarts.init(dEl, null, {renderer:'canvas', devicePixelRatio:2});
- c.setOption({
- backgroundColor:'transparent',
- tooltip:{ trigger:'item', formatter:'{b}: {c}间 ({d}%)', ...TOOLTIP_CFG },
- graphic:[{
- type:'text', left:'center', top:'40%',
- style:{ text:'128', fill:'#ffd740', font:'bold 80px Arial', textAlign:'center' }
- },{
- type:'text', left:'center', top:'58%',
- style:{ text:'实验室总数(间)', fill:'rgba(168,204,232,0.65)', font:'22px Arial', textAlign:'center' }
- }],
- series:[{
- type:'pie', radius:['42%','68%'], center:['50%','50%'], startAngle:100,
- itemStyle:{ borderRadius:6, borderColor:'rgba(3,14,31,0.6)', borderWidth:3 },
- label:{
- show:true,
- formatter: params => `{lvl|${params.name}}\n{cnt|${params.value}间}\n{pct|${params.percent}%}`,
- rich:{
- lvl:{ fontSize:24, fontWeight:700, color:'#ddf0ff', lineHeight:36 },
- cnt:{ fontSize:30, fontWeight:900, color:'#ffd740', lineHeight:42 },
- pct:{ fontSize:22, color:'rgba(168,204,232,0.6)', lineHeight:32 }
- },
- distanceToLabelLine:8
- },
- labelLine:{
- show:true, length:20, length2:25,
- lineStyle:{ color:'rgba(30,144,255,0.45)', width:2 }
- },
- data:[
- {value:12, name:'I级', itemStyle:{color:'#cc0000', shadowBlur:12, shadowColor:'rgba(204,0,0,0.5)'}},
- {value:28, name:'II级', itemStyle:{color:'#ff8000', shadowBlur:12, shadowColor:'rgba(255,128,0,0.5)'}},
- {value:45, name:'III级', itemStyle:{color:'#ffcc00', shadowBlur:12, shadowColor:'rgba(255,204,0,0.5)'}},
- {value:43, name:'IV级', itemStyle:{color:'#0066cc', shadowBlur:12, shadowColor:'rgba(0,102,204,0.5)'}}
- ],
- emphasis:{
- scale:true, scaleSize:8,
- itemStyle:{ shadowBlur:30, shadowColor:'rgba(30,144,255,0.6)' },
- label:{ fontSize:28 }
- }
- }]
- });
- }
- // --- 堆叠柱状图: 安全分级统计 ---
- const stEl = document.getElementById('chart-stack');
- if (stEl) {
- const c = echarts.init(stEl, null, {renderer:'canvas', devicePixelRatio:2});
- const cols = ['#cc0000','#ff8000','#ffcc00','#0066cc'];
- const lvls = ['I级','II级','III级','IV级'];
- const units = ['化学所','生物所','材料所','物理所','工程所','核能所','信息所','环境所','计量所','医学所'];
- const totals = [28,22,18,24,16,12,20,15,10,14];
- const dat = [
- [2,3,5,4,2,1,3,2,1,2],
- [5,6,4,7,4,3,5,4,3,4],
- [9,8,6,8,5,5,7,5,4,5],
- [12,5,3,5,5,3,5,4,2,3]
- ];
- const xL = units.map((u,i) => `${u}\n(${totals[i]})`);
- c.setOption({
- backgroundColor:'transparent',
- tooltip:{ trigger:'axis', axisPointer:{type:'shadow'}, ...TOOLTIP_CFG },
- legend:{ data:lvls, top:0, right:0, textStyle:{color:'#a8cce8',fontSize:25}, icon:'rect', itemWidth:25, itemHeight:15 },
- grid:{ left:30, right:8, top:55, bottom:110, containLabel:true },
- xAxis:{ type:'category', data:xL, axisLabel:{color:'#5890b8',fontSize:22,interval:0,lineHeight:35}, axisLine:{lineStyle:{color:'rgba(30,144,255,0.2)'}}, axisTick:{show:false} },
- yAxis:{ type:'value', name:'间', nameTextStyle:{color:'#5890b8',fontSize:25}, axisLabel:{color:'#5890b8',fontSize:25}, axisLine:{show:false}, splitLine:{lineStyle:{color:'rgba(30,144,255,0.1)'}} },
- dataZoom:[{ type:'inside', startValue:0, endValue:5 }],
- series:lvls.map((n,i) => ({
- name:n, type:'bar', stack:'total', barMaxWidth:55,
- data:dat[i].map(v => ({value:v, itemStyle:{color:cols[i], opacity:0.88}})),
- emphasis:{ itemStyle:{opacity:1} }
- }))
- });
- let idx = 0;
- setInterval(() => {
- idx++;
- c.dispatchAction({ type:'dataZoom', startValue:idx, endValue:idx+5 });
- if (idx >= units.length-5) idx = -1;
- }, 5000);
- }
- // --- 折线图: 进入人数走势 ---
- const lnEl = document.getElementById('chart-line');
- if (lnEl) {
- const c = echarts.init(lnEl, null, {renderer:'canvas', devicePixelRatio:2});
- c.setOption({
- backgroundColor:'transparent',
- tooltip:{ trigger:'axis', ...TOOLTIP_CFG },
- legend:{ data:['进入人数','在场人数'], top:0, right:0, textStyle:{color:'#a8cce8',fontSize:25}, icon:'circle', itemWidth:20, itemHeight:20 },
- grid:{ left:28, right:8, top:45, bottom:50, containLabel:true },
- xAxis:{ type:'category', data:['00:00','03:00','06:00','09:00','12:00','15:00','18:00','21:00','24:00'], axisLabel:{color:'#5890b8',fontSize:22}, axisLine:{lineStyle:{color:'rgba(30,144,255,0.2)'}}, axisTick:{show:false} },
- yAxis:{ type:'value', axisLabel:{color:'#5890b8',fontSize:22}, axisLine:{show:false}, splitLine:{lineStyle:{color:'rgba(30,144,255,0.1)'}} },
- series:[
- { name:'进入人数', type:'line', data:[0,0,2,86,128,145,160,90,30], smooth:true, symbol:'circle', symbolSize:12, lineStyle:{color:'#1e90ff',width:5}, itemStyle:{color:'#1e90ff'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(30,144,255,0.32)'},{offset:1,color:'rgba(30,144,255,0.02)'}])} },
- { name:'在场人数', type:'line', data:[0,0,2,62,98,108,120,72,20], smooth:true, symbol:'circle', symbolSize:12, lineStyle:{color:'#ffd740',width:5}, itemStyle:{color:'#ffd740'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(255,215,64,0.22)'},{offset:1,color:'rgba(255,215,64,0.01)'}])} }
- ]
- });
- }
- // --- 仪表盘: 设备在线率 ---
- const ggEl = document.getElementById('chart-gauge');
- if (ggEl) {
- const c = echarts.init(ggEl, null, {renderer:'canvas', devicePixelRatio:2});
- c.setOption({
- backgroundColor:'transparent',
- series:[{
- type:'gauge', radius:'90%', center:['50%','60%'],
- startAngle:210, endAngle:-30, min:0, max:100, splitNumber:5,
- axisLine:{ lineStyle:{ width:30, color:[[0.3,'#ef4444'],[0.6,'#f59e0b'],[1,'#1e90ff']] } },
- axisTick:{show:false}, splitLine:{show:false}, axisLabel:{show:false},
- pointer:{ icon:'path://M12.8,0.7l12.3,0 M0,0 l9.2,12.4 M0,0 l2.2,-1.6', offsetCenter:[0,'-60%'], width:8, length:'60%', itemStyle:{color:'#1e90ff'} },
- detail:{ valueAnimation:true, formatter:'{value}%', color:'#ffd740', fontSize:45, fontWeight:700, offsetCenter:[0,'30%'] },
- title:{ show:true, offsetCenter:[0,'62%'], color:'rgba(110,165,210,0.6)', fontSize:25 },
- data:[{value:94.5, name:'在线率'}]
- }]
- });
- }
- // --- 环形图: 设备分类 (2区设备统计上部) ---
- const rgEl = document.getElementById('chart-ring');
- if (rgEl) {
- const c = echarts.init(rgEl, null, {renderer:'canvas', devicePixelRatio:2});
- const ringData = [
- {value:680, name:'检测设备', itemStyle:{color:'#1e90ff', shadowBlur:8, shadowColor:'rgba(30,144,255,0.5)'}},
- {value:520, name:'分析仪器', itemStyle:{color:'#4361ee', shadowBlur:8, shadowColor:'rgba(67,97,238,0.5)'}},
- {value:380, name:'制备设备', itemStyle:{color:'#00e676', shadowBlur:8, shadowColor:'rgba(0,230,118,0.5)'}},
- {value:280, name:'安全设备', itemStyle:{color:'#ffd740', shadowBlur:8, shadowColor:'rgba(255,215,64,0.5)'}},
- {value:240, name:'辅助设备', itemStyle:{color:'#00e5c8', shadowBlur:8, shadowColor:'rgba(0,229,200,0.5)'}},
- {value:358, name:'其他', itemStyle:{color:'#f97316', shadowBlur:8, shadowColor:'rgba(249,115,22,0.5)'}}
- ];
- const ringCountMap = {检测设备:680, 分析仪器:520, 制备设备:380, 安全设备:280, 辅助设备:240, 其他:358};
- c.setOption({
- backgroundColor:'transparent',
- tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
- legend:{
- orient:'vertical', right:'1%', top:'middle',
- icon:'circle', itemWidth:28, itemHeight:28, itemGap:22,
- textStyle:{ color:'#a8cce8', fontSize:22 },
- formatter: name => `{nm|${name}} {vl|${ringCountMap[name]}台}`,
- rich:{
- nm:{ fontSize:22, color:'#a8cce8', width:90 },
- vl:{ fontSize:26, fontWeight:700, color:'#fff' }
- }
- },
- series:[{
- type:'pie', radius:['38%','62%'], center:['36%','50%'],
- itemStyle:{ borderRadius:4, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
- label:{ show:true, formatter:'{c}台', fontSize:20, color:'#a8cce8' },
- labelLine:{ length:10, length2:10, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
- data: ringData,
- emphasis:{ scale:true, scaleSize:5, itemStyle:{ shadowBlur:20, shadowColor:'rgba(30,144,255,0.6)' } }
- }]
- });
- }
- // --- 饼图: 设备使用状态 (2区设备统计下部) ---
- const ppEl = document.getElementById('chart-pie');
- if (ppEl) {
- const c = echarts.init(ppEl, null, {renderer:'canvas', devicePixelRatio:2});
- const pieData = [
- {value:486, name:'使用', itemStyle:{color:'#1e90ff', shadowBlur:10, shadowColor:'rgba(30,144,255,0.5)'}},
- {value:1840, name:'空闲', itemStyle:{color:'#00e676', shadowBlur:10, shadowColor:'rgba(0,230,118,0.5)'}},
- {value:34, name:'维修', itemStyle:{color:'#f59e0b', shadowBlur:10, shadowColor:'rgba(245,158,11,0.5)'}}
- ];
- const countMap = {使用:486, 空闲:1840, 维修:34};
- c.setOption({
- backgroundColor:'transparent',
- tooltip:{ trigger:'item', formatter:'{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
- legend:{
- show:true, orient:'vertical', right:'2%', top:'middle',
- icon:'circle', itemWidth:36, itemHeight:36, itemGap:40,
- textStyle:{ color:'#a8cce8', fontSize:28 },
- formatter: name => `{nm|${name}} {vl|${countMap[name]}台}`,
- rich:{
- nm:{ fontSize:28, color:'#a8cce8', width:90 },
- vl:{ fontSize:34, fontWeight:700, color:'#fff' }
- }
- },
- series:[{
- type:'pie', radius:['36%','62%'], center:['38%','52%'],
- itemStyle:{ borderRadius:5, borderColor:'rgba(3,14,31,0.5)', borderWidth:2 },
- label:{
- show:true,
- formatter: '{b}\n{c}台',
- fontSize:24, color:'#a8cce8', lineHeight:36,
- distanceToLabelLine:6
- },
- labelLine:{ length:18, length2:14, lineStyle:{color:'rgba(30,144,255,0.4)', width:2} },
- data: pieData,
- emphasis:{
- scale:true, scaleSize:6,
- itemStyle:{ shadowBlur:25, shadowColor:'rgba(30,144,255,0.6)' }
- }
- }]
- });
- }
- }
- initCharts();
- // ====================================================
- // MONITOR TREE
- // ====================================================
- function buildTree() {
- const el = document.getElementById('building-tree'); if (!el) return;
- const data = {label:'安科院主园区', children:[
- {label:'综合实验楼A', children:[
- {label:'1层', children:[{label:'A101 化学实验室'},{label:'A102 分析室'}]},
- {label:'2层', children:[{label:'A201 生物实验室'}]},
- {label:'3层', children:[{label:'A301 有机合成室'},{label:'A302 核磁共振室'},{label:'A303 质谱室'}]},
- {label:'4层', children:[{label:'A401 X射线室'}]}
- ]},
- {label:'物理实验楼B', children:[
- {label:'1层', children:[{label:'B101 光学实验室'}]},
- {label:'2层', children:[{label:'B201 低温实验室'}]}
- ]},
- {label:'工程技术楼C', children:[
- {label:'1层', children:[{label:'C101 机械加工室'}]},
- {label:'2层', children:[{label:'C201 材料测试室'}]}
- ]}
- ]};
- el.innerHTML = renderT(data, 0);
- el.addEventListener('click', e => {
- const l = e.target.closest('.tree-node-label'); if (!l) return;
- l.classList.toggle('selected');
- const ch = l.parentElement.querySelector('.tree-children');
- const ar = l.querySelector('.arrow');
- if (ch) ch.classList.toggle('open');
- if (ar) ar.classList.toggle('open');
- });
- }
- function renderT(node, depth) {
- const isLeaf = !node.children || !node.children.length;
- const icon = ['🏢','🏗️','📐','🔬'][Math.min(depth,3)];
- let h = `<div class="tree-node"><div class="tree-node-label" style="padding-left:${depth*12+8}px">
- <span class="arrow ${depth===0?'open':''}">${isLeaf ? ' ' : '▶'}</span>
- <span>${icon}</span><span>${node.label}</span></div>`;
- if (!isLeaf) {
- h += `<div class="tree-children ${depth===0?'open':''}">`;
- node.children.forEach(c => h += renderT(c, depth+1));
- h += '</div>';
- }
- return h + '</div>';
- }
- // ====================================================
- // CAMERAS
- // ====================================================
- const CAMS = ['A301 有机合成室','A302 核磁共振室','A303 质谱室','A301 走廊','A302 走廊','A303 走廊','A层公共区域','A层安全通道','A层出入口'];
- function buildCameras() {
- const g = document.getElementById('camera-grid'); if (!g) return;
- g.innerHTML = '';
- for (let i = 0; i < 9; i++) {
- const cell = document.createElement('div');
- cell.className = 'camera-cell' + (i===0 ? ' ai-cam' : '');
- const cv = document.createElement('canvas');
- cv.style.cssText = 'width:100%;height:100%;display:block';
- cell.appendChild(cv);
- cell.innerHTML += `<div class="camera-overlay"></div><div class="camera-label">${CAMS[i]}</div><div class="camera-rec">REC</div>${i===0?'<div class="camera-ai-badge">🤖 AI检测</div>':''}`;
- if (i===0) {
- const box = document.createElement('div');
- box.className = 'ai-detection-box';
- box.style.cssText = 'left:28%;top:18%;width:22%;height:38%';
- box.innerHTML = '<div class="ai-detection-label">危险行为: 未佩戴防护</div>';
- cell.appendChild(box);
- }
- g.appendChild(cell);
- setTimeout(() => drawFakeCam(cv, i), 150);
- }
- }
- function drawFakeCam(canvas, idx) {
- const ctx = canvas.getContext('2d');
- const par = canvas.parentElement;
- let pw, ph;
- function sz() { pw = canvas.width = par.offsetWidth; ph = canvas.height = par.offsetHeight; }
- sz(); window.addEventListener('resize', sz);
- let t = Math.random() * 1000;
- const noise = Array.from({length:200}, () => ({x:Math.random(),y:Math.random(),s:Math.random()*3,a:Math.random()*0.3}));
- function frame() {
- t += 0.5; if (!pw || !ph) sz();
- ctx.clearRect(0,0,pw,ph);
- const bg = ctx.createLinearGradient(0,0,0,ph);
- bg.addColorStop(0,'hsl(210,42%,5%)'); bg.addColorStop(1,'hsl(220,30%,3%)');
- ctx.fillStyle = bg; ctx.fillRect(0,0,pw,ph);
- ctx.strokeStyle = 'rgba(20,70,150,0.06)'; ctx.lineWidth = 0.5;
- const vp = {x:pw/2, y:ph*0.45};
- for (let gx=0;gx<=10;gx++) { const fx=pw*gx/10; ctx.beginPath();ctx.moveTo(fx,ph);ctx.lineTo(vp.x+(fx-pw/2)*0.3,vp.y);ctx.stroke(); }
- for (let gy=0;gy<=5;gy++) { const fy=ph*0.45+(ph*0.55)*(gy/5); const wd=pw*(0.3+0.7*(gy/5)); ctx.beginPath();ctx.moveTo((pw-wd)/2,fy);ctx.lineTo((pw+wd)/2,fy);ctx.stroke(); }
- ctx.fillStyle='rgba(0,35,90,0.4)'; ctx.fillRect(pw*0.1,ph*0.55,pw*0.35,ph*0.1); ctx.fillRect(pw*0.55,ph*0.55,pw*0.35,ph*0.1);
- ctx.fillStyle='rgba(0,18,55,0.5)'; ctx.fillRect(pw*0.12,ph*0.4,pw*0.08,ph*0.15); ctx.fillRect(pw*0.58,ph*0.38,pw*0.1,ph*0.17);
- const np = [2,1,3,1,2,1,0,1,2][idx]||1;
- for (let p=0;p<np;p++) {
- const px=pw*(0.3+p*0.15+Math.sin(t*0.01+p)*0.02);
- const py=ph*(0.6+Math.cos(t*0.008+p)*0.02);
- ctx.fillStyle='rgba(120,170,255,0.65)'; ctx.beginPath(); ctx.arc(px,py,pw*0.025,0,Math.PI*2); ctx.fill();
- ctx.fillStyle='rgba(70,130,220,0.4)'; ctx.fillRect(px-pw*0.02,py+pw*0.025,pw*0.04,ph*0.08);
- }
- const sl = (t*0.5) % ph; ctx.fillStyle='rgba(30,144,255,0.04)'; ctx.fillRect(0,sl,pw,2);
- noise.forEach(n => {
- ctx.fillStyle=`rgba(170,205,255,${n.a*0.5})`; ctx.fillRect(n.x*pw,n.y*ph,n.s,n.s);
- if (Math.random()<0.02) { n.x=Math.random(); n.y=Math.random(); }
- });
- requestAnimationFrame(frame);
- }
- frame();
- }
- // ====================================================
- // RESIZE CHARTS
- // ====================================================
- window.addEventListener('resize', () => {
- ['chart-donut','chart-stack','chart-line','chart-gauge','chart-ring','chart-pie'].forEach(id => {
- const el = document.getElementById(id);
- if (el) { const c = echarts.getInstanceByDom(el); if (c) c.resize(); }
- });
- });
- </script>
- </body>
- </html>
|