device-monitor.html 103 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363
  1. <!DOCTYPE html>
  2. <html lang="zh-CN" class="iframe-content">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>设备监控汇总 - 爱智农</title>
  7. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css">
  8. <link rel="stylesheet" href="https://at.alicdn.com/t/font_3114978_qe0b39no76.css">
  9. <link rel="stylesheet" href="../assets/css/global.css">
  10. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
  11. <script src="https://cdn.jsdelivr.net/npm/dayjs@1.10.7/dayjs.min.js"></script>
  12. <script src="https://cdn.jsdelivr.net/npm/dayjs@1.10.7/locale/zh-cn.js"></script>
  13. <style>
  14. :root {
  15. --primary: #0ea5e9;
  16. --primary-dark: #0369a1;
  17. --primary-light: #7dd3fc;
  18. --bg-dark: #0f172a;
  19. --bg-card: #1e293b;
  20. --text-primary: #f1f5f9;
  21. --text-secondary: #94a3b8;
  22. --success: #10b981;
  23. --warning: #f59e0b;
  24. --danger: #ef4444;
  25. --info: #3b82f6;
  26. --border: #334155;
  27. --radius: 8px;
  28. }
  29. body {
  30. font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
  31. background-color: var(--bg-dark);
  32. color: var(--text-primary);
  33. margin: 0;
  34. padding: 0;
  35. min-height: 100vh;
  36. overflow-x: hidden;
  37. }
  38. .page-container {
  39. padding: 60px 20px 20px;
  40. }
  41. /* 卡片组件 */
  42. .dashboard-card {
  43. background-color: var(--bg-card);
  44. border-radius: var(--radius);
  45. border: 1px solid var(--border);
  46. transition: all 0.3s ease;
  47. }
  48. .dashboard-card:hover {
  49. transform: translateY(-2px);
  50. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
  51. border-color: var(--primary);
  52. }
  53. /* 告警状态卡片 */
  54. .dashboard-card.alert {
  55. border: 1px solid var(--danger);
  56. background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
  57. }
  58. /* 数据更新闪烁效果 */
  59. .data-update {
  60. transition: all 0.3s ease;
  61. }
  62. .data-update.flash {
  63. animation: flash 0.5s ease;
  64. }
  65. /* 视频监控卡片 */
  66. .video-card {
  67. position: relative;
  68. background: var(--bg-dark);
  69. border-radius: var(--radius);
  70. overflow: hidden;
  71. border: 1px solid var(--border);
  72. transition: all 0.3s ease;
  73. }
  74. .video-preview {
  75. position: relative;
  76. padding-top: 75%; /* 4:3 aspect ratio */
  77. background: var(--bg-dark);
  78. }
  79. .video-preview > div {
  80. position: absolute;
  81. top: 0;
  82. left: 0;
  83. width: 100%;
  84. height: 100%;
  85. display: flex;
  86. align-items: center;
  87. justify-content: center;
  88. }
  89. .video-preview .camera-icon {
  90. width: 48px;
  91. height: 48px;
  92. color: var(--text-secondary);
  93. }
  94. /* 摄像头全屏按钮样式 */
  95. .camera-fullscreen-btn {
  96. position: absolute;
  97. top: 12px;
  98. right: 12px;
  99. width: 32px;
  100. height: 32px;
  101. display: flex;
  102. align-items: center;
  103. justify-content: center;
  104. background: rgba(0, 0, 0, 0.5);
  105. border: 1px solid rgba(255, 255, 255, 0.1);
  106. border-radius: 4px;
  107. color: var(--text-secondary);
  108. transition: all 0.2s ease;
  109. z-index: 10;
  110. }
  111. .camera-fullscreen-btn:hover {
  112. background: rgba(0, 0, 0, 0.7);
  113. color: var(--text-primary);
  114. border-color: var(--primary);
  115. }
  116. /* 状态标签 */
  117. .status-badge {
  118. font-size: 12px;
  119. padding: 2px 8px;
  120. border-radius: 12px;
  121. font-weight: 500;
  122. }
  123. .status-badge.online {
  124. background-color: rgba(16, 185, 129, 0.1);
  125. color: #10b981;
  126. }
  127. .status-badge.offline {
  128. background-color: rgba(239, 68, 68, 0.2);
  129. color: var(--danger);
  130. }
  131. .status-badge.warning {
  132. background-color: rgba(245, 158, 11, 0.1);
  133. color: #f59e0b;
  134. }
  135. /* 动画效果 */
  136. @keyframes pulse {
  137. 0% {
  138. box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
  139. }
  140. 70% {
  141. box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
  142. }
  143. 100% {
  144. box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
  145. }
  146. }
  147. @keyframes flash {
  148. 0% {
  149. opacity: 1;
  150. }
  151. 50% {
  152. opacity: 0.5;
  153. }
  154. 100% {
  155. opacity: 1;
  156. }
  157. }
  158. /* 下拉选择器样式 */
  159. select {
  160. background-color: var(--bg-card);
  161. border: 1px solid var(--border);
  162. color: var(--text-primary);
  163. padding: 8px 12px;
  164. border-radius: var(--radius);
  165. appearance: none;
  166. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
  167. background-repeat: no-repeat;
  168. background-position: right 8px center;
  169. background-size: 16px;
  170. padding-right: 32px;
  171. }
  172. select:focus {
  173. outline: none;
  174. border-color: var(--primary);
  175. box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.2);
  176. }
  177. /* 按钮样式 */
  178. .btn {
  179. padding: 8px 16px;
  180. border-radius: var(--radius);
  181. font-weight: 500;
  182. transition: all 0.3s ease;
  183. display: inline-flex;
  184. align-items: center;
  185. gap: 8px;
  186. }
  187. .btn-primary {
  188. background-color: var(--primary);
  189. color: white;
  190. }
  191. .btn-primary:hover {
  192. background-color: var(--primary-dark);
  193. }
  194. .btn-outline {
  195. border: 1px solid var(--border);
  196. color: var(--text-primary);
  197. transition: all 0.2s ease;
  198. }
  199. .btn-outline:hover {
  200. border-color: var(--primary);
  201. background-color: rgba(15, 23, 42, 0.5);
  202. }
  203. /* 全屏按钮 */
  204. .fullscreen-btn {
  205. position: absolute;
  206. top: 8px;
  207. right: 8px;
  208. background: rgba(0, 0, 0, 0.5);
  209. color: white;
  210. border: none;
  211. border-radius: 4px;
  212. padding: 4px;
  213. cursor: pointer;
  214. transition: all 0.3s ease;
  215. }
  216. .fullscreen-btn:hover {
  217. background: rgba(0, 0, 0, 0.8);
  218. }
  219. /* 设备总览卡片样式优化 */
  220. .overview-card {
  221. display: grid;
  222. grid-template-columns: repeat(3, 1fr);
  223. gap: 1.5rem;
  224. padding: 1.5rem;
  225. }
  226. .overview-stat {
  227. text-align: center;
  228. }
  229. /* 数据单位样式 */
  230. .data-unit {
  231. font-size: 0.875rem;
  232. color: var(--text-secondary);
  233. margin-left: 2px;
  234. }
  235. /* 刷新按钮动画 */
  236. .refresh-spin {
  237. animation: spin 1s linear infinite;
  238. }
  239. @keyframes spin {
  240. from {
  241. transform: rotate(0deg);
  242. }
  243. to {
  244. transform: rotate(360deg);
  245. }
  246. }
  247. /* 视频网格优化 */
  248. .video-grid {
  249. display: grid;
  250. grid-template-columns: repeat(3, minmax(0, 1fr));
  251. gap: 1rem;
  252. margin-bottom: 1rem;
  253. width: 100%;
  254. }
  255. /* 视频监控相关样式 */
  256. .video-container {
  257. padding: 1.5rem;
  258. background: var(--bg-card);
  259. border-radius: var(--radius);
  260. margin-bottom: 2rem;
  261. }
  262. .video-card {
  263. display: block;
  264. position: relative;
  265. background: var(--bg-dark);
  266. border-radius: var(--radius);
  267. overflow: hidden;
  268. border: 1px solid var(--border);
  269. transition: all 0.3s ease;
  270. }
  271. .video-card:hover {
  272. border-color: var(--primary);
  273. transform: translateY(-2px);
  274. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  275. }
  276. .video-preview {
  277. position: relative;
  278. padding-top: 75%; /* 4:3 aspect ratio */
  279. background: var(--bg-dark);
  280. width: 100%;
  281. }
  282. .video-preview > div {
  283. position: absolute;
  284. top: 0;
  285. left: 0;
  286. width: 100%;
  287. height: 100%;
  288. display: flex;
  289. align-items: center;
  290. justify-content: center;
  291. }
  292. .video-info {
  293. position: absolute;
  294. bottom: 0;
  295. left: 0;
  296. right: 0;
  297. padding: 0.75rem;
  298. background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
  299. }
  300. .video-title {
  301. color: white;
  302. font-size: 0.875rem;
  303. margin-bottom: 0.25rem;
  304. }
  305. .video-location {
  306. color: var(--text-secondary);
  307. font-size: 0.75rem;
  308. }
  309. /* 主视频样式 */
  310. .video-card.main-video {
  311. grid-column: span 1;
  312. border-width: 2px;
  313. border-color: var(--primary);
  314. }
  315. /* 16:9 宽高比容器 */
  316. .aspect-w-16.aspect-h-9 {
  317. position: relative;
  318. }
  319. /* 视频导航按钮样式 */
  320. .video-nav-btn {
  321. width: 32px;
  322. height: 32px;
  323. display: flex;
  324. align-items: center;
  325. justify-content: center;
  326. background: rgba(15, 23, 42, 0.8);
  327. border: 1px solid var(--border);
  328. border-radius: 4px;
  329. color: var(--text-secondary);
  330. transition: all 0.2s ease;
  331. }
  332. .video-nav-btn:hover {
  333. background: var(--bg-card);
  334. color: var(--text-primary);
  335. border-color: var(--primary);
  336. }
  337. .video-nav-btn:disabled {
  338. opacity: 0.5;
  339. cursor: not-allowed;
  340. }
  341. .video-nav-btn i {
  342. font-size: 16px;
  343. }
  344. /* 页码显示样式 */
  345. .page-indicator {
  346. padding: 0 12px;
  347. height: 32px;
  348. display: flex;
  349. align-items: center;
  350. background: rgba(15, 23, 42, 0.8);
  351. border: 1px solid var(--border);
  352. border-radius: 4px;
  353. color: var(--text-secondary);
  354. font-size: 14px;
  355. }
  356. /* 摄像头控制按钮 */
  357. .camera-control-btn {
  358. background: rgba(0, 0, 0, 0.5);
  359. color: var(--text-secondary);
  360. padding: 0.5rem;
  361. border-radius: var(--radius);
  362. transition: all 0.2s ease;
  363. }
  364. .camera-control-btn:hover {
  365. background: rgba(0, 0, 0, 0.7);
  366. color: var(--text-primary);
  367. }
  368. /* 云台控制按钮 */
  369. .ptz-btn {
  370. background: var(--bg-card);
  371. border: 1px solid var(--border);
  372. color: var(--text-primary);
  373. padding: 0.75rem;
  374. border-radius: var(--radius);
  375. transition: all 0.2s ease;
  376. }
  377. .ptz-btn:hover {
  378. background: var(--primary);
  379. border-color: var(--primary);
  380. }
  381. /* 预置位按钮 */
  382. .preset-btn {
  383. background: var(--bg-card);
  384. border: 1px solid var(--border);
  385. color: var(--text-primary);
  386. padding: 0.5rem;
  387. border-radius: var(--radius);
  388. font-size: 0.875rem;
  389. transition: all 0.2s ease;
  390. }
  391. .preset-btn:hover {
  392. background: var(--primary);
  393. border-color: var(--primary);
  394. }
  395. /* 全屏模式样式 */
  396. .fullscreen-mode {
  397. position: fixed;
  398. top: 0;
  399. left: 0;
  400. width: 100vw;
  401. height: 100vh;
  402. background: var(--bg-dark);
  403. padding: 1rem;
  404. z-index: 40;
  405. }
  406. .fullscreen-mode .video-grid {
  407. grid-template-columns: repeat(3, 1fr);
  408. grid-template-rows: repeat(2, 1fr);
  409. height: calc(100vh - 2rem);
  410. gap: 1rem;
  411. margin: 0;
  412. }
  413. /* 单个摄像头全屏模式 */
  414. #singleCameraFullscreen {
  415. display: none;
  416. position: fixed;
  417. top: 0;
  418. left: 0;
  419. width: 100vw;
  420. height: 100vh;
  421. background-color: var(--bg-dark);
  422. z-index: 50;
  423. }
  424. #singleCameraFullscreen.active {
  425. display: flex;
  426. }
  427. #singleCameraFullscreen .video-area {
  428. flex: 1;
  429. min-width: 0;
  430. display: flex;
  431. flex-direction: column;
  432. }
  433. #singleCameraFullscreen .control-panel {
  434. width: 280px;
  435. background: var(--bg-card);
  436. border-left: 1px solid var(--border);
  437. padding: 1.5rem;
  438. overflow-y: auto;
  439. }
  440. /* 云台控制按钮网格 */
  441. .ptz-controls {
  442. display: grid;
  443. grid-template-columns: repeat(3, 1fr);
  444. gap: 0.5rem;
  445. margin-bottom: 1.5rem;
  446. }
  447. .ptz-btn {
  448. aspect-ratio: 1;
  449. display: flex;
  450. align-items: center;
  451. justify-content: center;
  452. background: var(--bg-dark);
  453. border: 1px solid var(--border);
  454. color: var(--text-secondary);
  455. border-radius: var(--radius);
  456. font-size: 1.25rem;
  457. transition: all 0.2s ease;
  458. }
  459. .ptz-btn:hover {
  460. background: var(--primary);
  461. border-color: var(--primary);
  462. color: white;
  463. }
  464. .ptz-btn:active {
  465. transform: scale(0.95);
  466. }
  467. /* 预置位按钮网格 */
  468. .preset-grid {
  469. display: grid;
  470. grid-template-columns: repeat(2, 1fr);
  471. gap: 0.5rem;
  472. }
  473. .preset-btn {
  474. padding: 0.75rem;
  475. background: var(--bg-dark);
  476. border: 1px solid var(--border);
  477. color: var(--text-secondary);
  478. border-radius: var(--radius);
  479. transition: all 0.2s ease;
  480. text-align: center;
  481. }
  482. .preset-btn:hover {
  483. background: var(--primary);
  484. border-color: var(--primary);
  485. color: white;
  486. }
  487. /* 更新数据显示样式 */
  488. .data-update {
  489. display: flex;
  490. flex-direction: column;
  491. }
  492. .data-update .text-3xl {
  493. display: flex;
  494. align-items: baseline;
  495. }
  496. /* 设备监控区域样式 */
  497. .device-monitor-section {
  498. margin-bottom: 0; /* 移除底部边距,因为现在在卡片内部 */
  499. }
  500. /* 设备卡片网格布局 */
  501. .device-grid {
  502. display: grid;
  503. grid-template-columns: repeat(1, 1fr);
  504. gap: 1rem; /* 稍微减小间距,因为现在在卡片内部 */
  505. padding: 0.5rem; /* 添加内边距,避免卡片贴边 */
  506. min-height: 400px;
  507. }
  508. @media (min-width: 768px) {
  509. .device-grid {
  510. grid-template-columns: repeat(2, 1fr);
  511. }
  512. }
  513. @media (min-width: 1280px) {
  514. .device-grid {
  515. grid-template-columns: repeat(3, 1fr);
  516. }
  517. }
  518. @media (min-width: 1536px) {
  519. .device-grid {
  520. grid-template-columns: repeat(4, 1fr);
  521. }
  522. }
  523. /* 设备卡片基础样式 */
  524. .device-card {
  525. background-color: var(--bg-card);
  526. border: 1px solid var(--border);
  527. border-radius: var(--radius);
  528. padding: 1.5rem;
  529. transition: all 0.3s ease;
  530. height: 100%;
  531. min-height: 250px;
  532. display: flex;
  533. flex-direction: column;
  534. }
  535. .device-card:hover {
  536. transform: translateY(-2px);
  537. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
  538. }
  539. /* 告警状态卡片 */
  540. .device-card.alert {
  541. border-color: var(--danger);
  542. background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
  543. }
  544. /* 离线状态卡片 */
  545. .device-card.offline {
  546. opacity: 0.8;
  547. }
  548. /* 骨架屏动画 */
  549. @keyframes pulse {
  550. 0%, 100% {
  551. opacity: 1;
  552. }
  553. 50% {
  554. opacity: .5;
  555. }
  556. }
  557. .animate-pulse {
  558. animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  559. }
  560. /* 筛选按钮样式 */
  561. .filter-btn {
  562. padding: 0.5rem 1rem;
  563. border-radius: var(--radius);
  564. font-size: 0.875rem;
  565. color: var(--text-secondary);
  566. background: transparent;
  567. border: 1px solid var(--border);
  568. transition: all 0.2s ease;
  569. display: flex;
  570. align-items: center;
  571. gap: 0.5rem;
  572. }
  573. .filter-btn:hover {
  574. background: rgba(255, 255, 255, 0.05);
  575. border-color: var(--primary);
  576. }
  577. .filter-btn.active {
  578. background: var(--primary);
  579. border-color: var(--primary);
  580. color: white;
  581. }
  582. /* 状态点样式 */
  583. .status-dot {
  584. width: 8px;
  585. height: 8px;
  586. border-radius: 50%;
  587. display: inline-block;
  588. }
  589. .status-dot.warning {
  590. background-color: var(--warning);
  591. }
  592. .status-dot.offline {
  593. background-color: var(--danger);
  594. }
  595. /* 排序选择器样式 */
  596. .sort-select {
  597. background-color: var(--bg-card);
  598. border: 1px solid var(--border);
  599. color: var(--text-primary);
  600. padding: 0.5rem 2rem 0.5rem 1rem;
  601. border-radius: var(--radius);
  602. font-size: 0.875rem;
  603. appearance: none;
  604. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
  605. background-repeat: no-repeat;
  606. background-position: right 0.75rem center;
  607. background-size: 1rem;
  608. }
  609. .sort-select:focus {
  610. outline: none;
  611. border-color: var(--primary);
  612. }
  613. /* 分页按钮样式 */
  614. .page-btn {
  615. width: 32px;
  616. height: 32px;
  617. display: flex;
  618. align-items: center;
  619. justify-content: center;
  620. border-radius: var(--radius);
  621. color: var(--text-secondary);
  622. transition: all 0.2s ease;
  623. background: var(--bg-card);
  624. border: 1px solid var(--border);
  625. }
  626. .page-btn:hover:not(:disabled) {
  627. background: var(--primary);
  628. border-color: var(--primary);
  629. color: white;
  630. }
  631. .page-btn:disabled {
  632. opacity: 0.5;
  633. cursor: not-allowed;
  634. }
  635. /* 空状态样式 */
  636. .device-card.empty {
  637. justify-content: center;
  638. background: transparent;
  639. border: 2px dashed var(--border);
  640. }
  641. .device-card.empty:hover {
  642. transform: none;
  643. box-shadow: none;
  644. }
  645. /* 添加到现有的 style 标签中 */
  646. .time-range-btn,
  647. .indicator-btn {
  648. padding: 0.5rem 1rem;
  649. border-radius: var(--radius);
  650. font-size: 0.875rem;
  651. color: var(--text-secondary);
  652. background: transparent;
  653. border: 1px solid var(--border);
  654. transition: all 0.2s ease;
  655. }
  656. .time-range-btn:hover,
  657. .indicator-btn:hover {
  658. background: rgba(255, 255, 255, 0.05);
  659. border-color: var(--primary);
  660. }
  661. .time-range-btn.active,
  662. .indicator-btn.active {
  663. background: var(--primary);
  664. border-color: var(--primary);
  665. color: white;
  666. }
  667. /* 添加到现有的 style 标签中 */
  668. .history-data {
  669. display: none;
  670. position: absolute;
  671. top: 0;
  672. left: 0;
  673. right: 0;
  674. bottom: 0;
  675. background: var(--bg-card);
  676. z-index: 1;
  677. padding: 1rem;
  678. }
  679. .history-data.active {
  680. display: block;
  681. }
  682. .history-header {
  683. display: flex;
  684. justify-content: space-between;
  685. align-items: center;
  686. margin-bottom: 1rem;
  687. }
  688. .history-tabs {
  689. display: flex;
  690. gap: 0.5rem;
  691. margin-bottom: 1rem;
  692. }
  693. .history-tab {
  694. padding: 0.5rem 1rem;
  695. border-radius: var(--radius);
  696. font-size: 0.875rem;
  697. color: var(--text-secondary);
  698. background: transparent;
  699. border: 1px solid var(--border);
  700. transition: all 0.2s ease;
  701. }
  702. .history-tab.active {
  703. background: var(--primary);
  704. border-color: var(--primary);
  705. color: white;
  706. }
  707. .close-history {
  708. padding: 0.5rem;
  709. color: var(--text-secondary);
  710. transition: all 0.2s ease;
  711. }
  712. .close-history:hover {
  713. color: var(--text-primary);
  714. }
  715. /* 修改设备卡片样式 */
  716. .device-card {
  717. position: relative;
  718. overflow: hidden;
  719. }
  720. /* 添加或修改以下样式 */
  721. .modal-backdrop {
  722. position: fixed;
  723. top: 0;
  724. left: 0;
  725. width: 100vw;
  726. height: 100vh;
  727. background-color: rgba(15, 23, 42, 0.75);
  728. backdrop-filter: blur(4px);
  729. z-index: 50;
  730. display: none; /* 默认隐藏 */
  731. align-items: center;
  732. justify-content: center;
  733. padding: 1rem;
  734. opacity: 0;
  735. transition: opacity 0.3s ease;
  736. }
  737. .modal-backdrop.active {
  738. display: flex;
  739. opacity: 1;
  740. }
  741. .modal-content {
  742. background-color: var(--bg-dark);
  743. border-radius: var(--radius);
  744. width: 100%;
  745. max-width: 1000px;
  746. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  747. border: 1px solid var(--border);
  748. overflow: hidden;
  749. }
  750. .modal-header {
  751. display: flex;
  752. justify-content: space-between;
  753. align-items: center;
  754. padding: 1rem 1.5rem;
  755. border-bottom: 1px solid var(--border);
  756. }
  757. .modal-body {
  758. padding: 1.5rem;
  759. }
  760. .modal-close {
  761. background: transparent;
  762. border: none;
  763. color: var(--text-secondary);
  764. cursor: pointer;
  765. padding: 0.5rem;
  766. transition: color 0.2s ease;
  767. }
  768. .modal-close:hover {
  769. color: var(--text-primary);
  770. }
  771. /* 修改历史数据图表模态框的HTML结构 */
  772. .device-card {
  773. position: relative;
  774. }
  775. .history-btn {
  776. position: absolute;
  777. top: 1rem;
  778. right: 1rem;
  779. width: 32px;
  780. height: 32px;
  781. display: flex;
  782. align-items: center;
  783. justify-content: center;
  784. background: var(--bg-dark);
  785. border: 1px solid var(--border);
  786. border-radius: var(--radius);
  787. color: var(--text-secondary);
  788. transition: all 0.2s ease;
  789. z-index: 1;
  790. }
  791. .history-btn:hover {
  792. background: var(--primary);
  793. border-color: var(--primary);
  794. color: white;
  795. transform: translateY(-1px);
  796. }
  797. .history-btn svg {
  798. width: 16px;
  799. height: 16px;
  800. }
  801. /* 调整设备卡片头部布局 */
  802. .device-card .card-header {
  803. padding-right: 3rem; /* 为历史按钮留出空间 */
  804. }
  805. </style>
  806. </head>
  807. <body>
  808. <div class="page-container">
  809. <!-- 顶部控制区域 -->
  810. <div class="mb-6 grid grid-cols-12 gap-4">
  811. <!-- 农场和地块选择器 -->
  812. <div class="col-span-12 lg:col-span-8">
  813. <div class="dashboard-card p-4">
  814. <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  815. <div>
  816. <label class="block text-sm text-gray-400 mb-2">农场选择</label>
  817. <select id="farmSelector" class="w-full">
  818. <option value="all">全部农场</option>
  819. <option value="farm1">东湖智慧农场</option>
  820. <option value="farm2">西湖有机农场</option>
  821. <option value="farm3">南山生态农场</option>
  822. </select>
  823. </div>
  824. <div>
  825. <label class="block text-sm text-gray-400 mb-2">地块选择</label>
  826. <select id="areaSelector" class="w-full">
  827. <option value="all">全部地块</option>
  828. <option value="area1">东区1号地块</option>
  829. <option value="area2">东区2号地块</option>
  830. <option value="area3">西区1号地块</option>
  831. </select>
  832. </div>
  833. <div>
  834. <label class="block text-sm text-gray-400 mb-2">时间范围</label>
  835. <select id="timeRange" class="w-full">
  836. <option value="realtime">实时数据</option>
  837. <option value="1h">近1小时</option>
  838. <option value="24h">近24小时</option>
  839. </select>
  840. </div>
  841. </div>
  842. </div>
  843. </div>
  844. <!-- 刷新控制区 -->
  845. <div class="col-span-12 lg:col-span-4">
  846. <div class="dashboard-card p-4">
  847. <div class="flex items-center justify-between">
  848. <div class="flex items-center gap-4">
  849. <button id="refreshBtn" class="btn btn-primary">
  850. <i class="iconfont icon-refresh"></i>
  851. 刷新数据
  852. </button>
  853. <div class="text-sm text-gray-400">
  854. 自动刷新:
  855. <select id="autoRefresh" class="text-sm">
  856. <option value="15">15秒</option>
  857. <option value="30" selected>30秒</option>
  858. <option value="60">60秒</option>
  859. </select>
  860. </div>
  861. </div>
  862. <div class="text-sm text-gray-400">
  863. 最后更新:<span id="lastUpdate">刚刚</span>
  864. </div>
  865. </div>
  866. </div>
  867. </div>
  868. </div>
  869. <!-- 设备总览统计区 -->
  870. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7 gap-4 mb-6">
  871. <!-- 总体统计 -->
  872. <div class="dashboard-card xl:col-span-2">
  873. <div class="overview-card">
  874. <div class="overview-stat">
  875. <div class="text-2xl font-bold text-green-500">42</div>
  876. <div class="text-sm text-gray-400">在线设备</div>
  877. </div>
  878. <div class="overview-stat">
  879. <div class="text-2xl font-bold text-red-500">3</div>
  880. <div class="text-sm text-gray-400">离线设备</div>
  881. </div>
  882. <div class="overview-stat">
  883. <div class="text-2xl font-bold text-yellow-500">5</div>
  884. <div class="text-sm text-gray-400">告警设备</div>
  885. </div>
  886. </div>
  887. </div>
  888. <!-- 分类统计 -->
  889. <div class="dashboard-card p-4">
  890. <div class="text-center">
  891. <div class="text-3xl font-bold text-blue-500 mb-2">12</div>
  892. <div class="text-sm text-gray-400">摄像头</div>
  893. <div class="text-xs text-gray-500 mt-1">11在线 / 1离线</div>
  894. </div>
  895. </div>
  896. <div class="dashboard-card p-4">
  897. <div class="text-center">
  898. <div class="text-3xl font-bold text-green-500 mb-2">15</div>
  899. <div class="text-sm text-gray-400">传感器</div>
  900. <div class="text-xs text-gray-500 mt-1">14在线 / 1告警</div>
  901. </div>
  902. </div>
  903. <div class="dashboard-card p-4">
  904. <div class="text-center">
  905. <div class="text-3xl font-bold text-purple-500 mb-2">8</div>
  906. <div class="text-sm text-gray-400">气象设备</div>
  907. <div class="text-xs text-gray-500 mt-1">7在线 / 1离线</div>
  908. </div>
  909. </div>
  910. <div class="dashboard-card p-4">
  911. <div class="text-center">
  912. <div class="text-3xl font-bold text-yellow-500 mb-2">10</div>
  913. <div class="text-sm text-gray-400">控制设备</div>
  914. <div class="text-xs text-gray-500 mt-1">10在线</div>
  915. </div>
  916. </div>
  917. <!-- 今日统计 -->
  918. <div class="dashboard-card p-4">
  919. <div class="text-center">
  920. <div class="text-3xl font-bold text-red-500 mb-2">7</div>
  921. <div class="text-sm text-gray-400">今日告警</div>
  922. <div class="text-xs text-green-500 mt-1">5已处理</div>
  923. </div>
  924. </div>
  925. </div>
  926. <!-- 主要内容区域 -->
  927. <div class="flex flex-col gap-6">
  928. <!-- 视频监控区域 -->
  929. <div class="w-full">
  930. <div class="dashboard-card p-4">
  931. <div class="flex justify-between items-center mb-4">
  932. <div class="flex items-center gap-4">
  933. <h3 class="text-lg font-medium">视频监控</h3>
  934. <div class="text-sm text-gray-400">
  935. 共 12 个摄像头
  936. </div>
  937. </div>
  938. <div class="flex items-center gap-3">
  939. <div class="flex items-center gap-2">
  940. <button class="video-nav-btn" id="prevPage" title="上一页">
  941. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  942. <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
  943. </svg>
  944. </button>
  945. <div class="page-indicator">
  946. <span>1-6 / 12</span>
  947. </div>
  948. <button class="video-nav-btn" id="nextPage" title="下一页">
  949. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  950. <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
  951. </svg>
  952. </button>
  953. </div>
  954. <button class="video-nav-btn" id="gridFullscreen" title="全屏显示">
  955. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  956. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  957. </svg>
  958. </button>
  959. </div>
  960. </div>
  961. <!-- 视频卡片网格 -->
  962. <div id="videoGrid" class="video-container">
  963. <div class="video-grid">
  964. <!-- 摄像头 1-3 -->
  965. <div class="video-card">
  966. <button class="camera-fullscreen-btn" title="全屏查看">
  967. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  968. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  969. </svg>
  970. </button>
  971. <div class="video-preview">
  972. <div class="bg-slate-800">
  973. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  974. <path d="M23 7l-7 5 7 5V7z"/>
  975. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  976. </svg>
  977. </div>
  978. </div>
  979. <div class="video-info">
  980. <div class="flex justify-between items-center">
  981. <div>
  982. <div class="text-white">东区1号摄像头</div>
  983. <div class="text-sm text-gray-400">东区1号地块</div>
  984. </div>
  985. <span class="status-badge online">在线</span>
  986. </div>
  987. </div>
  988. </div>
  989. <!-- 摄像头 4 -->
  990. <div class="video-card">
  991. <button class="camera-fullscreen-btn" title="全屏查看">
  992. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  993. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  994. </svg>
  995. </button>
  996. <div class="video-preview">
  997. <div class="bg-slate-800">
  998. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  999. <path d="M23 7l-7 5 7 5V7z"/>
  1000. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1001. </svg>
  1002. </div>
  1003. </div>
  1004. <div class="video-info">
  1005. <div class="flex justify-between items-center">
  1006. <div>
  1007. <div class="text-white">东区2号摄像头</div>
  1008. <div class="text-sm text-gray-400">东区2号地块</div>
  1009. </div>
  1010. <span class="status-badge online">在线</span>
  1011. </div>
  1012. </div>
  1013. </div>
  1014. <!-- 摄像头 5 -->
  1015. <div class="video-card">
  1016. <button class="camera-fullscreen-btn" title="全屏查看">
  1017. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1018. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  1019. </svg>
  1020. </button>
  1021. <div class="video-preview">
  1022. <div class="bg-slate-800">
  1023. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1024. <path d="M23 7l-7 5 7 5V7z"/>
  1025. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1026. </svg>
  1027. </div>
  1028. </div>
  1029. <div class="video-info">
  1030. <div class="flex justify-between items-center">
  1031. <div>
  1032. <div class="text-white">东区3号摄像头</div>
  1033. <div class="text-sm text-gray-400">东区3号地块</div>
  1034. </div>
  1035. <span class="status-badge offline">离线</span>
  1036. </div>
  1037. </div>
  1038. </div>
  1039. <!-- 摄像头 6 -->
  1040. <div class="video-card">
  1041. <button class="camera-fullscreen-btn" title="全屏查看">
  1042. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1043. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  1044. </svg>
  1045. </button>
  1046. <div class="video-preview">
  1047. <div class="bg-slate-800">
  1048. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1049. <path d="M23 7l-7 5 7 5V7z"/>
  1050. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1051. </svg>
  1052. </div>
  1053. </div>
  1054. <div class="video-info">
  1055. <div class="flex justify-between items-center">
  1056. <div>
  1057. <div class="text-white">西区1号摄像头</div>
  1058. <div class="text-sm text-gray-400">西区1号地块</div>
  1059. </div>
  1060. <span class="status-badge online">在线</span>
  1061. </div>
  1062. </div>
  1063. </div>
  1064. <!-- 摄像头 7 -->
  1065. <div class="video-card">
  1066. <button class="camera-fullscreen-btn" title="全屏查看">
  1067. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1068. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  1069. </svg>
  1070. </button>
  1071. <div class="video-preview">
  1072. <div class="bg-slate-800">
  1073. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1074. <path d="M23 7l-7 5 7 5V7z"/>
  1075. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1076. </svg>
  1077. </div>
  1078. </div>
  1079. <div class="video-info">
  1080. <div class="flex justify-between items-center">
  1081. <div>
  1082. <div class="text-white">西区2号摄像头</div>
  1083. <div class="text-sm text-gray-400">西区2号地块</div>
  1084. </div>
  1085. <span class="status-badge online">在线</span>
  1086. </div>
  1087. </div>
  1088. </div>
  1089. <!-- 摄像头 8 -->
  1090. <div class="video-card">
  1091. <button class="camera-fullscreen-btn" title="全屏查看">
  1092. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1093. <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
  1094. </svg>
  1095. </button>
  1096. <div class="video-preview">
  1097. <div class="bg-slate-800">
  1098. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1099. <path d="M23 7l-7 5 7 5V7z"/>
  1100. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1101. </svg>
  1102. </div>
  1103. </div>
  1104. <div class="video-info">
  1105. <div class="flex justify-between items-center">
  1106. <div>
  1107. <div class="text-white">西区3号摄像头</div>
  1108. <div class="text-sm text-gray-400">西区3号地块</div>
  1109. </div>
  1110. <span class="status-badge online">在线</span>
  1111. </div>
  1112. </div>
  1113. </div>
  1114. </div>
  1115. </div>
  1116. </div>
  1117. </div>
  1118. <!-- 设备数据展示区域 -->
  1119. <div class="w-full">
  1120. <div class="dashboard-card p-4">
  1121. <div class="device-monitor-section">
  1122. <!-- 顶部控制栏 -->
  1123. <div class="flex justify-between items-center mb-6">
  1124. <div class="flex items-center gap-4">
  1125. <h3 class="text-lg font-medium">设备监控</h3>
  1126. <div class="flex gap-2">
  1127. <button class="filter-btn active">
  1128. 全部设备
  1129. <span class="ml-1 text-xs text-gray-400">(45)</span>
  1130. </button>
  1131. <button class="filter-btn">
  1132. <span class="status-dot warning"></span>
  1133. 告警设备
  1134. <span class="ml-1 text-xs text-gray-400">(5)</span>
  1135. </button>
  1136. <button class="filter-btn">
  1137. <span class="status-dot offline"></span>
  1138. 离线设备
  1139. <span class="ml-1 text-xs text-gray-400">(3)</span>
  1140. </button>
  1141. </div>
  1142. <div class="border-l border-slate-600 h-6"></div>
  1143. <div class="flex gap-2">
  1144. <button class="filter-btn">土壤监测</button>
  1145. <button class="filter-btn">水质监测</button>
  1146. <button class="filter-btn">气象监测</button>
  1147. </div>
  1148. </div>
  1149. <div class="flex items-center gap-4">
  1150. <select class="sort-select">
  1151. <option value="alert">告警优先</option>
  1152. <option value="time">更新时间</option>
  1153. <option value="name">设备名称</option>
  1154. </select>
  1155. <div class="flex items-center gap-2">
  1156. <button class="page-btn" disabled>
  1157. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1158. <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
  1159. </svg>
  1160. </button>
  1161. <span class="text-sm text-gray-400">1-6 / 45</span>
  1162. <button class="page-btn">
  1163. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1164. <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
  1165. </svg>
  1166. </button>
  1167. </div>
  1168. </div>
  1169. </div>
  1170. <!-- 设备卡片网格 -->
  1171. <div class="device-grid">
  1172. <!-- 第一行设备 -->
  1173. <!-- 土壤监测器卡片 -->
  1174. <div class="device-card">
  1175. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1176. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1177. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1178. </svg>
  1179. </button>
  1180. <div class="card-header">
  1181. <div class="flex items-center gap-2">
  1182. <h4 class="text-lg font-medium">土壤监测器 #1</h4>
  1183. <span class="status-badge online">在线</span>
  1184. </div>
  1185. <div class="text-sm text-gray-400 mt-1">东区1号地块</div>
  1186. </div>
  1187. <div class="grid grid-cols-2 gap-8 mb-6">
  1188. <div class="data-update">
  1189. <div class="text-sm text-gray-400 mb-1">土壤湿度</div>
  1190. <div class="text-3xl font-medium">32.5<span class="text-xl ml-1">%</span></div>
  1191. </div>
  1192. <div class="data-update">
  1193. <div class="text-sm text-gray-400 mb-1">土壤温度</div>
  1194. <div class="text-3xl font-medium">24.2<span class="text-xl ml-1">℃</span></div>
  1195. </div>
  1196. </div>
  1197. <div class="flex justify-between items-center">
  1198. <div class="text-sm text-gray-400">2分钟前更新</div>
  1199. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1200. </div>
  1201. </div>
  1202. <!-- 水质监测器卡片(告警状态) -->
  1203. <div class="device-card alert">
  1204. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1205. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1206. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1207. </svg>
  1208. </button>
  1209. <div class="card-header">
  1210. <div class="flex items-center gap-2">
  1211. <h4 class="text-lg font-medium">水质监测器 #2</h4>
  1212. <span class="status-badge warning">告警</span>
  1213. </div>
  1214. <div class="text-sm text-gray-400 mt-1">西区1号地块</div>
  1215. </div>
  1216. <div class="grid grid-cols-2 gap-8 mb-6">
  1217. <div class="data-update">
  1218. <div class="text-sm text-gray-400 mb-1">pH值</div>
  1219. <div class="text-3xl font-medium text-red-500">8.5</div>
  1220. </div>
  1221. <div class="data-update">
  1222. <div class="text-sm text-gray-400 mb-1">溶解氧</div>
  1223. <div class="text-3xl font-medium">6.2<span class="text-xl ml-1">mg/L</span></div>
  1224. </div>
  1225. </div>
  1226. <div class="flex justify-between items-center">
  1227. <div class="text-sm text-red-500">pH值超标</div>
  1228. <button class="btn btn-outline text-sm px-4 py-2">处理告警</button>
  1229. </div>
  1230. </div>
  1231. <!-- 气象监测器卡片 -->
  1232. <div class="device-card">
  1233. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1234. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1235. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1236. </svg>
  1237. </button>
  1238. <div class="card-header">
  1239. <div class="flex items-center gap-2">
  1240. <h4 class="text-lg font-medium">气象监测器 #1</h4>
  1241. <span class="status-badge online">在线</span>
  1242. </div>
  1243. <div class="text-sm text-gray-400 mt-1">东区气象站</div>
  1244. </div>
  1245. <div class="grid grid-cols-2 gap-8 mb-6">
  1246. <div class="data-update">
  1247. <div class="text-sm text-gray-400 mb-1">温度</div>
  1248. <div class="text-3xl font-medium">26.5<span class="text-xl ml-1">℃</span></div>
  1249. </div>
  1250. <div class="data-update">
  1251. <div class="text-sm text-gray-400 mb-1">湿度</div>
  1252. <div class="text-3xl font-medium">68<span class="text-xl ml-1">%</span></div>
  1253. </div>
  1254. </div>
  1255. <div class="flex justify-between items-center">
  1256. <div class="text-sm text-gray-400">1分钟前更新</div>
  1257. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1258. </div>
  1259. </div>
  1260. <!-- 离线设备卡片 -->
  1261. <div class="device-card offline">
  1262. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1263. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1264. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1265. </svg>
  1266. </button>
  1267. <div class="card-header">
  1268. <div class="flex items-center gap-2">
  1269. <h4 class="text-lg font-medium">土壤监测器 #4</h4>
  1270. <span class="status-badge offline">离线</span>
  1271. </div>
  1272. <div class="text-sm text-gray-400 mt-1">西区2号地块</div>
  1273. </div>
  1274. <div class="grid grid-cols-2 gap-8 mb-6">
  1275. <div class="data-update">
  1276. <div class="text-sm text-gray-400 mb-1">土壤湿度</div>
  1277. <div class="text-3xl font-medium text-gray-500">--<span class="text-xl ml-1">%</span></div>
  1278. </div>
  1279. <div class="data-update">
  1280. <div class="text-sm text-gray-400 mb-1">土壤温度</div>
  1281. <div class="text-3xl font-medium text-gray-500">--<span class="text-xl ml-1">℃</span></div>
  1282. </div>
  1283. </div>
  1284. <div class="flex justify-between items-center">
  1285. <div class="text-sm text-gray-500">设备离线 > 24h</div>
  1286. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1287. </div>
  1288. </div>
  1289. <!-- 第二行设备 -->
  1290. <!-- 水质监测器卡片 -->
  1291. <div class="device-card">
  1292. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1293. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1294. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1295. </svg>
  1296. </button>
  1297. <div class="card-header">
  1298. <div class="flex items-center gap-2">
  1299. <h4 class="text-lg font-medium">水质监测器 #3</h4>
  1300. <span class="status-badge online">在线</span>
  1301. </div>
  1302. <div class="text-sm text-gray-400 mt-1">东区2号地块</div>
  1303. </div>
  1304. <div class="grid grid-cols-2 gap-8 mb-6">
  1305. <div class="data-update">
  1306. <div class="text-sm text-gray-400 mb-1">pH值</div>
  1307. <div class="text-3xl font-medium">7.2</div>
  1308. </div>
  1309. <div class="data-update">
  1310. <div class="text-sm text-gray-400 mb-1">溶解氧</div>
  1311. <div class="text-3xl font-medium">5.8<span class="text-xl ml-1">mg/L</span></div>
  1312. </div>
  1313. </div>
  1314. <div class="flex justify-between items-center">
  1315. <div class="text-sm text-gray-400">5分钟前更新</div>
  1316. <div class="flex gap-2">
  1317. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1318. <button class="btn btn-outline text-sm px-4 py-2" onclick="showDeviceHistory(this.closest('.device-card'))">
  1319. <i class="iconfont icon-chart mr-1"></i>历史数据
  1320. </button>
  1321. </div>
  1322. </div>
  1323. </div>
  1324. <!-- 气象监测器卡片(告警状态) -->
  1325. <div class="device-card alert">
  1326. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1327. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1328. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1329. </svg>
  1330. </button>
  1331. <div class="card-header">
  1332. <div class="flex items-center gap-2">
  1333. <h4 class="text-lg font-medium">气象监测器 #2</h4>
  1334. <span class="status-badge warning">告警</span>
  1335. </div>
  1336. <div class="text-sm text-gray-400 mt-1">西区气象站</div>
  1337. </div>
  1338. <div class="grid grid-cols-2 gap-8 mb-6">
  1339. <div class="data-update">
  1340. <div class="text-sm text-gray-400 mb-1">温度</div>
  1341. <div class="text-3xl font-medium text-red-500">35.8<span class="text-xl ml-1">℃</span></div>
  1342. </div>
  1343. <div class="data-update">
  1344. <div class="text-sm text-gray-400 mb-1">湿度</div>
  1345. <div class="text-3xl font-medium">45<span class="text-xl ml-1">%</span></div>
  1346. </div>
  1347. </div>
  1348. <div class="flex justify-between items-center">
  1349. <div class="text-sm text-red-500">温度过高</div>
  1350. <button class="btn btn-outline text-sm px-4 py-2">处理告警</button>
  1351. </div>
  1352. </div>
  1353. <!-- 土壤监测器卡片 #5 -->
  1354. <div class="device-card">
  1355. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1356. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1357. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1358. </svg>
  1359. </button>
  1360. <div class="card-header">
  1361. <div class="flex items-center gap-2">
  1362. <h4 class="text-lg font-medium">土壤监测器 #5</h4>
  1363. <span class="status-badge online">在线</span>
  1364. </div>
  1365. <div class="text-sm text-gray-400 mt-1">南区1号地块</div>
  1366. </div>
  1367. <div class="grid grid-cols-2 gap-8 mb-6">
  1368. <div class="data-update">
  1369. <div class="text-sm text-gray-400 mb-1">土壤湿度</div>
  1370. <div class="text-3xl font-medium">28.5<span class="text-xl ml-1">%</span></div>
  1371. </div>
  1372. <div class="data-update">
  1373. <div class="text-sm text-gray-400 mb-1">土壤温度</div>
  1374. <div class="text-3xl font-medium">22.8<span class="text-xl ml-1">℃</span></div>
  1375. </div>
  1376. </div>
  1377. <div class="flex justify-between items-center">
  1378. <div class="text-sm text-gray-400">3分钟前更新</div>
  1379. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1380. </div>
  1381. </div>
  1382. <!-- 水质监测器卡片 #4 -->
  1383. <div class="device-card">
  1384. <button class="history-btn" onclick="showDeviceHistory(this.closest('.device-card'))" title="查看历史数据">
  1385. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1386. <path d="M12 20v-6M6 20V10M18 20V4"/>
  1387. </svg>
  1388. </button>
  1389. <div class="card-header">
  1390. <div class="flex items-center gap-2">
  1391. <h4 class="text-lg font-medium">水质监测器 #4</h4>
  1392. <span class="status-badge online">在线</span>
  1393. </div>
  1394. <div class="text-sm text-gray-400 mt-1">南区水质站</div>
  1395. </div>
  1396. <div class="grid grid-cols-2 gap-8 mb-6">
  1397. <div class="data-update">
  1398. <div class="text-sm text-gray-400 mb-1">pH值</div>
  1399. <div class="text-3xl font-medium">7.1</div>
  1400. </div>
  1401. <div class="data-update">
  1402. <div class="text-sm text-gray-400 mb-1">溶解氧</div>
  1403. <div class="text-3xl font-medium">6.5<span class="text-xl ml-1">mg/L</span></div>
  1404. </div>
  1405. </div>
  1406. <div class="flex justify-between items-center">
  1407. <div class="text-sm text-gray-400">1分钟前更新</div>
  1408. <button class="btn btn-outline text-sm px-4 py-2">查看详情</button>
  1409. </div>
  1410. </div>
  1411. </div>
  1412. </div>
  1413. </div>
  1414. </div>
  1415. </div>
  1416. </div>
  1417. <!-- 单个摄像头全屏模式 -->
  1418. <div id="singleCameraFullscreen">
  1419. <div class="video-area">
  1420. <div class="flex justify-between items-center p-4">
  1421. <h3 class="text-lg font-medium">东区1号摄像头</h3>
  1422. <button class="video-nav-btn" id="exitSingleFullscreen">
  1423. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  1424. <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
  1425. </svg>
  1426. </button>
  1427. </div>
  1428. <div class="flex-grow bg-slate-800 relative">
  1429. <!-- 视频画面区域 -->
  1430. <div class="absolute inset-0 flex items-center justify-center">
  1431. <svg class="w-24 h-24 text-slate-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  1432. <path d="M23 7l-7 5 7 5V7z"/>
  1433. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1434. </svg>
  1435. </div>
  1436. </div>
  1437. </div>
  1438. <div class="control-panel">
  1439. <div class="space-y-6">
  1440. <!-- 云台控制 -->
  1441. <div>
  1442. <h4 class="text-sm font-medium mb-3">云台控制</h4>
  1443. <div class="ptz-controls">
  1444. <button class="ptz-btn" title="左上">↖</button>
  1445. <button class="ptz-btn" title="向上">↑</button>
  1446. <button class="ptz-btn" title="右上">↗</button>
  1447. <button class="ptz-btn" title="向左">←</button>
  1448. <button class="ptz-btn" title="停止">●</button>
  1449. <button class="ptz-btn" title="向右">→</button>
  1450. <button class="ptz-btn" title="左下">↙</button>
  1451. <button class="ptz-btn" title="向下">↓</button>
  1452. <button class="ptz-btn" title="右下">↘</button>
  1453. </div>
  1454. </div>
  1455. <!-- 变倍控制 -->
  1456. <div>
  1457. <h4 class="text-sm font-medium mb-3">变倍控制</h4>
  1458. <div class="grid grid-cols-2 gap-3">
  1459. <button class="ptz-btn" title="放大">
  1460. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1461. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"/>
  1462. </svg>
  1463. </button>
  1464. <button class="ptz-btn" title="缩小">
  1465. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1466. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM7 10h6"/>
  1467. </svg>
  1468. </button>
  1469. </div>
  1470. </div>
  1471. <!-- 预置位 -->
  1472. <div>
  1473. <h4 class="text-sm font-medium mb-3">预置位</h4>
  1474. <div class="preset-grid">
  1475. <button class="preset-btn">位置1</button>
  1476. <button class="preset-btn">位置2</button>
  1477. <button class="preset-btn">位置3</button>
  1478. <button class="preset-btn">位置4</button>
  1479. <button class="preset-btn">位置5</button>
  1480. <button class="preset-btn">位置6</button>
  1481. </div>
  1482. </div>
  1483. <!-- 回放控制 -->
  1484. <div>
  1485. <h4 class="text-sm font-medium mb-3">回放控制</h4>
  1486. <div class="grid grid-cols-2 gap-3">
  1487. <button class="ptz-btn" title="开始回放">
  1488. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1489. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
  1490. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
  1491. </svg>
  1492. </button>
  1493. <button class="ptz-btn" title="暂停回放">
  1494. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1495. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
  1496. </svg>
  1497. </button>
  1498. </div>
  1499. </div>
  1500. <!-- 其他控制 -->
  1501. <div>
  1502. <h4 class="text-sm font-medium mb-3">其他控制</h4>
  1503. <div class="grid grid-cols-2 gap-3">
  1504. <button class="ptz-btn" title="截图">
  1505. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1506. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
  1507. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
  1508. </svg>
  1509. </button>
  1510. <button class="ptz-btn" title="录制">
  1511. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1512. <circle cx="12" cy="12" r="10" stroke-width="2"/>
  1513. <circle cx="12" cy="12" r="3" fill="currentColor"/>
  1514. </svg>
  1515. </button>
  1516. </div>
  1517. </div>
  1518. </div>
  1519. </div>
  1520. </div>
  1521. <!-- 历史数据图表模态框 -->
  1522. <div id="historyChartModal" class="modal-backdrop">
  1523. <div class="modal-content">
  1524. <!-- 模态框头部 -->
  1525. <div class="modal-header">
  1526. <div class="flex items-center gap-3">
  1527. <h3 id="modalTitle" class="text-lg font-medium"></h3>
  1528. <span id="modalStatus" class="status-badge"></span>
  1529. </div>
  1530. <button id="closeModal" class="modal-close">
  1531. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  1532. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
  1533. </svg>
  1534. </button>
  1535. </div>
  1536. <!-- 图表控制区域 -->
  1537. <div class="border-b border-slate-700 px-6 py-4">
  1538. <div class="flex items-center justify-between">
  1539. <div class="flex gap-2">
  1540. <button class="time-range-btn active" data-range="24h">近24小时</button>
  1541. <button class="time-range-btn" data-range="7d">近7天</button>
  1542. <button class="time-range-btn" data-range="30d">近30天</button>
  1543. </div>
  1544. <div class="flex gap-2" id="indicatorBtns">
  1545. <button class="indicator-btn active" data-indicator="all">所有指标</button>
  1546. </div>
  1547. </div>
  1548. </div>
  1549. <!-- 图表区域 -->
  1550. <div class="modal-body">
  1551. <div id="historyChart" class="w-full" style="height: 400px;"></div>
  1552. </div>
  1553. </div>
  1554. </div>
  1555. <script>
  1556. // 页面状态管理
  1557. const state = {
  1558. currentPage: 1,
  1559. pageSize: 10,
  1560. totalDevices: 45,
  1561. selectedFarm: 'all',
  1562. selectedArea: 'all',
  1563. timeRange: 'realtime',
  1564. deviceType: 'all',
  1565. sortOrder: 'default',
  1566. autoRefreshInterval: 30,
  1567. currentVideoIndex: 0
  1568. };
  1569. // 初始化页面
  1570. document.addEventListener('DOMContentLoaded', function() {
  1571. initializeSelectors();
  1572. initializeVideoControls();
  1573. initializeDeviceFilters();
  1574. setupAutoRefresh();
  1575. initializeDataUpdates();
  1576. });
  1577. // 初始化选择器
  1578. function initializeSelectors() {
  1579. const farmSelector = document.getElementById('farmSelector');
  1580. const areaSelector = document.getElementById('areaSelector');
  1581. const timeRange = document.getElementById('timeRange');
  1582. // 农场选择联动
  1583. farmSelector.addEventListener('change', function() {
  1584. state.selectedFarm = this.value;
  1585. updateAreaOptions(this.value);
  1586. refreshData();
  1587. });
  1588. // 地块选择
  1589. areaSelector.addEventListener('change', function() {
  1590. state.selectedArea = this.value;
  1591. refreshData();
  1592. });
  1593. // 时间范围
  1594. timeRange.addEventListener('change', function() {
  1595. state.timeRange = this.value;
  1596. refreshData();
  1597. });
  1598. }
  1599. // 初始化视频控制
  1600. function initializeVideoControls() {
  1601. const prevBtn = document.getElementById('prevPage');
  1602. const nextBtn = document.getElementById('nextPage');
  1603. const fullscreenBtn = document.getElementById('gridFullscreen');
  1604. prevBtn.addEventListener('click', () => switchVideo('prev'));
  1605. nextBtn.addEventListener('click', () => switchVideo('next'));
  1606. // 视频全屏
  1607. fullscreenBtn.addEventListener('click', function() {
  1608. const mainVideo = document.querySelector('.main-video');
  1609. if (mainVideo.requestFullscreen) {
  1610. mainVideo.requestFullscreen();
  1611. }
  1612. });
  1613. // 缩略图点击切换主视频
  1614. document.querySelectorAll('.video-grid .video-card:not(.main-video)').forEach((card, index) => {
  1615. card.addEventListener('click', () => {
  1616. state.currentVideoIndex = index;
  1617. updateMainVideo(index);
  1618. });
  1619. });
  1620. }
  1621. // 切换视频
  1622. function switchVideo(direction) {
  1623. const videos = document.querySelectorAll('.video-grid .video-card:not(.main-video)');
  1624. if (direction === 'next') {
  1625. state.currentVideoIndex = (state.currentVideoIndex + 1) % videos.length;
  1626. } else {
  1627. state.currentVideoIndex = state.currentVideoIndex === 0 ? videos.length - 1 : state.currentVideoIndex - 1;
  1628. }
  1629. updateMainVideo(state.currentVideoIndex);
  1630. }
  1631. // 更新主视频显示
  1632. function updateMainVideo(index) {
  1633. const mainVideo = document.querySelector('.main-video');
  1634. const selectedVideo = document.querySelectorAll('.video-grid .video-card:not(.main-video)')[index];
  1635. // 更新视频源和信息
  1636. mainVideo.querySelector('img').src = selectedVideo.querySelector('img').src;
  1637. mainVideo.querySelector('.text-white').textContent = `摄像头 #${index + 1}`;
  1638. // 高亮当前选中的缩略图
  1639. document.querySelectorAll('.video-grid .video-card:not(.main-video)').forEach((card, i) => {
  1640. card.classList.toggle('ring-2', i === index);
  1641. card.classList.toggle('ring-primary', i === index);
  1642. });
  1643. }
  1644. // 初始化设备筛选
  1645. function initializeDeviceFilters() {
  1646. const typeFilter = document.getElementById('deviceTypeFilter');
  1647. const sortOrder = document.getElementById('sortOrder');
  1648. typeFilter.addEventListener('change', function() {
  1649. state.deviceType = this.value;
  1650. refreshDeviceList();
  1651. });
  1652. sortOrder.addEventListener('change', function() {
  1653. state.sortOrder = this.value;
  1654. refreshDeviceList();
  1655. });
  1656. }
  1657. // 设置自动刷新
  1658. function setupAutoRefresh() {
  1659. const autoRefreshSelect = document.getElementById('autoRefresh');
  1660. let refreshInterval;
  1661. function updateRefreshInterval() {
  1662. if (refreshInterval) {
  1663. clearInterval(refreshInterval);
  1664. }
  1665. const seconds = parseInt(autoRefreshSelect.value);
  1666. state.autoRefreshInterval = seconds;
  1667. refreshInterval = setInterval(refreshData, seconds * 1000);
  1668. }
  1669. autoRefreshSelect.addEventListener('change', updateRefreshInterval);
  1670. updateRefreshInterval(); // 初始化定时器
  1671. // 手动刷新按钮
  1672. const refreshBtn = document.getElementById('refreshBtn');
  1673. const refreshIcon = refreshBtn.querySelector('.iconfont');
  1674. refreshBtn.addEventListener('click', function() {
  1675. refreshIcon.classList.add('refresh-spin');
  1676. refreshData();
  1677. setTimeout(() => {
  1678. refreshIcon.classList.remove('refresh-spin');
  1679. }, 1000);
  1680. });
  1681. }
  1682. // 初始化数据更新
  1683. function initializeDataUpdates() {
  1684. // 更新时间显示
  1685. function updateLastRefreshTime() {
  1686. const lastUpdate = document.getElementById('lastUpdate');
  1687. const now = new Date();
  1688. const diff = Math.floor((now - window.lastRefreshTime) / 1000);
  1689. if (diff < 60) {
  1690. lastUpdate.textContent = diff + '秒前';
  1691. } else if (diff < 3600) {
  1692. lastUpdate.textContent = Math.floor(diff / 60) + '分钟前';
  1693. } else {
  1694. lastUpdate.textContent = Math.floor(diff / 3600) + '小时前';
  1695. }
  1696. }
  1697. // 定时更新时间显示
  1698. window.lastRefreshTime = new Date();
  1699. setInterval(updateLastRefreshTime, 1000);
  1700. }
  1701. // 刷新数据
  1702. async function refreshData() {
  1703. // 更新最后刷新时间
  1704. window.lastRefreshTime = new Date();
  1705. // 模拟数据更新效果
  1706. const dataElements = document.querySelectorAll('.data-update');
  1707. dataElements.forEach(el => {
  1708. el.classList.add('flash');
  1709. setTimeout(() => el.classList.remove('flash'), 500);
  1710. });
  1711. // 这里应该调用实际的API获取最新数据
  1712. await Promise.all([
  1713. refreshDeviceList(),
  1714. refreshDeviceStats(),
  1715. refreshVideoFeeds()
  1716. ]);
  1717. }
  1718. // 刷新设备列表
  1719. async function refreshDeviceList() {
  1720. // 模拟API调用
  1721. console.log('Refreshing device list with filters:', {
  1722. farm: state.selectedFarm,
  1723. area: state.selectedArea,
  1724. type: state.deviceType,
  1725. sort: state.sortOrder,
  1726. page: state.currentPage
  1727. });
  1728. }
  1729. // 刷新设备统计
  1730. async function refreshDeviceStats() {
  1731. // 模拟API调用
  1732. console.log('Refreshing device statistics');
  1733. }
  1734. // 刷新视频源
  1735. async function refreshVideoFeeds() {
  1736. // 模拟API调用
  1737. console.log('Refreshing video feeds');
  1738. }
  1739. // 更新地块选项
  1740. function updateAreaOptions(farmId) {
  1741. const areaSelector = document.getElementById('areaSelector');
  1742. // 模拟API数据
  1743. const areaData = {
  1744. 'farm1': [
  1745. {id: 'area1', name: '东区1号地块'},
  1746. {id: 'area2', name: '东区2号地块'}
  1747. ],
  1748. 'farm2': [
  1749. {id: 'area3', name: '西区1号地块'},
  1750. {id: 'area4', name: '西区2号地块'}
  1751. ]
  1752. };
  1753. // 清空现有选项
  1754. areaSelector.innerHTML = '<option value="all">全部地块</option>';
  1755. // 添加新选项
  1756. if (farmId !== 'all' && areaData[farmId]) {
  1757. areaData[farmId].forEach(area => {
  1758. const option = document.createElement('option');
  1759. option.value = area.id;
  1760. option.textContent = area.name;
  1761. areaSelector.appendChild(option);
  1762. });
  1763. }
  1764. }
  1765. // 视频布局切换功能
  1766. document.addEventListener('DOMContentLoaded', function() {
  1767. const container = document.querySelector('.video-container');
  1768. const videoCards = document.querySelectorAll('.video-card');
  1769. const prevBtn = document.querySelector('.video-nav-btn[title="上一页"]');
  1770. const nextBtn = document.querySelector('.video-nav-btn[title="下一页"]');
  1771. const fullscreenBtn = document.querySelector('.video-nav-btn[title="全屏显示"]');
  1772. let currentPage = 1;
  1773. const cardsPerPage = 6;
  1774. const totalPages = Math.ceil(videoCards.length / cardsPerPage);
  1775. // 更新页码显示
  1776. function updatePagination() {
  1777. const start = (currentPage - 1) * cardsPerPage + 1;
  1778. const end = Math.min(currentPage * cardsPerPage, videoCards.length);
  1779. document.querySelector('.text-sm.text-gray-400').textContent =
  1780. `${start}-${end} / ${videoCards.length}`;
  1781. // 更新按钮状态
  1782. prevBtn.disabled = currentPage === 1;
  1783. nextBtn.disabled = currentPage === totalPages;
  1784. }
  1785. // 显示指定页的视频
  1786. function showPage(page) {
  1787. videoCards.forEach((card, index) => {
  1788. const shouldShow = index >= (page - 1) * cardsPerPage &&
  1789. index < page * cardsPerPage;
  1790. card.style.display = shouldShow ? '' : 'none';
  1791. });
  1792. updatePagination();
  1793. }
  1794. // 翻页事件
  1795. prevBtn.addEventListener('click', () => {
  1796. if (currentPage > 1) {
  1797. currentPage--;
  1798. showPage(currentPage);
  1799. }
  1800. });
  1801. nextBtn.addEventListener('click', () => {
  1802. if (currentPage < totalPages) {
  1803. currentPage++;
  1804. showPage(currentPage);
  1805. }
  1806. });
  1807. // 全屏显示
  1808. fullscreenBtn.addEventListener('click', () => {
  1809. if (container.requestFullscreen) {
  1810. container.requestFullscreen();
  1811. } else if (container.webkitRequestFullscreen) {
  1812. container.webkitRequestFullscreen();
  1813. } else if (container.msRequestFullscreen) {
  1814. container.msRequestFullscreen();
  1815. }
  1816. });
  1817. // 初始化显示
  1818. showPage(1);
  1819. });
  1820. document.addEventListener('DOMContentLoaded', function() {
  1821. const videoGrid = document.getElementById('videoGrid');
  1822. const gridFullscreenBtn = document.getElementById('gridFullscreen');
  1823. const singleFullscreenModal = document.getElementById('singleCameraFullscreen');
  1824. const exitSingleFullscreenBtn = document.getElementById('exitSingleFullscreen');
  1825. const cameraFullscreenBtns = document.querySelectorAll('.camera-fullscreen-btn');
  1826. // 显示指定页的视频
  1827. function showPage(page) {
  1828. const videoCards = document.querySelectorAll('.video-card');
  1829. const cardsPerPage = 3; // 每页显示3个
  1830. videoCards.forEach((card, index) => {
  1831. const shouldShow = index >= (page - 1) * cardsPerPage &&
  1832. index < page * cardsPerPage;
  1833. card.style.display = shouldShow ? 'block' : 'none';
  1834. });
  1835. updatePagination(page, Math.ceil(videoCards.length / cardsPerPage));
  1836. }
  1837. // 更新分页显示
  1838. function updatePagination(currentPage, totalPages) {
  1839. const cardsPerPage = 3;
  1840. const start = (currentPage - 1) * cardsPerPage + 1;
  1841. const end = Math.min(currentPage * cardsPerPage, 12);
  1842. document.querySelector('.text-sm.text-gray-400').textContent =
  1843. `${start}-${end} / 12`;
  1844. }
  1845. // 网格全屏模式
  1846. gridFullscreenBtn.addEventListener('click', () => {
  1847. if (document.fullscreenElement) {
  1848. document.exitFullscreen();
  1849. } else {
  1850. videoGrid.requestFullscreen();
  1851. }
  1852. });
  1853. // 进入全屏时添加样式
  1854. document.addEventListener('fullscreenchange', () => {
  1855. if (document.fullscreenElement === videoGrid) {
  1856. videoGrid.classList.add('fullscreen-mode');
  1857. // 全屏模式下显示6个摄像头
  1858. const videoCards = document.querySelectorAll('.video-card');
  1859. videoCards.forEach((card, index) => {
  1860. card.style.display = index < 6 ? 'block' : 'none';
  1861. });
  1862. } else {
  1863. videoGrid.classList.remove('fullscreen-mode');
  1864. // 退出全屏时恢复原始显示
  1865. showPage(1);
  1866. }
  1867. });
  1868. // 单个摄像头全屏
  1869. cameraFullscreenBtns.forEach(btn => {
  1870. btn.addEventListener('click', (e) => {
  1871. e.stopPropagation();
  1872. const card = btn.closest('.video-card');
  1873. const title = card.querySelector('.text-white').textContent;
  1874. singleFullscreenModal.querySelector('h3').textContent = title;
  1875. singleFullscreenModal.classList.add('active');
  1876. });
  1877. });
  1878. // 退出单个摄像头全屏
  1879. exitSingleFullscreenBtn.addEventListener('click', () => {
  1880. singleFullscreenModal.classList.remove('active');
  1881. });
  1882. // ESC 键退出单个摄像头全屏
  1883. document.addEventListener('keydown', (e) => {
  1884. if (e.key === 'Escape' && singleFullscreenModal.classList.contains('active')) {
  1885. singleFullscreenModal.classList.remove('active');
  1886. }
  1887. });
  1888. // 初始化显示第一页
  1889. showPage(1);
  1890. });
  1891. // 初始化图表相关功能
  1892. document.addEventListener('DOMContentLoaded', function() {
  1893. initializeChartModal();
  1894. });
  1895. // 初始化图表模态框
  1896. function initializeChartModal() {
  1897. const modal = document.getElementById('historyChartModal');
  1898. const closeBtn = document.getElementById('closeModal');
  1899. const timeRangeBtns = document.querySelectorAll('.time-range-btn');
  1900. let currentChart = null;
  1901. let currentDeviceType = '';
  1902. let currentDeviceId = '';
  1903. // 关闭模态框
  1904. function closeModal() {
  1905. modal.classList.remove('active');
  1906. setTimeout(() => {
  1907. if (currentChart) {
  1908. currentChart.dispose();
  1909. currentChart = null;
  1910. }
  1911. }, 300); // 等待过渡动画完成
  1912. }
  1913. // 初始化图表
  1914. function initializeChart() {
  1915. const chartDom = document.getElementById('historyChart');
  1916. if (currentChart) {
  1917. currentChart.dispose();
  1918. }
  1919. currentChart = echarts.init(chartDom);
  1920. }
  1921. // 更新指标按钮
  1922. function updateIndicatorButtons(deviceType) {
  1923. const container = document.getElementById('indicatorBtns');
  1924. const indicators = {
  1925. '土壤监测器': ['所有指标', '土壤湿度', '土壤温度', 'EC值'],
  1926. '水质监测器': ['所有指标', 'pH值', '溶解氧', '电导率', '浊度'],
  1927. '气象监测器': ['所有指标', '温度', '湿度', '光照', '风速', '降雨量']
  1928. }[deviceType] || ['所有指标'];
  1929. // 清空现有按钮
  1930. container.innerHTML = '';
  1931. // 添加新按钮
  1932. indicators.forEach((indicator, index) => {
  1933. const btn = document.createElement('button');
  1934. btn.className = `indicator-btn${index === 0 ? ' active' : ''}`;
  1935. btn.textContent = indicator;
  1936. btn.dataset.indicator = indicator;
  1937. btn.onclick = () => {
  1938. document.querySelectorAll('.indicator-btn').forEach(b => b.classList.remove('active'));
  1939. btn.classList.add('active');
  1940. updateChartData();
  1941. };
  1942. container.appendChild(btn);
  1943. });
  1944. }
  1945. // 更新图表数据
  1946. function updateChartData() {
  1947. const activeTimeRange = document.querySelector('.time-range-btn.active').dataset.range;
  1948. const activeIndicator = document.querySelector('.indicator-btn.active').dataset.indicator;
  1949. const data = generateMockData(activeTimeRange, currentDeviceType);
  1950. const option = {
  1951. backgroundColor: 'transparent',
  1952. tooltip: {
  1953. trigger: 'axis',
  1954. backgroundColor: 'rgba(15, 23, 42, 0.9)',
  1955. borderColor: '#334155',
  1956. textStyle: { color: '#f1f5f9' }
  1957. },
  1958. legend: {
  1959. data: getIndicatorNames(currentDeviceType, activeIndicator),
  1960. textStyle: { color: '#94a3b8' },
  1961. top: 0
  1962. },
  1963. grid: {
  1964. left: '3%',
  1965. right: '4%',
  1966. bottom: '3%',
  1967. containLabel: true,
  1968. top: 40
  1969. },
  1970. xAxis: {
  1971. type: 'category',
  1972. boundaryGap: false,
  1973. data: data.times,
  1974. axisLine: { lineStyle: { color: '#334155' } },
  1975. axisLabel: { color: '#94a3b8' }
  1976. },
  1977. yAxis: {
  1978. type: 'value',
  1979. axisLine: { lineStyle: { color: '#334155' } },
  1980. axisLabel: { color: '#94a3b8' },
  1981. splitLine: { lineStyle: { color: '#1e293b' } }
  1982. },
  1983. series: generateSeries(currentDeviceType, activeIndicator, data)
  1984. };
  1985. currentChart.setOption(option);
  1986. }
  1987. // 获取指标名称
  1988. function getIndicatorNames(deviceType, activeIndicator) {
  1989. const indicators = {
  1990. '土壤监测器': ['土壤湿度', '土壤温度', 'EC值'],
  1991. '水质监测器': ['pH值', '溶解氧', '电导率', '浊度'],
  1992. '气象监测器': ['温度', '湿度', '光照', '风速', '降雨量']
  1993. }[deviceType] || ['指标1', '指标2'];
  1994. return activeIndicator === '所有指标' ? indicators : [activeIndicator];
  1995. }
  1996. // 生成图表系列
  1997. function generateSeries(deviceType, activeIndicator, data) {
  1998. const indicators = {
  1999. '土壤监测器': {
  2000. names: ['土壤湿度', '土壤温度', 'EC值'],
  2001. units: ['%', '℃', 'ms/cm'],
  2002. colors: ['#0ea5e9', '#10b981', '#f59e0b']
  2003. },
  2004. '水质监测器': {
  2005. names: ['pH值', '溶解氧', '电导率', '浊度'],
  2006. units: ['', 'mg/L', 'ms/cm', 'NTU'],
  2007. colors: ['#0ea5e9', '#10b981', '#f59e0b', '#8b5cf6']
  2008. },
  2009. '气象监测器': {
  2010. names: ['温度', '湿度', '光照', '风速', '降雨量'],
  2011. units: ['℃', '%', 'lux', 'm/s', 'mm'],
  2012. colors: ['#0ea5e9', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
  2013. }
  2014. }[deviceType] || { names: [], units: [], colors: [] };
  2015. if (activeIndicator === '所有指标') {
  2016. return indicators.names.map((name, index) => ({
  2017. name: name,
  2018. type: 'line',
  2019. data: data[`values${index + 1}`],
  2020. smooth: true,
  2021. showSymbol: false,
  2022. lineStyle: { width: 2 },
  2023. areaStyle: { opacity: 0.1 },
  2024. itemStyle: { color: indicators.colors[index] }
  2025. }));
  2026. } else {
  2027. const index = indicators.names.indexOf(activeIndicator);
  2028. return [{
  2029. name: activeIndicator,
  2030. type: 'line',
  2031. data: data[`values${index + 1}`],
  2032. smooth: true,
  2033. showSymbol: false,
  2034. lineStyle: { width: 2 },
  2035. areaStyle: { opacity: 0.1 },
  2036. itemStyle: { color: indicators.colors[index] }
  2037. }];
  2038. }
  2039. }
  2040. // 生成模拟数据
  2041. function generateMockData(timeRange, deviceType) {
  2042. const times = [];
  2043. const values = {};
  2044. const points = timeRange === '24h' ? 24 : timeRange === '7d' ? 7 : 30;
  2045. const now = new Date();
  2046. const ranges = {
  2047. '土壤监测器': [
  2048. [20, 40], // 土壤湿度
  2049. [15, 30], // 土壤温度
  2050. [0.5, 2] // EC值
  2051. ],
  2052. '水质监测器': [
  2053. [6.5, 8.5], // pH值
  2054. [4, 8], // 溶解氧
  2055. [0.5, 2], // 电导率
  2056. [0, 10] // 浊度
  2057. ],
  2058. '气象监测器': [
  2059. [15, 35], // 温度
  2060. [40, 80], // 湿度
  2061. [0, 100000], // 光照
  2062. [0, 10], // 风速
  2063. [0, 50] // 降雨量
  2064. ]
  2065. }[deviceType] || [[0, 100]];
  2066. for (let i = points - 1; i >= 0; i--) {
  2067. const time = new Date(now - i * (timeRange === '24h' ? 3600000 : 86400000));
  2068. times.push(timeRange === '24h' ?
  2069. time.getHours() + ':00' :
  2070. (time.getMonth() + 1) + '/' + time.getDate());
  2071. ranges.forEach((range, index) => {
  2072. if (!values[`values${index + 1}`]) {
  2073. values[`values${index + 1}`] = [];
  2074. }
  2075. values[`values${index + 1}`].push(
  2076. (Math.random() * (range[1] - range[0]) + range[0]).toFixed(1)
  2077. );
  2078. });
  2079. }
  2080. return { times, ...values };
  2081. }
  2082. // 显示设备历史数据
  2083. window.showDeviceHistory = function(card) {
  2084. const deviceName = card.querySelector('h4').textContent;
  2085. const deviceType = deviceName.split(' ')[0];
  2086. const status = card.querySelector('.status-badge').cloneNode(true);
  2087. currentDeviceType = deviceType;
  2088. currentDeviceId = card.dataset.deviceId;
  2089. // 更新模态框标题和状态
  2090. document.getElementById('modalTitle').textContent = `${deviceName} - 历史数据`;
  2091. const modalStatus = document.getElementById('modalStatus');
  2092. modalStatus.className = status.className;
  2093. modalStatus.textContent = status.textContent;
  2094. // 更新指标按钮
  2095. updateIndicatorButtons(deviceType);
  2096. // 显示模态框
  2097. modal.classList.add('active');
  2098. // 等待模态框显示后再初始化图表
  2099. setTimeout(() => {
  2100. initializeChart();
  2101. updateChartData();
  2102. }, 100);
  2103. };
  2104. // 事件监听
  2105. closeBtn.addEventListener('click', closeModal);
  2106. modal.addEventListener('click', (e) => {
  2107. if (e.target === modal) {
  2108. closeModal();
  2109. }
  2110. });
  2111. timeRangeBtns.forEach(btn => {
  2112. btn.addEventListener('click', () => {
  2113. timeRangeBtns.forEach(b => b.classList.remove('active'));
  2114. btn.classList.add('active');
  2115. if (currentChart) {
  2116. updateChartData();
  2117. }
  2118. });
  2119. });
  2120. // 监听窗口大小变化
  2121. window.addEventListener('resize', () => {
  2122. if (currentChart) {
  2123. currentChart.resize();
  2124. }
  2125. });
  2126. // ESC键关闭模态框
  2127. document.addEventListener('keydown', (e) => {
  2128. if (e.key === 'Escape' && modal.classList.contains('active')) {
  2129. closeModal();
  2130. }
  2131. });
  2132. }
  2133. </script>
  2134. </body>
  2135. </html>